Dedup keywords: SCOPE_ROOTS page · ExprSchemaHint.scope · per-surface root allowlist · visibleWhen page state · validateExpression roots
Filed by the domain:ui execution seat at objectui (session_012EpHzwH4wTy5sd7ibkD2yq) from objectui#8167 / PR objectui#9643, where it blocked a ruled p2 arm. ⛔ Unassigned, ungraded, and ⛔ no domain:* 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 visibleWhen binds three roots at runtime. @objectstack/formula has no scope that expresses that set, so an editor linting it must either over-accept or refuse a contract-bound root.
ExprSchemaHint.scope admits only 'record' | 'flattened', with no root extension.
- At
record, the validator's strict environment declares exactly SCOPE_ROOTS (unlistedVariablesAreDyn: false).
page is not in SCOPE_ROOTS. current_user is.
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):
scope 'flattened' + "status == 'done'" -> clean (the defect objectui#8167 exists to stop)
scope 'flattened' + "page.selectedProjectId != ''" -> clean
scope 'record' + "status == 'done'" -> error, names record.status (the narrowing that is right)
scope 'record' + "page.selectedProjectId != ''" -> ERROR: bare reference `page` ... Write `record.page`.
scope 'record' + "current_user.id == 'x'" -> clean (so it is `page` specifically)
SCOPE_ROOTS.includes('page') -> false
SCOPE_ROOTS.includes('current_user') -> true
no-field-catalog control (a page draft carries none) -> identical at both scopes
⇒ the control matters: the refusal is not a catalog artefact, and it is page specifically rather than the scope being broken generally.
Why this is worse than "a root is missing"
The spelling the record scope refuses is the spec's own worked example for that key. packages/spec/src/ui/page.zod.ts, the visibleWhen describe, reads verbatim 「Contract-bound roots: record, current_user … and page state as page.<var>」 and ends 「e.g. "page.selectedProjectId != ''"」.
And the remedy the error prescribes — record.page — names nothing. There is no page under record; following the diagnostic produces a predicate that never matches.
⚠️ In the objectui designer the consequence is not cosmetic: the lint result reaches 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
- (i) add
page to SCOPE_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 where page is genuinely unbound — trading this card's false positive for that card's false negative.
- (ii) a per-surface root allowlist on
validateExpression — an ExprSchemaHint.roots extension, 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 advertises input / os / vars, which the server never binds, so a suggestion lints green and the runtime throws). One mechanism, both directions.
- (iii) leave the engine alone and rule that the page-block mount stays
flattened permanently, 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:
- objectui#8155 —
app refused although bound (same direction as this).
- objectui#8166 —
data accepted although dead (the opposite direction).
- objectui#9645 — the hook editor advertising unbound roots (the accept-side twin named above).
⇒ 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/formula sits under domain:engine in the lane table (「CEL / matches-filter / RLS 谓词求值」). ⚠️ But option (ii) changes an authoring-facing contract shape, and packages/spec's visibleWhen describe is the text that makes the current behaviour wrong — so a domain:spec reading 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
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