test(spec,objectql): pin the IMetadataService register → get round-trip across every shipped implementation (#7223) - #7371
Conversation
…ip across every shipped implementation (#7223) `register(type, name, data)` and `get(type, name)` are the contract's first two CRUD members, and the round-trip between them was exercised in exactly one place — `packages/spec/src/contracts/metadata-service.test.ts`, against a hand-rolled `Map`-of-`Map`s double written inside the test itself. No shipped implementation was held to it, which is the hole #6725 fell through: a shipped, exported `IMetadataService` could not perform its own most basic round-trip while the full objectql suite and all 64 `lint.yml` gates stayed green. Adds `METADATA_ROUNDTRIP_CASES` (`@objectstack/spec/contracts`) — 15 cases, one table, a thin driver per implementation, the shape `data/filter-logic-conformance.ts` already uses for filter backends — plus the two drivers that run it: - the contract's own reference double, in `packages/spec` (the dependency root, which can see no implementation); - every implementation this repo ships, in `packages/objectql` (the only package that can see all three at once): `MetadataManager` with and without a writable loader, `createMemoryMetadata`, `MetadataFacade`. The pre-existing Map double in `metadata-service.test.ts` is untouched — it pins the contract's type surface and its own inline round-trip, independent of any implementation. No shipped behaviour changes. Three cases get different answers from `MetadataFacade` than from the other implementations and the reference double; each is pinned as measured under a `// DIVERGENCE` marker and filed as its own card rather than reconciled here. Verified the suite is not vacuous by re-introducing the #6725 split locally (dropping the contributor write from `MetadataFacade.registerObjectBothPlaces`): four rows go red, including the plain object round-trip. Refs #7223, #6725, PR #7211, #6745. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193R6tMZqgrdFrCSnaogFc4
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #7223.
The gap
register(type, name, data)andget(type, name)are the contract's first two CRUD members, and the round-trip between them was exercised in exactly one place —packages/spec/src/contracts/metadata-service.test.ts, against a hand-rolledMap-of-Maps double written inside the test itself. No shipped implementation was held to it.That is the hole #6725 fell through:
MetadataFacade.register('object', …)wrote into a map none of its own reads consulted, every read answeredundefined, and the fullpackages/objectqlsuite plus all 64lint.ymlgates stayed green while a shipped, exported implementation of the platform's central metadata contract could not perform its own most basic round-trip.The shape
One table, a thin driver per implementation — the same shape
data/filter-logic-conformance.tsalready uses for filter backends.METADATA_ROUNDTRIP_CASES(@objectstack/spec/contracts, new file) — 15 cases, each a short sequence of writes followed by exactly one read: plain round-trip on an object-typed and a non-object-typed write, the miss shape on both, re-registration, type scoping in both directions, name case sensitivity (exact hit + lowercased miss), thedata.name-vs-argument keying question, the pluralobjectsspelling, a primitivedatavalue, and an after-unregistercontrol.Two drivers run it:
packages/spec/src/contracts/metadata-service-roundtrip-conformance.test.tspackages/objectql/src/metadata-service-roundtrip-conformance.test.tsMetadataManager(registry only),MetadataManager(writabledatasource:loader),createMemoryMetadata,MetadataFacadepackages/objectqlhosts the shipped-implementation half because it is the only package that can see all three at once — the same argumentmetadata-service-getobject-equivalence.test.ts(#6745) already makes for living there.packages/speccannot host it: the contract has no runtime and spec is the dependency root.The pre-existing Map double in
metadata-service.test.tsis untouched. It pins the contract's type surface and its own inline round-trip, independent of any implementation; the new spec-side driver pins the table's reference answers so a typo inexpectedcannot silently redefine conformance for every subject at once.MetadataManagerappears twice because itsregisterwrites the in-memory registry and persists to every writabledatasource:loader — a loader-less subject never executes the second half.Divergences — pinned, not resolved
Three cases get different answers from
MetadataFacadethan from the other implementations and the reference double. No shipped behaviour changes in this PR. Each answer is pinned as measured under a// DIVERGENCEmarker; which answer is correct is a separate ruling, filed as its own card.MetadataManager/createMemoryMetadata/ referenceMetadataFacadekey-is-the-name-argument-object/-nonobjectnameargument — readable backdata.name—getundefined,existsfalse,listNamesreports the other spellingplural-objects-type-is-its-own-storeobjectsandobjectare two stores — invisible under the singularget('object', n)primitive-data-roundtripsregisteraccepts, no member reads it backThe first is the most consequential:
register(t, n, d)→get(t, n), the exact proposition this card is about, does not hold onMetadataFacadewheneverd.name !== n. The contract TSDoc names the parameter on both members and says nothing aboutdata.name, so nothing in-tree currently rules which is right.Anti-vacuity
Verified by re-introducing the #6725 split locally (dropping the contributor write from
MetadataFacade.registerObjectBothPlaces): four rows go red, including the plain object round-trip. Reverted before commit. Dropping any single divergence override also goes red, and two wiring tests guard the override map from both directions — a stale override key, and a case no subject is held to.MetadataFacadeis asserted with a recursive-subset match rather than exact equality, because it answers the runtime-effective object (system fields injected bySchemaRegistry) rather than the stored document — exactly as #7223 predicted. That weaker match is scoped to the one subject that needs it via a declareddocumentFidelity; every other subject is held to exact equality, and all four get the same key/visibility assertions.Changeset
Non-empty,
@objectstack/spec: patch. This is not the tests-onlyskip-changesetcase: the PR adds 5 public exports to@objectstack/spec(METADATA_ROUNDTRIP_CASES+ 4 types), which third-party implementors can import to check their own backend — theFILTER_LOGIC_CASESprecedent, which is likewise on spec's public surface.check:api-surfacerecorded 0 breaking, 5 added;api-surface/contracts.jsonandexport-origins/contracts.jsonare regenerated, each new name resolving to one origin in the new file (no re-homing, no dual source). Nothing breaking ⇒ no ADR-0087.This PR touches
packages/spec/src/contracts/**— one new file plus four lines incontracts/index.ts. Flagged for the #6017 cross-seat declaration.Refs #7223, #6725, PR #7211, #6745, #6505 / PR #6723.
🤖 Generated with Claude Code
https://claude.ai/code/session_0193R6tMZqgrdFrCSnaogFc4
Generated by Claude Code