Repository navigation
[drivers] a driver returning a declared field as an own key holding undefined is a storage-contract defect — Option C from #8489 #9276
Description
Activity
Triage (first touch):
pm:on-hold, typeBug(storage-contract defect, measured).Reasoning, 2026-08-17: the durable fix lands entirely in
driver-memory/driver-mongodb, which are under the maintainer's 2026-08-05 investment freeze (cross-card anchor #5499). The one live consumer divergence was already converged at the canonical binder by #8489, and the defective row shape cannot arrive over the wire (undefineddoes not survive JSON) — reproducible but not user-reachable, so the Bug-hold queue-return branch does not apply. Not closed, because the maintainer's 2026-08-16 ruling on #8489 explicitly designates this card as the durable follow-up.Restart-when: closed #5499
(Earlier restart is legitimate only if the freeze ruling is narrowed on #5499 to exclude storage-contract normalization — that is a maintainer note to record there, not a seat call.)
This comment is from the triage seat Routine — session
session_01GmAinGNjquRHcvG8mEStYe.
Generated by Claude Code
Un-held — the restart condition was already satisfied nine days before it was written
Surfaced by the #12393 dev while scoping the
VALUE_ROUNDTRIPcase-set against this card's exclusion. Acting on it here because it is a queue-state decision, not a dev fix.This card sits
pm:on-holdwithRestart-when: … OR driver-memory / driver-mongodb come out of the #5499 investment freeze. Measured against the anchor card's own comments:event when driver-mongodbunfrozen — 「5499 解冻 mongodb 相关开发,继续全速推进」2026-08-11 04:34Z driver-memoryunfrozen — 「driver-memory 也解冻,继续清剩下两行 DEBT」, with "this card's freeze is fully dissolved … it no longer gates anything"2026-08-11 11:27Z this card's restart line written 2026-08-20 So the condition fired nine days before the sentence that names it. Nothing was ever going to wake this card: a restart condition is checked by looking for the event after the line was written, and this one was already in the past. That is precisely the failure the card's own note says the restart line was added to prevent — the line built the trap it was meant to close.
Moved to
pm:queue. Not claimed and not dispatched in this batch; it takes its place in normal selection.⚠️ Worth generalising rather than just fixing this instance: a restart condition whose event may already have occurred is not a hold, it is a silent drop. Anything writing one should check the condition's current state at the moment of writing — the same "re-verify the premise, don't inherit it" rule that applies to a card's citations applies to its restart line. Raising it here rather than filing a process card, since the mechanism is the same one #12395 / #12397 exercised correctly today by re-scanning their upstreams.For the record, this card is also now referenced from the
VALUE_ROUNDTRIPcase-set: an explicitly-undefineddeclared field is scoped out of that case-set by name, with this card cited and the condition for lifting the exclusion stated in the file — not ledgered, so no new escape hatch was opened.
Generated by Claude Code
Claimed and dispatched
- Session:
session_01W6HFzyH98W1YaQXhJUJt6o(seat post [PM seat] domain:engine — ⏳ vacant #6367) - Branch:
claude/issue-9276-driver-own-key-undefined - Comments re-read in the same action as the claim. No prior claim by any other session.
The hold is discharged and the scope note is now moot. This card's
Restart-when:line named three exits, one of them "driver-memory/driver-mongodbcome out of the #5499 investment freeze" — which had already happened nine days before that line was written, as I recorded here earlier today. The body's closing caveat — "whether the freeze covers this is a lane call" — is answered: there is no freeze. Both drivers are enrolled on ordinary terms.The fork, and where its boundary is
The card names the repair as "either dropped from the row or returned as
null". That is a real choice and it goes to the dev, because the evidence already points one way and the dev should confirm or refute it rather than inherit it: both measured consumers read an own key holdingundefinedas absent — CEL (has(record.status)isfalse) andmaterializeDeclaredFields, the latter by documented design. Dropping the key is the reading that matches what the platform already believes; returningnullwould make it a value, which is the one thing the two states are supposed to distinguish.⛔ But if the repair turns into a question about what a stored row may contain rather than what this driver emits, that is a storage-contract question and a maintainer floor — report the fork. Same boundary the seat applied to #12397 and #12578 today.
What must not be lost
⭐ The card's own starred note is carried into the brief verbatim, because it is the part most likely to be flattened: a fail-OPEN consumer on this input class is a different card entirely and must not wait for this one. The single consumer measured so far (#8489's eligibility gate in
plugin-sharing) was fail-closed before and after, so nobody was over-granted — that is why this is a contract-hygiene card and not an incident. If the dev's sweep turns up a fail-open consumer, it stops being this card.driver-mongodbis the other JS-backed driver in the family and the card asks for it to be measured the same way before deciding — not assumed to matchdriver-memory.SqlDrivercannot express the shape at all (a SQL NULL arrives asnull, a value), so the divergence is JS-backed versus SQL-backed by construction.Serial check:
packages/drivers/driver-memoryanddriver-mongodb. The open #12591 holdssql-driver.tsonly; #12578 ispackages/objectql; #12559 is in the i18n extract configs. No overlap.
Generated by Claude Code
- Session:
Dispatch killed by a container restart — work recovered and pushed, not verified
The dev seat was killed mid-task when the container restarted. Its worktree survived; nothing had reached origin. Recovered and pushed at 17:5xZ — 1 commit, clean tree:
263c74db00 fix(drivers): a declared field written as an explicit `undefined` is indistinguishable from one never writtenThe commit subject says the fork was answered the way the evidence pointed — drop the key, so the two states are indistinguishable — but there is no PR, no gate output, no ablation, and no
driver-mongodbmeasurement in the record. The card asked for mongodb to be measured rather than assumed to matchdriver-memory, and that is not evidenced anywhere. Treat the branch as a starting point.⭐ For whoever resumes, the starred note is still the one that must not be flattened: a fail-OPEN consumer on this input class is a different, more urgent card and must not wait for this one. Whether the killed dispatch's sweep turned one up is unknown — the sweep's result died with the seat, so it has to be re-run rather than presumed clean.
Second stall-and-recover on this seat today; the first is recorded on #12068 / #11671 / #12393 and in §7 of seat post #6367.
Generated by Claude Code
{ "issue": 9276, "status": "done", "premise_still_valid": true, "branch": "claude/issue-9276-driver-own-key-undefined", "pr": "https://github.com/objectstack-ai/objectstack/pull/12641", "finalHead": "1aad15dba5", "summary": "Resumed after the container restart. Inspected 263c74db00 rather than trusting it: both driver implementations were correct and are kept unchanged, but every piece of EVIDENCE the recovery note said was missing was genuinely missing and has been re-derived. The reproduction was re-taken on current origin/main against built dist and still stands. driver-mongodb was measured separately and does NOT match driver-memory. The fail-OPEN sweep was re-run with a firing positive control and found none. One thing was DISCARDED: the recovered tests stated the contract with toEqual, which on this repo's vitest ignores own keys holding undefined and was therefore vacuous on exactly this input class - moved to toStrictEqual so the contract assertion itself discriminates. Separately, a worktree-rescue actor committed this seat's live working tree at 02:23:47Z as e87cf37e86 with a message asserting no gate/ablation/sweep existed; the seat was alive and had already run all three. Content kept, message corrected additively in 1aad15dba5, mechanism filed as #12637.", "recovered": { "kept": "Both driver implementations, unchanged: withoutUndefinedOwnKeys in each package, toStoredRecord as the single write-door choke point in driver-memory, the insert-door placement in driver-mongodb, and the changeset (minor/minor). Read line by line and independently corroborated by my own measurements; the fork it chose is the one the evidence supports.", "rederived": "Everything evidential, because none of it existed in the record: the two-leg reproduction on current origin/main against built dist; the driver-mongodb measurement the card asked for by name; the fail-OPEN consumer sweep with a synthetic positive control; the gate union via dispatch-gates.mjs --repo and all 26 gate invocations; both ablations with predictions written first; full-repo pnpm lint.", "discarded": "The central assertion's spelling. `expect(comparable(written)).toEqual(comparable(neverWritten))` is VACUOUS on this input class - measured: expect({a:1,status:undefined}).toEqual({a:1}) PASSES, .toStrictEqual fails. toEqual ignores own keys holding undefined, so the one assertion stating the CONTRACT would have stayed green against the unrepaired driver and both files owed their discriminating power entirely to the key-list and `in` diagnostics, which pin a SPELLING ('the key is dropped') not the RULE ('the two rows are indistinguishable'). Three sites per file moved to toStrictEqual, with the measurement written beside the one that matters." }, "forkChosen": "drop the key", "forkReasoning": "Both measured consumers already read an own key holding undefined as ABSENT - CEL (has(record.f) is false) and materializeDeclaredFields by documented design. Returning null would make it a VALUE, which is the one thing the two states exist to distinguish, i.e. it would make the rows indistinguishable in the wrong direction. driver-memory already held 'undefined means the key is not emitted' in two of its own places (projectFields skips undefined; the matcher's $exists/$null read it as absent), so the returned row was the last surface in that driver still claiming presence - this is convergence, not a new rule. The storage-contract boundary was NOT crossed: scope is the insert doors and the values returned; $set-shaped patches are untouched and driver-memory normalises POST-merge, so neither driver answers 'what does a patch carrying undefined mean'. That remains a maintainer floor and is not reopened here.", "mongodbMeasurement": "MEASURED SEPARATELY, AND IT DOES NOT MATCH driver-memory. driver-memory is consistently wrong; driver-mongodb SPLITS across two of its own doors. With the repair reverted, create() returns the object it built in process -> own key holding UNDEFINED ('status' in row: true, row.status: UNDEFINED); but MongoClient defaults to ignoreUndefined:false and this driver sets no override, so BSON stores null for that same field and a real find() answers 'status' in row: true, row.status: null - a VALUE. One write, two answers, from one driver, and CEL reads those two answers differently. Measured without a server via a recording fake Db plus BSON.serialize/deserialize under the driver's own serialization setting. Both halves close at the insert doors.", "failOpenConsumerFound": false, "failOpenSweep": "Scope per the card: bare `in` / hasOwnProperty / Object.hasOwn presence test against a declared field under packages/, excluding materializeDeclaredFields. 110 non-test hits. 13 of those take a DRIVER-RETURNED ROW as their subject (the remainder test schema/fields maps, caller write payloads, config objects or response envelopes - none of which a driver emits). All 13 hand-triaged: every one fail-CLOSED or neutral on this input class. Sharpest case is engine.ts maskSecretFields, where presence is the SAFER branch - an own key holding undefined makes the masker run and normalise to null, absence skips it. plugin-sharing's eligibility seam is confirmed on the canonical binder (share-link-service.ts imports materializeDeclaredFields), so #8489 is landed. POSITIVE CONTROL: a synthetic fixture with three fail-open shapes (if (f in row) return true / if (!(f in row)) return false / a hasOwnProperty allow-predicate) fires 3/3 under the same patterns that produced the 110 - the zero is a reading, not a silent scan.", "clause2": "Observable behaviour on a shipped surface: YES. Accept set: NO - it does not move.", "clause2_evidence": "git diff --stat is 5 files: 1 changeset, 2 driver sources, 2 new test files (+555/-8). BEHAVIOUR: what these two published packages RETURN for one input class changes, and what driver-mongodb STORES for it changes - in-process code passing an explicitly-undefined property to create/bulkCreate/update/updateMany or seeding initialData no longer sees that key, and no null is written in MongoDB. Not reachable over the wire (undefined does not survive JSON). ACCEPT SET: `git diff --name-only origin/main` matches nothing against `.zod.|schema|refine|validat`; nothing that parsed before is refused now. EXPORTED NAMES: `git diff origin/main | grep -cE '^[+-][^+-].*\\bexport\\b'` -> 0, i.e. ZERO added-or-removed lines containing `export` anywhere in the diff. withoutUndefinedOwnKeys is module-local in each package; toStoredRecord is a private method. That measurement is why check:api-surface / check:export-origins / check:docs are not owed here - stated with evidence rather than assumed. Changeset graded minor for both packages on exactly that split (more than patch because shipped behaviour moves; not major because no API/type/exported name/accept set does); check:changeset-no-major green.", "gates": { "green": [ "pnpm --filter @objectstack/driver-memory test -> 29 files, 833 passed", "pnpm --filter @objectstack/driver-mongodb test -> 19 passed/5 skipped files, 438 passed/143 skipped (skips are mongod-backed)", "pnpm --filter @objectstack/driver-memory typecheck -> exit 0", "pnpm --filter @objectstack/driver-mongodb typecheck -> exit 0", "pnpm lint (eslint . --no-inline-config, FULL repo) -> exit 0, no narrowing claimed", "check:nul-bytes -> OK, 6989 text files scanned, no raw ASCII control bytes", "check:driver-memory-census -> OK (run BY HAND: derivation calls it unreachable-by-construction because its population literal is the package NAME, not a path - yet it is the gate most specific to this diff)", "check:driver-conformance -> OK, 50 covered cells, 0 DEBT, 0 exempt", "check:engine-double-contract -> OK", "check:where-matcher -> OK, 303 matchers, baseline key set verified against 0043c92, no files added", "check:slot-lookup -> OK, ratchet holds, no files added", "check:query-options-erasure -> OK, ratchet holds, no files added", "check:objectql-double-limit -> OK, baseline verified, no files added", "check:cross-package-test-inputs -> OK, 20 packages read outside themselves, all declared", "check:test-source-alias -> OK, 72 packages scanned", "check:type-source-resolution -> OK, 94 tsc programs", "check:published-files -> OK, 69 publishable packages", "check:page-declaration-shape -> OK", "check:type-check-coverage -> OK, 65/78 packages type-checked", "check:type-check-debt -> --re-measure OK, 31 ledger entries re-measured in 230.7s, 1687 raw tsc errors, none above its recorded number (on a fully built workspace closure)", "check:changeset-gate-self-tests -> OK", "check:objectui-changeset -> OK", "scripts/check-empty-changeset.mjs -> OK, 1 declaring changeset added", "scripts/check-adr-0087-registration.mjs -> OK, no declared-breaking changeset", "scripts/check-changeset-no-major.mjs -> OK, no major bump", "scripts/check-ci-filter-parity.mjs -> OK, 109 declared cross-package globs covered", "scripts/check-comment-mask-adoption.mjs -> OK, 20 strippers recorded", "scripts/check-plugin-teardown-shape.mjs -> OK, 63 Plugin implementations", "scripts/pm/release-rehearsal-clone.mjs --self-test -> OK", "scripts/docs-audit/check-affected-docs.mjs -> OK", "scripts/docs-audit/check-drift-comment.mjs -> OK" ], "red": [], "notMeasured": [ "CI gate-job convergence - not waited for, per the 2026-08-10 maintainer ruling that CI convergence is the PM's, not the dev's. Report is delivered at draft-PR time.", "check:api-surface / check:export-origins / check:docs - NOT owed and therefore not run, on the measurement above (zero added-or-removed `export` lines in the whole diff). Stated as a derivation, not as a skip.", "The mongod-backed driver-mongodb suites (143 tests) are skipped by the repo's own #5517 server gate, not by me. The new mongodb pins deliberately run WITHOUT a server, via a recording fake Db plus a BSON round trip under the driver's own serialization setting." ] }, "ablations": [ { "leg": "driver-memory - memory-driver.ts reverted to origin/main, both packages rebuilt", "predictedDirection": "RED", "predictedCount": "8 of 10 fail; survivors are 'filter semantics are unchanged' and 'null stays a VALUE'", "observed": "RED, 8 failed | 2 passed (10) - survivors exactly as predicted", "mutationProvedOnDisk": "anchored grep -cF BEFORE reading any result: withoutUndefinedOwnKeys 3 -> 0, `this.toStoredRecord(object, {` 3 -> 0, and the pre-fix call form `this.toStorageForms(object, {` back at 3. git diff --stat used only as corroboration, never as the proof.", "rebuild": "Both packages rebuilt; reach proved with scripts/ablation-dist-preflight.mjs --absent (marker absent from all 6 built files) on the mutation leg and present (2 built files) on the restore leg.", "restore": "trap restore EXIT INT TERM; markers back to 3/6; git diff on the two mutated files EMPTY (verified, printed as GIT_DIFF_EMPTY=yes). Restored from pristine `cp` copies - never `git checkout HEAD -- .` (#12204)." }, { "leg": "driver-mongodb - mongodb-driver.ts reverted to origin/main, both packages rebuilt", "predictedDirection": "RED", "predictedCount": "4 of 6 fail; survivors are the pure-BSON mechanism pin and 'null is a VALUE'", "observed": "RED, 4 failed | 2 passed (6) - survivors exactly as predicted", "mutationProvedOnDisk": "anchored grep -cF BEFORE reading any result: withoutUndefinedOwnKeys 6 -> 0.", "rebuild": "As above, --absent proved on the mutation leg, present on the restore leg.", "restore": "Same trap; marker back to 6; git diff empty." }, { "leg": "ablation re-run against the STRENGTHENED (toStrictEqual) pin", "predictedDirection": "RED, same counts", "predictedCount": "8 of 10 and 4 of 6, unchanged", "observed": "RED, 8/10 and 4/6 - unchanged, but those failures are now carried by the CONTRACT assertion rather than only by the key-list/`in` diagnostics", "note": "Run with dist carrying the FIX while src was ablated, versus run 1 where dist was ablated too. Identical counts both ways, which independently confirms these two test files' verdicts are src-mediated (intra-package relative specifiers), exactly as the import form predicts. The dist-mediated consumer is the reproduction probe, and that is why both packages were rebuilt on both legs." }, { "leg": "PREDICTED REVERSAL, reported as observed", "observed": "The probe's line '>>> mongodb create() and find() AGREE about status' prints TRUE in the ablated state as well, because it compares key PRESENCE only and both doors say 'present'. The line is insensitive to this defect; the split lives in the VALUES (UNDEFINED vs null). Predicted in writing before the run and reported rather than quietly dropped." } ], "tests": "All exit codes captured BEFORE any pipe (the runner writes each gate to its own file, then `local ec=$?`); every verdict quoted from the gate's own output line, never from a bare $?. Heavy work serialised through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 on every acquisition; longest wait 8m16s). Gate union re-derived and ratchet families re-run at final head 1aad15dba5 AFTER the last commit. Reproduction, ablations and green suites as detailed in `ablations` and `gates.green`.", "openQuestions": [], "out_of_scope_findings": [ "filed as #12637 (sub-issue of #12627): worktree rescue fired against a LIVE seat and left a WIP commit whose message asserts 'no gate was run, no ablation exists, the fail-OPEN sweep is not evidenced' in the present tense - all three false when written. Two defects: the rescue has no liveness precondition (uncommitted changes are evidence of work in progress, not of death - the same observation in both cases), and the prescribed WIP message asserts more than the rescuer can observe. Matters more once #12627 lands and makes this a standing step. Not labelled; left for triage." ], "deviationsDeclared": [ "The REST issues channel is unavailable from this seat: GET /repos/objectstack-ai/objectstack/issues with $GITHUB_TOKEN returns HTTP 403 'GitHub access is not enabled for this session'. The dedup search for the finding above therefore used ONE MCP search_issues call instead of the prescribed REST listing. Declared rather than silently substituted. Git push credentials are unaffected (both pushes succeeded).", "`gh` is not on PATH in this container; the draft PR was opened via the MCP create_pull_request tool." ] }
Generated by Claude Code
Generated by Claude Code
os-dev-report
⚠️ Re-posted with a LITERAL-TEXT marker. The previous comment (#issuecomment-5433679603) carried the report as an HTML-comment marker and GitHub's body sanitizer ate it — read back, that comment now begins with a bare code fence and is invisible to a marker scan. Same JSON, unchanged; this is the copy to parse. No comment-edit path exists from this seat (the MCP surface has no update-comment tool and the REST channel returns 403 here), so correcting it in place was not possible.{ "issue": 9276, "status": "done", "premise_still_valid": true, "branch": "claude/issue-9276-driver-own-key-undefined", "pr": "https://github.com/objectstack-ai/objectstack/pull/12641", "finalHead": "1aad15dba5", "summary": "Resumed after the container restart. Inspected 263c74db00 rather than trusting it: both driver implementations were correct and are kept unchanged, but every piece of EVIDENCE the recovery note said was missing was genuinely missing and has been re-derived. The reproduction was re-taken on current origin/main against built dist and still stands. driver-mongodb was measured separately and does NOT match driver-memory. The fail-OPEN sweep was re-run with a firing positive control and found none. One thing was DISCARDED: the recovered tests stated the contract with toEqual, which on this repo's vitest ignores own keys holding undefined and was therefore vacuous on exactly this input class - moved to toStrictEqual so the contract assertion itself discriminates. Separately, a worktree-rescue actor committed this seat's live working tree at 02:23:47Z as e87cf37e86 with a message asserting no gate/ablation/sweep existed; the seat was alive and had already run all three. Content kept, message corrected additively in 1aad15dba5, mechanism filed as #12637.", "recovered": { "kept": "Both driver implementations, unchanged: withoutUndefinedOwnKeys in each package, toStoredRecord as the single write-door choke point in driver-memory, the insert-door placement in driver-mongodb, and the changeset (minor/minor). Read line by line and independently corroborated by my own measurements; the fork it chose is the one the evidence supports.", "rederived": "Everything evidential, because none of it existed in the record: the two-leg reproduction on current origin/main against built dist; the driver-mongodb measurement the card asked for by name; the fail-OPEN consumer sweep with a synthetic positive control; the gate union via dispatch-gates.mjs --repo and all 26 gate invocations; both ablations with predictions written first; full-repo pnpm lint.", "discarded": "The central assertion's spelling. `expect(comparable(written)).toEqual(comparable(neverWritten))` is VACUOUS on this input class - measured: expect({a:1,status:undefined}).toEqual({a:1}) PASSES, .toStrictEqual fails. toEqual ignores own keys holding undefined, so the one assertion stating the CONTRACT would have stayed green against the unrepaired driver and both files owed their discriminating power entirely to the key-list and `in` diagnostics, which pin a SPELLING ('the key is dropped') not the RULE ('the two rows are indistinguishable'). Three sites per file moved to toStrictEqual, with the measurement written beside the one that matters." }, "forkChosen": "drop the key", "forkReasoning": "Both measured consumers already read an own key holding undefined as ABSENT - CEL (has(record.f) is false) and materializeDeclaredFields by documented design. Returning null would make it a VALUE, which is the one thing the two states exist to distinguish, i.e. it would make the rows indistinguishable in the wrong direction. driver-memory already held 'undefined means the key is not emitted' in two of its own places (projectFields skips undefined; the matcher's $exists/$null read it as absent), so the returned row was the last surface in that driver still claiming presence - this is convergence, not a new rule. The storage-contract boundary was NOT crossed: scope is the insert doors and the values returned; $set-shaped patches are untouched and driver-memory normalises POST-merge, so neither driver answers 'what does a patch carrying undefined mean'. That remains a maintainer floor and is not reopened here.", "mongodbMeasurement": "MEASURED SEPARATELY, AND IT DOES NOT MATCH driver-memory. driver-memory is consistently wrong; driver-mongodb SPLITS across two of its own doors. With the repair reverted, create() returns the object it built in process -> own key holding UNDEFINED ('status' in row: true, row.status: UNDEFINED); but MongoClient defaults to ignoreUndefined:false and this driver sets no override, so BSON stores null for that same field and a real find() answers 'status' in row: true, row.status: null - a VALUE. One write, two answers, from one driver, and CEL reads those two answers differently. Measured without a server via a recording fake Db plus BSON.serialize/deserialize under the driver's own serialization setting. Both halves close at the insert doors.", "failOpenConsumerFound": false, "failOpenSweep": "Scope per the card: bare `in` / hasOwnProperty / Object.hasOwn presence test against a declared field under packages/, excluding materializeDeclaredFields. 110 non-test hits. 13 of those take a DRIVER-RETURNED ROW as their subject (the remainder test schema/fields maps, caller write payloads, config objects or response envelopes - none of which a driver emits). All 13 hand-triaged: every one fail-CLOSED or neutral on this input class. Sharpest case is engine.ts maskSecretFields, where presence is the SAFER branch - an own key holding undefined makes the masker run and normalise to null, absence skips it. plugin-sharing's eligibility seam is confirmed on the canonical binder (share-link-service.ts imports materializeDeclaredFields), so #8489 is landed. POSITIVE CONTROL: a synthetic fixture with three fail-open shapes (if (f in row) return true / if (!(f in row)) return false / a hasOwnProperty allow-predicate) fires 3/3 under the same patterns that produced the 110 - the zero is a reading, not a silent scan.", "clause2": "Observable behaviour on a shipped surface: YES. Accept set: NO - it does not move.", "clause2_evidence": "git diff --stat is 5 files: 1 changeset, 2 driver sources, 2 new test files (+555/-8). BEHAVIOUR: what these two published packages RETURN for one input class changes, and what driver-mongodb STORES for it changes - in-process code passing an explicitly-undefined property to create/bulkCreate/update/updateMany or seeding initialData no longer sees that key, and no null is written in MongoDB. Not reachable over the wire (undefined does not survive JSON). ACCEPT SET: `git diff --name-only origin/main` matches nothing against `.zod.|schema|refine|validat`; nothing that parsed before is refused now. EXPORTED NAMES: `git diff origin/main | grep -cE '^[+-][^+-].*\\bexport\\b'` -> 0, i.e. ZERO added-or-removed lines containing `export` anywhere in the diff. withoutUndefinedOwnKeys is module-local in each package; toStoredRecord is a private method. That measurement is why check:api-surface / check:export-origins / check:docs are not owed here - stated with evidence rather than assumed. Changeset graded minor for both packages on exactly that split (more than patch because shipped behaviour moves; not major because no API/type/exported name/accept set does); check:changeset-no-major green.", "gates": { "green": [ "pnpm --filter @objectstack/driver-memory test -> 29 files, 833 passed", "pnpm --filter @objectstack/driver-mongodb test -> 19 passed/5 skipped files, 438 passed/143 skipped (skips are mongod-backed)", "pnpm --filter @objectstack/driver-memory typecheck -> exit 0", "pnpm --filter @objectstack/driver-mongodb typecheck -> exit 0", "pnpm lint (eslint . --no-inline-config, FULL repo) -> exit 0, no narrowing claimed", "check:nul-bytes -> OK, 6989 text files scanned, no raw ASCII control bytes", "check:driver-memory-census -> OK (run BY HAND: derivation calls it unreachable-by-construction because its population literal is the package NAME, not a path - yet it is the gate most specific to this diff)", "check:driver-conformance -> OK, 50 covered cells, 0 DEBT, 0 exempt", "check:engine-double-contract -> OK", "check:where-matcher -> OK, 303 matchers, baseline key set verified against 0043c92, no files added", "check:slot-lookup -> OK, ratchet holds, no files added", "check:query-options-erasure -> OK, ratchet holds, no files added", "check:objectql-double-limit -> OK, baseline verified, no files added", "check:cross-package-test-inputs -> OK, 20 packages read outside themselves, all declared", "check:test-source-alias -> OK, 72 packages scanned", "check:type-source-resolution -> OK, 94 tsc programs", "check:published-files -> OK, 69 publishable packages", "check:page-declaration-shape -> OK", "check:type-check-coverage -> OK, 65/78 packages type-checked", "check:type-check-debt -> --re-measure OK, 31 ledger entries re-measured in 230.7s, 1687 raw tsc errors, none above its recorded number (on a fully built workspace closure)", "check:changeset-gate-self-tests -> OK", "check:objectui-changeset -> OK", "scripts/check-empty-changeset.mjs -> OK, 1 declaring changeset added", "scripts/check-adr-0087-registration.mjs -> OK, no declared-breaking changeset", "scripts/check-changeset-no-major.mjs -> OK, no major bump", "scripts/check-ci-filter-parity.mjs -> OK, 109 declared cross-package globs covered", "scripts/check-comment-mask-adoption.mjs -> OK, 20 strippers recorded", "scripts/check-plugin-teardown-shape.mjs -> OK, 63 Plugin implementations", "scripts/pm/release-rehearsal-clone.mjs --self-test -> OK", "scripts/docs-audit/check-affected-docs.mjs -> OK", "scripts/docs-audit/check-drift-comment.mjs -> OK" ], "red": [], "notMeasured": [ "CI gate-job convergence - not waited for, per the 2026-08-10 maintainer ruling that CI convergence is the PM's, not the dev's. Report is delivered at draft-PR time.", "check:api-surface / check:export-origins / check:docs - NOT owed and therefore not run, on the measurement above (zero added-or-removed `export` lines in the whole diff). Stated as a derivation, not as a skip.", "The mongod-backed driver-mongodb suites (143 tests) are skipped by the repo's own #5517 server gate, not by me. The new mongodb pins deliberately run WITHOUT a server, via a recording fake Db plus a BSON round trip under the driver's own serialization setting." ] }, "ablations": [ { "leg": "driver-memory - memory-driver.ts reverted to origin/main, both packages rebuilt", "predictedDirection": "RED", "predictedCount": "8 of 10 fail; survivors are 'filter semantics are unchanged' and 'null stays a VALUE'", "observed": "RED, 8 failed | 2 passed (10) - survivors exactly as predicted", "mutationProvedOnDisk": "anchored grep -cF BEFORE reading any result: withoutUndefinedOwnKeys 3 -> 0, `this.toStoredRecord(object, {` 3 -> 0, and the pre-fix call form `this.toStorageForms(object, {` back at 3. git diff --stat used only as corroboration, never as the proof.", "rebuild": "Both packages rebuilt; reach proved with scripts/ablation-dist-preflight.mjs --absent (marker absent from all 6 built files) on the mutation leg and present (2 built files) on the restore leg.", "restore": "trap restore EXIT INT TERM; markers back to 3/6; git diff on the two mutated files EMPTY (verified, printed as GIT_DIFF_EMPTY=yes). Restored from pristine `cp` copies - never `git checkout HEAD -- .` (#12204)." }, { "leg": "driver-mongodb - mongodb-driver.ts reverted to origin/main, both packages rebuilt", "predictedDirection": "RED", "predictedCount": "4 of 6 fail; survivors are the pure-BSON mechanism pin and 'null is a VALUE'", "observed": "RED, 4 failed | 2 passed (6) - survivors exactly as predicted", "mutationProvedOnDisk": "anchored grep -cF BEFORE reading any result: withoutUndefinedOwnKeys 6 -> 0.", "rebuild": "As above, --absent proved on the mutation leg, present on the restore leg.", "restore": "Same trap; marker back to 6; git diff empty." }, { "leg": "ablation re-run against the STRENGTHENED (toStrictEqual) pin", "predictedDirection": "RED, same counts", "predictedCount": "8 of 10 and 4 of 6, unchanged", "observed": "RED, 8/10 and 4/6 - unchanged, but those failures are now carried by the CONTRACT assertion rather than only by the key-list/`in` diagnostics", "note": "Run with dist carrying the FIX while src was ablated, versus run 1 where dist was ablated too. Identical counts both ways, which independently confirms these two test files' verdicts are src-mediated (intra-package relative specifiers), exactly as the import form predicts. The dist-mediated consumer is the reproduction probe, and that is why both packages were rebuilt on both legs." }, { "leg": "PREDICTED REVERSAL, reported as observed", "observed": "The probe's line '>>> mongodb create() and find() AGREE about status' prints TRUE in the ablated state as well, because it compares key PRESENCE only and both doors say 'present'. The line is insensitive to this defect; the split lives in the VALUES (UNDEFINED vs null). Predicted in writing before the run and reported rather than quietly dropped." } ], "tests": "All exit codes captured BEFORE any pipe (the runner writes each gate to its own file, then `local ec=$?`); every verdict quoted from the gate's own output line, never from a bare $?. Heavy work serialised through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 on every acquisition; longest wait 8m16s). Gate union re-derived and ratchet families re-run at final head 1aad15dba5 AFTER the last commit. Reproduction, ablations and green suites as detailed in `ablations` and `gates.green`.", "openQuestions": [], "out_of_scope_findings": [ "filed as #12637 (sub-issue of #12627): worktree rescue fired against a LIVE seat and left a WIP commit whose message asserts 'no gate was run, no ablation exists, the fail-OPEN sweep is not evidenced' in the present tense - all three false when written. Two defects: the rescue has no liveness precondition (uncommitted changes are evidence of work in progress, not of death - the same observation in both cases), and the prescribed WIP message asserts more than the rescuer can observe. Matters more once #12627 lands and makes this a standing step. Not labelled; left for triage." ], "deviationsDeclared": [ "The REST issues channel is unavailable from this seat: GET /repos/objectstack-ai/objectstack/issues with $GITHUB_TOKEN returns HTTP 403 'GitHub access is not enabled for this session'. The dedup search for the finding above therefore used ONE MCP search_issues call instead of the prescribed REST listing. Declared rather than silently substituted. Git push credentials are unaffected (both pushes succeeded).", "`gh` is not on PATH in this container; the draft PR was opened via the MCP create_pull_request tool.", "The first report comment's HTML-comment marker was stripped by GitHub's sanitizer; verified by reading it back. Re-posted here with a literal-text marker, per the standing clause." ] }
Generated by Claude Code
Generated by Claude Code
Restart-when: a THIRD consumer of "is this declared field present" is written outside the canonical binder (a bare
in/hasOwnPropertypresence test against a declared field underpackages/, excludingmaterializeDeclaredFields), OR any consumer measured on this input class is fail-OPEN rather than fail-closed, ORdriver-memory/driver-mongodbcome out of the #5499 investment freezeFiled unassigned by the dev seat implementing #8489, per the maintainer's 2026-08-16 ruling on that card: "Option C filed as the durable follow-up (driver-side normalization of own-key-
undefinedas a storage-contract defect) — lands inpackages/drivers/*, its own card in that lane; the implementing dev or the identity seat files it with a backlink."Follow-up to #8489. ⛔ This records the follow-up only — no fix commitment; the lane grades it.
The shape
InMemoryDriverpreserves an explicitly-undefinedproperty throughcreateand hands it back fromfindas an own key holdingundefined. Measured onorigin/mainin a worktree at2d11ddbe3, against the builtdriver-memorydist:The control leg, same driver, same object — a declared field simply never written — behaves correctly and omits the key entirely:
SqlDrivercannot express the defective shape at all: a SQL NULL arrives asnull, which is a value. So the divergence is between JS-backed drivers and SQL-backed ones, on a row shape only the former can emit.Why it is a contract defect and not a curiosity
The two states are supposed to be distinguishable and each has a defined meaning — "the key is absent" versus "the key holds a value". An own key holding
undefinedis neither, and every consumer has to pick a reading:@objectstack/formulaengine, reads it as absent:has(record.status)isfalse, andrecord.status == nullfaultsNo such key: status.materializeDeclaredFields(packages/objectql/src/declared-fields.ts) reads it as absent too, deliberately and by documented design — "undefinedcounts as absent (not just a missing key): CEL treats an own key holdingundefinedexactly as it treats no key at all."name in recordpresence test reads it as present. That is what made it a live divergence in plugin-sharing re-derives materializeDeclaredFields instead of importing it — a second copy of the declared-field binding contract, already diverged #8489:plugin-sharingre-derived the declared-field binder with anincheck, and the two copies disagreed on exactly this one input class.So the row shape is a fork in the road that every consumer must independently know about, and the platform's own helper carries a dedicated sentence to survive it. That is the cost this card proposes to remove at the producer.
Why fixing it at the producer is the durable half
#8489 converged
plugin-sharingonto the canonical binder, which resolves the disagreement for that seam. It does not stop the shape being emitted, so the next consumer that reasons about key presence re-acquires the bug. Normalising at the driver — a declared field whose stored value isundefinedis either dropped from the row or returned asnull— would make the two binder implementations provably agree on every reachable input, and would let consumers stop carrying the special case.Scope, as understood from outside the lane
packages/drivers/*.driver-memoryis the measured emitter;driver-mongodbis the other JS-backed driver and is worth measuring the same way before deciding, since the two share the family. Note thatdriver-memory/driver-mongodbinvestment was frozen under #5499 — whether that freeze covers this is a lane call, not mine.undefinedproperty tocreate—undefineddoes not survive JSON, so it cannot arrive over the wire. No production consumer was measured. Direction of the consequence at the one consumer studied so far (#8489's eligibility gate) was fail-closed before that card and is fail-closed after it.Related
undefined比较数在仓内有五种读法 —— #6050 只在 driver-sql/turso 落了拒收,formula 读作「键缺失」、read-scope-sql 编成= NULL、driver-memory 读作 null #6125 — the same word, the other side of the query:undefinedas a filter COMPARAND had five readings across the repo (closed;read-scope-sqlhalf landed in PR fix(analytics): read scope 里的undefined比较数改为拒收,不再编成绑了 undefined 的合法 SQL (#6125) #6390). This card is aboutundefinedin a RETURNED ROW VALUE, which that card did not cover.$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 —driver-memoryvsformulaon "the field has no value", also matcher-side (closed).