Skip to content

fix(cli): ask the engine for family 3 of the sys_secret reference union, and re-scope the declared gap (#12804) - #12872

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-12804-union-self-serves-family-3
Aug 28, 2026
Merged

fix(cli): ask the engine for family 3 of the sys_secret reference union, and re-scope the declared gap (#12804)#12872
os-litant merged 1 commit into
mainfrom
claude/issue-12804-union-self-serves-family-3

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12804

Family 3 of the cross-producer sys_secret reference 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 real LocalCryptoProvider and the real datasource credential binder (deleted before this PR; its two legs are now permanent pins):

  • the engine answers — a datasource bound by the real binder and registered with registerDatasourceDef came back from engine.listDatasourceDefs() carrying its external.credentialsRef;
  • the union did not ask — with declaredDatasources: [] (the host stating it has none), collectSecretReferenceUnion returned complete: true while handleIds did 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) goes false to true — with complete unchanged.

The shape: a union of both sources, not a replacement

Family 3 assembles from three sources, none dominating:

sourceblind spot
persisted sys_metadata rowsa datasource declared in code never reaches it
the definitions the engine holds (listDatasourceDefs())the engine indexes only what was registered on this runtime
the host's declaredDatasourcescan omit a datasource a package manifest installed behind the host's back

References 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: undefined still refuses the whole union. The residue it covers is still unreachable: a datasource declared in code that nothing ever registered reaches neither sys_metadata nor the engine's index.
  • an engine slice that cannot list its definitions also refuses now, where before it would have contributed a silent empty answer. Symmetric with the host: absence means "nobody answered", and [] means "there are none".

SecretReferenceEngineLike gains listDatasourceDefs as 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 an EXIT INT TERM trap, proven restored by blob hash and an empty git diff HEAD. The module under test is imported by relative path inside its own package and no resolve.alias entry touches it, so vitest runs the source — no rebuild leg applies.

ablationresult
drop the engine half from the assemblerfamily 3 (engine half) - names a handle held ONLY by an engine-registered datasource definitionred; the host-half pin stayed green
drop the host half from the assemblerfamily 3 (host half) - names a handle held ONLY by the host-declared listred; the engine-half pin stayed green

Each 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.tsTest 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 --listFiles lists both the module and its test file in the program (this package's tsconfig.json includes src with no test exclusion, unlike its tsconfig.build.json).
  • pnpm lint — the whole-repo eslint . --no-inline-config, exit 0.
  • Gate families re-derived from the real change set with 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.
  • Not measured, not green:check:i18n-coverage printed COULD NOT MEASURE — one of twelve configs fails to lint because @objectstack/connector-mcp has no build output in this worktree, unrelated to this change. check:type-check-debt needs the whole workspace closure built; that build exceeded this container's foreground limit. Both are left to CI. @objectstack/cli's own TEST_DEBT entry measures the sibling test/ tree, which this PR does not touch; the test file it does touch lives under src and is inside the typechecked program.

Contract scope

SecretReferenceUnionInput is an exported CLI surface, so the contract fence was tested rather than assumed. No symbol of this module appears in packages/cli/src/index.ts (all 19 exported names scored zero, against a positive control that finds CompileCommand, ValidateCommand and isProcessEntry), and nothing outside @objectstack/cli imports 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b391fad3a40591a14e40aae466f25c86b9f9ebf0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 50e1b4fc6f02ca57ed93f5df567628f3d315bb7e — the merge of head 4ddd509ae0c937c28ce023285dcf78752aecf7b8 into base b391fad3a40591a14e40aae466f25c86b9f9ebf0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 28, 2026
@os-litant
os-litant marked this pull request as ready for review August 28, 2026 06:30
@os-litant
os-litant added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit 2082180Aug 28, 2026
34 checks passed
@os-litant
os-litant deleted the claude/issue-12804-union-self-serves-family-3 branch August 28, 2026 07:32
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire the sys_secret reference union to self-serve family 3 from ObjectQL.listDatasourceDefs(), and correct the three prose sites #12758 falsified

2 participants

@os-litant@claude