Uh oh!
There was an error while loading. Please reload this page.
feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434
Conversation
…mask driver-memory's published update/upsert types (#13878) IDataDriver.update() gains '| null' with a docblock that says when it is returned, reusing findOne's shape and delete's not-found vocabulary. InMemoryDriver.update()/upsert() carry explicit return types so the published .d.ts stops reading Promise<any>; upsert asserts the arm it can never take instead of widening its door. A type-level pin holds both the contract and the driver; the landed behaviour pin is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift CheckThis PR changes 3 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 — 132 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 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 && git checkout 23f76fc62cd45d8ac86fda6b6199304032a9e3f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a98b61b3ef695431db26097a28ae8e5f1dec8fdf 959fc96ec647d18db5656d6cdb12ff3b3c7988fe && git checkout -B drift-repro a98b61b3ef695431db26097a28ae8e5f1dec8fdf && git merge --no-ff 959fc96ec647d18db5656d6cdb12ff3b3c7988fe
node scripts/docs-audit/affected-docs.mjs --json a98b61b3ef695431db26097a28ae8e5f1dec8fdf
|
…nd carry BREAKING + ADR-0087 dispositions on the changesets (#13878 patch round) DriverInterfaceSchema.update outputs the same .nullable() record findOne already does, with the docblock sentence the TS interface carries. Both changesets keep minor and add the BREAKING banner plus the no-migration-prescription disposition; the driver-memory one names the unreachable-arm assertion in upsert(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
os-musk
commented
Sep 2, 2026
Landing provenance — engine execution seat (session Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head Flip pre-checks on head Action: Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…ers to what was measured Contract-review round on PR #14914. Changeset: `patch`/`patch` understated a published TYPE-surface narrowing plus a runtime behaviour change on two published drivers. Now `minor`/`minor` with a `**BREAKING**` sentence naming what breaks for TypeScript consumers, and exactly one ADR-0087 disposition -- `not-required (no-migration-prescription)`, the same shape and reasoning as `.changeset/driver-memory-update-upsert-honest-types.md` (PR #14434) one day earlier in this series. `type-surface-only` is not claimable: its predicate 4 (narrowed-from-erased) is false, and runtime behaviour moves too. `mongodb-update-missing-id.test.ts`: the header claimed a type-level pin here would be "never checked by anything". False -- `pnpm check:type-check-debt` re-measures this package with its tests un-hidden, which is exactly how CI caught the three TS18047 the widened declaration introduced. The section now states both programs, names the tsconfig exclusion as the filed defect (#14917), and gives the real reason the pin lives in the turso twin instead. Its reverse-verification paragraph now reports the OBSERVED leg (2 failed | 2 passed (4), all four ran) rather than predicting a compile-time red for a type pin this file does not have. `turso-update-missing-id.test.ts`: same correction. Restoring the fabricating EXPRESSION leaves the declaration untouched, so the `Equals` const cannot red and nothing fails at compile time; the parity pin is one assertion, not two halves; and the no-fabrication pin, omitted before, does red. The paragraph now names all five reds and all five greens from the run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Fixes#13878
Executes the director's ruling A on #13878 (comment 5494524403, maintainer batch #24 「同意」). Patch round after the in-seat contract review (comment 5505756612, REWORK → three changes, all landed in
7db94d680; head959fc96ecafter mergingorigin/main). Generic type arguments are written in SQUARE brackets throughout (the body sanitizer eats the angle-bracket spelling, backticks included).Clause-② self-reading (from the diff)
Yes.
packages/spec/src/contracts/data-driver.tschanges a declared return type on the publicIDataDrivercontract — apackages/spec/src/contractsdeclaration change (ruling item 6). Re-read on the patch head959fc96ec: still yes — the patch adds the same member's Zod mirror inpackages/spec/src/data/driver.zod.tsand changes no other contract member. Draft, labelledneeds:contract-review; the director seat named itself reviewer. No governed surface is touched.The declaration — before / after
IDataDriver.update():Promise[Record[string, unknown]]Promise[Record[string, unknown] | null], with a docblock@returnssentence: the updated record, ornullif no record with that id exists — the same "addressed one row, which may not exist" shape asfindOne, the not-found vocabularydeletealready carries (false); a driver configured to throw on missing records (strictMode) throws instead.The Zod mirror (patch round, review required change 1, ruled in-seat as part of ruling item 1):
DriverInterfaceSchema.updateinpackages/spec/src/data/driver.zod.tsnow outputsz.promise(z.record(z.string(), z.unknown()).nullable())— exactly the spellingfindOnealready uses on the same schema — and its docblock says "The updated record, ornullif no record with that id exists (a driver configured to throw on missing records throws instead)". Reached dist: the emitted runtime chunk carries.output(z.promise(z.record(z.string(), z.unknown()).nullable())).describe("Update record")verbatim, and the interface's.d.tschunk counts 2ZodPromise[ZodNullable[ZodRecord[...]]]outputs (findOne + update) where it counted 1 before.check:generatedon the rebuilt dist:All 15 generated artifacts are up to date—check:api-surface,check:authorable-surface,check:docsand the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.Zero new vocabulary;
updatewas the only by-id door without a not-found arm.The mask — root measured, minimal repair chosen
Measured on the committed tree at
79b6a22a5with a self-controlling type probe (IsAnyonInMemoryDriver['update' | 'upsert' | 'delete'],deleteas the positive control, removed under a trap, removal proven by emptygit status):src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'.— the control fired ondelete();update()andupsert()compiled clean ⇒ both resolve toany. Published:dist/index.d.ts:195-196readupdate(...): Promise[any]/upsert(...): Promise[any].private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) butTinfers a too-narrow literal{ id: unknown; created_at: unknown; updated_at: string }— the spread{ ...table[index], ...data, id, created_at, updated_at }drops the index signature at the final object literal — and cascades 19 errors: 4unknown[]-vs-Record[string, unknown][]sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results),find()readers inmemory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.update/upsertonly): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 onupdateagainst the then-old base declaration,upsert'sreturn this.update(...)(nullnot assignable), and three field reads inmemory-driver.test.ts.⇒ E2 is the minimal change that makes
dist/index.d.tshonest without a cascade, and it is what this PR ships:update(): Promise[Record[string, unknown] | null],upsert(): Promise[Record[string, unknown]].toStoredRecordis not annotated (it already carries[T](object, record: T): T; theanyarrives through its type parameter). #14435 is the DEFERRED ROOT of ruling item 2 — the store-channel repair the ruling names measured to cascade (E1), so explicit door annotations shipped as the no-cascade repair; ratified in-seat (review 5505756612, required change 5). The store'sany[]rows remain the channel behindfind/findOne/create(Promise[any[]]/Promise[any]/Promise[Record[string, any]]in the same.d.ts) — that is #14435.upsert: thenullarm ofupdateis unreachable on its path (existingRecordwas read from the same table with no yield in between; the review's independent leg verified the in-tick reading), soupsertasserts it loudly (throw) rather than widening its own door —IDataDriver.upsert()is not widened (the ruling namesupdate()only; review Q1 → A).Published doors after (
dist/index.d.ts, built from the final source):Pins
should return null on update of missing record in default mode— unchanged, green.memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFileslists it andmemory-driver.test.ts; 39 test files in the program) —IDataDriver['update']resolves to exactlyRecord[string, unknown] | null(read through spec's built.d.ts);InMemoryDriver['update' | 'upsert']are notanyand equal the contract's types — plus runtime cases (missing id ⇒null, read behind the narrowing the type now demands; upsert over an existing id ⇒ the merged record).memory-driver.test.tsnarrow before reading (expect(x).not.toBeNull()thenx!.field).Ablation — direction predicted before, measured after
Leg A — revert the contract widening alone (driver annotations stay). driver-memory reads spec through
dist(root tsconfig has nopaths), so: mutate (anchored: widened-count 0, reverted-count 1, blob444309b…vs HEAD8bec163…) →pnpm --filter @objectstack/spec build→node scripts/ablation-dist-preflight.mjs @objectstack/spec 'the widened update signature' --absent→✓ dist/: marker absent from all 215 built files -- the artifact the suite consumes no longer carries it.→ driver-memorytsc --noEmit. Predicted: RED with exactly (i) TS2416 onInMemoryDriver.updateagainst the old base and (ii) the pin'scontractUpdateDeclaresNullline; behaviour pin GREEN. Measured: exactly 2 errors —src/memory-driver.ts(677,9): error TS2416: Property 'update' in type 'InMemoryDriver' is not assignable to the same property in base type 'IDataDriver'.andsrc/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pinTests 2 passed | 72 skipped, exit 0. Restore:git checkout HEAD -- data-driver.tsundertrap ... EXIT INT TERMwith absolute paths, blob back to8bec163…== HEAD blob,git diff HEADempty; rebuild; preflight✓ dist/: marker present in 2 built files; tsc 0 errors.Leg B — revert the driver annotations alone (contract stays widened; tsc reads source, no dist leg). Mutation proven on disk (annotated-count 0 / bare-count 1 for both doors; blob
ef49241…vs HEADbbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors,memory-update-declared-null.test.tslines 49-52 (memoryUpdateIsAny,memoryUpdateIsContract,memoryUpsertIsAny,memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob,git diff HEADempty).Consumer-closure typecheck
Direction: prefix (
...@objectstack/driver-memory= downstream consumers, 19 packages) plus every package holding a driver-typed.update(call site. Closure built first —pnpm --filter '@objectstack/driver-memory^...' build, then@objectstack/specand@objectstack/driver-memoryrebuilt after the edits, then turbo^buildfor the consumer chunks — so every reading is against freshdist/*.d.ts.Who can see a return-type widening: only code that reads an
update()result or re-declares its type. Census (git grep, src + tests, whole repo): result-USING driverupdate()sites live in driver-memory (in-package, narrowed),metadata/src/loaders/database-loader.ts(the private_updatenow carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receiversSqlDriver/MongoDBDriver, declaredPromise[any]/Promise[Record[...]]— unaffected),plugin-sharing(receiverSqlDriver). Every forwarding wrapper namedupdate(object, …)in the repo is engine-shaped (3-arg) orPromise[unknown]/Promise[void]. The 14 prefix consumers without a driver-named receiver hold noInMemoryDrivervalue that calls.update((plugin-devandruntimehold one each, 0 calls). The review's independent leg re-derived the same census.Typechecked green at
a00813bd7(pre-merge; neither merge brought a change to any of these packages'update()surfaces, and the patch commit touches only the Zod mirror and two changesets): driver-memory (typecheck+tsc --listFiles), objectql, driver-mongodb, driver-turso, driver-sqlite-wasm, driver-sql, service-storage, plugin-webhooks (turbotypecheck,19 successful/18 successful, the rest cached), metadata (build— its dts emit is its only tsc program; green). Not typechecked here and declared: cli, client, client-react, cloud-connection, examples, hono, http-conformance, plugin-auth, plugin-dev, rest, runtime, service-datasource, service-sms, verify, dogfood — none reads anupdate()result (census above); CI'sType check workspace packagesjob runs them.Item 5 — the fabricating drivers (measured, not changed)
MongoDBDriver.update()(mongodb-driver.ts:403-424) andRemoteTransport.update()(remote-transport.ts:1517-1534) return an assembled row on a miss. Readers: the engine's by-id dispatch (objectql/src/engine.ts:11017— a fabricated row passes the'id' in resultguard, so the API answers 200 for a missing id on these two drivers only),TursoDriver.update's remote branch (turso-driver.ts:778, passes it through),RemoteTransport.bulkUpdate(if (updated)— a dead guard on this transport), and tests over existing rows only. Filed as #14428 (unassigned, no labels,Blocked-by: #13878). ⛔ Nothing underdriver-mongodb,driver-tursoordriver-sqlis edited.Driver-conformance ledger (lane standing reading)
Before the first edit (
79b6a22a5) and after the last commit (959fc96ec), identical:check-driver-conformance: OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.(matrix 5 drivers x 10 case-sets, every cellok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).Gates — union on the patch head
959fc96ecRe-derived on the merged patch tree with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(55 families — the same set as on2584285bc, nothing added or dropped;pnpm check:durability-log-levelwas already in it viadatabase-loader.ts). Every exit captured after a redirect, never through a pipe. The worktree was re-created for the patch round, so its dependency closure was rebuilt before the readings below (the first driver-memorytypecheckin the fresh tree read 6× TS2307 for the absent@objectstack/core/@objectstack/typesdist — an environment precondition, not a finding; green afterpnpm --filter '@objectstack/driver-memory^...' build).@objectstack/spec public API surface + factory signatures unchanged ✓; docs229 generated files in sync with packages/spec; exported-anyno exported type resolves to any: 2446 types + 1523 schemas across 17 entry points; export-origins current; driver-conformance above;check:doc-formula-expressions9 @example(s) judged clean across 1109 packages/spec/src files(measured after building@objectstack/lint+@objectstack/formula); engine-double-contract, where-matcher, query-options-erasure, type-check-coverage, durability-log-level, cross-package-test-inputs, test-source-alias all OK.check-adr-0087-registration: 2 declared-breaking changeset(s), each carrying an ADR-0087 disposition.—.changeset/driver-memory-update-upsert-honest-types.md [BREAKING] not-required (no-migration-prescription)·.changeset/idatadriver-update-declares-null.md [BREAKING] not-required (no-migration-prescription)·check-changeset-no-major: ✓ This diff introduces no major bump.·check-empty-changeset: ✓ No empty-frontmatter changeset introduced by this diff (3 declaring changeset(s) added).·check:changeset-gate-self-tests✓ (118 + 292 + 116 assertions).check-test-completeness: PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named.(exit 3) ·check-half-states: PREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused(exit 3) ·check:dual-build-cjs-loads:PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. Run pnpm build first. ⛔ This is NOT a pass: nothing was measured.(exit 3) ·check:type-check-debt: exit 3, its text says the run statesNOTHING about whether any DEBT or TEST_DEBT number is still correct(needs the ledgered packages' dependencies built) ·check-dev-prereqs: exit 1, its text✗ The workspace is not built — 1 unmet precondition, not a list of problems(a whole-workspacepnpm build— CI-owned).pnpm --filter @objectstack/spec typecheckexit 0 (check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s) held, unchanged) ·pnpm --filter @objectstack/spec test(vitest--maxWorkers=2):Test Files 451 passed (451) · Tests 12157 passed (12157)·pnpm --filter @objectstack/spec check:generated:All 15 generated artifacts are up to date·pnpm --filter @objectstack/driver-memory typecheckexit 0 ·pnpm --filter @objectstack/driver-memory test:Test Files 39 passed (39) · Tests 1028 passed (1028)·pnpm lintexit 0 (full repo) ·pnpm check:error-status-conformance:✓ every derivable runtime status is documented, and every documented status is reachable.Changesets
@objectstack/spec: minor— now carries BREAKING (the consumer-side compile obligation: a caller that read fields off anupdate()result narrows thenullarm first) andadr-0087: not-required (no-migration-prescription)— no metadata key is removed, renamed or re-shaped and nothing exists forobjectstack migrate metato rewrite; the obligation is a TypeScript narrowing at the call site (review required change 2, ruled in-seat; bump level unchanged, ruling item 6). ·@objectstack/driver-memory: minor— now carries BREAKING (ADR-0087's 2026-08-30 addendum names this exact shape: a published SDK method whose declared return moves offanyonto the contract) and the same disposition (type-surface-onlyis not claimable because the diff touchespackages/spec/**), plus the clause thatupsert()asserts the unreachablenullarm ofupdate()instead of widening its own declared return (review required change 3). ·@objectstack/metadata: patch(a private helper typed with the arm; no runtime change).Scope kept
⛔ Not
packages/drivers/driver-sql/**—SqlDriver.update()'s explicitPromise[any]atsql-driver.ts:6820is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ notdriver-mongodb/**/driver-turso/**(measured; #14428) · ⛔ notmemory.zod.ts(strictMode's sentence is now true as written) · ⛔ no otherIDataDrivermember · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactlypackages/spec/src/data/driver.zod.tsand the two changesets. Open branches at the first head: 382claude/*heads fetched and diffed againstorigin/main— none touched any of this PR's files.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68