Repository navigation
finding(skills): two published guides name app as an expression scope root, which the #8155 ruling removes #8810
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 9, 2026 分诊
documentationfindingdomain:skillspriority:p2pm:queue· typeBug· 未指派复核于 objectui
origin/mainb686ebf7d。两处坐标逐字命中,判断成立。复核
skills/objectui/guides/auth-permissions.md表行,逐字:|app,features| the ambient host scope |${features.multiOrgEnabled}|⇒ 确把app列为可读的表达式根。skills/objectui/rules/protocol.md:283,逐字:「visibleis a CEL predicate (features/current_user/app/data) that hides the param」。- 受管面判定成立:
scripts/check-governed-queue-guard.mjs:230逐字{ id: 'skills-catalog', prefix: 'skills/', glob: 'skills/**', what: 'the published skills catalog' }。⇒ 卡片「不折进 PR fix(app-shell): lint conditional-formatting in the record scope, and align the predicate scope to the engine's root vocabulary #8164 是因为会改变落地路径,不是嫌麻烦」的理由成立。 - 上游侧我今天单独测过:
@objectstack/formula的SCOPE_ROOTS(packages/formula/src/cel-engine.ts:94起)整表不含'app',阳性对照'ctx','features','parent','current','current_user'逐一在位。⇒ 「它不是一个根」是读数,不是转述。
⭐ 跨仓孪生:
protocol.md:283与 objectstackaction.zod.ts:417是同一句话我今天在上游分诊 objectstack**#17203** 时,按那张卡的嘱托扫了
packages/spec,扫出它没点到的第三处:packages/spec/src/ui/action.zod.ts:417——* (current_user/app/data/features). When it evaluates false the …⇒ 与本卡
protocol.md:283的括号内容是同一份清单,只是次序不同。这一类跨两个仓,上游三处(page.zod.ts:303/:326、action.zod.ts:417)+ 本仓两处 = 目前已测 5 处。⛔ 两仓各自落地,不合并成一张卡 —— 按
:199「domain:devx(工程面)与domain:spec(契约面)跨仓归各自车道」,且本仓这两处在受管面上,落地路径与上游完全不同。⇒ 已在两边互相点名,不设跨仓父卡。⚠️ 接手者请先看 objectstack#17203 的当前状态再动手:若上游那三处的措辞已被改定,本仓两处应当采同一措辞,⛔ 不要各写各的,否则这一类会分裂成两份真理。定级
priority:p2- 载体是已发布的 skills 树 —— 卡片说得对:「what customers author against」,比仓内文档的爆炸半径大。
- 后果是静默 fail-open:无法解析的可见性谓词读作"是",于是照着写的人得到一个默默放行的门,而不是一个报错。⛔ 且
auth-permissions.md正是讲权限的那份指南 —— 读者在那个语境里最不该拿到一个静默放行的例子。 - 与上游 #17203 同级(p2),同一类不该在两个仓有两个定级。
⛔ 不是 p1:
SCOPE_ROOTS与 ADR-0068 从未声明过app,契约面本身是对的;这是文档在教一个不存在的能力,不是运行时行为出错。到达面(有多少客户已照此写了
app.谓词)⛔ 未测量 —— 给降级触发器。车道
domain:skillsskills/**在车道表:249的domain:skills行里被逐字点名(「.claude/skills/**(含本文件)+skills/**」)。⭐ 这是车道表少数直接答得出的路径之一 —— 与我今天在别处遇到的docs/adr/**、.github/workflows/**无行可依不同。⇒ 归属无歧义。派发约束(受管面,⛔ 与普通卡不同)
本卡的 PR 只能触
skills/**两个文件:- ⛔ 不得与任何代码或
content/docs/**改动同 PR(混合 diff 会把整个 PR 变成受管的);卡片已记明非受管的那三处更正已在 PR fix(app-shell): lint conditional-formatting in the record scope, and align the predicate scope to the engine's root vocabulary #8164 里做掉了。 - ⛔ agent 席永不翻 ready、永不入队、永不挂 auto-merge、永不批准;走人工合并路径。
- 动手前跑
node scripts/check-governed-queue-guard.mjs --test确认路径面判定,⛔ 不凭记忆。
⛔ 卡片划的两条界,复核后维持
- ⛔ 不要顺手扫掉
skills/objectui/guides/schema-expressions.md:305与skills/objectui/rules/protocol.md:215—— 那两处说的是app.settings.users这种嵌套 bind 路径对dataSource解析,是数据绑定不是谓词作用域,不受影响。 - ⛔ 不要动
content/docs/guide/console-architecture.md:44的 ASCII 图 —— 它画的是 React context value,那个确实仍带app。
⭐ 并把卡片最后那句判据升为动手前的必做步骤:「if either turns out to be describing the context value rather than the predicate scope, it is fine as written」。⇒ 改每一处之前先答这一句;两处我读下来都是谓词作用域(一处在"表达式根"表里,一处紧跟
is a CEL predicate),但接手者要自己再答一遍。重新定级触发器(双向)
- 升 p1:若测到已发布的客户元数据或本仓示例里存在
app.谓词落在 fail-open 的面上 —— 即"会教坏人"变成"已经教坏了"。 - 降 p3:若上游 #17203 被裁为其它方向(例如恢复
app绑定),本卡前提随之移动 —— 届时重读再定,⛔ 不要沿用本次定级。
Generated by Claude Code
First-touch grading (skills lane self-triage, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-09T22:2xZ): admitted, class (c) — a published guide teaches an expression root (app) that the runtime does not bind, and an unresolvable visibility predicate fails OPEN, so a reader who follows it authors a gate that silently passes. Re-check on objectuiorigin/mainf52a9d7aat 22:17Z: both lines hit verbatim (auth-permissions.md:312table row;protocol.md:283predicate list); PR #8164 (the #8155 ruling) MERGED 12:16Z2615505d, so the premise stands; no open objectui PR touches either file (23 open PRs' file lists read; PR #8865 touchespage-builder.mdonly). Upstream twin objectstack#17203 is stillpm:queuein the spec lane (unassigned) — its threepackages/specsites are NOT yet reworded, so this card lands first and states its wording plainly for the spec seat to mirror (triage's⚠️ read both ways). Triage's grade stands:priority:p2, type Bug,pm:queue;findingleaves in this write (graded). Dispatched now at the default tier (published prose, no contract accept/reject change), reviewed in the seat atCONTRACT_REVIEW_TIER; governed (skills/**,skills-catalog) ⇒ draft at the human terminal.
Generated by Claude Code
Claim: PM loop round 6 — two lines in the published skills tree stop naming
appas an expression/predicate root, exactly as triage's 复核 bounds it:skills/objectui/guides/auth-permissions.md(the 「app,features| the ambient host scope」 table row) andskills/objectui/rules/protocol.md(the CEL-predicate root list besidevisible); before each edit the dev answers the card's own test — context value (fine as written) vs predicate scope (fix) — and records the answer. ⛔ The two data-binding mentions (schema-expressions.md:305,protocol.md:215—app.settings.usersresolving againstdataSource) and theconsole-architecture.md:44ASCII diagram stay untouched; ⛔ nothing outsideskills/**in the diff (a mixed diff changes the landing route). Wording: state the chosen spelling in the PR body so the spec seat's upstream twin (objectstack#17203,page.zod.ts:303/:326,action.zod.ts:417, still queued) can mirror it — one truth across the two repos.
Session:session_01MoTv7pn338AZ71owsp19gQ
Branch:claude/issue-8810-app-root-skills
Worktree:objectui-issue-8810
Domain:domain:skills(objectui)
File surface:skills/objectui/guides/auth-permissions.md+skills/objectui/rules/protocol.mdonly
Container & model:S (two published-prose lines, governed),mode:subagent,model: opus — objectui has no dispatch-gates.mjs (no path-derived tier reading); judged from content: published guidance corrected to the ruled contract, no accept/reject change ⇒ default-tier build, reviewed in the seat at claude-fable-5-1
Clause-②: no
Thread-read: 5609050376 (triage's 复核, the only comment; body read in full)
Serial constraints cleared: PR #8164 MERGED 12:16Z (2615505d) — the premise; no open objectui PR touches either file (23 open PRs read 22:17Z; PR #8865 touchesskills/objectui/guides/page-builder.md, disjoint); newest touchesd65b2baa/af212e60; objectui#8840 (PR #8848,CLAUDE.md, at the terminal) disjoint; objectstack#17203 is the upstream twin, ⛔ not a fold (two repos, two landing routes, per triage).
Generated by Claude Code
os-dev-report
{ "issue": 8810, "status": "done", "branch": "claude/issue-8810-app-root-skills", "pr": "https://github.com/objectstack-ai/objectui/pull/8906", "premise_still_valid": true, "summary": "Both card coordinates hit verbatim on origin/main f52a9d7a, so the premise stands. Two lines under skills/** stop naming `app` as an expression/predicate root: auth-permissions.md:312 ('| `app`, `features` | the ambient host scope |' becomes '| `features` | the ambient host scope |', example untouched) and protocol.md:283 ('(`features` / `current_user` / `app` / `data`)' becomes '(`data` / `features` / `current_user`)'). Diff is exactly 2 files, +2/-2, nothing outside skills/**. Surviving roots are spelled in SCOPE_ROOTS declaration order, measured against objectui's INSTALLED @objectstack/formula@17.4.0 (dist/index.d.ts:215; `app` absent, positive controls data=18, features=23, current_user=26 present) so objectstack#17203 can mirror one truth; the PR body carries that spelling. The three fenced-off surfaces are untouched: protocol.md:215, schema-expressions.md:305 (nested `bind` against dataSource) and content/docs/guide/console-architecture.md:44 (context value). Governed (skills-catalog): parked as DRAFT at the human terminal, not ready, not enqueued, no auto-merge, no approving review. No changeset written and no label applied - the changeset gate itself ruled none owed. Assignee arrived already set by the PM and was never written by this seat.", "test_answer_auth_permissions_312": "PREDICATE SCOPE => fix. The row sits under '### Expression scope: which roots resolve', whose lead-in reads '`SchemaRenderer` evaluates every schema expression against a fixed scope:', its first column header is `Root`, and every sibling row is an evaluator root with a dollar-brace example. That fixed scope is buildExpressionScope, measured at packages/app-shell/src/providers/ExpressionProvider.tsx:118 returning { current_user: user, user, ctx: { user }, os: { user }, data, features } - no `app`.", "test_answer_protocol_283": "PREDICATE SCOPE => fix. The sentence names the mechanism literally ('`visible` is a CEL predicate') and the parenthesis IS that predicate's root list; a React context value is not a CEL scope. Counter-check that keeps the test honest: the context value genuinely still carries `app` - same file, ExpressionProvider returns { user, app, data, features, evaluator } with the in-repo comment '`app` is still published on the context value ... It is NOT handed to the evaluator' - which is exactly why console-architecture.md:44 and the two `bind` mentions stay untouched.", "files_changed": [ "skills/objectui/guides/auth-permissions.md", "skills/objectui/rules/protocol.md" ], "gates": [ "check-governed-queue-guard.mjs --self-test => exit 0: 'OK check-governed-queue-guard self-test: 132 cases pass'", "check-governed-queue-guard.mjs --test (both paths) => exit 3: 'GOVERNED - 2 of 2 path(s) are on a governed surface: skills/** x2 - the published skills catalog'. 3 is the GOVERNED verdict, not a failure.", "check-skills-paths.mjs => exit 0: 'OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined)'", "check-skill-examples.mjs => exit 0: 'Every marked skill example holds up against the built types.' (14/14 ts fences judged, 0 failed; 70/70 json fences)", "check-skill-eval-tokens.mjs --self-test => exit 0: '29 cases pass'", "check-skill-eval-tokens.mjs => exit 0: 'Every must_contain token is taught by its own skill bundle.' (125 tokens, 0 red)", "check-doc-fence-languages.mjs --self-test => exit 0: '26 cases pass'", "check-doc-fence-languages.mjs => exit 0: 'every TypeScript block in 227 document(s) is fenced ts/tsx/typescript'", "check-control-bytes.mjs => exit 0: 'OK (scanned 7087 tracked text file(s); skipped 85 binary)'", "check-shell-escape-residue.mjs => exit 0: 'OK (5/5 root(s) resolved ... skills: 16 file(s), 210 fence(s))'", "check-doc-links.mjs => exit 0: 'Links are valid across 17 scan roots.'", "check-changeset-presence.mjs => exit 0: '2 file(s) changed, 0 of them published source of a package the release covers ... No source or published contract of a released package changed in this range, so no changeset is owed.'", "check-upstream-port-parity.mjs => exit 0: '3 ported file(s) match objectstack-ai/objectstack modulo their declared divergences'", "check-bash32-floor.mjs => exit 0: '13 tracked shell file(s) ... name no bash 4+ construct'" ], "tests": "No package source touched => no build closure and no package test/typecheck owed; repo-wide pnpm lint / pnpm test are CI's. All 14 gate invocations above ran in this worktree, each exit code captured by redirecting to a file BEFORE reading (never through a pipe), and each verdict quoted from the gate's own printed line. check-skill-examples needed a real build: its --build-filter derived 12 packages, run through the container's shared heavy-verify lock (OS_VERIFY_LOCK_SLOT=os-dev-8810) - 'VERDICT command-exit 0 - held the lock 185s - waited 0s', 29/29 turbo tasks successful - and the gate then ran green against the freshly built types. No ablation applies: the change is two prose lines with no guard to mutate.", "assumption_checks": [ "A (no dispatch-gates.mjs; derive by hand) - CONFIRMED, and 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 derived from the workflow triggers and run; all four green. Also: check-doc-links.mjs and check-changeset-presence.mjs exist only as workflow-invoked scripts, with no package.json alias - invoke them as 'node scripts/...'.", "B (fenced examples near the edited lines) - MEASURED, negative. Neither edited line is inside a fence: 14 fences in auth-permissions.md, line 312 outside all of them (nearest 296-302); 13 in protocol.md, line 283 outside (nearest 266-275). Both nearest fences are byte-unchanged, and check-skill-examples confirms the whole marked population still compiles.", "C (same-day churn may have moved the line numbers) - MEASURED, no drift. auth-permissions.md:312 and protocol.md:283 are still exact on f52a9d7a despite d65b2baa being the newest touch of the guide." ], "deviations": [ "CHANNEL SWITCH, declared: the repo-scoped REST probe could not run in this seat - curl to api.github.com was refused by the harness permission classifier (a local denial, NOT a 403 from the proxy), so no REST channel exists to prefer here. All card reads therefore went through MCP. The PM's 22:17Z dedup reading was used as fact and only the increment re-checked.", "EDIT NO-OP, reported rather than silently retried: the first perl -i pass on auth-permissions.md matched nothing (the dollar-brace in the pattern broke the escaping) and exited 0. The post-edit grep for the injected text caught it - injected absent, deleted text still present - so that reading was discarded and the edit redone as a literal python replacement asserting exactly one occurrence of the old string and zero of the new before writing. The protocol.md leg of that same pass DID land and was verified the same way." ], "mcp_calls": "6 - issue_read get (8810), issue_read get_comments (8810), issue_read get (objectstack#17203), create_pull_request, pull_request_read get (body read-back), add_issue_comment (this report). No search calls: the PM's dedup reading covered it.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: skills/objectui/guides/page-builder.md:329 reads 'reachable only through the `page` / `app` / `utility` / `home` / `record`' - a FOURTH mechanism, page kinds in a routing sentence, not expression roots and not the `bind` path either. Correct as it stands, so there is nothing to fix; recorded only so the next reader who greps for `app` across this tree does not have to re-derive it. Carrier: whoever next edits page-builder.md - objectui#8865 is open on that file today, but on a disjoint concern.", "noted, not filed: objectui's AGENTS.md states no commit-trailer convention of its own, so the model-free trailer pair was taken from objectstack's AGENTS.md (the sibling that does state it). Landed objectui history carries model-naming Co-Authored-By trailers; this commit uses the model-free pair. Not a defect, and no PR or person is queued to touch that text - carrier: none." ] }
Generated by Claude Code
ACCEPT — objectui#8810 (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, review taken 22:30–22:34Z atclaude-fable-5-1; seat tier read byget_sessionthis shift)Delivery: draft PR #8906, head
6aa48457, two files+2/−2(skills/objectui/guides/auth-permissions.md,skills/objectui/rules/protocol.md); os-dev-report 5609622528 read to the last field; the diff read in full.Verified on the branch against triage's bounds (5609050376), not on the report:
- Scope — exactly the two lines: the expression-roots table row drops
appand keepsfeatureswith its example; thevisiblepredicate list readsdata/features/current_userinSCOPE_ROOTSdeclaration order (measured by the dev against the installed@objectstack/formula@17.4.0dist/index.d.ts;appabsent, controls present). Nothing outsideskills/**in the diff (content/diff empty). - The two fences and the diagram stand —
protocol.md:215andschema-expressions.md:305(theapp.settings.usersbind path) byte-identical on the head;console-architecture.md:44untouched. Both pre-edit test answers recorded in the PR body (predicate scope on both, withExpressionProvider.tsx:118'sbuildExpressionScopeas the measurement and the context value'sappnamed as the reason the fences stay). - Governed —
check-governed-queue-guard.mjs --teston the head: exit 3, 「GOVERNED — 2 of 2 path(s) … the published skills catalog」; skills gates re-run here on the head:check-skills-paths✓,check-skill-eval-tokens✓ (125 tokens),check-doc-links✓,check-doc-fence-languages✓,check-changeset-presence「no changeset is owed」 (objectui has noskip-changesetlabel — the gate's verdict is the declaration, no label applied).git merge-tree --write-tree origin/main(f52a9d7a) × head: clean. Commit trailer is the model-free pair; no model identifier in commit, PR body or report. CI on6aa48457: 29 distinct checks green at 22:33Z (Skill Example Check included). - Assumption A: the precedent gate list was a subset — the four
skills/**readers (check-skills-paths,check-skill-examples,check-skill-eval-tokens,check-doc-fence-languages) are the full list for the nextskills/**card; recorded on the seat post.
Verdict: ACCEPT. Governed (
skills/**) ⇒ four-piece placed on PR #8906:needs-user-decision, the 终稿「维护者速读」, reviews requested from os-zhuang and hotlong; this seat does not flip, queue, arm or approve. Cross-repo: the chosen spelling (data/features/current_user) is pointed at from objectstack#17203 for the spec seat; ⛔ not a fold (two repos, two landing routes — triage's ruling).
Generated by Claude Code
- Scope — exactly the two lines: the expression-roots table row drops
Landing record — PR #8906 (
Fixesthis card; two publishedskills/objectui/**lines stop namingappas an expression root (data/features/current_userinSCOPE_ROOTSorder)) MERGED 2026-09-10T01:25:04Z through the merge queue, squash commit829a7067onorigin/main. GOVERNED: approved, flipped ready and enqueued by os-zhuang (merged_byos-zhuang) — this seat did none of the three. In-seat review ACCEPT 5609659277. Recorded by the skills seat, sessionsession_01MoTv7pn338AZ71owsp19gQ, at 2026-09-10T01:3xZ.Same stroke:
pm:dispatchedremoved and the assignee (os-justin, this seat) cleared on this auto-closed card;documentationandpriority:p2stay. The upstream twin objectstack-ai/objectstack#17203 (spec lane) carries the wording pointer (5609659954).
Generated by Claude Code
Filed unassigned and unlabelled for PM triage by the #7727 dev (session
session_01YBWFb5YgMU5dw8p2VKj16S), found while implementing the #8155 ruling in PR #8164. ⛔ Deliberately not folded into that PR — see "Why not folded in" below; the reason is a merge-route change, not effort.What
PR #8164 executes the #8155 ruling:
appis removed from app-shell's expression scope, because neither ADR-0068 nor@objectstack/formula'sSCOPE_ROOTSdeclares such a root. After it lands, two lines in the published skills tree state something false:skills/objectui/guides/auth-permissions.md:312— a table row reading, in effect, thatappandfeaturescome from "the ambient host scope".skills/objectui/rules/protocol.md:283— prose describing a predicate overfeatures/current_user/app/data.Both name
appas a readable expression/predicate root. It is not one any more.Why it matters more than a typo
This is the same defect class the #8155 ruling exists to remove: a surface advertising a root that nothing binds. Unresolvable visibility predicates fail open, so a reader who follows this guidance writes a gate that silently reads as "yes" rather than erroring. The published skills tree is what customers author against, so the blast radius is larger than the in-repo docs.
Not in scope, and still true
Two other
app.mentions in the same tree are a different mechanism and must not be swept up:skills/objectui/guides/schema-expressions.md:305andskills/objectui/rules/protocol.md:215— both read, in effect, that a nestedbindpath of the formapp.settings.usersresolves againstdataSource. That is data binding, not the predicate scope, and it is unaffected.skills/objectui/guides/app-composition.mdmentions of*.app.tsandapp.zod.tsare filenames.Why not folded into PR #8164
skills/**is a governed surface (AGENTS.md, andscripts/check-governed-queue-guard.mjs'sGOVERNED_SURFACES— theskills-catalogentry). A PR whose diff touches one governed path is governed entirely, and governed PRs stop in draft for a human to merge rather than taking the normal review-and-queue route.PR #8164 is currently not governed — verified:
node scripts/check-governed-queue-guard.mjs --testover its 10 paths returns NOT GOVERNED. Adding these two files would move it onto the human-merge path and change how it lands. That is a routing decision for the PM and the maintainer, not something for the implementing dev to take unilaterally, so it is filed rather than folded.The equivalent non-governed corrections were made in #8164:
content/docs/core/enhanced-actions.mdx,content/docs/guide/console-architecture.mdandcontent/docs/plugins/plugin-form.mdx.Note for whoever picks this up
The ASCII diagram at
content/docs/guide/console-architecture.md:44showsExpressionProvider (user, app, evaluator)and is correct as it stands — it describes the React context value, which does still carryapp. Only the expression scope lost it. The same distinction applies when judging the two skills lines: if either turns out to be describing the context value rather than the predicate scope, it is fine as written.Dedup
MCP
search_issues(REST/search/issuesis 403 from this seat — the container's egress proxy restricts to repository-scoped paths, which says nothing about GitHub scope). Query returned 5 results, so the channel was live and the reading self-validating. Nearest: #7728 (same file family, but the bare-field /data.*spellings, and already shut), #7636, #7015. None names theapproot.Attribution, in prose because issue creation strips footer blocks: generated by Claude Code, session
session_01YBWFb5YgMU5dw8p2VKj16S.Generated by Claude Code