Skip to content

fix(objectql,spec): rule the name argument as IMetadataService's effective key (#7378) - #7511

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7378-metadata-facade-roundtrip-ruling
Aug 11, 2026
Merged

fix(objectql,spec): rule the name argument as IMetadataService's effective key (#7378)#7511
os-zhuang merged 1 commit into
mainfrom
claude/issue-7378-metadata-facade-roundtrip-ruling

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #7378

⚠️ Part of, not Fixes, deliberately — and this contradicts the dispatch prompt. The prompt asked for Fixes #7378; the standing clause in .claude/agents/os-dev.md says a card whose other half is still in the decision box must not be closed by its PR. Cell 2 (the plural objects alias) is returned as needs_decision with the measurement below, so merging this would leave #7378 unanswered on that row. Surfacing the conflict rather than picking a side silently — flip the line to Fixes if the PM rules cell 2 out of this card's scope.

The ruling this implements

Maintainer ruling recorded on the card 2026-08-11 03:00Z, option (a), quoted verbatim:

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".

Option (c) — loud refusal on data.name !== name — is recorded as the v18 strictness candidate and is not implemented anywhere here.

Premise: verified, all three cells still reproduced at origin/main

Baseline before any edit, on origin/main @ 211abdb: the full @objectstack/objectql suite is green (178 files, 3149 tests) with the three DIVERGENCE pins asserting the facade's divergent answers — i.e. the divergences are exactly as filed, and the mechanism reads as described in metadata-facade.ts ({ ...data, name: data.name ?? name } handed to stores that key on the document).

What changed

Contract — packages/spec (declared on #6017 per declare-and-proceed)

Exact files touched in packages/spec:

  • packages/spec/src/contracts/metadata-service.ts — TSDoc only, on IMetadataService.register and .get: the argument is the effective key, data.name never overrides it, including for a data that is not an object (the member declares data: unknown). The parked option (c) is recorded on register as the v18 candidate.
  • packages/spec/src/contracts/metadata-service-roundtrip-conformance.ts — the shared case table: the header's "three cases are answered differently, pinned as measured" paragraph now records which rows the ruling settled and which one it did not; the why prose on the ruled rows is relabelled from "shipped implementations disagree" to the ruling; and one new case, array-data-roundtrips.

No type or export signature changed, so no consumer sweep was implicated; pnpm --filter @objectstack/spec check:generated reports all 13 generated artifacts already up to date (nothing to regenerate, nothing hand-edited).

Cell 1 — the effective key (RULED, aligned)

MetadataFacade.register derived its key from the document twice over: data.name ?? name, and definition.id ? 'id' : 'name' as the generic store's key field. A document whose own name (or id) disagreed with the argument was filed under the document's spelling — get/exists missed it and listNames reported the other name. A new toKeyedDefinition helper reconciles the document to the argument before either store sees it, and the generic store is now always keyed on name.

Reconciling rather than key-only is forced by the stores, and is stated in the helper's TSDoc: SchemaRegistry.registerItem reads item[keyField] and registerObject keys objectContributors on the schema's own name. It is also what ObjectQL.registerMetadataCollections already does on the plugin ingest path (item.name === itemName ? item : { ...item, name: itemName }), so a facade write and a plugin write now agree about identity instead of keying on different fields.

Cell 3 — non-object data (RULED, aligned)

A primitive data was accepted with no throw and then registered under the literal key undefined (the registry logged Registered setting: undefined), readable back through no member. It is now boxed as { name, content } — the shape this class's own reads already unwrap (get/list return item?.content ?? item, listNames reads item?.name), so the write half now produces what the read half was already prepared to consume. SchemaRegistry was never the obstacle: it stores any value, only the key derivation was document-based.

Arrays are boxed for the same reason, which is the new array-data-roundtrips case: an array passes a typeof data === 'object' guard, so the old spread turned [a, b] into { 0: a, 1: b } — corruption where the primitive row measured loss.

Cell 2 — the plural objects alias: STOPPED, not aligned (needs_decision)

Measured before touching it, per the dispatch's stop clause. 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 (packages/objectql/src/registry.ts, "Special handling for 'object' and 'objects' types"). The facade's own isObjectType governs only the WRITE side. So 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 through registry.getItem to getObject(n) and answers undefined. That is MetadataFacade.register('object', …) writes where neither of its own object reads look #6725 exactly, re-opened for the plural spelling: the pinned row would go green while the silent loss it exists to prevent got worse.
  2. Removing the registry alias runs against the platform's enforced normalization direction. Plural-to-singular folding is owned below this contract and enforced there: canonicalMetaType / PLURAL_TO_SINGULAR canonicalizes every /meta request type at the protocol boundary (meta overlays: unnormalized type segment creates phantom rows that shadow the code-authored listing and cannot be deleted #4432), RestServer.metaTypeSingular does it at the REST boundary, and check:meta-type-normalized is a CI gate that refuses any decision made on the un-normalized :type — three authorization bypasses (/meta 的每个按类型闸门只在单数拼写下生效 —— 复数(PD #3 的规范拼写)整条绕过,含 book audience、app RBAC、dashboard 能力门 #3984, ADR-0057 D10 的 dashboard requiresService 组件门禁在 GET /meta/:type/:name 的缓存分支(默认路径)被完全跳过 #5881, GET /meta/books/:name(复数拼写)绕过 ADR-0046 §6.7 audience 门禁 —— 缓存分支的 doc/book 排除写的是字面量比较 #6241) came from exactly that. registry.test.ts pins listItems("objects") as a deliberate alias.

So the open question is a ruling of its own: does IMetadataService key its type stores on the raw string (the reference semantics) or on the canonical type (what the rest of the platform does)? The DIVERGENCE_2 pin survives, now carrying this measurement and the escalation.

To be explicit about the gate the dispatch asked about: check:meta-type-normalized passes on this branch, and it scans packages/rest/src only — it does not judge the service contract's stores. Its enforced direction is what conflicts, not its scan surface.

The conformance pins now bear the ruled load

The two ruled rows are not "divergence assertion deleted". key-is-the-name-argument-* moved from absent to a new readable-keyed-by-argument answer, so the driver now asserts the document comes back under the argument carrying every other authored key, exists is true, and listNames reports the argument exactly once. primitive-data-roundtrips and array-data-roundtrips have no per-subject entry at all — the facade is held to the table's own reference answer like every other subject. And one new assertion applies to every subject and every readable row: when the written document carries a name that is not the key being read, that stale spelling must not appear in listNames — which is what stops a "stored under both keys" implementation from passing the ruled rows for the wrong reason.

Strictness-ledger parking for option (c): reported, not forced

The dispatch named docs/audits/2026-07-unknown-key-strictness-ledger.md. Measured, its scope does not admit this entry: 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/…, and check:strictness-ledger requires every row to name a spec file whose sites the row judges). Option (c) is a runtime-contract argument-disagreement rule on an interface member — a different axis, with no z.object site to be a verdict about, in a machine-checked file that is a known merge-conflict magnet. Per the dispatch's own instruction I did not force it.

Parked instead where the next implementer will actually be standing: in the IMetadataService.register TSDoc itself, as an explicit "PARKED for v18 consideration, do not implement ahead of that ruling" paragraph next to the rule it would tighten, plus the ruling comment already on #7378. Naming a better home is the PM's call if this one is wrong.

Behaviour-affecting consumers (blast radius)

Swept for in-tree constructors and callers of MetadataFacade.register: there are none. The only in-tree references are the two re-exports (packages/objectql/src/index.ts, .../core.ts), documentation prose in packages/spec / packages/mcp / plugin-security, and the facade's own tests. Nothing in-tree relies on document-name keying or on the plural alias read-through, so the in-repo blast radius of this change is the conformance driver and the facade's own suite.

Out of tree, the class reaches hosts through those two exports. A downstream host that relied on register keying by data.name or data.id rather than by the argument it passed will see items move to the argument's key; that is the ruled change and it is stated in the changeset.

Changeset: patch for both packages, argued

patch, not minor: the ruling classifies the old facade behaviour as a defect against the contract this repo already declares — 4 of the 5 measured implementations conformed before this PR, and the TSDoc change writes down what the parameter names already implied rather than adding a capability. packages/spec gains no new authorable surface (TSDoc plus one conformance case). The counter-argument for minor is real and is why the changeset spells the behaviour change out: an out-of-tree host CAN observe items moving keys. It is not a new feature and it removes silent loss, so patch with a loud changeset body is the honest label.

Verification

  • Build closure first: pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build — green.
  • pnpm --filter @objectstack/objectql test178 files, 3153 tests passed (baseline on origin/main was 178 / 3149; the +4 are the new array case across the driver's four subjects).
  • pnpm --filter @objectstack/spec test374 files, 9803 tests passed (this includes the reference double's own driver replaying the new case).
  • pnpm --filter @objectstack/objectql --filter @objectstack/spec typecheck — green both.
  • Reverse verification, direction predicted first: restoring the old keying (data.name ?? name plus the id key field) was predicted to turn exactly 4 rows red, all on the MetadataFacade subject. Measured: 4 failed | 62 passed, red on key-is-the-name-argument-object, key-is-the-name-argument-nonobject, primitive-data-roundtrips, array-data-roundtrips, all under ['MetadataFacade'], every other subject untouched. Fix restored from a patch file (no git stash).
  • Gates run locally, all green: check:adr-anchors, check:changeset-gate-self-tests, check:doc-authoring, check:doc-formula-expressions (in packages/lint, where it lives — there is no root script of that name; its spec-TSDoc half judged 9 examples clean), check:docs-audit-scope, check:durability-log-level, check:engine-double-contract, check:i18n (after building @objectstack/cli, which it requires), 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 artifacts already current).

Generated by Claude Code

…fective key (#7378)

Maintainer ruling of 2026-08-11 on #7378, option (a): the `name` argument is
the effective storage key and `data.name` never overrides it. Written into the
contract TSDoc on `register` / `get`, and `MetadataFacade` aligned to it.

Two of the three measured divergence cells are settled:

- the effective key — `MetadataFacade.register` derived its key from the
  document (`data.name ?? name`, and `data.id` for the generic store), so a
  document whose own name disagreed with the argument was filed under the
  document's spelling and `get`/`exists` missed it. It now keys on the
  argument, reconciling the document to it.
- the dropped non-object `data` — a primitive was accepted with no throw and
  filed under the literal key `undefined`, readable back through no member. It
  is now boxed as `{ name, content }`, the shape this class's own reads already
  unwrap. Arrays are boxed too rather than spread into `{ 0: …, 1: … }`.

The third cell (the plural `objects` alias) is NOT changed: the alias that
decides it is `SchemaRegistry.getItem`/`listItems`' own read-side special-case,
and both routes to aligning it are worse than the divergence — narrowing the
facade's write side re-opens #6725 for the plural spelling, and removing the
registry alias runs against the platform's enforced plural→singular direction
(`canonicalMetaType` #4432, `RestServer.metaTypeSingular`,
`check:meta-type-normalized`). Its pin survives with that measurement written
out, and the question is escalated on #7378.

The conformance pins for the ruled rows now assert the ruled behaviour rather
than the old `absent` divergence, plus a new `array-data-roundtrips` case and a
universal assertion that a document's own name never becomes a second key.

Option (c), loud refusal on a disagreement, is recorded as the v18 strictness
candidate and deliberately not implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtffEdkV9BA8f4yQXGXP9E
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 4:18am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 11, 2026 04:50
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 9051802 Aug 11, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7378-metadata-facade-roundtrip-ruling branch August 11, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants