refactor(cli): drop the dead { records } normalizer from os secret orphans, and make the driver port state the array contract - #15093
Conversation
… orphans`
`os secret orphans` wrapped both of its driver reads in a local `rowsOf()` that
unwrapped `{ data: [...] }`, lifted a bare row into `[row]` and filtered out
non-object entries. Every limb was unreachable.
The card that asked for this refused to let unreachability be inferred from
`IDataDriver.find`'s declaration, because the counter-case is real: the
console's `ObjectStackAdapter.find()` resolves to a normalized `QueryResult`
envelope and never to an array. So the concrete driver was read instead.
`secretDriver` resolves through `ObjectQL.getDriverForObject('sys_secret')`,
which hands back a registered driver instance unwrapped; the five
`IDataDriver` implementations in this tree — `SqlDriver` (and
`SqliteWasmDriver`, which extends it without overriding `find`), `TursoDriver`
local and remote, `MongoDBDriver`, `InMemoryDriver` — return an array on every
path they can return on, `[]` included. Driven for real, the CLI's own boot
resolves `com.objectstack.driver.sql` for both objects and both reads answer a
bare array holding the seeded rows.
`SecretReferenceDriverLike.find` is narrowed from `Promise<unknown>` to
`Promise<Record<string, unknown>[]>` in the same change. That port is where the
normalizer came from: its own doc comment already said it matched
`IDataDriver.find`, and the return type said otherwise, so every caller that
could not see an array in the type wrote its own answer for shapes no producer
emits. The port now states the contract it claimed to state. The union's three
reads keep their explicit `let result: unknown` locals and are untouched.
Behaviour is unchanged for any driver that keeps the contract. One that does
not now fails loudly rather than having a row silently dropped — and a
`sys_secret` row dropped from this read is a row dropped from the report, which
this command exists to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 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 665045de06845c96bed98a98e158849a50a83289 && git checkout 665045de06845c96bed98a98e158849a50a83289
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6665c5c916caf6d95022734328ae82154e1a5f10 d4a66d4bd22633401a2b6ec52bfe14fc33b117e3 && git checkout -B drift-repro 6665c5c916caf6d95022734328ae82154e1a5f10 && git merge --no-ff d4a66d4bd22633401a2b6ec52bfe14fc33b117e3
node scripts/docs-audit/affected-docs.mjs --json 6665c5c916caf6d95022734328ae82154e1a5f10
|
Fixes #14843
os secret orphanswrapped both of its driver reads in a localrowsOf()that unwrapped adataenvelope, lifted a bare row into a one-element list, and filtered out non-object entries. Every limb was unreachable. It is removed, and both call sites now read the arrays their driver returns.The card's caveat was the whole job, so the concrete driver was read
Triage made the filing's own caveat binding and refused to let the removal follow from
IDataDriver.findbeing declared to resolve to an array of string-keyed unknown records — because the counter-case is real. The console'sObjectStackAdapter.find()resolves to a normalizedQueryResultenvelope and never to an array. Two methods spelledfind, opposite answers. SosecretDriverwas resolved to its concrete implementation instead of inferred.Static resolution.
stack.kernel.getService('objectql')is theObjectQLengine;getDriverForObject(name)callsgetDriver(), which returns the instance held in the engine'sdriversmap with nothing interposed (registerDriverstores the instance as given).sys_secretdeclares nodatasourcebinding, so it resolves to the default driver, which the standalone stack builds throughcreateDefaultDatasourceDriverFactory. That factory can build exactly five things, and they are everyIDataDriverimplementation in this tree:findSqlDriversql-driver.ts:5611, delegating tofindRowsSqliteWasmDriverSqlDriverwith nofindoverrideTursoDriverturso-driver.ts:644formatRemoteRows(...)overRemoteTransport.find, whose every return is amapRows(...)map or an empty array; local:super.findMongoDBDrivermongodb-driver.ts:333InMemoryDrivermemory-driver.ts:556results, an array from its first assignment through every branchRuntime reading, which is what the card actually asked for.
orphans.driver-contract.test.tsboots the stack this command boots —bootSchemaStackwith the command's ownextraPluginslist — against a real sqlite file, and reads the seam:getDriverForObject('sys_secret')andgetDriverForObject('sys_setting')both resolvecom.objectstack.driver.sql, aSqlDriver;Array.isArraycheck alone is satisfied by a driver that answers an empty array to everything, which is the reading that would make this removal look safe while the command silently reported nothing;dataproperty and arecordsproperty are asserted absent on the value itself;if (!result)limb.⇒ The premise holds. The limb was dead.
The second file, declared
Beyond
orphans.tsand its test, this PR changes one line plus its doc comment inpackages/cli/src/utils/secret-reference-union.ts:SecretReferenceDriverLike.findis narrowed from a Promise ofunknownto a Promise of an array of string-keyed unknown records.That port is where the normalizer came from, and it is the contract-first half of this change rather than a convenience. The sentence directly above that declaration already read "Matches
IDataDriver.find" — which declares an array — so the declaration and its own comment disagreed, and the comment was the correct one. A caller that cannot see an array in the type writes its own answer for envelope shapes no producer emits, and that is exactly whatorphans.tsdid. Fixing it at the consumer instead (a cast, or a restated local port) would have kept the drift and added a second de-facto spelling of one contract.Blast radius, measured: the union's own three reads each declare an explicit
unknownlocal for the result and are untouched; the port's other implementors are the realObjectQL(throughgetDriverForObject, already anIDataDriver) and the two hand-built engine slices insecret-reference-union.test.tsandsys-secret-orphan-sweep.test.ts, both of which delegate to the real engine. All compile and pass unchanged.Nothing else is touched. ⛔
scripts/check-react-page-adapter-contract.mjsand its population are untouched, per triage's fence.Both call sites, before and after
Reverse verification from the committed state, both source files restored from
origin/mainfor the BEFORE leg, with the mutation confirmed on disk by anchored greps in both directions before anything was measured, and the restore confirmed by blob hash againstHEADplus an emptygit diff HEAD:The end-to-end half is inside those 16: the command is run for real, with
--json, against the same database, and the report is checked for a value that could only have travelled through each of the two formerrowsOfcall sites —counts.totaland the seeded row id from thesys_secretread, andlegacyInlineRowsfrom thesys_settingread, which is derived fromsettingRowsand from nothing else. Identical on both legs.Behaviour
Unchanged for any driver that keeps the contract. What changes is what happens if one ever does not: the command fails loudly instead of dropping the row. That is the right direction here — a
sys_secretrow dropped from this read is a row dropped from the report, and this command's whole safety property is that no row goes unmentioned. A driver answering something other than an array is a contract violation to fix at that driver, not to absorb at a consumer.Verification
At
d4a66d4bd2(final HEAD), clean tree:pnpm --filter @objectstack/cli typecheck— exit 0. That istsc --noEmitpluscheck:test-typecheck, which reported "OK — @objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json; 3 file(s) / 28 error(s) / 6 pinned signature(s) held", unchanged. The new test lives undersrc/, so it is inside the build config'ssrcinclude and really is typechecked — the siblingtest/tree that config never reaches is a separate layer, covered by the second command.pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/secret/ src/utils/secret-reference-union.test.ts src/utils/sys-secret-orphan-sweep.test.ts— Test Files 4 passed (4), Tests 62 passed (62).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackatd4a66d4bd2on a clean tree: 42 families. The ones this diff actually moves were run locally and are green, each exit code captured after a redirect and never across a pipe:check:nul-bytes(8199 files, no raw control bytes),check:engine-double-contract,check:where-matcher,check:objectql-double-limit,check:test-source-alias,check:cross-package-test-inputs,check-undeclared-dep-imports,check:published-files,check:changeset-gate-self-tests,check-empty-changeset,check-changeset-no-major,check-adr-0087-registration,check:objectui-changeset,check:dispatcher-error-vocabulary,check:query-options-erasure,check:type-check-coverage,check-keyed-text-bounds,check-closing-keyword-parity,check-comment-mask-adoption, andcheck:i18nafter building its declared prerequisite closure ("OK — 9 package(s), all bundles in sync, no undeclared authoring keys"). The rest of the 42 is CI's farm.check:i18n-coverageexits 3 — COULD NOT MEASURE: 1 of its 13 configs (examples/app-showcase/objectstack.config.ts) could not be linted because@objectstack/connector-mcphas no build output in this worktree, and the gate refuses to judge its ratchet on a partial round. That is a worktree build-state gap in a package this diff does not touch, and clearing it needs a build across the example apps, which is CI's to run.scripts/pm/check-half-states.mjsexits 3 — PREREQUISITE NOT MET in this container (it cannot reach the trigger-file index). Reported as NOT MEASURED, not as a pass; it is a PM-board gate and reads nothing this diff changes.parserOptions.projectandprojectServiceoccur zero times ineslint.config.mjs, and the config's own header records the positive-control measurement for that), so a per-file run is a complete reading for those files and this diff cannot move the verdict on a file it does not touch.pnpm exec eslint --no-inline-config --format jsonover the three changed TypeScript files: 3 of 3 files linted — an out-of-population file would have come back as an ignore warning instead — with 0 errors, 0 warnings, exit 0. The repo-widepnpm lintis CI's run; this narrowing is declared rather than silently taken.scripts/pm/os-verify-lock.sh, with verdicts read off its VERDICT line rather than a bare shell status.Out of scope, filed rather than ridden in
#15092 records a THIRD instance of the same class, found while doing this:
rowsOfinpackages/cli/src/utils/secret-reference-union.ts:295, over three driver reads. It is deliberately not fixed here — it is a different judgement (that module's contract is that a read which did not happen is a gap, and itsfilterwould silently drop a malformed row rather than gap the family, a defect pointing the other way), and triage fenced this card to one file.It is also the evidence #14460's ruling said would be needed before the
domain:devxgate-population card could be argued: three instances now, in three files and three kinds of caller — an app-showcase job handler, a CLI command, and a CLI utility module — all under a gate whose population is app-showcase page modules pluscontent/docsreact-page samples. ⛔ No gate change is proposed here or there; that call belongs to thedomain:devxcard, and #15092 is unassigned and ungraded.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code