Repository navigation
fix(driver-memory): a no-value row satisfies $nin / $notContains in the reference matcher (#13166) - #13356
Conversation
…he reference matcher The reference matcher `driver-sql` was aligned TO for #5146 diverged from the platform's settled INCLUDE direction in 3 of 6 measured cells. Two independent causes, one per reading of "no value": 1. `checkCondition`'s pre-switch guard allowlisted `$ne` but not `$nin` / `$notContains`, so a MISSING key short-circuited to "no match" before those arms ran. 2. The `$notContains` arm rejected a `null` on its `typeof value !== 'string'` TYPE test rather than on the predicate — the half the guard cannot reach. Both now answer one named predicate, `noValueSatisfiesNegation`. The four in-code statements that spelled the #5499 freeze as LIVE and used it to excuse the divergence are settled together with the disposition rather than re-tensed: the freeze dissolved 2026-08-11, and the "driver-mongodb does this too" clause was never true for this operator family. `$exists` is untouched — it is the neighbouring cell with a different backend list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…mory-matcher-no-value-negated-operators # Conflicts: # packages/services/service-analytics/src/read-scope-sql.ts
📓 Docs Drift Check2 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 22 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 7cd1357b7ac4c10f87d36a951a5ece5fc4eae3bf && git checkout 7cd1357b7ac4c10f87d36a951a5ece5fc4eae3bf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 5ed889a2bf3481406a3c8e2e596be6a41d495b36 && git checkout -B drift-repro 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 && git merge --no-ff 5ed889a2bf3481406a3c8e2e596be6a41d495b36
node scripts/docs-audit/affected-docs.mjs --json 3ec8646f1b71deaa95a6675ddbbbf6303c79ba05 |
PM review — accepted for landing (held in draft until CI is complete and green)Reviewed at head ScopeSeven files. Zero All three fences held, and fence 1 was cleared by measurementFence 1 was the real risk: this matcher is the reference the SQL family was aligned to in #5146, so the stop condition was whether aligning it overturns already-aligned SQL behaviour. It does not — Fence 2 (enrolment, not adding a row) was respected in the way that costs the most and is therefore worth naming: the backend is now fixed, which is the precondition — but enrolment lives in Fence 3 (
|
Docs-drift advisory — resolved by measurement. Nothing to change.The run lists nothing but flags that two changed files yielded no anchor ( Read on
The only ⇒ No hand-written page teaches the behaviour this PR corrects, and none carries the stale-freeze claim the four in-code sites did. Nothing filed, nothing edited. Recorded so the next reviewer does not have to re-derive it, and because the advisory's own "not a clean bill of health" caveat applies squarely here — the empty list was not the answer; the controlled search was. Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33297884013 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
队列弹出 —— ⛔ 不是本 PR 的失败。已定位到根因,且不重排,理由如下。失败的是什么队列构建 33297884013: 为什么不是本 PR 的本 PR 的文件面是:changeset ·
我在 #13258 落地前就把这一点写给了它的作者,指名了是哪一条断言、为什么会变红,并明确提出可以压住 #13329 让他们先落:#13258 (comment) 。他们选择继续。 ⇒ 合并队列把两者放进了同一条投机栈,所以 #13258 的红把排在它后面的本 PR 一起弹了出来。队列分诊 bot 自己也这么判:「已弹出 2 个 PR(#13258、#13356),但它们同属 1 条投机栈 ⇒ 1 次独立命中」。 ⛔ 为什么不重排修复不在我手里:它是另一个席位 PR 里的一条断言,⛔ 我不会去改别人的 PR,也没有可以「移植进本 PR」的补丁 —— 本 PR 根本不包含那个文件。 而队列分诊清单的第 3 条正对上:「等前面的 PR 落地或失败出队后再重排一次即可,⛔ 不要连续重排」。#13258 仍带着那条陈旧断言排在前面,现在重排只会再烧一轮全队列重建、然后以同样的原因再弹一次。 ⇒ 等 #13258 落地或出队,再重排一次。 我这一次重排额度未动用,留给那个时候。 本 PR 自身的状态未变
Generated by Claude Code |
…eference matcher (objectstack-ai#20822 group 4) (objectstack-ai#21395) Fixes objectstack-ai#20822 Clause-②: no objectstack-ai#20822 group 4, the card's last group: the comments and docblocks outside `driver-memory` that still named its retired reference matcher (`memory-matcher.ts`, retired by commit `8fec76a2b`) as a live surface. This is the carry group 1's ACCEPT put on the card's last group PR. Claim: the PM's `Claim:` comment 5948997842 (branch `claude/issue-20822-retired-matcher-pointers`). Cross-lane declarations: spec seat post (5949027331) and services seat post (5949038856). **The seat confirms `Fixes` at ACCEPT.** Hypothesis H1 (the site list is complete outside `driver-memory`) is falsified: 28 more sites outside the claim's file surface still name the matcher as live (25 comments and docblocks, one JSON ledger note, and 2 string literals in code). They are not edited here. They are listed under "Sites outside the claim's surface" below, and the route is the open question in the `os-dev-report` on objectstack-ai#20822. Base `11905a4f8b`; `origin/main` `db0cf2231b` merged once (merge `ff241ad71a`, no conflict, no file in this diff). Head `ff241ad71a`. Net diff against `main`: 10 files, +68 / -35. Not governed. ## What changes Comment and docblock prose only. A sentence that named the matcher as a live surface now names what carries the semantics today, measured per site, or says the matcher is retired. Historical sentences stay. | Site | Reading at base | Action | |---|---|---| | spec `filter-logic-conformance.ts:15` | live: the backend table's "In-memory matcher / `memory-matcher`" row | now "In-memory query path / `driver-memory` `normalizeFilterCondition`, then mingo", with the retirement in the same row (H3) | | spec `filter-comparand-shape.ts:127` | live: "the matcher's own answers ... are sealed behind this refusal" | past tense, plus the retirement | | spec `filter-comparand-shape.ts:142` and `:144` | live: "compares through JS coercion", "the matcher is not repaired" | past tense, plus the retirement | | service-analytics `objectql-strategy.ts:1687` | live: "`driver-memory`'s matcher ... pin" `{$not: {}}` | now `driver-memory`'s query path (`memory-driver-document-not.test.ts` pins it) | | service-analytics `objectql-strategy.ts:2055` (the unlock read it at `:2014`) | live: `memory-matcher.ts` "does" read `$regex` | past tense, until `$regex` and then the matcher were retired | | service-analytics `filter-normalizer-not-null-safe.test.ts:50` | live: points at the deleted `memory-matcher-not-null-safe.test.ts` | now `memory-driver-document-not.test.ts`, which holds its cells | | service-analytics `objectql-contains-canonical-operator.test.ts:31`, `:103`-`:110`, `:118`, `:305` | live: the mirror evaluates "the way `memory-matcher.ts` does", and "`driver-memory`'s `$regex` arm is deliberate and serves a real producer" | past tense; the producer's move to `$contains` is pointed at in `filter-refusal.ts`. `:118` and `:305` are in the same file but not in the unlock's list | | service-storage `attachment-read-visibility.test.ts:13` | live: "Mirrors `memory-matcher.ts` and `formula`'s `matches-filter.ts`" | now mirrors `formula`'s `matches-filter.ts`; the matcher is past tense | | service-storage `attachment-read-visibility.test.ts:326` | live: points at the deleted `memory-matcher-or-semantics.test.ts` | now `memory-driver-filter-logic-conformance.test.ts`, which holds its cases | | plugin-security `claim-seed-ownership.ts:91` | live, and wrong before the retirement: the `id IN (...)` scan attributed to `memory-matcher.ts` | now mingo's `$in`, which `InMemoryDriver` hands the list to (measured below) | | formula `matches-filter-not-null-safe.test.ts:17` | live: points at the deleted `memory-matcher-not-null-safe.test.ts` | now `memory-driver-document-not.test.ts` | | formula `matches-filter-not-null-safe.test.ts:120` | live: the matcher "answers the opposite" | the query path answers the same as this face (`memory-driver-document-not.test.ts` pins `['1']`); the matcher answered the opposite until PR objectstack-ai#13356 and is retired | | `docs/design/predicate-compilation-convergence.md:44`, `:56`, `:358` | census rows anchored at `3711e0b763` | past tense plus the retirement, as PR objectstack-ai#21336 did for the F7 row | Read and left as they are, because each is already historical or not a claim about a live matcher: spec `filter-logic-conformance.ts:184`, `:211` (a measurement table dated by its commits), `:244`, `:480`; `:492` names `memory-matcher-no-value-negated-operators.test.ts`, which still exists under that name and holds the live path's cells; `:503` and `:509` are string literals in the past tense; `filter-comparand-shape.ts:122`-`:124` (the reason for the 2026-08-31 ruling); formula `matches-filter-icontains.test.ts:91` ("what the reference matcher was moved onto"); the design doc's `:510` (the D6 decision row) and `:570` (a commit-table row). ## Measurements **H3, what `driver-memory` evaluates a filter with today.** `InMemoryDriver.find`, `count`, `updateMany`, `deleteMany` and the others call `convertToMongoQuery` (`memory-driver.ts:1421`). It runs `assertFilterConditionShape` (`filter-refusal.ts`), then `normalizeFilterCondition` (`memory-driver.ts:1600`), and hands the result to mingo's `Query`. `memory-driver-filter-logic-conformance.test.ts` runs `FILTER_LOGIC_CASES` through `InMemoryDriver.find`, and `check:driver-conformance` holds it. So the spec table's in-memory row names the query path. **`claim-seed-ownership.ts`'s `id IN (...)` sentence.** `normalizeFieldOperators`' `$in` arm (`memory-driver.ts:1862`) passes `$in` through. mingo 7.2.4's `$in` predicate (`operators/_predicates.js`) is built once per query and called once per document; each call runs `intersection([values, list])` (`util/_internal.js`), which fills a hash map from the whole list. So the sentence's "linear scan of the id list PER ROW" holds, through mingo, and the attribution to the matcher was wrong. **Code-token guard (PR objectstack-ai#21357's two readings), base `11905a4f8b` against the working tree at head, TypeScript 6.0.3.** Reading 1 is the parser's leaf nodes from a `forEachChild` walk, so comments are trivia and JSDoc is never visited; a leaf that is not a token is re-scanned with trivia skipped. Reading 2 is the token stream from a `getChildren` walk, with JSDoc nodes skipped. Identifiers and string, template and numeric literals are compared in full. - Real run over all 8 touched `.ts` files: 26,645 base tokens (reading 2), **0 files with a token change** (exit 0). - Comment control ("invents no second one" to "invents NO second one", `objectql-strategy.ts`): 0 files changed (exit 0). - Positive control, an identifier (`filterNodeToCondition` to `filterNodeToConditionX`, `objectql-strategy.ts`): DIFFER on both readings (exit 1). - Positive control, a string literal (a `FILTER_LOGIC_CASES` `name` gains an `X`, `filter-logic-conformance.ts`): DIFFER on both readings (exit 1). - Positive control, a numeric literal (`MAX_BULK_PER_ROW_HOOK_ROWS / 2` to `/ 3`, `claim-seed-ownership.ts`): DIFFER on both readings (exit 1). Each mutation went through `scripts/ablation-replace.mjs` in wrap mode (anchor 1 to 0). Each restore was proven: blob equal to `HEAD` and `git diff HEAD` empty. **`dist` reach (H4), three legs plus a determinism leg.** The five packages' `dist` files were hashed after each build. Every build exited 0 and ran under the shared verify lock. All four legs ran at `3ba971f1b6`; the later commits change no file in the five packages. - Leg 1: a turbo build of the five packages and their closure (20 tasks, all five cache misses). - Leg 2: the 8 changed files of the five packages back at their base blobs (8 of 8 proven equal), then each package's own `build`. - Leg 3: the 8 files restored (8 of 8 equal to their `HEAD` blob, `git diff HEAD` empty), then the same five builds. - Leg 4: `@objectstack/spec`'s own `build` again. It equals leg 3 in all 230 files, so that build path is deterministic. | Package | Changed | Added non-test lines found verbatim in `dist` | Leg 2 against leg 3 | Changeset | |---|---|---|---|---| | `@objectstack/spec` | 2 src files | 1 of 8: the table row, in `data/index.d.ts` and `data/index.d.mts` | 34 files differ: `data/index.d.ts` / `.d.mts`, 30 source maps (line offsets), 2 build-input hashes | `patch` | | `@objectstack/service-analytics` | 1 src + 2 test files | 5 of 5, in `index.js` / `index.cjs` (one also in `index.d.ts` / `index.d.cts`) | 6 of 6 differ | `patch` | | `@objectstack/plugin-security` | 1 src file | 0 of 4 | only `index.js.map` and `index.mjs.map` differ: line offsets, because the docblock grew by two lines; the maps carry no `sourcesContent` | none | | `@objectstack/formula` | 1 test file | none | 0 differ | none | | `@objectstack/service-storage` | 1 test file | none | 0 differ | none | `.changeset/20822-retired-matcher-pointers.md` therefore declares `patch` for `@objectstack/spec` and `@objectstack/service-analytics`, comment text only, with `Clause-②: no`. Each changeset sentence maps to a diff line: the spec table row at `filter-logic-conformance.ts:15`, and the two `ObjectQLStrategy` comments at `objectql-strategy.ts:1687` and `:2055`. ## Gates and tests (head `ff241ad71a`) - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `ff241ad71a` (10 paths against merge base `db0cf2231`) derived 89 commands. All 89 ran, each exit code captured before any pipe. 87 exited 0 on the first run. `check:dual-build-cjs-loads` and `check:i18n` exited 3 (PREREQUISITE NOT MET: unbuilt workspace packages), not a measurement. Both exited 0 after a whole-workspace build (`turbo run build --filter=!@objectstack/docs`, 72 tasks, VERDICT command-exit 0). `--ran` reports "89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN" and exits 0. - **Tests, under the verify lock, `vitest run --maxWorkers=2`:** - spec `--project local`: 598 files, 17,529 passed, 1 todo; - spec `--project repo`: 48 files, 849 passed; - formula: 43 files, 1,257 passed; - service-analytics: 167 files, 3,786 passed, 83 skipped; - service-storage: 41 files, 629 passed; - plugin-security: 159 files, 3,479 passed, 23 skipped. - **Typecheck:** `pnpm --filter ... typecheck` exits 0 for spec (with `check:scripts-typecheck` and `check:test-typecheck`), formula, service-analytics, service-storage and plugin-security. Formula, service-storage and plugin-security also run `check:test-typecheck`, and service-analytics' `tsc --listFiles` program holds 164 of its `__tests__` files, both touched ones included. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 8 touched `.ts` files plus `service-analytics/dist/index.js` as a control gives 9 results, 0 errors and 1 warning: the control's ignore notice. None of the 8 is reported ignored, and each resolves under `--print-config`. `eslint.config.mjs` never enables type-aware linting (its lines 327-328 say so), so a comment edit cannot move the verdict on an untouched file. The repo-wide `pnpm lint` is CI's run. - **Bytes:** `pnpm check:nul-bytes` exits 0. A control-byte scan over the 10 changed files finds none. ## Sites outside the claim's surface (round 0; superseded by patch round 1 below) Read at base `11905a4f8b` with every spelling: `memory-matcher`, `reference matcher`, `in-memory matcher`, `memory matcher`, `match()`, the `memory-matcher-*` test-file names, and "`driver-memory`'s matcher". `driver-memory`, `CHANGELOG.md` and `content/docs/releases/**` are excluded. Each of these still names the matcher as a live surface: - `service-analytics` `read-scope-not-null-safe.test.ts:43`: points at the deleted `memory-matcher-not-null-safe.test.ts`. It is on group 1's list and inside the ACCEPT's "service-analytics test docblocks", but not in the unlock's list or the claim. - `driver-mongodb` `mongodb-filter.ts:1151` ("it is the oracle both drivers agree with"). - `driver-turso` `remote-transport-boolean-identity.test.ts:43` and `remote-transport-not-operator.test.ts:40`. - `formula` `matches-filter.ts:729` (`asciiCaseInsensitiveContains` is "the same one `driver-memory`'s matcher ... call[s]"). - `objectql`: - `having-filter.ts:23`, `:26` and `:2064`; - `having-filter.test.ts:8` and `:60`; - `number-comparand-declared-type-door.ts:46` ("the memory matcher compares"; which face it means is ambiguous); - `validation/record-validator.ts:533` ("five hand-rolled shape tests", one of them the matcher); - `tsconfig.test.json:22` and `test-typecheck-debt.json:3` (a JSON string). - `plugin-security`: - `bootstrap-declared-capabilities.test.ts:39` and `bootstrap-system-capabilities.test.ts:29`; - `rls-check-stored-form.ts:40`, written after the retirement, so it probably means the query path. - `service-analytics` `strategies/filter-normalizer.ts:450` ("the in-memory matcher ... already held to" the table). - `spec`: - `filter.zod.ts:411`, `:940` and `:3111` (live implementation-status tables), `:1198`, `:1242` (the exported `asciiCaseInsensitiveContains` docblock) and `:3142` (the `$empty` table); - `filter-text-conformance.ts:342` ("both then and now"); - `ui/view.zod.ts:605` ("`match()` runs `assertFilterConditionShape`"). - **String literals**, outside this group's form: - spec `filter.zod.ts:470`, the author-facing refusal for a `null` ordering comparand. Measured on the published schema door: `FieldOperatorsSchema.safeParse({ $gt: null })` answers "... its reference matcher compares through JS coercion ...", in the present tense. - spec `filter-operator-vocabulary.test.ts:86`, an assertion message that prescribes editing the reference matcher. The governed `.claude/skills/pm-dispatch/references/compile-surfaces.md:16` stays on the seat post's protocol observation, as the ACCEPT placed it. 34 more hits outside the surface are historical (past tense, dated measurements, or string literals in the past tense). The `os-dev-report` on objectstack-ai#20822 lists them. ## Patch round 1 (head `54c8e70e88`) Section added by the `domain:engine#1` seat, from the dev's patch-round report (5952834398 on objectstack-ai#20822). - **The seat's answer to round 0's open question:** A, minus the two string literals (claim amendment 5950842658). The round corrects the 26 comment, docblock and ledger-note sites listed there, under the same rule. - `read-scope-not-null-safe.test.ts:43` comes first, in its own commit `68ca26224f`. It is the site inside group 1's ACCEPT carry that the seat's unlock had dropped, so **every site that ACCEPT carried is now corrected, and `Fixes objectstack-ai#20822` stands.** - The added surfaces are declared on the spec (5950854566) and services (5950863357) seat posts. - **Not edited:** - the two string literals `filter.zod.ts:470` (the author-facing null-ordering refusal) and `filter-operator-vocabulary.test.ts:86` (an assertion message). They are objectstack-ai#21397's (spec lane), so this PR stays comment-only; - `filter.zod.ts:3106` ("that driver's matcher", generic staging reasoning with an ambiguous referent); - the governed `compile-surfaces.md`. - **Commits:** - `1118eebbcd` merges `origin/main` `56238d890d` once, with no conflict. objectstack-ai#21372's hunks moved no listed line; - then `68ca26224f`, `1bf9b24f26`, `57d675df10`, and `54c8e70e88` (the changeset). The net diff against `main` is 28 files, +152/−81, not governed. - **Measured per site:** each rewritten sentence names what carries the semantics today, or says commit `8fec76a2b` retired the matcher. The probes behind the sentences that state a behaviour: - mingo's string ordering for `filter.zod.ts:411`; - mingo over the declared-number table for `number-comparand-declared-type-door.ts:46`; - `InMemoryDriver`'s null-or-missing match for the two `plugin-security` bootstrap tests; - the callers of `asciiCaseInsensitiveContains`. Historical sentences are untouched. - **Code-token guard** (both readings, base = merge base `56238d890`, so it covers the whole PR): - all 24 touched `.ts` files: **0 files changed**; - the two JSON files through `ts.parseJsonText`, with `test-typecheck-debt.json`'s `_note` masked: 0 changed. That `_note` is the text this round edits by design; an `entries` value control still DIFFERS under the mask; - controls (identifier, string, numeric, and JSON value) each DIFFER. - **`dist` reach** (three legs; the legs agree byte for byte in 276 of 276 files): | package | rewritten lines in `dist` | entry | |---|---|---| | `@objectstack/spec` | 12 of 21: 9 in the filter declaration chunk and 3 in the data bundles; `filter.zod.ts` and `view.zod.ts` also ship as source | `patch`, extended | | `@objectstack/formula` | 3 of 3 | `patch`, added | | `@objectstack/objectql` | 3 of 19 | `patch`, added | | `@objectstack/service-analytics` | its round-0 entry stands; this round's line reaches only the source maps | unchanged | | `driver-mongodb`, `plugin-security` | maps only, or nothing | none | | `driver-turso` | test files only | none | - **Tests and typecheck** at `54c8e70e88`: - the suites of `spec`, `formula`, `objectql`, `driver-turso`, `driver-mongodb`, `plugin-security` and `service-analytics` are green. `driver-mongodb`'s real-mongod suites are not measured here: there is no `mongod`; - every touched package's typecheck exits 0; - gates: 95 derived, 95 run, 0 not measured. ## Acceptance notes - **Build path, not this diff.** Leg 1 (turbo) and leg 3 (each package's own `build`) are both at the same text. They differ in 14 `@objectstack/spec` declaration files (`api`, `automation`, `contracts`, `marketplace`, `system` and two `node-executor.zod` chunks, `.d.ts` / `.d.mts`). Leg 3 equals leg 4 byte for byte, so each path is deterministic and the rewrite's effect was read on legs 2 and 3, which share a path. The difference between the paths is not explained here. - **`main` moved after the merge.** Seven more commits landed after `db0cf2231b` (to `69a12a0952` when this was written); none touches a file here. The derivation's only stale input among them is `scripts/sdui-manifest.record.json`. CI judges the merge ref. - **Contract review** is owed at tier: the path limb is `packages/spec/src/**`, non-test. The seat runs it. --- _Generated by [Claude Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…rds instead of a tracker number (stage 6) (objectstack-ai#21593) Part of objectstack-ai#20749 Clause-②: no Stage 6 of the `domain:spec` lane's share of the runtime-string burn-down (ruling `5902360492`, form D): class (c), the case notes and names of the eight shared conformance modules in `packages/spec/src`, with the two test seams that bind their text. Every rewritten note or name now states in words what the cited decision was, or drops a citation its sentence already explained. Text only. ## What changed - **58 messages / 68 tracker ids / 40 distinct cards** in eight modules: `contracts/metadata-service-roundtrip-conformance.ts` and, in `data/`, `aggregation-conformance.ts`, `filter-comparand-type-conformance.ts`, `filter-logic-conformance.ts`, `filter-text-conformance.ts`, `filter-text-operator-declared-type.ts`, `temporal-conformance.ts`, `value-roundtrip-conformance.ts`. Fifty-two are `note` / `why` texts and six are case names. - **The selector seam** (`packages/lint/src/validate-empty-combinators.test.ts:275`): selects the same four empty-combinator cases by a phrase the rewritten notes carry instead of `objectstack-ai#5322` (A3 below). - **The name-pin seam** (`packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts:369`): pins the renamed infix `icontains` case verbatim, the same strength (A4 below). - One `@objectstack/spec` **patch** changeset, `Clause-②: no`: the conformance tables ship in the package's `dist` (measured: the new note text is in `dist/data/index.mjs`; the two test files ship nowhere, since `@objectstack/lint` and `@objectstack/service-analytics` publish `dist` only and neither dist carries the seam text, with the package's own symbols as the positive control). - No generated artifact moves (`check:generated` green; the tables are values behind annotated types, so the API surface is unchanged). ## Census at the base (A1) Stage 3's instrument (`census.cjs`, byte-identical copy, md5 `e5fe562290fcd1ac392bd560cb86d861`) at base `cc645f2385`: class (c) is **58 messages / 68 ids in 8 files, 40 distinct cards**, exactly stage 3's count (the `[c-SELECTOR]` 4 / 7 and `[c-NAMEPIN]` 1 / 1 rows included). Under the ~60-card bar, so one stage. Lines are the base's. | file (under `packages/spec/src`) | line:id | messages / ids | field | consumer | |:--|:--|--:|:--|:--| | `contracts/metadata-service-roundtrip-conformance.ts` | 194:objectstack-ai#6725 · 202:objectstack-ai#6725 · 280:objectstack-ai#7378 · 296:objectstack-ai#7378 · 304:objectstack-ai#7378 | 5 / 5 | `why` | none prints it: both runners title a row by its `id`; the text is a reader's note in the shared table | | `data/aggregation-conformance.ts` | 342:objectstack-ai#6409 · 398:objectstack-ai#11065,objectstack-ai#11151 · 407:objectstack-ai#11065 · 417:objectstack-ai#11152 419:objectstack-ai#11249 · 535/550/561:objectstack-ai#15546 · 577/596:objectstack-ai#6401 | 9 / 11 | `note` | the assertion message in the driver-memory, driver-sql, driver-turso, driver-sqlite-wasm, objectql and mongodb-translation suites | | `data/filter-comparand-type-conformance.ts` | 153:objectstack-ai#7956 · 160:objectstack-ai#7872 · 272/282/289:objectstack-ai#19757 (names) · 178:objectstack-ai#5234 · 191:objectstack-ai#6050 (notes) | 7 / 7 | 5 `name`, 2 `note` | names are test titles in five driver suites and in `filter-save-door-face-parity.test.ts`; notes are assertion messages | | `data/filter-logic-conformance.ts` | 339/362:objectstack-ai#3774 · 420:objectstack-ai#5322 · 426:objectstack-ai#5322,objectstack-ai#5134 · 432/438:objectstack-ai#5322,objectstack-ai#5297 · 470:objectstack-ai#5298 · 476:objectstack-ai#5146 · 503/509:objectstack-ai#5298,objectstack-ai#13356 · 539:objectstack-ai#5299,objectstack-ai#5962 · 561/567/573/579/595:objectstack-ai#20444 | 16 / 22 | `note` | the assertion message in the eleven filter-logic harnesses; **the one selector**: `validate-empty-combinators.test.ts:275` picks the four `objectstack-ai#5322` notes | | `data/filter-text-conformance.ts` | 300:objectstack-ai#8934 (name) · 309:objectstack-ai#8934 · 349:objectstack-ai#6518,objectstack-ai#6682 · 428:objectstack-ai#4706 · 436:objectstack-ai#5710,objectstack-ai#6993 · 452:objectstack-ai#5240 | 6 / 8 | 1 `name`, 5 `note` | names are test titles in the text-conformance suites; **the one name pin**: `icontains-dialect-sql.test.ts:369`; notes are assertion messages | | `data/filter-text-operator-declared-type.ts` | 392:objectstack-ai#8296 · 553:objectstack-ai#8371 · 554:objectstack-ai#8296 | 3 / 3 | `note` | the assertion message in `engine-text-operator-declared-type-door.test.ts` | | `data/temporal-conformance.ts` | 211:objectstack-ai#3773 · 215/216:objectstack-ai#3777 · 220:objectstack-ai#20600 (row `why`) · 270:objectstack-ai#3777 · 393:objectstack-ai#3773 · 448/457:objectstack-ai#1874 · 549:objectstack-ai#3994 (case `note`) | 9 / 9 | 4 `why`, 5 `note` | a row `why` is seeded as DATA into each harness's `why` string column (never filtered on); a case `note` is an assertion message | | `data/value-roundtrip-conformance.ts` | 213:objectstack-ai#11535 · 225/226:objectstack-ai#11782 | 3 / 3 | `note` | printed in the TEST TITLE `round-trips NAME (NOTE)` in five driver suites | No non-test code reads any of these texts (every `.note` / `.why` read outside a test is another table's: `RETIRED_FILTER_OPERATORS`, the driver-conformance ledger). ## Delivered (A2): each site, the decision as read, the new words Each cited card was read with its body and every comment; the decision column names the record (comment id, or the landing record where the card carries none). Line = the base line of the edit anchor. "Old → new" shows only the words that changed. | file:line | ids | decision as read | old → new | |:--|:--|:--|:--| | `contracts/metadata-service-roundtrip-conformance.ts:194` | objectstack-ai#6725 | objectstack-ai#6725: card answers 404 here; landing record: changesets 1507ba3 / bbee302 (objectql and spec CHANGELOGs): MetadataFacade.register('object') wrote into a map none of its own object reads consulted | objectstack-ai#6725 would have failed → that catches an object write landing in a store none of the object reads consult — the hole a shipped implementation once fell through | | `contracts/metadata-service-roundtrip-conformance.ts:202` | objectstack-ai#6725 | objectstack-ai#6725: card answers 404 here; landing record: changesets 1507ba3 / bbee302 (objectql and spec CHANGELOGs): MetadataFacade.register('object') wrote into a map none of its own object reads consulted | produced objectstack-ai#6725 → let an object write land in the generic store while every object read looked elsewhere | | `contracts/metadata-service-roundtrip-conformance.ts:280` | objectstack-ai#7378 | objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a name/data.name disagreement, row 2 one answer converged with check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378, maintainer 2026-08-12, row 1 → maintainer 2026-08-12, row 1 of the round-trip ruling | | `contracts/metadata-service-roundtrip-conformance.ts:296` | objectstack-ai#7378 | objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a name/data.name disagreement, row 2 one answer converged with check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378, maintainer 2026-08-12, row 2 → maintainer 2026-08-12, row 2 of the round-trip ruling | | `contracts/metadata-service-roundtrip-conformance.ts:304` | objectstack-ai#7378 | objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a name/data.name disagreement, row 2 one answer converged with check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378, maintainer 2026-08-12, row 3 → maintainer 2026-08-12, row 3 of the round-trip ruling | | `data/aggregation-conformance.ts:342` | objectstack-ai#6409 | objectstack-ai#6409: card body, executing the objectstack-ai#6188 split ruling: count_distinct stays declared and is enforced as distinct non-null values on the SQL family | objectstack-ai#6409: → count_distinct was kept and enforced on every face, as distinct non-null values: | | `data/aggregation-conformance.ts:398` | objectstack-ai#11065 objectstack-ai#11151 | objectstack-ai#11065: card answers 404 here; landing record: driver-memory changeset 2095040: avg/sum over a boolean answer the same numbers as every SQL face (a boolean is worth 1 or 0) · objectstack-ai#11151: PR objectstack-ai#12819 (triage 5446817173): driver-mongodb's boolean aggregand answers the ruled values too | objectstack-ai#11065/objectstack-ai#11151: an arithmetic accumulator that drops booleans answers its identity 0 here — a plausible number, which → A boolean aggregand is worth 1 or 0 on every face — driver-memory and then driver-mongodb were both moved onto that answer: an arithmetic accumulator that drops booleans answers its identity 0 here — a plausible number, which | | `data/aggregation-conformance.ts:407` | objectstack-ai#11065 | objectstack-ai#11065: card answers 404 here; landing record: driver-memory changeset 2095040: avg/sum over a boolean answer the same numbers as every SQL face (a boolean is worth 1 or 0) | objectstack-ai#11065: the rate-over-a-flag-column shape (an SLA-violation rate, a win rate). → The rate-over-a-flag-column shape (an SLA-violation rate, a win rate) that driver-memory answered null for until it counted a boolean as 1 or 0. | | `data/aggregation-conformance.ts:417` | objectstack-ai#11152 objectstack-ai#11249 | objectstack-ai#11152: maintainer ruling 2026-08-28, option A (changeset f6fa22c in the driver CHANGELOGs; decision request 5446817173): booleans aggregate as numbers, min/max answer 0/1 · objectstack-ai#11249: ruling 5386670755 (false/true for min/max over a boolean), superseded by objectstack-ai#11152's 2026-08-28 ruling | objectstack-ai#11152 ruling (2026-08-28): booleans aggregate as numbers with no per-aggregate exception, so the order statistics answer 0/1 in the same domain sum/avg answer in — not false/true (objectstack-ai#11249, superseded) → Ruled 2026-08-28: booleans aggregate as numbers with no per-aggregate exception, so the order statistics answer 0/1 in the same domain sum/avg answer in — not the false/true an earlier ruling had chosen, which this one superseded | | `data/aggregation-conformance.ts:535` | objectstack-ai#15546 | objectstack-ai#15546: ruling 5572006116 (option A): a non-empty group whose aggregand is NULL in every row sums to 0 on every face | objectstack-ai#15546: → Ruled: a non-empty group whose aggregand is NULL in every row sums to 0. | | `data/aggregation-conformance.ts:550` | objectstack-ai#15546 | objectstack-ai#15546: ruling 5572006116 (option A): a non-empty group whose aggregand is NULL in every row sums to 0 on every face | objectstack-ai#15546: the reachability control for the → The reachability control for the all-NULL-sums-to-0 | | `data/aggregation-conformance.ts:561` | objectstack-ai#15546 | objectstack-ai#15546: ruling 5572006116 (option A): a non-empty group whose aggregand is NULL in every row sums to 0 on every face | objectstack-ai#15546: the partial-null control → The partial-null control for the same ruling | | `data/aggregation-conformance.ts:577` | objectstack-ai#6401 | objectstack-ai#6401: dev report 5229051388 (PR objectstack-ai#6849): GroupByNode.alias is enforced, not removed; every SQL face projects the group under alias ?? field | objectstack-ai#6401: t → A structured group node's `alias` names the projected group column on every face — the SQL faces that ignored it were made to honour it. T | | `data/aggregation-conformance.ts:596` | objectstack-ai#6401 | objectstack-ai#6401: dev report 5229051388 (PR objectstack-ai#6849): GroupByNode.alias is enforced, not removed; every SQL face projects the group under alias ?? field | objectstack-ai#6401: the degenerate alias. Its twin above → The degenerate alias, under the same every-face `alias` rule. Its twin above | | `data/filter-comparand-type-conformance.ts:153` | objectstack-ai#7956 | objectstack-ai#7956: ACCEPT 5264821447: a measurement only; its control cell ({qty: {$eq: 100}}) returned the row on every driver, so the zeros were real answers | objectstack-ai#7956 control cell → control cell of the cross-driver comparand-type measurement | | `data/filter-comparand-type-conformance.ts:160` | objectstack-ai#7872 | objectstack-ai#7872: maintainer ruling 5265944890 and ACCEPT 5273103599 (PR objectstack-ai#8234): a shared comparand-type door; a safe-range bigint narrows to its exact number, beyond 2^53 is refused | (objectstack-ai#7872) → at the shared comparand door | | `data/filter-comparand-type-conformance.ts:178` | objectstack-ai#5234 | objectstack-ai#5234: ACCEPT 5217619370 (PR objectstack-ai#6296): a non-$field object member of $in/$nin and an object LIKE comparand are refused; each member is judged on its own | right (objectstack-ai#5234) → right, each judged like a scalar comparand | | `data/filter-comparand-type-conformance.ts:191` | objectstack-ai#6050 | objectstack-ai#6050: ruling B 5211349926: an undefined comparand is refused loudly; null keeps its meaning as the null predicate | (objectstack-ai#6050's untouched half) — → — the half left untouched when an undefined comparand was ruled a loud refusal — so | | `data/filter-comparand-type-conformance.ts:272` | objectstack-ai#19757 | objectstack-ai#19757: ruling 5793368540 (letter 乙): an array in the implicit-equality slot is refused at the shared face, for every driver at once | (objectstack-ai#19757) → at the shared face | | `data/filter-comparand-type-conformance.ts:282` | objectstack-ai#19757 | objectstack-ai#19757: ruling 5793368540 (letter 乙): an array in the implicit-equality slot is refused at the shared face, for every driver at once | (objectstack-ai#19757) → at the shared face | | `data/filter-comparand-type-conformance.ts:289` | objectstack-ai#19757 | objectstack-ai#19757: ruling 5793368540 (letter 乙): an array in the implicit-equality slot is refused at the shared face, for every driver at once | too (objectstack-ai#19757) → too, at the shared face | | `data/filter-logic-conformance.ts:339` | objectstack-ai#3774 | objectstack-ai#3774: defect: driver-sql OR-ed a $or branch's own keys and operators; rule: everything inside one filter object ANDs at every depth (card body) | objectstack-ai#3774: compiled → A $or combines its branches, never the keys inside one: a driver that OR-ed a branch's own keys compiled this | | `data/filter-logic-conformance.ts:362` | objectstack-ai#3774 | objectstack-ai#3774: defect: driver-sql OR-ed a $or branch's own keys and operators; rule: everything inside one filter object ANDs at every depth (card body) | objectstack-ai#3774: a single-key branch is miscompilable too — the operator map is looped with the same → A single-key branch is miscompilable too — that driver looped the operator map with the same OR | | `data/filter-logic-conformance.ts:420` | objectstack-ai#5322 | objectstack-ai#5322: maintainer ruling 5185589944: every face reduces an empty combinator to its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole tree | objectstack-ai#5322: a → Ruled: every face reduces an empty combinator to its boolean identity. A | | `data/filter-logic-conformance.ts:426` | objectstack-ai#5322 objectstack-ai#5134 | objectstack-ai#5322: maintainer ruling 5185589944: every face reduces an empty combinator to its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole tree · objectstack-ai#5134: defect repaired by PR objectstack-ai#5243 (ACCEPT 5178806144): driver-sql dropped an empty $and/$or group instead of applying the boolean identity; empty $or and empty $not now compile to FALSE | objectstack-ai#5322/objectstack-ai#5134: a disjunction of zero conditions matches nothing. Fail-closed for an RLS scope — a disjunct list that loops to zero items hides every row instead of exposing the table → Ruled: every face reduces an empty combinator to its boolean identity. A disjunction of zero conditions matches nothing. Fail-closed for an RLS scope — a disjunct list that loops to zero items hides every row instead of exposing the table, as a SQL lowering that dropped the empty group once did | | `data/filter-logic-conformance.ts:432` | objectstack-ai#5322 objectstack-ai#5297 | objectstack-ai#5322: maintainer ruling 5185589944: every face reduces an empty combinator to its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole tree · objectstack-ai#5297: dispatch 5184116664: read-scope-sql's { $not: {} } compiled to nothing (an RLS scope with no WHERE) and dropped a {} disjunct; both aligned to the boolean identity | objectstack-ai#5322: collapsing to the surviving branches instead compiles `a = x` — a silently NARROWED scope (objectstack-ai#5297) → Ruled: every face reduces an empty combinator to its boolean identity, so `{}` is a TRUE disjunct. Collapsing to the surviving branches instead compiles `a = x` — a silently NARROWED scope, the answer the RLS read-scope compiler gave until it was aligned | | `data/filter-logic-conformance.ts:438` | objectstack-ai#5322 objectstack-ai#5297 | objectstack-ai#5322: maintainer ruling 5185589944: every face reduces an empty combinator to its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole tree · objectstack-ai#5297: dispatch 5184116664: read-scope-sql's { $not: {} } compiled to nothing (an RLS scope with no WHERE) and dropped a {} disjunct; both aligned to the boolean identity | objectstack-ai#5322: emitting nothing for it runs the query UNSCOPED — on an RLS lowering that is a permission bypass (objectstack-ai#5297) → Ruled: every face reduces an empty combinator to its boolean identity, so NOT of `{}` is FALSE. Emitting nothing for it runs the query UNSCOPED — on an RLS lowering that is a permission bypass, which the read-scope compiler was until it compiled this to an always-false clause | | `data/filter-logic-conformance.ts:470` | objectstack-ai#5298 | objectstack-ai#5298: ruling 5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin / $notContains are NULL-safe on the non-negated path, a no-value row is included | objectstack-ai#5298 → Ruled NULL-safe on every face | | `data/filter-logic-conformance.ts:476` | objectstack-ai#5146 | objectstack-ai#5146: maintainer ruling 5181102507: $not is NULL-safe on every driver; a row with no value does not satisfy the negated condition, so the negation returns it | objectstack-ai#5146: the same ruling reached through the combinator → The same NULL-safe ruling reached through the combinator, where it was first made for `$not` itself | | `data/filter-logic-conformance.ts:503` | objectstack-ai#5298 | objectstack-ai#5298: ruling 5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin / $notContains are NULL-safe on the non-negated path, a no-value row is included | objectstack-ai#5298 option A → Ruled NULL-safe | | `data/filter-logic-conformance.ts:503` | objectstack-ai#13356 | objectstack-ai#13356: PR objectstack-ai#13356: driver-memory's reference matcher realigned so a no-value row satisfies $nin / $notContains | PR objectstack-ai#13356 → it was realigned to the ruling | | `data/filter-logic-conformance.ts:509` | objectstack-ai#5298 | objectstack-ai#5298: ruling 5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin / $notContains are NULL-safe on the non-negated path, a no-value row is included | objectstack-ai#5298 option A → Ruled NULL-safe | | `data/filter-logic-conformance.ts:509` | objectstack-ai#13356 | objectstack-ai#13356: PR objectstack-ai#13356: driver-memory's reference matcher realigned so a no-value row satisfies $nin / $notContains | PR objectstack-ai#13356 → it was realigned to the ruling | | `data/filter-logic-conformance.ts:539` | objectstack-ai#5299 objectstack-ai#5962 | objectstack-ai#5299: ruling 5219858433 item 2, kept by 5238865560: $exists means has a value (non-null), never key-presence, since SQL cannot tell a missing key from null · objectstack-ai#5962: PR objectstack-ai#5962 (ACCEPT 5205011148 on objectstack-ai#5298): the NULL-safe operators and the $exists has-a-value reading shipped | objectstack-ai#5299 cell 2 / objectstack-ai#5962: `$exists` means HAS A VALUE, never key-presence → Ruled: `$exists` means HAS A VALUE, never key-presence, because SQL cannot tell a missing key from a stored null | | `data/filter-logic-conformance.ts:561` | objectstack-ai#20444 | objectstack-ai#20444: card body executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers $empty by the field's declared type; $empty: false is the exact complement | objectstack-ai#20444: null is empty on every row of the ruled table → Every face answers `$empty` by the field's declared type, and null is empty under every type's arm | | `data/filter-logic-conformance.ts:567` | objectstack-ai#20444 | objectstack-ai#20444: card body executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers $empty by the field's declared type; $empty: false is the exact complement | objectstack-ai#20444: the exact complement → `$empty: false` is the exact complement by ruling | | `data/filter-logic-conformance.ts:573` | objectstack-ai#20444 | objectstack-ai#20444: card body executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers $empty by the field's declared type; $empty: false is the exact complement | objectstack-ai#20444: `$empty` spells its NULL case out, so it → The ruled `$empty` arms spell their NULL case out, so `$empty` | | `data/filter-logic-conformance.ts:579` | objectstack-ai#20444 | objectstack-ai#20444: card body executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers $empty by the field's declared type; $empty: false is the exact complement | objectstack-ai#20444: → Under the same declared-type arms, | | `data/filter-logic-conformance.ts:595` | objectstack-ai#20444 | objectstack-ai#20444: card body executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers $empty by the field's declared type; $empty: false is the exact complement | objectstack-ai#20444: a face that lowers `$empty` → A face that lowers `$empty` to the field's declared-type arm | | `data/filter-text-conformance.ts:300` | objectstack-ai#8934 | objectstack-ai#8934: ruling A 5305708745: icontains joins the view and infix vocabularies; ilike/$ilike and icontains/$icontains are never aliased onto each other | objectstack-ai#8934 → ruled never an alias of ilike | | `data/filter-text-conformance.ts:309` | objectstack-ai#8934 | objectstack-ai#8934: ruling A 5305708745: icontains joins the view and infix vocabularies; ilike/$ilike and icontains/$icontains are never aliased onto each other | capability (objectstack-ai#8934) → capability, ruled when `icontains` joined the view and infix vocabularies | | `data/filter-text-conformance.ts:349` | objectstack-ai#6518 objectstack-ai#6682 | objectstack-ai#6518: ACCEPT 5226386725 (PR objectstack-ai#6706): the SQL family's $contains family is case-exact (GLOB on the SQLite dialects), per objectstack-ai#4706 Q2 = A · objectstack-ai#6682: ACCEPT 5251849449 and the memory half: the hardcoded case-folding flag came off driver-mongodb and driver-memory, the last two folding faces | match. SQLite's LIKE folds ASCII — the defect objectstack-ai#6518 replaced with GLOB on the SQLite dialects; a JS backend's equivalent is a RegExp carrying the `i` flag, which objectstack-ai#6682 took off → match: the `$contains` family is case-sensitive on every backend, by ruling. SQLite's LIKE folds ASCII — the defect the SQL family replaced with GLOB on the SQLite dialects; a JS backend's equivalent is a RegExp carrying the `i` flag, since taken off driver-memory and driver-mongodb, | | `data/filter-text-conformance.ts:428` | objectstack-ai#4706 | objectstack-ai#4706: ruling B 5199214776: $regex retired under ADR-0049 with a loud refusal naming the replacement, $icontains added | objectstack-ai#4706 retired the operator over → `$regex` was retired over, by ruling, with a loud refusal naming `$icontains` | | `data/filter-text-conformance.ts:436` | objectstack-ai#5710 objectstack-ai#6993 | objectstack-ai#5710: ACCEPT 5201171068 (PR objectstack-ai#5812): plugin-auth's adapter stopped emitting bare $regex for better-auth contains, unblocking the $regex retirement · objectstack-ai#6993: ACCEPT 5231388316 (PR objectstack-ai#7054): the expired status claims were re-measured by executing each face (2026-08) and rewritten | objectstack-ai#5710 flipped that producer before any backend enrolled this case (re-verified 2026-08, objectstack-ai#6993 → That producer was moved off `$regex` before any backend enrolled this case (re-verified 2026-08 by executing each face | | `data/filter-text-conformance.ts:452` | objectstack-ai#5240 | objectstack-ai#5240: maintainer ruling 5181107825: { field: {} } is refused loudly (INVALID_FILTER) on every backend, not read as TRUE or FALSE | objectstack-ai#5240 refused `{ field: {} }` over → for which a field with zero operators, `{ field: {} }`, is refused by ruling | | `data/filter-text-operator-declared-type.ts:392` | objectstack-ai#8296 | objectstack-ai#8296: standing ruling recorded in 5277439872: a where on a formula field is refused (INVALID_FIELD 400) because no driver materialises the column | objectstack-ai#8296 door refuses EVERY formula filter with INVALID_FIELD 400 whatever the `returnType` → unmaterializable-field door refuses EVERY formula filter with INVALID_FIELD 400 whatever the `returnType` (no driver stores a formula column) | | `data/filter-text-operator-declared-type.ts:553` | objectstack-ai#8371 | objectstack-ai#8371: ruling 5300373151 (option 2): a type-directed verdict on the dotted head segment; the structured/JSON head stays deliberately unjudged | unjudged there, objectstack-ai#8371 → left unjudged there, by ruling | | `data/filter-text-operator-declared-type.ts:554` | objectstack-ai#8296 | objectstack-ai#8296: standing ruling recorded in 5277439872: a where on a formula field is refused (INVALID_FIELD 400) because no driver materialises the column | objectstack-ai#8296 → the unmaterializable-field door | | `data/temporal-conformance.ts:211` | objectstack-ai#3773 | objectstack-ai#3773: defect: SQLite read an epoch-ms Field.datetime as a Julian day so date buckets were NULL; repaired by storage-aware bucketing (card body; cross-update 5099832227 on objectstack-ai#3777) | (objectstack-ai#3773) → as SQLite bucketing once did | | `data/temporal-conformance.ts:215` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day $lte on a datetime column stopped at midnight and dropped the rest of the final day; the bound keeps the whole day (card body; 5099832227) | by the objectstack-ai#3777 bug → when a bare-day upper bound stopped at midnight | | `data/temporal-conformance.ts:216` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day $lte on a datetime column stopped at midnight and dropped the rest of the final day; the bound keeps the whole day (card body; 5099832227) | by the objectstack-ai#3777 bug → when a bare-day upper bound stopped at midnight | | `data/temporal-conformance.ts:220` | objectstack-ai#20600 | objectstack-ai#20600: triage direction 5886142901, landed PR objectstack-ai#20643: the whole-day bound past 9999-12-31 is unbounded above and the drivers compile none | (objectstack-ai#20600) → and none is compiled | | `data/temporal-conformance.ts:270` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day $lte on a datetime column stopped at midnight and dropped the rest of the final day; the bound keeps the whole day (card body; 5099832227) | objectstack-ai#3777: the default dashboard window → The default dashboard window, whose bare-day $lte keeps the whole final day | | `data/temporal-conformance.ts:393` | objectstack-ai#3773 | objectstack-ai#3773: defect: SQLite read an epoch-ms Field.datetime as a Julian day so date buckets were NULL; repaired by storage-aware bucketing (card body; cross-update 5099832227 on objectstack-ai#3777) | objectstack-ai#3773 famil → family of the SQLite bucketing defect that read an epoch-ms datetime as a Julian da | | `data/temporal-conformance.ts:448` | objectstack-ai#1874 | objectstack-ai#1874: the templates' date-equality family that surfaced ADR-0053, whose Phase 1 stores Field.date as its calendar day (ADR-0053 Surfaced-by line; card body, no comments) | objectstack-ai#1874: `date == today` silently matched nothing while dates were stored as instants. E → `date == today` silently matched nothing while dates were stored as instants; ADR-0053 stores a date as its calendar day, so e | | `data/temporal-conformance.ts:457` | objectstack-ai#1874 | objectstack-ai#1874: the templates' date-equality family that surfaced ADR-0053, whose Phase 1 stores Field.date as its calendar day (ADR-0053 Surfaced-by line; card body, no comments) | from the objectstack-ai#1874 family → that surfaced ADR-0053 | | `data/temporal-conformance.ts:549` | objectstack-ai#3994 | objectstack-ai#3994: Field.time takes one canonical HH:MM:SS[.fff] text form on write, filter and read (ADR-0053 addendum D-C1..D-C3; driver-sql changeset 9774b78) | objectstack-ai#3994, measured → Measured before `Field.time` took one canonical `HH:MM:SS[.fff]` text form | | `data/value-roundtrip-conformance.ts:213` | objectstack-ai#11535 | objectstack-ai#11535: defect: a single-to-multi-value change kept the old text column on Postgres, so arrays came back as stringified literals; the drift detector now reports the base-type mismatch (claim 5395496220) | objectstack-ai#11535's exact shape → the exact shape a single-value column kept for a multi-value field corrupted | | `data/value-roundtrip-conformance.ts:225` | objectstack-ai#11782 | objectstack-ai#11782: landing 5406878917 (PR objectstack-ai#12019): a declared boolean answers JSON booleans on every read door and dialect; MySQL's tinyint 1/0 leaked before | objectstack-ai#11782 read this back as 1 on MySQL → MySQL read this back as 1 until every read door presented a declared boolean as true/false | | `data/value-roundtrip-conformance.ts:226` | objectstack-ai#11782 | objectstack-ai#11782: landing 5406878917 (PR objectstack-ai#12019): a declared boolean answers JSON booleans on every read door and dialect; MySQL's tinyint 1/0 leaked before | objectstack-ai#11782 → that MySQL read-back | | `lint: validate-empty-combinators.test.ts:275` | objectstack-ai#5322 | objectstack-ai#5322: maintainer ruling 5185589944: every face reduces an empty combinator to its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole tree | objectstack-ai#5322 → every face reduces an empty combinator to its boolean identity | | `analytics: icontains-dialect-sql.test.ts:369` | objectstack-ai#8934 | objectstack-ai#8934: ruling A 5305708745: icontains joins the view and infix vocabularies; ilike/$ilike and icontains/$icontains are never aliased onto each other | objectstack-ai#8934 → ruled never an alias of ilike | ## The selector seam (A3) The filter over `FILTER_LOGIC_CASES` now tests `(c.note ?? '').includes('every face reduces an empty combinator to its boolean identity')` where it tested `.includes('objectstack-ai#5322')`. The phrase is the objectstack-ai#5322 ruling in words, and the four rewritten notes open with it. Measured with an AST extractor over the table (`name` and `note` of all 36 cases, then the selector applied exactly as the test applies it): - base `cc645f2385` with `objectstack-ai#5322` → **4 of 36**: `$not of {} is FALSE — NOT TRUE`, `a {} branch is a TRUE disjunct and absorbs its $or`, `empty $and is TRUE — the AND identity`, `empty $or is FALSE — the OR identity`. - head with the phrase → **the same 4 of 36**, the same names. - cross: the old selector on the head selects 0 (so the move is required), the new phrase on the base selects 0. - control outside the set: the broader word `Ruled` selects 8, the four plus `$exists true selects exactly the valued rows`, `$ne …`, `$nin …` and `$notContains returns the rows with no value`; none of those four is picked by the phrase. - mutation control on a scratch copy (anchor hit 1, replacement present 1, anchor left 0): dropping the phrase from the empty-`$or` note leaves 3, which the test's own guard (`toEqual` on the four sorted names, `toBe(4)`) turns red. The test asserts exactly what it asserted before (the names pin, the count, and the row-set wording per case); only the selector moved. `validate-empty-combinators.test.ts` ran green: 21 of 21, the three identity cases included. ## The name pin (A4) `'icontains (the infix/view spelling, objectstack-ai#8934) lowers to $icontains — % stays a LITERAL through that door too'` becomes `'icontains (the infix/view spelling, ruled never an alias of ilike) lowers to $icontains — % stays a LITERAL through that door too'` in `FILTER_TEXT_CASES`, and the `toEqual` pin in `icontains-dialect-sql.test.ts:369` moves to the new string verbatim. No other consumer keys on that name (searched the whole tree for the old name and its fragments: the source and the pin only); the other consumers use it as a test title. The file ran green: 16 of 16. The five renamed `FILTER_COMPARAND_TYPE_CASES` names have no pin anywhere; they are test titles only. ## Text only (A5) Stage 3's `skeleton.cjs` (one line changed: the TypeScript load path to this worktree), TypeScript 6.0.3: leg 1 an AST skeleton with every string's text masked (a `+` chain's adjacent string operands read as one string), leg 2 the text of every string group, each changed group required to carry a tracker id before and none after, every other group byte-identical. Base copies vs the committed files, **10 of 10 SAME on both legs, exit 0**: | file | tokens | string groups | changed | |:--|--:|--:|--:| | `metadata-service-roundtrip-conformance.ts` | 1352 | 157 | 5 | | `aggregation-conformance.ts` | 1268 | 133 | 9 | | `filter-comparand-type-conformance.ts` | 1309 | 107 | 7 | | `filter-logic-conformance.ts` | 1682 | 223 | 16 | | `filter-text-conformance.ts` | 1093 | 137 | 6 | | `filter-text-operator-declared-type.ts` | 1821 | 114 | 3 | | `temporal-conformance.ts` | 2041 | 374 | 9 | | `value-roundtrip-conformance.ts` | 1197 | 175 | 3 | | `validate-empty-combinators.test.ts` | 2684 | 129 | 1 | | `icontains-dialect-sql.test.ts` | 4345 | 217 | 1 | Parse diagnostics 0 / 0 throughout. 58 changed groups in the eight modules, one per seam. No case's filter, input, expected rows, verdict, code, operator, dialect, order or count moves. Edits were applied by a script whose 62 anchors each had to hit exactly once before any write, and were read back from disk after it (each anchor gone, each replacement present once). Census after the edit: class (c) **0 / 0**; non-test 118 → 60 messages, 297 → 229 ids; test strings unchanged (1804 / 1920). ## Pins, titles and quotes (A6) - Pins moved: the two seams above, nothing else. Every old note and name was searched across the tree in 32-character windows: no test asserts a changed phrase; the remaining hits are code comments and other modules' own prose that share a phrase with an unchanged part of a note. - Titles that follow the text: the comparand-type and text-case names (test titles in the driver suites), and the `VALUE_ROUNDTRIP_CASES` notes (`round-trips NAME (NOTE)`). No skip list, ledger or snapshot names any of those titles. - Quotes: no `content/docs/**` page and no `skills/**` file quotes a changed note or name. - `TEMPORAL_ROWS[].why` is seeded into a `string` column (a `varchar` on Postgres and MySQL), so the four rewritten `why` texts stay short (164 characters at most). ## Gates Head `1427993cc3` (the merge of `origin/main` `901e7cf13a`; that merge touched no file under `packages/spec`, `packages/lint` or `packages/services/service-analytics`, 0 diff lines there). - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 11 paths vs merge base `901e7cf13`, 153 changed lines; **88 derived commands, 88 exit 0** on `1427993cc3`, each exit code written from `$?` before any pipe; `--ran`: "✓ dispatch-gates --ran: 88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED". - `pnpm --filter @objectstack/spec build` exit 0 (the dependency closure is empty: no workspace dependencies); `check:generated`: "✓ All 15 generated artifacts are up to date". - `pnpm --filter @objectstack/spec test`: "Test Files 606 passed (606) / Tests 17952 passed | 1 todo (17953)"; `typecheck` exit 0 ("check:test-typecheck: OK — … 52 file(s) / 246 error(s) / 135 pinned signature(s) held"). - `pnpm --filter @objectstack/lint test`: "Test Files 119 passed (119) / Tests 5620 passed (5620)"; `typecheck` exit 0. - `pnpm --filter @objectstack/service-analytics test`: "Test Files 175 passed (175) / Tests 4152 passed | 253 skipped (4405)"; `typecheck` exit 0; `icontains-dialect-sql.test.ts` alone 16 of 16. - `pnpm check:doc-authoring` (self-test, then the run): "✓ doc authoring guard: 17337 customer-facing string(s) across 1251 spec sources clean" and "sibling-package prose ids hold the baseline — 0 pinned site(s) … no growth, no burn-down unrecorded". `pnpm check:nul-bytes`: "check-nul-bytes: OK (scanned 9947 text file(s) …; no raw ASCII control bytes)". - Changeset gates: `check-changeset-no-major.mjs`, `check-empty-changeset.mjs`, `check-adr-0087-registration.mjs` (each `--base origin/main`) exit 0; with this body as the `--event` payload, see the report. - ESLint, a proven narrowing: `eslint --no-inline-config --format json` over the 10 changed TS files reads 10 files, 0 errors, 0 warnings; the population is ESLint's own (`calculateConfigForFile` returns a config for each, `isPathIgnored` false); invariance: `eslint.config.mjs` enables no type-aware linting (`parserOptions.project` / `projectService` null for all 10), so a string-text edit cannot move an untouched file's verdict. Repo-wide `pnpm lint` is CI's. - Spec `test:repo` was not run locally: none of its 51 files names a changed module or table. ## Acceptance notes - **Two cited cards answer 404** in this repository (objectstack-ai#6725 and objectstack-ai#11065; the same numbers in objectui are unrelated PRs). Their decisions were read from the shipped landing records instead: the objectql and spec CHANGELOG entries `1507ba3` / `bbee302` (`MetadataFacade.register('object')` wrote into a map none of its own object reads consulted) and the driver-memory entry `2095040` (a boolean aggregand counts as 1 or 0, as on every SQL face). Neither decision read as unclear. - Two decisions live in shipped records rather than card comments: objectstack-ai#11152's 2026-08-28 ruling (option A, booleans aggregate as numbers, superseding objectstack-ai#11249) is recorded in changeset `f6fa22c` and matches the source comment above those cases; objectstack-ai#3994's in ADR-0053's D-C addendum. - Left for later stages, untouched here: the seam files' own test titles (`(objectstack-ai#5322/objectstack-ai#5659)`, `[objectstack-ai#16028]`) and every other test string; code comments (including the section comments beside these cases); class (f) in `api/error-code-ledger.zod.ts` and `kernel/public-auth-features.ts`; `migrations/registry.ts`. - Not governed: no `.claude/**`, `skills/**`, ADR, NORTH-STAR or AGENTS.md path. ## Line budget 153 changed lines (+90 / -63) over 11 files vs merge base `901e7cf13` (dispatch-gates), under the 5000 human-merge threshold: the eight modules, the two test files (one line each) and the changeset. No generated file, no governed surface. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #13166
Implements the settled ruling (#5146 → #5298 option A, re-affirmed 2026-08-10 after the reversal was priced and withdrawn): a row with no value SATISFIES a negation-carrying operator.
driver-memory's reference matcher diverged in 3 of the card's 6 measured cells. Both independent causes are fixed, and the four in-code statements that spelled the #5499 freeze as LIVE are settled together with the disposition rather than re-tensed.The defect, reproduced first
The card's fixture, run before any source edit — 6 of 14 assertions red, and exactly the three cells the card named:
Two independent causes, one stated ruling
Both live in
packages/drivers/driver-memory/src/memory-matcher.ts:checkCondition's pre-switch guard allowlisted$nebut not$nin/$notContains, so a missing key short-circuited to "no match" before those arms ran.$notContainsarm rejected anullon itstypeof value !== 'string'type test rather than on the predicate — thename: nullhalf, which guard 1 cannot reach (it only ever seesundefined).Both now answer one named predicate,
noValueSatisfiesNegation, whose docblock carries the ruling chain. Two spellings of one ruling is how they came apart in the first place.A present, non-string value keeps the answer it had — only the no-value cells moved, and there is an assertion pinning that.
Fence 1 cleared by measurement, not by assumption
This matcher is the reference the SQL family was aligned to for #5146, so the stop condition was: does aligning it overturn already-aligned SQL-family behaviour? It does not, and no SQL-side file is touched.
driver-sqlandformulaalready answered the include direction, so this change movesdriver-memoryto them:@objectstack/driver-sql@objectstack/formula@objectstack/driver-memory@objectstack/service-analytics@objectstack/objectqlsql-driver-not-null-safe.test.tsstill asserts['1']for both filters the memory pin asserts; the two files agree again because this one moved.What does NOT change for users
InMemoryDriver.find()is unaffected.match()is not in this package's export surface, and the live mingo path users actually reach already answered the include direction — measured on the card's fixture through the public API,['2','3']for all three operators. The observable effect is that this package's two filter faces now agree where they used to disagree. The changeset is graded to exactly that.The four prose sites, settled with the disposition
Not re-tensed — #13089's triage forbade that, and rightly: a tense-only rewrite would have carried a measured-false claim forward as settled-looking prose. Each site was wrong twice: (a) the freeze dissolved 2026-08-11; (b) the "driver-mongodb does this too" clause was never true for this operator family (
translateFieldOperatorspasses$ninthrough and compiles$notContainsto{ $not: { $regex } }, both of which match a missing or null field).packages/objectql/src/having-filter.ts— the card's site 1.packages/services/service-analytics/src/read-scope-sql.ts— the card's site 2.packages/drivers/driver-memory/src/memory-matcher-not-null-safe.test.ts— the card's site 3, including the load-bearing sentence that was the stated reason the assertions had not been flipped. Two assertions inverted in place, with the old values and the reason recorded beside them.packages/drivers/driver-memory/src/memory-driver-document-not.test.ts— found during execution, not named in the card. Itsdescribealready read "disposition open (driver-memory's reference matcher still answers $notContains / $nin the pre-ruling way on a no-value row — the #5499 freeze that excused it dissolved 2026-08-11, so the divergence is now unexcused and untracked #13166/$exists still reads KEY-PRESENCE rather than has-value on driver-memory's live mingo path and driver-mongodb — the #5499 freeze that excused it dissolved, #13166 explicitly excludes it, so it is now unexcused AND untracked #13195)" and its note already named the LIVE column correct for these two operators, so it was built for exactly this edit. Its$ninand$notContainsrows now assert live and reference agreeing; its$existsrow is untouched and stays a pinned divergence.⛔ No assertion was re-baselined onto whatever the matcher began printing. Each was inverted onto a target named as correct before the fix existed — the live path's answer, and
formula's.Merge note — PR #13321 landed mid-flight and owns half of site 2
#13321 merged as
881f8d8e88while this branch was in flight and rewroteread-scope-sql.ts:179-180, one of the sites this card was assigned. Its text correctly records the thaw and that the debt is now DUE, but it kept the second error — it still reads as thoughdriver-mongodbanswers the cell differently.Resolved by extending its landed text, not reverting it: the thaw/DUE framing is kept verbatim in substance, and the #13166 disposition is added on top (mongodb was never a holdout; driver-memory was, on its reference face only, and is aligned now).
having-filter.tswas left byte-identical to main by #13321 as promised, so there was no conflict there.⛔ Also unchanged: the other freeze-tense sites #13321 owns.
Non-vacuity — ablation, direction predicted in writing first
The prediction was recorded before either leg ran. The subject resolves through a relative import, so vitest reads
src/, notdist/— evidenced by the repro failing and the fix passing with no build between them, so no rebuild leg is owed here.$exists/$ne/$null$notContainsarm reverted to the bare type test$notContainscells; "both readings agree" stays GREENLeg B is the one worth reading. With the guard fixed,
undefinedreaches the arm and fails the same type test asnull, so both columns are equally wrong and the agreement assertion cannot see the cause. That is the concrete reason the six-cell structure is load-bearing and must not be collapsed: a table asserting only "the two readings answer alike" would be blind to cause 2.Each mutation was proven on disk by content grep plus a
git hash-objectdiffering from the HEAD blob, and each restore was proven byte-identical to the HEAD blob by hash comparison rather than by an exit code, with an absolute-pathtrap ... EXIT INT TERMon both legs. Final tree hash equals the HEAD blob.Verification
All readings below are from the final commit,
5ed889a2bf.driver-memory,objectql,service-analytics— all Done, exit 0.tsc --listFilesconfirms all three edited/added test files are inside the program, so "typecheck clean" genuinely covers them rather than silently excluding*.test.ts.check-driver-conformance: OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt.and
where-matcher conformance holds: 316 matcher(s) discovered … 0 silently-wrong and 0 unjudged … none new.check-test-completeness.mjs— "There is no local log to hand it, so the local reading for this gate is NOT MEASURED. ⛔ It is not a red."check-half-states.mjs— "Nothing was swept … it is no reading at all." (needs a GitHub credential; a board sweep, unrelated to this diff)check:dual-build-cjs-loads— "PREREQUISITE NOT MET … ⛔ This is NOT a pass: nothing was measured." (needs a fullpnpm build; its own self-test passed, 59 cases)check-engine-split-ratio.mjsfirst refused on the shallow clone; deepened with--shallow-sinceand it then measured and exited 0. A refusal was never counted as a pass.Declared narrowing: the repo-wide
pnpm lintand the full-workspacecheck:type-check-debtratchet were not run locally — the latter refuses without a built workspace closure, and a full build risks the container's foreground cap. The structural half,check:type-check-coverage, is green, and the added test file typechecks clean inside its package program.Lint & Repo Gatesruns gates the derivation never names (#13333), so CI owns the farm here.Deliberately out of scope
$exists— the neighbouring cell ($exists still reads KEY-PRESENCE rather than has-value on driver-memory's live mingo path and driver-mongodb — the #5499 freeze that excused it dissolved, #13166 explicitly excludes it, so it is now unexcused AND untracked #13195). Different backend list (driver-mongodbreads key-presence too). Untouched, and still pinned as a divergence in both pin files.FILTER_LOGIC_CASES— the backend is fixed, which is the precondition fence 2 names, but enrolment lives inpackages/spec/src/**, which this dispatch fenced off. That is why this PR saysPart ofand not a closing keyword. Details and the residualfilter-logic-conformance.ts:179cell are in the report on the card.packages/spec/src/**,docs/adr/**,.claude/**,skills/**orcontent/docs/releases/**changes. No test skipped, disabled or quarantined; no empty commit.Generated by Claude Code
Generated by Claude Code