Repository navigation
fix(formula,plugin-security)!: delete formula's whole-day copy of the bare-day bound (lteBound); the RLS seam refuses a non-number on a numeric column - #21336
Conversation
… as written; its whole-day copy (lteBound) is deleted The RLS compile seam and the engine's aggregate seam lower a bare-day $lte / $between with the shared lowerFilterCondition, and the RLS write check judges a declared date column in its stored form, so this face keeps no copy of the rule (ADR-0053 D-D1 items 5 and 9). The direct-call cases move to the seam's lowering or to stored-form inputs. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
… numeric column, as where does; pin the moved aggregate answers The compiled policy filter runs the spec's number-comparand verdict, the one the engine's where door consults: a comparand a numeric column cannot be compared with drops the policy through the refused-comparand route (read deny sentinel, write 403), and a numeric string is narrowed to its number. The objectql pins record what formula's deleted whole-day copy moves at the registry-less, audit-opt-out, text/text and direct applyInMemoryAggregation positions. Changesets for formula and plugin-security. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
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 13f8e49843428b24665e3154a48324e0222b0dd6 && git checkout 13f8e49843428b24665e3154a48324e0222b0dd6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 23365eaedf5bd07795b3c3a47ff6f113469e5432 f6a9373da41384d11a45a095372965154643cce8 && git checkout -B drift-repro 23365eaedf5bd07795b3c3a47ff6f113469e5432 && git merge --no-ff f6a9373da41384d11a45a095372965154643cce8
node scripts/docs-audit/affected-docs.mjs --json 23365eaedf5bd07795b3c3a47ff6f113469e5432
|
Contract reviewServed-tier: Read-only and adversarial, on PR #21336 at the head above; merge base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… review fixes for #21242 The WARN detail of a number-comparand refusal at the RLS compile seam is written for the clause it refused: the path is rooted at the clause, and the PostgreSQL server-bind sentence appears only for `using`, the clause a driver binds; `check` is evaluated in-process. Pinned for both clauses, plus an end-to-end text-column write-check cell. The plugin-security changeset names the additive `RlsFieldGuard.number` member and states the read reading as measured (SQLite only). The convergence design doc's F7 row is retired. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…ls at its one call site check:dispatcher-error-vocabulary reads PolicyNumberComparandRefusal as a code helper (its constructor assigns a parameter to .code), so the codes it stamps must be readable at the call site: 'INVALID_FILTER' and 400, the literals the spec's verdict types its refusal with. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…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>
…ld } against a column with no comparison class, as where does; applyInMemoryAggregation takes the reference rules (objectstack-ai#21406) Fixes objectstack-ai#21299 Clause-②: no (narrowing) **Status: the seat answered the open question with C (5952549615 on objectstack-ai#21299).** The no-class sentence stays `objectql`-local (`noClassReason`) beside `crossClassReason`, its precedent. The REST twin pin asserts that each engine position's reason equals the real `where` twin's diagnostic, verbatim, and that is the drift guard. The dispatch's no-third-copy clause is relaxed for this sentence only. Patch round 1 (`b397060136`) replaced the PENDING docblock with the decision record. A spec-side home (option A below) is recorded as a candidate for the spec lane in the seat's hand-over ledger, not filed. Dispatched by `domain:engine#1` (seat post objectstack-ai#6367), claim 5949513813, branch `claude/issue-21299-having-no-class-refusal`. Base `db0cf2231b`, `origin/main` merged at `69a12a0952`, head `b397060136` (patch round 1; round 0 was `466e632c33`). ## What changes - **`packages/objectql/src/having-filter.ts`** - The seam `crossClassReferenceViolation` now acts on the spec verdict's `no-class` answer as well as `cross-class`. Its reason is `where`'s sentence for the same pair: the referenced column first, then the target, as `driver-sql`'s `applyCrossFieldComparison` asks them. No second rule: both answers come from the one `crossFieldComparisonVerdict` call. - Both callers ask it of every reference. With `addDays`, only the `no-class` answer is taken from the seam (`where` asks it before it reads an offset); the cross-class half of an `addDays` pair stays `offsetPairViolation`'s, unchanged. - `assertAggregationFilterReferencesAreDeclared` is exported (module-level only; neither package entry re-exports `having-filter.ts`). `AggregationFilterDeclaration.object` becomes optional, so a caller with no object name gets a diagnostic that names the field alone. - **`packages/objectql/src/in-memory-aggregation.ts`**: `applyInMemoryAggregation`, when handed `fields`, runs `assertAggregationFilterReferencesAreDeclared` on each per-aggregation filter before the JSON-column rule, before any row. That is the same function and the same order `engine.aggregate` uses. The signature is unchanged. Without `fields`, nothing new is judged. - **Pins** - `packages/objectql/src/engine-aggregate-reference-verdict-positions.test.ts` (new): the enumeration pin and the card's shapes at the engine door. - `packages/objectql/src/engine-aggregate-filter.test.ts`: objectstack-ai#21242's direct `applyInMemoryAggregation` pin is split in two. With no field map it still answers 1 of 4. With a field map the cross-class pair is now refused, which is the ruled direction ("one door, whichever caller enters it"). - `packages/rest/src/aggregation-filter-where-doors.test.ts`: the positions × verdicts parity table against the real `where` twin on `SqlDriver`. The reasons are read from the twin's own withheld diagnostic, never retyped. `boot()` now takes an optional object and rows. - **`.changeset/21299-having-no-class-reference.md`**: `@objectstack/objectql` `minor`, BREAKING, `Clause-②: no (narrowing)`, `adr-0087: not-required (no-migration-prescription)`. ## Baseline and after (H1), through `engine.aggregate` on `SqlDriver` / better-sqlite3 Probe fixture: 6 rows, `photo` image, `tags` multiselect, `due_f` formula (`returnType: date`). Base `db0cf2231b`; after = `be8d2c428b` (built dists). | shape | base: populated | base: empty | after (both) | `where` twin (base and after) | |---|---|---|---|---| | per-aggregation `{ customer_id: { $ne: { $field: 'photo' } } }` (text vs image) | counted 6 of 6 | 0 | `INVALID_FILTER` / 400 | 400, `"photo" (type "image") has no scalar stored column a comparison can read.` | | per-aggregation `{ amount: { $ne: { $field: 'tags' } } }` (number vs multiselect) | 400 from the per-row array floor (row-dependent; 6 of 6 when `tags` is null) | 0 | `INVALID_FILTER` / 400 | 400, `"tags" (type "multiselect") ...` | | `having: { photo: { $ne: { $field: 'n' } } }` on an image groupBy | kept all 6 groups | `[]` | `INVALID_FILTER` / 400 | twin `photo` vs `amount`: 400, `the target field "photo" (type "image") ...` | | per-aggregation `{ closed_at: { $lte: { $field: 'due_f' } } }` (datetime vs formula) | counted 0 of 6 | 0 | `INVALID_FILTER` / 400 | 400, `"due_f" (type "formula") ...` | | control: `{ due_on: { $lte: { $field: 'due_on' } } }` | 6 | 0 | 6 / 0, unchanged | 6 / 0 | After the change, each per-aggregation refusal logs `where`'s reason verbatim, and the REST pin compares it against the twin's diagnostic. ## Hypotheses - **H1, confirmed.** On the base, `crossClassReferenceViolation` acted only on `cross-class`, through its two callers. `CROSS_FIELD_NO_CLASS_REASONS` is `list-or-object`, `file`, `formula`. `where`'s words are `driver-sql`'s, under `INVALID_FILTER` / 400. - **H2, none exists.** The sentence "has no scalar stored column a comparison can read" is written twice, inline, in `driver-sql`'s `applyCrossFieldComparison` (referent arm and target arm), and exported nowhere. `objectql` does not depend on `driver-sql`. `@objectstack/spec` and `@objectstack/core` hold no no-class sentence. Two other no-class wordings exist, neither of them `where`'s: `@objectstack/formula`'s `describeColumn` (behind the exported `findCrossFieldClassRefusal`) and `@objectstack/lint`'s RLS / sharing-rule doors. Per the dispatch, adding one to `packages/spec` is a spec edit, so this run stopped before it. The seat answered with C (5952549615); see the decision record below. - **H3, no changed answer for the existing callers.** `packages/verify`'s `checkDateBucketParity` calls `applyInMemoryAggregation(rows, ast)` with no `fields`, so the reference rules have nothing to judge. - Read, not run: no other in-repo caller outside `objectql` passes `fields` either (the dogfood parity tests, `service-analytics`' bucket-key test, the `driver-mongodb` parity test). - Measured against the rebuilt dists: `@objectstack/verify` 16 files / 120 tests passed, and dogfood 168 files passed (1 skipped), 1374 tests passed (9 skipped). - The one answer that moved is objectstack-ai#21242's own with-field-map pin, and it moved by the ruling. - **H4, posture kept, and recorded as rows.** - Registry-less host: not judged at the per-aggregation filter, at `having`, or at `applyInMemoryAggregation` without `fields`. - Audit-opt-out object's row-carried `created_at` / `updated_at`: not judged at the engine positions. Measured on `SqlDriver`: the `where` twin refuses it 400 ("the target field "created_at" is not a declared field"), while the per-aggregation filter counts 6 of 6. That divergence is pinned in the REST table as the recorded posture. - objectstack-ai#21336's pins stay green: the registry-less `picked: 3`, `['c1']` and the audit-opt-out 1 of 4. - **H5, unreachable, and pinned.** A `multiple: true` select, lookup or user is refused `INVALID_FIELD` / 400 by the engine's groupBy door and by its `min` / `max` door before `having` is read. Measured on `SqlDriver` and in the pin, for a groupBy, `min` and `max` of each. The same holds for `multiselect` and `json`. The only no-class columns that reach `having` are a file-family groupBy (every driver) and a formula groupBy on a driver whose door does not refuse it (the engine's in-memory path; `driver-sql` refuses a formula groupBy itself). ## The enumeration pin Spec verdict names: the dispatch's `same-class` is the spec's `comparable`. | verdict | `where` (driver-sql) | `having` | `aggregations[i].filter` | `applyInMemoryAggregation` (with `fields`) | |---|---|---|---|---| | `comparable` | answered | answered | answered | answered | | `cross-class` | 400 | 400 | 400 | 400 | | `no-class` | 400 | 400; list-or-object is refused earlier by the groupBy door | 400; a formula KEY is refused earlier by the materializable-filter door (`INVALID_FIELD`), as in `where` | 400 | | `unjudged` (type outside `FieldType`) | judged by the driver's own aliases | unreachable: the registry refuses the object | unreachable: same | not judged | | no declaration | 400 | not judged | not judged | not judged | - **objectql pin.** It sweeps all 15 × 15 ordered pairs at each of the three engine-side positions. The columns are one per row of `CROSS_FIELD_COMPARISON_TYPE_CLASSES`, plus `select` with `multiple: true`, plus a type outside `FieldType`. Each cell's expected answer is computed from `crossFieldComparisonVerdict` itself. A refused cell must be 400 before any read, and must name both classes or the no-class column with its declared type; the filter positions must keep those names off the wire. - **What makes it fail until placed.** - The verdict posture is a `Record` keyed by the spec's verdict union, so a new verdict is a type error. It is also a runtime failure: the set of verdicts the sweep meets must equal the record's keys. - The no-class reasons the sweep covers must equal `CROSS_FIELD_NO_CLASS_REASONS`. - The filter slots of `EngineAggregateOptionsSchema` must equal `where`, `having` and `aggregations[i].filter`. - The published exports of the two evaluator modules must equal `applyInMemoryAggregation` (a position) and `bucketDateValue` (not a position). - **REST pin.** One row per verdict this object can give: comparable, cross-class, and no-class for file, list-or-object and formula. Each row runs at all four positions, empty and populated. Every position's answer (refusal envelope, or total count) must equal the `where` twin's. For the no-class rows, the twin's reason, read from its withheld diagnostic, must appear verbatim in the per-aggregation log line, in `applyInMemoryAggregation`'s diagnostic and, for the file row, in the `having` message. ## Reverse verification (at `623b7474bd`, before the merge) The mutation was made with `node scripts/ablation-replace.mjs` and its EXIT / INT / TERM trap restore. - **Mutation.** It replaced only the seam's no-class arm, `if (verdict.verdict === 'no-class') return noClassReason(...)`, with an always-false guard carrying the marker `ABLATION_21299`. Anchor 1 → 0; blob `f4c9c878df05` → `eb68be65ef10`. - **Build.** `objectql` was rebuilt (exit 0). `ablation-dist-preflight` found the marker in 4 built files. - **Prediction, then reading.** The no-class refusal pins go red and the controls stay green. That is what happened: - `objectql`: **10 failed / 303 passed** (313). The 10 are the three sweeps, the three filter shapes, the `having` image shape, the two `addDays`-against-a-formula rows (per-aggregation and `having`, both answered with the arm off), and the audit-on `created_at` vs image control. The comparable, cross-class, census, no-declaration, `multiple`-seam and same-class rows stayed green, as did all of `engine-aggregate-filter.test.ts` and `engine-aggregate-having-comparand-shape.test.ts`. - `rest`: **3 failed / 23 passed** (26). The 3 are the no-class rows (file, list-or-object, formula). The comparable and cross-class rows, the audit-opt-out posture row and every earlier test stayed green. - **Restore.** Proven four ways: the blob equals HEAD (`f4c9c878df05`), `git diff HEAD` is empty, `--absent` preflight passes after a rebuild ("marker absent from all 14 built files", whole tree clean), and `git status --porcelain` is empty. The re-run was 313 / 26 passed. ## Verification (all heavy runs through `os-verify-lock.sh`) The suites below ran at `be8d2c428b` (the merge). Two commits followed. `251922e411` gives the pin file's two single-choice columns their `options`, the legal declaration `FieldSchema` now requires; `466e632c33` corrects one changeset sentence. At `251922e411` that file was re-run (26 passed) and `objectql` was re-typechecked (exit 0, test layer held at 40 / 234). The whole gate union was re-run at `466e632c33` (below). - Full `@objectstack/objectql`: `--project local` 365 files, **7381 passed**; `--project repo` 1 file, 5 passed. `VERDICT command-exit 0`. - `@objectstack/rest` `aggregation-filter-where-doors.test.ts`: 26 passed. `@objectstack/verify`: 16 files, 120 passed. Dogfood (`pnpm --filter @objectstack/dogfood exec vitest run`) against the rebuilt dists: 168 passed + 1 skipped files, 1374 passed + 9 skipped tests. Each run's verdict was `command-exit 0`. - Typecheck: - `pnpm --filter @objectstack/objectql run typecheck` exit 0. Its test layer held at 40 files / 234 errors, unchanged, so the new pin file adds none. - `pnpm --filter @objectstack/rest run typecheck` exit 0, test layer 0 errors. - Build after the merge: `turbo run build --filter='@objectstack/dogfood^...' --filter=@objectstack/rest --filter=@objectstack/verify`, 63 of 63 tasks. - Gates, at `466e632c33`: - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 65 commands. All 65 ran with exit 0, each exit code recorded beside the printed command. - `--ran` reports "65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)". - In the earlier pass at `251922e411`, `check:dual-build-cjs-loads` answered `PREREQUISITE NOT MET` (exit 3: 8 unrelated packages had no `dist/`). Those packages were built (41 of 41 turbo tasks from cache), and the gate exited 0 there and in the final pass. - The four roster gates whose rosters sit under my paths also exited 0: `check-changeset-fixed`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`. - NOT MEASURED, because their argv takes workflow values and CI runs them: `check-issue-citations --census`, `check-shard-attestation`, `check-test-completeness`. - Lint, a proven narrowing: - Command: `pnpm exec eslint --no-inline-config --format json` on the 5 changed `.ts` files gives 5 results, 0 errors, 0 warnings. - Why the narrowing holds: `eslint.config.mjs` is not type-aware (no `parserOptions.project`; header near `:328`), so this diff cannot move any untouched file's verdict. - The full `pnpm lint` is CI's. ## Deviations (both accepted by the seat, 5952549615; the `rest` test file is declared on the cli seat post, 5952559844) - `packages/rest/src/aggregation-filter-where-doors.test.ts` is outside the claim's file surface, and it is a test only. The `where` twin can only be refused on a real `driver-sql`, which `objectql` does not depend on. This is the same harness and the same reason as objectstack-ai#21255's deviation 2. - The seam judges the `no-class` answer for an `addDays` pair too. Before, an `addDays` pair against a formula was answered at both positions. One against a file or list column was refused, but in the `addDays` pair rule's words: that rule reads those types as `text`, so it answered with a cross-class or an offset sentence (read from the source, not measured). It now gets the no-class sentence. This stays within "no-class references at these positions", and the changeset says so. ## Decision record (H2): C The seat chose **C** in 5952549615. `objectql` keeps the local copy (`noClassReason`), on the `crossClassReason` precedent, which landed with objectstack-ai#21255 (PR objectstack-ai#21297, contract review PASS 5944780154). The REST twin pin is the drift guard. Two options were not taken: - **A**, a pure sentence function beside the verdict in `packages/spec`: it adds a published spec export (`Clause-②: yes`, spec-lane work) and a `driver-sql` edit outside this claim. It would also move the cross-class sentence. It is recorded as a candidate for the spec lane, not filed, because no reader gets a wrong answer today. - **B** strands `formula`, which depends on `spec` alone. **Patch round 1** (`b397060136`, report 5953205116) changed only `noClassReason`'s docblock, +8/−5, every line inside the JSDoc block. At that head: - the pins pass: `objectql` 313 and `rest` 26; - `objectql` typecheck exits 0; - gates: 65 derived, 65 run, all exit 0; - the full suites stand from `be8d2c428b`, because no source line outside comments changed since then (proven by the filtered diff). ## Acceptance notes (nothing filed) - `having` refusal messages exceed the REST envelope's 500-character bound, so the wire cuts the closing clause about how a column's class is read. The no-class reason sits in the surviving prefix (the REST pin asserts it on the wire). This predates the PR (objectstack-ai#21255's note). Carrier: 承接者:无. - The `where` words for a cross-class pair say "is stored as" where the engine positions say "is" (objectstack-ai#20127's convention for a computed column). The no-class sentence needs no such reading and is byte-identical. --- _Generated by [Claude Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21242
Clause-②: no (narrowing)
This deletes
@objectstack/formula's own copy of the whole-day upper bound (lteBoundinmatches-filter.ts). Ruling5933322270(letter A, on #21109) decided the deletion once the RLS write check judged the stored form (PR #21235). Its precondition, #21255, landed asc2cd65154, as the seat's answer B (5942310644) sequenced. The deletion opened one fail-closed write/read split, a number column against a day string. The engine'swheredoor already refuses that comparand, so the RLS compile seam inplugin-securitynow runs the same spec verdict. That surface was declared on the services seat post (5945507305) and in claim amendment5945512244before the edit.Base
5f7d84769a(the fast-forward of the branch toorigin/main). Headf6a9373da4(the contract review 5946309118 judgedbe721ef6fb; the review round addeddd75c24d11andf6a9373da4, see below).#20822 is closed by the seat at landing. This PR does not close it.
What changes
packages/formula/src/matches-filter.ts.lteBound, its docblock, theaddWholeDaysbullet that named it, and thenextUtcCalendarDay/isUnboundedAboveimports are deleted.$lteand a$betweenmaximum now compare as written, with the sameorder()every other ordering operator uses. A docblock on the$ltearm records where the whole day is applied now and gives the remedy for a caller that passes no seam:lowerFilterCondition(filter, { isDatetimeColumn }).packages/plugins/plugin-security/src/rls-compiler.ts.RlsFieldGuardgainsnumber: each declared column'stype, and a formula'sreturnType.judgeCompiledComparandsrunsnarrowPolicyNumberComparandsafter the shape door and before the comparand-type door, the engine'swhereorder.numberComparandFieldVerdict/numberComparandDoorVerdictdecide,NUMBER_COMPARAND_DOOR_SCALAR_OPERATORS/NUMBER_COMPARAND_DOOR_LIST_OPERATORSname the positions, andnumberComparandRefusalMessagegives the words. No rule is restated.refused-comparandroute: the read gets the deny sentinel and the write a 403. A numeric string is narrowed copy-on-write, and the provenance mark is carried.packages/plugins/plugin-security/src/security-plugin.ts.numberComparandFieldsCacheis filled in the sameloadObjectFieldNamespass as thedatetimecache and cleared with it. Both guard construction sites pass it.plugin-securitygains a new pin file. There are two changesets:formulaandplugin-security, eachminor, BREAKING,Clause-②: no (narrowing).Caller table (H1), re-run at
5f7d84769alteBoundwas atmatches-filter.ts:902,$lteused it at:688and$betweenat:693, as H1 said. The three commits betweenc2cd65154and the base touch none of these files. There are 4 production call sites in 2 packages (objectql,plugin-security), not 3:formulais the definer. Every other non-test hit is a re-export, a type or prose.objectqlhaving-filter.ts:1657compareWithReference(havingandaggregations[i].filter, every{ $field }comparison)plugin-securityrls-check-stored-form.ts:316storedFormCheckJudge(the RLS write check)datetime. The compile seam lowersdatetimeonly, and the stored-form step coversdate/datetime/timeplugin-securityexplain-engine.ts:972matchUnderDeclaredColumns(the explain row verdict)usingfilter, on stored rowsplugin-securitysecurity-plugin.ts:3622(:3611at base)clearsWall(the tenant wall)tenantLayer0FilterOf(tenant-layer.ts:173-179) emits only{ organization_id: X },{ organization_id: { $in } }or the deny-id equalityThe two registry-less pins (H2)
H2 said the deletion moves both pins by design. Measured, it moves neither as written: no row of their fixtures closes on its due day, so the as-written comparison gives the same answer. Each pin is kept, and each is extended with one boundary row so that it now records the move. The reverse check reddens both.
engine-aggregate-filter.test.ts, registry-lessclosed_at $lte { $field: 'due_on' }over ORDERSpicked: 3picked: 3o7closed on its due day at 15:00 (added)picked: 4of 7picked: 3of 7'2026-01-05T15:00:00.000Z'sorts above'2026-01-05', which is ADR-0053 D-D1 item 5 for a caller that passes no seamengine-aggregate-having-comparand-shape.test.ts, registry-lesslast_closed $lte { $field: 'first_due' }over DT_ROWS['c1']['c1']c4closed on its due day at 15:00 (added)['c1', 'c4']['c1']Residual rows (H3, #21299) and one more mainline row
Measured through
engine.aggregate, which RESTPOST /api/v1/data/:object/querycalls, on both driver doors, with the before and after formula dists swapped in turn. Every changed answer is pinned.wheretwinsystemFields: { audit: false }): row-carriedcreated_at/updated_at$lte { $field: 'due_on' }, per-aggregationfilterengine.aggregateINVALID_FILTER/ 400) and inSqlDriver'swhere(measured on an audit-on object; the audit-opt-outwherewas not driven)havingengine.aggregatemax(created_at)on an undeclared field isINVALID_FIELD/ 400applyInMemoryAggregation,closed_at $lte { $field: 'due_on' }, with or without a field map@objectstack/objectqlexportengine.aggregate)textcolumns of one class,code $lte { $field: 'label' }with'2026-01-05 noon'against'2026-01-05'engine.aggregateper-aggregationfilter, andhavingover two groupBy columnsSqlDriverwherekeepsb2only;SqlDriverengine.aggregatewent frompicked: 2to1, now matching the twinThe last row moves toward
where. It is the aggregate twin of the RLS text-to-text cell (R3), and it is pinned in both test files.The RLS write check (H4)
Measured through
ObjectQL.insert+SecurityPlugin+SqlDriver(better-sqlite3), as a member, with the policy'susingequal to itscheck:titletext$lte '2026-01-05','2026-01-05T15:00:00Z''2026-01-05 noon'title $lte { $field: 'code' },code='2026-01-05'amountnumber$lte '9999-12-31', 5refused-comparandon both clausesdate/datetimeon the boundary day,datetime $lte '9999-12-31', text earlier day, number vs number, date vs date reference, text vs'9999-12-31'The
wheredoor question, measured:engine.findwithwhere { amount: { $lte: '9999-12-31' } }returnsINVALID_FILTER/ 400 ("compares a declared number field against "9999-12-31" … which is not a number"). The same holds for'abc', while'10'is narrowed (1 row). So the seam refuses the same comparand, per answer B.Direct-call cases (H5)
H5 said "the storage-form lowering" means
@objectstack/core'stemporalStorageForm. Measured:formuladepends on@objectstack/specalone (package.json), so its tests cannot reachcorewithout a new dependency edge, a vitest alias and a lockfile change. The lowering the seams apply to a bound is spec'slowerFilterCondition(ADR-0053 D-D1 item 4), andtemporalStorageFormis what the write check applies to the image, pinned inrls-check-stored-form.test.ts(green). So the cases move to the seam's lowering, or to storage-form literal inputs. None is deleted:matches-filter.test.ts, calendar-day block. These 8 assertions changed answer: the instants at 00:00, 09:15 and 23:59:59.999 under$lte '2026-07-28',$betweenat 21:40, the last day at 23:59:59.999, aDateat 10:00, the same-day$between, and'9999-12-30T10:00Z'. Each is now asserted as written (false), and its whole-day answer is asserted throughlowerFilterCondition(…, { isDatetimeColumn })(true), together with the lowered shape ($ltthe next day;{ $null: false }on the last day). The other assertions are kept, and run through the lowering where that is meaningful. One case is new: a number against a day string is false.matches-filter-field-reference-offset.test.ts. "A calendar day stays a calendar day" moves to the storeddateform (done: '2026-03-02'). The old instant-against-day pair is now pinned as written (false ataddDays: 1, true at2), and handed the declared columns it is refusedINVALID_FILTER/ 400.matches-filter-temporal-conformance.test.ts. EveryTEMPORAL_CASESfilter runs throughlowerFilterConditionwith the declared{ at: datetime, on: date }map, the RLS compile seam's input, as thedriver-mongodbanddriver-memoryconformance suites already do. The expected rows are unchanged.Filter-semantics faces (
compile-surfaces.md)driver-sql: already conformant. It compiles a bare-day bound as written on a column that is notdatetime. Measured:where { title: { $lte: '2026-01-05' } }hides the instant text, andtitle $lte { $field: 'code' }keepsb2only. There is no whole-day arm insql-driver.ts(one comment hit).nextUtcCalendarDayhits inremote-transport.ts). The transport tests that import the matcher pass (103).read-scope-sql.ts.read-scope-sql-conformance(which imports the matcher) is green.native-sql-strategy.ts:1761,objectql-strategy.ts:566,preview-evaluator.ts:106) are removed by its own deletion (ADR-0053 D-D1 item 9), not by this card.native-sql-filter-logic-conformanceis green.formula: changed (this PR).having-filter: answers changed, source untouched.{ $field }pairs that no seam lowers are compared as written, and both aggregate test files pin the change.driver-memory/driver-mongodb: already conformant. Their filter compilers carry no copy (0 hits inmemory-matcher.ts/mongodb-filter.ts). The cube face'sdateRangewindow (memory-analytics.ts:1236) is D-D1 item 8 and out of scope.Review round (head
f6a9373da4, after contract review PASS 5946309118)Section added by the seat (
domain:engine#1), from the dev's review-round report:plugin-securitychangeset. Two sentences are corrected:RlsFieldGuard, the optionalfieldGuardargument of the root-exportedRLSCompiler.compileFilter, gains the optionalnumbermember. It is additive and not an accept-set change;using. Both clauses are pinned.plugin-securitydoor: a bare-day$lteon a text column now refuses the write (403) and hides the row on read, with the controls admitted. It goes red with the oldformuladist swapped in.check:dispatcher-error-vocabulary(red ondd75c24d11) is fixed by spelling the one call site's code and status as literals (INVALID_FILTER, 400), the gate's preferred remedy.f6a9373da4: theplugin-securityfull suite passes (3,423); the wholeLint & Repo Gatesjob, run locally fromlint.yml, is 185 of 185 steps green;dispatch-gatesis 67 derived, 67 run, 0 not measured.Verification (head
be721ef6fb, throughscripts/pm/os-verify-lock.sh)turbo run build --filter='@objectstack/dogfood^...'(63 of 63) on the base tree. After the change,formulaandplugin-securitywere rebuilt. Theformuladist has 0lteBoundhits (3 before).@objectstack/formula: 43 files, 1257 passed;@objectstack/plugin-security: 158 files, 3418 passed and 23 skipped, including PR fix(plugin-security): the RLS write check judges a date / datetime / time column in its stored form #21235'srls-check-stored-formpins;@objectstack/objectql:test360 files, 7113 passed, andtest:repo1 file, 5 passed;@objectstack/dogfood, against the rebuiltformula/plugin-securitydists (0lteBoundhits in theformuladist, 4narrowPolicyNumberComparandshits in theplugin-securitydist): 164 files passed and 1 skipped, 1354 tests passed and 3 skipped, exit 0.formula,plugin-securityandobjectql: exit 0,check:test-typecheckOK.driver-sql: 8 files, 481 passed and 4 skipped;driver-sqlite-wasm: 6 files, 217 passed;driver-turso: 2 files, 103 passed;driver-memory: 18 passed;driver-mongodb: 37 skipped (nomongodhere);platform-objects: 18 passed;service-analytics: 2 files, 72 passed;restaggregation-filter-where-doors: 20 passed.dispatch-gates --commandsderived 66 commands. All 66 were run and all exited 0.dispatch-gates --ranreports "66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN". Two gates first hit my own 280 s wrapper timeout (check:query-options-erasure,check:type-check-debt) and were re-run to exit 0.check:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (exit 3) because 8 unrelated packages had nodist/. They were built (41 of 41 tasks from cache), and the gate then exited 0. Lint is narrowed, not the fullpnpm lint. The checked population is the 9.tsfiles of the diff, linted witheslint --no-inline-config --format json: 9 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project, header at:327) and noimportplugin, so this diff cannot move the verdict on any file it does not touch. The fullpnpm lintis left to CI.ablation-replaceturned the number door off at its one call site (judgeCompiledComparands(…, fieldGuard?.number)without the third argument), on-disk anchor 1 to 0.rls-number-comparand-door.test.tsthen went 4 failed / 7 passed: the three refusal pins and the== '10'narrowing pin. The restore was proven by blob equal to HEAD and an emptygit diff HEAD.$ltewith a'ablation-21242'marker (on disk 1; anchor 1 to 0). Theformulasuites went 9 failed. 4 of those are the as-written pins: the bare-day block, the last-day as-written case, the9999-12-30control's as-written leg, and the instant-against-shifted-day pair. The other 5 come from the ablation's simpler day arithmetic on an impossible day and on9999-12-31(two temporal-conformancedatecases, the impossible-day case, and one leg each of the seam and no-instant cases), whichlteBoundhandled with its calendar check andUNBOUNDED_ABOVE.ablation-dist-preflightfound the marker indist/. Theobjectqlpins went 6 failed of 286: both registry-less boundary pins, the text/text, audit-opt-out andapplyInMemoryAggregationpins, and thehavingtext/text pin.--restore, a rebuild,--absentpreflight exit 0, blobf963059621equal to HEAD, and an emptygit diff HEAD.Acceptance notes (out of the claim's surface; nothing filed)
plugin-security/src/rls-check-stored-form.test.ts:32-41, :233. The header "formula's whole-day copy is out of reach here" and thewithoutWholeDayCopymatcher wrapper are now redundant: the wrapper changes no answer at head, and the pins stay green. Carrier: the services seat when it next touches the file (otherwise 承接者:无).docs/design/predicate-compilation-convergence.md:361: the F7 row still listslteBound(2 sites). Stale prose, 承接者:无.driver-sql/src/cross-field-conformance-cases.ts:145: the note names "memorylteBound". It was already stale before this PR, 承接者:无.check.amount.$lte/using.amount.$lte), and it names PostgreSQL's server error only forusing, the clause a driver binds;checkis evaluated in-process. Both clauses' wording is pinned inrls-number-comparand-door.test.ts.plugin-securitychangeset names one facet that admits more: after narrowing,record.amount == '10'admits a write of10. The check used to refuse that write, compared as text, while the read showed the row. That facet is read/write agreement, and it ships under theno (narrowing)line.Generated by Claude Code