Repository navigation
[spec] parseFilterAST lowers ['a', '=', { $field: 'b' }] to a bare field spec no backend reads as equality #7597
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 11, 2026 Triage:
needs-user-decisionappended;domain:spec(filer-applied) confirmed correct.- Classification: the card's own conclusion — lowering an equality triple with a
FieldReferenceSchemacomparand to{ field: { $eq: ref } }"is a semantics change at the authoring face, so it wants a ruling rather than a drive-by fix" — and the alternative (reading bare{ $field }as implicit equality) reverses the evaluator posture JS 求值面全体拒收$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520 deliberately kept. Both branches move the authoring-face semantics ofpackages/spec, sodomain:specper standing instruction ③'s reverse red line. - Premise verified on
origin/main@744b8f5:parseFilterASTlives atpackages/spec/src/data/filter.zod.ts:1205(single lowering sink per [engine] driver-sql 编译 spec 未声明的「数组 where 方言」—— 与 Turso remote 的拒收分叉,需一次定调(接纳进 spec 或响亮弃用) #5158 holds — no second definition found). - Dedup: distinct from same-batch [spec]
FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 (declared-but-unresolved list positions) and [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 (analytics faces) — this one is the lowering sink itself. No existing card names the bare-$fieldequality lowering; no open PR touches it. - Release read: silent-no-match is real but sits on the unreleased
$fieldcapability whose SQL half ([spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222) is still in flight and now refuses this shape loudly with an actionable message; not judged RC-blocking on its own — the ruling can ride the [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 follow-up window.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Classification: the card's own conclusion — lowering an equality triple with a
Maintainer ruling recorded 2026-08-11 (PM session, executing the maintainer's direct instruction in chat — verbatim: 「接受你的全部建议,请更新 issue 的状态和标签」, accepting the four-lens decision-inbox review in full).
Ruling: fix the lowering.
parseFilterASTlowers an equality triple whose comparand is aFieldReferenceSchemato{ field: { $eq: ref } }— the spelling already implemented on both evaluation paths. ⛔ Do NOT change the memory evaluator's unknown-operator posture; #6520's decision stands. Single-sink change per #5158.Rationale (four-lens): silent zero-row matching is the worst failure shape; the fix is one sink, no new capability surface, and removes a trap AI-authored filters walk straight into.
State:
needs-user-decision→pm:queue(domain:specseat).
Generated by Claude Code
Round-1 selection note (spec lane, session
session_01JY2Q5Xto1u8YHADgrZDTnk): deferred to the NEXT round by the batch cap — round 1 took #7495/#7496/#7513 (fan-out-ordered). Not shelved; this card rides first in round 2, ahead of #7596 (same-file serialization: both editpackages/spec/src/data/filter.zod.ts, and this ruled silent-zero-rows fix outranks the removal).Facts recorded for the dispatching round (all re-verified 09:18Z on
origin/main):- Post-fix(spec): stop folding like/ilike onto $contains at the wire (#7536) #7593 the file grew:
parseFilterASTis now at:1415— the triage comment's:1205is stale. - docs(spec):
fieldsstops prescribing a dotted path no driver resolves (#7601) #7617 (docs prose, in flight, sibling sub-lane) declares a different file face; re-verify at dispatch time. - The dispatch prompt must ask the dev whether the lowering fix changes [spec]
FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596's removal scope (re-pricing rule — both cards share the filter contract face).
Generated by Claude Code
- Post-fix(spec): stop folding like/ilike onto $contains at the wire (#7536) #7593 the file grew:
Claim: PM loop round 1 backfill (spec lane — slot freed by #7495's voiding; maintainer instructed 加速派发)
Session:session_01JY2Q5Xto1u8YHADgrZDTnk
Branch:claude/issue-7597-equality-field-ref-lowering
Worktree: cloud dev session (own container)
Domain:domain:spec
File surface:packages/spec/src/data/filter.zod.ts(parseFilterAST, ~:1415) + its tests + the cross-field conformance corpus (cross-field-conformance-cases.tsregion, additive). ⛔ Not the$between/$in/$ninunions — that region is #7596's, serialized behind this card. Stop on breach; explain in the report.
Container & model: M (ruled authoring-face semantics change with cross-path conformance obligations),mode:cloud, model: opus
Serial constraints cleared: #7596 (same file) is explicitly queued BEHIND this card (hot-file serial queue, seat post #6017; defer note on #7596 09:27Z). No in-flight PR touchesfilter.zod.ts(checked 09:20Z; #7593 merged this morning — anchors re-verified post-merge). #7617 (in flight, sibling sub-lane) declares a docs-prose face.
Generated by Claude Code
OS-DEV-REPORT — #7597
os-dev-report
Draft PR: #7672 · Branch:
claude/issue-7597-equality-field-ref-lowering· Base:origin/main@cc3555ePremise verified, then fixed. Measured on
origin/mainbefore the first edit: all four$eqspellings (=,==,equals,eq) lowered['amount', '<op>', { $field: 'budget' }]to the bare{"amount":{"$field":"budget"}}and the in-memory evaluator returned[], while>/gtlowered to{"$gt":ref}and returned["1"]. After the fix all four lower to{"amount":{"$eq":{"$field":"budget"}}}and return["3","6"]— the conformance corpus's declared$eqrow set.parseFilterASTis atfilter.zod.ts:1415as the dispatch said (the triage comment's:1205was stale); confirmed single sink — no second definition.Full spelling set measured, not assumed.
AST_OPERATOR_MAPfolds exactly those four onto$eq, andconvertComparison's implicit-equality branch intercepts exactly those four, so the two lists coincide. A new test sweeps the whole exportedVALID_AST_OPERATORSvocabulary and fails if any spelling lowers a reference comparand to a bare field spec.Compile-surface enumeration (full evidence in the PR body): face 1
driver-sqlalready conformant and now exercised by the new authoring arm; face 2 turso RemoteTransport already conformant (loudINVALID_FILTERbefore and after); face 3read-scope-sqlnot reachable (read scopes are policy-compiled, never through this sink) and owned by #7598; face 5formulais the reference implementation; half-face objectqlhavingout of reach (havingis aFilterConditionobject, never AST-lowered);driver-memory/driver-mongodbout of scope per the #5499 freeze.⚠️ One measured consequence the PM should weigh — face 4,service-analyticsfilter-normalizer. The equality triple moves there from a loudINVALID_FILTER/400("Unsupported filter operator$field") into the same SILENT bucket the ordering spellings have occupied since #3948:assertCompilableComparandgates only the LIKE family and$in/$ninmembers, so a scalar operator's object comparand falls through totoSqlBindValueand is bound as JSON text. Not a new defect class — one more spelling entering an existing one — and the same shift lands on the frozendriver-memory. Left unfixed deliberately, on the precedent this exact surface set: #7604's triage records that #5222's dev seat declined to widen analytics as a side effect and the PM called that right; refusing only$eqthere would create a fresh asymmetry inside the face, and refusing all six is #7598's open question 1. Measurement posted to #7598.Re-pricing #7596: untouched. The list positions are unchanged by this PR (
in/nin/betweenlowerings pass their comparand through as before), and the two diff hunks start at:1350/:1376, so #7596's unions at:319-320and:819-820are byte-identical and unshifted. It gets marginally cleaner to argue — after this fix the remaining declared-but-unexecutable$fieldpositions are exactly the ones #7596 removes — but neither easier nor harder to do.Region boundary respected: the
$between/$in/$ninunions were not touched.{ "issue": 7597, "status": "done", "branch": "claude/issue-7597-equality-field-ref-lowering", "pr": "https://github.com/objectstack-ai/objectstack/pull/7672", "premise_still_valid": true, "summary": "parseFilterAST's implicit-equality shorthand now branches on the comparand: an equality triple whose comparand is a FieldReferenceSchema lowers to { field: { $eq: ref } } instead of the bare { field: { $field } } that no backend reads as an equality, so ['amount','=',ref] finally behaves like ['amount','>',ref] on both evaluation paths. All four $eq spellings are covered and a vocabulary sweep test keeps a fifth from re-opening it; literal comparands, non-string $field carriers, and the evaluator's #6520 unknown-operator posture are untouched — a hand-authored bare field spec keeps today's fate on every backend. The cross-field conformance corpus gains an AUTHORING arm (CROSS_FIELD_AUTHORED_CASES) that enters through the sink and holds the lowered filter to the same both-paths-same-rows rule, run by driver-sql per dialect cell and by driver-sqlite-wasm. One consequence to weigh: at service-analytics' filter-normalizer (and the frozen driver-memory) the equality spelling shifts from a loud refusal into the silent JSON-bind bucket the ordering spellings already sit in — measured, reported, and left to #7598 rather than widening analytics as a side effect.", "tests": "pnpm --filter @objectstack/spec test => 377 files / 9880 tests passed (includes the new src/data/filter-field-reference-lowering.test.ts). pnpm --filter @objectstack/driver-sql test => 86 passed | 4 skipped (90) files, 1399 passed | 50 skipped tests; the 15 new authoring-arm cases confirmed running by name under -t '#7597'. pnpm --filter @objectstack/driver-sqlite-wasm test => 24 files / 374 tests passed. pnpm --filter @objectstack/formula test => 25 files / 631 tests passed. typecheck (spec, driver-sql, driver-sqlite-wasm) clean. Gates: check:driver-conformance OK (37 covered cells, 3 DEBT, 0 exempt); check:merge-driver, check:adr-anchors, check:spec-parsed-alias OK; check:authorable-surface OK (1276 defaults unchanged, no anchor diff); scripts/check-nul-bytes.mjs OK (7069 files). REVERSE VERIFICATION, direction predicted first: removing the fix via a patch file must turn the AUTHORING arm red and leave the hand-authored bare-form refusal green. Measured exactly that -- 14 authoring cases plus 'the equality TRIPLE no longer lowers to that bare spelling' failed with \"expected { amount: { '$field': 'budget' } } to deeply equal { amount: { '$eq': ... } }\", while the '>' control and the bare-form refusal pin stayed green; restored with git apply. CI status at report time: in_progress (draft PR just opened; PM owns convergence).", "open_questions": [ { "question": "service-analytics' filter-normalizer (face 4) and the frozen driver-memory now answer the equality-with-$field shape SILENTLY (JSON-bound comparand, zero rows) where they previously refused loudly. Fix inside #7598's scope, or does the PM want a narrow refusal landed sooner?", "options": [ "A - leave it to #7598 (queued, domain:services, owns both analytics emitters; its open question 1 is implement-vs-refuse for exactly these faces)", "B - land a narrow $field refusal at the analytics scalar-comparand door in a follow-up card, ahead of #7598's larger decision, so the face stays loud for all six operators" ], "recommendation": "A. Refusing only $eq at that door would create a fresh asymmetry inside the analytics face -- the exact defect class this card closes -- and refusing all six pre-empts #7598's ruling on whether these emitters implement $field instead. The shift is not a new defect class: the ordering spellings have been silent there since #3948, so this is one more spelling joining an existing, already-filed hole. The measurement has been posted to #7598 so its executor inherits it rather than re-deriving it." } ], "out_of_scope_findings": [ "not filed (existing card): the analytics silent-bind residue above belongs to #7598 (#7604 was closed as its duplicate) - measurement posted there as a comment rather than a twin issue", "FieldReferenceSchema's execution-support docblock (packages/spec/src/data/filter.zod.ts, ~:36-64) still says SQL push-down refuses a field reference and that compiling it 'is tracked as its own capability in #5222' - stale since #7582 landed that compilation. Doc-only, adjacent to but not part of this card's change; left untouched, flagged here for the PM to route (a docs-accuracy pass on the same file would also catch it)." ] }
Generated by Claude Code
Step-7 review — ACCEPT (spec-lane PM, session
session_01JY2Q5Xto1u8YHADgrZDTnk). PR: #7672 (draft,Fixes #7597).Verified against GitHub, not the report's claims:
- The ruling lands exactly: one branch in the single sink (
convertComparison) — aFieldReferenceSchemacomparand in the equality shorthand lowers to{ $eq: ref }; literals keep implicit equality (pinned); a non-string$fieldis not a reference (predicate mirrorsdriver-sql'sfieldReferenceOf— good cross-layer consistency); JS 求值面全体拒收$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520's evaluator posture untouched and pinned on the hand-authored bare form. - The spelling set was measured, not assumed: all four
$eqspellings fixed, case folding covered, and a vocabulary sweep overVALID_AST_OPERATORSpins that NO spelling can lower a reference comparand to a bare field spec — a fifth spelling entering the map cannot silently re-open this. - Compile-surface enumeration is complete — every face has a conclusion with evidence, including the half-face (
havingnever passes the sink — stated with the metadata-protocol quote) and the frozen faces (measured anyway, for the record). - The one honest trade, reported not buried: face 4 (
service-analyticslowerAnalyticsWhere) shifts loud→silent for the equality spelling — it joins the same silent bucket the ORDERING spellings have always occupied there. Measured before/after, precedent cited ([spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222's decline-to-widen, PM-endorsed), and the measurement posted to [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 whose card owns the analytics$fieldposture wholesale. This is the right scoping call; flagged in the round report so the maintainer sees the trade. - Re-pricing [spec]
FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 answered: untouched — list positions pass through unchanged, the:319-320/:819-820unions are byte-identical and unshifted, and the removal boundary it draws gets cleaner. [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 dispatches after this merges, per the hot-file queue. - Reverse verification in the predicted direction (patch-file restore): 14 authoring cases + the triple pin red with the fix removed; the
>control and the re-authored bare-form refusal stayed green. - Gates: spec 377/9880, driver-sql 1399 passed, sqlite-wasm 374, formula 631, typecheck clean across the three,
check:driver-conformance37 cells OK,check:authorable-surfaceno anchor diff, nul-bytes clean. Changeset spec minor + driver-sql minor, justified with the Filter AST:likeis folded to$containsat the wire — wildcards bind as literals and driver-sql'slike/ilikearm is unreachable #7536 precedent. - No generated-tree files in the diff — this PR does NOT join the os-regen relay; it can flip independently once its gate jobs conclude.
Landing plan: flip to ready + auto-merge on gate-job conclusions (flip check armed); the merge queue serializes it against #7655/#7657 mechanically.
Generated by Claude Code
- The ruling lands exactly: one branch in the single sink (
- added a commit that references this issue
on Oct 7, 2026
Measured while implementing #5222 (cross-field push-down). Filing unassigned — recording, not claiming.
The fact
parseFilterASTlowers an authored triple differently depending on the operator spelling. Measured on currentmain:[['amount', '>', { $field: 'budget' }]]{"amount":{"$gt":{"$field":"budget"}}}[['amount', 'gt', { $field: 'budget' }]]{"amount":{"$gt":{"$field":"budget"}}}[['amount', '=', { $field: 'budget' }]]{"amount":{"$field":"budget"}}← bare[['amount', 'equals', { $field: 'budget' }]]{"amount":{"$field":"budget"}}← bareThe equality spellings drop the operator, because a literal comparand's implicit-equality form is
{ field: value }— correct for a literal, but for a field reference it produces a field spec whose only key is$field.Nothing reads that shape as an equality. In
matches-filter.ts,evalFieldsees an all-$key set and dispatches$fieldtoevalOp, which has no arm for it and returns its fail-closedfalse. So the filter silently matches no record. On the SQL side the same shape arrives as an OPERATOR named$field.Why it matters
Two authored spellings of one intent, two fates:
['amount', '>', ref]works (post-#5222 on SQL, and always in memory),['amount', '=', ref]silently matches nothing. The failure is invisible on the path that produces it.#5222 narrowed the SQL side to give this shape an actionable refusal naming
$eqrather than a generic operator list — but that is a driver-side mitigation of a lowering defect, and it deliberately did NOT compile the shape, because compiling it would make SQL answer rows for a filter the memory evaluator answersfalsefor (a new divergence in the change that closed one).The decision this needs
Most likely:
parseFilterASTshould lower an equality triple whose comparand is aFieldReferenceSchemato{ field: { $eq: ref } }rather than the bare form — the spelling that IS implemented on both paths. That is a one-sink change (parseFilterASTis the single lowering sink per #5158), but it is a semantics change at the authoring face, so it wants a ruling rather than a drive-by fix.Alternatively,
matches-filter.tscould read a bare{ $field }field spec as an implicit equality — but that changes the evaluator's unknown-operator posture, which #6520 examined and deliberately KEPT.Refs
$field编译为列对列比较(cross-field comparison push-down) #5222 (the driver-side refusal + its wording)FilterArrayis input-only sugar;parseFilterASTis the single sink)$icontains(driver-memory 两面 / driver-mongodb / objectqlhaving/ formula)—— SQL 族已实现,同一 filter 在内存 double 上抛错 #6520 (the evaluator's unknown-operator posture, kept deliberately)