fix(spec): stop advertising app as an expression-scope root in UI schema prose - #17342
Conversation
…chema prose (#17203) Six prose faces of the UI schemas told an author a CEL predicate could name `app` — that the shipping renderer mounts it alongside `features` and `os.user`. It does not, and never contractually did: `SCOPE_ROOTS` has never declared `app`, ADR-0068 has never ruled it, and decision batch #67 ruled option B, which ObjectUI shipped by dropping the binding. Deletes the `app` token from all six faces, leaving `features`, `os.user`, `data`, `current_user`, `record` and `user` in place and in order, and the "renderer behaviour, NOT contract-guaranteed" framing verbatim: - ui/page.zod.ts — "Ambient roots" docblock + the published `.describe()` on `PageComponentSchema.visibleWhen` - ui/action.zod.ts — param-level `visible` docblock + the action-level `visible` docblock, which stated the same claim unbackticked (`record/user/app/features`) - ui/component.zod.ts — the `page:tabs` ambient-root resolution example and its "also mounts the ambient …" sentence The two latter faces were invisible to the token-co-occurrence probe that found the first three; the probe here searched by the claim instead. Regenerates content/docs/references/ui/page.mdx, which republishes the `.describe()` verbatim, and adds a pin test over all six faces with lit and dark probe controls. No accept set moves: `SCOPE_ROOTS` is untouched and every schema parses exactly what it parsed before. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 134 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 897dfa7e7c7d4d214a6df5c110e0aedf50f07d8d && git checkout 897dfa7e7c7d4d214a6df5c110e0aedf50f07d8d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cca1dc0bfb1e380442670843606d6a0c7762f6ba 52bc5eaf1c8a982ab1546c8ccf8fd82a14625a6a && git checkout -B drift-repro cca1dc0bfb1e380442670843606d6a0c7762f6ba && git merge --no-ff 52bc5eaf1c8a982ab1546c8ccf8fd82a14625a6a
node scripts/docs-audit/affected-docs.mjs --json cca1dc0bfb1e380442670843606d6a0c7762f6ba
|
ACCEPT — head
|
⛔ BLOCKED at the enqueue gate — by a measured false positive, and the instrument's own text says the author does not pay for it
This PR is complete, correct and green. 34 check names, 30 success / 4 skipped / 0 failure / 0 pending; combined commit status The blockLanding pre-check ② is
The tell is false, measured: ⛔ Why the seat is not clearing it, and this is the script's own ruling
⇒ Every route out is closed by design except repairing the matcher:
Where that repair livesCard #17300 — filed by this seat earlier, from a different tell (T2, firing adversely on the retirement ledger's own generated rows, where the accept set had shrunk). This is the second instance and a distinct cause, posted there as evidence ( ⭐ A single-line key declaration whose value string is edited is indistinguishable, to a line-oriented tell, from a newly added key.
StateCard #17203 is now ⛔ Nothing about this PR needs changing. Escalated to the maintainer as a lane-wide block rather than a per-PR problem. Generated by Claude Code |
⛔ Seat correction: the block was mine, and it was wrong. Landing.
Two facts I did not establish before blocking
Why holding it was the wrong reading of the script's own rulingI quoted the C5 docblock as authority for blocking. Read again what it actually says:
⭐ Holding a correct, green PR indefinitely IS making the author pay for it — and the author here is the lane, which produces What the gate's purpose actually needed, and hasThe declaration limb is satisfied:
Diff over that file: 3 added / 3 removed — a pure edit. Lit control 2; dark control ⇒ The declaration is correct, the diff is what it says, and the instrument is wrong about one line. ⛔ I did not re-declare What changes, on the recordThe seat's landing pre-check list treated ⛔ Card #17203 goes back from Landing now: ready, then the queue. Governed-check exit 0 over the final six paths (lit control with Generated by Claude Code |
Fixes #17203
Clause-②: no — this does not widen an accept set or a public surface.
SCOPE_ROOTSis untouched, so a predicate namingappis accepted and rejected in precisely the places it was before; no schema shape, enum, alias table orstrictObjectshape changed;check:authorable-surface,check:api-surfaceandcheck:generatedall pass, which is the mechanical statement that the authorable and exported surfaces did not move; andcheck-widening-tells --declaration noagainst this diff exits 0. Deleting a token from a.describe()narrows what is advertised.needs:contract-reviewis not applied. (Full reasoning under Clause ② below.)Six prose faces of the UI schemas told an author that a CEL predicate could name
app— that the shipping renderer mounts it as an expression-scope root alongsidefeaturesandos.user. It does not, and it never contractually did. This deletes theapptoken from all six.features,os.user,data,current_user,recordanduserall stay, in place and in their existing order, and the "renderer behaviour, NOT contract-guaranteed" framing is unchanged.⛔
SCOPE_ROOTSinpackages/formula/src/cel-engine.tsis not touched. Widening it to fit the old prose is option A, ruled not adopted in decision batch #67 (2026-09-07), with its producer-side card #16420 closednot_planned.What I measured
Premises first. The card cites
page.zod.ts:303and:326; the claim comment warned those had already moved to:322/:345after PR #17257.origin/mainmoved again during this work, from501959b72to47863f4fb, so every site was located by content, never by line number. All three known premises reproduce at47863f4fb. Nothing was already fixed.The widened probe. Triage (comment 5608367315) stated plainly that its own probe was token co-occurrence —
`app`nearcurrent_user/mounts— and therefore could not see a site stating the same claim in different words. It namedaction.zod.ts:417's "same scope as the action-levelvisible" as evidence that a site describing the action-levelvisiblescope existed which the probe had missed.It was right. I searched by the claim ("what does this file say the expression scope roots are") rather than by the token shape, and found three more sites, two of which the original probe was structurally blind to:
ui/page.zod.ts"Ambient roots" docblockui/page.zod.tsPageComponentSchema.visibleWhen.describe()ui/action.zod.tsparam-levelvisibledocblockui/action.zod.tsaction-levelvisibledocblockui/component.zod.tsambient-root name-resolution exampleui/component.zod.ts"also mounts the ambient …" sentenceSite 4 is exactly the one triage predicted, and it shows why the probe shape mattered — it states the claim unbackticked and slash-separated:
No probe keyed on
`app`could ever have matched that string.Probe shapes, with their controls
A count is not a reading until you look at what it matched, so each probe carries controls.
T1 — backticked
appin scope-root prose acrosspackages/spec/src/ui. Before: 5 matches (sites 1, 2, 3, 5, 6). After: 0.T2 — unbackticked
appinside a slash/comma-separated root list acrosspackages/spec/src. Before: 1 match (site 4). After: 0.LIT control (a term known present, which must read > 0):
`features`reads 2 / 1 / 3 acrosspage.zod.ts/action.zod.ts/component.zod.ts, unchanged before and after — the sentences survived the token deletion rather than being deleted with it.os.userreads 2 inpage.zod.ts, unchanged.DARK control (a fabricated term, which must read 0):
`appzz_scope_root`reads 0 in all three files, before and after.The control that matters most here:
page.zod.tsstill contains`app`three times on two lines — every one of them the page type (`app`vs`utility`vs`blank`), a different word this card deliberately does not touch. That is the standing proof that a bareappprobe over this file cannot answer the scope-root question at all. It also caught a real error while I wrote the pin test:grep -canswers lines, not occurrences, and read 2 where the truth is 3. The pin asserts occurrences for that reason.Which faces are published
Two of the six are published; four are TSDoc that no generator reads. Regenerating the reference docs changed exactly one file,
content/docs/references/ui/page.mdx, which republishes the.describe()verbatim.That settles the p3 downgrade trigger triage left open — it asked whether these sites reach any generated authoring surface. They do.
.describe()is republished into the reference docs, so the carrier is not merely a source file and thepriority:p2grading stands.check:docsconfirms all 228 generated files are back in sync.Is the site list complete?
For the claim as stated in
packages/spec: yes, to the limit of two independent probes, one keyed on the token and one on the claim, whose post-edit readings are both 0 against healthy lit and dark controls.Repo-wide: I cannot claim completeness, and I will not. A probe can only match wordings I thought to look for — which is precisely how the first enumeration missed three sites. What I can state is what a repo-wide sweep for the claim in any wording turned up outside
packages/spec: two carriers, both handled explicitly below.Out of scope, filed
#17330 —
packages/lint/src/validate-expressions.tsdeclaresFIELD_RULE_AMBIENT_ROOTS = ['app'], meaning "bound at some evaluation site thoughSCOPE_ROOTSdoes not declare it". Batch #67 removed the binding, so that premise is now false, and a field-level*Whenreadingappstill earns the "renderer-mounted, wrong surface" diagnostic instead of the honest unbound-root one. It also anchors its docblock on the verypage.zod.tssection this PR edits, citing it as its in-repo source of truth — so this PR leaves that citation describing a sentence that no longer says what it quotes.Not fixed here: it is live lint behaviour in another package with its own test pinning the current value directly (
expect([...FIELD_RULE_AMBIENT_ROOTS]).toEqual(['app'])), so it is a deliberate change with a test to move, not a mechanical prose delete. Filed rather than folded.Noted, not filed:
packages/lint/CHANGELOG.mdandpackages/platform-objects/CHANGELOG.mdcarry the old claim in shipped release history. Changelogs are historical records and are correct as written — no carrier, deliberately untouched.Tests
New pin
packages/spec/src/ui/expression-scope-app-root.pin.test.tsholds all six faces — the published.describe()through schema introspection, the five TSDoc faces through anchored source reads — plus the lit and dark controls above as standing assertions, so a future zero here stays a reading.PageComponentSchemais aZodPipe, not aZodObject, because ADR-0089 D3a made it.strict().transform(…)..shapeisundefinedon it and every assertion reached through.shapewould throw rather than measure. The published face is read at.def.in.shape, with a comment saying why.Ablation (one-shot, not left in the tree). Mutated all six faces to re-introduce
app, from the committed state:git diff HEADnon-empty.appoccurrences where 3 is the truth). The 2 that stayed green are the dark controls, which is the correct direction for them.git hash-objectequality against the HEAD blob hash of all three files and an emptygit diff HEAD— not by an exit code. Wrapped intrap … EXIT INT TERMwith absolute paths, and restored withgit checkout HEAD --, never a baregit checkout --.Gates
Everything below ran at
52bc5eaf1c, the final commit, with each exit code landed to a file and read back — never through a pipe, never a bare$?.Ran green:
pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typechecktsconfig.test.jsonpnpm lint(repo-wide, exact argv)check:docscheck:generatedcheck:authorable-surfacecheck:api-surfacecheck:objectui-pin-citations.objectui-sha, 16 historical recordedcheck-spec-docblock-symbol-anchorscheck:nul-bytescheck:test-source-aliascheck:type-check-coveragecheck:published-filescheck:pm-widening-tellscheck-empty-changeset/check-changeset-no-majorcheck-closing-keyword-paritycheck:docs-audit-scope/check:doc-authoring/check:doc-frontmatter/check:docs-single-h1Lint is the full repo-wide population, not a narrowed run — it completed inside the foreground budget, so no narrowing claim is needed. For the record,
eslint.config.mjs:328states there is noparserOptions.projectand no typed rules, so this diff could not move an untouched file's verdict in any case.NOT MEASURED, called out as such:
check:docsfirst returned exit 1 —packages/spec/json-schema is older than packages/spec/src. That is a PREREQUISITE NOT MET, not a red gate:json-schema/is a gitignored build artifact whose mtime the ablation's restore leg bumped. Re-rangen:schema(verified noMERGE_HEAD, so no anchor rollback risk) andcheck:docsthen returned 0. The green above is that second run.os-verify-lockcall returned 99 (queue budget exhausted, lock held by a sibling'sspec build). That is NOT MEASURED, not red. The slotissue-17203kept its place, the interval went to lock-free work, and the command was re-run to a real verdict.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 106 commands for this change set. Running all of them locally would saturate the shared box, so the implicated subset above ran and the remainder is left to CI — including the 45 artifact-roster families, the 11 wide-population families, the 5 families taking a value from the workflow, and the 6 path-scheduled CI jobs, all of which that tool reports as outside its own runnable total.Clause ②
No — this does not widen an accept set or a public surface. I agree with the seat's reading, and measured rather than assumed it:
SCOPE_ROOTSis untouched, so a predicate namingappis accepted and rejected in precisely the places it was before.strictObjectshape changed. The diff is six comment/string edits, one regenerated doc, one changeset, one new test.check:authorable-surface,check:api-surfaceandcheck:generatedall pass, which is the mechanical statement that the authorable and exported surfaces did not move.check-widening-tells --declaration noagainst this diff: exit 0 — the 3 contract source files judged against a declared surface show no widening tell; the other 3 are a changeset, a generated doc and a test, none of which declares a contract.Deleting a token from a
.describe()narrows what is advertised.needs:contract-reviewis therefore not applied.Note for the reviewer
Comment 5609659954 (skills seat) records that the objectui twin spelled the surviving roots
data/features/current_user, inSCOPE_ROOTSdeclaration order, and observed that site 3's parenthesis "mirrors to that string exactly" — while explicitly deferring the ordering question to the spec seat. The dispatch instruction for this card is that the surviving roots "stay and stay in place", so I deleted only theapptoken and reordered nothing. Site 3 therefore readscurrent_user/data/features: the same three roots as objectui, in this repo's existing order. Flagging it so the seat can confirm that is the intended call rather than an oversight.I also left the
.objectui-shapin190fbd01d061in the page.zod docblock untouched.check:objectui-pin-citationsgrades it a historical citation (recorded, not checked), and the sentence it supports stays true for the roots that remain.⛔ Left draft, not enqueued, no auto-merge — the seat flips it after review.
Generated by Claude Code
Generated by Claude Code