fix(spec): the null ordering-comparand refusal names only evaluation faces that exist - #21408
Conversation
…ertion name only faces that exist
The schema-door refusal for a null $gt/$gte/$lt/$lte comparand gave, as its
example of the evaluation faces disagreeing, driver-memory's reference matcher,
which has been deleted. The parenthesis now names two faces that exist and were
measured to disagree: driver-memory's query path admits a stored-null row for
{"$gte": null}; driver-sql compares against SQL NULL and admits no row. The
first sentence, the prescription and the ruling sentence are unchanged.
The filter-operator vocabulary assertion message no longer lists the deleted
reference matcher among driver-memory's faces.
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 138 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 d7e7faa8ea0152fd8da5a2d4e7b960a3d09b1c11 && git checkout d7e7faa8ea0152fd8da5a2d4e7b960a3d09b1c11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ceb4a939b42f7c91ac8e9d371df34b8663d3f7bc 703bb2816894a4a8809f1c02b8f43360246b1118 && git checkout -B drift-repro ceb4a939b42f7c91ac8e9d371df34b8663d3f7bc && git merge --no-ff 703bb2816894a4a8809f1c02b8f43360246b1118
node scripts/docs-audit/affected-docs.mjs --json ceb4a939b42f7c91ac8e9d371df34b8663d3f7bc |
…aces do not agree Measured, driver-sql and formula agree on an ordering against null (both admit no row), so "no two evaluation faces agree" was false read pairwise. The schema-door message (nullOrderingComparandMessage) and its runtime twin (nullOrderingComparandError) now both say "the evaluation faces do not agree"; every other sentence of each is unchanged. The vocabulary assertion's every-face list now also names driver-sql and driver-turso's remote transport, which clearing $empty edited. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ll-ordering-message-faces
Contract reviewServed-tier: Inputs read: card #21397 (body and all five comments: triage 5950939866, claim 5951484481, dev report 5952679308, the seat's answer and claim amendment 5952717122, dev report 5955118186), PR #21408 (body as rewritten by the seat, file list, net diff against ① Derived judgmentsAccept set: nothing moves, at any door — RIGHT. The schema-door slot ( Public surface: two published runtime strings, text only — RIGHT. No export is added, removed or renamed ( The card's filing gate was class (a), a false present-tense fact, so the new text has to be true. Each clause, judged against the code at the head:
Tree-wide residue: ② Semver level
③ Boundary flagsDev report 5952679308 (round 1):
Seat ruling 5952717122: honoured — one phrase per string, twin's string only ( Dev report 5955118186 (patch round 1):
Triage direction 5950939866, item by item: both texts name only faces that exist — yes (①); the refusal's first sentence and ruling sentence stay — yes, byte-identical in the diff; the twin checked for the parenthesis — it carried none, and its one phrase moved under the seat's answer B; Claim 5951484481: the file surface is honoured (plus the amendment's twin string); the dev ran Check-runs on the head, as read: the seven required contexts — Implemented-by: VERDICT: PASS Generated by Claude Code |
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.packages/spec/src/data/filter.zod.ts,nullOrderingComparandMessage. This is the schema-door refusal for anullcomparand 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:(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).$gte).packages/spec/src/data/filter-comparand-shape.ts,nullOrderingComparandError. This is the runtime twin, theparseFilterASTrefusal. 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.filter.zod.ts:5imports./filter-comparand-shape. So the two messages stay parallel rather than shared.$gte, contextfind('deal')), inside the 500-char client bound thatfilter-comparand-shape.test.tspins.packages/spec/src/data/filter-operator-vocabulary.test.ts. The assertion message's EVERY-face list:driver-memory (query path, reference matcher, analytics face)becomesdriver-memory (query path, analytics face);driver-sqlanddriver-turso (remote transport). Clearing$empty(commitf1e921ab8e) edited both, insql-driver.tsandremote-transport.ts, andremote-transport.tscompiles its own WHERE (buildWhereSQL)..changeset/21397-null-ordering-message-faces.md. A@objectstack/specpatch, because both messages ship.No comment line in
filter.zod.tsorfilter-comparand-shape.tsis touched. #21395 landed meanwhile and holds those lines, andorigin/mainis merged in (703bb28168); againstmain, the branch's diff is exactly the four files above.Measurement: what each existing face answers
The question is what an ordering against
nullmatches on each evaluation face that exists today. The refusal is skipped by calling each face directly, withoutparseFilterAST. The run used builtdist/atecb6ca0258plus this diff, with the fixture frommemory-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}:InMemoryDriver.find(query path)SqlDriver.find, better-sqlite3matchesFilterCondition{n: {$gt: null}}{n: {$gte: null}}{n: {$lt: null}}{n: {$lte: null}}{id:1,n:5},{id:2,n:0}and{id:4}. All three faces admit no row for all four operators.{n: {$gte: 0}}answers rows 1 and 2 on all three faces.memory-driver.ts,convertToMongoQuery, thennormalizeFilterCondition, then mingo) and driver-sql.$gtearm calls knexwhere(field, '>=', null), which compiles tocol >= ?with a NULL binding. That is UNKNOWN in SQL.new Query({n: {$gte: null}})gives the same answer as driver-memory'sfind.col OP ?with a NULL binding.havingand service-analytics: these refuse the shape themselves (assertListComparandShapes), so they give no answer to compare.dist/data/index.js,FieldOperatorsSchema.safeParse({ $gt: null })answerssuccess: falsewith the new message. The control{ $gt: 5 }answerssuccess: true.Pins that read each message
None of these pins reads either changed phrase. All are green and unloosened at
703bb28168.nullOrderingComparandMessage):filter.test.ts:176–:179: onComparisonOperatorSchema, the first sentence, both prescription halves, and the absence ofInvalid input.filter.test.ts:187: the first sentence, onFieldOperatorsSchema.filter.test.ts:217: the negative control.filter-save-door-face-parity.test.ts:347: the save-door message equalsFieldOperatorsSchema's message for the same comparand. Both come from the one function.filter-save-door-face-parity.test.ts:351–:352: the prescription.nullOrderingComparandError):filter-comparand-shape.test.ts:436–:446: the first sentence, the path, both prescription halves, the authoring spellings and the UNFILTERED tail.filter-value-type-fidelity.test.ts:495andwhere-face-arms-refusal.test.ts:88: the first sentence.git grepover the whole tree (excludingCHANGELOG.mdand the releases pages) finds no other test that reads either phrase.Tests and gates
All of these ran at head
703bb28168, the merge oforigin/mainceb4a939b4. Every heavy run went throughos-verify-lock.@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.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.filter-value-type-fidelity.test.tsandwhere-face-arms-refusal.test.ts: 2 files, 191 passed.pnpm --filter @objectstack/spec typecheck: exit 0.check:test-typecheckreports OK, with 52 files, 246 errors and 135 pinned signatures held.@objectstack/specsuite: 600 files passed, with 17606 tests passed and 1 todo.dispatch-gates --commandsderived 85 commands. Results:check:type-check-debthit a 420 s local timeout, then exited 0 under the lock: "26 raw tsc error(s) total, none above its recorded number".dispatch-gates --ranreports "85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED".check:doc-authoring: 17232 customer-facing strings clean.check:nul-bytes: OK, 9746 files.check-empty-changeset: 1 declaring changeset.check-changeset-no-major: nomajorbump.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