You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] two §1c-adjacent silent wrong answers that are NOT on the retry path — cross-type in membership, and == against a Date-valued binding #7168
Out-of-scope findings measured while implementing #7098 (PR #7166). Recorded per Prime Directive #10, unassigned.
#7098 was scoped to what the ADR-0032 §1c retry rewrites once armed. Both facts below are the same shape — a silently wrong boolean, { ok: true }, no fault, no log line — but neither is on the retry path at all, so neither could be folded into that fix. Closing either one means changing the clean path, which changes answers for expressions that never faulted; that is a different and larger decision, which is why these are filed rather than fixed.
Fact 1 — a string-serialized numeric field never matches a numeric list
record.n in [1, 7] with { n: "7" } -> { ok: true, value: false }
CEL membership is total across types, so this answers rather than faulting. Because it never faults, the §1c retry never arms, and the value is false both before and after PR #7166 — measured on origin/main and on the fix branch.
This matters because it is drawn from exactly the pool §1c exists for: Field.rating → "5.0", Field.currency → "250000.00". An author writing record.status_code in [1, 2, 3] against a string-serialized numeric field gets a silent false for every row.
Note the interaction with #7098, which is why it surfaced now: before PR #7166 this expression's answer depended on whether an unrelated conjunct faulted — record.n >= 4 && record.n in [1, 7] returned true (the scope-wide hydration retyped n) while record.n in [1, 7] alone returned false. PR #7166 makes the answer consistently false in both positions. That is the correct outcome for that card — the in result was never a fault to rescue — but it leaves this fact standing on its own.
Fact 2 — == against a Date-VALUED binding is not covered by #3183's rewrite
rewriteTemporalEquality (#3183) wraps a field operand in date(...) only when its counterpart is a temporal call — today(), now(), daysFromNow(), daysAgo(). A counterpart that is a Date-valued binding is not wrapped, so the comparison stays string == Timestamp and answers a silent false.
The reachable shape is a mixed-provenance comparison, e.g. record.due == previous.due in a hook or validation scope where previous arrives from the driver as a Date and record from a JSON payload as a "YYYY-MM-DD" string. Both sides are the same logical field and the same calendar day, and the predicate answers false.
Class
Observation-class, each with a demonstrated wrong value. Frequency is unmeasured and none is claimed — the trigger is measured, the incidence is not. No production report is attached to either.
A grading seat may reasonably split these: fact 1 is a data-shape mismatch with a broad blast radius and no obvious fix that does not change clean-path answers; fact 2 is a bounded gap in an existing rewrite whose per-occurrence machinery already exists and could plausibly be extended to a Date-valued counterpart the same way it handles a temporal call.
Related
#7098 / PR #7166 (what the retry rewrites — this is the clean path instead) · #3183 (rewriteTemporalEquality) · #1530 / #1534 (why §1c exists) · #6679 / PR #7097 (what arms the retry) · ADR-0032 §1c.
Triage: finding (domain:engine-core stays — clean-path CEL evaluation in the formula/objectql family).
Classification: observation-class per the card's own grading — both facts are demonstrated silent wrong booleans that are NOT on the §1c retry path; closing either means changing clean-path answers for expressions that never faulted, which is a larger deliberate decision; frequency is unmeasured and no production report is attached. Held for the findings round.
Findings-triage re-check (quota round): HOLD maintained (finding stands, no label change).
Premise re-verified on origin/main @ 2c28df9: rewriteTemporalEquality still wraps only temporal calls (pinned at packages/formula/src/cel-engine.test.ts:528–530), so Fact 2's Date-valued-binding gap stands; and PR #7166's landing means Fact 1's cross-type in now answers a consistent silent false in all positions, exactly as this card records. Both facts remain clean-path semantics — closing either changes answers for expressions that never faulted, which is a larger semantics ruling, not a mechanical fix. Held; when a disposition pass wants to act on this, it should go to the decision box rather than straight to the queue.
Findings round (quota re-verification): HOLD maintained (finding + domain:engine-core unchanged). Re-checked @ origin/mainc546c89: the rewriteTemporalEquality pin block is live at packages/formula/src/cel-engine.test.ts:528-530 (temporal-call rewrite only), so Fact 2's Date-valued-binding gap stands; Fact 1's consistent silent false for cross-type in is unchanged since PR #7166. Both remain clean-path semantics — the exit is a semantics ruling via the decision box, not a mechanical fix. Held.
Findings triage round, 2026-08-11 (maintainer-directed). Graded: hold — both facts change CLEAN-path answers for expressions that never faulted, which is a semantics decision larger than a queue card (the body says so itself). Restart: the next ADR-0032 §1c family card, where these two measured cases enter the ruling as evidence rather than being decided in isolation.
Findings-round escalation: needs-user-decision (domain:engine-core, type Bug). Three consecutive holds have each concluded "the exit is the decision box" while parking behind an uncheckable restart ("the next §1c family card") — a fourth hold would just repeat that. Escalating with the two facts separated, because they price differently.
Premise (re-checked)
git grep -n rewriteTemporalEquality origin/main -- packages/formula/src → the temporal-equality rewrite pin block is live (cel-engine.test.ts:528-551) and wraps only temporal calls (today()/now()/…). Both silent-wrong-boolean facts stand: (1) cross-type in membership answers false instead of erroring/coercing; (2) == against a Date-valued binding (not a temporal call) misses the rewrite and answers false.
Question
Clean-path CEL evaluation semantics — these change answers for expressions that never faulted, so neither an implementer nor a PM can rule them.
Options & recommendation
Fact 2 (Date-valued binding ==): fix — extend rewriteTemporalEquality to Date-valued bindings. Bounded, uses existing machinery, closes a same-family gap the platform already decided to close for temporal calls.
Fact 1 (cross-type in): record-and-defer — any change (error vs coerce) moves clean-path answers broadly, and there is zero measured pull; defer until a measured victim decides the direction.
Platform long-term coherence: Fact 2 extension shrinks an existing special case; Fact 1 would introduce a new cross-type coercion policy — bigger than this card.
Measured business pull: zero on both; that is an argument for the asymmetric treatment above, not for silence — these are silent wrong booleans in published expression semantics.
AI-agent error-resistance: silent-false is the worst failure class for AI-authored filters; but changing eval semantics without a victim risks flipping pinned behaviour nobody can name.
Maintainer ruling recorded — asymmetric treatment adopted. Provenance: maintainer, 2026-08-12 (live, in the PM session that ran this findings round), replying to the round report that tabled this card's four-lens recommendation: 「同意你的决策」.
Binding ruling:
Fact 2 (== against a Date-valued binding): FIX — extend rewriteTemporalEquality to cover Date-valued bindings, the same-family gap the platform already closed for temporal calls.
Fact 1 (cross-type in membership): RECORD-AND-DEFER — no change to clean-path cross-type semantics without a measured victim. Reopen trigger: the first measured wrong answer attributable to cross-type in. Not filed as a separate card (zero pull); this comment is the durable record.
Dispatch constraints (engine-core seat): scope = Fact 2 only; PR uses Fixes #7168 (Fact 1's deferral is recorded here and survives closure). Extend the existing pin block (cel-engine.test.ts:528-551 today, locate by content) with the Date-valued-binding arm — and keep the negative pins: expressions that SHOULD stay false-on-mismatch stay false. needs-user-decision → pm:queue swapped with this record.
Claim: PM loop round 2
Session: session_014C8pAprWdmtecFsEprZax4
Branch: claude/issue-7168-temporal-eq-date-binding
Worktree: objectstack-issue-7168
Domain: domain:engine-core
File surface: packages/formula/src (rewriteTemporalEquality) + its pin block in cel-engine.test.ts (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus
Serial constraints cleared: packages/formula — no card has touched it this shift; in-flight #8093 (producer unlocated, expected objectql/metadata-protocol) and #8118 (objectql + plugin-webhooks) are both in different packages. ⛔ Nothing in this lane holds formula.
Model rationale — stated because I am NOT escalating, and that needs a reason
opus, ⛔ not claude-fable-5. The mandatory clause covers a change that alters contract accept/reject behaviour or widens a public face; Fact 2 does neither. It changes the answer a comparison returns, in packages/formula — not the set of expressions the platform accepts, and not packages/spec. The clause's stated rationale is that an error on the single contract surface poisons every downstream consumer; that is not this seam.
⭐ The stronger argument is the maintainer's own: this card was ruled by splitting it. Fact 1 — the genuine clean-path semantics decision — was deferred by name, and Fact 2 was called a FIX on the grounds that it closes "the same-family gap the platform already decided to close for temporal calls". The decision half was removed from this card by the ruling itself; what remains is the bounded half. ⛔ Escalating the tier reflexively here would be treating "it touches evaluation" as if it were the criterion, when the criterion is the acceptance surface.
Seat #6019. The 11:15Z maintainer ruling is carried into the dispatch verbatim and is not reopenable — including its ⛔ scope fence (Fact 2 only) and its instruction that Fact 1's deferral is recorded on this card and survives closure.
{
"issue": 7168,
"status": "done",
"branch": "claude/issue-7168-temporal-eq-date-binding",
"pr": "https://github.com/objectstack-ai/objectstack/pull/8160",
"premise_still_valid": true,
"summary": "Fact 2 only, per the 11:15Z ruling. Extended the temporal-equality rewrite so a field operand is coerced with date(...) when its counterpart is a Date-valued BINDING, not just a temporal call — record.due == previous.due with a hydrated Date and a JSON date string now answers true instead of a silent false (and != stops answering a silent true). PM mechanism assumption FALSIFIED, and it changed the implementation: a binding's runtime type is NOT visible in the AST at all, so the temporal-call arm's shape does not carry. That arm is a pure function of the source and is memoized under it; this verdict is a property of the ROW. The arm therefore reads the evaluation scope — reusing rewriteFaultedOperands' (#7098) scopePath/resolveScopePath/coercionFor primitives — runs only in evaluate() where a scope exists, and is never cached against the source (caching it would apply row 1's verdict to row 2; that trap is pinned). What IS cached per source is the analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one. The fence is held by requiring the counterpart to be a real Date AND this operand to be an ISO-8601 string that parses: date() is toDate, and new Date(\"5\") and new Date(\"05\") both parse to 2001-05-01, so an unconditional wrap would invent equalities and convert silent-wrong-false into silent-wrong-true. Fact 1 (cross-type in membership) is untouched and NOT filed as a card, per the ruling; the PR body states in its own section that the Fixes #7168 closure does not retract Fact 1's deferral.",
"tests": "REVERSE VERIFICATION (direction predicted before running: new arm red, negative pins green on BOTH sides — observed exactly that). Source reverted to origin/main via patch file, new tests kept: 'Tests 5 failed | 80 passed (85)'. The 5 failures are all and only the new arm — 'wraps the ISO-string operand when its counterpart is a Date-valued binding', 'composes with the temporal-call arm on one expression', 'a same-day mixed-provenance comparison answers true instead of a silent false', 'a datetime string matches the same INSTANT', 'the verdict is per ROW, not cached against the source' — with the card's literal defect as 'AssertionError: expected { ok: true, value: false } to deeply equal { ok: true, value: true }'. Every negative pin passed on origin/main AND with the fix. WITH FIX: pnpm --filter @objectstack/formula test -> 'Test Files 25 passed (25) / Tests 643 passed (643)'; typecheck (tsc --noEmit) clean; eslint on both changed files exit 0. DOWNSTREAM (prefix filter = consumers): @objectstack/objectql 'Test Files 191 passed / Tests 3390 passed', @objectstack/service-automation 'Test Files 79 passed / Tests 940 passed'. GATES: check:driver-conformance 'OK — 40 covered cell(s), 0 in the DEBT ledger'; check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s), none new; baseline key set verified against 37b82ed: no files added'; check:nul-bytes 'OK (scanned 7414 text file(s))' plus a manual control-byte grep over all three changed files (no hits); check:type-check-debt --re-measure 'OK — 33 ledger entr(ies) re-measured, 1966 raw tsc error(s) total, none above its recorded number' after a full closure build (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70 successful — its first run exit-1'd as the missing-module cascade the dispatch warned about). No baseline raised; --lower not run despite the gate suggesting it for 9 pre-existing surplus entries, none of them in packages/formula. Gates re-derived for the final surface with scripts/pm/dispatch-gates.mjs: same single path-matched family.",
"open_questions": [],
"out_of_scope_findings": []
}
Notes for the PM, beyond the JSON:
The dispatch's flagged design question is answered by measurement, not by analogy. The body's expectation that Fact 2's machinery "could plausibly be extended the same way it handles a temporal call" is half right: the date(...) coercion is indeed the right one (measured — the manual wrap turns the failing comparison true), but the dispatch mechanism could not be reused. A temporal call is recognisable in the AST; a Date-valued binding is not, and is indistinguishable from a string binding until a row is in hand. The scope-aware path already existed for the §1c retry ([finding] the ADR-0032 §1c hydration retry re-interprets sub-comparisons that already evaluated cleanly — hydrateOverloadStrings's docblock claims it cannot #7098) and is what this reuses.
One trap worth naming: the existing rewrite is memoized per source string, which is sound only because it is a pure function of the source. Extending it in place without separating the scope-dependent arm would have made the same expression answer with a previous row's verdict. There is a dedicated pin for that (the verdict is per ROW, not cached against the source), and it is one of the 5 that go red on origin/main.
Deliberately not "fixed": a date-ONLY string against a Date carrying wall-clock time stays false. date() parses rather than truncating to a calendar day, and those are genuinely different instants — day-truncating both sides would flip correct falses to wrong trues for real datetime comparisons. Pinned as such.
Nothing filed. No out-of-scope findings surfaced, and Fact 1 was deliberately not filed as a card, per the ruling's explicit instruction.
CI was not waited on: reporting at draft-PR time per the 2026-08-10 maintainer decision. Gate conclusions are the PM's read.
Supersedes the earlier report — the PM added the docs paragraph to scope after it was posted, and main advanced under the branch. Same status, larger file surface.
{
"issue": 7168,
"status": "done",
"branch": "claude/issue-7168-temporal-eq-date-binding",
"pr": "https://github.com/objectstack-ai/objectstack/pull/8160",
"premise_still_valid": true,
"summary": "Fact 2 only, per the 11:15Z ruling. Extended the temporal-equality rewrite so a field operand is coerced with date(...) when its counterpart is a Date-valued BINDING, not just a temporal call — record.due == previous.due with a hydrated Date and a JSON date string now answers true instead of a silent false (and != stops answering a silent true). PM mechanism assumption FALSIFIED, and it changed the implementation: a binding's runtime type is NOT visible in the AST at all, so the temporal-call arm's shape does not carry. That arm is a pure function of the source and is memoized under it; this verdict is a property of the ROW. The arm therefore reads the evaluation scope — reusing rewriteFaultedOperands' (#7098) scopePath/resolveScopePath/coercionFor primitives — runs only in evaluate() where a scope exists, and is never cached against the source (caching it would apply row 1's verdict to row 2; that trap is pinned). What IS cached per source is the analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one. The fence is held by requiring the counterpart to be a real Date AND this operand to be an ISO-8601 string that parses: date() is toDate, and new Date(\"5\") and new Date(\"05\") both parse to 2001-05-01, so an unconditional wrap would invent equalities and convert silent-wrong-false into silent-wrong-true. Per the PM's addition, the formulas.mdx callout describing this exact rewrite was extended to name the binding counterpart plus the fence — verified implicated by reading it, not taken on trust. Fact 1 (cross-type in membership) is untouched, NOT filed as a card, and NOT mentioned in the docs edit, per the ruling; the PR body states in its own section that the Fixes #7168 closure does not retract Fact 1's deferral.",
"file_surface": [
"packages/formula/src/cel-engine.ts",
"packages/formula/src/cel-engine.test.ts",
".changeset/temporal-equality-date-valued-binding.md",
"content/docs/data-modeling/formulas.mdx (domain:devx surface — PM is filing the declaration)"
],
"tests": "REVERSE VERIFICATION (direction predicted before running: new arm red, negative pins green on BOTH sides — observed exactly that). Source reverted to origin/main via patch file, new tests kept: 'Tests 5 failed | 80 passed (85)'. The 5 failures are all and only the new arm — 'wraps the ISO-string operand when its counterpart is a Date-valued binding', 'composes with the temporal-call arm on one expression', 'a same-day mixed-provenance comparison answers true instead of a silent false', 'a datetime string matches the same INSTANT', 'the verdict is per ROW, not cached against the source' — with the card's literal defect as 'AssertionError: expected { ok: true, value: false } to deeply equal { ok: true, value: true }'. Every negative pin passed on origin/main AND with the fix. WITH FIX, re-run AFTER the merge of origin/main: pnpm --filter @objectstack/formula test -> 'Test Files 25 passed (25) / Tests 643 passed (643)'; typecheck (tsc --noEmit) clean; eslint on both changed source files exit 0. DOWNSTREAM (prefix filter = consumers): @objectstack/objectql 'Test Files 191 passed / Tests 3390 passed', @objectstack/service-automation 'Test Files 79 passed / Tests 940 passed'. GATES, all re-derived for the final 4-file surface via scripts/pm/dispatch-gates.mjs (the .mdx pulled in a docs family the first derivation did not have): check:driver-conformance 'OK — 40 covered cell(s)'; check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s), none new; baseline key set verified against a0fdc56: no files added'; check:nul-bytes 'OK (scanned 7426 text file(s))' plus a manual control-byte grep over every changed file (no hits); check:type-check-debt --re-measure 'OK — 33 ledger entr(ies) re-measured in 256.5s, none above its recorded number' after a full closure build (70/70 successful); check:doc-formula-expressions 'OK — 22 record-scoped formula example(s) across 394 files / 1410 TS blocks judged clean'; check:docs-audit-scope OK incl. 'release-owned pages are in scope and read-only'; check:quick-reference-counts OK; check:role-word OK; check:objectui-changeset OK; check-changeset-no-major 'introduces no major bump'. No baseline raised; --lower not run despite the gate suggesting it for 9 pre-existing surplus entries, none in packages/formula.",
"open_questions": [],
"out_of_scope_findings": []
}
Notes for the PM, beyond the JSON:
The docs paragraph was genuinely implicated — confirmed by reading it, not assumed. Its mechanism sentence is "the engine now rewrites temporal == / != comparisons (coercing the field operand with date(...))", and the only counterpart it names is today(). That is precisely the function this PR extends. Two of its other claims were re-checked against my diff and remain true, so they were left alone: ordering is untouched, and string-literal equality is untouched — both are pinned negatives here.
One thing you should know I did NOT do:check:objectui-pin-fresh came back red. It is not mine — main advanced 7 commits mid-task and one of them bumped .objectui-sha (6314e87 to 6d77acf), which my branch predates. I did not hand-edit that pin (another lane's artifact, Release process: prevent frontend (objectui) changesets being missed when the console pin lags #3340); I merged origin/main instead, which took their landed change legitimately. Gate green afterwards. Because a merge invalidates earlier measurements, the formula suite, typecheck, ESLint and both named ratchets were all re-run against the merged tree — including a second full closure build before re-measuring the debt ledger.
The dispatch's flagged design question is answered by measurement, not by analogy. The date(...) coercion is the right one (measured — the manual wrap turns the failing comparison true), but the dispatch mechanism could not be reused: a temporal call is recognisable in the AST, a Date-valued binding is not, and is indistinguishable from a string binding until a row is in hand.
Deliberately not "fixed": a date-ONLY string against a Date carrying wall-clock time stays false. date() parses rather than truncating to a calendar day, and those are genuinely different instants — day-truncating both sides would flip correct falses to wrong trues for real datetime comparisons. Pinned as such, and stated in the changeset.
Nothing filed. No out-of-scope findings surfaced, and Fact 1 was deliberately not filed, per the ruling's explicit instruction.
CI not waited on, per the 2026-08-10 maintainer decision; gate conclusions are yours to read.
⭐ My mechanism assumption was falsified, and the falsification changed the implementation
I wrote: "I have not verified how a Date-valued binding is distinguishable from a string binding at rewrite time… if the AST cannot see it, say so and describe what you did instead."
The AST cannot see it at all. A binding's runtime type is not in the source, so the temporal-call arm's shape does not carry: that arm is a pure function of the source and is memoized under it, while this verdict is a property of the ROW. So the new arm reads the evaluation scope (reusing #7098's scopePath / resolveScopePath / coercionFor primitives), runs only inside evaluate() where a scope exists, and is never cached against the source.
⭐ And the trap that follows is pinned rather than merely avoided: caching it would apply row 1's verdict to row 2. There is a test named for exactly that. What is cached per source is the cheaper analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one.
⛔ This is the opposite of forcing the analogy the card's filer expected ("the machinery already exists and could plausibly be extended the same way"). The dispatch flagged that expectation as the filer's, not a measurement — and it turned out not to hold.
⭐ The fence is the part I would have accepted a weaker version of
I asked that expressions which should stay false stay false, and warned the over-broad failure converts silent-wrong-false into silent-wrong-true. The dev did not just assert it — it found the concrete mechanism:
date() is toDate, and new Date("5") and new Date("05") both parse to 2001-05-01, so an unconditional wrap would invent equalities.
So the arm requires the counterpart to be a real Dateand this operand to be an ISO-8601 string that parses. ⚠️ Without that second condition this card would have shipped the same defect pointing the other way — and "5" is not a contrived input for a text field being compared to a date.
The reverse verification is the shape I ask for
Direction predicted before running; observed exactly. Source reverted to origin/main, new tests kept: 5 failed / 80 passed — the five failures are all and only the new arm, and the card's literal defect appears as expected { ok: true, value: false } to deeply equal { ok: true, value: true }. Every negative pin passed on origin/main AND with the fix — which is what makes them regression pins rather than restatements.
Downstream consumers re-run after the merge: objectql 3390 passed, service-automation 940 passed.
Verified independently
check
reading
scope fence
Fact 1 untouched, not filed as a card, and not mentioned in the docs edit — exactly as the ruling directed. The PR body states in its own section that the Fixes #7168 closure ⛔ does not retract Fact 1's deferral, which is the thing a future reader would otherwise get wrong
the docs half
the formulas.mdx callout was extended to name the binding counterpart plus the fence, after reading the prose to confirm it was implicated — ⛔ not taken on my word. That is the right order: I flagged it, the dev verified it
path face
packages/formula/src/cel-engine.ts + its test + changeset + content/docs/data-modeling/formulas.mdx — ⛔ no docs/adr/**, no skills root. Clean. The .mdx is domain:devx's and the declaration is filed on #6023
gates
re-derived for the final 4-file surface — the .mdx pulled in a docs family the first derivation did not have (check:doc-formula-expressions, check:docs-audit-scope, check:quick-reference-counts, check:role-word). ⭐ Re-deriving after the surface changed is exactly the discipline; the first list would have missed four gates
ratchets
both green on a full built closure; ⛔ no baseline raised, --lower not run despite the gate suggesting it for nine pre-existing entries — none in packages/formula
⚠️Third sighting of check:objectui-pin-fresh, and this one resolved itself: main advanced 7 commits mid-task and one of them bumped .objectui-sha (6314e87 → 6d77acf). The dev merged rather than hand-editing another lane's pin artifact — ⛔ the correct call — and re-ran every measurement against the merged tree. So the bump I raised with the queue steward has since landed from elsewhere; ⛔ I am not claiming credit for a fix I did not cause, only recording that the condition changed.
Why it is not flipped
CI restarted at 19:05Z on the merge commit: 11 concluded, 0 failure; TypeScript Type Check and ESLintin_progress, filter still queued — so the Test Core and Dogfood shards have not even been scheduled yet. ⛔ Not flipping on a partially-scheduled run. Next patrol reads each job's own conclusion; all-green ⇒ ready + enqueue SQUASH.
Activity
os-zhuang commentedon Aug 10, 2026
Triage:
finding(domain:engine-corestays — clean-path CEL evaluation in the formula/objectql family).rewriteTemporalEquality(BaredateField == today()silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183) whose per-occurrence machinery already exists — it can plausibly be promoted separately from fact 1's broader data-shape question.3e8e669, measured on bothorigin/mainand the [finding] the ADR-0032 §1c hydration retry re-interprets sub-comparisons that already evaluated cleanly —hydrateOverloadStrings's docblock claims it cannot #7098 fix branch (PR fix(formula): the ADR-0032 §1c retry rewrites only the operands that faulted (#7098) #7166); consistent with the current tree.hydrateOverloadStrings's docblock claims it cannot #7098/PR fix(formula): the ADR-0032 §1c retry rewrites only the operands that faulted (#7098) #7166 (retry path — explicitly fenced apart), BaredateField == today()silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183, [finding]isNumericOverloadErroris the last author-text read left in cel-engine.ts — it arms the ADR-0032 §1c hydration retry off/no such overload/i, and our ownmatches()binding can put that phrase in a native error #6679/PR fix(formula): arm the CEL hydration retry off cel-js's structured code (#6679) #7097 all referenced; no open card owns the clean path.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Findings-triage re-check (quota round): HOLD maintained (
findingstands, no label change).Premise re-verified on
origin/main@2c28df9:rewriteTemporalEqualitystill wraps only temporal calls (pinned atpackages/formula/src/cel-engine.test.ts:528–530), so Fact 2's Date-valued-binding gap stands; and PR #7166's landing means Fact 1's cross-typeinnow answers a consistent silentfalsein all positions, exactly as this card records. Both facts remain clean-path semantics — closing either changes answers for expressions that never faulted, which is a larger semantics ruling, not a mechanical fix. Held; when a disposition pass wants to act on this, it should go to the decision box rather than straight to the queue.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 11, 2026
Findings round (quota re-verification): HOLD maintained (
finding+domain:engine-coreunchanged). Re-checked @origin/mainc546c89: therewriteTemporalEqualitypin block is live atpackages/formula/src/cel-engine.test.ts:528-530(temporal-call rewrite only), so Fact 2's Date-valued-binding gap stands; Fact 1's consistent silentfalsefor cross-typeinis unchanged since PR #7166. Both remain clean-path semantics — the exit is a semantics ruling via the decision box, not a mechanical fix. Held.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage round, 2026-08-11 (maintainer-directed). Graded: hold — both facts change CLEAN-path answers for expressions that never faulted, which is a semantics decision larger than a queue card (the body says so itself). Restart: the next ADR-0032 §1c family card, where these two measured cases enter the ruling as evidence rather than being decided in isolation.
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Findings-round escalation:
needs-user-decision(domain:engine-core, type Bug). Three consecutive holds have each concluded "the exit is the decision box" while parking behind an uncheckable restart ("the next §1c family card") — a fourth hold would just repeat that. Escalating with the two facts separated, because they price differently.Premise (re-checked)
git grep -n rewriteTemporalEquality origin/main -- packages/formula/src→ the temporal-equality rewrite pin block is live (cel-engine.test.ts:528-551) and wraps only temporal calls (today()/now()/…). Both silent-wrong-boolean facts stand: (1) cross-typeinmembership answers false instead of erroring/coercing; (2)==against a Date-valued binding (not a temporal call) misses the rewrite and answers false.Question
Clean-path CEL evaluation semantics — these change answers for expressions that never faulted, so neither an implementer nor a PM can rule them.
Options & recommendation
Fact 2 (Date-valued binding
==): fix — extendrewriteTemporalEqualityto Date-valued bindings. Bounded, uses existing machinery, closes a same-family gap the platform already decided to close for temporal calls.Fact 1 (cross-type
in): record-and-defer — any change (error vs coerce) moves clean-path answers broadly, and there is zero measured pull; defer until a measured victim decides the direction.Platform long-term coherence: Fact 2 extension shrinks an existing special case; Fact 1 would introduce a new cross-type coercion policy — bigger than this card.
Measured business pull: zero on both; that is an argument for the asymmetric treatment above, not for silence — these are silent wrong booleans in published expression semantics.
AI-agent error-resistance: silent-false is the worst failure class for AI-authored filters; but changing eval semantics without a victim risks flipping pinned behaviour nobody can name.
Startup scope discipline: Fact 2 bounded (one rewrite arm + pins); Fact 1 unbounded — defer.
本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Maintainer ruling recorded — asymmetric treatment adopted. Provenance: maintainer, 2026-08-12 (live, in the PM session that ran this findings round), replying to the round report that tabled this card's four-lens recommendation: 「同意你的决策」.
Binding ruling:
==against a Date-valued binding): FIX — extendrewriteTemporalEqualityto cover Date-valued bindings, the same-family gap the platform already closed for temporal calls.inmembership): RECORD-AND-DEFER — no change to clean-path cross-type semantics without a measured victim. Reopen trigger: the first measured wrong answer attributable to cross-typein. Not filed as a separate card (zero pull); this comment is the durable record.Dispatch constraints (engine-core seat): scope = Fact 2 only; PR uses
Fixes #7168(Fact 1's deferral is recorded here and survives closure). Extend the existing pin block (cel-engine.test.ts:528-551today, locate by content) with the Date-valued-binding arm — and keep the negative pins: expressions that SHOULD stay false-on-mismatch stay false.needs-user-decision→pm:queueswapped with this record.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
Claim: PM loop round 2
Session:
session_014C8pAprWdmtecFsEprZax4Branch:
claude/issue-7168-temporal-eq-date-bindingWorktree:
objectstack-issue-7168Domain:
domain:engine-coreFile surface:
packages/formula/src(rewriteTemporalEquality) + its pin block incel-engine.test.ts(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusSerial constraints cleared:
packages/formula— no card has touched it this shift; in-flight #8093 (producer unlocated, expectedobjectql/metadata-protocol) and #8118 (objectql+plugin-webhooks) are both in different packages. ⛔ Nothing in this lane holdsformula.Model rationale — stated because I am NOT escalating, and that needs a reason
opus, ⛔ notclaude-fable-5. The mandatory clause covers a change that alters contract accept/reject behaviour or widens a public face; Fact 2 does neither. It changes the answer a comparison returns, inpackages/formula— not the set of expressions the platform accepts, and notpackages/spec. The clause's stated rationale is that an error on the single contract surface poisons every downstream consumer; that is not this seam.⭐ The stronger argument is the maintainer's own: this card was ruled by splitting it. Fact 1 — the genuine clean-path semantics decision — was deferred by name, and Fact 2 was called a FIX on the grounds that it closes "the same-family gap the platform already decided to close for temporal calls". The decision half was removed from this card by the ruling itself; what remains is the bounded half. ⛔ Escalating the tier reflexively here would be treating "it touches evaluation" as if it were the criterion, when the criterion is the acceptance surface.
Seat #6019. The 11:15Z maintainer ruling is carried into the dispatch verbatim and is not reopenable — including its ⛔ scope fence (Fact 2 only) and its instruction that Fact 1's deferral is recorded on this card and survives closure.
Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
{ "issue": 7168, "status": "done", "branch": "claude/issue-7168-temporal-eq-date-binding", "pr": "https://github.com/objectstack-ai/objectstack/pull/8160", "premise_still_valid": true, "summary": "Fact 2 only, per the 11:15Z ruling. Extended the temporal-equality rewrite so a field operand is coerced with date(...) when its counterpart is a Date-valued BINDING, not just a temporal call — record.due == previous.due with a hydrated Date and a JSON date string now answers true instead of a silent false (and != stops answering a silent true). PM mechanism assumption FALSIFIED, and it changed the implementation: a binding's runtime type is NOT visible in the AST at all, so the temporal-call arm's shape does not carry. That arm is a pure function of the source and is memoized under it; this verdict is a property of the ROW. The arm therefore reads the evaluation scope — reusing rewriteFaultedOperands' (#7098) scopePath/resolveScopePath/coercionFor primitives — runs only in evaluate() where a scope exists, and is never cached against the source (caching it would apply row 1's verdict to row 2; that trap is pinned). What IS cached per source is the analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one. The fence is held by requiring the counterpart to be a real Date AND this operand to be an ISO-8601 string that parses: date() is toDate, and new Date(\"5\") and new Date(\"05\") both parse to 2001-05-01, so an unconditional wrap would invent equalities and convert silent-wrong-false into silent-wrong-true. Fact 1 (cross-type in membership) is untouched and NOT filed as a card, per the ruling; the PR body states in its own section that the Fixes #7168 closure does not retract Fact 1's deferral.", "tests": "REVERSE VERIFICATION (direction predicted before running: new arm red, negative pins green on BOTH sides — observed exactly that). Source reverted to origin/main via patch file, new tests kept: 'Tests 5 failed | 80 passed (85)'. The 5 failures are all and only the new arm — 'wraps the ISO-string operand when its counterpart is a Date-valued binding', 'composes with the temporal-call arm on one expression', 'a same-day mixed-provenance comparison answers true instead of a silent false', 'a datetime string matches the same INSTANT', 'the verdict is per ROW, not cached against the source' — with the card's literal defect as 'AssertionError: expected { ok: true, value: false } to deeply equal { ok: true, value: true }'. Every negative pin passed on origin/main AND with the fix. WITH FIX: pnpm --filter @objectstack/formula test -> 'Test Files 25 passed (25) / Tests 643 passed (643)'; typecheck (tsc --noEmit) clean; eslint on both changed files exit 0. DOWNSTREAM (prefix filter = consumers): @objectstack/objectql 'Test Files 191 passed / Tests 3390 passed', @objectstack/service-automation 'Test Files 79 passed / Tests 940 passed'. GATES: check:driver-conformance 'OK — 40 covered cell(s), 0 in the DEBT ledger'; check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s), none new; baseline key set verified against 37b82ed: no files added'; check:nul-bytes 'OK (scanned 7414 text file(s))' plus a manual control-byte grep over all three changed files (no hits); check:type-check-debt --re-measure 'OK — 33 ledger entr(ies) re-measured, 1966 raw tsc error(s) total, none above its recorded number' after a full closure build (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70 successful — its first run exit-1'd as the missing-module cascade the dispatch warned about). No baseline raised; --lower not run despite the gate suggesting it for 9 pre-existing surplus entries, none of them in packages/formula. Gates re-derived for the final surface with scripts/pm/dispatch-gates.mjs: same single path-matched family.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, beyond the JSON:
date(...)coercion is indeed the right one (measured — the manual wrap turns the failing comparisontrue), but the dispatch mechanism could not be reused. A temporal call is recognisable in the AST; a Date-valued binding is not, and is indistinguishable from a string binding until a row is in hand. The scope-aware path already existed for the §1c retry ([finding] the ADR-0032 §1c hydration retry re-interprets sub-comparisons that already evaluated cleanly —hydrateOverloadStrings's docblock claims it cannot #7098) and is what this reuses.the verdict is per ROW, not cached against the source), and it is one of the 5 that go red onorigin/main.Datecarrying wall-clock time staysfalse.date()parses rather than truncating to a calendar day, and those are genuinely different instants — day-truncating both sides would flip correctfalses to wrongtrues for real datetime comparisons. Pinned as such.Generated by Claude Code
docs(formulas): name the Date-valued-binding counterpart in the tempo…
os-zhuang commentedon Aug 12, 2026
Supersedes the earlier report — the PM added the docs paragraph to scope after it was posted, and
mainadvanced under the branch. Same status, larger file surface.{ "issue": 7168, "status": "done", "branch": "claude/issue-7168-temporal-eq-date-binding", "pr": "https://github.com/objectstack-ai/objectstack/pull/8160", "premise_still_valid": true, "summary": "Fact 2 only, per the 11:15Z ruling. Extended the temporal-equality rewrite so a field operand is coerced with date(...) when its counterpart is a Date-valued BINDING, not just a temporal call — record.due == previous.due with a hydrated Date and a JSON date string now answers true instead of a silent false (and != stops answering a silent true). PM mechanism assumption FALSIFIED, and it changed the implementation: a binding's runtime type is NOT visible in the AST at all, so the temporal-call arm's shape does not carry. That arm is a pure function of the source and is memoized under it; this verdict is a property of the ROW. The arm therefore reads the evaluation scope — reusing rewriteFaultedOperands' (#7098) scopePath/resolveScopePath/coercionFor primitives — runs only in evaluate() where a scope exists, and is never cached against the source (caching it would apply row 1's verdict to row 2; that trap is pinned). What IS cached per source is the analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one. The fence is held by requiring the counterpart to be a real Date AND this operand to be an ISO-8601 string that parses: date() is toDate, and new Date(\"5\") and new Date(\"05\") both parse to 2001-05-01, so an unconditional wrap would invent equalities and convert silent-wrong-false into silent-wrong-true. Per the PM's addition, the formulas.mdx callout describing this exact rewrite was extended to name the binding counterpart plus the fence — verified implicated by reading it, not taken on trust. Fact 1 (cross-type in membership) is untouched, NOT filed as a card, and NOT mentioned in the docs edit, per the ruling; the PR body states in its own section that the Fixes #7168 closure does not retract Fact 1's deferral.", "file_surface": [ "packages/formula/src/cel-engine.ts", "packages/formula/src/cel-engine.test.ts", ".changeset/temporal-equality-date-valued-binding.md", "content/docs/data-modeling/formulas.mdx (domain:devx surface — PM is filing the declaration)" ], "tests": "REVERSE VERIFICATION (direction predicted before running: new arm red, negative pins green on BOTH sides — observed exactly that). Source reverted to origin/main via patch file, new tests kept: 'Tests 5 failed | 80 passed (85)'. The 5 failures are all and only the new arm — 'wraps the ISO-string operand when its counterpart is a Date-valued binding', 'composes with the temporal-call arm on one expression', 'a same-day mixed-provenance comparison answers true instead of a silent false', 'a datetime string matches the same INSTANT', 'the verdict is per ROW, not cached against the source' — with the card's literal defect as 'AssertionError: expected { ok: true, value: false } to deeply equal { ok: true, value: true }'. Every negative pin passed on origin/main AND with the fix. WITH FIX, re-run AFTER the merge of origin/main: pnpm --filter @objectstack/formula test -> 'Test Files 25 passed (25) / Tests 643 passed (643)'; typecheck (tsc --noEmit) clean; eslint on both changed source files exit 0. DOWNSTREAM (prefix filter = consumers): @objectstack/objectql 'Test Files 191 passed / Tests 3390 passed', @objectstack/service-automation 'Test Files 79 passed / Tests 940 passed'. GATES, all re-derived for the final 4-file surface via scripts/pm/dispatch-gates.mjs (the .mdx pulled in a docs family the first derivation did not have): check:driver-conformance 'OK — 40 covered cell(s)'; check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s), none new; baseline key set verified against a0fdc56: no files added'; check:nul-bytes 'OK (scanned 7426 text file(s))' plus a manual control-byte grep over every changed file (no hits); check:type-check-debt --re-measure 'OK — 33 ledger entr(ies) re-measured in 256.5s, none above its recorded number' after a full closure build (70/70 successful); check:doc-formula-expressions 'OK — 22 record-scoped formula example(s) across 394 files / 1410 TS blocks judged clean'; check:docs-audit-scope OK incl. 'release-owned pages are in scope and read-only'; check:quick-reference-counts OK; check:role-word OK; check:objectui-changeset OK; check-changeset-no-major 'introduces no major bump'. No baseline raised; --lower not run despite the gate suggesting it for 9 pre-existing surplus entries, none in packages/formula.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, beyond the JSON:
==/!=comparisons (coercing the field operand withdate(...))", and the only counterpart it names istoday(). That is precisely the function this PR extends. Two of its other claims were re-checked against my diff and remain true, so they were left alone: ordering is untouched, and string-literal equality is untouched — both are pinned negatives here.check:objectui-pin-freshcame back red. It is not mine —mainadvanced 7 commits mid-task and one of them bumped.objectui-sha(6314e87to6d77acf), which my branch predates. I did not hand-edit that pin (another lane's artifact, Release process: prevent frontend (objectui) changesets being missed when the console pin lags #3340); I mergedorigin/maininstead, which took their landed change legitimately. Gate green afterwards. Because a merge invalidates earlier measurements, the formula suite, typecheck, ESLint and both named ratchets were all re-run against the merged tree — including a second full closure build before re-measuring the debt ledger.date(...)coercion is the right one (measured — the manual wrap turns the failing comparisontrue), but the dispatch mechanism could not be reused: a temporal call is recognisable in the AST, a Date-valued binding is not, and is indistinguishable from a string binding until a row is in hand.Datecarrying wall-clock time staysfalse.date()parses rather than truncating to a calendar day, and those are genuinely different instants — day-truncating both sides would flip correctfalses to wrongtrues for real datetime comparisons. Pinned as such, and stated in the changeset.Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
ACCEPTED on substance — PR #8160. ⛔ Ready-flip held: CI restarted after the merge and has not converged.
Engine-core seat #6019,
session_014C8pAprWdmtecFsEprZax4.⭐ My mechanism assumption was falsified, and the falsification changed the implementation
I wrote: "I have not verified how a Date-valued binding is distinguishable from a string binding at rewrite time… if the AST cannot see it, say so and describe what you did instead."
The AST cannot see it at all. A binding's runtime type is not in the source, so the temporal-call arm's shape does not carry: that arm is a pure function of the source and is memoized under it, while this verdict is a property of the ROW. So the new arm reads the evaluation scope (reusing #7098's
scopePath/resolveScopePath/coercionForprimitives), runs only insideevaluate()where a scope exists, and is never cached against the source.⭐ And the trap that follows is pinned rather than merely avoided: caching it would apply row 1's verdict to row 2. There is a test named for exactly that. What is cached per source is the cheaper analysis of which occurrences could depend on a scope at all, so the hot path is unchanged for sources without one.
⛔ This is the opposite of forcing the analogy the card's filer expected ("the machinery already exists and could plausibly be extended the same way"). The dispatch flagged that expectation as the filer's, not a measurement — and it turned out not to hold.
⭐ The fence is the part I would have accepted a weaker version of
I asked that expressions which should stay
falsestayfalse, and warned the over-broad failure converts silent-wrong-falseinto silent-wrong-true. The dev did not just assert it — it found the concrete mechanism:So the arm requires the counterpart to be a real⚠️ Without that second condition this card would have shipped the same defect pointing the other way — and "5" is not a contrived input for a text field being compared to a date.
Dateand this operand to be an ISO-8601 string that parses.The reverse verification is the shape I ask for
Direction predicted before running; observed exactly. Source reverted to
origin/main, new tests kept: 5 failed / 80 passed — the five failures are all and only the new arm, and the card's literal defect appears asexpected { ok: true, value: false } to deeply equal { ok: true, value: true }. Every negative pin passed onorigin/mainAND with the fix — which is what makes them regression pins rather than restatements.Downstream consumers re-run after the merge:
objectql3390 passed,service-automation940 passed.Verified independently
Fixes #7168closure ⛔ does not retract Fact 1's deferral, which is the thing a future reader would otherwise get wrongformulas.mdxcallout was extended to name the binding counterpart plus the fence, after reading the prose to confirm it was implicated — ⛔ not taken on my word. That is the right order: I flagged it, the dev verified itpackages/formula/src/cel-engine.ts+ its test + changeset +content/docs/data-modeling/formulas.mdx— ⛔ nodocs/adr/**, no skills root. Clean. The.mdxisdomain:devx's and the declaration is filed on #6023.mdxpulled in a docs family the first derivation did not have (check:doc-formula-expressions,check:docs-audit-scope,check:quick-reference-counts,check:role-word). ⭐ Re-deriving after the surface changed is exactly the discipline; the first list would have missed four gates--lowernot run despite the gate suggesting it for nine pre-existing entries — none inpackages/formulacheck:objectui-pin-fresh, and this one resolved itself:mainadvanced 7 commits mid-task and one of them bumped.objectui-sha(6314e87→6d77acf). The dev merged rather than hand-editing another lane's pin artifact — ⛔ the correct call — and re-ran every measurement against the merged tree. So the bump I raised with the queue steward has since landed from elsewhere; ⛔ I am not claiming credit for a fix I did not cause, only recording that the condition changed.Why it is not flipped
CI restarted at 19:05Z on the merge commit: 11 concluded, 0 failure;
TypeScript Type CheckandESLintin_progress,filterstillqueued— so theTest CoreandDogfoodshards have not even been scheduled yet. ⛔ Not flipping on a partially-scheduled run. Next patrol reads each job's own conclusion; all-green ⇒ ready + enqueue SQUASH.Generated by Claude Code
fix(formula): cover a Date-valued binding in the temporal-equality re…