docs(skills): move the three published guides off the retired dataSource expression root - #9378
Conversation
…ession root
`SchemaRenderer` published `SchemaRendererProvider`'s injected `DataSource`
adapter as the expression root `data`, and `useDataScope` — what a node's
`bind` resolves through — walked that same adapter. The maintainer ruled both
retired (objectui#9308, 2026-09-13, option B): scope now arrives on the
`PredicateScopeProvider` channel a host publishes, and `dataSource` is the
adapter and nothing else. The published skill package still taught both halves.
`schema-expressions.md`, `data-integration.md` and `rules/protocol.md` now
state the scope channel, and two wiring examples carry the `os:check` marker so
they are COMPILED against the built `dist/*.d.ts` rather than only read: the
marked ts population moves 14 -> 16, 0 failed, and the json population is
unmoved at 70.
⭐ Not a renaming, and a note that only swapped provider names would mislead.
Measured on the built evaluator, a MISSING root and a PRESENT-but-empty root
differ: `${data.status == 'draft'}` is `false` against `{ data: <adapter> }` and
against `{ data: undefined }`, while on a scope carrying no `data` key the
condition path fail-softs to `true` and a text key renders the raw source
characters. So a `visible` gate authored from these pages was hidden on every
row and is now shown on every row; the same gate spelled `hidden` flips the
other way. The migration note states that flip, says that re-publishing `data`
restores the old always-false verdict rather than fixing the gate, and points
row gates at `record` — the runtime-layer root (ADR-0089 D3).
Every `${data.*}` example on these pages is kept verbatim and made reachable by
the wiring example publishing a `data` root, rather than rewritten to bare
names: `skill-guide-provider-envelope` renders those exact spellings, and
rewriting them would move a pin's subject inside another pull request's blind
spot.
The `object-*` reader-list paragraph and the `data-table` `bind` pothole
(objectui#6575) are untouched.
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Co-authored-by: Claude <noreply@anthropic.com>
… the evaluator
The migration callout compressed two different answers into one cell. They are
not the same answer and a reader needs both, because each has its own
reader-visible symptom.
Re-measured on the built evaluator with firing controls. Discriminating control
first: `${data.nope}` against a scope that DOES publish `data` returns
`undefined` — a present root with an absent member — while the same shape
against a scope with no `data` root at all returns something else entirely.
| scope | `evaluateExpression` | `evaluateCondition` |
|---|---|---|
| `{ data: { status: 'draft' } }`, `${data.status}` | `"draft"` | `true` |
| `{ data: { status: 'draft' } }`, `Status: ${data.status}` | `"Status: draft"` | `true` |
| `{ data: { status: 'draft' } }`, `${data.nope}` | `undefined` | `false` |
| `{}`, `${data.status}` | `"${data.status}"` | `true` |
| `{}`, `Status: ${data.status}` | `"Status: ${data.status}"` | `true` |
| `{}`, `${data.status == 'draft'}` | `"${data.status == 'draft'}"` | `true` |
| `{ data: undefined }`, same predicate | `false` | `false` |
| `{ data: {} }`, same predicate | `false` | `false` |
So the `true` on a missing root is the PREDICATE layer failing soft, not the
evaluator's answer. The evaluator hands back the template's own source
characters, in full-string and mixed forms alike — which is what a `content`
key then paints on screen. The callout now carries one column per layer and
says to read both.
The conclusion is unchanged: a `data.*` visibility gate still flips from
constant-hide to constant-show.
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: In-seat review by the ① Derived judgments
② Semver levelNone owed — ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
changeset-claim-re-read
|
|
Parked back to DRAFT at 2026-09-20T09:11Z — this is the queue guard's own step 1, ⛔ not a judgement on the change. Acted by the Why it had to leave the queueThis PR entered the merge queue at 2026-09-20T08:56:39Z while both clause-② carriers were still open: the
and names the remedy, which is what this comment records doing:
⛔ What this seat did NOT do⛔ Did not strip the Why it mattered to other workThe queue stacks entries. While this PR's group kept failing, three other PRs were queued on top of it — objectui#10052, objectui#9996 and objectui#9924 — and were being rebuilt behind a group that could not pass. ⭐ objectui#9996 had already been ejected twice (2026-09-19 05:35Z and 06:49Z) for an unrelated reason of its own. The authority for acting outside this lane, and its three parts
What unparks itThe dispatching seat completes the in-seat clause-② review and posts the verdict on this PR or on objectui#9370; on PASS that same seat strips the carrier from both carriers, cites the record, then flips ready and re-enqueues. On FAIL it is a patch round. Generated by Claude Code |
…ta` expression root (objectui#9379) (objectstack-ai#9669) Fixes objectstack-ai#9379 ⛔ **Governed surface — this PR stays a DRAFT.** `scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.md` exits **3** and prints: *"One governed path governs the WHOLE pull request … Park it as a DRAFT and leave the merge to the maintainer"*, naming `GOVERNED_APPROVERS: os-zhuang, hotlong`. No ready-flip, no queue, no auto-merge from this seat. ## What is repaired The 2026-09-13 maintainer ruling on objectui#9308 (option B) stopped `SchemaRenderer` publishing the injected `DataSource` adapter as the expression root `data`. `skills/objectui/guides/auth-permissions.md` still taught both halves of the retired wiring. The **document** is what was wrong — the runtime is right — so the guide is moved onto the channel that does publish roots, `PredicateScopeProvider`, in the same shape PR objectstack-ai#9369 used for `content/docs/guide/schema-rendering.md` and `packages/react/README.md`. - permission flags are published as an ambient **scope** and read by the names they were published under; - the scope table gains a `record` row (ADR-0089 D3 makes `record` the runtime-layer row root) and now states that `data` is a root only when the host publishes one; - the trap paragraph is re-derived rather than re-worded (see below). ### The premise, checked rather than relayed `packages/react/src/SchemaRenderer.tsx` carries the decision in its own words: *"`data` is NOT here, and the absence is the decision (objectui#9308, maintainer ruling 2026-09-13 option B)"*. Nothing in this PR asks for `dataSource` to become a real root — that would widen a published surface. Prose only; no package source is touched. ### Re-measured on the built evaluator, not remembered `packages/core/dist` (built from this branch's base, `cf601fff6`), over the guide's own gate `${!canDeleteContacts}` and its `data.`-rooted predecessor: | expression | scope | `evaluateCondition` | `evaluateExpression` | |---|---|---|---| | `${!data.canDeleteContacts}` | `{}` — no root at all | `true` | the source text `${!data.canDeleteContacts}` | | `${!data.canDeleteContacts}` | `{ data: {} }` — adapter-shaped | `true` | `true` | | `${!data.canDeleteContacts}` | `{ data: { canDeleteContacts: true } }` | `false` | `false` | | `${!canDeleteContacts}` | `{ canDeleteContacts: true }` — published as a root | `false` | `false` | | `${!canDeleteContacts}` | `{}` | `true` | the source text `${!canDeleteContacts}` | Two consequences the old paragraph could not state: a bare name **does** resolve when the host publishes it as a root (so the old "reachable only under the `data.` root" sentence is now false in its own right), and the predicate layer fails soft to `true` while the interpolation layer prints the characters you typed. Both are in the new text. ### One bounded repair in the same paragraph The flag example derived its booleans from `permissions.check(...)`, which answers a `{ allowed, … }` **object**. An object is truthy, so `${!canDeleteContacts}` was permanently `false` and the gate showed the button to **every** user — the mirror image of the trap the page warns about. The example now publishes `permissions.can(...)`, which answers a boolean, and the guide says why. Without this the repaired example would be untrue on its own terms. ## The class, re-derived (⛔ triage's "fourth" is not relayed) Instrument, run on this branch's base `cf601fff6`: every tracked `skills/**/*.md`, `content/docs/**/*.md`, `packages/*/README.md` and `README.md`; every line naming `dataSource`, judged against a ±4-line window for a teaching token (`${data`, a `data.` root, `bind`, `useDataScope`, `scope`) and for a correction token (`PredicateScopeProvider`, "not an expression root", objectui#9308, "the ADAPTER"). Raw hits were then adjudicated by hand, because the raw signal does not distinguish the class from the unrelated `PageComponentSchema.dataSource` element-data-source key. **Members of the class (5):** | file | state | |---|---| | `skills/objectui/guides/data-integration.md` | held by open PR objectstack-ai#9378 | | `skills/objectui/guides/schema-expressions.md` | held by open PR objectstack-ai#9378 | | `skills/objectui/rules/protocol.md` | held by open PR objectstack-ai#9378 | | `skills/objectui/guides/auth-permissions.md` | **this PR** | | `skills/objectui/guides/testing.md` | objectui#9380, held serial — ⛔ not touched | **⭐ A sixth candidate, reported and not folded in:** `skills/objectui/guides/page-builder.md` — its integration sequence says *"Provide `dataSource` and contextual data through renderer provider"* and every schema example on the page then reads `${data.metrics.activeUsers}` / `${data.userRole}`. It names no other channel, so a reader wires the retired one. On a governed surface every extra file widens what a human has to approve, so it is left for its own card. **Control, same instrument, same run:** `content/docs/guide/architecture.md` and `content/docs/guide/expressions.md` were surfaced by the identical `dataSource`-plus-teaching-window query and both teach the **correct** root (*"`SchemaRendererProvider`'s `dataSource` is not an expression root"*), so the instrument discriminates and the count above is a reading rather than an empty query. Adjudicated **not** in the class, also by the same run: `content/docs/guide/{ci-cd-pipeline,data-source,user-state-persistence}.md`, `content/docs/rfcs/0001-clipboard-paste.md`, `packages/{plugin-detail,plugin-list,react}/README.md` — every one of those names the adapter's own methods or the per-element `dataSource` spec key, neither of which is this class. ## Gates — each verdict is the gate's own line | gate | verdict | |---|---| | `node scripts/check-skill-examples.mjs` | exit 0 — *"Every marked skill example holds up against the built types."* `Marked: 15 ts fence(s) (floor 13), 70 json fence(s) (floor 70)`; `Semantic phase: 15 of 15 ts fence(s) judged, 0 failed`. The one `tsx` fence this PR adds is inside that 15. | | `node scripts/check-skills-paths.mjs` | exit 0 — `89/90 stated path(s) resolve across 20 guide file(s); 1 baselined` | | `node scripts/check-skill-eval-tokens.mjs` | exit 0 — *"Every must_contain token is taught by its own skill bundle."* | | `node scripts/check-changeset-presence.mjs` | exit 0 — *"No source or published contract of a released package changed in this range, so no changeset is owed."* ⇒ nothing is owed; no label is involved in that verdict. | | `node scripts/check-new-cross-file-line-citations.mjs` | exit 0 — `0 new citation(s), enforcement report-only` | | `pnpm check:control-bytes` | exit 0 — `scanned 7788 tracked text file(s)`; plus a direct `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over the changed file, exit 1 (no match) | | `pnpm exec vitest run packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx scripts/__tests__/check-skill-eval-tokens.test.ts scripts/__tests__/check-skill-examples.test.ts scripts/__tests__/check-skills-paths.test.ts` | exit 0 — `Test Files 4 passed (4)`, `Tests 201 passed (201)` | The four test files are the ones `node scripts/markdown-test-inputs.mjs --list` names as readers of `skills/objectui/**` and `.claude/skills/**`. ESLint is not owed by this diff: `eslint.config.js` declares no markdown surface, and `pnpm lint` is CI's repo-wide run either way. Build: `turbo run build --concurrency=2 $(node scripts/check-skill-examples.mjs --build-filter)` — `29 successful, 29 total` — so the fences above were judged against **built** `dist/*.d.ts`. ## Governed-surface size readings | reading | before | after | net | |---|---|---|---| | `skills/objectui/guides/auth-permissions.md` | 367 lines | 403 lines | **+36** | | whole published catalogue (`skills/**/*.md`) | 4604 lines | 4640 lines | **+36** | One file, one section. The added lines are the measured verdict table, the `record` row, and one `tsx` fence that the examples gate now type-checks; a second scope fence was drafted and dropped as duplicate teaching. ## Acceptance notes - `noted, not filed: skills/objectui/guides/auth-permissions.md`'s provider-composition example still nests only `SchemaRendererProvider`, with no `PredicateScopeProvider` beside it. It is not false — the adapter belongs there — but a reader copying it gets no scope. Carrier: whoever takes the `page-builder.md` card above, which needs the same nesting shown once. ## 维护者速读(草稿) **改了什么** —— 只改一份已发布技能指南 `skills/objectui/guides/auth-permissions.md` 的「表达式可见性」与「表达式作用域」两节。把权限标志的发布通道从已退休的 `SchemaRendererProvider dataSource` 改成 `PredicateScopeProvider`,作用域表补上 `record` 行,并按实测重写那段陷阱说明。⛔ 不动任何运行时代码。 **为什么改** —— 2026-09-13 您对 objectui#9308 的裁决(选项 B)已经让渲染器不再把注入的适配器发布为表达式根 `data`;这份指南两半都还在教。它按人读文档的速度持续制造错写法,并且它 **推荐** 的那个写法今天同样失败,失败方式还和它自己警告的那个不同。 **风险与代价(含回滚)** —— 纯文档,风险面是「教得对不对」,不是运行时。三道技能门禁(examples / paths / eval-tokens)各自的判定行都在上表,均为 exit 0;changeset 门禁自己判定无需 changeset。回滚 = revert 这一个 commit,无迁移、无发版影响。⚠️ 顺带修掉同段里一处独立错误:示例用 `check(...)`(返回对象,恒真)当布尔标志,改成 `can(...)`;不改它,新示例自己就是假的。 **席位意见** —— **你要做的** —— 这是受管面:PR 保持 draft,合并权在您。需要 `os-zhuang` / `hotlong` 其一的 APPROVED review,或由您直接人工合并(人工合并本身即评审记录)。另外请裁决上面那个第六个候选文件 `page-builder.md` 是否单开一卡 —— 本 PR 刻意没有把它折进来。 --- _Generated by [Claude Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…d mark its fence (objectui#9671) (objectstack-ai#9994) Fixes objectstack-ai#9671 Clause-②: yes Draft against `main`. `skills/**` is a governed surface (Tier H): this PR stays draft until an authorized approval, and the seat lands it. Dev run under the `domain:skills` seat (objectstack#7623), session `https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2`. ## What changed One file: `skills/objectui/guides/auth-permissions.md`, the `usePermissions` hook example under the heading "usePermissions hook". - The two button gates rest on `can('contacts', 'update')` / `can('contacts', 'delete')` — a **boolean**, and the one spelling the guide's neighbouring paragraph ("Publish `can(...)`") already publishes. `check(...)` is gone from the example: it answers a `PermissionCheckResult` object, an object is truthy, and so gated on it both buttons rendered for a denied user. - Each gate is annotated `: boolean`. That annotation is the tripwire: a `check(...)` put back in that position no longer compiles (TS2322), so the next instance of this defect class goes red in the gate instead of shipping (ablation leg A below). - The fence carries the `os:check` HTML-comment marker on the line directly above it and is tagged `tsx`, so `scripts/check-skill-examples.mjs` compiles it against the built dist from now on. To compile it: `Button` is imported from `@object-ui/components`, and `contact` is typed inline as an object with an optional numeric `salary`. `checkField(...)` is untouched (it already answers a boolean). - The `contact` argument is dropped from the gates — see the API-shape decision. ## API-shape decision: `can(object, action)`, record dropped `can` takes no record; the old example passed `contact` to `check`. Measured against the BUILT `packages/permissions/dist/index.js`, with a throwing Proxy handed in as `record` (any `get` / `has` / `ownKeys` / descriptor read on it throws and is counted), over the guide's own `PermissionProvider` config: | role | action | `can()` (no record) | `check(..., record).allowed` | agree | |---|---|---|---|---| | admin | update | true | true | yes | | admin | delete | true | true | yes | | viewer | update | false | false | yes | | viewer | delete | false | false | yes | | owner | update | true | true | yes | | owner | delete | true | true | yes | - `record property accesses observed: 0` — `evaluatePermission` (`packages/permissions/src/evaluator.ts`) never reads a field of the record. Its only use of `record` is presence: when a record is passed AND the role carries `rowPermissions`, the role's grant additionally requires some row rule's `actions` to list the action. `evaluateCondition` in the same file has no caller on the `check` path (its callers are tests and other packages' own evaluators). - On the guide's own config the two spellings agree on every (role, action) above, so dropping the argument changes no verdict the example demonstrates. - Boundary, stated so the parameter is not read as inert: a role that grants `delete` whose row rules list only `read` answers `true` without a record and `false` with one — still with zero reads of the record. That is a per-role static toggle, not record-level gating; the guide's own section "Row filters are returned verbatim — nothing interpolates them" already says the package evaluates no row filter client-side. An example passing `contact` therefore taught record-level gating the package does not perform; the boolean spelling that keeps the example true is `can(...)` with no record. - Ruled out: `check(...).allowed` (a second spelling for the same question — the drift the card names), and any new helper or package API change (the repair is in the guide). ## Line readings (the two the skills rule requires) | reading | before (`c255b38`, unchanged at `edbcf1e`) | after (`48d3d2b`) | delta | |---|---|---|---| | `skills/objectui/guides/auth-permissions.md` | 403 | 409 | +6 (budget: net ≤ +6) | | whole package, every `skills/objectui/**/*.md` summed | 4,640 | 4,646 | +6 | (`wc -l`; `origin/main` moved from `c255b38` to `edbcf1e` between dispatch and branch-out with both readings unchanged.) ## Marked population At `48d3d2b` the gate prints `Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70)`; `--list` shows the new row `skills/objectui/guides/auth-permissions.md:225 [tsx] marked pass`. Before: the BASE tree carries 85 `os:check` markers under `skills/` plus `.claude/skills/` and this branch 86 (`git grep -c` on the BASE ref vs the worktree — the unbuilt BASE tree cannot print the gate's own line), so ts 15 → 16, json 70 → 70. `MARKED_FLOOR` in `scripts/check-skill-examples.mjs` is left at `ts: 13`. Its header invites raising it in the PR that adds a marker; that file is outside this card's claimed file surface (the guide, and the eval JSON only if a gate demanded it), so it is not touched here — see Acceptance notes. ## Gates (exit captured before any pipe; verdict lines quoted) Closure build first, under objectstack's verification lock: `pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2` → `Tasks: 29 successful, 29 total` · lock `VERDICT command-exit 0 · held the lock 217s`. | command | exit | verdict line | |---|---|---| | `pnpm check:skill-examples` | 0 | `Semantic phase: 16 of 16 ts fence(s) judged, 0 failed.` · `Every marked skill example holds up against the built types.` | | `pnpm check:skill-eval-tokens` | 0 | `Every must_contain token is taught by its own skill bundle.` (eval JSON untouched) | | `pnpm check:skills-paths` | 0 | `check-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined).` | | `pnpm check:new-line-citations` | 0 | `VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0` | | `pnpm check:control-bytes` | 0 | `check-control-bytes: OK (scanned 8061 tracked text file(s); skipped 85 binary).` | | `node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/auth-permissions.md` | 3 | `GOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1` (informational; the expected reading) | | `node scripts/check-changeset-presence.mjs` | 0 | `No source or published contract of a released package changed in this range, so no changeset is owed.` — no changeset added | | `pnpm exec vitest run packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx` (the test `markdown-test-inputs.mjs --changed` names for this guide) | 0 | `Test Files 1 passed (1)` · `Tests 8 passed (8)` | | `pnpm exec vitest run scripts/__tests__/check-skill-examples.test.ts` | 0 | `Test Files 1 passed (1)` · `Tests 113 passed (113)` | NOT MEASURED locally: `pnpm lint` (repo-level `turbo run lint`, CI's run). Reason it was not run, not a measurement: the diff is one `.md` file and `eslint.config.js` configures no markdown processor. ## Ablation From the committed state `48d3d2b`, each leg through objectstack's `scripts/ablation-replace.mjs`: mutation proven by anchor counts and blob hash, restore proven by blob == HEAD blob (`b9f4672`) and an empty `git diff HEAD`, tree clean after each leg. - **Leg A — defect back, marker kept**: `check` re-added to the destructure and `canEdit: boolean = check('contacts', 'update', contact)`. Gate exit **1**: `[semantic] skills/objectui/guides/auth-permissions.md:236:9 TS2322: Type 'PermissionCheckResult' is not assignable to type 'boolean'.` · `Semantic phase: 16 of 16 ts fence(s) judged, 1 failed.` Direction: red, as predicted. - **Leg B — same defect, marker removed**: gate exit **0**, `Marked: 15 ts fence(s) (floor 13)`, `Semantic phase: 15 of 15 ts fence(s) judged, 0 failed`, `Every marked skill example holds up`. That is the blind spot the card names, reproduced: unmarked, the defect is invisible to the only instrument that can see it. It also shows the floor at 13 does not red on this fence being unmarked (16 → 15), which is why the floor note above exists. - A first attempt at leg B did not run: the tool refused a marker removal whose replacement text was a substring of its anchor (`x1 → x1`), restored, and exited 1 before the gate; the leg was re-run with a distinct replacement. Recorded so the count of runs is honest. ## Acceptance notes - noted, not filed: `MARKED_FLOOR` `ts` stays 13 with 16 marked; the gate header's invited raise is outside this card's file surface. 承接者: the landing seat (a one-line follow-up in `scripts/check-skill-examples.mjs`), or none if the seat prefers the floor to move only with a census. - noted, not filed: the guide's "Permission evaluation pipeline" step 4 reads "If record provided + row permissions exist: evaluate row-level filter"; the evaluator consults the row rules' `actions` list and never the filter or the record (measured above). Different paragraph, outside this card's section; prose precision, not a copied-code defect. 承接者: none identified. - Board: 0 of 12 open PRs touched this guide at dispatch; PR objectstack-ai#9378 (objectui#9370) holds three other guides of the package and is not touched. - No label writes from this run; `needs:contract-review` is the seat's to hang on this PR. ## 维护者速读(草稿) **改了什么** — `skills/objectui/guides/auth-permissions.md` 里 `usePermissions` 那段示例:两个按钮的显隐改为看 `can('contacts', 'update')` / `can('contacts', 'delete')`(布尔值),不再看 `check(...)`(一个对象,恒为真);示例围栏加了 `os:check` 标记并改为 `tsx`,补上编译所需的 `Button` 导入与 `contact` 类型;去掉了传给 `check` 的 `contact` 参数。 **为什么改** — 这段代码会被客户项目照抄。原写法下被拒绝的用户也能看到「编辑」「删除」按钮,权限门形同虚设;而该围栏此前未被任何门禁编译,所以没人发现。实测(对着已构建的 `@object-ui/permissions`)`check` 从不读取记录的任何字段,`can` 在指南自己的配置上与 `check(..., record).allowed` 逐项一致,所以去掉记录参数不改变示例演示的任何结论。 **风险与代价(含回滚)** — 只改一个已发布技能文件(净 +6 行,在 PM 预算内),不动任何包源码、不动 API,不需要 changeset。回滚 = revert 这一个 commit。留下的一处:门禁的 `MARKED_FLOOR` 仍是 13(现有 16 个标记围栏),要不要同步抬到 16 交席位决定。 **席位意见** — **你要做的** — 受管面 PR,需要一条获授权账号的 APPROVED review;批准后由席位入队落地。无需其他动作。 --- _Generated by [Claude Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_ Co-authored-by: Claude <noreply@anthropic.com>
…sion roots (objectui#9672) (objectstack-ai#9997) Fixes objectstack-ai#9672 Clause-②: yes ## What changed `skills/objectui/guides/page-builder.md` only — one guide, seven hunks, net +8 lines (331 → 339 against BASE `edbcf1e7a`). 1. Section 4 「Wire renderer and registry cleanly」 step 3 no longer says 「Provide `dataSource` and contextual data through renderer provider」. It now names ONE channel: `PredicateScopeProvider` from `@object-ui/react` — every key of the `scope` you hand it becomes an expression root (and `bind` reads the same bag). It also states which roots the renderer adds on its own (`record`, `page`), that nothing publishes `data`, and what `SchemaRendererProvider`'s `dataSource` still is: the adapter the object-bound blocks fetch through, not an expression root. The provider fence is pointed at, not copied — `guides/auth-permissions.md` carries it since PR objectstack-ai#9669. 2. The four `${data.…}` reads the page's own examples made (BASE :43, :47, :179, :193) now read roots the surrounding prose says the host published: `${userRole !== 'admin'}`, `${metrics.activeUsers}`, `${metrics.growth}`. Two short sentences carry the publication claim (section 3's lead-in and the 「Expression evaluation boundaries」 lead-in), the same shape as auth-permissions.md's 「a page whose host published `userRole`」. Residual `${data.` in this file after the change: 0. ## The measurement that decided the channel `packages/react/src/SchemaRenderer.tsx`, the evaluator that `hidden` / `content` / a `statistic`'s `value` are evaluated on (the `new ExpressionEvaluator({ … })` block inside `evaluatedSchema`): - spreads `usePredicateScope()` — the `PredicateScopeProvider` context (`packages/react/src/hooks/useExpression.ts`: `PredicateScopeProvider` / `usePredicateScope`, exported through `hooks/index.ts` and the package index); - adds `current_user` (alias of a published `user`), `record` (from `RecordContextProvider`, only when a row is bound) and `page` (page variables); - binds NO `data`. The block's own comment: 「`data` is NOT here, and the absence is the decision (objectui#9308, maintainer ruling 2026-09-13 option B)」. `SchemaRendererContext.dataSource` is read by `useViewData` (the object-bound blocks' adapter) and is no longer an expression root; `useDataScope` — what `bind` reads — now walks `usePredicateScope()` too (same ruling, per its docblock). So under the old step-3 wiring a `${data.metrics.activeUsers}` has no root: `hidden` fails soft (hidden for everyone), a `text` `content` / `statistic` `value` prints its own source text — the card's silent failure, reproduced by reading rather than asserted. `useExpression` / `useCondition` (the widget tier) merge the same published scope under the caller's local context, so the roots are the same on both tiers. ## Class count, re-derived (⛔ not copied forward) `git grep -c '\${data\.' -- 'skills/objectui/**/*.md'` at `edbcf1e7a`: architecture.md 1 · data-integration.md 2 · page-builder.md 4 · schema-expressions.md 21 · testing.md 4 · rules/protocol.md 13 — six files, 45 reads. Only page-builder.md is touched here: data-integration.md / schema-expressions.md / rules/protocol.md are PR objectstack-ai#9378's (objectui#9370); testing.md is objectui#9380's; auth-permissions.md was repaired by PR objectstack-ai#9669 and reads 0. ## Line readings (the PM's budget: net ≤ +8 in the file) | reading | before (`edbcf1e7a`) | after | |---|---|---| | `skills/objectui/guides/page-builder.md` | 331 | 339 (+8, at the cap) | | package `skills/objectui/**/*.md`, 16 files | 4,640 | 4,648 | Package spelling: `find skills/objectui -name '*.md' -type f` piped through `cat` and `wc -l` (a `git ls-files 'skills/objectui/**/*.md'` glob skips the two top-level files and reads 4,439 — not the figure used). ## Region fence — open PR objectstack-ai#9592 (objectui#7945) PR objectstack-ai#9592's only hunk on this file is `@@ -105,18 +105,16 @@` (section 5, the `events` bag → `action:button`). BASE :102–:122 — that hunk plus its leading context — is byte-identical at :109–:129 of this branch (`diff` empty). Nothing here touches section 5, so the two land in either order. ## The card's 「related」 item The 「Provider composition pattern」 example that nests only `SchemaRendererProvider` is NOT in page-builder.md (`composition` → 0 hits in this file at BASE; control `SchemaRenderer` → 5). It lives in `skills/objectui/guides/auth-permissions.md` under the heading 「## Provider composition pattern」, an unmarked `typescript` fence — a file outside this card's surface (PR objectstack-ai#9994 / objectui#9671 holds it). Not touched here; handed to the seat in the dev report. ## Gates (exit captured before any pipe; the gate's own verdict line quoted) - build closure under the shared lock — `pnpm exec turbo run build $(node scripts/check-skill-examples.mjs --build-filter) --concurrency=2` → `VERDICT command-exit 0` (29/29 cached, shared worktree cache) - `node scripts/check-skill-examples.mjs --self-test` → exit 0, 「60 cases pass」 - `pnpm check:skill-examples` → exit 0: 「Semantic phase: 15 of 15 ts fence(s) judged, 0 failed. JSON phase: 70 fence(s) parsed, 0 failed.」 — all ten marked page-builder.md fences read `pass` in `--list` - `pnpm check:skill-eval-tokens` → exit 0: 「Every must_contain token is taught by its own skill bundle.」 (no eval touched) - `pnpm check:skills-paths` → exit 0 (88/89 resolve, 1 baselined) - `pnpm check:new-line-citations` → exit 0 on the committed head: 「0 new citation(s)」 - `pnpm check:control-bytes` → exit 0 (8061 files) - `node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/page-builder.md` → exit 3, 「GOVERNED — 1 of 1 path(s)」 — expected: Tier H, this PR stays draft until an authorized approval - `node scripts/check-changeset-presence.mjs` → exit 0, no changeset owed (`skills/**` is not published source) - `pnpm exec vitest run scripts/__tests__/check-skill-eval-tokens.test.ts` → exit 0, 36 passed (the only test naming `page-builder.md`; it uses a fixture by that name) `pnpm lint` not run locally: repo-wide and CI-owned; the diff is one `.md` outside every eslint population. ## Acceptance notes - noted, not filed: the auth-permissions.md 「Provider composition pattern」 fence above nests no `PredicateScopeProvider`, so a reader copying it gets no expression scope — incompleteness, not a false statement. 承接者: the seat, since PR objectstack-ai#9994 holds that file. ## 维护者速读(草稿) **改了什么**:只改 `skills/objectui/guides/page-builder.md` 一个文件,净 +8 行。第 4 节第 3 步原来教读者「通过 renderer provider 提供 `dataSource` 和上下文数据」,而这条通道在 objectui#9308 之后已不再给表达式发布 `data.*` 根。现在这一步只点名一条能用的通道:`PredicateScopeProvider`(`@object-ui/react`),交给它的 `scope` 的每个键都成为表达式的根;同时写明渲染器自己只补 `record` 与 `page`、没有任何东西发布 `data`、`SchemaRendererProvider` 的 `dataSource` 是对象绑定块取数的适配器而不是表达式根。页面自己的四处示例从 `${data.userRole}` / `${data.metrics.activeUsers}` 改为读宿主已发布的 `userRole` / `metrics`,并用一句话写明「宿主发布了这两个键」。 **为什么改**:照旧文接线的读者,页面不报错但结果是错的:`hidden` 门对所有人都隐藏,`text` 的 `content` 与 `statistic` 的 `value` 把 `${data.metrics.activeUsers}` 这串字符原样打到页面上。这是 objectui#9379(auth-permissions.md,PR objectstack-ai#9669 已修)同一类问题的又一个成员;修法照抄那次的形状,不照抄字句。 **风险与代价(含回滚)**:纯文档改动,不动任何包源码、不发版、不欠 changeset;`check:skill-examples` 对本文件十个标记示例全部通过。与在途 PR objectstack-ai#9592(改同文件第 5 节)区域不相交,先后合并都干净。回滚 = revert 这一个 commit。 **席位意见**:(留空) **你要做的**:`skills/**` 是受管面,本 PR 停在 draft;由 `os-zhuang` / `hotlong` 之一给一条 APPROVED review 后,认领席落地。 --- _Generated by [Claude Code](https://claude.ai/code/session_01W5y9kRg1YtYaMQYExVLRc2)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9370
⛔ GOVERNED SURFACE (
skills/**,GOVERNED_SURFACESskills-catalog). This PR parks as a draft by design and needs an APPROVED review from an authorized approver (os-zhuang,hotlong). ⛔ Not flipped ready, not enqueued, no auto-merge. The precedent on this exact surface is #9352 / card #9311, merged as28be0786d.69aa9c017, not assumed:SchemaRenderer.tsxstill carries thedata: dataSourcebinding, andSchemaRendererContext.tsxstill readsconst dataSource = context?.dataSourceinsideuseDataScope. Both halves are still LIVE on this base — #9369 is open, not merged. So these pages become true the moment #9369 lands and are ahead of the code until then. Every evaluator measurement below is from that same pre-#9369 tree, which is the right place to take it: the evaluator itself is what #9369 stops feeding, and the evaluator's own behaviour does not move. Sequencing is a maintainer decision, not something this PR can enforce.What moved
The maintainer ruled objectui#9308 option B on 2026-09-13:
SchemaRendererstops publishing the injectedDataSourceadapter as the expression rootdata(b1), anduseDataScope— what a node'sbindresolves through — reads the ambient scope a host publishes viaPredicateScopeProviderinstead of walking that adapter (b2). The published skill package still taught both halves.Three files, matching the shape #9369 used for
content/docs/guide/schema-rendering.mdandpackages/react/README.md: publish the host values under real names throughPredicateScopeProvider, read them by those names, and state thatdataSourceis the adapter and not a root.guides/schema-expressions.mdbindresolution prose, the bound-list comment, debugging-checklist item 3guides/data-integration.mduseDataScoperesolution sentence, thedataroot claimrules/protocol.mdbindcomment and nested-path sentence, plus a callout carrying the retirement and pointing at the verdict flip⛔ Untouched, as the card requires: the
object-*reader-list paragraph and thedata-tablebindpothole (objectui#6575 still rulesdata-tabledoes not readbind).⭐ The part that is not a renaming
A migration note that only swaps provider names is wrong here, because the verdict moved. Measured by me on the built evaluator (
packages/core/dist), with firing controls — ⛔ and thetrueon a missing root belongs to the predicate layer, not to the evaluator:evaluateExpressionevaluateCondition{ data: { status: 'draft' } },${data.status}"draft"trueStatus: ${data.status}"Status: draft"true${data.nope}— present root, absent memberundefinedfalse{ data: {} }— adapter-shaped,${data.status == 'draft'}falsefalse{ data: undefined }, same predicatefalsefalse{}— nodataroot,${data.status}"${data.status}"true{}— nodataroot,Status: ${data.status}"Status: ${data.status}"true{}— nodataroot,${data.status == 'draft'}"${data.status == 'draft'}"trueThe third control is the discriminating one: a root that is PRESENT with an absent member yields
undefined, while a root that is MISSING yields the template's own source characters. So the two layers answer a missing root differently, and each has its own reader-visible symptom:true. A"visible": "${data.status == 'draft'}"authored from these pages was hidden on every row and is now shown on every row; spelled"hidden"it flips the other way.contentbuilt from a missing root paints the literal characters${data.status}on screen.The callout carries one column per layer and tells the reader to read both. It also states that re-publishing
datarestores the OLD always-falseverdict rather than fixing the gate, and points row gates atrecord— the runtime-layer root under ADR-0089 D3.What I decided about the ~two dozen
${data.…}examplesMeasured, not estimated:
schema-expressions.mdcarries 21 lines spelling${data.(one of them is the census's own scope-table row). Repo-wide underskills/: 21 here, 14 inrules/protocol.md, 5 inpage-builder.md, 4 intesting.md, 2 each indata-integration.mdandauth-permissions.md, 1 inarchitecture.md, plus 4 inevals/*.json.Decision: keep every one of them verbatim and make them reachable, rather than rewrite them to bare roots. The page's new wiring example publishes a root literally named
data, and the scope table says in a row that every${data.*}example on the page assumes exactly that. Three reasons this is the right divergence from #9369'scontent/docsshape:skill-guide-provider-envelope.test.tsx, as re-derived in feat(react)!: unbind the data-source adapter from the expression scope, and pointbindat the scope channel #9369, publishesscope = { data: PROVIDER }and renders${data.customers}/${data.label}. Rewriting the guide spellings would move that pin's subject from inside a second pull request — the "green alone, red together" shape.CANONICAL_ROOT_BY_LAYERputsdataat the metadata layer. What was retired is the renderer AUTO-publishing the adapter there, not the name.⛔ Nothing was silently rewritten: the only expression text this PR changes is the two
dataSource = {…}comments that named the retired wiring.Re-measurement of the card's per-line census
The card measured on the objectui#9308 branch. Re-measured against
origin/main69aa9c017— every cited line still lands on the cited text:origin/main69aa9c017dataSource = { customerNames: … },useDataScope("customerNames")**Nested paths work:** … resolves \dataSource.app.settings.users``// ✅ Bound data, already node-shaped: dataSource = { rows: … }premise_still_valid: true.Enumeration beyond the census (⛔ out of scope for this PR)
--measurejudges every candidate fence, marked or not, so a page can be wrong where no gate looks. Reading the prose as well turned up two more:guides/auth-permissions.mdcarries the same retired claim in its own words — a scope table row| \data` | the `dataSource` passed to `SchemaRendererProvider` |, the sentence "Keys of thedataSourceobject are reachable only under thedata.root", and "With no host scope mounted,dataandpage` are all you get". ⛔ Out of scope here: PR docs(skills): guard both useAuth members in the auth-permissions example #9374 holds that file, so an in-place edit would collide. Filed as finding(skills):auth-permissions.mdstill teachesdataSourceas thedataexpression root — a fourth file the objectui#9370 census missed #9379.guides/testing.mdPattern 5 assertsgetByText('Secret')for a node gated on${userRole !== "admin"}— a BARE root that no channel publishes, before or after the ruling. Copy the example and the assertion fails. Different defect class from this card, and independent of the ruling — it is wrong onmaintoday. Filed as bug(skills):testing.mdPattern 5 gates on a bareuserRoleroot nothing publishes — copy the example and the assertion throws #9380.Noted, not filed: the
${data.*}examples inpage-builder.md,architecture.md,i18n.md,project-setup.mdandevals/*.jsonare in the same reachable-only-if-the-host-publishes class; they are not falsifiably wrong and the skills lane that takes the auth-permissions card is their natural carrier.Verification
Reproduce-first, as required — the harness is trustworthy before any red or green from it is read:
Gate, verbatim, before and after this diff:
⭐ The marked ts population moves 14 to 16 and the json population is unmoved at 70 — both floors are SHRINK-ONLY and neither is breached. The two new marked fences are the wiring examples, so the thing this card is about is now COMPILED against the built
dist/*.d.tsrather than only read.Derived by hand from
package.jsonplus.github/workflows/(this repo has nodispatch-gates.mjs); every one run on this tree:check-skill-examples.mjs --self-testcheck-skill-examples.mjscheck-skills-paths.mjscheck-skill-eval-tokens.mjs --self-test/check-skill-eval-tokens.mjscheck-control-bytes.mjscheck-shell-escape-residue.mjscheck-new-cross-file-line-citations.mjscheck-changeset-presence.mjscheck-doc-links.mjscheck-governed-queue-guard.mjs --self-testPackage tests that READ these three documents (
markdown-test-inputs.mjs --changednames all three as test inputs, so CI runs the full shards): reported below the fold once the shared verify lock grants a turn.Line readings for the governed surface, as the contract requires:
guides/schema-expressions.mdguides/data-integration.mdrules/protocol.mdskills/**The added lines are the correction itself: one migration callout, two now-compiled wiring examples, and the sentences that replace the retired claims. No re-wrap was used to buy lines.
Changeset
None owed, by measurement rather than by inheritance:
node scripts/check-changeset-presence.mjs— "Compared the working tree with69aa9c017(merge-base with origin/main): 3 file(s) changed, 0 of them published source of a package the release covers, 0 of them a manifest whose published contract moved … No source or published contract of a released package changed in this range, so no changeset is owed." (exit 0)skillsinfiles/exports/main/module/types/bin. Positive control on the same scan: 38 manifests listdistinfiles[]. Nothing underskills/is shipped by any npm package.28be0786d(fix(skills): guard the DataSource read in the marked data-integration example #9352), changed oneskills/file and carried no changeset.skills/guide, DOES carry an empty-frontmatter changeset declaring "no package is released by this change". Both forms pass the gate. If the empty-frontmatter declaration is the house style for this surface, say so and I will add one.Governed-queue-guard verdict on this diff (verbatim)
Clause-② — contract review
needs:contract-reviewis hung on this PR in the same stroke as opening it, mirroring the card. Publishedskills/**making falsifiable contract-semantics claims about which roots a tier binds. ⛔ The governed-surface human merge does not substitute for it; the two stack, and neither limb is mine to clear.Inherited reds — ⛔ not from this PR
Bundle Analysisis red onmain(arrived with chore(deps): runpnpm dedupe --lockfile-onlyon an untouched main — the measurement (objectui#9215) #9316; a maintainer decision).Doc Snippet Type Checkis red onmainuntil feat(react)!: unbind the data-source adapter from the expression scope, and pointbindat the scope channel #9369 lands. This diff touches no document that gate scans (content/docs+ package READMEs + rootREADME.md, explicitly notskills/**).Acceptance notes
packages/componentspins feat(react)!: unbind the data-source adapter from the expression scope, and pointbindat the scope channel #9369 re-derived were read for what they assert about these files before a byte was changed, and every string they pin is intact —Rule: Keys Live on the Node, thehoists every key onto\s+the noderegex, the absence of the two retired sentences, theno md matches "instead of \props.`"class guard, and the"bind": "customerNames"` list example in each of the three guides.rules/protocol.md's two measured tables still citeorigin/mainf1c27f037as the commit they were measured on. This PR restates their WIRING, not their outcomes, and says so inline; a re-measurement on a current commit is a separate piece of work, and theskill-guide-provider-envelopepin is what actually holds those outcomes today. Carrier: whoever next re-derives that pin.维护者速读(草稿)
改了什么
把发布中的
skills/objectui三份指南从「dataSource就是表达式根data、bind走dataSource」改成 ruling 之后的真实通道:宿主用PredicateScopeProvider发布作用域,dataSource只是取数适配器。另外给两个接线示例加上os:check标记,让它们第一次真的被编译校验。为什么改
2026-09-13 的裁决(objectui#9308 option B)已经把这两件事退役,但发布给 AI 读的技能包还在教它们。更要紧的是:这不是改名。同一条
data.*门,旧接线下恒判false、新接线下因为根缺失走 fail-soft 恒判true—— 一个visible门从「每行都藏」变成「每行都显」。只换 provider 名字的迁移说明会让读者踩正这一脚。风险与代价(含回滚)
bindat the scope channel #9369 还没合。在它合进去之前,这三页描述的是尚未落地的行为。bindat the scope channel #9369 之后合。${data.*}示例原文,只让接线示例发布一个叫data的根 —— 因为skill-guide-provider-envelope这个 pin 正在渲染这些拼写,改写它们会在另一个 PR 的盲区里挪动 pin 的被测主体。git revert一笔即可,不影响任何已发布包(本仓 43 个 manifest 无一发布skills/)。席位意见
(留空,待
os-zhuang/hotlong定稿)你要做的
bindat the scope channel #9369,再本 PR。check-governed-queue-guard机械要求的)。needs:contract-review需要一份书面复核记录后才清 —— ⛔ 两道保障叠加,人工合并不替代它。skills/改动到底要不要写空 frontmatter changeset(fix(skills): guard the DataSource read in the marked data-integration example #9352 没写、docs(skills): guard both useAuth members in the auth-permissions example #9374 写了,门禁两边都放行)。Generated by Claude Code
Generated by Claude Code