fix(datasource,runtime): declare the guarded optional-driver loads as optional peers - #13401
Conversation
… optional peers (#12943) Five guarded `await import(...)` loads of workspace driver packages sat in published `src/**` with no manifest declaration an installing consumer could see. `@objectstack/service-datasource` reaches driver-turso, driver-sqlite-wasm (two sites) and driver-mongodb; `@objectstack/runtime` reaches driver-turso. Two of them were devDependencies, which tells a consumer nothing; driver-turso was in no section of either manifest. Each is now an optional `peerDependencies` entry plus `peerDependenciesMeta: { optional: true }` — the form `@objectstack/cli` already uses for driver-turso. Nothing is installed and no code path changes: an optional peer declares a relationship that already existed at runtime. The `rest` to `objectql` occurrence of the same shape is deliberately left alone; rest's non-coupling to the data engine is a stated architectural position, not a hygiene gap. pnpm links an optional workspace peer, so three test pins that had reached their missing-package arm with no stub stop doing so. Each had said in advance what to do about it. All three now stage the absence with `vi.doMock` and keep every assertion, including the typed-identity ones behind serve.ts's `e instanceof MissingDriverPackageError` boot-fatality branch. The five `optional-runtime-probe` ledger rows the declarations make stale are deleted, as that gate demands and only as far as it demands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. 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 cced02902f279daee5ca98d8e6c85d9dd089526b && git checkout cced02902f279daee5ca98d8e6c85d9dd089526b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a68c61267f9ae45b43974d9a3992952067c26a3f 69c27808a2c3501ab887446393936fb0fee650d8 && git checkout -B drift-repro a68c61267f9ae45b43974d9a3992952067c26a3f && git merge --no-ff 69c27808a2c3501ab887446393936fb0fee650d8
node scripts/docs-audit/affected-docs.mjs --json a68c61267f9ae45b43974d9a3992952067c26a3f |
Fixes #12943
Implements the maintainer's ruling of 2026-08-29 (option 甲, verbatim 「同意」) as one PR across
service-datasourceandruntime, exactly as that ruling directs.What changed
Five guarded
await import(...)loads of workspace driver packages sat in publishedsrc/**with no manifest declaration an installing consumer could see. Each is now an optionalpeerDependenciesentry pluspeerDependenciesMeta: { optional: true }— the form@objectstack/clialready uses fordriver-turso:@objectstack/service-datasourcedriver-turso,driver-sqlite-wasm,driver-mongodb@objectstack/runtimedriver-tursoNothing is installed and no code path changes: an optional peer declares a relationship that already existed at runtime, so
npm ls, a lockfile, an audit tool and a reader of the manifest can all see the driver a datasource may ask for, instead of learning about it only by hitting the failure arm.driver-mongodbanddriver-sqlite-wasmweredevDependenciesofservice-datasource, which tells a consumer nothing;driver-tursowas in no section of either manifest.⛔ The sixth occurrence of the same shape (
resttoobjectql, degrading to501 NOT_IMPLEMENTED) is deliberately untouched: rest's non-coupling to the data engine is a stated architectural position, not a hygiene gap.The five
optional-runtime-proberows the declarations make stale are deleted fromscripts/check-undeclared-dep-imports.mjs— as that gate demands ("once the manifest declares it, the finding is gone and the row is stale, which is RED") and only as far as it demands. ⛔ No row was relaxed to quiet the gate. Of the ledger's 7 rows, 2 remain:resttoobjectql, and the unrelatedtype-onlyrow.The pin conversions, and a population larger than the card's
pnpm links an optional workspace peer, so a pin that reached its missing-package arm because the package did not resolve stops doing so. The card named one such pin. Measured: there are three, and two of them are in
packages/runtime:service-datasource/src/__tests__/default-datasource-driver-factory.test.tsruntime/src/turso-driver-factory.convergence.test.tsruntime.runtime/src/standalone-stack.libsql.test.ts⛔ None is deleted or weakened. All three now stage the absence with
vi.doMockand the resolver's ownERR_MODULE_NOT_FOUND, keeping every assertion — including the typed-identity ones behindserve.ts'se instanceof MissingDriverPackageErrorboot-fatality branch.⭐
vi.doMockwithoutvi.resetModules(), deliberately. A reset re-evaluatesmissing-driver-package-error.jsalongside the factory, so the arm raises a different class object andinstanceofis false for a perfectly correct error — measured, in an earlier form of this branch, asexpected MissingDriverPackageError … to be an instance of MissingDriverPackageError. Without the reset the identity assertions stay pointed at the binding the host actually holds, which is the only reason they are worth asserting. The two#7385sibling arms keep using the reset-basedraiseWithPackageAbsenthelper, untouched — measured, they never relied on the accident, because both their packages already resolve here.Prose in five places stated the opposite of the new manifests (e.g. *"is not declared by
@objectstack/runtimeat all"*) and is corrected inruntime/src/turso-driver-factory.ts,service-datasource/src/default-datasource-driver-factory.tsand one comment incli/src/utils/storage-driver.test.ts`.Verification
Gate family derived at edit time with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(11 paths, never a copied list). Exit codes captured before any pipe. Union re-run on the final commit69c27808a.pnpm lint(eslint . --no-inline-config), not a narrowed scan: exit 0.undeclared-dep-imports(+--self-test),nul-bytes,agent-test-spelling,bash32-floor,changeset-gate-self-tests,cli-command-ids,cross-package-test-inputs,doc-authoring,entry-guard,i18n,i18n-coverage,llms-txt,logger-receiver-detach,objectql-double-limit,objectui-changeset,override-consistency,page-declaration-shape,parse-guard,pm-half-states,pnpm-filter-targets,published-files,slot-lookup,test-source-alias,type-source-resolution,watch-hint-literal,where-matcher,query-options-erasure,engine-double-contract,type-check-coverage,type-check-debt,pm-dispatch-gates,empty-changeset,dual-build-cjs-loads.i18n/i18n-coverage/type-check-debtfirst answered PREREQUISITE NOT MET ("Nothing was checked") on an unbuilt tree — recorded as NOT MEASURED, never as red — and are green above only after the full closure buildturbo run build --filter=./packages/* --filter=./packages/*/*.✓ check:undeclared-dep-imports: 78 workspace packages …, 2 ledger row(s), all evidence intact.andcheck-type-check-coverage --re-measure: OK — 30 ledger entr(ies) re-measured, 1558 raw tsc error(s) total, none above its recorded number.⛔ NoDEBT/TEST_DEBTentry was added or raised.service-datasource585/585 in 27 files;runtime2968/2968 in 201 files;clistorage-driver.test.ts36/36. Typechecks:service-datasource,runtime,cliall exit 0.runtime'stsconfig.jsonexcludes**/*.test.ts, sort-typecheckgreen says nothing about the two runtime test files edited here (--listFileshits: 0). Measured separately with a temporary project that includes them: both files are in the program (hits: 1 each) and zero errors are attributed to either; the 217 errors that program reports are pre-existing residue inruntime's other excluded test files, which is why the exclusion exists and is already ledgered. The probe config was removed and its removal proven (git statusclean). Forclionly the one edited file was run, its change being a comment.Reverse verification
Predicted direction, declared before running: with the five rows deleted, removing a declaration can no longer produce a stale-row red — it must produce an unledgered-finding red. Observed exactly that. Dropping
driver-tursofromservice-datasource's peers:The complementary direction was measured too, before the rows were cut: with the declarations landed and the rows still present, the gate named all five as stale and refused. So the deletion is a measured requirement, not an inference.
Ablation — proof the stub still ENTERS the arm (the failure mode that turns a real test into a decoration). Mutating the turso catch in
default-datasource-driver-factory.tsto rethrow the raw resolver error turned exactly one test red — the converted pin — onexpected '[vitest] There was an error when mock…' to contain 'npm install @objectstack/driver-turso', with the other 49 still passing. Resolution path stated: the test imports its subject by a relative specifier inside the same package, so vitest readssrc/*.tsand no rebuild leg applies. Mutation proven on disk (marker count 1, blob7470571cto3d275d2b); restore proven by state (blob back to7470571c, marker count 0,git diff HEADempty,git statusclean), under anEXIT INT TERMtrap with absolute paths.Measured readings that differ from the card
auto-install-peersinstalls missing non-optional peers.pnpm installsays otherwise:pnpm-lock.yamlgainslink:../../drivers/driver-tursounder both importers, and the symlinks exist. Triage had flagged that it never reproduced this; it reproduces.#7385siblings do not rely on the accident — they have always staged absence, because their packages already resolve here. The extra work was inpackages/runtimeinstead.runtime's occurrence is the same class: a guarded dynamicimport()of an optional package answering with the same typed error. Only the thunk replacement is host-specific.LEDGERholds 7 rows, 6 of kindoptional-runtime-probe(controls: a nonsense kind returns 0;type-onlyreturns its 1 row). Eleven is the count of text occurrences of the string, which includes the header,LEDGER_KINDS, two diagnostics and a self-test fixture.One consequence recorded rather than hidden
Declaring the peer adds a workspace edge
service-datasourcetodriver-turso, anddriver-tursodevDepends on@objectstack/verifytoruntimetoservice-datasource— a cycle. It is invisible to the real build path: turbo's package graph does not readpeerDependencies(measured — the 20-task graph forservice-datasource#buildcontains nodriver-turso), sopnpm buildand CI are unaffected. It is visible topnpm --filter 'PKG^...', which does read peer edges and now selects a cyclic superset with no topological order. Building through turbo is the workaround used here. Flagged for review rather than decided.Generated by Claude Code