Repository navigation
finding(formula): no scope expresses what a page block's visibleWhen binds - record scope refuses the spec's OWN page.var example and prescribes the meaningless record.page, disabling Save in the designer #18554
Description
Activity
⛔ AMENDMENT by the filer — the option list above is INCOMPLETE, and the missing option needs no change to this repo at all
domain:uiexecution seat at objectui,session_012EpHzwH4wTy5sd7ibkD2yq. ⛔ Still ungraded, ⛔ still unrouted, ⛔ this card still rules on nothing. Amending my own filing.What I got wrong
The card's framing — that a page block's
visibleWhenis 「blocked on an engine-vocabulary change」 — is over-stated, and I wrote it that way by carrying the implementing dev's phrasing without testing it. An independent contract-review subagent, running the installed@objectstack/formula@17.4.0rather than reading its source, measured two facts the option list should have been built on:firstUndeclaredReference(source, knownNames)is already published by the engine — it takes the known-name set as an argument.@objectstack/lint's view/page gate already lints this exact surface, withVIEW_PAGE_EXTRA_ROOTS = ['current_user', 'page'].
⇒ the vocabulary the page-block surface needs already exists and is already in use one layer up.
The missing option
- (iv) objectui's own
celAuthoringtakes thefirstUndeclaredReference(source, knownNames)route, supplying the page surface's root set the way@objectstack/lint's view/page gate already does. ⭐ This requires no objectstack change whatsoever — it is an objectui-side fix, and it is a clause-②yesin its own right.
⇒ this materially changes the card. As filed it reads as an objectstack blocker gating an objectui arm; with (iv) on the table it may be neither a blocker nor objectstack's. ⛔ Which option is right is still not this seat's to decide, and the routing question the card ends on is now genuinely open in a way it was not when I filed it.
What still stands
The measurement is unchanged and was reproduced by the reviewer at runtime rather than from source:
SCOPE_ROOTShas nopage;recordscope refusespage.selectedProjectId != '', which is the spec's own worked example, with the meaningless remedyrecord.page;current_useris accepted, so it ispagespecifically; and aroots/slothint passed tovalidateExpressionchanges nothing today.⭐ One reading added by the reviewer and worth keeping: under
flattenedwith a field catalog,page.*produces a non-blocking did-you-mean warning, never an error. So the two faces differ by more than strict/lenient, which option (iii) would need to weigh.⚠️ Why I am flagging this loudly rather than quietly editing the bodyThis card exists in a family about artefacts asserting things they have not measured, and objectstack#18506 is this lane's card reporting exactly that pattern. I filed this one ~30 minutes ago carrying an unmeasured claim from a report into a card's option list, where triage would have graded it. ⇒ the sixth instance of the pattern, and the second by this seat today. Recorded in the open, per that card's own discipline.
domain:uiexecution seat ·session_012EpHzwH4wTy5sd7ibkD2yq· filer's amendment · reading taken 2026-09-17T01:21Z
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsClaim: PM loop round 1
Session:session_01CqmCgU5RGDoJYhHUMVp2af
Branch:claude/issue-18554-formula-page-block-scope
Worktree:objectstack-issue-18554
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/formula/src/(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus (default judgement tier)— quoting this dispatch's owndispatch-gates.mjs --tier: 「Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).」 ⇒ default judgement tier: the card is a scope/contract shape question, ⛔ not a mechanical edit.
Clause-②: yes
Thread-read: 5715732918
Serial constraints cleared:⚠️ THE FORMULA SEAM IS SERIALISED TO ONE CARD AT A TIME, AND THIS IS THE ONE. Three cards now sit in packages/formula: this one, #18318 (delete the never-bound EvalContext.api, ruled B in batch #148) and #18545 (register can, ruled A in batch #147). ⭐ The #18545 ruling imposes the constraint itself, verbatim: 「#18318 … sits on the same buildScope / EvalContext seam … the two land in sequence, not in parallel.」 This seat extends that to THIS card as well, because ExprSchemaHint.scope and EvalContext plausibly share packages/formula/src/types.ts and ⛔ no itinerary can be established without a PR. This card goes first on the take-order: pm:blocking outranks p2. #18318 and #18545 are held and the hold is recorded on each. No other lane card touches this package; in-flight devs at claim time: 0.Why this card is first
The take-order is
priority:p0>pm:blocking>target:board > p1 > p2 > … . This card carriespm:blockingand the two ruled siblings are plain p2. ⇒ It goes first even though the other two now carry fresh maintainer rulings.⭐ And the block is real and cross-repo: triage records that it holds up a ruled p2 arm in objectui (objectui#8167 / PR objectui#9643). ⇒ Landing this releases work in another repo, which is exactly what
pm:blockingis computed to surface.Premises — triage's, and they were measured against a RUNNING engine
⭐ Recorded because it changes how much re-derivation is owed: triage states the reading is 「对已安装引擎跑的四角 + 一个对照」 — four corners plus a control against the installed engine, ⛔ not inferred from source, and an earlier source-side reading agreed. ⇒ Two independent instruments already concur.
The defect, as graded:
ExprSchemaHint.scopeaccepts only'record' | 'flattened'and supports no root extension;- under
record, the validator's strict env declares onlySCOPE_ROOTS, andpageis not in it (current_useris); - ⇒ the editor must either over-accept or refuse a contract-bound root.
- ⭐ The concrete consequence:
recordscope refuses the spec's OWNpage.varexample and prescribes a meaninglessrecord.page⇒ Save is disabled in the designer.
⚠️ Re-measure all of it on your base anyway — ⛔ the card is two weeks of tree-motion away from nothing, and this lane has watched three cards' line numbers and one card's counts rot in a single day.⚠️ Clause-②: yes, declared nowWhatever shape the repair takes, it changes what the validator accepts — either a new root becomes expressible or a scope gains an extension point. That is the authorable surface widening.
⇒
Clause-②: yesat column 0 of the PR body,needs:contract-reviewon the card in the same stroke as this claim, an in-seat contract review owed before it lands, and a changeset — ⛔skip-changesetis wrong on a card that moves what the validator accepts.⚠️ If your measurement shows the repair is purely a refusal-message or docs change with no accept-set movement, say so and argue it down — ⛔ but do not assume that; the card's own framing is that a contract-bound root is being refused.
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18554, "status": "done", "branch": "claude/issue-18554-formula-page-block-scope", "pr": "https://github.com/objectstack-ai/objectstack/pull/18696", "session": "session_01CqmCgU5RGDoJYhHUMVp2af", "premise_still_valid": true, "summary": "Re-measured every premise on this base and all of them reproduce: SCOPE_ROOTS has no `page` (`current_user` is in it); `scope: 'record'` refuses `page.selectedProjectId != ''` — the worked example page.zod.ts:390's own visibleWhen describe ends with — and prescribes the meaningless `record.page`; `flattened` accepts it together with the bare-field shorthand; the `current_user` control is clean, so it is `page` specifically; a roots/slot hint changed nothing. Repair: `ExprSchemaHint.roots` — an authoring surface names the binding roots it mounts BEYOND the platform baseline, threaded into the record-scope bare-reference check, into the flattened did-you-mean set, and into introspectScope's advertised roots. Option (ii), scoped to the one direction the engine lacked: collectCelRootIdentifiers already lets a CLOSED surface refuse a baseline root, so only the widening direction was unexpressible. ⛔ Not permissiveness: at a surface declaring `page`, `status == 'done'`, an undeclared root, and a mixed source all still refuse. A mistyped root (`pge.selectedProjectId`) is now pointed at `page` instead of at `record.pge`, behind three guards (no declared roots / a known field / no near-miss all keep the old message). The filer's option (iv) is not foreclosed — it stays available and this key is the same vocabulary reaching the shared validator, so a surface can declare its roots without dropping out of validateExpression and losing the compile check, braces hint, field existence, role catalog and type-soundness pass. ⛔ EvalContext untouched (types.ts not in the diff), no `can` added: no collision with #18318 or #18545, and `roots` is plain declarative data, not a resolver callback, so it does not narrow #18545's ruled shape. Assignee was set by the PM (huangyiirene) before dispatch and is untouched; the newest Claim (5716206309) names this branch.", "tests": "Final head 4f385ea7e7; every reading below is from that head. (1) `pnpm --filter @objectstack/formula test` -> 30 files / 873 tests passed, 17 of them new in src/validate-surface-roots.test.ts. (2) `pnpm --filter @objectstack/formula typecheck` -> green (tsc --noEmit + check:test-typecheck: 'test layer compiles under tsconfig.test.json; 3 file(s) / 7 error(s) / 4 pinned signature(s) held'). (3) Dependency closure built: `pnpm --filter '@objectstack/formula^...' build` green before any measurement, and the whole-workspace build `turbo run build --filter='./packages/*' --filter='./packages/*/*'` -> 72/72 successful. (4) GATES — one count, one reconciliation, on the final head: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 57 families from the 3 real changed paths; all 57 ran with exit codes captured to disk before anything was read; `--ran` verdict: '57 derived famil(ies) accounted for — 57 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN' (exit 0). Three exited 3 PREREQUISITE NOT MET on the first pass (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) and were re-run to exit 0 after the workspace build. check:cross-package-test-inputs -> exit 0, no spurious red (consistent with #18641 / e1f4f06d1d). (5) ACCEPT-SET DELTA: the same four-corners+control script run against dist built before and after the change differs on EXACTLY ONE line — the `roots` probe, ERROR -> clean. Four corners and the control are byte-identical. (6) REVERSE VERIFICATION (one-shot, from the committed state): the record-scope arm reverted to `firstUndeclaredReference(source)`. Mutation proved on disk by anchor counts (new=1 old=0 -> new=0 old=1) and a moved blob hash (e751cb547c38e184df5aa6fc04abc9d23c608dcd -> fa72f8950bb729fd430fddd0f534397bfb70d026), never by an editor's exit code. Predicted direction was 'turns red' and it does: 2 of 17 pins fail ('resolves once the surface declares `page` as one of its roots'; 'judges the declared root and the bare field in one source, refusing on the field'), 15 pass. The mistyped-root pins stay green because that arm is the refusal message and the mutation was the accept side — the expected partition. Restore leg: `git checkout HEAD --` naming the file by absolute path, under a trap on EXIT/INT/TERM; proved by the blob hash back to e751cb54 and an EMPTY `git diff HEAD`, then the file re-run green 17/17. No dist is in this ablation's resolution path (the test imports ./validate as package-relative source), so no rebuild leg applies. (7) LINT — a DECLARED narrowing, not the repo-level scan. `pnpm lint` (eslint . --no-inline-config) is CI's run. Ran: eslint --no-inline-config --format json over the 2 changed source files -> 0 errors / 0 warnings / 2 files counted from the JSON. Population read from ESLint's own API (not guessed): 6815 tracked files carry a linted extension and ESLint ignores none of them globally. Immutability: this repo's single eslint.config.mjs never enables type-aware linting for ANY file (parserOptions.project and projectService appear zero times; the config states it in prose with a recorded positive control), so no rule resolves types across files and this diff cannot move the verdict of a file it does not contain. The changeset .md carries no linted extension. (8) Control-byte scan over all three changed paths: zero hits; check:nul-bytes exit 0.", "mcp_calls": "0 — no MCP GitHub tool was called at all (no read tools, no write tools)", "api_writes": "2 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft, base main) · POST /repos/objectstack-ai/objectstack/issues/18554/comments (this report). Plus 2 `git push` (the pre-edit empty-branch write probe, and the implementation commit). The budgeted POST /issues/{n}/labels was DELIBERATELY UNSPENT — see open_questions[0]: the only label this PR is owed is `needs:contract-review`, which this role file assigns to the seat and forbids the dev to hang, unhang or wait on.", "open_questions": [ { "question": "The role file says a dev hangs labels on the PR the moment it opens ('打标签是你的步骤,PR 一开出就打'), and in the same section says `needs:contract-review` 归席位, ⛔ 不挂不摘不等. On this card the only label the PR is owed is that one, so the two clauses point opposite ways and I took the narrower prohibition: NO label write was made. Reading, as instructed: PR #18696 carries NO labels at all; card #18554 carries needs:contract-review. `node scripts/pm/check-clause2-carriers.mjs --pair 18696` exits 4 (NOT clause-② legible) on row C1 — the dual carrier is written half way, card side hung, PR side bare.", "options": [ "A — the seat hangs needs:contract-review on PR #18696 in its own stroke (SKILL.md: 「PR 一现即挂 PR」), which is what the C1 row is asking for and clears the split", "B — the role file's 'PR 一开出就打' is amended to name which labels a dev may hang, so the next dev does not have to resolve the same conflict from scratch" ], "recommendation": "A now, B as a follow-up on the role file — the split is live on the board this minute and only the seat can close it, while the wording conflict costs the next dev a derivation rather than a defect." } ], "out_of_scope_findings": [ "noted, not filed: `introspectScope` advertises `input`/`os`/`vars` to every caller regardless of what the surface binds — the accept-side twin this card's body names. This PR makes declared roots JOINABLE to that list but deliberately does not narrow it, because narrowing what a surface advertises is that card's verdict. Successor/carrier: objectui#9645, already open and named in this card's own body — no new card owed.", "noted, not filed: the filer's amendment records that under `flattened` WITH a field catalog `page.*` yields a non-blocking did-you-mean warning. Measured here it is catalog-CONDITIONAL: fields ['pages','status'] warns, fields ['status','amount'] and ['page_size'] produce NO warning at all — the shared nearestName threshold, behaving as designed. In the page-draft case the card describes (no catalog) `flattened` is simply silent. A correction to a reading, not to code; recorded in the PR's premise table where a reviewer sees it. Carrier: this PR's body.", "noted, not filed: `origin` carries branch `claude/issue-18545-formula-can-function` with ZERO commits ahead of `main` — the empty-branch landing marker a dispatched dev pushes before its first edit, on a card the dispatch records as HELD behind this one. No file overlap with this PR in either direction (this diff touches neither types.ts nor cel-engine.ts). Not a defect and not mine to act on; the seat owns the take-order. Carrier: this report." ], "gates": "57 derived / 57 run / 0 NOT-MEASURED / 0 UNRUN, reconciled with --ran on final head 4f385ea7e7 (dispatch-gates exit 0)", "clause_2": "yes (widening) — at column 0 of the PR body; changeset .changeset/18554-expr-schema-hint-surface-roots.md at `@objectstack/formula: minor` carries the same line. skip-changeset would be wrong: @objectstack/formula publishes, and this moves what the validator accepts.", "files_changed": [ "packages/formula/src/validate.ts", "packages/formula/src/validate-surface-roots.test.ts", ".changeset/18554-expr-schema-hint-surface-roots.md" ], "file_surface_respected": "yes — packages/formula/src/ plus the changeset the Clause-② declaration obliges; no breach.", "sanitizer_check": "Card #18554's body shows NO sanitizer truncation: it ends on its own closing line ('⛔ This seat did not run a dedup query for this card. Dedup words are at the top.') followed by the platform's appended bare footer, and every fenced block closes. PR #18696's body was read back in full after creation: byte-identical to what was sent except a stripped trailing newline (10377 -> 10376), one footer, `Fixes #18554` as line 1, `Clause-②: yes (widening)` at column 0. No angle-bracket fragment and no HTML comment was sent on any surface." }
Generated by Claude Code
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsContract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:4f385ea7e767966c3e8385c260a3c701954e2049domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, in-seat review of the clause-② it dispatched. Written 2026-09-17T15:28Z. PR #18696. ⛔ Every reading below is command output on this head, not the PR body's account of it.The fuse, before the verdict. Served tier read from
get_session→external_metadata.last_served_model— ⛔ notsession_context.model, which is the configured value the fuse does not take.dispatch-gates.mjs --tier packages/formula/src/validate.tsanswers no path-derived mandate: none of this PR's three paths hits any of the 3 declared globs, so the bar is the default judgment tier and the reading meets it.⚠️ That run also printed STALE TREE — this checkout is 39 commits behindorigin/main, andscripts/pm/dispatch-gates.mjs, the file that declares the globs, is itself one of the files that changed across the range. ⇒ ⛔ Not waved off:MANDATORY_TIER_GLOBSandSUSPECT_TIER_GLOBSwere diffed betweend93400f42eandorigin/mainand are identical, so the stale tree cannot have moved this answer. A tier answer derived from a tree nobody is on is the exact shape that warning exists to refuse.① Derived judgments
Six movements are derivable from the diff. Each is named and judged, ⛔ not summarised.
-
New published key —
ExprSchemaHint.roots?: readonly string[](validate.ts:133). Optional, so no existing construction site breaks;readonly, so the validator cannot mutate a caller's array. Right. ⇒ A new key on a published payload is the mechanicalyesfloor, which is why this record is owed at all. -
Accept set,
recordarm (validate.ts:804).firstUndeclaredReference(source, surfaceRoots). Those names land inbuildScopedEnvas a second registration loop (cel-engine.ts:207-209); theSCOPE_ROOTSloop above it (:199-201) is untouched by this diff. ⇒ Purely additive, and it is the movement the card asked for. Right. -
Accept set,
flattenedarm (validate.ts:833).[...schema.fields, ...(schema.roots ?? [])]— a spread onto the existing array. Its effect is strictly narrower than (2): it suppresses a did-you-mean warning, not an error. Right. -
Refusal-message movement (
mistypedRootMessage). Gated atvalidate.ts:607—surfaceRoots.length === 0returnsnull— so no call site that does not opt in can have its message move. ⭐ This guard is what makes "the message change rides entirely inside the new opt-in" a property of the code rather than a claim about it. Right. -
Advertised surface (
introspectScope,validate.ts:886). The seven baseline names stay literals and are deduped through aSet. Right — and feeding accept and advertise from one declaration is the correct call: a root the validator accepts but the advertised list never names is a spelling an author has no way to discover. -
What is deliberately NOT moved.
collectCelRootIdentifiersis untouched, and the key's own docblock (validate.ts:110-120) forbids bolting the refusal direction onto it. ⇒ Two directions, two mechanisms. Right.
⭐ The claim that carries this PR is structural, ⛔ not conventional. "It only ever adds" holds because there is no expression in the diff by which a value of
rootsremoves a name from any set — (2) and (3) are spreads onto existing arrays, (5) is a spread into aSetbeside seven literals. That is checkable by reading, which is what makes the claim reviewable at all rather than a promise about future maintenance.⛔ A probe this seat opened against the new key, and closed against itself
Reviewing a newly published key, the seat asked what a caller's string does when it reaches a regex constructor, and measured the constructor shape directly:
root value new RegExppageok my$rootok — the $escape atvalidate.ts:572foo(throws SyntaxError ⇒ It never reaches one.
isNamespaceUsehas exactly one call site (validate.ts:609) and is passedbare, ⛔ never an element ofroots.barecomes fromfirstUndeclaredReference, whose only return path is the capture of/Unknown variable:\s*([A-Za-z_$][\w$]*)/atcel-engine.ts:297-298— the character class is the guarantee, and$is the single metacharacter it admits, which is the one the escape handles. The comment atvalidate.ts:570-571is therefore an accurate statement of an invariant enforced one module away, ⛔ not a reassurance — and it was verified at the enforcing line rather than taken at its word.Where a caller's root string does land, all three destinations were read:
cel-engine.ts:207-209—registerVariable(name, 'dyn')insidetry {} catch {}. A name cel-js refuses is swallowed ⇒ the root is simply never declared ⇒ the previous refusal stands. ⭐ Fail-closed, which for a widening key is the only direction that is safe to get wrong.nearest(validate.ts:642) — Levenshtein over the candidates, no regex.mistypedRootMessage's template — string interpolation into a message, nothing else.
⇒ No finding. Recorded rather than dropped, because a reviewer who forms the same suspicion should not have to re-run it, and because a probe that closes is evidence about the key, ⛔ not wasted motion.
The test face, judged on strength rather than count
17 pins. Four of them are why this review is confident:
validate-surface-roots.test.ts:182is a dark control — the same near-miss still warns when the root is NOT declared, so the zero at:169is not a vacuous pass.:104judges a declared root and a bare field in one source and still refuses on the field — the shape that would catch "declaring a root quietly disabled the arm".:151-166is anit.eachover all three non-adopting shapes (no hint ·rootsabsent ·roots: []), which is exactly claim (4) and the reason it is not taken on trust.:207pins the one behavioural consequence of swapping an array literal for aSet: a baseline root passed inrootsis advertised once, not twice.
The reverse verification partitions the way the code predicts — reverting the accept arm turns exactly the two accept pins red and leaves the mistyped-root pins green. ⭐ A mutation that took everything down would have said the pins are coupled, ⛔ not that they are strong.
② Semver level
minoron@objectstack/formula, declared in.changeset/18554-expr-schema-hint-surface-roots.md, which carriesClause-②: yes (widening); the PR body carries the same line at column 0.patchwould be wrong: a new key on a published payload is the mechanical floor foryes, and the accept set moves besides.majorwould be wrong: nothing is removed, and no caller that does not passrootshas its verdict or its prescription move — which (4) and theit.eachestablish rather than assert.- ⛔
skip-changesetwould be wrong:@objectstack/formulapublishes.
Machine read in this act:
node scripts/pm/check-clause2-carriers.mjs --pair 18696→ exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree."③ Boundary flags
One
open_questionsentry and three out-of-scope notes. Each gets an answer.Q1 — the role file's "a dev hangs labels the moment the PR opens" against "
needs:contract-reviewis the seat's — do not hang it, unhang it, or wait on it". The dev took the narrower prohibition, wrote no label, and escalated with the split named and measured (row C1, exit 4). ⇒ Correct call, and the right escalation — it handed a judgement back rather than choosing silently. Its option A was taken in the seat's own stroke before this record: the label now hangs on both carriers and--pair 18696exits 0. Its option B — amend the role file to name which labels a dev may hang — the seat adopts as owed, to be filed on the skills lane. It costs the next dev a derivation, ⛔ not a defect, so it does not hold this PR.F1 —
introspectScope's baseline roots are still surface-blind. Agreed, and ⛔ not discharged by this PR. Narrowing what a surface advertises is objectui#9645's verdict to give; this change makes declared roots joinable to that list without touching it. ⇒ This review blesses the join, ⛔ not the baseline.F2 — the
flatteneddid-you-mean is catalog-conditional. Accepted as a correction to a reading, ⛔ not to code: the gate isnearest's shared threshold atvalidate.ts:642, behaving as designed. No carrier owed, and the PR body is the right place for it.F3 —
origincarriesclaude/issue-18545-formula-can-functionwith zero commits. ⛔ Not taken on the dev's word; re-measured here on both readings the charter requires, because the failure direction is a live card read as already claimed:git rev-list --count origin/main..origin/claude/issue-18545-formula-can-function= 0, and astate=allPR query on that head returns 0. ⇒ A probe branch, ⛔ not in-flight work, and it does not block #18545's dispatch.Implemented-by:
claude/issue-18554-formula-page-block-scope
Reviewed-by:session_01CqmCgU5RGDoJYhHUMVp2af⇒
mode:subagent, so the pair compares a branch against a session and is ⛔ not SELF-REVIEW by the charter's own reading: in-seat review of a subagent's diff is the design. Stated plainly so no reader has to infer it.VERDICT: PASS
⏹️ ⛔ Landing is NOT cleared by this record alone. Pre-landing check ① is now in case; ② will be re-read at the clearing stroke, ⛔ not carried over from above; ③ is not met — CI on this head is still converging. ⇒ The carriers stay hung until all three are in case, and this seat follows the PR to MERGED.
Generated by Claude Code
-
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report reviewed — ACCEPT. PR #18696 is ready and in the merge queue.
domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, R2, 2026-09-17T15:37Z. Report:5716781616. Contract-review record:5716968600(PASS). ⛔ Verified against GitHub and the tree, ⛔ not against the report's narrative.Form, scope and CI
criterion reading draft · base · head now false·main·4f385ea7e767966c3e8385c260a3c701954e2049Fixes #18554line 1 ·Clause-②: yes (widening)at column 0both present changed files 3 — 1 source, 1 test, 1 changeset ( +366/−3)path face, derived by check-governed-merges --pr 186960 of 3 hit the register ⇒ ⛔ not governed CI, latest per name 34 checks · 0 non-green · 0 pending; all 7 required contexts green queue ready_for_review15:35:59Z,added_to_merge_queue15:36:01Z — read off the event list, ⛔ not theauto_mergefield, which the queue clears on entry⭐ The round's best piece of work — the accept-set delta was MEASURED, not argued
A widening card invites a PR body that reasons about what the change should do. This one built
packages/formula/distfromorigin/mainbefore any edit, ran a four-corners-plus-control probe script against it, then ran the identical script against the built change:the same script run against dist built before and after the change differs on EXACTLY ONE line — the
rootsprobe, ERROR → clean. All four corners and the control are byte-identical.⇒ ⭐ That is the claim「it only ever adds」as an experiment with a control, ⛔ not as a property asserted about a diff. The seat's own review then found the same claim holds structurally — no expression in the diff removes a name from any set — so the two readings are independent and agree.
⭐ It corrected the thread's own premise instead of inheriting it
The filer's amendment recorded that under
flattenedapage.*reference yields a did-you-mean warning. Measured here, that warning is catalog-conditional:fields: ['pages','status']warns;['status','amount']and['page_size']produce no warning at all. In the page-draft case the card actually describes — no catalog —flattenedis simply silent.⇒ The dev re-measured a premise it was handed, found it narrower than recorded, and put the correction where a reviewer sees it. ⛔ It did not quietly build on the version that suited it.
⛔ Seat ruling on the one escalation — the dev was right to hand it back
open_questions[0]:os-dev.mdsays a dev hangs labels the moment the PR opens (:300) and, twelve lines earlier, thatneeds:contract-reviewis the seat's — ⛔ do not hang, unhang or wait on it (:288). On this card that label was the only one the PR was owed, so the two clauses point opposite ways.The dev took the narrower prohibition, wrote no label, and escalated with the split measured (
--pair 18696exit 4, row C1). Ruling: correct, on both halves. The alternative reading has a dev hanging the clause-② gate label on the work it just wrote — the shapecheck-widening-tells.mjs's own docblock refuses at:163-167. Its option A was taken in the seat's stroke; its option B is now filed as #18699, bare, for the skills lane to grade.⭐ A dev that chooses silently between two conflicting instructions leaves no trace that the conflict exists. This one left the trace, and that is why there is a card.
Verification, spot-checked rather than taken on report
- Reverse verification partitions the way the code predicts — reverting the record-scope accept arm turns 2 of 17 pins red (both accept-side), and the mistyped-root pins stay green because the mutation was the accept side, ⛔ not the refusal message. Mutation proven on disk by anchor counts (
new=1 old=0→new=0 old=1) and a moved blob hash; restoration proven by the hash returning and an emptygit diff HEAD. - ⭐ A dark control on the flattened zero: the test at
:182holds that the same near-miss still warns when the root is not declared, so the zero at:169is not a vacuous pass. The seat looked for this specifically; a widening PR whose new-behaviour pins all sit on one side of the switch proves nothing about the other side. - Gates: 57 derived / 57 run / 0 NOT-MEASURED / 0 UNRUN, one count and one reconciliation on the final head, exit codes captured to disk before anything was read. Three first exited 3
PREREQUISITE NOT METand were re-run green after a full build — ⛔ correctly not reported as passes. - Lint was narrowed, and the narrowing is argued from ESLint's own API:
parserOptions.project/projectServiceappear zero times in the singleeslint.config.mjs, so no rule resolves types across files and this diff cannot move a verdict on a file it does not contain; population 6815 tracked files, read rather than estimated. ⛔ Accepted as a declared narrowing, not as「lint passed」. check:cross-package-test-inputspassed — a fifth independent measurement that [finding]check:cross-package-test-inputsexits 0 or 1 on the same commit depending on whether adist/exists — and lint.yml runs it with no build ahead of it, so CI only ever sees the blind leg #18621 stays discharged since fix(gates): judge a cross-package walk root against the index, not the working tree #18641.
Findings
⏹️ Three
noted, not filed, each with a named carrier:introspectScope's surface-blind baseline (→ objectui#9645, ⛔ not discharged by this PR, and the contract review says so explicitly), the catalog-conditional did-you-mean (a correction to a reading, carrier = this PR's body), and the zero-commit branchclaude/issue-18545-formula-can-function. ⛔ The last was not taken on the dev's word: re-measured on both readings the charter requires —rev-list --count= 0,state=allPR query = 0 ⇒ a probe branch, and #18545's dispatch is unblocked.⛔ One probe the seat opened and closed against itself is recorded in the contract review rather than raised here: a suspected regex-metacharacter path on the newly published key is not reachable, because the value reaching
isNamespaceUseis cel-js'sUnknown variablecapture, constrained by the character class atcel-engine.ts:297-298. Verified at the enforcing line, ⛔ not by trusting the comment that claims it.mcp_calls0 · REST writes 2 (+2git push). ⛔ The budgeted label write was deliberately unspent, and that was right. ⛔ No denied-roster tool was called.⇒ Ready + merge queue (SQUASH). This seat follows it to MERGED.
Generated by Claude Code
- Reverse verification partitions the way the code predicts — reverting the record-scope accept arm turns 2 of 17 pins red (both accept-side), and the mistyped-root pins stay green because the mutation was the accept side, ⛔ not the refusal message. Mutation proven on disk by anchor counts (
huangyiirene commented
on Sep 17, 2026 CollaboratorMore actionsLanding reconciliation — PR #18696 is MERGED; this card is closed out. Written 2026-09-17T16:00Z.
domain:engineexecution seat,session_01CqmCgU5RGDoJYhHUMVp2af, R2 close. ⛔ Every criterion below is command output, ⛔ not the merge event's echo.criterion reading squash commit e75cc3c2f862967e43e49f191f6556b57625ac56parent count 1 ( 2fd23d0985) ⇒ genuinely a squash. ⛔ Not read off the auto-merge echo, which reportsmergein this repo and is non-diagnosticcontent verification on origin/main(⛔ not the merge event)roots?: readonly string[]1 ·function mistypedRootMessage1 ·function isNamespaceUse1 · the new pin file presenttest count, reconciled the file carries 14 it(literals; the report claims 17 pins. ⭐ The gap is theit.eachover three non-adopting shapes — 13 plain + 1 each × 3 = 17 cases. ⛔ Checked rather than waved through: a count that does not reproduce is the first thing to reconcileclosed_by_pull_requeststotal_count1, only #18696 ⇒ ⛔ no card was silently closed by a body keywordlane reconciliation open domain:enginecards 55 → 54; the only disappearance fromopenis this card ⇒ the expected one, and ⛔ nothing else was caught by a closing keywordlabel residue pm:dispatchedstripped vialabel-write.mjs, read back. This card now carriespriority:p2·pm:blocking·domain:engine⛔
pm:blockingwas deliberately LEFT, and the reason is a void zeroThe state model says
pm:blockingis 「自Blocked-by:索引推导的缓存,⛔ 不手工挂」. To judge whether the cache is now stale this seat searched for anything declaringBlocked-by: #18554and got 0 — and then took a must-hit control on the same instrument,in:body "Blocked-by:"repo-wide, which also returned 0 for a string this repo certainly contains.⇒ ⭐ The instrument does not fire, so the zero is void and proves nothing. Hand-stripping a derived label on the strength of a measurement that cannot distinguish 「nothing points here」 from 「the search cannot see it」 would be the same error in the other direction. ⛔ The label stays; whatever maintains that cache owns it.
⚠️ For the record, the known downstream is cross-repo: objectui#8167's page-block arm, ruled C, withdrawn from PR objectui#9643 because it had no legal answer at that mount. That arm now has one. ⛔ This seat does not close or re-open anything in objectui.The serial seam this releases
packages/formulawas held to one card at a time and this was the one. ⇒ The chain is now open at its head, in the take-order the #18545 ruling fixed verbatim (「#18318 … sits on the same buildScope / EvalContext seam … the two land in sequence, not in parallel」):#18318 (ruled B — delete the never-bound
EvalContext.api;Clause-②: yes;@objectstack/formulaminor) → then #18545 (ruled A).⚠️ ⛔ Neither is dispatched in this stroke: active devs = 3 =batch:3. The release is recorded so the next fire takes #18318 without re-deriving the order, ⛔ not so it jumps the cap.⏹️ Residue: zero. The three
noted, not fileditems in the ACCEPT each carry a named successor — objectui#9645 for the surface-blind advertised baseline (⛔ explicitly not discharged by this PR), this PR's body for the catalog-conditional did-you-mean correction, and the seat for the zero-commit probe branch (re-measured, it is not in-flight work). ⇒ ⛔ No new card is owed by this landing.
Generated by Claude Code
github-actions commented
on Sep 17, 2026 on Sep 17, 2026 – with GitHub ActionsContributorMore actionsos-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:blocking.- Closing pull request: fix(formula): let an authoring surface declare the binding roots it mounts (ExprSchemaHint.roots) #18696, merged.
- Closing commit
e75cc3c2f8, merged intomain. - Left untouched:
priority:p2,domain:engine— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 35266370639 · trigger
scheduleGenerated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Dedup keywords:
SCOPE_ROOTS page·ExprSchemaHint.scope·per-surface root allowlist·visibleWhen page state·validateExpression rootsFiled by the
domain:uiexecution seat at objectui (session_012EpHzwH4wTy5sd7ibkD2yq) from objectui#8167 / PR objectui#9643, where it blocked a ruled p2 arm. ⛔ Unassigned, ungraded, and ⛔ nodomain:*applied — routing is the triage seat's. This seat's reading of where it lands is at the end, as input only.The defect
A page block's
visibleWhenbinds three roots at runtime.@objectstack/formulahas no scope that expresses that set, so an editor linting it must either over-accept or refuse a contract-bound root.ExprSchemaHint.scopeadmits only'record' | 'flattened', with no root extension.record, the validator's strict environment declares exactlySCOPE_ROOTS(unlistedVariablesAreDyn: false).pageis not inSCOPE_ROOTS.current_useris.Measured against the INSTALLED engine, four corners plus a control
Run by the objectui dev on the real package, ⛔ not derived from source (an earlier reading of the same fact was source-only and reached the same answer):
⇒ the control matters: the refusal is not a catalog artefact, and it is
pagespecifically rather than the scope being broken generally.Why this is worse than "a root is missing"
The spelling the
recordscope refuses is the spec's own worked example for that key.packages/spec/src/ui/page.zod.ts, thevisibleWhendescribe, reads verbatim 「Contract-bound roots:record,current_user… and page state aspage.<var>」 and ends 「e.g."page.selectedProjectId != ''"」.And the remedy the error prescribes —
record.page— names nothing. There is nopageunderrecord; following the diagnostic produces a predicate that never matches.onBlockingIssuesChange→inspectorBlocking, which disables Save and auto-save while that block is selected. So an author writing the spec's own documented example would be locked out of saving, and told to write something meaningless.Options, with their costs — ⛔ this card rules on nothing
pagetoSCOPE_ROOTS. Smallest change, and the list's own docblock says it errs toward declaring more, because a missing root is a false positive that breaks a build. ⛔ Cost: it widens the accept set for every surface, including the hook and validation-rule surfaces wherepageis genuinely unbound — trading this card's false positive for that card's false negative.validateExpression— anExprSchemaHint.rootsextension, or a named surface vocabulary. ⛔ Cost: a larger change. ⭐ Benefit: it is the general answer, and it also closes the accept-side twin of the same gap (objectui#9645 — the hook condition editor advertisesinput/os/vars, which the server never binds, so a suggestion lints green and the runtime throws). One mechanism, both directions.flattenedpermanently, accepting that the bare-field shorthand is never caught there.Both the implementing dev and an independent contract-review subagent, reasoning separately, preferred (ii). ⛔ Neither is the deciding seat, and this card records that as input, ⛔ not as a recommendation this seat is entitled to make.
Related, and it is a family rather than a one-off
objectui#8167's own body already groups these as tails of objectui#5741 Phase 2:
apprefused although bound (same direction as this).dataaccepted although dead (the opposite direction).⇒ four cards, one missing mechanism: the validator has no way to say which roots a given surface binds.
What is blocked on this right now
objectui#8167's page-block arm, ruled C by the director seat with a maintainer 「同意」. The arm was implemented, failed its clause-② review on exactly this fact, and has been withdrawn from PR objectui#9643 — which now ships the hook arm alone. The ruling is sound; it simply has no legal answer at that mount until the engine gains one.
This seat's routing reading — input only
packages/formulasits underdomain:enginein the lane table (「CEL /matches-filter/ RLS 谓词求值」).packages/spec'svisibleWhendescribe is the text that makes the current behaviour wrong — so adomain:specreading is at least arguable. ⛔ Triage's call, not this seat's.⛔ This seat did not run a dedup query for this card. Dedup words are at the top.
Generated by Claude Code