HOW IT'S BUILT
Platform Architecture
Three tiers — a Capital plugin suite, a neutral hub service, and Teamcenter 2506 — connected by one JSON contract. Every mechanism on this page ran live against Capital 2512 and Teamcenter 2506, July 3–5, 2026.
The three tiers
Tier 1 — the Capital plugin suite
Everything on the Capital side ships in one JAR, deployed to <CapitalHome>\plugins\XceleratorTcImport. We build it with Capital's own toolchain — the Eclipse compiler and Java 21 JRE that ship inside the install, plus Capital's own Jackson jars — so there is no JDK, no Maven, and no classloader conflict (build.ps1 -Deploy is the whole pipeline). The jar registers against Capital 2512's documented extension points: publish actions for platform, functional, and logic designs (IXPlatformArchitectureDesignAction, IXFunctionDesignAction, IXLogicAction), the Assign Structure Parent… action, the import/sync action, and Teamcenter Browser panels on all three apps — Systems Architect, Systems Modeler, and Logic Designer — via the design-tree tab and Design Inspector panel interfaces. proven live
Tier 2 — the tc-service hub
The hub is a deliberately boring piece of engineering: a zero-dependency Python stdlib service on port 8080. It has three jobs — a store (one merged head per project plus append-only revision history), an enrichment layer (the "engineers type nothing" machinery, below), and a SOA client that speaks Teamcenter's JSON gateway directly. Its REST surface is the whole integration contract: GET /health, GET /export/bundle?project=, GET /export/spine?under=&depth=, GET /export/capital-xml?project=, POST /import/bundle, plus the Systemscenter views at /map. Anything that can produce or consume the bundle — Capital, NX, a Cameo export, a seed script — plugs in without touching the others.
Tier 3 — Teamcenter 2506
Teamcenter is the configuration-managed master. The hub logs into the JSON SOA gateway at https://<teamcenter-gateway> (CSRF-token handshake, cookie session) and materializes the bundle as real, typed objects: components become Fnd0LogicalBlock items nested as Fnd0LogicalBOMLine occurrences, functions become Functionality items with directed Network_Port pins, allocations become Seg0Allocate relations, conductors become Fnd0LogicConn items, and requirements become Requirement items related via IMAN_requirement. One Logical BOM spans the functional, platform, logic, and fluid layers — that shared tree is the systems center, and Systemscenter is the layer that renders and exchanges it.
The neutral exchange bundle (v1.2)
Every tier speaks one JSON document, defined in docs/exchange-format.md. Eight optional data arrays under a required meta block; every object carries a free-form attributes map for anything without a first-class field. The schema is additive across versions — a v1.0 bundle still imports unchanged.
| Array | Carries | Teamcenter mapping |
|---|---|---|
components | LRUs, ECUs, spine nodes; parentId · provenance · effectivity · domains[] | Fnd0LogicalBlock + BOM occurrence under its resolved parent |
functions | Logical functions + hierarchy (SysML-mastered) | Functionality items with port GDEs |
allocations | Function → component decisions (Capital SA-mastered) | Seg0Allocate, revision level |
networks | Buses/carriers, protocol, baud | carrier items (Fnd0LogicConn) |
signals | E/E payload with source/target functions | Signal + Network exchange items |
messages | Frames grouping signals | E/E payload grouping |
connections | Component-to-component edges, any domain; attributes.tcKind distinguishes logic nets/wires from platform edges | Fnd0LogicConn trace-linked to both endpoints |
requirementLinks | Requirement → element traceability | Requirement + IMAN_requirement (relations only, never LBOM occurrences) |
Trimmed to essentials, a Capital publish of one electrical add looks like this:
{
"meta": {
"schemaVersion": "1.2",
"source": "capital-systems-architect",
"project": "UAV Platform",
"effectivity": "unit>=151"
},
"components": [{
"id": "ELEC-IFPCB",
"name": "Invert Fuel Pump Circuit Breaker",
"type": "ECU",
"domain": "ELECTRICAL",
"parentId": "LOG-ACFSCB",
"provenance": "elec-add",
"effectivity": "unit>=151",
"attributes": { "milStd1808": "24 50" }
}],
"functions": [{ "id": "FN-PUMPCTL", "name": "Control pump outflow" }],
"allocations": [{ "functionId": "FN-PUMPCTL", "componentId": "LOG-CGMC" }],
"connections": [{
"id": "CN-FPPWR", "domain": "ELECTRICAL",
"fromComponentId": "ELEC-FPP", "toComponentId": "FUEL-PUMP",
"attributes": { "tcKind": "power" }
}],
"requirementLinks": [{
"requirementId": "REQ-000123", "targetType": "COMPONENT",
"targetId": "LOG-STGINV", "linkType": "derives"
}]
}
provenance or effectivity collides with a native Capital attribute and makes the 2512 importer silently drop the entire cluster — empty design, no error. The contract mandates the TC_-prefixed names (TC_PARENT_ID, TC_PROVENANCE, TC_EFFECTIVITY) everywhere they ride in Capital. We learned this live; it is now a hard format rule.The hub's store
Every import lands in three places at once. First, the merged head — one bundle per project, upserted by external id, which is what /export/bundle serves and what the Teamcenter push reads. Second, an append-only revision history under store-data\revisions\<project>\: each delivery is snapshotted pristine, as sent — before any enrichment touches it — so we always have the configuration-management trail of exactly what each tool delivered. Third, a content-hash dedup: the Capital plugin delivers every publish twice for resilience (outbox file and HTTP POST), and the store hashes each delivery with SHA-256 so the second arrival is acknowledged but skips both the merge and the Teamcenter push. Dual delivery, single effect. proven live

Hub enrichment — engineers type nothing
The hub's rule is that a publishing engineer supplies exactly one fact — the structure parent of a new element — and everything else is derived or preserved. Six behaviors in store.import_bundle make that true:
| Behavior | What it guarantees |
|---|---|
| Merge-preserve | An update never erases enrichment the incoming tool didn't send: empty parentId/provenance/effectivity keep their prior values, and prior attributes survive unless explicitly overwritten. A naive re-publish cannot strip the structure — this closed a real incident where a stale plugin briefly wiped head parentage. |
| Auto-provenance | New components inherit authorship from meta.source: nx* → fuel-add, cameo|sysml → sysml-add, capital* → elec-add. Nobody types a provenance string. |
| Effectivity default | New components inherit meta.effectivity (which the Capital plugin in turn fills from a design-level TC_EFFECTIVITY property, set once per change wave by the lead). |
| Name→parent patch maps | tc-service/patch-maps/parentid-patch.<domain>.json, read fresh on every import: delivered files that carry no parentage get it filled by component name. The safety net that makes third-party files nest correctly with no plugin rebuild. |
| Stable-identity guard | Regenerating a Capital logic design mints all-new design-local UIDs; the store dedups devices by Component_Base_ID, connectors by name, and logic nets/signals by (tcKind, name) — migrating head references when an id changes. Regenerate + republish means updates, never duplicates. |
| Derived multi-domain participation | domains[] is recomputed over the merged head on every import: primary domain ∪ declared domains ∪ evidence (a power connection implies ELECTRICAL, a fluid line implies FUEL, a Software-typed allocated function implies SOFTWARE). A pump that is fuel + electrically powered + firmware-bearing stays one element with one identity, visible in all three filtered views. |

FUEL +SW +ELEC on the fuel pump — one element, three disciplines.Delivery paths — from a Ctrl+S to a nested BOM line
Right-click → Custom → Publish to Teamcenter… walks the open design into a bundle. External ids resolve in order: stamped TC_ID → Element ID → Capital UID, so re-publishing a Teamcenter-materialized design resolves the original items.
The plugin writes an atomic outbox file (logical-bom-<project>.json) and POSTs to /import/bundle. The hub's outbox watcher claims files into staging, then moves them to processed/ — or quarantines failures to failed/, never silently dropping one. SHA-256 dedup collapses the two arrivals into one import.
The store applies merge-preserve, auto-provenance, effectivity defaults, and patch maps; records the pristine revision; recomputes domains[].
push_bundle ensures rather than creates: ensure_item looks each id up live before minting anything, and ensure_structure opens the parent's BOM window, expands existing children, and adds only missing occurrences. Parents resolve first among items created this push, then live in Teamcenter — so a fuel publish nests under a spine node the SysML seed created weeks earlier. Unresolvable parents are reported in summary.errors and fall back to the root: a visible failure, never silent wrong-nesting.
The push summary (items created/existing, occurrences added, nested parents, errors) lands in the hub log and the Systemscenter activity feed. The Wildfire round trip's summary read items_created 0 / items_existing 134 / relations_existing 199 / errors [] — the zero-create proof. proven live
Two namespacing mechanisms keep a shared dev instance sane: runTag prefixes every created id per test round (and must be empty in production — cross-publish parent resolution depends on untagged spine ids), and teamcenter.legacyRunTags lets a production push adopt items minted under an older tag instead of re-minting them.

setProperties variant returns success and writes nothing — a silent no-op, probe-verified. Type-distinguishing data like the logic layer's seg0Kind (LogicDevice/LogicConnector) must therefore be stamped at creation via createObjects compound input, and seg0Kind is multi-valued (string array props). The hub bakes both rules in.Closing the loop — Teamcenter back into Capital
The reverse direction is what makes this bi-directional rather than a publish pipe. teamcenter_export.py inverts every relation the push writes: it walks the LBOM structure into components with parentage, reads GDE ports with directions, resolves allocations, carriers, and signal routing, and extracts the logic detail layer — nested devices with their pins, root connectors, and conductors with device endpoints. The extraction was semantically lossless against the Wildfire seed on the first full run: zero diffs.
capital_project_xml.py then renders that bundle as Capital project XML — the same project.dtd format the Cameo bridge writes, loadable on any Capital seat via Project Manager → File → Import, no plugin required. One generated file carries three sections: a <functiondesign> (functions, pins, directed nets), a <platformtopologydesign> (clusters, interfaces, carriers, signal routing, function allocations cross-referenced into the functional section), and — with ?logic=1 — a real <logicaldesign> whose devices carry Element ID = the Teamcenter detail item and Component_Base_ID = the source component, so a publish-back from Capital resolves the exact original items. The generator also emits the OTI section: Capital's per-project property dictionary, giving every engineer a TC_PARENT_ID Quick-Access-Panel dropdown whose legal values are derived from the Teamcenter-backed structure — never hand-typed. One deliberate exclusion: pathway/diagram graphics stay Capital-native (layout is presentation, not data), so the generator defaults to the proven importable shape.

Identity across the loop is the subtle part, because Capital re-keys all baseids when it bootstraps a project from XML — only the Element ID properties survive. So the generator runs two baseid strategies:
| Scenario | baseid strategy | What happens in Capital |
|---|---|---|
| Bootstrap (first import) | Deterministic — TCX + SHA-1 derived from the Teamcenter ids | Capital creates the project and mints its own internal baseids; the stamped Element ID properties become the durable join keys |
| Merge (every later import) | Harvested — the real Capital baseids, exported once via PM → File → Export → Select Project… and stored in config.json capitalBaseids | Import opens the Import Designs to Project dialog against the existing project: Copy As Revision adds design revision B/C/… (safe; does not refresh project OTI), Overwrite converges the design exactly to the XML |

<?xml — the generator writes UTF-8 without BOM — and the DOCTYPE's DTD path must resolve on the importing machine (the service stamps it from config; point it at <CapitalHome>\dtd\project.dtd). Both were found the hard way and are now enforced by the generator.Put together, the loop closes in both directions: Teamcenter-authored structure materializes as native, editable Capital designs; Capital-authored elements publish back and nest into the shared spine; and a full re-publish of a materialized design resolves every original object with zero creates. That closed loop — not any single tier — is the architecture. proven live