Skip to content

fix(driver-memory,driver-mongodb): refuse a non-boolean $exists comparand with INVALID_FILTER / 400, as $null's is refused (#20897) - #20979

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20897-exists-non-boolean-refused
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20897-exists-non-boolean-refused

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20897
Clause-②: no (narrowing)

A non-boolean $exists comparand ("yes", 1, "false", 0, null, undefined, an object) is now refused with INVALID_FILTER / 400 on driver-memory and driver-mongodb, which were the two faces still answering it. The words are driver-sql's nonBooleanExistsComparandError, and the refusal takes the same place and form as each driver's existing $null refusal. true and false answer exactly as before.

Why the declaration is no (narrowing): FieldOperatorsSchema declares $exists: z.boolean(), so this pulls two faces back to the declared contract. It reuses the existing INVALID_FILTER code and driver-sql's existing wording, and it adds no key, code or accepted shape. It does narrow what driver-memory and driver-mongodb accept: a filter they used to answer is now refused. The changeset declares it BREAKING, minor for both packages under the launch-window convention for accept-set narrowings, with the ADR-0087 disposition not-required (already-registered filter-query-face-comparands-refused-at-save). That registered entry's reason already states that every query face refuses a non-boolean flag, and its replacement is this change's migration.

What was wrong

Measured on origin/main f6ccca4a with an object px holding row a (name: "won") and row b (name: null):

face "yes" 1 "false" 0 / null true false
engine.find over driver-memory [b] [b] [b] [b] [a] [b]
driver-memory analytics (cube) face, sum over the rows a a a b a b
driver-mongodb translateFilter $eq: null $eq: null $eq: null $eq: null $ne: null $eq: null
engine.find over driver-sql (better-sqlite3) refused refused refused refused [a] [b]

driver-memory's query path and driver-mongodb's emitter both asked value === true and sent every other value to the no-value side, so $exists: "yes" returned the rows with NO value. That is the author's intent inverted. driver-memory's own cube face read the same flag by truthiness (Boolean(raw[0])) and answered the opposite rows for "yes", 1 and "false". So one package gave two answers to one filter.

The landing: per-face gates, not an engine door

The PM's H1 held. $null's refusal lives per face, and no engine-level door judges a flag. The spec's shared comparand faces (assertListComparandShapes, normalizeFilterComparandTypes) deliberately leave $null / $exists / $empty to the faces, as filter.zod.ts records. Each driver already runs one validating walk ahead of its lowering, and $null is refused there. This PR puts $exists beside it:

  • driver-memory filter-refusal.ts: assertFilterConditionShape refuses a non-boolean $exists through a new nonBooleanExistsComparandError, next to the $null / $empty checks. Every memory entry runs this gate before it lowers anything, so the one edit covers find, findOne, count, aggregate, updateMany, deleteMany and the analytics face (query() and generateSql()). All of these were measured. memory-driver.ts is not touched: its $exists arm now sees only booleans.
  • driver-mongodb mongodb-filter.ts: reduceFilterKey (the walk) refuses it next to the $null gate, and the emitter's $exists arm keeps a local check with the same constructor, as the $null arm does.

I did not add an engine-level door. The faces that still accept a non-boolean $exists after this PR are the engine's own in-process evaluators: objectql's aggregation filter and having, plus @objectstack/formula. Their evaluability doors (assertHavingIsEvaluable, assertAggregationFilterIsEvaluable, and assertConditionIsEvaluable, which already refuses a non-boolean $empty) live in having-filter.ts. That file and formula's matches-filter.ts are fenced for this card. A door anywhere else would be a second copy of the rule that still does not reach them. They are measured and named below for routing.

Every compile face

# face conclusion
1 driver-sql applyFilterCondition (and driver-sqlite-wasm, Turso local, which inherit it) Already compliant. engine.find on better-sqlite3 and driver-sqlite-wasm find refuse "yes", 1, "false", 0 and null with INVALID_FILTER / 400, and true / false give [a] / [b]. Pinned in sql-driver-out-of-contract-filter-input.test.ts (the [#5369] block: "yes", 1, 0, null, undefined, {}, "false", plus a true / false control). Not edited (#20822 group 2 in flight).
2 Turso RemoteTransport buildWhereSQL Already compliant. remote-transport-null-comparand-refusal.test.ts (block d) and turso-local-remote-null-parity.test.ts ("both transports REFUSE a non-boolean $exists"): 90 tests green at this head. Not edited.
3 service-analytics read-scope-sql compileScopedFilterToSql Already compliant. assertBooleanFlagComparands refuses $null / $exists / $empty, fail-closed. read-scope-boolean-flag-comparand.test.ts is green.
4 service-analytics filter-normalizer lowerAnalyticsWhere Already compliant. assertBooleanNullFlags refuses a non-boolean $null / $exists before any lowering. where-boolean-flag-refusal.test.ts is green (the two suites, 104 tests).
5 formula matchesFilterCondition Out of scope (#20869 is in flight on this file). Measured: "yes", 1, "false", 0 and null all match row b, because v === true ? actual != null : actual == null. Its $null arm is unrefused the same way. Named in the report for the PM to route.
half objectql having-filter (applyHaving, matchesHaving, matchesAggregationFilter) Out of scope (serial behind #20822 F8, then #20873). Measured through engine.aggregate on memory AND SQLite: both the aggregation filter and having read $exists as !!target. "yes", 1 and "false" select the valued rows and groups, and 0 / null select the no-value ones. The engine evaluates these in-process, so no driver refuses them. For context, $null: "yes" on these two faces drops the constraint (every row or group comes back). Named in the report.
unfrozen driver-memory (query path and cube face) Changed: refused on every entry (above).
unfrozen driver-mongodb translateFieldOperators Changed: refused on the walk, with the emitter check kept.

Pins

  • memory-null-comparand-refusal.test.ts: the case "$exists is deliberately NOT tightened here" pinned the inverted answer ['2'] for $exists: "yes". It now pins the refusal (code INVALID_FILTER, status 400, driver-sql's first sentence, the position) for "yes", 1 and "false", through the live path and the gate alike, with true / false as the control. The case was flipped, not deleted.
  • memory-exists-non-boolean-refusal.test.ts (new) is the multi-face invariant: every entry of the package (the eight listed above) refuses the seven non-booleans. For true / false, every read entry, the cube face included, answers exactly find()'s rows. The same row set as find(), or INVALID_FILTER, and never a third answer. On the cube face, undefined and a plain object are refused first by that face's comparand-type check, which is its documented precedence. It is the same envelope and the same position, with that face's own sentence. The file also pins the refusal at every depth, including behind a TRUE identity ($or: [{}, …]), and checks that a refused updateMany / deleteMany leaves the store untouched.
  • mongodb-exists-non-boolean-refusal.test.ts (new): the seven non-booleans refused on the translator, with the position inside combinators and behind a boolean identity that would otherwise settle the node before the emitter runs. true / false still translate to $ne: null / $eq: null and equal their $null mirror.
  • SQLite: the existing driver-sql pins above already assert "yes", 1 and "false" refused with a true / false control. driver-sql is fenced, so they were cited, not duplicated.

Pin sweep. A repo-wide grep for a non-boolean $exists literal and for the $exists refusal's words outside the faces that already refuse found one pin that asserted the old answer, the memory case above. The CHANGELOG entries that say "$exists is deliberately NOT tightened" are released text and are not edited.

Verification (driver changes at da4feaf8; final head 378effc8)

  • pnpm --filter @objectstack/driver-memory test: 68 files, 1500 tests passed.
  • pnpm --filter @objectstack/driver-mongodb test: 30 files passed, 5 skipped, 671 passed and 172 skipped. The skipped files need a real mongod, which is opt-in (OS_TEST_MONGODB_MEMORY_SERVER_ENABLED). The new pins do not need it: they run on the translator.
  • typecheck for both packages exit 0. The new memory test is in tsc --noEmit's program (--listFiles), and the mongodb one is in tsconfig.test.json's (check:test-typecheck).
  • Reverse verification, run from the committed fix through scripts/ablation-replace.mjs, which asserts each mutation on disk (anchor count 1 to 0, blob changed) and proves each restore (blob equals HEAD, git diff HEAD empty). Both packages' tests import src/ directly, so no dist/ was involved.
    • Memory gate removed: the two memory files went red, 55 failed / 24 passed. The 24 that stayed green are the 18 $null cases of the older file, the two true / false controls and the four cube-face cells the type face refuses first.
    • MongoDB walk gate removed, emitter check kept: red, 1 failed / 9 passed. Only the identity-settled case failed, as predicted: the emitter still refuses the flat shapes, and only the walk reaches a node an identity settles.
    • MongoDB walk gate and emitter check both removed: red, 9 failed / 1 passed. Only the true / false control stayed green.
    • The first attempt at the second leg was an empty operation. Its replacement text contained the anchor, so the tool refused the mutation and nothing was measured. The anchor was changed and the leg re-run.
  • Driver conformance ledger (node scripts/check-driver-conformance.mjs): 50 covered, 0 DEBT, 0 exempt before and after.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands after the last commit (378effc8) derived 79 families. --ran reconciles them as 79 accounted for: 77 run and exit 0, and 2 NOT MEASURED. Those two are check:dual-build-cjs-loads, which reads every package's dist/, and check:type-check-debt, whose re-measure needs the whole build closure. Both exit 3 (PREREQUISITE NOT MET) on this partial build and are declared to CI. check:skill-examples first exited 3 for want of the client-react build, and ran green once that closure was built.
  • ESLint, narrowed to the 7 changed code files (the 6 .ts files and scripts/cross-package-test-inputs.mjs) with --no-inline-config: 7 files, 0 errors, 0 warnings (counted from --format json). Each file resolves a config (--print-config). eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on an untouched file.

Outside the drivers: the envelope caller census

The new memory suite calls analytics.query( twice on its own MemoryAnalyticsService. @objectstack/client's envelope-caller-census.test.ts walks the whole repo for that call shape, so CI went red at da4feaf8 (Test Core (5/6)). The census prescribes classifying every counted site, and its receiver split already has the class for a producer call. So LEDGER gains one NOT_SDK row (count 2, receiver service), and its two exact-count controls move with it: the service-receiver control now expects 3 sites and pins their files, and the verdict split reads 3 not-SDK. Nothing is exempted or loosened. The census as it stood at da4feaf8 goes red locally on the same two cases CI named. At 378effc8 it is 20/20 green, and the client package's tests (641) and typecheck pass.

The census reads that driver-memory file, so pnpm check:cross-package-test-inputs requires it declared. scripts/cross-package-test-inputs.mjs names the one file in @objectstack/client's globs, and turbo.json mirrors it into @objectstack/client#test inputs, so a change to that suite re-runs the census. It is per-file, not packages/**, for the price the census header records. check:ci-filter-parity passes, because core already covers the path.

Acceptance notes


Generated by Claude Code

…rand as $null's is refused

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…boolean $exists refusal

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 2 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f8178ffeceba053667353f86ec193f2152ad557c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e99b1d782f4040516528da55a5c9d4d321be4f5c — the merge of head 94daed863bd7ef3069c572f65ab3bc333ed1e35c into base f8178ffeceba053667353f86ec193f2152ad557c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e99b1d782f4040516528da55a5c9d4d321be4f5c && git checkout e99b1d782f4040516528da55a5c9d4d321be4f5c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f8178ffeceba053667353f86ec193f2152ad557c 94daed863bd7ef3069c572f65ab3bc333ed1e35c && git checkout -B drift-repro f8178ffeceba053667353f86ec193f2152ad557c && git merge --no-ff 94daed863bd7ef3069c572f65ab3bc333ed1e35c

node scripts/docs-audit/affected-docs.mjs --json f8178ffeceba053667353f86ec193f2152ad557c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

claude added 2 commits October 1, 2026 00:05
…ites in the envelope caller census

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
…t-set narrowing (minor, breaking)

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
… census ledgers as a client test input

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 378effc8caf39153ed30e3cab93f6c7317d451f7
Local-runs: none

Inputs: card #20897 (body and all seven comments: triage 5916839636, claim 5921382209, reports 5921891523 and 5922406027, claim amendment 5922434933, correction 5922450072, id fix 5922467874); PR #20979 body, its nine-file list and the net diff against merge base f6ccca4a4 (484 insertions, 20 deletions, six commits); the 41 check-runs on this head. Registry entries, driver sources and gate scripts were read at the merge base only, to judge the claims against them. Nothing built, run or re-run.

① Derived judgments

  1. driver-memory accept set narrows at the one shape gate — right. assertFieldConstraintShape (filter-refusal.ts) refuses a non-boolean $exists between the existing $null and $empty checks. Reach verified on the base: assertFilterConditionShape is called once in memory-driver.ts (line 1379, the FilterCondition branch every entry lowers through) and once in memory-analytics.ts (normalizeFilters, line 1751), after normalizeFilterComparandTypes and before flattenFilterCondition. So the gate fires ahead of both readers of the flag that exist in non-test source: the live path's val === true arm (memory-driver.ts 1797) and the cube face's set lowering by truthiness (memory-analytics.ts 112 / 360). The type face running first on the cube face, so undefined and a plain object get its sentence there, is the base's call order, as the new suite pins.
  2. Wording and envelope — right. The leading sentence is driver-sql's nonBooleanExistsComparandError (sql-driver.ts 4411) verbatim; the tail re-aims the "this driver" clause exactly as the $null twin in the same file does; the constructor is unsupportedFilterError, the same INVALID_FILTER / 400 envelope. One condition, one wording holds.
  3. No new public export — right, and the changeset's "No export or published type changes" holds. nonBooleanExistsComparandError is module-exported from filter-refusal.ts, but driver-memory/src/index.ts does not re-export that module and the package exports map is . only. In driver-mongodb the constructor is module-private; the one published function whose accept set narrows is translateFilter (index.ts 7), which the changeset declares.
  4. driver-mongodb gate on the walk plus the emitter arm — right. classifyFilterKey refuses with the hasOwnProperty / typeof shape the $null gate uses one paragraph up; the emitter's $exists arm keeps a local totality check with the same constructor and path spelling as the $null arm at base line 1288. The only $exists reader in the package's non-test source is that arm, so the walk covers every entry.
  5. Pin flip, not deletion — right. The memory-null-comparand-refusal.test.ts carve-out case now pins the refusal (code, status, driver-sql's first sentence, position, live path equals gate) for 'yes', 1, 'false' with a true / false control. The triage's pin set (memory and SQLite: 'yes', 1, 'false' refused; true / false unchanged) is met: the SQLite half is driver-sql's existing [#5369] block at the base, which pins 'yes', 1, 0, null, undefined, {} and the string 'false' with the same control — cited, not duplicated, because driver-sql is fenced for #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula, having); the memory reference matcher retires (D6) #20822 group 2.
  6. Census ledger row and controls — right. The new memory suite holds exactly two analytics.query( call sites (lines 286 and 352; comments are masked; generateSql is not in METHODS), with no client. prefix, so receiver service, count 2, verdict NOT_SDK is the census's own class for a producer call. The service-receiver control moves 1 to 3 with every file pinned, the split moves 1 to 3, and the order assertion is deterministic because the census sorts sites by file then line. SKIP_DIRS is untouched; nothing is exempted. Test Core (5/6) success on this head is the gate verdict.
  7. Gate wiring (scripts/cross-package-test-inputs.mjs, turbo.json) — the minimal shape, nothing loosened; the CI verdict on it is outstanding. The census's ACCEPTED_WALK_RADII entry records the bare recursive packages glob as refused on cost; the edit names one file, and turbo.json mirrors it into @objectstack/client#test inputs, which Layer B of check-cross-package-test-inputs requires once the ledger names the file. Adding an input only widens that task's cache-invalidation radius. The gate runs in Lint & Repo Gates, which is in_progress on this head: the dev's local exit 0 is a claim, not a verdict (see ③).
  8. Landing site, per face rather than an engine door — right under the claim's own terms. Claim 5921382209 allowed packages/objectql only if the per-face gates could not give one answer on every face. Every driver face now agrees (driver-sql, sqlite-wasm and both Turso transports already refused; memory and mongodb now do). The faces that still accept are objectql's aggregation filter / having evaluators and formula's matchesFilterCondition, both fenced by the same claim, measured and named; the changeset's "Unchanged" paragraph discloses the aggregation residual to consumers.

No derived judgment in the PR body or the changeset was found wrong.

② Semver level

Clause-②: no (narrowing)

  • The arm is right. No published schema, authorable key, export or type moves: FieldOperatorsSchema already declares $exists: z.boolean(), packages/spec/src is untouched, both drivers' exports maps are unchanged. What moves is the runtime accept set of two published driver packages, and it narrows. That is the (narrowing) arm, BREAKING by AGENTS.md §3; yes would be false and no (widening) malformed. The PR body (line 2), the changeset and claim amendment 5922434933 carry the same line.
  • minor for both packages is right. The launch-window guard refuses major; the 2026-09-04 ruling in pr-automation says breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level. The lane precedent is 4b4ee88f (PR fix(objectql)!: a no-operator object beneath a relation, structured-JSON or undeclared id column is refused INVALID_FILTER / 400 on every driver (#20745) #20781, fix(objectql)!: minor, Clause-②: no (narrowing)). The changeset carries the !, the banner, the FROM/TO ("write the boolean itself"), who is affected, and what is unchanged. Check Changeset (check-changeset-no-major plus check-adr-0087-registration) is success on this head, in both runs.
  • The ADR-0087 marker, not-required (already-registered filter-query-face-comparands-refused-at-save), judged against the entry at the merge base (packages/spec/src/migrations/entries/semantic/18.filter-query-face-comparands-refused-at-save.ts) — right. The entry pre-dates the base (the gate's own check). Its surface names "a $null or $exists flag that is not a boolean"; its reason already states "every query face refuses a non-boolean $null / $exists flag, because the backends read one in opposite directions"; its replacement is this changeset's whole migration ("A flag is the boolean itself ... and $exists is the inverse"). This PR makes that reason true on the two drivers it overstated at registration. The ADR's D7 table warns that the gate admits already-registered on resolution alone and never on coverage, so coverage was checked by hand: covered. The other categories are closed on facts: registered would add a second entry whose prescription objectstack migrate meta has nothing to rewrite with (no stored row moves; a stored non-boolean flag is already refused at the save door by this very entry); no-migration-prescription, the category 4b4ee88f used, is refused by the gate against a body that carries "What to write instead", so that precedent does not transfer; unpublished is false; runtime-interface-only and type-surface-only need a path-and-symbol with no metadata surface, and FilterCondition is one. One discoverability caveat, not a defect: the ledger projects this prescription under the spec's major-18 guide, not under the drivers; the drivers' own CHANGELOGs ship the changeset's prescription, so the consumer channel is covered.

③ Boundary flags

Implemented-by: claude/issue-20897-exists-non-boolean-refused
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 01:02
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 01:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 1, 2026
Conflicts resolved so both census declarations survive: the rest
nested-relation pin's NOT_SDK row and this branch's driver-memory row,
the service-receiver control at 8 sites over three files, the NOT_SDK
split at 8 (recomputed from the merged ledger), and both per-file
client test inputs in scripts/cross-package-test-inputs.mjs and turbo.json.

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m and removed size/l labels Oct 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 94daed863bd7ef3069c572f65ab3bc333ed1e35c
Local-runs: none

Delta review. The at-tier record for the previous head 378effc8caf39153ed30e3cab93f6c7317d451f7 is PR comment 5922574141 (PASS). This head is the merge commit 94daed863 with parents 378effc8c (the branch, parent 1) and f8178ffec (origin/main, parent 2): a true merge, no rebase, no force-push, 19 main commits brought in (f6ccca4a4..f8178ffec, 123 files, among them 8d329f02e PR #20916 and f8178ffec PR #20984). Inputs: card #20897 body and all nine comments (triage 5916839636, claim 5921382209, reports 5921891523 and 5922406027, amendment 5922434933, correction 5922450072, id fix 5922467874, ACCEPT 5922589958, merge-round report 5923016917); PR #20979 body, its nine-file list, the net diff against the new base (git diff f8178ffec 94daed863: 9 files, 480 insertions, 19 deletions) and the merge's total (git diff 378effc8c 94daed863); the check-runs on this head, read once at 2026-10-01T01:47Z. Blob shas and head-tree line numbers below were read with git show / git diff on the fetched refs; nothing built, run or re-run. Prior conclusions are adopted only where this section says the merge left their bytes untouched.

① Derived judgments

  1. The narrowing itself is byte-identical to the reviewed head — prior ① 1, 2, 4, 5 and 8 adopted. The six non-conflicted files have the same blob at both heads: .changeset/20897-exists-non-boolean-refused.md b590d27, driver-memory/src/filter-refusal.ts c1be71d, memory-exists-non-boolean-refusal.test.ts 48c369c, memory-null-comparand-refusal.test.ts e6dd7ab, driver-mongodb/src/mongodb-exists-non-boolean-refusal.test.ts f7f4426, mongodb-filter.ts 277bbb4. Main did not touch driver-mongodb in the window, so the walk-plus-emitter gate and its pins are unmoved.
  2. The memory gate still runs ahead of every $exists reader after fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984 — right, re-verified on the merged tree. f8178ffec changed memory-driver.ts (+185) and memory-analytics.ts (+100) for $contains membership only: filterContainsTest, isJsonStoredField, containsMemberCandidates, an object parameter on normalizeFieldOperators, and the cube face's containment / members builders. It added no $exists reader and no entry that skips the gate. At this head convertToMongoQuery (memory-driver.ts 1416) calls assertFilterConditionShape(filters, 'filter') at 1444 before normalizeFilterCondition at 1446, and all six query entries route through it (719, 953, 1025, 1070, 1317, 1380); the cube face's normalizeFilters (memory-analytics.ts 1815) gates at 1825 before flattenFilterCondition at 1826, and both query() (1146) and generateSql() (1590) call it. The flag's only readers in non-test source remain the live arm (memory-driver.ts 1885) and the cube face's set lowering (memory-analytics.ts 112 / 361), both behind the gate. The merge-round report's claim on this holds against the tree.
  3. Spec contract and public surface untouched by the merge — right. FieldOperatorsSchema still declares $exists: z.boolean() at filter.zod.ts 1593 and 1673; main's edit to that file is the $contains membership docblock. driver-memory/src/index.ts does not re-export filter-refusal.js and the package exports map is . only (fix(driver-memory): $contains on a multi-valued or JSON-stored field is membership, on every face #20984's new MemoryContainsTest type is not re-exported either, and is not this PR's). The changeset's "No export or published type changes" still holds.
  4. The three conflicted files differ from origin/main only by this branch's additions — right, the conflict-resolution claims judged against the diff. git diff f8178ffec 94daed863 on envelope-caller-census.test.ts, scripts/cross-package-test-inputs.mjs and turbo.json shows: one LEDGER row (driver-memory suite, analytics.query, receiver service, count 2, NOT_SDK) after main's rest row (count 5); the §2 service-receiver control moved 6 to 8 in main's form (sorted unique file set) holding the three files, plus the branch's assertion that every service site's method is analytics.query; the §3 split moved 6 to 8 with the title re-counted; one per-file glob with its comment after main's rest glob; one turbo.json input after main's rest input, comma added. Nothing of main's was dropped and nothing was loosened (SKIP_DIRS untouched, no exemption).
  5. The recomputed counts are right. Recounted from the head blob of the census file, not from the report: 20 ledger rows; PAYLOAD_DEPENDENT 18, RESULT_INSENSITIVE 10, NOT_SDK 8; receiver: 'service' rows sum to 8 (1 + 5 + 2) across exactly the three files the control lists; total 36. The form change in §2 (the branch's per-site 3-entry list became main's 3-file set) loses no pin: §3 "the mechanical enumeration and the hand ledger agree, site for site" compares file|method|receiver keys against ledger counts, so the per-file counts 5 and 2 stay asserted there.
  6. "Nothing beyond the union but three comment re-wordings" — right. The every(... 'analytics.query') assertion the resolution kept is present at 378effc8c (one occurrence) and absent on main, so it is the branch's, not a merge addition. The re-wordings are the §2 comment ("Two" to "Three producer faces", receivers and files re-stated) and the scripts/ comment ("ONE file under packages/" to "Declared by name for the same reason"; "the entry above" to "the scripts/** entry"), each inside its conflict hunk and each true at the merged tree.
  7. Gate wiring stays minimal — right, with the CI verdict outstanding. The glob and the turbo.json input add one declared input each to @objectstack/client#test; adding an input only widens that task's cache-invalidation radius. check:cross-package-test-inputs and check:turbo-task-graph run in Lint & Repo Gates, in_progress at the read (③); the dev's local exit 0 is a claim.
  8. Landing site per face, and the residual set, unmoved by the merge — prior ① 8 adopted with one fact added. packages/objectql/src/having-filter.ts was not touched on main. packages/formula/src/matches-filter.ts was, by 05be35259 (refusal text: the cross-class field-comparison refusal (972 characters) is cut at the 500-character client bound before its remedy sentence, so no caller of /data or security/explain reads the fix #20869), but only the cross-field class error text; its $exists identity read (v === true ? actual != null : actual == null, line 747 at head) is unchanged, so the fenced residue is the same set the prior record named.
  9. Merge commit trailers — right. 94daed863 carries Claude-Session plus Co-authored-by: Claude, the same model-free pair as the six branch commits; its message states the resolution (both census declarations, control at 8, split at 8, both inputs), which the diff confirms.

No derived judgment in the PR body, the changeset or the merge-round report was found wrong at this head.

② Semver level

Clause-②: no (narrowing)

  • Prior ② adopted: the changeset blob is identical (b590d27), and nothing the merge brought in moves its basis. Still minor for both drivers with the !, the BREAKING banner, FROM/TO, who is affected and what is unchanged; still the (narrowing) arm, since no schema, key, export or type moves and the runtime accept set of two published packages narrows. The PR body line 2 and the amendment 5922434933 carry the same line.
  • The ADR-0087 marker still resolves at the new base — re-checked. packages/spec/src/migrations/entries/semantic/18.filter-query-face-comparands-refused-at-save.ts exists at 94daed863 with id: 'filter-query-face-comparands-refused-at-save', its surface naming "a $null or $exists flag that is not a boolean" and its reason stating every query face refuses one. Main's registry edits in the window (c6b3a01d5) touched form-view-subform-columns-closed and inline-grid-column-currency-scale-refused, not this entry. The sibling .changeset/20874-memory-contains-membership.md main added for the same package does not interact with this changeset's level.
  • Check Changeset is success on this head: the gate's own verdict on no-major and ADR-0087 registration.

③ Boundary flags

Implemented-by: claude/issue-20897-exists-non-boolean-refused
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit a3dc817 Oct 1, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants