Skip to content

fix(skills): stop naming app as an expression root in two published guides - #8906

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8810-app-root-skills
Sep 10, 2026
Merged

fix(skills): stop naming app as an expression root in two published guides#8906
os-zhuang merged 1 commit into
mainfrom
claude/issue-8810-app-root-skills

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8810

Two lines in the published skills tree stopped being true when objectui#8164 landed the
objectui#8155 ruling: app is no longer an expression/predicate root, because neither ADR-0068
nor @objectstack/formula's SCOPE_ROOTS ever declared one. An unresolvable visibility predicate
fails open on these surfaces, so a reader who followed either line authored a gate that
silently passes — and one of the two lines is in the permissions guide, which is the worst place
to hand somebody a silently-open gate.

Diff is exactly two lines, both under skills/**. Nothing else.

Before / after

skills/objectui/guides/auth-permissions.md:312 — a row in the table under
### Expression scope: which roots resolve:

-| `app`, `features` | the ambient host scope | `${features.multiOrgEnabled}` |
+| `features` | the ambient host scope | `${features.multiOrgEnabled}` |

features is a root and stays, with its example unchanged.

skills/objectui/rules/protocol.md:283 — the visible root list on an action param:

 - `required` blocks submit; `visible` is a CEL predicate
-  (`features` / `current_user` / `app` / `data`) that hides the param.
+  (`data` / `features` / `current_user`) that hides the param.

The card's own test, answered once per line before editing

objectui#8810 and triage both make this the gating question: is the line describing the React
context value
(which still carries app — fine as written) or the predicate scope (fix)?
Answered independently here, and both answers agree with triage's read.

Line 1, auth-permissions.md:312 — PREDICATE SCOPE ⇒ fix. The table's own lead-in sentence is
`SchemaRenderer` evaluates every schema expression against a fixed scope:, its first column
header is Root, and every sibling row is an evaluator root carrying a ${...} example. That
"fixed scope" is buildExpressionScope, measured on this branch at
packages/app-shell/src/providers/ExpressionProvider.tsx:118:

return { current_user: user, user, ctx: { user }, os: { user }, data, features };

No app. A React context value has no rows in a table of expression roots.

Line 2, protocol.md:283 — PREDICATE SCOPE ⇒ fix. The sentence names the mechanism literally
(`visible` is a CEL predicate) and the parenthesis is that predicate's root list. The context
value is not a CEL scope, so this line cannot be describing it.

And the counter-example, which is why the test is worth asking. The context value genuinely
does still carry app — same file, ExpressionProvider returns
{ user, app, data, features, evaluator }, with the in-repo comment
"app is still published on the context value — DashboardView reads it as a plain value. It is
NOT handed to the evaluator". So the three surfaces that describe that are correct as they stand
and are deliberately untouched:

  • skills/objectui/rules/protocol.md:215 and skills/objectui/guides/schema-expressions.md:305
    the nested bind path app.settings.users resolving against dataSource. Data binding, a
    different mechanism; bind is not expression-evaluated at all.
  • content/docs/guide/console-architecture.md:44 — the ASCII diagram of the context value. Also
    outside skills/**, and a mixed diff would change this PR's landing route.

Wording for the upstream twin (objectstack#17203)

That card is open, spec lane, still queued: its three sites are not reworded yet, so this PR lands
first and states the spelling plainly so the two repos end up with one truth rather than two.

The chosen rule: spell the surviving roots in the order SCOPE_ROOTS declares them. Measured,
not recalled — read through objectui's installed dependency rather than any checkout of the
producer, since ^17.0.0 is what this repo actually consumes. Resolved to
@objectstack/formula@17.4.0, dist/index.d.ts:215, quoted whole:

declare const SCOPE_ROOTS: readonly ["record", "previous", "input", "output", "os", "vars", "variables", "automation", "context", "args", "item", "env", "user", "step", "result", "trigger", "event", "payload", "data", "params", "config", "settings", "ctx", "features", "parent", "current", "current_user"];

app is absent. Positive controls present in the same read: data (index 18), features (23),
current_user (26), plus user (12), os (4), ctx (22) — so the absence is a reading of a live
list, not an empty match.

⇒ The canonical spelling for a list of these three is data / features / current_user, and
that is what protocol.md:283 now carries. action.zod.ts:417's parenthesis upstream is the same
three roots plus app in a different order (current_user / app / data / features), so it
mirrors to exactly that string. ⚠️ page.zod.ts:303 / :326 are not a clean mirror: their list
is app, features, os.user, and os.user is a path under the os root rather than a root
itself, so the ordering rule does not settle it — that stays the spec seat's call, and only the
app token is common ground. ⛔ Not folded into one card: two repos, two landing routes.

Gates

No package source is touched, so there is no build closure to run and no repo-wide pnpm lint /
pnpm test owed here — those are CI's. objectui has no dispatch-gates.mjs, so the list below was
derived by hand from package.json scripts plus every .github/workflows/*.yml trigger, then run
in full. Each gate's own verdict line, not a bare exit code:

gate exit verdict
check-governed-queue-guard.mjs --self-test 0 OK check-governed-queue-guard self-test: 132 cases pass
check-governed-queue-guard.mjs --test (both paths) 3 ⛔ GOVERNED — 2 of 2 path(s) are on a governed surface: skills/** x2 — the published skills catalog
check-skills-paths.mjs 0 ✅ OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined)
check-skill-examples.mjs 0 Every marked skill example holds up against the built types. (14/14 ts fences, 70/70 json, 0 failed)
check-skill-eval-tokens.mjs --self-test 0 OK ... 29 cases pass
check-skill-eval-tokens.mjs 0 Every must_contain token is taught by its own skill bundle. (125 tokens)
check-doc-fence-languages.mjs --self-test 0 26 cases pass
check-doc-fence-languages.mjs 0 ✅ every TypeScript block in 227 document(s) is fenced ts/tsx/typescript
check-control-bytes.mjs 0 ✅ OK (scanned 7087 tracked text file(s); skipped 85 binary)
check-shell-escape-residue.mjs 0 ✅ OK (5/5 root(s) resolved ... skills: 16 file(s), 210 fence(s))
check-doc-links.mjs 0 Links are valid across 17 scan roots.
check-changeset-presence.mjs 0 see below
check-upstream-port-parity.mjs 0 ✓ 3 ported file(s) match ... modulo their declared divergences
check-bash32-floor.mjs 0 ✓ 13 tracked shell file(s) ... name no bash 4+ construct

--test exiting 3 is the GOVERNED answer, not a failure — it is this PR's routing verdict and
the reason the PR is parked as a draft.

Changeset. objectui has no skip-changeset label, so the gate's own verdict is the
declaration, quoted verbatim:

Compared the working tree with f52a9d7ad (merge-base with origin/main): 2 file(s) changed,
0 of them published source of a package the release covers, 0 of them a manifest whose published
contract moved, 0 under a package changesets ignores, 0 changeset(s) added.
✅ No source or published contract of a released package changed in this range, so no changeset
is owed.

⇒ No changeset written, and ⛔ no label applied.

check-skill-examples needed a real build (its --build-filter derives 12 packages). Run
through the container's shared heavy-verify lock: 29/29 turbo tasks successful,
VERDICT command-exit 0 · held the lock 185s · waited 0s, and the gate then ran green against the
freshly built types.

Acceptance notes

  • Landing route. Governed (skills/** = skills-catalog). Draft at the human terminal:
    ⛔ not flipped ready, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no approving review from this seat.
  • Assumption checks the dispatch asked for, all three measured.
    • Line numbers: auth-permissions.md:312 and protocol.md:283 are still exact on f52a9d7a,
      despite d65b2baa being the newest touch of the guide. No drift to correct.
    • Fences: neither edited line is inside one. Counted mechanically — 14 fences in
      auth-permissions.md (line 312 outside all of them; nearest is 296–302), 13 in protocol.md
      (line 283 outside; nearest is 266–275). Both nearest fences are byte-unchanged, and
      check-skill-examples confirms the whole marked population still compiles.
    • Gate list: the dispatch's precedent list was a subset. It did not name the four gates that
      actually read skills/** markdown — check-skills-paths, check-skill-examples,
      check-skill-eval-tokens, check-doc-fence-languages. All four were derived from the workflow
      triggers and run; all four are green. Reported so the next skills/** card starts from the
      full list.
  • Noted, not filed — skills/objectui/guides/page-builder.md:329 reads
    "reachable only through the page / app / utility / home / record" and is a fourth
    mechanism again: those are page kinds in a routing sentence, not expression roots. Correct as
    it stands, out of this card's bounds, and recorded only so the next reader who greps `app`
    across this tree does not have to re-derive it. Carrier if it ever needs one: whoever next edits
    page-builder.md — objectui#8865 is open on that file today, but on a disjoint concern.
  • Reversibility. Two lines, no code, no generated artifact. Revert is the diff backwards.

维护者速读(草稿)

改了什么 —— 发布给使用者的 skills 树里,两行文档不再把 app 列为可以在表达式/谓词里读取的根。
一行是权限指南里「哪些根能解析」那张表,一行是 protocol.md 里 action 参数 visible 谓词的根清单。
只动这两行,不动代码,不动任何别的文件。

为什么改 —— objectui#8155 已裁定:引擎的 SCOPE_ROOTS 是契约,前端向它对齐,而那张表里从来没有
app;PR #8164 已经把绑定摘掉了。文档没跟上,于是已发布的指南在教一个运行时不兑现的能力。后果不是
报错而是静默放行:照着写的可见性谓词解析不到,在这两个面上读作「是」,门就默默开着。客户是照这棵
skills 树写元数据的,爆炸半径比仓内文档大;而其中一行恰好就在讲权限的那份指南里 —— 那是最不该给出
一个静默放行示例的位置。

风险与代价(含回滚) —— 风险很低:两行散文,没有代码路径、没有生成产物、没有 changeset(门禁自己
判定不欠)。已发布的 features 根和它的例子原样保留,只删 app 一个词。表达式根清单的次序改成按
SCOPE_ROOTS 的声明次序写,是为了让上游 objectstack#17203 能照抄同一份拼写,不至于两个仓分裂成两份
真理。回滚就是把这个 diff 反向打回去,没有任何后续依赖。

席位意见 ——

你要做的 —— 这是受管面(skills/**),按规矩停在 draft 等你人工合并;你的那次合并动作本身就是审核
记录。另外有一件跨仓的事需要你知道:上游 objectstack#17203 的三处还没改,本 PR 先落地并在正文里
写明了选定的拼写(data / features / current_user),供 spec 席位镜像;其中 page.zod.ts 那两处
os.user,不是干净的镜像,留给 spec 席位自己判。


Generated by Claude Code

… guides

`app` is not a root the runtime binds. objectui#8155 ruled that
`@objectstack/formula`'s `SCOPE_ROOTS` is the contract and app-shell aligns to
it; PR #8164 shipped that -- `buildExpressionScope` in
`packages/app-shell/src/providers/ExpressionProvider.tsx` returns
`{ current_user, user, ctx:{user}, os:{user}, data, features }` and no `app`.

Two lines in the PUBLISHED skills tree still taught it, and an unresolvable
visibility predicate FAILS OPEN on these surfaces, so a reader who followed
them authored a gate that silently passes:

  guides/auth-permissions.md -- the "Expression scope: which roots resolve"
  table listed `app`, `features` on one row. `features` IS a root and stays,
  with its `${features.multiOrgEnabled}` example; `app` is dropped.

  rules/protocol.md -- the `visible` CEL-predicate root list beside an action
  param read (`features` / `current_user` / `app` / `data`).

Surviving roots are spelled in the order `SCOPE_ROOTS` declares them, measured
against the installed `@objectstack/formula@17.4.0` (`dist/index.d.ts:215`,
the version objectui's `^17.0.0` resolves to): `data` at index 18, `features`
at 23, `current_user` at 26 -- so `data` / `features` / `current_user`. The
upstream twin objectstack#17203 mirrors that spelling.

Both lines were tested against the card's own criterion before editing: each
describes the PREDICATE SCOPE, not the React context value. The context value
does still carry `app` (`ExpressionProvider` returns
`{ user, app, data, features, evaluator }` -- `DashboardView` reads it as a
plain value), so the surfaces that describe THAT are correct as they stand and
are untouched: the `app.settings.users` nested-`bind` mentions in
`rules/protocol.md:215` and `guides/schema-expressions.md:305` (data binding
against `dataSource`, a different mechanism) and the
`content/docs/guide/console-architecture.md:44` ASCII diagram.

Claude-Session: https://claude.ai/code/session_01MoTv7pn338AZ71owsp19gQ
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

维护者速读

改了什么 — 发布给使用者的 skills 树里两行文档:权限指南里「哪些根能解析」那张表删掉 app(features 与其示例原样保留);protocol.md 里 action 参数 visible 谓词的根清单改为 data / features / current_user,按引擎 SCOPE_ROOTS 的声明次序写。只动这两行,不动代码、不动其它文件;讲数据绑定的两处 app.settings.users 与架构图里的 context value 都没动,因为那两个机制确实仍带 app

为什么改 — objectui#8155 已裁定引擎的 SCOPE_ROOTS 是契约、前端向它对齐,PR #8164 已把 app 从表达式作用域摘掉;文档没跟上,于是已发布的指南在教一个运行时不兑现的根。后果是静默放行而不是报错:照着写的可见性谓词解析不到,读作「是」,门默默开着 —— 而其中一行恰在讲权限的指南里。分诊席复核定级 p2,本席按车道自分诊准入后派出。

风险与代价(含回滚) — 两行散文,无代码路径、无生成产物;changeset 门禁自判不欠。次序按 SCOPE_ROOTS 写是为了让上游 objectstack#17203 的三处能照抄同一份拼写,避免两仓分裂成两份真理(其中 page.zod.ts 两处含 os.user,不是干净镜像,留 spec 席自判)。回滚 = 反向打回这个 diff。

席位意见 — 本席以契约复审档复核通过:两行逐字核过、三处围栏未动、受管判定与 skills 门禁在该提交上全绿、CI 29 项全绿、合并无冲突。建议合并。

你要做的 — 受管面走人工合并:看一眼这两行,认可即批准并合并本 PR。是否合并?


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 10, 2026 01:07
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 829a706 Sep 10, 2026
31 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8810-app-root-skills branch September 10, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(skills): two published guides name app as an expression scope root, which the #8155 ruling removes

3 participants