Skills
Cameo Model Build Discipline
Skill
cameo-model-build-discipline. Build a large Cameo/MagicDraw SysML model programmatically without producing a model that passes its own audit and is still wrong. Covers the decision that must be made BEFORE the first element (organize by function or by phase, and what each costs at export time), the Jython authoring traps that report success and change nothing (getOwnedElement under-enumerating, relation re-homing defeating idempotency, a batch save that throws while the audit prints clean), and the audit discipline that catches a self-graded model - three-valued outcomes, negative controls, and the meta-failures where a check that was never written reads exactly like a pass. Ships mdzip_audit.py, a zero-dependency offline checker that runs on any .mdzip with no Cameo and no licence. Use before starting any generated Cameo model, when an audit reports all-green, or for any "the build said it worked" investigation.
Distilled from a 23,000-element SysML model built programmatically over one night, graded four times by two independent sessions, and wrong in ways its own 27-of-27 audit could not see. Everything here is EXERCISED on Cameo Enterprise Architecture 2024x Refresh3 unless marked otherwise.
The single most useful sentence in this file:
A check that was never written cannot fail, and its absence reads exactly like a pass.
That model reported 27 of 27 PASS while carrying zero traceability from any of its 48 interfaces to any function, test case or hazard. Every object the predicate needed EXISTED, correctly stereotyped, in quantity: 76 test cases, 80 functions, 40 hazards, 780 relations. Only the relations binding them to the interface were missing, and no check asked. A negative control cannot catch that class either, because there is nothing to inject a fault into.
0. Run the checker. It needs nothing.
scripts/mdzip_audit.py is standard-library Python 3. No Cameo, no licence, no
running instance, no plugin, no network. It reads a saved .mdzip directly.
python mdzip_audit.py MODEL.mdzip
python mdzip_audit.py MODEL.mdzip --stereotypes
python mdzip_audit.py MODEL.mdzip --reach WdsInterface:uml:Activity
Exit status is 1 if anything FAILED or could not be answered, so it works as a build gate and an unanswerable run is not a green build.
It reports OUTCOMES, never bare numbers: PASS, FAIL, CANNOT_TELL,
VACUOUS. That is not decoration. all([]) is True, so a universal
assertion over an empty set prints as a pass while proving nothing, and a
parser that resolved nothing prints zero exactly like a model that contains
nothing.
Within minutes of first running, it found a real defect the in-model audit had
been reporting clean: 101 duplicate relations on the
(stereotype, client, supplier) triple, from two different build runs, while
the model's own dedupe_relations() printed "0 duplicates removed". The
in-model dedupe iterates a declared list of stereotype names, so it is
structurally incapable of seeing a duplicate carrying any other stereotype, and
it reports that blindness as cleanliness. Same family as everything below.
1. Decide the package organization BEFORE the first element
This is the decision you cannot cheaply undo, and it is invisible until someone tries to split the model.
Cameo's Export Packages to New Project... wizard (right-click a package, Project Usages) runs a Package Dependencies check first. It flags four kinds of cross-package coupling:
| Kind | Coupling |
|---|---|
| 1 | Dependency/Abstraction, including Refine/Satisfy/Verify/Allocate/Trace, pointing outside the package |
| 2 | Applied stereotype whose defining profile is outside the package |
| 3 | Tagged value whose defining property or enumeration is outside the package |
| 4 | Type reference (port, parameter, property) whose type is outside the package |
Severity is the tell. Organized by FUNCTION, with each function's stereotypes, signal types and requirements co-located with it, violations come back as non-blocking Warnings. Organized by PHASE or ELEMENT TYPE, with separate top-level siblings for Functional Architecture, Interfaces/ICD and Requirements, exporting a functional package produces blocking Error-level violations across all four kinds, because a function's stereotype, its signal types and the requirements it refines are almost never co-located with it.
Measured on the phase-organized model, by mdzip_audit.py:
export kind 1 cross-package relations 609 of 780 (78%)
export kind 4 cross-package type refs 783 of 1071 (73%)
homing relation owned by a package containing NEITHER end 90 of 780
Neither shape is wrong. Phase organization is what let three sessions grade that model by concern all night, independently and in parallel. Function organization is what lets it split into used projects later. Pick deliberately, write down which you picked and why, and expect the other one to be expensive.
Two honest limits, both of which belong in any report you write:
- You cannot measure severity from the file. Warning versus Error is wizard behaviour and is nowhere in the XMI. The checker measures the coupling that produces violations; it does not predict severity, and says so in its own output.
- Kind 2 does not discriminate and must not be scored. It fires at ~100% for every model, because a profile is a separate top-level package in essentially every project and is never inside the package being exported. Measured 13,012 of 13,012 on the real model. Weighting it alongside kinds 1 and 4 swamps the only two signals that separate the shapes. A predicate that selects the whole population is not a filter.
2. Authoring traps: the call succeeds and nothing happens
getOwnedElement() UNDER-ENUMERATES under Jython
for c in owner.getOwnedElement() returns a subset of these EMF
collections, with no error and no warning. Index access returns the full set.
Measured on the same tree minutes apart: 11,374 elements by Python iteration,
21,967 by index, maximum ownership depth 5, nothing truncated by depth.
def _children(owner):
out = []
col = owner.getOwnedElement()
for i in range(col.size()): # NOT `for c in col`
try:
out.append(col.get(i))
except:
pass
return out
This is the worst trap in the list because find_child() drives every
creator's idempotency check. An under-enumerating read means "no existing child
of this name" is sometimes wrong, and the creator then makes a duplicate.
An idempotency check built on an under-enumerating read actively causes the
defect it exists to prevent. The same shape appears in stereotype attribute
loops: an under-read adds a SECOND attribute of the same name, after which
read_tag() silently reads whichever it reaches first.
Setting client/supplier RE-HOMES the relationship
MagicDraw moves an Abstraction to the nearest common namespace of its two
ends when you set them. It does not stay in the package you added it to.
Consequences, both silent: name-in-package idempotency stops working and every
re-run creates another copy (measured: 1,750 Satisfy where 169 were authored,
5,019 duplicates total), and any coverage ratio scoped to the authoring package
falls over time while the model improves.
Key relation idempotency on the TRIPLE (stereotype, clientId, supplierId)
indexed over the whole tree. Never on a name inside an owner. Never scope a
relation query to a package. A headless checker that assumes relations stay
where they were authored will mis-locate them.
A batch save can throw while the audit prints perfect
Project has no getProjectDescriptor(). A build that audits 27/27 and then
fails to save loses everything, and nothing in the audit output can tell you.
from com.nomagic.magicdraw.core.project import ProjectDescriptorsFactory as PDF
pm.saveProject(PDF.getDescriptorForProject(PRJ), True)
Both ProjectsManager.saveProject overloads take a ProjectDescriptor, never
a Project. Enumerate them by reflection rather than guessing.
Gate the save on a clean build AND zero audit failures, then assert on the
file: mtime changed, byte count changed, PRJ.isDirty() False. An absent
exception is not evidence. A batch process discards unsaved edits silently.
Other measured traps
- Macros: pure ASCII only. Non-ASCII kills
execfileand the PEP-263 fix also fails under the macro engine. Check bytes immediately before every run, not once. A build-time ASCII guard caught a star character in a comment here and cost one cycle instead of a corrupted run. TREE_DIAGRAM_LAYOUTERraises a BLOCKING MODAL DIALOG on any diagram whose structure is not a tree. Not an exception, cannot be caught, waits for a human, once per diagram. It took a 77-diagram pass from 9.8 s to 91.7 s and the entire difference was clicking. On an unattended run it hangs forever. General rule: a layouter that can REFUSE a structure will ASK rather than throw. Only use layouters that degrade silently. CIRCULAR is excluded too."SysML Requirement Diagram"is not a valid diagram type; it is"Requirement Diagram". Seven diagrams were silently never created.- Requirement
Idmust be re-applied in its own session. Cameo's auto-numbering overwrites an Id set inside the session that created it. - An additive-only enumeration creator keeps BOTH names through a rename, so the model grades correctly while an auditor grepping the file correctly reports legacy strings present. Both readings are true of different things.
3. Audit discipline
Three-valued, always
PASS / FAIL / CANNOT_TELL. An uncharacterized element is unspecified,
which is not a failure. Collapsing it into FAIL reports an undescribed thing as
a broken thing. Report an outcome, never a bare number.
Every check needs a negative control, and the control can be wrong
Inject one fault per check inside an edit session, require the check to move out of PASS, then cancel the session and re-verify on the artifact. Assert on "did the verdict change, and to what", never "did anything fail": that also catches a check that stays PASS while degrading to unevaluable.
Two ways the control itself was wrong here, both of which manufactured a non-existent defect:
- It only inspected the FAIL bucket on a deliberately three-valued audit, and reported a correct check as dead. The near-miss fix would have broken a correct rule to satisfy a broken instrument.
- A partial removal cannot falsify an "at least one" predicate. The fault
emptied ONE
Verifyrelation on an element that had three, so the element still reached two and a correct check reported STAYED PASS. Strip EVERY matching relation for one chosen element.
A wrong control does not merely miss defects, it manufactures them.
Never report an absence without a positive control in the same run
Two confident zeros here were the instrument, not the model, an hour apart with a different proxy each time:
- A parser reading relationship ends as
client=attributes found ZERO relations in a model holding 780. MagicDraw uses nested<client xmi:idref=.../>elements. - The same parser reported
testcase 0because it searched the stereotype nameTestCasewhen the project definesWdsTestCase.
The control is what caught both: a run that also resolves a known-good relationship class proves the instrument works. Without it, the second zero would have been reported as independent corroboration of someone else's correct finding, which is the worst possible way to be right.
mdzip_audit.py enforces this structurally: it refuses to report a zero for a
stereotype name that matched nothing, calling it CANNOT_TELL and printing the
names that do exist.
A predicate that selects everything is not a filter
A safety check written as "every interface carrying a DAL must trace to a
hazard" selected all 48 interfaces, because 16 carried the value NonAirborne.
It asked a much stronger question than intended and failed with 25 names. The
correct set, criticality == FlightSafety, was 18 of 48: a proper subset.
Narrowing a predicate right after it fails is the move nobody challenges, so require two things before accepting it: the new set must be a proper subset (one that selects everything or nothing is the tell), and the failure must be acted on rather than defined away. Of those 18, four genuinely had no hazard link and those four were authored. Print the population the narrowed check no longer covers, so the gap does not vanish with the predicate.
Resolve a stereotype by PROFILE AND NAME, never by name alone
This one produced 101 duplicate relations in a model whose relate() was
already correctly triple-keyed for idempotency, and it took three sessions and
an outside grep to find.
Two profiles can define the same stereotype name. Trace exists in both the
SysML profile and the UML StandardProfile. A helper that does
StereotypesHelper.getStereotype(project, "Trace") with no profile argument
binds to whichever answers first, and that can differ between runs.
Everything downstream inherits the non-determinism silently:
- Idempotency breaks.
relate()keyed on(stereotype, client, supplier)compares the stereotype it just resolved against relations carrying the other profile's stereotype, finds no match, and creates a duplicate. Every re-run adds another. The triple key was right; the stereotype in it was unstable. - Coverage queries come up short. Measured on the real model:
sysml:Trace34,StandardProfile:Trace27. A traceability query filtering on one namespace sees 34 of 61 and reports the rest as absent. - A self-audit still passes, because it resolves the name the same wrong way consistently. Internally consistent, externally wrong.
Check for it directly. Every relation kind should resolve to exactly one profile:
ns = collections.defaultdict(collections.Counter)
for m in re.finditer(r"<(\w+):(\w+)\s[^>]*base_(?:Abstraction|Dependency"
r"|DirectedRelationship)=", xmi):
ns[m.group(2)][m.group(1)] += 1
mixed = [k for k, v in ns.items() if len(v) > 1] # must be empty
On the model that produced this note: Satisfy, Verify, DeriveReqt and
Refine were cleanly sysml; only Trace was split. One mixed stereotype was
enough to generate a third of the duplicates.
Members of a duplicate pair are NOT interchangeable
When you deduplicate, something decides which copy dies. Position (first, lowest id, oldest timestamp) is the obvious rule and it is wrong, because the copies differ in what REFERENCES them.
Measured while clearing those 101 duplicates: of the 101 pairs, 23 had exactly one member displayed on a diagram. Deleting by position would have stripped 23 relations off diagrams. Nothing reports it: the model still validates, the relation still exists, the diagram just quietly shows less.
Scan the presentation data for inbound references and preserve the referenced
member. The BINARY-* members of a .mdzip are plain XML
(<?xml ...?><mdOwnedViews>), so this is a standard-library scan for
xmi:idref, no Cameo required:
displayed = set()
for n in zf.namelist():
if n.startswith("BINARY-"):
displayed.update(re.findall(r"xmi:idref='([^']+)'",
zf.read(n).decode("utf-8", "ignore")))
keep = next((r for r in rids if r in displayed), rids[0])
Then assert on the diff afterwards, not on the end state: removed set == planned set exactly, zero off-plan removals, and diagram reference count unchanged. Absence is both what a correct removal looks like and what removing the wrong thing looks like.
An idempotency guard must key on VALIDITY, not presence
A re-encoding pass skipped any element that already HAD a value, which was exactly the set still holding the OLD format. The run reported success and changed nothing on the records it existed to fix. Key on "is the stored value one of the legal tokens", never on non-emptiness.
Encode verdicts as tokens in their own field
A field carrying verdict, basis and rationale together as prose cannot be
graded by anyone else. Split them: lifecycleControl a token from a closed set,
lifecycleBasis how the judgement was reached, lifecycleRationale the prose.
Then a check can assert the token parses, and an outside grader can read it.
Do not let a class rule decide an instance judgement
A class answers "what characterizes this MEDIUM". It never answers "what
lifecycle does this integration have". An optical class covered both a fibre
data link and a sapphire window: the characterization fields fit both, and their
lifecycles have nothing in common. A single class-derived rule produced wrong
answers in opposite directions on those two, which is why neither example
alone would have exposed it. Where a class rule stands in for a judgement,
record the basis (DERIVED vs INDIVIDUAL) so a reader can see which is which,
and make the derived case ANNOUNCE itself rather than sit silent.
4. Closing a traceability gap without manufacturing one
When a reachability check fails, the tempting fix is volume: author one test case per interface named "Verify IF_X" and every number goes green. That is the same counterfeit in new clothes. Quantity of correctly-typed objects is exactly what a level-3 counterfeit looks like from outside.
What worked instead, in order:
- Reuse only where it is genuinely true. An existing bonding-resistance test really is the test for a bonding interface. Check what the test DOES, never match on a similar-sounding name.
- Author real tests where nothing genuine exists, each with a real method, facility and pass criterion. Ten were written; four power rails sharing one bus-load inspection would have read as covered and told nobody whether the 5 V rail holds regulation through a transient.
- Record an honest N/A as a typed value with a rationale where an element
truly has no such role. A bonding strap carries no function; it got
functionalRoleBasis = NOT_APPLICABLEand a sentence saying why, rather than a link to an invented function. - Record a capability where it lives and reference it elsewhere. A metric that sums evidence across levels always rewards saying it twice.
4b. Splitting a monolithic model into used projects
PROVENANCE: the recipe below was EXERCISED end to end by the MSP2 session
(jpo-msp2-cameo-model) on TC item 007033 and relayed here. The classpaths and
method signatures were then VERIFIED independently against the shipped
lib/core-2024.3.0-55-260588bf.jar with javap, because a wrong classpath in a
skill is the failure mode where specificity reads as authority.
Why you would: a monolithic model can get large enough that Teamcenter Check-In
fails server-side. Measured on a 42k-element model with 33,145 elements marked
for Deep Sync: findTcContainer: !Statement too complex. Try rewriting the query to remove complexity. Deterministic, identical on retry, not transient.
Splitting shrinks the Deep Sync comparison scope per Check-In.
Split BY FUNCTION, not along your existing package boundaries. The MSP2
session's first attempt split along the source model's own Functional Architecture / Logical Architecture containers and that was wrong: those are
the (phase-shaped) organisation of the SOURCE, not real target models. The fix
was to pull common function and block content into a genuinely new Reference
Architecture package and promote the design packages to top-level siblings.
A rename that is conceptually a different thing should be a NEW object, not a renamed one. (Chris's rule, and it applies well beyond Cameo.)
Run section 1's checker first. On a phase-organized model expect the wizard to fight every functional package.
The export itself is GUI-only. Project Usages -> Export Packages to New Project... creates a new never-before-synced .mdzip; mark it for Deep Sync,
save, then Put Model pushes it to TC as its own new item. No supported headless
API covers the export write step (ModulesService, ExtractManager,
ProjectDescriptorsFactory all checked). A human has to click it. The resulting
project-usage link IS readable headlessly, straight off the raw
com.nomagic.ci.metamodel.project zip entry.
Moving elements yourself re-homes relations, and you must finish the job. The GUI prompts to auto-fix this; the Open API does not. When an Abstraction/Dependency ends up with its two ends in different packages, re-home it yourself into whichever package owns its client, or it is left stranded at the wrong level and the wizard's Package Dependencies check flags it:
target_package.getPackagedElement().add(relation) # same containment
# mechanism as the elements
Cross-project deep copy (recovering content from an older snapshot into a new split), both signatures verified present in the shipped jar:
from com.nomagic.magicdraw.copypaste import CopyPasting
from com.nomagic.magicdraw.openapi.uml import SessionManager
# copyPasteElement(Element, Element, boolean) -- static, 3-arg
CopyPasting.copyPasteElement(src_element, target_parent, True)
⚠ With two projects open, the ambient default will silently target the wrong one. This is not folklore, it is visible in the overloads:
createSession(java.lang.String) <- binds to the ACTIVE project
createSession(com.nomagic.magicdraw.core.Project, String) <- explicit, use this one
The one-argument form compiles, runs, reports success and edits whichever
project Cameo considers active, which is not necessarily the one your script
variable points at. Same for most MCP wrappers (cameo_find,
cameo_save_project). Always re-select the project by name before reading OR
saving:
prj = [p for p in Application.getInstance().getProjectsManager().getProjects()
if p.getName() == WANTED][0]
Put Model and Publish are different operations by design. Put Model always
mints a NEW TC item; Publish updates the existing checked-out one and keeps the
checkout. Confusing them costs a real orphan item, and one was created that way.
Related, and the reason this belongs in a discipline skill: a tool's own verdict
TEXT is not evidence. An ITEM MISMATCH warning was found firing on every
checkout regardless of correctness, because it regex-matched a staging path that
never exists in the real working directory. Verify with a direct
tc_get_properties read on the revision uid, plus optIsCheckedout /
checkedOutByLoggedInUser, never the wrapper's summary line.
Verifying a split offline, and why the checker was blind to one
EXERCISED 2026-08-24 on the WildfireSystem split (item 007911, the extracted
Reference Architecture). Three defects in mdzip_audit.py surfaced in one pass,
all of the same family: an incomplete view wearing the shape of a complete one.
An extracted used project stores its content in
com.nomagic.magicdraw.uml_model.shared_model, not in...uml_model.model, which holds only the Model root. The checker read only the latter, indexed 5 elements for a 48-element project, and said CANNOT_TELL. It had been blind to EVERY used project since it was written.proxy.local__PROJECT$*members stay excluded on purpose: those are snapshots of content owned by OTHER projects.A relation whose far end lives in a used project serialises that end as an
href, not anxmi:idref:
<client xmi:idref="_2024x_..."/>
<supplier href="MSP2_Reference_Architecture.mdzip#_2024x_..."/>
Reading only idref formed zero pairs on every split model, and the tool
then blamed its own parser. The href names the used project file, so a split
model can be verified offline including which project each far end lives in.
uml:InformationFlowwas missing from RELATION_TYPES, and its ends areinformationSource/informationTargetrather than client/supplier. Every ItemFlow was skipped: the tool reported 102 relations on a model holding 114 and never mentioned the other 12. Item flows are the construct that has to survive to Teamcenter, so that was the worst possible thing to drop quietly.
⇒ Zero resolved endpoints now has three distinct outcomes, because it had three distinct causes that all printed identically: VACUOUS when no relation-shaped element exists at all (reported WITH the indexed element count as the positive control proving the parser is not blind), VACUOUS naming the used project when the ends are href references, and CANNOT_TELL only for a genuine parser failure. The resolved count is now always reported against the relation-shaped total, so a shortfall cannot hide.
What a good split looks like, measured on both halves:
parent 633 elements, 7 top-level packages, 114 relations, 0% cross-boundary
child 48 elements, 0 relations (by design), VACUOUS not CANNOT_TELL
633 + 48 = 681 = the pre-split total, exactly
Check the arithmetic closes. A split that loses elements looks identical to one that does not, unless you add the two halves up.
4c. Getting a model INTO Teamcenter: what is actually proven
PROVENANCE: assembled 2026-08-24 from two sessions that each tried it independently and compared notes. Read the status line on each item before planning around it.
⚠ SUPERSEDED 2026-08-24, THE SAME DAY IT WAS WRITTEN. The login half is SOLVED. Kept in place rather than deleted, per the house grounding rule, because the diagnosis below is still correct and still explains what you will see if you reach for the wrong tool.
What is now proven, by the cameo-ops session, EXERCISED 2026-08-20 on
vm2606 and read off their tool contracts rather than paraphrased:
- Unattended login: PROVEN.
cameo_ops_loginreportsloginSuccessfultrue,isUserLoggedIntrue, dialog gone, nobody at the machine. The trick is that the dialog has a URL field the login POJO has no setter for, so the API can fill every field except the one that enables the button. It sets the others byWM_SETTEXTand then POSTS a real keystroke carrying the URL's last character, because the button is enabled by an EDIT event and not by field content. Nothing enters the OS input queue and no window is raised. ⚠ Their own caveat, worth repeating: the Login button is STICKY, so "enabled" alone proves nothing. They gate on a three-rung ladder (fresh dialog disabled, still disabled after WM_SETTEXT, enabled by the posted keystroke) precisely because the first end-to-end attempt passed while proving nothing. - Headless Check-Out: PROVEN.
cameo_ops_checkoutwithroute="import"calls the vendor's ownimportProject, verified server-side with controls. The earlier "unreachable" claim was retracted by them after reading the shipped jars withjavap:TeamcenterObjectis a plain POJO and nothing comes from a GUI browse. The real gap had been the TAIL of the operation (loadOpenUpdateProject,updateLocalMappings,changeProjectName,saveActiveProject), andupdateLocalMappingsis what writes the project options the checked-out-by-me guard reads. - Headless publish and Check-In: STILL OPEN as of 2026-08-24.
⇒ The generalisable lesson is about the shape of the original claim, not the
Cameo detail. "You cannot get logged in" was written from two identical
reproductions and was true of the route being used. It was false as a statement
about the stack, because a different route existed and had not been tried. That
is the same scope error section 3 warns about: state the claim at the scope of
what was actually tested. "cameo_tc_login times out against a headless worker"
survives scrutiny and is still true. "You cannot get logged in" did not survive
four days.
(historical, and still accurate about cameo_tc_login specifically) That
tool times out against a headless worker: the connector raises its own
interactive "Welcome to Teamcenter" dialog and blocks the AWT event thread
until the tool's 300 s timeout expires. The credential is read from the DPAPI
store correctly and never reaches the dialog. Reproduced twice, identically.
Closing the dialog externally does not rescue it, because "Welcome to
Teamcenter" IS the login dialog, so closing it cancels the login.
Which tool, when a login does exist:
cameo_tc_publishis the right one. It calls the realCameoPublishServiceImpl.publish(...), which is what BOTH GUI menu items go through, captured live.cameo_tc_pushis NOT. It callsexport(), reachable only from a separate dialog the main menu does not expose. Called against an already-bound project it minted a duplicate Teamcenter item (007320) instead of resolving to the existing one, becauseexport()has no identity resolution to fall back on.cameo_ops_pushis Put Model / Synchronize, and Synchronize on a project that was never checked out lands content as NEW ITEMS. That is how four duplicate items appeared on one tier.
RESOLVED 2026-08-24, was "unconfirmed": minting a Teamcenter item from a
never-associated project WORKS. Kept here rather than deleted, per the house
grounding rule. The original caveat was sound: every "new" model on record had
been created by the GUI Export Packages to New Project... wizard against an
already-associated source, and the wizard mints the item itself before any
publish happens, so a cold .mdzip was genuinely untested. It has now been
done: item 007782-WildfireSystem was minted from a project that had never
been associated with Teamcenter. See section 4d. Note the publish was still a
human clicking in the GUI, so nothing above about headless publishing changes.
Deep Sync does NOT cascade headlessly. cameo_deep_sync(element_ids=<pkg>)
marks exactly the ids passed and none of their contents, while the GUI's own
marking does cascade. Verified by reading isSharedElement() on a child after
a "successful" parent marking and finding zero TC stereotypes. Walk the whole
subtree and pass every id explicitly. A publish with nothing marked reports
success and lands empty containers, which on an update overwrites what was
already there: one model went from 182 occurrences to 1 after a clean,
error-free Check-In.
Do not trust the check-in verdict. cameo_ops_checkin returned ok:false
on three check-ins that genuinely succeeded. Verify with a direct
tc_get_properties read on the revision uid instead.
Keep the shared set small. A model with 4,954 Deep-Sync-marked elements
fails Check-In outright with a Teamcenter-side
findTcContainer: !Statement too complex, deterministic and reproducible in
the GUI. Two models that publish cleanly carry 532 and 119. That error is NOT
a validation problem: the same model's validation errors were taken from 82 to
about 10 and the check-in error did not change one character.
4d. The Allocate DIRECTION rule: one end aborts the entire export
STATUS: EXERCISED 2026-08-24 on tier vm2606, both the failure and the fix,
verified by reading Teamcenter back. This is not derived from a mapping file.
The rule: author «Allocate» with the BLOCK as client and the ACTIVITY as supplier. The other way round aborts the whole export with:
Export Failed: Relating an object of type "System Block Revision" to an object
of type "Function Revision" is not allowed, because the "Allocate" relation is
not supported between these two objects.
Not a warning, not a skipped relation. Nothing publishes.
Why this is easy to get backwards: client=Activity, supplier=Block is the conventional SysML reading (the function is allocated TO the component), and it is what a careful modeller writes by default. It is also what the vendor mapping file permits, so nothing upstream of Teamcenter objects to it.
The measurement, six models in one workspace:
| Model | Direction | Outcome |
|---|---|---|
| MSP2 SoC Consolidation | client=Block | published x3 |
| MSP2 Pin-Compatible Refresh | client=Block | published |
| IFFS_Exercise | client=Block | TC-shared |
| IFFS_PipelineTest | client=Activity | DELETED to get through, backup named pre-dropallocate |
| InvertedFlightFuelSystem | client=Activity | left unshared, unlike its Satisfies |
| WildfireSystem (52 relations) | client=Activity | EXPORT FAILED, then fixed by flipping |
Every Allocate that ever reached Teamcenter has the block as client. Every Activity-client one was deleted or quietly excluded. Nobody had ever pushed one, which is why this surfaced only when somebody did.
⚠ What is NOT established: the mechanism. The failing case names the
Allocate relation; the working case creates Seg0Allocate. Why the client end
changes which relation the connector requests is unknown. Do not carry a causal
story. In particular, "the connector maps supplier onto the GRM primary" is
FALSE and was briefly propagated into two sessions' notes before being caught:
the block is the GRM primary in the failed attempt AND in the successful one, so
direction is not selecting the primary at all.
A display-name trap that sends you chasing a type that does not exist.
"System Block" is the UI label of Fnd0LogicalBlock. The same object returns
ui="System Block Revision" and db="Fnd0LogicalBlockRevision". An hour was
nearly spent planning how to make blocks "land as Fnd0LogicalBlock instead of
System Block". They were never two things. Always read the db value before
treating a Teamcenter type name in an error message as a distinct type.
How to verify it landed, server side: the relation is a reference property
on the BLOCK revision, so read it from there. The Function revision does not
carry it (Seg0Allocate is absent from its property set entirely, which is
different from present-and-empty, and that difference is the signal).
tc_get_properties(uids=[<block revision uid>], attributes=["Seg0Allocate"])
-> ui: ["007772/A;1-Hold Hardwired Inhibit", "007774/A;1-Assure Safety"]
expandGRMRelationsForPrimary is NOT needed for this and cost several failed
attempts guessing struct shapes: it returned DOM parsing error on two
plausible payloads. The reference-property read is one call and needs no schema.
Census, not spot check. Reading one block proves the relation type works; it
does not prove the model landed. Extract every authored (block, function) pair
from the live model, read Seg0Allocate on every allocated block revision, and
compare as SETS in both directions. On WildfireSystem that was 52 authored and
52 in Teamcenter, zero missing and zero extra. A count match alone would not
have caught a swapped pair.
⚠ Enumeration traps hit during that census, both of which report like success:
tc_query_by_typecaps at 50 items while honestly reportingnFound: 891.max_resultsis accepted and ignored. ReadnFound, neverlen(items).tc_find_itemsmatchesobject_nameonly. A pattern like0077*aimed at the item id returns a clean zero that looks like "not published".
Also settled by the same run: a .mdzip that has NEVER been associated with
Teamcenter CAN be published, minting its item cold. Section 4c flagged this as
unconfirmed because every prior "new" model came from the Export Packages wizard
against an already-associated source. Item 007782-WildfireSystem was minted
from a cold project. The publish itself was still a human clicking in the GUI,
so the headless-publish blocker in 4c stands unchanged.
4e. A named Activity is not a function: giving it an interface that lands
STATUS: EXERCISED 2026-08-24 on tier vm2606, seven packages, 126 parameters
and 42 object flows authored, verified server-side.
The failure this fixes is invisible in every count you would normally run.
A generated model can hold 52 Activities, correctly named, correctly allocated
to their performing blocks, all 52 landing in Teamcenter as first-class
Functionality objects, and still be worthless downstream:
52 Activities
0 actions 0 activity parameter nodes
0 object flows 0 activity partitions
Name-only shells. They land, they allocate, and they have no inputs, no outputs and no connection to each other. Per the CSAInt mapping contract that means Capital receives functions with no interface, which Chris called "not practical". An audit that counts Activities, checks their Allocate direction and reads them back from Teamcenter passes this model completely.
⇒ Add a check that the functions have parameters. Counting activities is not counting functions.
The measured mapping (from tcuid stamps the publish writes back, each one
confirmed by a server-side read):
uml:Activity -> Functionality
uml:Parameter (owned by the ActivityParameterNode) -> Network_Port
uml:ObjectFlow (pin-to-pin on CallBehaviorActions) -> Network / NetworkRevision
(item-level PSConnectionRevision)
⚠ It is the PARAMETER that crosses, not the ActivityParameterNode. Measured
on one package: parameters 20/20 stamped, nodes 0/20. The node is the diagram-side
object; the parameter carries the identity. Authoring a node without its
Parameter produces nothing in Teamcenter and no error.
The interface needs BOTH halves. Parameter nodes alone give each function a
port list and no connectivity. The chain requires a parent Activity holding a
CallBehaviorAction per sub-function, with ObjectFlows between their pins:
p = EF.createParameterInstance(); act.getOwnedParameter().add(p)
p.setName(sig_name); p.setType(signal); p.setDirection(PD.IN) # or PD.OUT
n = EF.createActivityParameterNodeInstance(); act.getNode().add(n)
n.setName(sig_name); n.setType(signal); n.setParameter(p)
cba = EF.createCallBehaviorActionInstance(); parent.getNode().add(cba)
cba.setBehavior(sub_activity) # pins are created to match its parameters
f = EF.createObjectFlowInstance(); parent.getEdge().add(f)
f.setSource(out_pin); f.setTarget(in_pin)
⚠ Gate 3 decides which flows survive, and it is already documented in
cameo-tc-element-authoring: an ObjectFlow exports only when BOTH ends are pins
on a CallBehaviorAction. Measured here: 42 authored, 41 landed. The single
miss was the one flow whose target was the parent's own ActivityParameterNode
rather than a pin. That flow is still worth authoring (it is what makes the
parent function's interface real in SysML) but it produces no Teamcenter object.
★ Read that skill BEFORE authoring, not after. This session derived the pin-to-pin rule by measuring a 41-of-42 result and only then found Gate 3 stating it outright. The right answer, by the expensive route.
Verification, three independent instruments that agreed:
- Local, free, no Teamcenter: the publish writes
tcuidback onto the elements that crossed. An element with no stamp did not become a TC object. 8 of 9 ObjectFlows stamped in the pilot package, matching exactly the 8 found server-side. - Server-side by type and id range: new
Networkitems appeared with ids above the highest pre-existing item, one per landed flow, each named for its signal. - Count arithmetic per package, parameters and flows re-read from the model after authoring rather than counted in the authoring loop.
Where the connectivity actually lives, and where it does NOT. Established by the CSA Integration session, 2026-08-24, four negative routes and one positive:
seg0Source / seg0Target on a NetworkRevision ABSENT FROM THE TYPE, any casing
expandGRMRelationsForPrimary AND ForSecondary only "NetworkRevision Master"
connection occurrences in the model BOM only block connectors
connection occurrences under the Functionality 0
⇒ A Network carries no endpoints. Reconstruct the net by joining
Network_Port occurrences on their SIGNAL UID - a port occurrence under a
Functionality carries signalUids, and two ports on different Functionalities
sharing one are the two ends.
⛔ CORRECTED 2026-08-24, hours after first publishing. This section originally
read "11 distinct signals, all 11 spanning two or more different parents, zero
orphans, matching the Network items one for one." Every clause after "11 distinct
signals" was wrong. The first probe grouped pins by the parentId of each
OCCURRENCE, and one function appears under several parent occurrences, so it
over-counted distinct functions and manufactured connectivity that was not there.
Deduping functions to distinct objects first:
007912 Detect Ignition 11 nets, 8 connected, 3 single-endpoint
007914 Disseminate Alert 9 nets, 6 connected, 3 single-endpoint
The join still works and the connectivity is still recoverable. "Zero orphans" was not.
⚠ A single-endpoint net is not automatically a modelling defect, and you must
separate two causes before calling it one: a genuinely unconsumed output, or an
output whose consumer lives in a DIFFERENT mission thread. On the package above
all three are explainable from the authoring spec: two are deliberately terminal
(SensorHealth, FireRadiativePower are produced and never consumed anywhere)
and one (TowerDetection) is consumed by a function in another used project, so
it is correct and merely invisible from inside one package.
⚠ Do NOT expect a one-to-one match between Network objects and signal-joined
nets. One output pin feeding two input pins is TWO ObjectFlows in UML and
therefore two Network items with the SAME name, but ONE net with three
endpoints under the signal join. Measured: 007922-MwirFrame and
007926-MwirFrame in the same model. That is structural, not a naming accident,
and it is why matching nets to Networks BY NAME collapses the pair and leaves one
with no endpoints. There is no property on a Network referencing its signal
(checked across casings with a fake-property control), so the Network side of
that match has no durable key at all.
⚠ Join on the uid, never the name. They coincide on a clean model, which makes name-matching look sufficient; signal names are NOT unique across items in this workspace and a prior session classified the wrong item set exactly that way.
⚠ object_string is a wiring check on ONE type only. On a
Seg0ItemFlowExchanges it is derived as <source>-<target>, so non-empty proves
both ends landed. On a Network it is just id-name and proves nothing about
connectivity. A check published without its scope gets misapplied.
4f. Two durable uids can both be correct and still not join
EXERCISED 2026-08-24. Worse than a name collision, and it looks like nothing at all.
Two sessions exchanged a file keyed on a Teamcenter uid, precisely to avoid matching on names. The uids had zero intersection. Neither side was wrong:
publisher emitted Seg0IntfSpecRevision 007854/A;1-VnirFrame <- the REVISION
consumer expanded Seg0IntfSpec 007854-VnirFrame <- the ITEM
The publish stamps the REVISION uid into the model, so that is the only durable key an offline reader can emit. An occurrence expand returns the ITEM. Same signal, one level apart, and a direct join matches nothing.
⚠ The symptom is a clean empty result, which reads as "the other side sent an empty file". A name collision at least produces a wrong answer somebody can question. This produces zero rows while both sides are behaving correctly, and the natural conclusion is that the sender is broken.
⇒ Before joining two populations on a uid, resolve one specimen from each side
and print its TYPE. Seg0IntfSpecRevision next to Seg0IntfSpec settles it in
one call. Do this BEFORE building the join, not after it returns nothing.
⇒ Put the translation in the middle, not in either producer. The emitter should send what it can prove it has; the consumer should send what it can observe; whoever owns the interface resolves revision to item.
⇒ Both sides need a loud failure. The consumer reports whether the join
actually used the uid (joinedByUid) so a silent fallback to name matching is
visible. The emitter exits non-zero rather than write a record with a missing
key. One-sided detection lets the other side ship the bug.
Corollary for any cross-session data file: announce staleness, do not wait to be asked. The same manifest was consumed one revision behind because six new functions had been authored after it was sent. The consumer had no way to detect that, and its counts were quietly wrong rather than visibly wrong.
5. Checklist before calling a generated model done
- Package organization chosen deliberately and written down
-
mdzip_audit.pyrun on the SAVED file, zero FAIL and zero CANNOT_TELL - Save verified by mtime + byte delta +
isDirty() == False, not by absence of an exception - Every creator uses index access, never Python iteration over EMF collections
- Relation idempotency keyed on the triple, and a duplicate sweep run over the WHOLE tree rather than a declared stereotype list
- Negative control exists, fires on every check, and strips ALL matching links for "at least one" predicates
- Asked, explicitly: what does my audit NOT ask?
- Every reported absence has a positive control in the same run
- Every narrowed predicate is a proper subset, with the excluded population printed
See also: cameo-headless-automation (running any of this without the GUI),
cameo-tc-element-authoring (making it land in Teamcenter),
diagnose-silent-failure (the general method these are all instances of).
Generated from skills/cameo-model-build-discipline/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.