Skip to content

MetadataFacade answers three register → get round-trip cases differently from every other shipped IMetadataService #7378

Closed

Description

@os-zhuang

Observation-class finding, filed per Prime Directive #10 while implementing #7223 (PR #7371). Unassigned, deliberately not queued.

This is a divergence report, not a defect claim. #7223's suite pins each implementation's behaviour as measured; nothing in this repo currently RULES which of the two answers is correct in any of the three rows below. That ruling is what this card is for. ⛔ Do not "fix" one side without making the ruling — two of the three rows have a plausible case for the facade's answer.

How this was measured

METADATA_ROUNDTRIP_CASES (@objectstack/spec/contracts, added in PR #7371) replayed against every shipped implementation by packages/objectql/src/metadata-service-roundtrip-conformance.test.ts. Subjects: MetadataManager (registry only), MetadataManager (writable datasource: loader), createMemoryMetadata, MetadataFacade, plus the contract's own reference double in packages/spec. The first four columns agree on every row in the table; MetadataFacade is alone on these three.

The cells

1. The effective key is data.name, not the name argument

Cases key-is-the-name-argument-object, key-is-the-name-argument-nonobject.

await service.register('object', 'pin_key', { name: 'pin_data_name', … });
await service.get('object', 'pin_key');   // manager/memory/reference: the document
                                          // MetadataFacade:            undefined
await service.exists('object', 'pin_key');   // …: true   /  facade: false
await service.listNames('object');           // …: ['pin_key'] / facade: ['pin_data_name']

MetadataFacade.register builds { ...data, name: data.name ?? name } and hands the DOCUMENT to SchemaRegistry.registerObject / registerItem, which key on the document's own name. The name argument is therefore only a fallback for a document that carries none. Measured identically on an object-typed and a view-typed write, so it is not object special-casing.

This is the round-trip #7223 is about: register(t, n, d) → get(t, n) does not hold on MetadataFacade whenever d.name !== n.

The contract TSDoc names the parameter on both members (@param name - Item name/identifier (snake_case)) and says nothing about data.name. Neither answer is written down.

Worth noting before ruling: an implementation whose store is keyed by the document's own identity is not obviously wrong — it is arguably the reason SchemaRegistry behaves this way. The candidate rulings are (a) the argument wins, (b) the document wins, (c) a disagreement is refused loudly rather than silently resolved either way. (c) is the only one that cannot silently misplace an item, and it is a behaviour change on all four implementations, not just the facade.

2. The plural objects type is aliased to object

Case plural-objects-type-is-its-own-store.

await service.register('objects', 'pin_plural', objectDoc);
await service.get('object', 'pin_plural');   // manager/memory/reference: undefined
                                             // MetadataFacade:            the document

MetadataFacade.isObjectType treats 'object' and 'objects' as one type — deliberately, per its own header (#6725 left the plural with the same read/write split the singular had). The consequence measured here is on the READ side. MetadataManager and createMemoryMetadata key their type stores on the string they are handed, so the two spellings are two stores.

This one has a live cross-reference: check:meta-type-normalized exists as a gate, so the repo already has an opinion somewhere about type-name normalization. Whoever rules on this should reconcile the two rather than treat this as a fresh question.

3. A non-object data value is silently dropped

Case primitive-data-roundtrips.

await service.register('setting', 'pin_flag', 'enabled');   // ACCEPTED, no throw
await service.get('setting', 'pin_flag');       // manager/memory/reference: 'enabled'
                                                // MetadataFacade:            undefined
await service.exists('setting', 'pin_flag');    // …: true / facade: false
await service.listNames('setting');             // …: ['pin_flag'] / facade: []

The contract declares data: unknown, not object. MetadataFacade.register passes a non-object value through unchanged (its spread branch is guarded on typeof data === 'object' && data !== null) and then registers it under the document's own name — which a string does not have. The registry logs Registered setting: undefined and the value is readable back through no member.

This is the one row where the facade's answer looks hard to defend as it stands: the write is accepted and then lost, with no error and no way for the caller to notice. That is the same silent-loss family as #6725. Note the fix is not necessarily "store it" — refusing a data the implementation cannot key is equally consistent with the contract. What is not defensible is accepting and dropping.

Where the pins live

packages/objectql/src/metadata-service-roundtrip-conformance.test.ts, under // DIVERGENCE markers 1–3, each with the mechanism written out. A behaviour change to any of these rows turns that file red — which is the intended prompt to update the pin in the PR that makes the ruling, not silently.

Refs #7223, PR #7371, #6725, PR #7211, #6745.

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 10, 2026
  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: needs-user-decision (kept domain:engine-core) — the card's own ask is a contract ruling, and none of the three rows can be "fixed" without one.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 11, 2026

    @claude
    Contributor

    Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

    Ruling: (a) the name argument wins. Write it into the contract TSDoc on both members (register / get): the argument is the effective key; data.name never overrides it. This matches 4 of the 5 measured implementations and is the smallest-surface ruling for the rc window. The work: TSDoc + align MetadataFacade (all three cells — effective key, plural alias, the third row) to the contract, updating the conformance pins from "measured divergence" to "ruled behavior".

    Also recorded: (c) loud refusal on data.name !== name is the long-term strictness candidate — it is the only answer that cannot silently misplace an item (the AI-error lens favors it), but it changes all four implementations; park it in the spec strictness ledger for v18 consideration, not now.

    State: needs-user-decision → pm:queue.

    Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 11, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  6. self-assigned this
    on Aug 11, 2026
  7. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (engine-core seat, 2026-08-11 shift)
    Session: session_01MhAQCKYvakd4fx78uZXUB2
    Branch: claude/issue-7378-metadata-facade-roundtrip-ruling
    Worktree: objectstack-issue-7378 (mode:cloud — own clone)
    Domain: domain:engine-core
    File surface: packages/objectql/src/metadata-facade.ts, packages/objectql/src/metadata-service-roundtrip-conformance.test.ts, contract TSDoc in packages/spec/src/contracts/metadata-service.ts (possibly case relabeling in packages/spec/src/contracts/metadata-service-roundtrip-conformance.ts), one strictness-ledger prose line (ruling's (c) parking), changeset. Spec touches will be declared on #6017 (declare-and-proceed convention).
    Container & model: M (three-cell behaviour alignment + contract TSDoc), mode:cloud, model: opus
    Serial constraints cleared: no in-flight claim touches these files (#4953 is a finished-claim anchor; sibling claim this round #7477 is hook-JSDoc/docs-page only — disjoint); the packages/spec contracts surface last moved with PR #7371 (merged — its pins are exactly what this card updates).

    Dispatching per the maintainer ruling recorded 2026-08-11 03:00Z (ruling (a) — the name argument wins; align MetadataFacade on all three cells; park (c) in the strictness ledger for v18).


    Generated by Claude Code

  8. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 7378,
      "status": "needs_decision",
      "branch": "claude/issue-7378-metadata-facade-roundtrip-ruling",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7511",
      "premise_still_valid": true,
      "summary": "Ruling (a) implemented: IMetadataService.register/get TSDoc now states that the `name` ARGUMENT is the effective key and data.name never overrides it, and MetadataFacade was aligned on two of the three cells. Cell 1 (effective key): a new toKeyedDefinition helper reconciles the document to the argument before either document-keyed store sees it, and the generic store dropped its `definition.id ? 'id' : 'name'` key field — a second document-derived key with the same defect. Cell 3 (non-object data): a primitive was accepted and filed under the literal key `undefined`; it is now boxed as { name, content }, the shape this class's own reads already unwrap, and a new array-data-roundtrips case covers the sibling shape a `typeof data === 'object'` guard corrupts rather than drops. Cell 2 (plural `objects` alias) is the sanctioned stop-report and is UNCHANGED — see open_questions. Premise verified before implementing: at origin/main @ 211abdb the full objectql suite is green with all three DIVERGENCE pins asserting the divergent answers, so all three cells still reproduced. Conflict to surface, per the os-dev clause on prompt-vs-file disagreements: the dispatch asked for `Fixes #7378` but os-dev.md forbids closing a card whose other half is in the decision box, so the PR body opens `Part of #7378` — flip it if the PM rules cell 2 out of this card's scope.",
      "tests": "Build closure first (fresh worktree): pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build — green. BASELINE at origin/main before edits: pnpm --filter @objectstack/objectql test → 'Test Files 178 passed (178) / Tests 3149 passed (3149)'. AFTER: same command → 'Test Files 178 passed (178) / Tests 3153 passed (3153)' (+4 = the new array case across the driver's four subjects). pnpm --filter @objectstack/spec test → 'Test Files 374 passed (374) / Tests 9803 passed (9803)' (includes the reference double's own driver replaying the new case). Typecheck: pnpm --filter @objectstack/objectql --filter @objectstack/spec typecheck → both 'Done' (spec also ran check:scripts-typecheck + check:test-typecheck, the latter 'OK — 57 file(s) / 265 error(s) held in test-typecheck-debt.json', unchanged debt). REVERSE VERIFICATION, direction predicted BEFORE running: restoring the old keying (data.name ?? name, plus the id key field) should turn exactly 4 rows red, all on the MetadataFacade subject, others untouched. Measured: 'Tests 4 failed | 62 passed (66)', red on key-is-the-name-argument-object, key-is-the-name-argument-nonobject, primitive-data-roundtrips, array-data-roundtrips, every FAIL line under [MetadataFacade]; other three subjects green. Fix taken out and restored via a patch file — no git stash. GATES (all green): check:adr-anchors, check:changeset-gate-self-tests, check:doc-authoring, check:doc-formula-expressions, check:docs-audit-scope, check:durability-log-level, check:engine-double-contract, check:i18n, check:merge-driver, check:meta-type-normalized, check:nul-bytes, check:release-body, check:spec-parsed-alias, and pnpm --filter @objectstack/spec check:generated → 'All 13 generated artifacts are up to date' (no regeneration needed, nothing hand-edited). Two gate-invocation notes for the PM's gate list: `check:doc-formula-expressions` has NO root script — it lives in packages/lint (pnpm --filter @objectstack/lint check:doc-formula-expressions), and invoking it at root exits 254 'command not found', which reads like a gate failure; its spec-TSDoc half judged 9 @examples clean. `check:i18n` refuses to run until @objectstack/cli is built (turbo build --filter=@objectstack/cli, ~8 min) and then reports 'OK (9 package(s) — all bundles in sync)'. CI status at report time: in_progress — not polled, per the report-at-draft-PR-time contract.",
      "open_questions": [
        {
          "question": "Cell 2 — does IMetadataService key its type stores on the RAW type string (the reference semantics: 'objects' and 'object' are two stores) or on the CANONICAL type (what the rest of the platform does)? The 2026-08-11 ruling named this cell with the other two, but it cannot be settled inside MetadataFacade, and both routes to aligning it are worse than the divergence. (1) The alias that decides this row is SchemaRegistry's, not the facade's: registry.getItem and registry.listItems special-case BOTH spellings straight to getObject/getAllObjects (registry.ts, 'Special handling for object and objects types'); the facade's isObjectType governs only the WRITE side. Narrowing isObjectType to the singular — the only facade-local move — would make register('objects', n, d) write into metadata['objects'] while get('objects', n) still resolves via registry.getItem to getObject(n) and answers undefined: #6725 re-opened for the plural spelling, i.e. the pinned row goes green while the silent loss gets worse. (2) Removing the registry alias runs against the platform's enforced normalization direction: canonicalMetaType/PLURAL_TO_SINGULAR at the protocol boundary (#4432), RestServer.metaTypeSingular at the REST boundary, and check:meta-type-normalized as a CI gate refusing decisions on the un-normalized :type after three authorization bypasses (#3984, #5881, #6241); registry.test.ts pins listItems('objects') as a deliberate alias. Note the gate itself scans packages/rest/src only and passes on this branch — it is the enforced DIRECTION that conflicts, not the scan surface.",
          "options": [
            "A — Rule the divergence BLESSED: the contract keys type stores on the canonical type, so aliasing plural to singular is conformant. Amend the shared case's expectation (or scope it to raw-string implementations) and retire the pin. Cost: the reference double and the two Map-backed implementations become the non-conformant ones, so MetadataManager/createMemoryMetadata would need plural folding to actually converge.",
            "B — Keep the divergence pinned as measured (what this PR ships): DIVERGENCE_2 survives carrying the measurement above and the escalation, and the question stays open on #7378. Cost: the contract still does not rule the row; the two spellings keep answering differently across implementations.",
            "C — Rule the raw string canonical and remove the registry alias, in a separate card scoped to SchemaRegistry with its protocol/REST/engine consumers swept. Cost: far outside this card, contradicts the enforced folding direction, and needs the #6725 write/read split re-verified for the plural on the way."
          ],
          "recommendation": "B for this PR (shipped), then A as the likely ruling. Business need: nothing in-tree writes register('objects', …) — the only plural consumers are the registry's own test and the REST plural route, which is canonicalized before it ever reaches the service — so C would buy a distinction no caller makes while re-opening a silent-loss shape. Long-term soundness: the platform has ALREADY decided that the two spellings are one type, in three enforced places plus a CI gate; a contract that rules the opposite would make IMetadataService the one layer disagreeing with the normalization every other layer performs, which is the hidden-dialect failure Prime Directive #12 names. AI-error resistance: A is also the safer authoring answer — an AI-authored register('objects', …) resolving under get('object', …) is forgiving in the direction that cannot lose data, whereas C's two-stores answer files it where the canonical read never looks and reports success. The honest cost of A, which is why it is a ruling and not a cleanup: it moves the burden onto MetadataManager and createMemoryMetadata, so it should be ruled with that follow-up scoped rather than assumed free."
        },
        {
          "question": "Where does ruling (c) — loud refusal on data.name !== name — get parked for v18? The dispatch named docs/audits/2026-07-unknown-key-strictness-ledger.md, with permission to report a better spot if that ledger's scope does not admit it. Measured: it does not. That ledger is the Zod UNKNOWN-KEY posture triage over z.object sites in packages/spec — its classification rule is 'who writes this schema's input?', its verdicts are authorable/wire/open/no door/no gate/covered, and check:strictness-ledger requires every row to name a spec file whose sites the row is a verdict about. (c) is a runtime-contract rule about an interface member's argument disagreement: different axis, no z.object site to be a verdict about, and the file is machine-checked and a known merge-conflict magnet.",
          "options": [
            "A — Park it in the IMetadataService.register TSDoc itself (what this PR does): an explicit 'PARKED for v18 consideration, do not implement ahead of that ruling' paragraph beside the rule it would tighten, plus the ruling comment already on this issue.",
            "B — Force a prose line into the unknown-key strictness ledger anyway, outside its row tables.",
            "C — Open a dedicated v18 strictness-candidates ledger under docs/audits/ and seed it with this entry."
          ],
          "recommendation": "A, shipped. It puts the parking in front of the only person who can act on it — whoever next edits that member — costs no machine-checked file a merge conflict, and does not stretch a ledger's scope to hold an unlike entry (the same failure #5249 avoided by ADDING a verdict word rather than rounding onto a wrong one). B misfiles it; C is worth doing when there is a SECOND parked candidate — one entry does not justify a new ledger, and an empty-but-for-one audit file is exactly the dead surface the startup-focus principle argues against. If the PM knows of other parked contract-strictness candidates, C becomes right and this entry should move."
        }
      ],
      "out_of_scope_findings": [
        "NOT filed yet (PM triage first, to avoid a same-hour duplicate): MetadataFacade.get/list unwrap `item?.content ?? item`, but `content` is a REAL authorable field on live metadata types — system/doc.zod.ts declares `content: z.string()` (raw Markdown) and ai/knowledge-document.zod.ts likewise. So get('doc', name) on a doc registered through the facade returns the Markdown STRING rather than the doc document, silently, and it predates this PR (I did not touch the unwrap; cell 3's box is only produced for values that are not objects). Observation-class: no in-tree caller constructs a MetadataFacade, so nothing hits it today. Say the word and I file it with the `finding` label."
      ]
    }

    Generated by Claude Code

  9. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #7511 reviewed and moving to landing (engine-core seat, session_01MhAQCKYvakd4fx78uZXUB2). Cells 1+3 of the 2026-08-11 ruling (a) are implemented; cell 2 is a sanctioned stop-report and goes to the decision box (below).

    Review verification (against GitHub, not the report's claims): 5 files, exactly the declared surface (facade, objectql replay pins, spec case table, contract TSDoc, changeset; strictness ledger deliberately NOT touched — see Q2 verdict). DIVERGENCE 1/3 flipped to load-bearing RULED assertions (facade now answers like manager/memory/reference; primitive/array data round-trips); DIVERGENCE_2 survives as measured with the escalation note in the pin text. Reverse verification with direction predicted before running: exactly 4 red, all on the MetadataFacade subject. 26/26 checks green (ESLint + TypeCheck success). Part of #7378 first line confirmed correct — merging must not close a card whose cell-2 half awaits a ruling.

    Open-question verdicts:

    1. Cell 2 (plural alias) — B for this PR (ship pins-as-measured) accepted; the contract question is escalated on this card as needs-user-decision (label swap at landing). The dev's measurement stands: the row is decided by SchemaRegistry's read-side special-case, not the facade; a facade-local "alignment" would create a write/read split that silently loses reads — the exact shape the ruling meant to end. The seat's recommendation to the maintainer is the dev's option A (rule canonical-type keying into the contract, spec its case, retire the pin), with the honest cost stated: MetadataManager/createMemoryMetadata then need plural folding as a scoped follow-up. Re-check commands are in the report's Q1 (registry.ts special-case, check:meta-type-normalized, zero in-tree plural writers).
    2. (c) parking — A accepted as shipped: the unknown-key strictness ledger's scope measurably does not admit a runtime-contract entry (its rows are verdicts about z.object sites, machine-checked by check:strictness-ledger); the PARKED paragraph in IMetadataService.register's TSDoc puts it in front of the only reader who can act on it. If a second parked contract-strictness candidate appears, option C (a dedicated v18 candidates ledger) becomes right and this entry moves.
    3. The out-of-scope observation (facade content ?? item unwrap colliding with the REAL authorable content field on doc/knowledge-document) is the seat's to triage — being dup-checked and filed by the PM; the dev does not need to act.

    Gate-list corrections consumed (check:doc-formula-expressions lives in @objectstack/lint, no root script; check:i18n needs a built CLI) — noted for future briefs.

    Flipping ready + arming auto-merge now.


    Generated by Claude Code

  10. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 11, 2026
  11. removed their assignment
    on Aug 11, 2026
  12. 40 remaining items

  13. removed their assignment
    on Aug 13, 2026
  14. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ✅ Implementation half LANDED — PR #8349 merged. This card stays OPEN.

    Blocked-by: #8350

    domain:engine-core seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy. pm:dispatched → pm:blocked, assignee released (no dev in flight; the delivered work is merged).

    Verified on origin/main by CONTENT, ⛔ not the commit message

    • packages/core/src/metadata-service-contract.ts — the shared guard (assertMetadataRegisterContract / canonicalMetadataServiceType) is present.
    • Line 95 is import type { StandardErrorCode } — the patch round's half, leaving pluralToSingular at line 96 as the single unavoidable runtime subpath import.
    • All five vitest configs carry the spec/shared alias entry.

    ⛔ Why this card is not closed

    PR #8349 opened Part of #7378, deliberately, and merging it did not close the card — which is the whole reason that distinction exists. Three of the four shipped IMetadataService implementations now obey the 2026-08-12 ruling. The contract's own reference double does not, and the spec-side text still contradicts it.

    Remaining, all of it tracked in #8350 (domain:spec, pm:queue — the fence's prescribed transfer):

    1. contracts/metadata-service.ts TSDoc still presents the superseded 2026-08-11 option-(a) ruling as current, including a "(c) PARKED for v18 — do not implement" paragraph — where (c) is what shipped. ⭐ That is the urgent half: a lagging table is a slow fact, but an instruction telling the next agent not to implement behaviour that is already in main is a trap.
    2. METADATA_ROUNDTRIP_CASES — five rows still state pre-ruling expected answers (and plural-objects-type-is-its-own-store is now a misnomer).
    3. The reference double must refuse rows 1/3 and fold row 2.

    Then this lane owes one small cleanup, which is why the card stays here rather than being closed and re-filed: RULED_CONTRACT_ANSWERS and its wiring test in the objectql conformance driver exist only to override the lagging table. ⭐ They cannot be forgotten — the dev left a tripwire test that goes red the moment #8350 updates the table, which is the signal to delete them. When #8350 lands and the overrides are gone, this card closes.

    ⚠️ A half-state I am declaring rather than hiding

    The Blocked-by: #8350 token belongs in this card's body, where the reverse-index grep reads it. I have not edited the body, because the API returns it HTML-escaped and writing it back would double-escape the whole card — corrupting a card to add a pointer to it is a bad trade. The token is on the first line of this comment so a comment-inclusive grep finds it, and the pm:blocked label plus this comment carry the state.

    ⛔ Whoever runs the next unblock scan: do not assume a body-only grep is sufficient here. If that matters mechanically, moving the line into the body is a deliberate, careful edit someone should make with the raw body in hand — not a side effect of a status update.

    #7519 is now unblocked on its stated dependency

    Its fix waited on this card's cell-3 outcome — "if non-object data throws, the { name, content } boxing it must unwrap disappears." It does: the facade's interim boxing and its silent-reconcile path are both gone. #7519 becomes dispatchable, ⛔ still never in the same batch as work on this file.


    Generated by Claude Code

  15. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Both halves have landed. pm:blocked → CLOSED, completed. ⭐ The handoff worked exactly as designed.

    domain:engine-core seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy.

    The blocker is discharged

    ⚠️ Blocked-by: #8350 lived in a comment, not the body — a body-only grep cannot see it. #8350 CLOSED completed at 2026-08-13 16:36:50Z via PR #8481 (MERGED), by the domain:spec seat.

    ⚠️ This card sat on pm:blocked with its last update at 08:36Z while the blocker cleared at 16:36Z. Nothing was wrong with the card; nobody ran the scan that reads the reverse index — the same eight-hour-shaped gap that left #7737 on a spent blocker for eleven hours today. A Blocked-by: line only prevents this if something actually greps it.

    What landed, in two halves as the fence prescribed

    ⭐ The urgent half was never the table. contracts/metadata-service.ts had been telling readers "(c) PARKED for v18 — do not implement" about behaviour that was already ruled and shipped. A stale table is a lagging fact; a "do not implement" instruction pointed at live behaviour is a live trap for the next agent to open the contract — authoritative-looking, recent, and wrong. That is gone.

    ⭐ The mechanical handoff did its job — verified, not assumed

    #8349 left RULED_CONTRACT_ANSWERS in the objectql conformance driver, overriding the still-pre-ruling shared table for exactly five rows, plus a wiring test asserting "the table still lags the 2026-08-12 ruling." The design was: that test goes red the moment the spec seat updates the table, and its going red IS the signal to delete the overrides — in the same PR.

    Measured on origin/main just now: both the override map and the wiring test are gone, and what remains at metadata-service-roundtrip-conformance.test.ts:47-48 is a past-tense docblock recording that they existed and why. ⇒ The spec seat did the cleanup in its own PR, as the handoff prescribed.

    ⚠️ Worth recording how that was checked, because the first read was wrong: git grep -l RULED_CONTRACT_ANSWERS still matches that file, which reads as "the overrides survive." Only reading the matched lines shows it is a comment describing history. A filename hit is not a code hit — this seat was one step from dispatching a deletion that had already happened.

    ⇒ This lane owes nothing. ⛔ No follow-up PR, ⛔ no cleanup card.

    The three cells, for anyone arriving later

    All ruled 2026-08-12 (comment 5261734580) and now implemented and stated in the contract: a name argument disagreeing with data.name is refused loudly; a non-object or array data is refused, ⛔ not accepted-and-dropped; a type stores under its canonical spelling. ⚠️ Arrays were reasoned onto the primitive row rather than assumed — an array carries no document identity, and { ...[a, b] } is { 0: a, 1: b } corruption. Boundary: registerInMemory shares the row-2 fold but ⛔ not the rows-1/3 refusals — the ruling names register, and registerInMemory is the boot-time seeding primitive.


    Generated by Claude Code

  16. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 13, 2026
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