Skip to content

[drivers] a driver returning a declared field as an own key holding undefined is a storage-contract defect — Option C from #8489 #9276

Description

@os-project-manager

Restart-when: a THIRD consumer of "is this declared field present" is written outside the canonical binder (a bare in / hasOwnProperty presence test against a declared field under packages/, excluding materializeDeclaredFields), OR any consumer measured on this input class is fail-OPEN rather than fail-closed, OR driver-memory / driver-mongodb come out of the #5499 investment freeze

⚠️ Restart condition added 2026-08-20 by the PM seat; nothing else in this body changed. An incremental unlock scan found this card in pm:on-hold with no Restart-when: line at all — a state no mechanism could ever wake it from. It is kept on hold (not closed) because the trap it records sits on a security-adjacent path; the exit above names the three ways it becomes live. Maintainer authorization for the disposition, verbatim: 「接受你的建议」.

⭐ The second leg is the one that matters most. The only consumer measured so far (#8489's eligibility gate in plugin-sharing) was fail-CLOSED both before and after that card — nobody was over-granted. A fail-open consumer on this input class is a different card entirely, and it should not wait for this one.

Filed 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-undefined as a storage-contract defect) — lands in packages/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

InMemoryDriver preserves an explicitly-undefined property through create and hands it back from find as an own key holding undefined. Measured on origin/main in a worktree at 2d11ddbe3, against the built driver-memory dist:

InMemoryDriver.create('article', { id: 'a1', title: 't', status: undefined })
InMemoryDriver.find('article', {})  ->  row own keys : [ id, title, status, created_at, updated_at ]
                                        'status' in row : true
                                        row.status      : UNDEFINED

The control leg, same driver, same object — a declared field simply never written — behaves correctly and omits the key entirely:

InMemoryDriver.create('article', { id: 'a2', title: 't2' })
InMemoryDriver.find(...)  ->  row own keys : [ id, title, created_at, updated_at ]
                              'status' in row : false

SqlDriver cannot express the defective shape at all: a SQL NULL arrives as null, 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 undefined is neither, and every consumer has to pick a reading:

  • CEL, measured against the real @objectstack/formula engine, reads it as absent: has(record.status) is false, and record.status == null faults No such key: status.
  • materializeDeclaredFields (packages/objectql/src/declared-fields.ts) reads it as absent too, deliberately and by documented design — "undefined counts as absent (not just a missing key): CEL treats an own key holding undefined exactly as it treats no key at all."
  • A plain name in record presence 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-sharing re-derived the declared-field binder with an in check, 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-sharing onto 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 is undefined is either dropped from the row or returned as null — 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-memory is the measured emitter; driver-mongodb is the other JS-backed driver and is worth measuring the same way before deciding, since the two share the family. Note that driver-memory / driver-mongodb investment was frozen under #5499 — whether that freeze covers this is a lane call, not mine.

⚠️ Severity is for triage to grade, and a filing-time grade is unreliable in both directions. Recording honestly what is and is not established: the shape is reachable through a first-party shipped driver (measured above), but reaching it requires in-process code passing an explicitly-undefined property to create — undefined does 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

Activity

  1. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Triage (first touch): pm:on-hold, type Bug (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 (undefined does 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

  2. added theissue type on Aug 17, 2026
  3. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    Un-held — the restart condition was already satisfied nine days before it was written

    Surfaced by the #12393 dev while scoping the VALUE_ROUNDTRIP case-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-hold with Restart-when: … OR driver-memory / driver-mongodb come out of the #5499 investment freeze. Measured against the anchor card's own comments:

    event when
    driver-mongodb unfrozen — 「5499 解冻 mongodb 相关开发,继续全速推进」 2026-08-11 04:34Z
    driver-memory unfrozen — 「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_ROUNDTRIP case-set: an explicitly-undefined declared 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

  4. self-assigned this
    on Aug 26, 2026
  5. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    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-mongodb come 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 holding undefined as absent — CEL (has(record.status) is false) and materializeDeclaredFields, the latter by documented design. Dropping the key is the reading that matches what the platform already believes; returning null would 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-mongodb is 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 match driver-memory. SqlDriver cannot express the shape at all (a SQL NULL arrives as null, a value), so the divergence is JS-backed versus SQL-backed by construction.

    Serial check: packages/drivers/driver-memory and driver-mongodb. The open #12591 holds sql-driver.ts only; #12578 is packages/objectql; #12559 is in the i18n extract configs. No overlap.


    Generated by Claude Code

  6. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    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 written
    

    The 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-mongodb measurement in the record. The card asked for mongodb to be measured rather than assumed to match driver-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

  7. os-warren commented on Aug 27, 2026

    @os-warren
    Collaborator
    {
      "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

  8. os-warren commented on Aug 27, 2026

    @os-warren
    Collaborator

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions