LearnNewsExamplesServices
Frontmatter
id18502
titleA tear-out receipt reads only the origin document, so a transferred catalog record is indistinguishable from a lost one
stateClosed
labels
bugaitestingdashboard
assigneesneo-opus-grace
createdAtSep 8, 2026, 11:54 PM
updatedAtSep 9, 2026, 10:59 AM
githubUrlhttps://github.com/neomjs/neo/issues/18502
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 9, 2026, 10:59 AM

A tear-out receipt reads only the origin document, so a transferred catalog record is indistinguishable from a lost one

Closed v13.2.0/archive-v13-2-0-chunk-20 bugaitestingdashboard
neo-opus-grace
neo-opus-grace commented on Sep 8, 2026, 11:54 PM

Serves: #17844's AC-5 — the instrument its largest failure class needed to be dispositioned. Not a lane under an open epic; stated rather than claimed.

The problem

executeTearOutStep's receipt reports itemKeptInCatalog from the origin's dockModel alone:

keptInCatalog = Boolean(documentAfter.items?.[itemId])   // documentAfter = the ORIGIN's

Catalog ownership is per-participant — WorkspaceSet derives preserveItemIds from the other participants' items, which only parses if an item lives in exactly one catalog. So itemKeptInCatalog: false has two readings with opposite repairs:

  • the record was lost — a real defect in the vessel path;
  • the record moved to the vessel's participant — correct behaviour, and the assertion is wrong.

The receipt cannot tell them apart, and neither can a reader. Six of #17844's 43 invariant failures carry exactly that signature, and the ambiguity cost a full evening of source elimination — ten writers read and cleared — before a runtime measurement settled it.

Why it is worth an instrument rather than one investigation

The next reader of detachItem commit did not reach committed document truth inherits the same ambiguity. The message names a conclusion ("did not reach") the code cannot support, and the receipt lacks the one field that would discriminate. Answering it once in prose leaves the next occurrence exactly as expensive.

The fix

A spec-side helper capturing catalog keys per participant, folded into the failure message beside the receipt. Reads only shipped surfaces — getPopupStates for registered participants, getWorkspaceDocument for committed truth per key — so no production instrumentation is added to observe it.

AC

  • AC-1 A helper returns catalog keys per participant id, main included, reading only existing public surfaces. No file under src/ or apps/ changes.
  • AC-2 null (unregistered participant) and [] (registered, empty catalog) remain distinct return values. Collapsing them hides the transfer the helper exists to detect.
  • AC-3 The snapshot travels in the failure message of the tear-out assertion, so a reader gets the answer without re-running the battery.
  • AC-4 It lives beside callWorkstationGesture in test/playwright/e2e/utils/, so WorkstationTearOutSourceContinuityNL — which carries the same message on two of its own failures — uses it without a second copy.
  • AC-5 Demonstrated on the live failing scene: the snapshot names which participant holds the item.

Out of scope

The repair of waitForTearOutCommit's predicate. On the tear-out path it asserts the plain-detach contract (the origin keeps the record) against a path whose purpose is to hand that record to another participant, so it is false by design after a correct tear-out. That change lives in apps/workstation/tour/NativeGestureDriver.mjs and belongs to @neo-gpt-emmy, who owns that surface and raised the transfer reading first.

Revalidation trigger

If the predicate repair lands before this does, the helper still earns its place — it becomes the arm proving the fix, showing the item under the vessel key and a passing test. If instead a reader shows the receipt already carries participant-scoped catalog truth by a path this body missed, close as already-resolved.