Skip to content

fix(formula): has() over an indexed or call receiver evaluates as the CEL spec defines it - #25275

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-zu4f7d
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-zu4f7d

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25267

Clause-②: yes (widening)
Metadata-protocol: added

What was wrong

The CEL spec defines has(e.f) for any expression e: a presence test on field f of whatever e evaluates to. cel-js 8.0.0 (the latest release) implements has() as a macro whose type-check walks the selection chain back to its root and refuses the argument with has() invalid argument unless that root is an identifier. So has(data.environments[0].storage.usedMb), has(record.items[0].name) or has(dyn(x).y) faulted at every type-check and every evaluation.

Measured on origin/main 17f3c65 before the change, through @objectstack/formula's public entries:

door has(data.environments[0].storage.usedMb) has(record.a.b) (control)
celEngine.evaluate ok: false, has() invalid argument ok: true
celEngine.compile refused accepted
validateExpression('predicate', …) (object / flow / action / sharing-rule / hook slots) refused accepted
validateVisibilityPredicates (page and view visibleWhen) 0 findings 0 findings

The page and view gate checks syntax, unknown functions and bare identifiers, never types (the #6253 / #13594 rulings), so on that surface the predicate published clean and then failed open in the console. The fault is cel-js's own (no ObjectStack wrapping is involved), and Environment.registerFunction('has(ast): bool', …) cannot replace the built-in macro: cel-js refuses the overlapping signature.

The fix: the engine evaluates it, as the spec says

The route is the "evaluate" branch of the card. has() over an indexed or call receiver is valid CEL, and the engine can honour it without forking cel-js.

rewriteIndexedHas (in cel-engine.ts, next to the #3306 rewriteNullableTernary, built the same way: a parse, an AST walk, a reserialize only when something changed, a bounded memo) lowers such a call to e.?f.hasValue(). That is the spec's definition of optional field selection (e.?f is optional.of(e.f) when has(e.f), else optional.none()), so the two are the same test. cel-js evaluates the receiver exactly as its has() does: an absent key before the last step still faults No such key, and only the last step answers false.

  • It fires only on a has() whose argument is a . selection rooted in something other than an identifier. has(record.a.b) is left to cel-js. So is every argument the spec itself refuses: a bare name (has(status)) and an index as the last step (has(record.items[0])) still fault has() invalid argument everywhere.
  • It is applied at every place cel-js type-checks: evaluate, compile, firstUndeclaredReference, inferCelType and the overload-fault probe. The parse-only front end (parseCelToAst) keeps the authored has(…) call. It already parsed, and AST consumers such as the lint's has(…) span mask keep finding the call they key on.
  • No lint change is needed. Every rule that judges a CEL predicate reaches the type verdict through @objectstack/formula (validateExpression, celEngine.compile), or does not judge types at all (the page and view rules). They now agree with the runtime.

Tests

New packages/formula/src/cel-has-indexed-receiver.test.ts, 9 tests:

  • the filed reading (present answers true, absent answers false), and agreement with the 'usedMb' in … form the filing worked around with;
  • parity with cel-js's own identifier-rooted has() over ten value kinds (map with or without the key, key holding null / false, empty map, null, list, int, string, bool) and two path depths, with faults compared by kind and first line;
  • every door gives one verdict: compile, validateExpression, evaluate, inferCelType (bool), and firstUndeclaredReference, which now reports a bare status behind an indexed has() instead of being masked by the macro's fault;
  • controls: has(status) and has(record.items[0]) are still refused by compile, validateExpression and evaluate;
  • the rewrite is narrow (five untouched spellings), idempotent, and lowers a has() nested inside a lowered receiver.

Ablation at baf8e22, with an early return source; injected into rewriteIndexedHas through scripts/ablation-replace.mjs: the suite went 8 failed / 1 passed (the controls test is the one that stays green, as it should). The restore was verified as blob == HEAD (f4f10ad31d5c) and git diff HEAD empty. No dist/ is involved: the suite imports ./cel-engine from source.

Runs at head baf8e22 (under os-verify-lock.sh):

  • pnpm --filter @objectstack/formula build && … test && … typecheck: exit 0. Test Files 52 passed (52), Tests 1376 passed (1376).
  • pnpm --filter @objectstack/lint test: exit 0. Test Files 153 passed (153), Tests 7076 passed (7076).

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands at baf8e22. All 67 were run, with exit codes captured before any pipe, and --ran reconciles them: 66 run, 1 NOT-MEASURED, 0 unrun. 66 exited 0. Two of those first exited 3 on missing prerequisites and were 0 on a rerun: check-plugin-teardown-shape --self-test after fetching its shallow-clone fixture commit, and check:lean-entry-closure after building @objectstack/objectql. The NOT-MEASURED one is check:dual-build-cjs-loads (exit 3, needs every package's dist), which is left to CI.

Dogfood: n/a. The title carries no ! and the Clause-② arm is (widening).

Acceptance notes

  • Why Clause-②: yes (widening) / added. The claim expected no. Measured from the diff, validateExpression used to refuse this argument on object, flow, action, sharing-rule and hook predicates, and now accepts it. That is a wider publish accept set, and a newly accepted value for an author. The changeset is minor on @objectstack/formula accordingly.
  • A docblock this makes stale, outside the claim's file surface and left alone. packages/spec/src/automation/flow-value-slot-template.ts (guardOf) and the test title at flow-value-slot-template.test.ts:201 say has() refuses an index anywhere in its argument at run time. That reason is now false. The behaviour it explains (no guard is printed for a path with a numeric segment) is still right, because a path ending in an index cannot be guarded by has(), and an out-of-range index faults instead of answering false.
  • The sibling class stays open. has(status) and has(record.items[0]) on a page or view visibleWhen still publish with 0 findings (measured: validateVisibilityPredicates against a page component, with record.x === 1 as the firing control), and still fault at runtime. Closing that means adopting one more check() verdict on the page and view rule, which the lint: validate-visibility-predicates passes unknown CEL functions clean (the validateExpression premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594 ruling ("只拒未知函数裁定,⛔ 不搬运 check() 的其他抱怨") reserves for the maintainer. It is not done here.
  • Error excerpts. A runtime fault inside a lowered call (an absent intermediate key) quotes the lowered source in cel-js's excerpt, the same property the Shipped template formula fields silently evaluate to null on @objectstack 15.1.1 — daysBetween / Timestamp−Timestamp / floor in stored formulas (hr tenure_years, time_off days) #3306 rewrite already has.
  • Removal. When cel-js lifts the identifier-root restriction from its has() macro, rewriteIndexedHas and its five call sites can go, together with its own describe block; the evaluation and every-door pins keep holding.

Generated by Claude Code

… CEL spec defines it

cel-js 8.0.0's has() macro refuses any selection chain not rooted in an
identifier, so has(data.environments[0].storage.usedMb) faulted
"has() invalid argument" at every check and every evaluation, while a page
visibleWhen (whose publish gate only parses) published it and failed open.

The engine lowers such a call to the spec's equivalent e.?f.hasValue()
wherever cel-js type-checks: evaluate, compile, firstUndeclaredReference,
inferCelType and the overload-fault probe. Arguments the spec refuses
(has(status), has(a[0])) are untouched and still refused.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 8dfd1265f006ea0a6f727fa966e980ede57b4214 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2d1cc99836aa52fd190e48f140d7b9fda9f2944 — the merge of head baf8e225450a844e7c7e0401c5efea54b2f9d7ad into base 8dfd1265f006ea0a6f727fa966e980ede57b4214, 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 f2d1cc99836aa52fd190e48f140d7b9fda9f2944 && git checkout f2d1cc99836aa52fd190e48f140d7b9fda9f2944
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8dfd1265f006ea0a6f727fa966e980ede57b4214 baf8e225450a844e7c7e0401c5efea54b2f9d7ad && git checkout -B drift-repro 8dfd1265f006ea0a6f727fa966e980ede57b4214 && git merge --no-ff baf8e225450a844e7c7e0401c5efea54b2f9d7ad

node scripts/docs-audit/affected-docs.mjs --json 8dfd1265f006ea0a6f727fa966e980ede57b4214

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: baf8e225450a844e7c7e0401c5efea54b2f9d7ad
Local-runs: none

Read-only, at 2026-10-22T19:42Z: card #25267 (body and both comments, the claim 6348364574 and the os-dev-report 6349466484), PR #25275 (body, file list, net diff against main, its one comment), the check-runs on the head, and these files at the head: packages/formula/src/cel-engine.ts (whole: buildEnv / CEL_ENV_OPTIONS, buildScopedEnv, buildTypedEnv, runProgram, firstUndeclaredReference, collectCelRootIdentifiers, parseCelToAst / parseCelToAstWithReason, inferCelType, firstTypeMismatch, rewriteNullableTernary, rewriteIndexedHas / indexedHasSelection / HAS_CALL_RE, celEngine.compile / evaluate), packages/formula/src/cel-has-indexed-receiver.test.ts, .changeset/25267-cel-has-indexed-receiver.md, packages/formula/src/validate.ts (validateExpression, inferExpressionType), packages/formula/src/unknown-function.ts, packages/formula/src/index.ts and package.json (exports), packages/lint/src/validate-visibility-predicates.ts (hasCallSpans / maskHasCalls / firstUnboundReference, the #13594 function-existence call), packages/lint/src/validate-expressions.ts (the predicate-slot validateExpression calls), packages/lint/src/flow-variable-scope.ts and flow-cel-root-scope.ts (the firstUndeclaredReference callers), packages/services/service-automation/src/engine.ts (registerFlow's predicate pass), packages/plugins/plugin-sharing/src/share-link-service.ts (eligibility evaluate), packages/spec/src/automation/flow-value-slot-template.ts (guardOf) and its test at line 201, packages/spec/src/shared/expression.zod.ts (the dialect table). Rules read: AGENTS.md (Post-Task Checklist step 3, Prime Directives 10 / 12 / 14), scripts/pm/clause2-line.mjs (ruling A on #25105), scripts/check-changeset-no-major.mjs header, scripts/pm/record-recognisers.mjs (REVIEW_FACES, servedTierStands, the template), docs/releases-maintenance.md (release-note sections). cel-js 8.0.0's own source is not installed in this container, so its macro and optional-type semantics are taken from the parity pins in the new test and the check-runs, not from a reading of the library.

Served-tier note. The brief asked for CONTRACT_REVIEW_TIER on this line. reviewFace on the PR's three paths returns the declaration face (no schema path, no governed path, no (narrowing) arm), and record-recognisers.mjs is exact by face in both directions (servedTierStands; --template --face declaration prints TIER_DEFAULT), so the token the face maps to is the one written here. The session that rendered it runs at CONTRACT_REVIEW_TIER; that is a fact about the reviewer, not the line. The declaration face carries no Scope: key line by the template's own rule, so the files read are listed in prose above.

① Derived judgments

Check-runs on the head at 2026-10-22T19:42Z: Lint & Repo Gates, Build Core, Governed Surface Queue Guard, Check Changeset, Check PR Size, every Lint · and Type Check · lane except Type Check · workspace, and the four card/claim checks are success; Test Core (1/6 … 6/6) and Type Check · workspace are still in_progress — reported as pending, not waited for. The full-battery lanes are skipped on a PR run, as designed. No check-run is red.

  1. rewriteIndexedHas — presence semantics at every entry point: right. indexedHasSelection fires only on a call node named has with exactly one argument whose node is a . select and whose ./.? chain bottoms out in a node that is not an id (an [] index, a call, a list/map literal). The lowering replaces the call with rcall hasValue on a .? select over the same (receiver, field) pair — the receiver is kept as an ordinary select chain, so an absent key before the last step still evaluates receiver.key and faults No such key (fault, not false), and only the final .?f answers false. That is the CEL spec's definition of has(e.f) over optional selection. It is applied at the five sites where cel-js's checker or evaluator sees the source: compile, evaluate, firstUndeclaredReference, inferCelType, firstTypeMismatch. Every environment those sites use carries enableOptionalTypes: true (CEL_ENV_OPTIONS, buildScopedEnv, both arms of buildTypedEnv), so the lowered .? / hasValue() form parses and checks in each. The parse-only front end (parseCelToAstWithReason / parseCelToAst, collectCelRootIdentifiers, the pushdown lowering) is deliberately untouched, so the lint's AST consumers (hasCallSpans, null-guard reading, root collection) still see the authored has(…) call — verified against validate-visibility-predicates.ts, which masks has(…) spans from parseCelToAst's AST and only then calls firstUndeclaredReference on a source that no longer contains has(, so the rewrite is a no-op there by the regex gate.
  2. Bare name and index-as-last-step still refused: right. has(status): the argument is an id, not a . select, so no rewrite; has(record.items[0]): the argument is an [] node, not ., so no rewrite; both reach cel-js's own macro and still fault has() invalid argument at compile, validateExpression and evaluate — pinned by the controls test. has(record.a.b) (identifier root) is left to cel-js unchanged.
  3. No already-evaluating expression changes its answer: right. The rewrite touches only a has() call whose selection chain is not identifier-rooted, and every such call faulted at parse-time macro expansion before this change — the card measured evaluate answering ok: false, and runProgram runs no separate check(), so the fault was the macro's own. The cheap gate HAS_CALL_RE excludes .has( (a member call, rcall in the AST) and longer names; a has( inside a string literal passes the gate, parses to no call node and is left unchanged. The memo is keyed on source alone and the rewrite reads no scope, so caching is sound. Order of composition is consistent: rewriteIndexedHas is innermost at every site, and the later passes (rewriteNullableTernary, rewriteTemporalEquality, rewriteFaultedOperands) parse through optional-type-enabled environments and match no .? node, so they neither break on nor rewrite the lowered form.
  4. firstUndeclaredReference reports more, never less, and only on sources compile already refused: right. has(a[0].b) && status == 'x' used to answer null (the macro fault took cel-js's single error slot) and now answers status. Its consumers (flow-variable-scope, flow-cel-root-scope, validateExpression's bare-ref arm) can therefore gain a finding on such a source, but every such source was already refused by the validateExpression → compile pass at the same lint/registration step, so no metadata that published before is refused now. inferCelType now returns bool for the form instead of null; firstTypeMismatch keeps its verdict (UNSOUND_OVERLOAD_RE matched neither the old macro fault nor any new message).
  5. Accept-set change, measured from the diff. Widened: every slot judged through celEngine.compile — validateExpression('predicate', …) on object validation/visibility rules, flow conditions, action and sharing-rule predicates and hook predicates, AND validateExpression('value', …) on flow assignment values, Field.formula and seed dynamic values (the value role takes the same compile branch at validate.ts:988), plus inferExpressionType. Narrowed: nothing. The page/view visibleWhen gate is unchanged in both directions — its syntax verdict is parseCelToAst's (parsed before, parses now) and its one adopted check() verdict, firstUnknownFunctionCall, filters compile's message on the no-overload vocabulary, which neither has() invalid argument nor a clean compile matches. The engine and the publish validator now agree on this class, which is the card's acceptance line.
  6. Public surface: unchanged. rewriteIndexedHas is exported from cel-engine.ts for its test, but @objectstack/formula publishes only . → dist/index.*, and index.ts does not re-export it — the same standing as rewriteNullableTernary. No spec artifact moves; Check Changeset and the spec gates are green.
  7. Changeset prose — one incompleteness, not a wrong declaration. The "What widens" bullet names the five predicate slots and omits the value-role slots named in item 5. The claim is narrower than the behaviour (the safe direction under Prime Directive 10's corollary), so it is a note for the dev, not a FAIL item: one clause on the bullet would make the changelog sentence complete.
  8. Test file. Nine pins, imported from source (no dist leg); the parity test compares the lowered form against cel-js's own identifier-rooted has() over ten value kinds × two depths by outcome kind and first message line, which is the right oracle given that the library's semantics cannot be read here. The ablation reported in the PR body (8 failed / 1 passed with the rewrite short-circuited, controls staying green) is consistent with the pins' shape.

② Semver level

  • Clause-②: yes (widening) — true. The line answers 「本卡放宽接受集或扩大公开面吗」 (clause2-line.mjs). Measured from the diff, the publish accept set is relaxed: strings every compile-backed door refused before are accepted now (item 5). The public face is unchanged (item 6); the accept-set half alone makes the answer yes, and (widening) is the only arm that fits.
  • Metadata-protocol: added — true. AGENTS.md Post-Task Checklist step 3 defines the kind as "the change to what a metadata author can write — a new key, accepted value or metadata type is added". An author can now write has(a[0].b) in a predicate or value slot where the platform refused it: a newly accepted value. No key is removed, no requirement tightened, no meaning or default changed, so neither narrowed nor semantic applies.
  • @objectstack/formula: minor — true. Same step: "a kind takes at least minor on a package the diff moves"; ruling A on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105 (clause2-line.mjs header): "minor when the protocol an author writes against is added to". The diff moves only @objectstack/formula.
  • The dev's open question (A vs B): A, decided by the rules, no escalation. Reading B — "an implementation gap in an already-declared CEL dialect, so no / none / patch" — would need a rule that keys the level on the declared dialect rather than on what the platform accepts. The repo's rules key it on measurement: the Clause-② line is "measured from the diff" (the claim comment says so; record-recognisers.mjs reads every face "off the pull request's committed tree, never off a seat's statement"), the Metadata-protocol kind is defined over what an author "can write", and Prime Directive 10's corollary makes the enforced set the platform's claim ("keep the claim as narrow as the enforcement"). The repo's own contract text also declared the narrower behaviour as the platform's — the guardOf docblock in packages/spec says an index anywhere in a has() argument "is refused when it runs" — so "already-declared CEL dialect" is not what the spec package said. Reader-facing consequence: added places the entry under 「新增能力」 in the release body (docs/releases-maintenance.md), where an upgrading author learns the form is accepted; B would file it under 「其他修复」 and hide exactly that. The fix(formula): summary prefix beside minor is fine: the level follows the kind alone.

③ Boundary flags

  • Flag: Clause-② / Metadata-protocol reading (open question). Answered above: A stands; keep yes (widening), added, minor.
  • Flag: route taken (evaluate, not refuse). Right choice under Prime Directive 12 — the metadata is spec-compliant CEL, so the consumer was the thing to fix, not the author; and it takes the engine fix without a lint change, since every type verdict already flows through @objectstack/formula. The card's "engine and publish validator agree" acceptance is met for this class.
  • Flag: error excerpts quote the lowered source on a runtime fault inside a lowered call. Accepted: it is the same property rewriteNullableTernary already has, the authored has(…) stays in every parse-only surface and in issue.source at lint, and the class of fault (No such key on an intermediate step) names the key the author wrote.
  • Flag: removal note (drop rewriteIndexedHas and its five call sites when cel-js lifts the identifier-root restriction). Fine as a code-comment note; nothing to do now.
  • Flag: the sibling class stays open (out-of-scope finding 1). has(status) and has(record.items[0]) on a page or view visibleWhen still publish with 0 findings and fault at runtime, so the console still fails open on them. Answer: maintainer. Closing it means adopting a second check() complaint on the page/view rule, which ruling lint: validate-visibility-predicates passes unknown CEL functions clean (the validateExpression premise is falsified) — ruled: extend to function-existence ERROR, scoped supersession of the parse-only ruling #13594 refinement 3 reserves: 「只拒未知函数裁定,⛔ 不搬运 check() 的其他抱怨。」 (quoted verbatim from validate-visibility-predicates.ts). No seat may reverse that; it needs the maintainer's word on which of the two remedies the card's "Wanted" section allows (adopt the has() invalid argument verdict on that rule, or refuse at publish with the in-form message). The dedupe pass over the 106 open cards at 2026-10-22T19:42Z found no card for this class (only Formula/CEL: has() over an indexed path is accepted at authoring and refused at runtime, so a visibleWhen using it fails open #25267 itself and finding(service-automation,lint): the resume door evaluates a screen field's visibleWhen over the run's variables, wider than the declared scope (the screen's own field names), and nothing refuses an undeclared name at authoring #20178, a different visibleWhen scope finding), so the ruling request needs a carrier: a class-(c) card with the dev's measured repro (validateVisibilityPredicates 0 findings against the record.x === 1 control; celEngine.evaluate faulting on both), addressed to the maintainer for the ruling before any seat implements. Not this PR.
  • Flag: stale guardOf docblock and test title (out-of-scope finding 2). packages/spec/src/automation/flow-value-slot-template.ts (guardOf, "an index anywhere in its argument … is refused when it runs") and flow-value-slot-template.test.ts:201 ("has() refuses one at run time") state a reason that is false after this PR; the behaviour they explain stays right, because a path ending in an index (vars.rows[0], one of the chain's own steps) is still refused and an out-of-range index faults rather than answering false. Answer: card, and it cannot ride this PR. Two reasons: the claim's file surface is packages/formula/src/, packages/lint/src/, their tests and one changeset, "stop on breach", and packages/spec/src/** is a breach; and reviewFace reads packages/spec/src/** (tests excepted) as the schema face, so adding that file would move this PR onto CONTRACT_REVIEW_TIER's schema face and invalidate this record. It is a docs-only, S-class domain:spec follow-up (two comment edits, no behaviour), and it needs a card rather than an acceptance note because the note dies with this PR while a false claim in the contract package is text the next agent reasons from.

Implemented-by: claude/project-thread-zu4f7d
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T20:06Z · head baf8e225450a844e7c7e0401c5efea54b2f9d7ad

  1. Shape: draft, base main, one closing keyword (Fixes #25267, after the session's attribution block). check-governed-merges.mjs --pr 25275 reads not governed and +254/−6, so this is an ordinary queue landing. Three files: packages/formula/src/cel-engine.ts, its new test, the changeset.
  2. Premise measured: the dev measured cel-js 8.0.0's has() macro refusing every non-identifier-rooted selection at type-check and evaluation, and pinned the CEL-spec answer for index and call receivers in cel-has-indexed-receiver.test.ts. The review read the lowering at all five checker/evaluator sites and found it held.
  3. Gates: check-runs on this head 34 success and 7 path- or opt-in-skipped, with none failed or pending. The dev's local dispatch-gates run: 67 derived, 66 run with exit 0, 1 NOT-MEASURED (check:dual-build-cjs-loads, needs every package dist; left to CI), 0 UNRUN.
  4. Contract review: PASS on this head, record 6349699572. It serves TIER_DEFAULT because reviewFace reads the declaration face on these paths; the reviewer ran at contract-review effort.
  5. Accepted deviations:
    • The branch is the card thread's claude/project-thread-zu4f7d, as claim 6348364574 names it.
    • origin/main was not merged before landing (7 commits behind, none in these files); the merge queue rebuilds on main.
    • The session's attribution block takes PR body lines 1–2.
  6. Prose faces (the seat's own read):
  7. Out of scope:
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 20:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 20:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 9d96b2b Oct 22, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-zu4f7d branch October 22, 2026 20:42
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

Development

Successfully merging this pull request may close these issues.

Formula/CEL: has() over an indexed path is accepted at authoring and refused at runtime, so a visibleWhen using it fails open

2 participants