Skip to content

The nameField write-side strip drops an author's EXPLICIT pointer when it coincides with the derivation — make it stored-row-aware (option C), now that #8184 has landed and the fence is lifted #8381

Closed as not planned

Description

@os-zhuang

Filed by the #8268 dev agent, at the PM's ruling on that card's open question. Unassigned. The refinement below changes no resolved answer today — it is a fidelity fix for what gets persisted, not a behaviour fix.

Context — what shipped, and why

#8268 converged the ADR-0079 nameField designation at the /meta read exits. That read stamp created a write obligation (the landed #4326 round-trip pin caught it immediately), so #8268 also shipped the write-side inverse: SchemaRegistry.stripProvisionedPrimaryFrom, reached via stripMaterializedStampsFrom.

That strip is exactness-bounded — the pointer comes off only when it is identical to what provisionPrimary(body-without-pointer, { synthesize: false }) would designate. A pointer naming any other field is the author overriding the derivation and is kept untouched (pinned as KEEPS an author's own designation that overrides the derivation).

The PM ruled option A (ship the exactness-bounded strip) and asked for this card. Option B (do not strip) was ruled out: it bakes a read-added pointer into the tenant's sys_metadata row, its checksum and every history diff — the phantom-customisation harm #6562 already ruled against — and leaves the two halves of one seam asymmetric.

The residual divergence this card closes

nameField is a single string, so "the author wrote nameField: 'name'" and "the read derived nameField: 'name'" are the same bytes in the served document. The strip cannot tell them apart, so an author's explicit pointer that happens to coincide with the derivation is dropped on a round trip.

This is NOT the trade stripProvisionedSearchCompanionFrom makes. That one compares against a platform-canonical field DEFINITION, where coincidence with something an author typed is implausible. Here coincidence is the common case: an author who picks name as the title on an object with a name field writes exactly what the derivation would have produced.

Pinned as an expected divergence in packages/objectql/src/meta-object-primary-designation-roundtrip.test.ts (MEASURES that an author pointer identical to the derivation does NOT survive), so a fix here turns that pin red and it should be updated in the same PR.

Why it is currently harmless, and when it stops being

registerObject runs provisionPrimary over every base layer at load, so the pointer is re-derived on every boot: the resolved answer is identical with or without the stored key, now and at any future boot. What is lost is stability of intent — if a later edit adds a field that outranks the current one in deriveDisplayField's tiers (tier 1 name-ish exact, in NAME_ISH_EXACT priority order; tier 2 affix; tier 3 declaration order), the title moves, where a stored explicit pointer would have held it.

The fix — option C

Make the strip stored-row-aware: remove the pointer only when it is redundant with the derivation and absent from the currently persisted row. Lossless in both directions — a read-added pointer never persists, an author-written pointer never disappears.

This needs the existing stored row at the strip site, which means saveMetaItem itself rather than only the helper it calls.

⚠️ The fence that blocked this on #8268 is LIFTED — do not re-derive it

#8268 could not take this route because saveMetaItem's body was concurrently held by #8184, and that card's dispatch fenced it off. #8184 has since landed (003feae65), so saveMetaItem is available. Whoever takes this card should not re-discover the old fence and assume it still holds.

Note saveMetaItem already reads the existing row on this path (the destructive-change diff and the provenance gate both consult it), so the stored body is likely reachable without a second read — confirm rather than assume.

Where (anchored on function names, not line numbers)

  • packages/objectql/src/registry.ts — stripProvisionedPrimaryFrom (the exactness-bounded strip as shipped), stripMaterializedStampsFrom (the whole-seam inverse), materializeServedObjectOnto (the read half).
  • packages/metadata-protocol/src/protocol.ts — stripMaterializedFromRegistry and stripServedObjectColumns (the helpers), and saveMetaItem, which holds the stored row this card needs.
  • ⚠️ Whatever shape this takes must keep the landed [P3] Read decorations (_diagnostics, _draft) round-trip into persisted sys_metadata bodies #4326 pins green: meta-object-primary-designation-roundtrip.test.ts and meta-object-search-companion-roundtrip.test.ts.

Scope note

⛔ This is about the nameField strip only. The sibling findings on the same seam are separately tracked: #8375 (the multi-tenant indexes stamp has no converger at all) and #8376 (needs-user-decision — the read exits materialize the folded object while the registry materializes the base).

Related

#8268 (the class fix and its stamp inventory) · #8184 (003feae65, whose landing lifts the fence) · #4326 (the byte-identical round-trip invariant) · #6562 · #8038 · ADR-0005 §Validation · ADR-0079


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ⚠️ Option C as written does NOT close the gap — read this before implementing it

    Two corrections to this card's premise. The first came from the dev who filed it, while writing the pin; the second follows from it and is mine.

    1. It is not a round trip — the pointer is never stored at all

    The pin's first version asserted the pointer was stored, then lost on a round trip. It failed: expected undefined to be 'name'. The strip runs on every save, so an author's redundant pointer is dropped by the first write that carries it — before any read, any /meta involvement, any round trip. There is nothing to lose because nothing is ever persisted.

    Credit where due: the pin failed and the response was to re-derive the mechanism and correct the card, not to adjust the pin until it passed. The body above still describes the round-trip shape in places and should be read with this correction in front of it.

    2. Therefore option C, as stated, is equivalent to option A on the case it exists to fix

    C's rule is strip only when redundant with the derivation and absent from the currently persisted row. Walk the two scenarios:

    stored row incoming body C's verdict
    Phantom customisation — author saved without a pointer, a read materialised one, client saves that body back no pointer redundant pointer absent ⇒ strip ✅ correct
    The loss this card is about — author deliberately writes nameField: 'name' on a fresh object no pointer (nothing stored yet) redundant pointer absent ⇒ strip ❌ same loss as A

    C preserves a pointer only once it is already stored — and under A or C it can never become stored, so there is no state from which C's protection can ever engage. "Lossless in both directions" does not hold as written.

    The underlying reason is worth stating plainly, because it is what makes this hard rather than fiddly: at the write boundary the two scenarios are byte-identical in every field the platform can see — same incoming pointer, same absent stored pointer. No comparison of body against stored row can separate them, because the difference is provenance, not content.

    What a taker should actually price

    Do not implement C expecting it to work. The axis that carries real signal is CREATE versus UPDATE: on a create there is no prior read of this object to have injected anything, so a redundant pointer in a create body is author-written; the phantom-customisation path necessarily goes through a read first, and therefore an update. So the shape worth costing is roughly do not strip on create; strip on update when absent from the stored row.

    That is not airtight either — a client can create from a template it read elsewhere — so the honest framing of this card is "author-written and read-added are indistinguishable by content; here are the options and which harm each accepts", not "a lossless fix exists and here it is." Price CREATE/UPDATE, price a provenance mark, and price simply keeping A, before assuming a clean answer is available.

    What has not changed

    The PM ruling stands: A ships. It remains observationally identical to C for every consumer — the resolved answer is the derivation's either way, registerObject re-derives on every boot, and the pin at meta-object-primary-designation-roundtrip.test.ts asserts both the unchanged resolved answer and that the read still serves it. This is a persistence-fidelity gap, not a behaviour regression, and option B stays ruled out (it bakes a read-added pointer into the tenant's row, checksum and history diffs — the #6562 phantom-customisation harm).

    The fence note in the body is still correct and still worth reading: saveMetaItem was held by #8184, #8184 has landed (003feae65), so whatever shape is chosen is immediately actionable.


    Generated by Claude Code

  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    ESCALATE — pm:queue → needs-user-decision. The option in this card's title is measurably disproved, and what is left is a choice of which harm to accept.

    PM session session_01XeQRiAa7vYRVX5Fog7Zby8, domain:metadata lane. Picked this up as the next dispatchable card after #8595 and #8608 landed; the pre-dispatch premise check stopped it. ⛔ Not claimed, no dev dispatched.

    Why it is not dispatchable as filed

    The card's title prescribes option C ("make it stored-row-aware"). Comment 5279591027 — from the dev who filed it, while writing the pin — measured that C does not close the gap:

    stored row incoming body C's verdict
    phantom customisation no pointer redundant pointer strip ✅
    the loss this card is about no pointer (fresh object) redundant pointer strip ❌ same as A

    C protects a pointer only once it is already stored, and under either A or C it can never become stored — so there is no state from which C's protection can ever engage. Dispatching the card's own title would ship a change that does nothing for the case it names.

    The reason is structural rather than fiddly, and it is why no clean answer is sitting here: at the write boundary the two scenarios are byte-identical in every field the platform can see. The difference is provenance, not content, so no comparison of body against stored row can separate them.

    ⚠️ Worth saying plainly: that correction exists because the pin failed and the response was to re-derive the mechanism rather than adjust the pin until it passed. The card body above still describes the round-trip shape in places and must be read with the correction in front of it.

    What is actually on the table

    • Keep A (ships today). An author's explicit nameField: 'name' is dropped by the first write that carries it. Observationally identical for every consumer — the resolved answer is the derivation's either way, registerObject re-derives on every boot — so this is a persistence-fidelity gap, not a behaviour regression.
    • CREATE vs UPDATE. On a create there is no prior read to have injected anything, so a redundant pointer in a create body is author-written; the phantom path necessarily goes through a read, hence an update. ⚠️ Not airtight — a client can create from a template it read elsewhere.
    • A provenance mark. The only shape that separates the two by construction rather than by heuristic, and the only one that introduces new machinery to carry.
    • ⛔ Option B stays ruled out — it bakes a read-added pointer into the tenant's row, checksum and history diffs (the meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562 phantom-customisation harm).

    Four-prism card face

    1. Platform long-term coherence — a provenance mark is the only option that makes the distinction expressible; CREATE/UPDATE encodes a correlation the platform cannot actually verify, which is a special case that will need exceptions. Keeping A is coherent but leaves the declaration and the stored row permanently disagreeing.
    2. Measured business pull — thin, and stated honestly: zero consumer-visible difference today. The harm is that an author writes something true and the platform silently discards it. No incident, no claimant. That argues against spending new machinery here and for either keeping A or waiting for pull.
    3. AI-agent error-resistance — cuts both ways, which is why it does not settle it. Silently discarding an author's explicit declaration is the tolerant-consumer shape this repo keeps refusing; but baking a read-added pointer into a tenant's row (B) is the phantom-customisation shape it also refuses, and CREATE/UPDATE would do that on the template path.
    4. Startup scope discipline — keeping A costs nothing. CREATE/UPDATE is small but not airtight. A provenance mark is real, permanent surface for a gap with no measured claimant.

    This seat's recommendation

    Keep A, and close or hold this card — unless you want the provenance mark for its own sake. The measured pull is zero, the fidelity gap is invisible to every consumer, and both improvement options trade a silent discard for either an unverifiable heuristic or permanent new machinery. If it stays open, pm:on-hold with a named restart condition (a real author or agent reporting a lost declaration) is more honest than pm:queue, which is what it was carrying.

    ⛔ I have not closed or held it — that is your call, not this seat's. What I have changed is the state that said "ready to dispatch", because the option it prescribes was disproved on this very thread.


    Generated by Claude Code

  3. os-zhuang commented on Aug 15, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (delegated adjudication; delegation 2026-08-15 verbatim 「决策你直接帮我做」, batch confirmed 「同意」)

    Ruled: keep A (the shipped exactness-bounded strip). This card moves to pm:on-hold — not queued, not closed.

    The title's Option C is disproved on this thread by the filing dev's own measurement: the pointer is never persisted, so there is no stored state from which C's protection could ever engage — C is behaviourally identical to A on the exact case it was named for. At the write boundary the two scenarios are byte-identical; the difference is provenance, not content, so no content heuristic closes this. Option B stays ruled out (#6562 phantom-customisation harm). CREATE/UPDATE encodes a correlation the platform cannot verify (template-create path), and a provenance mark is permanent machinery for a gap with zero measured pull — no consumer-visible difference, no incident, no claimant.

    Restart condition, named: a real author or agent reports a lost explicit nameField declaration (i.e. the fidelity gap acquires a claimant, e.g. a tier-outranking field is later added and a title silently moves where a stored pointer would have held it). On that report, reopen toward the provenance-mark direction — the only shape that separates the two cases by construction — and price it then.

    Until then: A stands, the MEASURES that an author pointer identical to the derivation does NOT survive pin stays as the honest record of the divergence, and no dev is spent here.

    Label flipped needs-user-decision → pm:on-hold.


    Generated by Claude Code

  4. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    ContributorAuthor

    Closed not planned under the one-time pm:on-hold stock reclassification (policy anchor: #9043).

    Maintainer rulings, 2026-08-16, verbatim and untranslated:

    我建议有些暂时不准备开发的应该直接关闭。

    确认,先处理 objectstack

    Provenance: maintainer, 2026-08-16, skills-lane live PM session (session_017TNzEetykdh7ceZGwuAPLq); relayed by this bookkeeping sweep. Under the finalized pm:on-hold definition recorded on #9043, the state is legal only with a machine-readable Restart-when: exit — closed <owner/repo>#N, or an executable predicate. manual — <reason> is not a legal exit: a card no mechanism can revive is closed here, with the reason and provenance recorded, rather than held indefinitely.

    Held-reason as read from this thread: the maintainer ruled 2026-08-15 to keep option A (the shipped exactness-bounded strip) — option C was measurably disproved on this thread and option B stays ruled out — leaving a persistence-fidelity gap with zero measured pull whose only named restart was a human report of a lost explicit nameField declaration.

    This closure is the record, not a verdict on validity — reopen freely when the need or a machine-readable restart condition materializes.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions