feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types - #14434

Merged
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared
Sep 2, 2026
Merged

feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types#14434
os-musk merged 4 commits into
mainfrom
claude/issue-13878-update-null-declared

Conversation

@os-musk

@os-muskos-musk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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; head 959fc96ec after merging origin/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.ts changes a declared return type on the public IDataDriver contract — a packages/spec/src/contracts declaration change (ruling item 6). Re-read on the patch head 959fc96ec: still yes — the patch adds the same member's Zod mirror in packages/spec/src/data/driver.zod.ts and changes no other contract member. Draft, labelled needs:contract-review; the director seat named itself reviewer. No governed surface is touched.

The declaration — before / after

IDataDriver.update():

  • before: Promise[Record[string, unknown]]
  • after: Promise[Record[string, unknown] | null], with a docblock @returns sentence: the updated record, or null if no record with that id exists — the same "addressed one row, which may not exist" shape as findOne, the not-found vocabulary delete already 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.update in packages/spec/src/data/driver.zod.ts now outputs z.promise(z.record(z.string(), z.unknown()).nullable()) — exactly the spelling findOne already uses on the same schema — and its docblock says "The updated record, or null if 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.ts chunk counts 2 ZodPromise[ZodNullable[ZodRecord[...]]] outputs (findOne + update) where it counted 1 before. check:generated on the rebuilt dist: All 15 generated artifacts are up to datecheck:api-surface, check:authorable-surface, check:docs and the JSON schemas all unchanged, so nothing was regenerated and nothing hand-edited.

Zero new vocabulary; update was the only by-id door without a not-found arm.

The mask — root measured, minimal repair chosen

Measured on the committed tree at 79b6a22a5 with a self-controlling type probe (IsAny on InMemoryDriver['update' | 'upsert' | 'delete'], delete as the positive control, removed under a trap, removal proven by empty git status):

  • E0 (probe only): exactly 1 error — src/zz-probe-13878.ts(5,14): error TS2322: Type 'true' is not assignable to type 'false'. — the control fired on delete(); update() and upsert() compiled clean ⇒ both resolve to any. Published: dist/index.d.ts:195-196 read update(...): Promise[any] / upsert(...): Promise[any].
  • E1 (retype the root: private db: Record[string, Record[string, unknown][]]): un-masks (all three probe lines fire) but T infers 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: 4 unknown[]-vs-Record[string, unknown][] sites inside the driver (lines 567 / 748 / 814 / 1100, mingo results), find() readers in memory-datetime-storage.test.ts, and property reads in three test files. A different lie, not an honest type.
  • E2 (explicit honest return types on update / upsert only): un-masks (all three probe lines fire); 8 errors, every one a real reader: TS2416 on update against the then-old base declaration, upsert's return this.update(...) (null not assignable), and three field reads in memory-driver.test.ts.
  • E3 (E1 + E2): 14 errors — E1's cascade survives.

E2 is the minimal change that makes dist/index.d.ts honest without a cascade, and it is what this PR ships: update(): Promise[Record[string, unknown] | null], upsert(): Promise[Record[string, unknown]]. toStoredRecord is not annotated (it already carries [T](object, record: T): T; the any arrives 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's any[] rows remain the channel behind find / findOne / create (Promise[any[]] / Promise[any] / Promise[Record[string, any]] in the same .d.ts) — that is #14435.

upsert: the null arm of update is unreachable on its path (existingRecord was read from the same table with no yield in between; the review's independent leg verified the in-tick reading), so upsert asserts it loudly (throw) rather than widening its own door — IDataDriver.upsert() is not widened (the ruling names update() only; review Q1 → A).

Published doors after (dist/index.d.ts, built from the final source):

update(object: string, id: string | number, data: Record[string, any], options?: DriverOptions): Promise[Record[string, unknown] | null];
upsert(object: string, data: Record[string, any], conflictKeys?: string[], options?: DriverOptions): Promise[Record[string, unknown]];

Pins

  • Behaviour pin should return null on update of missing record in default mode — unchanged, green.
  • New memory-update-declared-null.test.ts: type-level pins inside the package's tsc program (tsc --listFiles lists it and memory-driver.test.ts; 39 test files in the program) — IDataDriver['update'] resolves to exactly Record[string, unknown] | null (read through spec's built .d.ts); InMemoryDriver['update' | 'upsert'] are not any and 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).
  • Three in-package readers in memory-driver.test.ts narrow before reading (expect(x).not.toBeNull() then x!.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 no paths), so: mutate (anchored: widened-count 0, reverted-count 1, blob 444309b… vs HEAD 8bec163…) → pnpm --filter @objectstack/spec buildnode 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-memory tsc --noEmit. Predicted: RED with exactly (i) TS2416 on InMemoryDriver.update against the old base and (ii) the pin's contractUpdateDeclaresNull line; 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'. and src/memory-update-declared-null.test.ts(46,7): error TS2322: Type 'true' is not assignable to type 'false'.; behaviour pin Tests 2 passed | 72 skipped, exit 0. Restore: git checkout HEAD -- data-driver.ts under trap ... EXIT INT TERM with absolute paths, blob back to 8bec163… == HEAD blob, git diff HEAD empty; 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 HEAD bbc3527…). Predicted: RED, exactly the four driver-side consts of the pin. Measured: exactly 4 errors, memory-update-declared-null.test.ts lines 49-52 (memoryUpdateIsAny, memoryUpdateIsContract, memoryUpsertIsAny, memoryUpsertIsContract). Restore proven byte-exact (blob == HEAD blob, git diff HEAD empty).

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/spec and @objectstack/driver-memory rebuilt after the edits, then turbo ^build for the consumer chunks — so every reading is against fresh dist/*.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 driver update() sites live in driver-memory (in-package, narrowed), metadata/src/loaders/database-loader.ts (the private _update now carries the arm; both callers discard the result), driver-turso / driver-sql / driver-mongodb tests (receivers SqlDriver / MongoDBDriver, declared Promise[any] / Promise[Record[...]] — unaffected), plugin-sharing (receiver SqlDriver). Every forwarding wrapper named update(object, …) in the repo is engine-shaped (3-arg) or Promise[unknown] / Promise[void]. The 14 prefix consumers without a driver-named receiver hold no InMemoryDriver value that calls .update( (plugin-dev and runtime hold 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 (turbo typecheck, 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 an update() result (census above); CI's Type check workspace packages job runs them.

Item 5 — the fabricating drivers (measured, not changed)

MongoDBDriver.update() (mongodb-driver.ts:403-424) and RemoteTransport.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 result guard, 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 under driver-mongodb, driver-turso or driver-sql is 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 cell ok; dialect axis 8 suites, 10 of 10 dialect-scored cells matrix-routed).

Gates — union on the patch head 959fc96ec

Re-derived on the merged patch tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (55 families — the same set as on 2584285bc, nothing added or dropped; pnpm check:durability-log-level was already in it via database-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-memory typecheck in the fresh tree read 6× TS2307 for the absent @objectstack/core / @objectstack/types dist — an environment precondition, not a finding; green after pnpm --filter '@objectstack/driver-memory^...' build).

  • 50 / 55 measured green — api-surface @objectstack/spec public API surface + factory signatures unchanged ✓; docs 229 generated files in sync with packages/spec; exported-any no 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.
  • Changeset / ADR-0087 gates: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).
  • NOT MEASURED, in each gate's own words: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 states NOTHING 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-workspace pnpm build — CI-owned).
  • pnpm --filter @objectstack/spec typecheck exit 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 typecheck exit 0 · pnpm --filter @objectstack/driver-memory test: Test Files 39 passed (39) · Tests 1028 passed (1028) · pnpm lint exit 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 an update() result narrows the null arm first) and adr-0087: not-required (no-migration-prescription) — no metadata key is removed, renamed or re-shaped and nothing exists for objectstack migrate meta to 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 off any onto the contract) and the same disposition (type-surface-only is not claimable because the diff touches packages/spec/**), plus the clause that upsert() asserts the unreachable null arm of update() 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 explicit Promise[any] at sql-driver.ts:6820 is measured and reported here, not edited; it is now #14438, filed by the PM since #13854 is closed (via PR #14170) · ⛔ not driver-mongodb/** / driver-turso/** (measured; #14428) · ⛔ not memory.zod.ts (strictMode's sentence is now true as written) · ⛔ no other IDataDriver member · ⛔ no ratchet re-baselined, no test deleted or weakened; the patch round touched exactly packages/spec/src/data/driver.zod.ts and the two changesets. Open branches at the first head: 382 claude/* heads fetched and diffed against origin/main — none touched any of this PR's files.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-memory, @objectstack/metadata, @objectstack/spec, touching 5 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/data-modeling/queries.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/data-engine.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/kernel/services.mdx(via IDataDriver (symbol, a top-level interface))
  • content/docs/permissions/authentication.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx(via DatabaseLoader (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via DatabaseLoader (symbol, a top-level class), InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v16.mdx(via InMemoryDriver (symbol, a top-level class))
  • content/docs/releases/v17.mdx(via IDataDriver (symbol, a top-level interface), InMemoryDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 132 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 a98b61b3ef695431db26097a28ae8e5f1dec8fdfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 23f76fc62cd45d8ac86fda6b6199304032a9e3f6 — the merge of head 959fc96ec647d18db5656d6cdb12ff3b3c7988fe into base a98b61b3ef695431db26097a28ae8e5f1dec8fdf, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a98b61b3ef695431db26097a28ae8e5f1dec8fdf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 2, 2026
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — engine execution seat (session session_0112hMx9hjJ9BgB28X97DS68).

Clause-② PR. In-seat contract review PASS on the card: comment 5506180905 (#13878; patch head 959fc96ec; tier fuse read claude-fable-5-1 via get_session; provenance the maintainer's 2026-08-31 ruling — in-seat review by a tier-qualified seat, PASS ⇒ the same seat clears the carriers and lands). Carriers: needs:contract-review cleared on card #13878 and on this PR at 07:40Z with compare read-back (card bug, priority:p2, pm:dispatched, domain:engine; PR documentation, size/m, tests, tooling, protocol:data). check-clause2-carriers.mjs --pair answers exit 3 from this seat (environment); the two-leg MCP read substitutes.

Flip pre-checks on head 959fc96ec: every one of the 37 check runs completed with conclusion success or skipped (Lint & Repo Gates completed 08:00:15Z; Check Changeset green with the two **BREAKING** + adr-0087: not-required (no-migration-prescription) dispositions); governed-surface test on the final 9-path file list: 0 of 9 path(s) hit the register ⇒ ordinary queue landing; closing-keyword two-read done at the PASS (Fixes #13878, correct — ruling A executes in full).

Action: draft: false then auto-merge (squash) — the merge queue takes it from here.


Generated by Claude Code

@os-musk
os-musk enabled auto-merge September 2, 2026 08:02
@os-musk
os-musk added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 93940d4Sep 2, 2026
39 checks passed
@os-musk
os-musk deleted the claude/issue-13878-update-null-declared branch September 2, 2026 08:28
os-musk pushed a commit that referenced this pull request Sep 3, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-musk@claude