Uh oh!
There was an error while loading. Please reload this page.
fix(cli): ask the engine for family 3 of the sys_secret reference union, and re-scope the declared gap (#12804) - #12872
Conversation
…on (#12804) The cross-producer reference union's family 3 was pure over the artefacts its caller supplied. #12758 landed the producer half — registerDatasourceDef retains external.credentialsRef and ObjectQL.listDatasourceDefs() reads it back — so the engine can answer; the union never asked. Measured before the change: a code-registered datasource with a bound handle plus declaredDatasources: [] produced a union reporting complete: true while omitting that live handle. Family 3 now assembles from three sources — persisted sys_metadata rows, the definitions the engine holds, and the host's declared list — as a union, not a replacement, because neither code-side source dominates the other. The declared gap is re-scoped, not removed: declaredDatasources: undefined still refuses the whole union (a datasource declared in code and never registered reaches neither source), and an engine slice that cannot list its definitions now gaps the family rather than contributing an empty answer. listDatasourceDefs is an OPTIONAL port member, so every slice that satisfied SecretReferenceEngineLike before still does. The three prose sites #12758 falsified are rewritten, not trimmed: the retired mechanism was "the engine drops credentialsRef"; the live one is "the engine's index covers only what was registered, so the residue is invisible until the host is asked". A test pins that the operator-facing gap message carries the new mechanism and not the old. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 23 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 50e1b4fc6f02ca57ed93f5df567628f3d315bb7e && git checkout 50e1b4fc6f02ca57ed93f5df567628f3d315bb7e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b391fad3a40591a14e40aae466f25c86b9f9ebf0 4ddd509ae0c937c28ce023285dcf78752aecf7b8 && git checkout -B drift-repro b391fad3a40591a14e40aae466f25c86b9f9ebf0 && git merge --no-ff 4ddd509ae0c937c28ce023285dcf78752aecf7b8
node scripts/docs-audit/affected-docs.mjs --json b391fad3a40591a14e40aae466f25c86b9f9ebf0 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12804
Family 3 of the cross-producer
sys_secretreference union now asks the engine, and the declared gap is re-scoped rather than removed.Reproduced first, on real code, before any edit
A scratch vitest file drove both sides against a real
ObjectQL, the realLocalCryptoProviderand the real datasource credential binder (deleted before this PR; its two legs are now permanent pins):registerDatasourceDefcame back fromengine.listDatasourceDefs()carrying itsexternal.credentialsRef;declaredDatasources: [](the host stating it has none),collectSecretReferenceUnionreturnedcomplete: truewhilehandleIdsdid not contain that live handle.A union that calls itself complete while omitting a live credential is precisely the precondition failure #8103's deletion predicate rests on. After the change the same reproduction flips exactly one assertion —
handleIds.has(codeHandle)goesfalsetotrue— withcompleteunchanged.The shape: a union of both sources, not a replacement
Family 3 assembles from three sources, none dominating:
sys_metadatarowslistDatasourceDefs())declaredDatasourcesReferences are de-duplicated on the exact handle-plus-holder pair, which is information-preserving. Two sources disagreeing about one datasource keep both handles: dropping either is the under-report that deletes live credentials.
The guarantee is strengthened, not removed
The falsification criterion was: after the change there must still exist an input shape that makes the union refuse rather than return a silent empty answer. Measured — there are now two, both pinned:
declaredDatasources: undefinedstill refuses the whole union. The residue it covers is still unreachable: a datasource declared in code that nothing ever registered reaches neithersys_metadatanor the engine's index.[]means "there are none".SecretReferenceEngineLikegainslistDatasourceDefsas an optional member, so every slice that satisfied the port before still satisfies it. Both structural devices #12663 shipped still hold: the family set is closed and the result container is keyed on it (omitting a family is a type error), and each family's result still distinguishes "enumerated" from a declared gap.The three falsified prose sites: the REASON is rewritten, not the clause deleted
The retired mechanism was "the engine drops
credentialsRef". The live one is "the engine's index covers only what was registered, so the residue is invisible until the host is asked" — same conclusion, different mechanism, and the next reader judges by mechanism. All three sites carry the new mechanism, including the operator-facing gap message an incident responder reads to understand why a union was refused. A test pins that the message states the live mechanism and does not carry the retired one.Two ablations for family 3, because it now has two sources
Each half was ablated separately on the committed tree, the mutation confirmed on disk by anchored count and blob hash before any verdict was read, and restored with
git checkout HEAD --on an absolute path under anEXIT INT TERMtrap, proven restored by blob hash and an emptygit diff HEAD. The module under test is imported by relative path inside its own package and noresolve.aliasentry touches it, so vitest runs the source — no rebuild leg applies.family 3 (engine half) - names a handle held ONLY by an engine-registered datasource definitionred; the host-half pin stayed greenfamily 3 (host half) - names a handle held ONLY by the host-declared listred; the engine-half pin stayed greenEach half's pin reds alone, so neither half is covering for the other.
Verification
Run on the final commit
4ddd509ae:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/secret-reference-union.test.ts—Test Files 1 passed (1) / Tests 30 passed (30)(20 pre-existing pins plus 10 new).pnpm --filter @objectstack/cli typecheck— exit 0. The new type-level pin is really evaluated:tsc --noEmit --listFileslists both the module and its test file in the program (this package'stsconfig.jsonincludessrcwith no test exclusion, unlike itstsconfig.build.json).pnpm lint— the whole-repoeslint . --no-inline-config, exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, all green:check:nul-bytes,check:changeset-gate-self-tests,check:objectui-changeset,check:pm-half-states,check:objectql-double-limit,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:published-files,check:page-declaration-shape,check:slot-lookup,check:comment-mask-adoption,check:plugin-teardown-shape,check:type-check-coverage,check:i18n(OK (9 package(s) - all bundles in sync)), plus the changeset/ADR scripts.check:i18n-coverageprintedCOULD NOT MEASURE— one of twelve configs fails to lint because@objectstack/connector-mcphas no build output in this worktree, unrelated to this change.check:type-check-debtneeds the whole workspace closure built; that build exceeded this container's foreground limit. Both are left to CI.@objectstack/cli's ownTEST_DEBTentry measures the siblingtest/tree, which this PR does not touch; the test file it does touch lives undersrcand is inside the typechecked program.Contract scope
SecretReferenceUnionInputis an exported CLI surface, so the contract fence was tested rather than assumed. No symbol of this module appears inpackages/cli/src/index.ts(all 19 exported names scored zero, against a positive control that findsCompileCommand,ValidateCommandandisProcessEntry), and nothing outside@objectstack/cliimports it — the only out-of-package hits for its names are prose lines in an existing changeset. The port change is a widening: an optional member added, no signature narrowed.Generated by Claude Code