Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): derive discovery version instead of the hardcoded '1.0' literal - #11297
Conversation
….0'` literal (#11235) `getDiscovery()` filled `DiscoverySchema`'s "System Identity" `version` with the constant `'1.0'`, while the other producer of the same field (`HttpDispatcher.getDiscoveryInfo()` in `@objectstack/runtime`) filled it with its own constant `'1.0.0'` until #10993. Two producers disagreeing on one field is what proves neither literal was a contract value. It now resolves an injected `OS_RUNTIME_VERSION` stamp, falling back to this package's own installed version, then `'unknown'` — the same shape #10993 landed, with a package-local resolver because the dependency direction forbids importing runtime's. `tsup.config.ts` gains `shims: true` so the CJS build's `import.meta.url` is real, exactly as `packages/runtime/tsup.config.ts` does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…`shims: true` (#11235) Reverse verification removed `shims: true` and rebuilt: at this target esbuild does not empty `import.meta` in the CJS output, it emits `createRequire(import.meta.url)` verbatim, so `dist/index.cjs` throws `SyntaxError: Cannot use 'import.meta' outside a module` at load time — `require('@objectstack/metadata-protocol')` fails outright rather than degrading to `'unknown'`. Both comments now state what was measured on this package instead of what the sibling package's comment predicts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 02409c23fbcd71478133ce33e511838148edb9de && git checkout 02409c23fbcd71478133ce33e511838148edb9de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 764dbbccbd00bbf61936bf128be524e8d8ad1bed 23d2831b8eec03c365051740a57f72d829fd52f3 && git checkout -B drift-repro 764dbbccbd00bbf61936bf128be524e8d8ad1bed && git merge --no-ff 23d2831b8eec03c365051740a57f72d829fd52f3
node scripts/docs-audit/affected-docs.mjs --json 764dbbccbd00bbf61936bf128be524e8d8ad1bed
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11235
Premise check
Confirmed at
origin/main(1fa05a69e):packages/metadata-protocol/src/protocol.ts:5095carriedversion: '1.0'insidegetDiscovery()'s return. Premise holds for the producer.One part of the card's framing does not survive the check, and it matters — reported rather than quietly worked around. The card describes this producer as "served over REST by
packages/rest/src/rest-server.ts:3196". It is called there, and itsversionis overwritten on the next line:config.api.versiondefaults to'v1'(normalizeConfig()) and is the API version identifier that builds the mounted URL (api.apiPath ?? \${api.basePath}/${api.version}`; spec declares itz.string().default('v1').describe('API version identifier')). So the'1.0'` literal never reached the REST wire, and neither does its replacement.That does not make this card moot, and it is deliberately not fixed here:
getDiscovery()is a declared protocol method (ObjectStackProtocol.getDiscovery(),packages/spec/src/api/protocol.zod.ts:2457) — embedders calling it directly get the producer's own answer, and the schema-conformance gate in this package judges that answer.@objectstack/restoverwrites discoveryversionwithconfig.api.version('v1') — the URL path segment served in the schema's "System Identity" field, masking the producer entirely #11292, with the options and a recommendation.The evidence, which needs no opinion about what
versionshould beDiscoverySchemahas exactly two producers (the spec's own census,packages/spec/src/api/discovery.zod.ts:421). They filled the sameversion: z.string()"System Identity" field with different constants —'1.0.0'inHttpDispatcher.getDiscoveryInfo()(until #10993),'1.0'here. Both literals are therefore provably not meaningful contract values: ifversionwere a contract, two producers would not each invent their own; if it is not, it should not be hardcoded.Chosen shape — matched to what #10993 actually landed, not to the card's wording
Read #11242 (the merged #10993 fix) rather than the card's wording, per dispatch. Same three-step resolution, same env var, same
'unknown'last resort:OS_RUNTIME_VERSION— the operator/build-pipeline stamp. The same variable, not a new one:cloud-connection-plugin.tsalready read it and [finding] /api/v1/health reports a hardcoded version: '1.0.0' — a field that exists and lies, so no consumer can use it for artifact identity #10993 made it/health's source. One stamp, one meaning — a stamped deployment now gets the same answer from both discovery producers and/health, which is the producer disagreement closed at its source. AGENTS.md [WIP] Create a new release version #9'sOS_{DOMAIN}_{NAME}config-value shape.package.jsonversion, viacreateRequire(import.meta.url)('../package.json').'unknown'only if both are unavailable — honest about not knowing rather than a plausible-looking literal.Two deliberate differences from the sibling, each forced:
packages/metadata-protocol/src/discovery-version.ts, not exported from the package index).@objectstack/runtimedepends on@objectstack/metadata-protocol, not the reverse, soresolveRuntimeVersion()cannot be imported here; hoisting a shared helper totypes/corewas declined at triage. NamedresolveDiscoveryVersion()rather than duplicatingresolveRuntimeVersionso it cannot be confused with the sibling or withPROTOCOL_VERSION(the unrelated wire-protocol number inspec/kernel).HttpDispatcher's constructor because the dispatcher is long-lived;getDiscovery()builds a fresh document per call and has no construction moment to freeze against.tsup.config.tsgainsshims: true, exactly aspackages/runtime/tsup.config.tsdid for the sibling.Reverse verification — both legs, on-disk proof, restored by
trapFix committed first (
88fb4b6ce), so every restore is agit checkout HEAD --back to a known state. Each ablation script installs anEXIT INT TERMtrap whose handler restores the mutated file (spelled without angle brackets here on purpose — GitHub's body sanitizer eats short bracketed placeholders, which is how the previous revision of this paragraph rendered as an EMPTY trap handler and read as though the restore were a no-op), and each mutation is proven on disk by anchored counts of the specific text, not by an editor's exit code or a baregit diff --stat.Ablation 1 — restore the
'1.0'literal on the producer. Predicted direction: red.The fourth case stayed green, correctly:
'1.0'is a non-empty string that is not'unknown', which is what that case asserts. It is not an anti-literal pin and does not pretend to be one. No build was needed for this leg — the test imports./index.js, a package-relative specifier vitest resolves tosrc/, so nodist/is in the path.Ablation 2 — drop
shims: trueand rebuild. Predicted direction: the CJS consumer degrades to'unknown'. Observed: worse than predicted, and reported as observed.At this target esbuild does not empty
import.metain the CJS output, it emits it verbatim — so without the shimrequire('@objectstack/metadata-protocol')throws at load time and the package is unimportable from CJS, not merely degraded. Both comments were rewritten to state the measured behaviour rather than the sibling's prediction (23d2831b8). Restore leg rebuilt too, and re-verified — no mutateddist/survived the ablation:Both built formats verified against a real
getDiscovery()callNot a unit test: constructed
ObjectStackProtocolImplementationfrom the builtdist/index.cjsanddist/index.jsand read the served field.Pin design — a property, not a string
packages/metadata-protocol/src/discovery-version.test.ts, four cases, every one driving the realgetDiscovery()(never the resolver in isolation, never source text) so a fix that stops being wired in fails here.The load-bearing case names no expected string at all: it asserts that two different injected stamps produce two different served values. No constant can satisfy that, whichever constant someone picks — which is precisely what a specific-string assertion cannot promise. The value-level cases sit beside it for the ordinary failure (stamp read but mangled), not in place of it, and they name both
'1.0'and'1.0.0'so a restoration of either literal is caught twice.Consumer-by-value survey — with the controls that prove the queries were live
Dispatch asked whether discovery's
versionis consumed by value (version comparison, feature gating, cache key), because that would make this more than a bug fix. I found no such consumer inobjectstack,objectuiorcloud.Controls, because a zero-hit query is not a reading until a known-present one proves it ran:
discovery(Info|Result|Data)?\.version/data.version(repo)apiName→ 5 hitssemveranywhere → 10+ filescacheKey→ 5+ files"version ==="neardiscoveryx-objectstack-build-sha→ 10 hits acrossobjectstackANDcloud, so org search reaches both sibling reposWhat the by-value hits actually are:
packages/adapters/hono/src/hono.test.ts:95pins'1.0'from its own mock ofgetDiscoveryInfo(the other producer — untouched here);packages/client/tests/integration/01-discovery.test.ts:38asserts/^v?\d+/against the REST-served value, which isconfig.api.version('v1') and unaffected;packages/spec/src/api/discovery.test.ts:207is a schema-parse fixture.packages/metadata-core/src/protocol-handshake.tscomparesPROTOCOL_VERSIONagainst a package manifest — a different field entirely.Stated at its real strength: "I found no consumer" is not "there is no consumer." Embedder code outside these three repos is not visible from here.
Tier
Default tier holds, on content: no schema shape change, no new field, no widened export (
discovery-version.tsis package-internal — not re-exported fromsrc/index.ts, mirroringruntime-version.ts). Only where one field's value comes from.packages/specuntouched.Gate verdicts — exit captured before any pipe, each quoted from the gate's own output
Run at
23d2831b8(this PR's head,git status --porcelainempty at run time). The list was re-derived withnode scripts/pm/dispatch-gates.mjsafter the changeset existed, and every path-derived and convention-triggered family it named was run except the one noted at the end.pnpm lint(repo-wideeslint . --no-inline-config) —LINT_EXIT=0. Full farm, no narrowing to declare.pnpm --filter @objectstack/metadata-protocol test—Test Files 138 passed | 2 skipped (140)/Tests 1883 passed | 10 skipped (1893).pnpm --filter @objectstack/metadata-protocol build—ESM ⚡️ Build success/DTS ⚡️ Build success, both formats.pnpm check:nul-bytes—check-nul-bytes: OK (scanned 6336 text file(s) …; no raw ASCII control bytes).pnpm check:cross-package-test-inputs—OK: 14 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.pnpm check:published-files—✓ 69 publishable package(s) of 78 workspace member(s) declare a \files` whitelist …`pnpm check:test-source-alias—check-test-source-alias OK — 72 packages with tests scanned; …pnpm check:where-matcher—✓ where-matcher conformance holds: 282 matcher(s) discovered … baseline key set verified against 1fa05a6: no files added.pnpm check:query-options-erasure—✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new … no files added.pnpm check:engine-double-contract—OK — 384 pinned, 133 in the DEBT ledger, 2 exempt.pnpm check:type-check-coverage— exit 0 (structural layer).node scripts/check-empty-changeset.mjs—✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).node scripts/check-changeset-no-major.mjs—✓ This diff introduces no \major` bump.`node scripts/check-adr-0087-registration.mjs—✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).check:durability-log-level,check:filter-alias-parity,check:slot-lookup,check:type-source-resolution,check:objectui-changeset,check:changeset-gate-self-tests,scripts/check-ci-filter-parity.mjs,scripts/check-plugin-teardown-shape.mjs,scripts/docs-audit/check-affected-docs.mjs.One declared narrowing.
pnpm check:type-check-debt(--re-measure) requires the whole workspace build closure and re-runs tsc across all 33 ledger entries; that is CI's run. What it would judge about this diff was measured directly instead:tsc --noEmit -p packages/metadata-protocol/tsconfig.jsonreports 63 errors — exactly this package's recorded ledger number (errors: 63inscripts/check-type-check-coverage.mjs) — with 0 attributed tosrc/discovery-version.ts, 0 tosrc/discovery-version.test.tsand 0 tosrc/protocol.ts. The count landing on the ledger figure is the before/after comparison; the ratchet cannot move.Out of scope, filed not fixed
@objectstack/restoverwrites discoveryversionwithconfig.api.version('v1') — the URL path segment served in the schema's "System Identity" field, masking the producer entirely #11292 —@objectstack/restoverwrites discoveryversionwithconfig.api.version('v1'), serving the URL path segment in the "System Identity" field and masking both producers on the wire. The wire-visible half of this defect family; carries the options and a recommendation.versionvalue ("1.0.0") that no producer has ever served #11295 —content/docs/api/client-sdk.mdx:90documentsdiscovery.versionas"1.0.0", a value no producer has ever served.No fourth
DiscoverySchemaproducer exists: the spec's own census names exactly two, and the only non-test caller ofgetDiscovery()in the tree isrest-server.ts.Generated by Claude Code