TeamcenterKnowledge

Skills

Diagnose Silent Failure

Skill diagnose-silent-failure. Diagnose a call that reports success and does nothing, or a reading that looks like a fact about the world and is an artefact of how it was read. Gives the capture-first procedure, the five shapes a wrong reading takes, and the control discipline that kills a hypothesis in one run instead of six. Use when an API returns ok but the effect is absent, when a flag reads false and you are about to explain why, when a count looks wrong, or any time you are about to write "the tool does not support X" - and especially when you have already tried two explanations.

Recorded 2026-08-13 after a session that spent roughly two hours and six hypotheses on a Cameo / Teamcenter Check-Out, and then solved it in ten minutes with one capture. Every hypothesis was plausible. Every one was argued from a partial reading. The capture was available after the first failure.

This skill is about method, so it applies to any vendor API, not only Teamcenter.

The rule

When an operation reports success and the effect is missing, go and watch the working path before you explain the broken one.

If a human can do it in a UI, the call exists and can be recorded. Recording it is almost always cheaper than the second hypothesis, and always cheaper than the fourth.

⚠ The trap is that theorising feels like progress and capture feels like setup. Measured on the run above: six hypotheses, about two hours, all wrong. One capture, ten minutes, complete answer.

Do this, in this order

  1. Assert on the artefact, not the return value. Re-read the object, the file bytes, the row, the pixels. "It returned ok" is not evidence. This is what turns an invisible failure into a visible one, and until you have it you are debugging nothing.

  2. Find the second reading that should differ. One reading is an opinion. A wrong reading and a right one, side by side, is a measurement. Concretely: the same probe against a known-good case, the same field before and after, the working model next to the broken one.

  3. Check the instrument before believing it. Put something in the probe whose answer you already know. If a search for seven things returns zero for all seven, and one of them definitely exists, the probe is broken - not the world.

  4. Capture the working path. Attach a recorder, intercept the traffic, read the vendor's own bytecode, diff a known-good export. Record a human doing the thing that works.

  5. Diff, and read the ORDER. The missing call is usually before the one you were studying, not inside it.

  6. Only now form a hypothesis, and make it one a single run can kill.

The five shapes of a wrong reading

All five came out of one session. They are the same mistake wearing different clothes: a value asked for by the wrong name, or read with the wrong meaning, then reported as a fact about the world.

# shape what it looked like what it was
1 wrong key probe returned 0 matches for all 7 elements result key was matches, code read elements
2 wrong quantity "counts do not match, build failed" log gave absolute totals, expectation was deltas
3 wrong shape list argument recorded as null collections serialise as size/elements, code read value
4 wrong accessor fileName empty, "session is unbound" wrapper returned empty; Project.getFileName() had the path
5 name-inferred meaning isCheckOut:false = "checkout failed" it is true on CANCEL and false on CHECKOUT

Never infer a field's meaning from its name. Establish it by finding a case where it must be true and a case where it must be false. Shape 5 produced the most confident wrong statement of the session, and it was one comparison away from being caught.

⇒ Shapes 1-4 were all caught by a control. Include one in every probe as a matter of course: a term that must hit, an object that must exist, a query that must return zero.

Shape 6, the worst of all: an under-enumerating read

Added 2026-08-13, measured by the Wildfire Detection session. In Jython, for c in element.getOwnedElement() returns a SUBSET, with no error and no truncation warning. Index access via size() / get(i) returns the full set.

for-loop traversal   11,374 elements
size()/get(i)        21,967 elements     same tree, same depth 5

This is the one that causes damage rather than merely hiding it. A tree walk drives find_child(), find_child() drives idempotency, and an under-enumerating read therefore manufactures the duplicates it exists to prevent. It is a wrong reading with a write attached.

⇒ Never trust a language-level iterator over a foreign collection until you have compared it against an index walk on the same object. One line, and it is the difference between a correct engine and one that quietly doubles its output every run.

A WRONG CONTROL MANUFACTURES DEFECTS

The control discipline has its own failure mode, and it is worse than having no control.

Same session, same day: a negative control reported a correct three-valued check as dead, because the control only inspected the FAIL bucket. The fix it implied would have broken a correct rule to satisfy a broken instrument.

⇒ Assert on "did the verdict change, and to what", never on "did anything fail". A control that watches one outcome bucket cannot see a checker that is working and returning a different bucket.

A responsive health check does NOT rule out a blocking modal

Wall-clock time far above the norm is the real signal. Measured: a 77-diagram layout pass went from 9.8 s to 91.7 s, and the entire difference was a human clicking a dialog.

Be precise about what your probe touched, because two things get conflated:

  • a ping handled on a worker thread proves only that the process is alive. It says nothing about the UI thread and must never be read as "no dialog is blocking".
  • a call marshalled onto the UI thread that returns does prove that thread is servicing tasks. (Swing modal dialogs spin a nested event loop rather than freezing the EDT, which is why the dialog stays clickable - so this can legitimately return while a modal is up.)

Both were mistaken for each other in one evening, in both directions: a single-threaded HTTP executor made a whole tool unanswerable and that was misread as "the event thread is wedged", and a liveness ping was read as evidence no dialog was up.

Silent success is a category, not an accident

Learn to recognise these on sight. Each returns something that looks like the healthy case:

  • Cap or truncation that reports the truncated result as complete. A spec of 426 targets arrived as 11 and the run printed ATTACH_OK. Anything appended last to a length-limited string is deleted first, which in that case was the redaction directive.
  • Partial call that stages without committing. checkout() returned a staging path, an empty error message and isErrorExists:false, and checked nothing out - because the real operation was three calls and it made the middle one.
  • Scope mismatch where the answer describes something narrower than the question. A flag read against the active project while the relevant one was a second open project of the same name.
  • Broken checker reporting a pass. A crash where a verdict belongs, an exit code that cannot distinguish crash from clean, all([]) returning True.
  • A success LINE next to a zero COUNT. tcxml_import printed "The import operation has completed successfully", exited 0, and its own summary table read New 0 | Update 0 | Skip 0 | Fail 0 | Total 0. It had created nothing. The per-element reason was in the log the whole time ("Xml attribute [object_name = ...] ignored as not found in database"). ⇒ When a tool prints both a verdict and a count, the count is the verdict. Read the table.

⇒ After writing any cap, guard, or check: ask what the caller sees when it fires. If it is what they see on success, that is the defect, whether or not it has fired yet.

The instrument that is unique by construction

A comparison can only detect a change if the thing compared is stable when nothing changes. Some artefacts are not.

Verifying that a stock Teamcenter transfer mode had NOT been damaged, by exporting it before and after and comparing file hashes, reported UNCHANGED: False. Nothing had been touched. Every TC XML export embeds <Header ... date="..." time="...">, so two exports of an identical object always differ - here by a 25-second stamp. Comparing the field that mattered, the clauses attribute, showed them byte-identical at 5,712 characters.

⚠ This is the false-alarm twin of everything else in this file, and it is just as expensive: it very nearly produced a report that live data had been damaged when it had not. Same family as a wrong control manufacturing defects, above.

Before trusting a diff, ask what varies in it that you do not care about. Timestamps, generation stamps, ordering, uids minted per call. Diff the field you are actually asserting on, not the container it arrived in.

An empty CONTAINER reads exactly like an empty SUBJECT

An expand, a walk or a traversal that finds nothing looks the same whether the subject is empty or the container it is walked through was never built. Both return a well-formed, non-faulting, nearly empty result.

Measured 2026-08-21 on vm2606. Item 007033 was checked in from Cameo and reported success. Its occurrence tree expanded to exactly one node, the root, so the forward transform emitted a Capital design with 0 components, 0 functions and 0 allocations, cleanly and without error. Read as "this model is empty", that is completely wrong: the tier holds 113 MSP2 objects, and one block revision alone returns two live Seg0Allocate relations.

The item has no BOM view revision. The check-in created the objects and attached the .mdzip dataset; it never built a product structure. With no structure there is nothing to walk, and the walk says so in the only vocabulary it has.

The discriminator is a property of the CONTAINER, and it is one call. Read structure_revisions on the item revision, against a known-good control in the same session:

structure_revisions occurrences
007146 (control, known good) 007146/A-View 199
007033/A, 007033/B, 007030/A, 007103/A empty 1

getProperties on a created object CANNOT establish structure membership, and this is the trap that makes the whole thing persuasive. The session that did the check-in verified its work by reading 007256 and 007257 back off the server, found them present, and correctly concluded the check-in had worked. Both facts are true at once: the objects exist AND they are not in any structure. Object existence and structure membership are different questions, and only one of them was asked. Same family as "distinguish the layers": authoring an object is not assembling it into anything.

⇒ Generalises past Teamcenter. Any read of the form "walk X and report what is inside" needs the existence of X's container established separately, or its emptiness is uninterpretable. A BOM with no view, a folder that was never created, a saved query bound to nothing, a namespace with no members.

The recovery, when the container is missing but the objects are not. A Cameo .mdzip records the Teamcenter uid the connector stamped into every element it landed, so the model file itself carries the object set the structure would have given you. tools/cameo/mdzip_tc_stamps.py and bin/tc-read-exchange-stamped.mjs in Capital_TC_Integration do this: 33 of 33 uids resolved and type-checked live, 20 relations read off the block revisions. Two cautions carried in those tools: a stereotype application binds through base_Class for a Block and base_Element for most others, so matching one attribute finds 53 of 86 and reads as though the rest were never stamped; and this route is sound about what it contains and cannot prove completeness, because an object Teamcenter holds that the model never stamped is invisible to it. Say which route produced an artifact.

The worst one: a call that succeeds at something ELSE

Harder than a no-op, because the call really did work and the artefact really did change - just not where you look for it afterwards.

Worked example, measured by the Wildfire Detection session on 2026-08-13 in MagicDraw/Cameo: setting an Abstraction's client and supplier makes the tool RE-HOME the relationship to the common namespace of its two ends. The standard idempotency check - look for an existing relation by name inside the owner package - therefore stops finding it, because the object has moved. Every re-run creates another copy.

1,750 Satisfy    where   169 were authored
2,020 DeriveReqt where   152 were authored
1,000 Allocate   where   107 were authored
      = 5,019 duplicates after ~10 runs of an engine that was correctly idempotent by its own lights

Every build log reported the correct small numbers throughout, because it counted CREATES rather than the resulting population. Nothing in any run looked wrong.

⇒ Two rules fall out of it, and both generalise well past Cameo:

  • Count the POPULATION, not your own writes. A build log that reports what it did is not a measurement of what exists. Re-read the whole set and count.
  • Never key an idempotency probe by container. Key it by identity - here (stereotype, clientID, supplierID) over the whole tree - because containment is a property the tool may change underneath you. Then verify the dedupe by REBUILDING the index and re-counting, rather than by trusting the delete calls.

Silent no-op by precondition

A close cousin: the call is correct, the arguments are correct, and a state precondition nobody documented makes it do nothing. Same session, same day: Layouting.layout(...) requires the diagram to be OPEN and the edit session CLOSED. Run it inside a session and every shape stays at the origin, with no exception and no message.

⇒ When a UI-adjacent API does nothing, suspect an ambient-state precondition (open/closed, active, focused, session, transaction) before suspecting the arguments.

⚠ One demonstrated mechanism is not evidence of only one

The hardest failure in this file, because everything about it looks like success.

Worked example, Wildfire Detection session, 2026-08-13. Duplicate relations were traced to re-homing: setting an Abstraction's client/supplier moves it to the common namespace of its ends, so a package-scoped idempotency check stops finding it. That explanation was real, well evidenced, shipped to another session as complete - and it was the second-order cause. Hours later an under-enumerating read turned up (see Shape 6) and it was the first-order one.

The partial fix genuinely reduced the duplicate count, which is exactly what made it look finished. A number moving in the right direction is not proof the mechanism is fully understood.

⚠ What actually surfaced the real cause was an auditor refusing to accept an 86-vs-85 count as an off-by-one. A discrepancy of one is the easiest thing in the world to wave through, and it was the thread that unravelled the whole thing.

⇒ After a fix, re-measure the POPULATION and require it to hit the expected number exactly. "Far fewer" is not "none". Treat a residual of one the same way you would treat a residual of a thousand.

The four that generalise past any one tool

A lazily-loaded container reports what it has MATERIALISED, not what it HOLDS. getPresentationElements() returns zero on a diagram that was never loaded, which is indistinguishable from an empty diagram at the call site. Cost: a wrong "the layouter produced nothing" conclusion, and an investigation aimed at the layouter instead of the reader. ⇒ Force loading before believing a zero. Same family as Shape 6, minus the write.

The framework's own automation overwrites your write, inside the same session. Cameo's requirement auto-numbering silently replaced an Id: SMK-001 written, 2 read back. No failure, nothing logged, 126 requirements affected, build log green throughout. Re-applying it in a SEPARATE later session stuck. ⇒ When a tool has an opinion about a field, your write and its write are racing inside the transaction and last-writer-wins is documented nowhere. Verify a field in a different session from the one that set it.

A mid-stream check can be true when taken and false at the end. Seven ItemFlows verified complete at write time, all references present, asserted immediately - and finished the run with an empty realizingConnector, because a LATER wire changed a port's conjugation and the reference was dropped. ⇒ Verifying at the moment of writing proves the write, not the end state. Where later operations can disturb earlier ones, the fix is not a better mid-stream check, it is a different check run later: a final re-assert pass over the finished artefact.

A factory can return a structurally incomplete object. createAssociationInstance() creates an Association with no member ends: present in the tree, carrying no types, drawing no path - which reads as a diagram problem rather than as an association that was never really made. ⇒ A create* returning a valid-looking object is not a guarantee it is usable. Check the parts the thing needs in order to mean anything, not just that it exists.

A shared single name silently resolving to the wrong instance

Two defects found on 2026-08-13, in the same codebase, hours apart. They are the same bug at two layers, and they belong together because seeing one should make you look for the other.

layer the shared name what it silently selected
inside a process getActiveProject() the wrong PROJECT, when two are open
across processes one handshake file at a fixed path the wrong PROCESS entirely

Neither errors. Neither logs. Both return a plausible object and let the caller proceed.

The cross-process one had two halves, and the quieter half was the worse:

  • revoke - teardown deleted the shared handshake unconditionally, orphaning a still-running instance that held two open models. Produces an obvious absence. Recoverable.
  • steal - startup OVERWROTE the shared handshake unconditionally, so for ~40 seconds every client resolving it reached a different instance than it asked for and evaluated scripts there. Produces confident wrong work. Not recoverable, and not visible from either side - the victim instance kept working perfectly throughout, which is precisely why nobody could see it.

Any fixed name shared by multiple instances is a defect waiting for a second instance. Give each instance its own identity, make the shared name a CLAIM (written only when free or already yours, released only by its owner), and on release hand the name to a survivor rather than blanking it.

Test the survivor case explicitly. "Last one out leaves the others dark" cannot reproduce on a developer machine running a single instance, which is where it will be tested.

Verify a name-resolution fix against the OPERATING SYSTEM, not against itself

handshake_exists: true plus a working ping is exactly the pair that can both be true while the file is wrong - a cached client connection touches neither. The check that settled it:

Get-NetTCPConnection -LocalPort 18811   ->  LISTENING, owned by pid 15188
processes present                       ->  15188 only

Port and pid in the file agreeing with what the OS says is listening is the artefact. And note the near-miss: 18811 is also the default worker port, so "the file says 18811" was consistent with either instance and could not have discriminated on its own. A value that both hypotheses predict is not evidence.

Reaching for the label instead of the thing

Three instances in one two-session collaboration, 2026-08-20/21. None was carelessness; all three were competent people reaching for a NAME that stands next to the fact instead of the fact.

the label reached for the thing what it would have cost
"wrong token is 401" the server returns 403 a stub right for the wrong reason, unable to catch a caller that distinguishes them
"guard on bridgeVersion" VERSION had never been bumped - identical on every build the guard would have passed on exactly the build it existed to reject
"compare the built and installed file timestamps" ask the RUNNING process what it loaded answers a question about the file system, not about the instance in front of you

⇒ The third is the general form: a label describes an artefact at rest; the question is almost always about the thing that is running. For a JVM that is one line, and it beats every proxy:

Class.forName("com.example.Thing").getProtectionDomain().getCodeSource().getLocation()

⇒ And the second is the sharpest warning: a version string is only evidence if something bumps it. Check that it has ever changed before building anything on it. git log -S on the constant is the whole test.

Verify a peer's claim before acting on it, especially a good peer's

All three surfaced the same way: each session checked the other's claim against the code instead of accepting a plausible statement from someone demonstrably competent. Accepting is cheaper every single time, and the better the peer, the stronger the pull.

⚠ The asymmetry that makes this worth a rule: a wrong claim from a trusted peer arrives already endorsed, so it skips the scrutiny an unfamiliar claim gets. Two of these three were about to be built ON, not merely believed.

⇒ Verify, then say plainly that you verified and what you found. "I checked X before changing anything on the strength of it" is worth writing out: it tells the next reader which claims in the record were tested and which were taken on trust.

The real argument is sharper than "peers are unreliable", and it is the reverse. All three examples above were caught by a peer, never by their author - and in each case the author had every means to check and no reason to. A claim you would have made yourself is the one you will not think to test. That is why the check has to come from someone else, and why it is worth spending a peer's attention on something you are confident about.

An acceptance criterion can be invalidated by the work it governs

The failure mode with no obvious name, and the author is structurally the last person able to see it.

Worked example, 2026-08-13 to 08-21. A roadmap item for headless Check-Out was filed with a DONE MEANS naming a specific instrument:

"a Check-Out is verified BY EFFECT - isProjectCheckedOutByLoggedInUser true on a re-read - followed by a Check-In that releases it."

Correct when written. The investigation it governed then discovered that that very flag goes stale, and nobody re-read the criterion against the finding. The item was days from being closed against an instrument the work itself had disproved - and it would have read as completely clean, because every individual claim in it was true.

When an investigation invalidates an instrument, the acceptance criteria that name it are downstream of the finding and nobody will tell you. Grep for the instrument by name across the tracker, the tests and the docs the moment you retire it. The criterion is a checker like any other, and this is the cap rule applied to the definition of done.

⇒ Two roles make this catchable, and both are refusals rather than actions:

  • Refuse to close on evidence you did not witness. Filing "shipped" citing someone else's run launders their claim into the permanent register under your name. The reviewer in this case declined on exactly that ground.
  • Refuse a credit you cannot account for. The same discipline pointed at praise, which gets markedly less scrutiny than a claim about a field name - and is therefore where a wrong attribution survives.

Do not loosen a guard to make a test pass

When a guard blocks a test, the question is whether the guard is wrong, and that is answered by probing outside it, never by relaxing it. In the session above, bypassing the check-out guard produced a NullPointerException from the connector's own visitor - which proved the guard had been right all along. Had it simply been switched off, the run would have gone green and destroyed data.

Write the failure up with its consequence attached

An abstract rule does not survive the pull of finishing. A concrete one does.

  • weak: "verify by effect"
  • strong: "checkout() returns a staging path and isErrorExists:false while checking nothing out; the Check-In then dies on a null stereotype two hours later"

Keep the dead hypotheses in the write-up. They are what stops the next person re-running them, and they are usually the more useful half.

Scope the claim to what was tested

"Check-Out cannot be automated" and "checkout() alone does not check out" are different claims and only the second was tested. Write the one you tested. A reader cannot tell from a headline that only one of several routes was tried, and an over-scoped negative gets quoted for months.

Provenance, on every line

Mark each recorded fact EXERCISED (it came back from a real call) or DERIVED (reasoned from a doc, a schema, or source). Specificity reads as authority: a precise, plausible, derived note is more expensive when wrong than a vague one, because people act on it. The fix is not to be vaguer. It is to say which.


Generated from skills/diagnose-silent-failure/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.