test(rest): pin the /meta/mapping item shape on three producer paths - #16475
Conversation
`GET /api/v1/meta/mapping` serves each item as the raw spec document with `targetObject` at the TOP LEVEL, and the console's import wizard depends on that silently: `@object-ui/data-objectstack`'s `listImportMappings` filters items by a top-level `targetObject` and degrades every failure to an EMPTY LIST. `GetMetaItemsResponseSchema` declares `items` as `unknown[]`, so the spec states nothing about the per-item shape, and the consumer-side pin carries a copied fixture of today's body -- by construction it moves right or wrong together with this producer. A consumer that degrades to empty + a schema that declares nothing + no producer pin = a reading that cannot fail. This adds the producer's half, in the existing real-stack harness (real `RestServer` route table over a real `ObjectStackProtocolImplementation` on a real `ObjectQL` + sqlite `:memory:`), over THREE producer paths rather than one: a manifest `mappings:` entry through `registerApp`, a direct `registry.registerItem`, and `MetadataManager.register` installed as the `metadata` service -- the registrar the file-based artifact loader uses, and the one whose publish envelope (`name`/`packageId`/`state`/`metadata`) could realistically start reaching the wire. A pin over `registerApp` alone would be green forever precisely because `registerApp` is not the producer anyone would change. `boot()` gains an optional service-registry argument, because a `metadata` service is read through `getServicesRegistry()` and cannot be attached after the protocol is constructed. Omitted, it is byte-for-byte the previous boot. Whether `GetMetaItemsResponseSchema.items` should be narrowed from `unknown` is a spec decision and is deliberately NOT taken here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
PM review — re-derived independently. Not landing yet: 3 checks still running.
Re-measured by me, not accepted from the report
⭐ The
|
Fixes #15907
GET /api/v1/meta/mappingserves each item as the raw spec document withtargetObjectat the top level, decorated and wrapped in a{ type, items }envelope. The console's import wizard depends on that silently:
@object-ui/data-objectstack'slistImportMappingsfilters items by a top-leveltargetObjectand degrades every failure to an empty list.GetMetaItemsResponseSchemadeclaresitemsasunknown[], so the spec statesnothing about the per-item shape. The consumer-side pin (objectui#7738) carries a
copied fixture of today's body, so by construction it moves right or wrong
together with this producer. And
packages/resthad no test that lists/meta/mappingat all — the named-mapping suite in this same file reads itsartifact through
getMetaItem, never through the list door.A consumer that degrades to empty, a schema that declares nothing, and no
producer-side pin add up to a reading that cannot fail: if the producer began
serving the
MetadataManagerpublish envelope (name/packageId/state/metadata), the selector would simply stop appearing, on every deployment, and areleased feature would read downstream as a hardcoded client. This PR adds the
producer's half.
What it pins
In the existing real-stack harness — real
RestServerroute table over a realObjectStackProtocolImplementationon a realObjectQLplus sqlite:memory:—GET /api/v1/meta/mappinganswers items whosetargetObjectis top-level andwhose
metadatamember is absent. The consumer's own predicate is replicatedon this side of the wire (a filter, not an index lookup), because the filter is
what makes a shape change silent downstream: it answers
[], not an error.Three producer paths, because one is not enough
A pin over
registerAppalone would be green forever precisely becauseregisterAppis not the producer anyone would change. The three paths covered:mappings:viaregisterAppregistry.registerItemMetadataManager.registerinstalled as themetadataserviceboot()gains an optional service-registry argument: ametadataservice is readthrough
getServicesRegistry()and cannot be attached after the protocol isconstructed. Omitted, it is byte-for-byte the previous boot.
The pin was ablated per path — prediction written first, then observed
Each leg mutated one producer to serve the publish envelope, rebuilt the mutated
package's
dist/(these specifiers resolve throughexportstodist/, with novitest alias back to source, so an unrebuilt ablation would have been a false
green), proved the mutation was live in the artifact with
scripts/ablation-dist-preflight.mjs, then restored under an absolute-pathtrap ... EXIT INT TERMand proved the restore by blob equality againstHEAD,an empty
git diff HEAD, and a--absentpreflight.engine.ts, the manifest-collection registrationmetadata-manager.ts,MetadataManager.list()registry.ts,SchemaRegistry.registerItemLegs 1 and 2 are complements: one producer path moves while the others do not, and
the pin notices the one that moved. That is the property a single-path pin cannot
have. Leg 3 was predicted to red paths 1 and 2 together and did:
registerAppreaches the registry through
registerItem, so there is no cut below that callwhich path 2 reaches and path 1 does not. Paths 1 and 2 are therefore not
separable producers below
registerItem— leg 1, which cuts above it in themanifest loop, is what separates them.
Re-derivation, and one place the card's positive claim was too strong
The item shape was re-derived here rather than inherited. Measured through the
list door on all three paths: the raw document,
targetObjecttop-level, nonested
metadatamember — the card holds.One refinement: the card describes every item as decorated with
_packageId/_provenance/_diagnostics. Measured at this commit, the three paths agree onthe document but not on the decorations —
registerAppstamps_packageIdand
_provenance(it knows the owning package); the directregisterItemandMetadataManagerpaths carry_diagnosticsalone. So "they all look the same" isnot a premise this pin is entitled to, and it does not assert the decorations.
Deliberately not done
GetMetaItemsResponseSchema.itemsis not narrowed fromunknown. That is aspec decision with a separate card and a manual floor for narrowing a published
declaration.
objectuiis touched.rest-server.tsis not touched.Verification
green. Two first returned
exit 3 PREREQUISITE NOT MET(check:dual-build-cjs-loads,check:type-check-debt) — recorded as NOT MEASURED, then the package closure wasbuilt as
lint.ymldoes and both re-run green.pnpm --filter @objectstack/rest typecheck: green — and the edited file wasconfirmed present in the tsc program via
--listFiles(1 hit), so this iscoverage, not adjacency.
pnpm --filter @objectstack/rest exec vitest run: 187 files, 3182 tests, all pass.eslint's own
--format jsonoutput, 0 errors, 0 warnings; the repo's singleeslint.config.mjsenables type-aware linting for no file at all, so this diffcannot move the verdict on any untouched file.
b11efe3d951, this branch's head.Test-only change, publishing nothing from any package: no changeset,
skip-changeset.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Generated by Claude Code