Commit 68c5ab7
fix(spec): the null ordering-comparand refusal names only evaluation faces that exist (#21408)
Fixes #21397
Clause-②: no
## What changed
Two texts named driver-memory's reference matcher (`memory-matcher.ts`),
which has been deleted. Both now name only faces that exist. Both null
ordering-comparand refusals also said "no two evaluation faces agree",
which measures false read pairwise (driver-sql and formula agree), so
both now say only what was measured. This is a text-only change: no door
accepts or refuses anything new.
1. **`packages/spec/src/data/filter.zod.ts`,
`nullOrderingComparandMessage`.** This is the schema-door refusal for a
`null` comparand of `$gt` / `$gte` / `$lt` / `$lte`. The first sentence
(`null is not a valid $gt comparand.` and its siblings), the `{"$eq":
null}` / `{"$ne": null}` prescription and the ruling sentence (`Ruled
2026-09-01: …`) are byte-identical. The second sentence changes in two
places:
- "no two evaluation faces agree" becomes "the evaluation faces do not
agree".
- The parenthesis changes. Before: `(driver-memory's live path reads two
absences as equal; its reference matcher compares through JS coercion)`.
After: `(driver-memory's query path reads a stored null as equal to it,
so {"$gte": null} admits that row; driver-sql compares against SQL NULL
and admits no row)`.
- The longest message is 484 characters (`$gte`).
2. **`packages/spec/src/data/filter-comparand-shape.ts`,
`nullOrderingComparandError`.** This is the runtime twin, the
`parseFilterAST` refusal. Only its string changes, at `:708`: "null is
not ordered; no two evaluation faces agree on what it matches." becomes
"null is not ordered; the evaluation faces do not agree on what it
matches." Every other sentence is unchanged.
- It never carried the parenthesis.
- It cannot import the schema-door message, because `filter.zod.ts:5`
imports `./filter-comparand-shape`. So the two messages stay parallel
rather than shared.
- The longest refusal is 479 characters (`$gte`, context
`find('deal')`), inside the 500-char client bound that
`filter-comparand-shape.test.ts` pins.
3. **`packages/spec/src/data/filter-operator-vocabulary.test.ts`.** The
assertion message's EVERY-face list:
- drops the deleted face: `driver-memory (query path, reference matcher,
analytics face)` becomes `driver-memory (query path, analytics face)`;
- adds `driver-sql` and `driver-turso (remote transport)`. Clearing
`$empty` (commit `f1e921ab8e`) edited both, in `sql-driver.ts` and
`remote-transport.ts`, and `remote-transport.ts` compiles its own WHERE
(`buildWhereSQL`).
4. **`.changeset/21397-null-ordering-message-faces.md`.** A
`@objectstack/spec` `patch`, because both messages ship.
No comment line in `filter.zod.ts` or `filter-comparand-shape.ts` is
touched. #21395 landed meanwhile and holds those lines, and
`origin/main` is merged in (`703bb28168`); against `main`, the branch's
diff is exactly the four files above.
## Measurement: what each existing face answers
The question is what an ordering against `null` matches on each
evaluation face that exists today. The refusal is skipped by calling
each face directly, without `parseFilterAST`. The run used built `dist/`
at `ecb6ca0258` plus this diff, with the fixture from
`memory-null-ordering-comparand-unreachable.test.ts`.
In the **stored-null** reading the rows are `{id:1,n:5}`, `{id:2,n:0}`
and `{id:3,n:null}`:
| filter | driver-memory `InMemoryDriver.find` (query path) | driver-sql
`SqlDriver.find`, better-sqlite3 | formula `matchesFilterCondition` |
|---|---|---|---|
| `{n: {$gt: null}}` | no row | no row | no row |
| `{n: {$gte: null}}` | **row 3** | no row | no row |
| `{n: {$lt: null}}` | no row | no row | no row |
| `{n: {$lte: null}}` | **row 3** | no row | no row |
- **The missing-key reading:** the rows are `{id:1,n:5}`, `{id:2,n:0}`
and `{id:4}`. All three faces admit no row for all four operators.
- **Positive control:** `{n: {$gte: 0}}` answers rows 1 and 2 on all
three faces.
- **The disagreeing pair the parenthesis names:** driver-memory's query
path (`memory-driver.ts`, `convertToMongoQuery`, then
`normalizeFilterCondition`, then mingo) and driver-sql.
- driver-sql's `$gte` arm calls knex `where(field, '>=', null)`, which
compiles to `col >= ?` with a NULL binding. That is UNKNOWN in SQL.
- Raw mingo `new Query({n: {$gte: null}})` gives the same answer as
driver-memory's `find`.
- **Not measured:**
- driver-mongodb: there is no server in this container.
- driver-turso and the Postgres / MySQL dialects: these were read, not
run. They compile the same `col OP ?` with a NULL binding.
- driver-memory's analytics face, objectql `having` and
service-analytics: these refuse the shape themselves
(`assertListComparandShapes`), so they give no answer to compare.
- **The published door:** over the built `dist/data/index.js`,
`FieldOperatorsSchema.safeParse({ $gt: null })` answers `success: false`
with the new message. The control `{ $gt: 5 }` answers `success: true`.
## Pins that read each message
None of these pins reads either changed phrase. All are green and
unloosened at `703bb28168`.
- **Schema-door message (`nullOrderingComparandMessage`):**
- `filter.test.ts:176`–`:179`: on `ComparisonOperatorSchema`, the first
sentence, both prescription halves, and the absence of `Invalid input`.
- `filter.test.ts:187`: the first sentence, on `FieldOperatorsSchema`.
- `filter.test.ts:217`: the negative control.
- `filter-save-door-face-parity.test.ts:347`: the save-door message
equals `FieldOperatorsSchema`'s message for the same comparand. Both
come from the one function.
- `filter-save-door-face-parity.test.ts:351`–`:352`: the prescription.
- **Runtime twin (`nullOrderingComparandError`):**
- `filter-comparand-shape.test.ts:436`–`:446`: the first sentence, the
path, both prescription halves, the authoring spellings and the
UNFILTERED tail.
- The 500-char bound in the same file.
- service-analytics `filter-value-type-fidelity.test.ts:495` and
`where-face-arms-refusal.test.ts:88`: the first sentence.
- **The vocabulary assertion message:** no pin reads it. It prints only
when its assertion fails.
- **Elsewhere:** `git grep` over the whole tree (excluding
`CHANGELOG.md` and the releases pages) finds no other test that reads
either phrase.
## Tests and gates
All of these ran at head `703bb28168`, the merge of `origin/main`
`ceb4a939b4`. Every heavy run went through `os-verify-lock`.
- **Build:** the `@objectstack/service-analytics^...` closure plus the
`@objectstack/lint...` closure, spec included: exit 0.
- **`pnpm --filter @objectstack/spec check:generated`:** all 15
generated artifacts are up to date.
- **The pinning files** (`filter.test.ts`,
`filter-operator-vocabulary.test.ts`, `filter-comparand-shape.test.ts`,
`filter-save-door-face-parity.test.ts`): 4 files, 427 passed and 1 todo.
- **service-analytics** `filter-value-type-fidelity.test.ts` and
`where-face-arms-refusal.test.ts`: 2 files, 191 passed.
- **`pnpm --filter @objectstack/spec typecheck`:** exit 0.
`check:test-typecheck` reports OK, with 52 files, 246 errors and 135
pinned signatures held.
- **The full `@objectstack/spec` suite:** 600 files passed, with 17606
tests passed and 1 todo.
- **`dispatch-gates --commands`** derived 85 commands. Results:
- 84 exited 0.
- `check:type-check-debt` hit a 420 s local timeout, then exited 0 under
the lock: "26 raw tsc error(s) total, none above its recorded number".
- `dispatch-gates --ran` reports "85 derived famil(ies) accounted for —
85 run, 0 NOT-MEASURED".
- Named verdicts:
- `check:doc-authoring`: 17232 customer-facing strings clean.
- `check:nul-bytes`: OK, 9746 files.
- `check-empty-changeset`: 1 declaring changeset.
- `check-changeset-no-major`: no `major` bump.
- `check-adr-0087-registration`: no declared-breaking changeset.
- `check-issue-citations`: no issue citations added.
- `check:where-matcher`: 0 silently-wrong.
- `check:dual-build-cjs-loads`: 105 entry points across 66 packages
load.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bdd3654 commit 68c5ab7
4 files changed
Lines changed: 33 additions & 6 deletions
File tree
- .changeset
- packages/spec/src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
705 | 705 | | |
706 | 706 | | |
707 | 707 | | |
708 | | - | |
| 708 | + | |
709 | 709 | | |
710 | 710 | | |
711 | 711 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
87 | | - | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
88 | 89 | | |
89 | 90 | | |
90 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
467 | 467 | | |
468 | 468 | | |
469 | 469 | | |
470 | | - | |
471 | | - | |
472 | | - | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
473 | 474 | | |
474 | 475 | | |
475 | 476 | | |
| |||
0 commit comments