Repository navigation
fix(driver-mongodb): MongoDBDriver compiles the whole-day comparison it is handed; formula's copy stops on a measured answer move - #21105
Conversation
…alendarDay, F7 lteBound) Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…lowering; F7 stops on a measured answer move (lteBound restored) Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…a-mongodb-formula
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 083009c060133b6d4239615fc20f42f9ac885df0 && git checkout 083009c060133b6d4239615fc20f42f9ac885df0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2742e53709b790ee76d2e8d1d07e23aa68de2110 d1137d7ea9c03481058e3a04d65cc13faf4873bc && git checkout -B drift-repro 2742e53709b790ee76d2e8d1d07e23aa68de2110 && git merge --no-ff d1137d7ea9c03481058e3a04d65cc13faf4873bc
node scripts/docs-audit/affected-docs.mjs --json 2742e53709b790ee76d2e8d1d07e23aa68de2110 |
Contract reviewServed-tier: PR #21105 (card #20822 group 3a, ① Derived judgmentsF6, deleted — right. At the base, Accept-set changes the diff implies, each judged:
F7, measured and stopped — right as the half the PR says it is. The report names the two paths no seam lowers: the RLS write Carried, not this PR's — right: the three #20987 engine faces move to group 3b under the claim's H3 fallback ( Check-runs on the head (31). 16 ② Semver level
③ Boundary flagsDev flags (the report's
Acceptance notes:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…rness text state each case in words instead of a tracker number (stage 5b) (objectstack-ai#21139) Fixes objectstack-ai#20513 Clause-②: no **Stage 5b, the last stage of this lane under the maintainer's A / A ruling on the card (5902360492): the `src/`-shipped test strings.** These are the 28 literals (32 occurrences in 6 files) that PR objectstack-ai#20924's body inventoried as stage 5b and release 5922539397 restates: shared case labels, a suite title, two contract `why` texts and the mongod test-harness text. After this PR the `domain:engine` packages hold zero rows in `scripts/doc-authoring-prose-id.baseline.json`. Text only: no error `code`, export, HTTP status, case value or control flow moves. ## What this does Each label or harness line sent the reader to a tracker number for the reason behind it. In form D, as stages 1 to 5a applied it, the number goes. Where the sentence already stated the case, only the citation goes (17 literals). Where the sentence leaned on the number, it now says the case in words (11 literals): | Where | Cited | The text now says | |---|---|---| | `driver-mongodb` `test-mongod.ts` skip reason | 5517 | "retired from default test runs because concurrent downloads made green runs exit 1" | | `driver-mongodb` `test-mongod.ts` abandoned-download warning | 5517 | "Ignoring the MongoDB binary download abandoned by a concurrent-download race" | | `ENGINE_DELETE_DISPATCH_CASES`: where.id plus a CAS operator, `multi: false` | 11009 | "refused, the by-id path would drop the CAS guard" | | `ENGINE_FINDONE_PREDICATE_CASES`: an empty `where` object | 3896 | "read as match-every-row, so NOT a predicate" | | `ENGINE_UPDATE_DISPATCH_CASES`: payload id beside a DIFFERENT scalar where.id | 11142, 5748 | "refused; the payload id no longer silently wins over a where.id naming another row" | | same table: payload id beside an `$in` where.id, no multi | 11230 | "refused; the by-id write would silently drop the declared row SET" | | same table: payload id beside a NULL where.id | 11230 | "refused; a declared where.id that is not one primary key is never silently dropped" | | same table: payload id beside a FALSY scalar where.id | 11142, 11230 | "(it is a scalar, so neither the different-row nor the non-scalar refusal applies)" | | same table: where.id plus a CAS operator, `multi: false` | 11009 | "the redeliver shape, refused rather than dropping its CAS guard" | | same table: payload id plus an extra where predicate, no multi | 11009 | "refused; the by-id write would silently drop the predicate, through the payload door" | | `OBJECT_SCHEMA_MASK_CASES` `write-capable-caller/exempt` `why` | 7020, 6603 | "D4 is DERIVED from the schema write gate (`manage_metadata`)" | Citation only: the contract suite's `serialized-form identity` group title; 3 delete, 2 findOne and 9 update labels; the empty-readable-set `why`; the mongod acquisition-timeout message. Each new clause was read against today's code: `resolveEngineUpdateDispatch` (the different-row refusal fires only on a truthy scalar where.id, the non-scalar refusal only on a declared non-scalar), `ObjectQL.requireFindOnePredicate` (absent, null and an empty object are read as match-every-row), and `OBJECT_SCHEMA_WRITE_CAPABILITIES` (`manage_metadata`, the key the D4 exemption is derived from). The `test-mongod.ts` docblock "Names the issue and the switch" now says "Names the reason and the switch", so it stays true. No other comment or docblock moves; code comments still cite ids, as in every earlier stage. **Re-pins (H4).** The two `objectstack-ai#5517` number pins in `mongodb-memory-server-gate.test.ts` now assert the text that replaced the number: the skip line contains "retired from default test runs because concurrent downloads made green runs exit 1", and the guard's warning contains "download abandoned by a concurrent-download race". Neither pin was deleted. Both were proven able to fail (below). ## Ledger (`scripts/doc-authoring-prose-id.baseline.json`) Recomputed with `node scripts/check-doc-authoring.mjs --census-ledger` (exit 0: no growth refusal) and copied into place. The diff is 25 deleted lines, nothing added. | File | Before | After | |---|---|---| | `drivers/driver-mongodb/src/test-mongod.ts` | 3 | 0 | | `metadata-core/src/contract-suite.ts` | 1 | 0 | | `metadata-core/src/engine-delete-dispatch.ts` | 4 | 0 | | `metadata-core/src/engine-findone-predicate.ts` | 3 | 0 | | `metadata-core/src/engine-update-dispatch.ts` | 18 | 0 | | `metadata-core/src/object-schema-fls-contract.ts` | 3 | 0 | | **This stage** | **32 in 6 files** | **0** | Whole ledger: files 175 → 169, pairs 441 → 428, occurrences 653 → 621. Gate pinned sites 549 → 521. No other file's row moves. After each of the four merges of `origin/main` (below) the recomputed ledger was byte-identical to the committed one. ## Hypotheses, measured - **H1, held.** At `origin/main` `9c8b65aa2` the six files held exactly the inventory: `objectstack-ai#5517` ×3; `objectstack-ai#7856` ×1; `objectstack-ai#11009` ×4 (delete); `objectstack-ai#11767`, `objectstack-ai#3896`, `objectstack-ai#4346` (findOne); `objectstack-ai#11009` ×6, `objectstack-ai#11142` ×4, `objectstack-ai#11230` ×4, `objectstack-ai#5748` ×4 (update); `objectstack-ai#3545`, `objectstack-ai#6603`, `objectstack-ai#7020` (FLS). The gate's census listed 28 literals carrying 32 occurrences. - **H2, held.** The recomputed ledger has 0 rows under `packages/objectql`, `core`, `formula`, `metadata*`, `platform-objects`, `drivers/driver-*` and `plugins/plugin-pinyin-search` (the `domain:engine` path set). The lane's ledger is empty. The whole ledger is not, so the gate's stale-baseline arm keeps its blindness floor. - **H3, nothing keys on the old labels.** Each of the 28 old literals was searched byte for byte across the repo (`dist` excluded). Each was found only in this stage's own source files (two delete labels have update twins), with two exceptions. The tail `download — objectstack-ai#5517)` also sits in a comment in `mongodb-pipeline-evaluator.testkit.ts`. `serialized-form identity (objectstack-ai#7856)` also titles an independent `describe` in `metadata-core/test/canonicalize.test.ts`, a test file outside the ledger. Both are left alone. No CI workflow, `package.json` script, vitest config, snapshot or skip list carries the labels or a `-t` filter on them. The cited numbers appear elsewhere only in workflow comments, an ADR anchor's invariant prose and a merge-parent sha. The labels reach other code only as test names: objectql's `engine-*-dispatch` / `engine-findone-predicate` parity loops, `service-queue`'s `it.each`, and `assertObjectSchemaMaskCase`'s failure text. Each label stays unique within its table (35 / 19 / 18). - **H4, held.** See Re-pins. ## Changeset `.changeset/20513-metadata-core-case-labels-state-the-case.md`: `patch` for `@objectstack/metadata-core`. Its case tables ship in `dist/index.*`, and the contract suite and FLS table ship in `dist/testing.js` (measured after build: the new labels found in those files). There is **no `@objectstack/driver-mongodb` entry**, because `test-mongod.ts` is not published. The package builds `src/index.ts` only, and only test files import `test-mongod.ts`. Measured after build: `mongodSkipReason`, "retired from default test runs", "concurrent-download race" and "waiting for the MongoDB binary" are each in 0 files under `packages/drivers/driver-mongodb/dist`. The positive control `MongoDBDriver` is in 4. ## Text-only proof A TypeScript-AST skeleton of each changed `.ts` file compares merge base `670680e93` with head `24b72b809`. In the skeleton, every string literal and template text is one placeholder, pure-literal `+` operands collapse, and comments are never read. Result: 7 of 7 SAME, with token and literal-slot counts identical per file. Control: the same tool reports DIFF on `rest` `import-template.ts` across `6f1f1c103`, a real code change. ## Tests (head `24b72b809` unless noted) - Build: `turbo run build` over `./packages/*` and `./packages/*/*` under the verify lock, 71/71, after the last merge. - `@objectstack/metadata-core`: 16 files / 298 tests passed; `typecheck` exit 0. - `@objectstack/driver-mongodb`: 30 files passed and 5 skipped, 675 tests passed and 172 skipped (the skips need a MongoDB server); `typecheck` exit 0, including `check:test-typecheck`. - Consumers of the case tables, against the rebuilt `metadata-core` `dist`: - `objectql`, 7 files / 181 tests: `engine-delete-dispatch`, `engine-update-dispatch`, `engine-findone-predicate`, `engine-update-by-id-payload-id`, `engine-update-multi-payload-id`, `engine-write-not-found-gate` and `validation/operator-object-write-value`; - `service-queue` `db-queue-adapter`, 38; - `rest` `meta-object-fls`, 95; - `runtime` `domains/meta-object-fls`, 85; - `metadata-fs` `contract`, 31; - `metadata-protocol` `sys-metadata-repository.contract` and `protocol.update-path-id-wins`, 64. - A verbose run before the merges showed the parity loops naming the new labels. objectql's tests resolve `metadata-core` through `dist`, so this shows they read the rebuilt build. - Re-pin mutations, one-off, on the committed head (`2729901db`), through `scripts/ablation-replace.mjs` in WRAP mode (restore armed on EXIT/INT/TERM): - Skip-reason anchor ×1 → ×0: `mongodb-memory-server-gate.test.ts` 1 failed / 7 passed. - Warning anchor ×1 → ×0: 1 failed / 7 passed. - Each restore: blob equals HEAD's (`22a95643c8`), and `git diff HEAD` is empty. After both, `git diff HEAD` was 0 bytes and `git status --porcelain` 0 lines. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) at `24b72b809` derived 69 commands. All 69 ran one at a time from the worktree, each exit 0. `--ran` reports "69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN". Among them: - `check:doc-authoring`: 521 pinned sites across 169 files, no growth, no burn-down unrecorded; - `check:driver-conformance`: 50 covered cells, 0 DEBT, 0 exempt, the same reading as before the change; - `check:engine-double-contract`, `check:dual-build-cjs-loads` (105 require entry points across 66 packages load, read from the post-merge build), `check:nul-bytes`, `check:type-check-coverage` and `check:type-check-debt`. After the first merge (`packages/spec` moved on main): `pnpm --filter @objectstack/spec check:generated` reported all 15 artifacts up to date. Narrowed lint: `eslint --no-inline-config --format json` over the 7 changed `.ts` files reported 7 files, 0 errors and 0 warnings, none ignored (counts from eslint's JSON). The resolved `parserOptions` are `ecmaVersion: latest, sourceType: module`, with no `project` or `projectService`. So no type-aware rule runs, and this diff cannot move an untouched file's verdict. Repo-wide `pnpm lint` is CI's. NOT MEASURED locally (CI's): the live MongoDB cells of `driver-mongodb`, the Test Core shards, Dogfood, and the workspace type-check lanes. ## Acceptance notes - `origin/main` was merged four times while this was open (`1d55ee66e`, `4f1c99822`, `4516a0643`, `24b72b809`; the third brings in objectstack-ai#21097). None of the incoming commits touches a file this PR changes. They touch `objectql`, `rest`, `runtime` and `metadata-protocol`, so those consumer suites were re-run at the final head. `main` moved again after the gate union; the queue rebuilds on it. - PR objectstack-ai#21105, in `driver-mongodb`'s filter and temporal suites, landed before the first merge and shares no file with this PR. - `metadata-core/test/canonicalize.test.ts` titles its own `describe` `serialized-form identity (objectstack-ai#7856)`, and `mongodb-memory-server-gate.test.ts` titles two `describe` blocks `[objectstack-ai#5517] …`. Both are `*.test.ts` files, which the ruling's third category (`src/`-shipped test strings) and the ledger do not cover. They are left as they are: noted, not filed. - The lane children objectstack-ai#20749, objectstack-ai#20751 and objectstack-ai#20753 carry `Blocked-by:` lines naming this card. They unlock when it leaves the open state on merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20822
Clause-②: no
Group 3a of #20822 (#5930 step 4, the per-face deletions), under ruling 5902355785 (D4 (b): "drivers receive the seams' lowered input; no permanent local guards on faces that bypass a seam") and ADR-0053 D-D1 as amended. This PR does not close the card: group 3b (F8
having, serial onhaving-filter.ts) does.What this PR does
driver-mongodb: deleted.translateFieldOperators(mongodb-filter.ts) no longer applies the whole-day upper bound itself.MongoDBDrivercompiles the comparison it is handed.formulalteBound: stopped and reported, not deleted. Deleting it moves an answer on a seam-fed path (the card's stop rule). The measurements are below, and the decision is asked in the report.matches-filter.tshas no net change in this PR.$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987's engine faces move to group 3b (triage pointer 5922592744). These are the mongodb$contains$regex, formula'scase '$contains'and theengine.tsdelete-probe docblock. The shared membership module (json-membership-sql.tsin@objectstack/core) is not onorigin/main: ate07566b73,git ls-tree -rnames 0 paths matchingjson-membership. Also,@objectstack/formuladepends on@objectstack/specalone, so an import from core would add a package edge.H1: the sites, at
origin/main6f578888anextUtcCalendarDayoccurs 4 times inmongodb-filter.ts: the import (:27), thetranslateFieldOperatorsdocblock (:1123), and 2 call sites. The call sites are the$ltearm (:1211) and the$betweenarm (:1305). The card's "4 sites" are these 4 occurrences. All 4 are gone. The@objectstack/coreimport is gone with them, and the clobber-class table inassembleLoweredWrites' docblock loses its$ltrow.lteBoundhas 2 call sites, the$ltearm (:668) and the$betweenarm (:673), plus the function itself (:867). All 3 are kept.H2: direct callers, with their answers before and after
F6. No in-repo production caller hands
MongoDBDrivera bare-day$lteor$betweenoutside a seam:secret-reference-union.ts(where: { type: 'datasource' }, an equality) andsecret orphans;LifecycleServicearchive ($ltan ISO instant);DatabaseLoader.queryHistoryin driver mode, which has been its own seam since group 1b.Their answers do not move. The answer that moves belongs to a hypothetical direct call such as
driver.find('task', { where: { created_at: { $lte: '2026-07-28' } } })on a declareddatetimecolumn:$lt2026-07-29 midnight, so the whole day was kept;$lte2026-07-28 midnight, the comparison as written (item 5).Through the engine seam the answer is
$ltthe next day both before and after.F7. Its callers were measured with the copy deleted (
d045f1d6c), then the copy was restored (394df163a):check(security-plugin.ts, step 3.6). It is fed by the RLS compile seam, which lowers only the guard'sdatetimecolumns (item 7). Its answer moves, as the table below shows.explain-engine.ts). Its filters come fromcomputeLayeredRlsFilter, so they too are compiled through the RLS seam. It evaluates the STORED record, where adatecolumn holds day text. No answer move was found.security-plugin.ts, step 3.7). It evaluates the organization wall only, with no temporal bound, so its answer is unchanged.having-filter.ts,compareWithReference). It hands F7{ $field }comparands, which the lowering never rewrites. Its answer moves for a referent that resolves to a bare day, as the second finding below shows.matchesFilterCondition. Under item 5 it would get the comparison it wrote.F7: the measured answer move
The probe is temporary and not committed. It runs through the public insert door: ObjectQL, SecurityPlugin and
SqlDriveron SQLite. The object hasdue_on: dateandsigned_on: datetime. One policy carriesusingandcheck, bothrecord.due_onat most'2026-01-05'. The typed RLS seam leaves thedatecolumn as written,{ due_on: { $lte: '2026-01-05' } }, asrls-shared-lowering-seam.test.tspins. A member inserts each value. The same value is also written by the system and read back by the member.due_on'2026-01-05'2026-01-05'2026-01-05T10:00:00.000Z'2026-01-05PERMISSION_DENIED/ 403new Date('2026-01-05T10:00:00Z')2026-01-05PERMISSION_DENIED/ 403'2026-01-06'(control)2026-01-06The record validator accepts both non-day spellings for a
datefield: aDate, and a string with a leadingYYYY-MM-DD. The driver stores them as that day. Thecheckjudges the raw image before the driver converts it.lteBoundis what reads the bound as the whole day there, on a column the typed seam does not rewrite. With it deleted, the write is refused although the read shows the stored row. ADR-0053 D-D1 item 9 says a copy is "idempotent on lowered input". That premise does not hold for this face.A second answer moves too. A
$lteagainst a{ $field, addDays }reference whose referent resolves to a bare day gets the whole day only fromlteBound. The lowering keeps any{ $field }comparand as written, so no seam carries this rule.matches-filter-field-reference-offset.test.tspins it ("a calendar day stays a calendar day, so a$ltestill covers the whole shifted day"), and that pin goes red with the copy deleted.Existing suites with the F7 copy deleted (
d045f1d6c, formula rebuilt):matches-filter-temporal-conformance.test.tsand 4 inmatches-filter.test.ts. The 25th is the$fieldoffset pin above, which cannot be routed.datecolumn case, so only the probe shows it.having/ aggregate (src/having,src/engine-aggregate): 15 files, 573 passed.F6: what moves, and what does not
where,aggregations[i].filterand the RLS compile seam hand this driver$ltthe next day, a split$between, and$null: falseon9999-12-31. The deleted copy answered that input identically, as ablations A1 and A2 below show. Adatecolumn answers as before, because its stored day text orders the same either way.SqlDriver's (item 7's scope; seat answer 5915193659: "declare it in the changeset; it is not a decision"). On a registered object, a bare-day$lteor$betweenmaximum on a column that is not declareddatetimeis now compared as written. That covers atextcolumn holding ISO instant text and an undeclared column.lowerFilterCondition(where, { isDatetimeColumn }).mongodbinary in this container, andOS_TEST_MONGODB_MEMORY_SERVER_ENABLEDis off, as the card's recorded gaps state. The gated suites that calldriver.finddirectly are routed through a typed seam helper, as the sibling drivers' suites are:mongodb-temporal-conformance.test.ts(bothTEMPORAL_CASESandTEMPORAL_TIME_CASES, tokens resolved first) andmongodb-datetime-storage.test.ts(its 3 bare-day cells). Neither ran here.Pins, routed and flipped
mongodb-filter.test.ts. The calendar-day block is split in two. The first half is the direct call, compiled as written: a bare-day$lte, a bare-day$between, the authored array form, and the last supported day for both. The second half is the seam-fed control, every former cell throughlowerFilterConditionwithcreated_atdeclareddatetime, with the same documents as before.mongodb-operator-key-clobber.test.ts. The enumeration rows change: a bare-day$ltenow writes$lte, a bare-day$betweennow writes$gteand$lte, and the$ltcontested row is removed. The "fourth member" cell is now two-sided:$ltebeside$ltis uncontested;$ltarrives as its own conjunct, and both bounds survive in either key order.mongodb-filter-shape-refusal.test.ts. A bare-day range is pinned as written, plus the seam-fed control.mongodb-time-storage.test.ts. A comment only.Ablations
Each ablation runs on the committed head through
scripts/ablation-replace.mjsin WRAP mode, nested: the import anchor first, then the arm. The driver-mongodb suite imports./mongodb-filter.jsfromsrc, so no dist is involved.$ltearm restored: 5 red, exactly the direct-call$ltecells (3 inmongodb-filter.test.ts, 2 in the clobber file). Every seam-fed cell stayed green.$betweenarm restored: 4 red, exactly the direct-call$betweencells (2 inmongodb-filter.test.ts, 1 in the shape-refusal file, 1 clobber row). Every seam-fed cell stayed green.de35f1ee395candgit diff HEADis empty.Compile surfaces
driver-mongodbtranslateFieldOperators: the whole-day copy is deleted. It compiles as handed, and seam-fed answers are unchanged (A1, A2).formulamatchesFilterCondition: unchanged. The stop is reported, andlteBoundstays until the decision.engine.tsdelete-probe docblock (referenceProbeFilter): unchanged. It moves to group 3b with the membership items.Tests
The full figures are in the report comment on #20822, measured at
d1137d7ea.pnpm --filter @objectstack/driver-mongodb typecheck: exit 0, includingcheck:test-typecheck.check:driver-conformance, before and after: OK, 50 covered cells, 0 DEBT, 0 exempt.eslint --no-inline-config --format jsonover the 8 changed.tsfiles gives 8 files, 0 errors and 0 warnings.eslint.config.mjsnever enables type-aware linting, so no untouched file's verdict can move. The fullpnpm lintis CI's.Acceptance notes (noted, not filed)
@objectstack/coreis now test-only indriver-mongodb. Its lastsrcimport was the deletednextUtcCalendarDayone, and the remaining importer ismongodb-temporal-conformance.test.ts. The manifest still lists it underdependencies. Moving it todevDependenciesis a manifest and lockfile edit outside this card. Carrier: none.filter.zod.tsaround:434,:734and:750namesSqlDriver.calendarDayUpperBoundRewrite/calendarDayBetweenRewrite, and the case title inread-scope-shared-lowering-seam.test.tsconcerns the RLS change of group 2. Release 5924791165 places them with the last group PR, so the carrier is group 3b.query-syntax.mdx's sentence on direct calls names only theSqlDriverfamily. It reads: "A filter handed directly toSqlDriveror a driver built on it … is compared as written".MongoDBDriver(this PR) andInMemoryDriver(group 1b) now answer the same way. The sentence is incomplete but not false. Carrier: group 3b.Generated by Claude Code