Skip to content

docs(skills): move the three published guides off the retired dataSource expression root - #9378

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-9370-skills-data-root
Sep 20, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-9370-skills-data-root

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #9370

⛔ GOVERNED SURFACE (skills/**, GOVERNED_SURFACES skills-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 as 28be0786d.

⚠️ Merge after #9369, and this PR states which world it measured in. Re-derived from my own tree at base 69aa9c017, not assumed: SchemaRenderer.tsx still carries the data: dataSource binding, and SchemaRendererContext.tsx still reads const dataSource = context?.dataSource inside useDataScope. 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: SchemaRenderer stops publishing the injected DataSource adapter as the expression root data (b1), and useDataScope — what a node's bind resolves through — reads the ambient scope a host publishes via PredicateScopeProvider instead 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.md and packages/react/README.md: publish the host values under real names through PredicateScopeProvider, read them by those names, and state that dataSource is the adapter and not a root.

file what changed
guides/schema-expressions.md the scope table, a new COMPILED wiring example, the migration callout, the bind resolution prose, the bound-list comment, debugging-checklist item 3
guides/data-integration.md the architecture diagram's channel split, "Static data" rewritten onto the scope channel as a COMPILED example, the useDataScope resolution sentence, the data root claim
rules/protocol.md both measured tables' WIRING restated, the bind comment 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 the data-table bind pothole (objectui#6575 still rules data-table does not read bind).

⭐ 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 the true on a missing root belongs to the predicate layer, not to the evaluator:

scope, expression evaluateExpression evaluateCondition
CONTROL { data: { status: 'draft' } }, ${data.status} "draft" true
CONTROL same scope, Status: ${data.status} "Status: draft" true
CONTROL same scope, ${data.nope} — present root, absent member undefined false
{ data: {} } — adapter-shaped, ${data.status == 'draft'} false false
{ data: undefined }, same predicate false false
{} — no data root, ${data.status} "${data.status}" true
{} — no data root, Status: ${data.status} "Status: ${data.status}" true
{} — no data root, ${data.status == 'draft'} "${data.status == 'draft'}" true

The 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:

  • predicate layer — fails soft to 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.
  • interpolation layer — does not fail soft at all. A content built 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 data restores the OLD always-false verdict rather than fixing the gate, and points row gates at record — the runtime-layer root under ADR-0089 D3.

What I decided about the ~two dozen ${data.…} examples

Measured, not estimated: schema-expressions.md carries 21 lines spelling ${data. (one of them is the census's own scope-table row). Repo-wide under skills/: 21 here, 14 in rules/protocol.md, 5 in page-builder.md, 4 in testing.md, 2 each in data-integration.md and auth-permissions.md, 1 in architecture.md, plus 4 in evals/*.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's content/docs shape:

  1. A pin renders those exact spellings. skill-guide-provider-envelope.test.tsx, as re-derived in feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369, publishes scope = { 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.
  2. ADR-0089 D3 does not forbid the name. CANONICAL_ROOT_BY_LAYER puts data at the metadata layer. What was retired is the renderer AUTO-publishing the adapter there, not the name.
  3. Most of those examples are about something else — which text keys carry expressions, type preservation, the troubleshooting section. The root name is scaffolding, and churning 20 lines of scaffolding on a governed surface buys the reader nothing.

⛔ 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/main 69aa9c017 — every cited line still lands on the cited text:

cited on origin/main 69aa9c017 verdict
112 ` Top-level data fields
113 ` `data`
302-303 dataSource = { customerNames: … }, useDataScope("customerNames") exact
305 **Nested paths work:** … resolves \dataSource.app.settings.users`` exact
393 // ✅ Bound data, already node-shaped: dataSource = { rows: … } exact

premise_still_valid: true.

Enumeration beyond the census (⛔ out of scope for this PR)

--measure judges 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:

Noted, not filed: the ${data.*} examples in page-builder.md, architecture.md, i18n.md, project-setup.md and evals/*.json are 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:

$ node scripts/check-skill-examples.mjs --self-test     # BEFORE the build
EXIT=2 — PRECONDITION NOT MET: the self-test's type-check leg needs the workspace built

$ ...os-verify-lock.sh -c 'turbo run build $(--build-filter) --concurrency=2'
VERDICT command-exit 0 · held the lock 44s · waited 0s

$ node scripts/check-skill-examples.mjs --self-test     # AFTER the build
EXIT=0 — ✓ 59 cases pass

Gate, verbatim, before and after this diff:

BEFORE (exit 0)
Scanned 20 guide(s) under skills, .claude/skills: 121 ts/tsx/typescript fence(s), 70 json/jsonc fence(s).
Marked: 14 ts fence(s) (floor 13), 70 json fence(s) (floor 70)
Semantic phase: 14 of 14 ts fence(s) judged, 0 failed.
JSON phase:     70 fence(s) parsed, 0 failed.
Every marked skill example holds up against the built types.

AFTER (exit 0)
Scanned 20 guide(s) under skills, .claude/skills: 122 ts/tsx/typescript fence(s), 70 json/jsonc fence(s).
Marked: 16 ts fence(s) (floor 13), 70 json fence(s) (floor 70)
Semantic phase: 16 of 16 ts fence(s) judged, 0 failed.
JSON phase:     70 fence(s) parsed, 0 failed.
Every marked skill example holds up against the built types.

⭐ 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.ts rather than only read.

Derived by hand from package.json plus .github/workflows/ (this repo has no dispatch-gates.mjs); every one run on this tree:

gate exit
check-skill-examples.mjs --self-test 0
check-skill-examples.mjs 0
check-skills-paths.mjs 0 — 88/89 stated paths resolve, 1 pre-existing baselined
check-skill-eval-tokens.mjs --self-test / check-skill-eval-tokens.mjs 0 / 0
check-control-bytes.mjs 0 — 7539 tracked text files
check-shell-escape-residue.mjs 0 — 16/16 skills documents under a declared root
check-new-cross-file-line-citations.mjs 0 — 0 new citations
check-changeset-presence.mjs 0
check-doc-links.mjs 0
check-governed-queue-guard.mjs --self-test 0 — 185 cases

Package tests that READ these three documents (markdown-test-inputs.mjs --changed names 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:

reading before after delta
guides/schema-expressions.md 569 635 +66
guides/data-integration.md 485 513 +28
rules/protocol.md 341 354 +13
whole published package skills/** 5201 5308 +107 (+2.1%)

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:

  1. node scripts/check-changeset-presence.mjs — "Compared the working tree with 69aa9c017 (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)
  2. Independently: 0 of 43 workspace manifests mention skills in files / exports / main / module / types / bin. Positive control on the same scan: 38 manifests list dist in files[]. Nothing under skills/ is shipped by any npm package.
  3. The merged precedent on this surface, 28be0786d (fix(skills): guard the DataSource read in the marked data-integration example #9352), changed one skills/ file and carried no changeset.

⚠️ One inconsistency worth a maintainer's eye rather than my guess: open PR #9374, also a single 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)

$ node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/schema-expressions.md skills/objectui/guides/data-integration.md skills/objectui/rules/protocol.md
⛔ GOVERNED — 3 of 3 path(s) are on a governed surface:
   skills/** x3 — the published skills catalog
     - skills/objectui/guides/schema-expressions.md
     - skills/objectui/guides/data-integration.md
     - skills/objectui/rules/protocol.md

   One governed path governs the WHOLE pull request — proportion is not a question.
   ⛔ Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT and leave the merge
      to the maintainer; a human merge IS the review record for a governed surface.
   The merge-queue run of "Governed Surface Queue Guard" refuses this diff unless an APPROVED review by an
   authorized approver (GOVERNED_APPROVERS: os-zhuang, hotlong) is on the pull request — on
   whichever commit it was left (maintainer ruling 2026-09-04).
exit 3

Clause-② — contract review

needs:contract-review is hung on this PR in the same stroke as opening it, mirroring the card. Published skills/** 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

Acceptance notes

  • ⛔ No test was skipped, quarantined or weakened. No assertion was loosened. The marked-fence floors both hold and the ts population grew.
  • The card's "the pins are green while the prose beside them is wrong" warning was taken literally: the six packages/components pins feat(react)!: unbind the data-source adapter from the expression scope, and point bind at 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, the hoists every key onto\s+the node regex, the absence of the two retired sentences, the no md matches "instead of \props.`"class guard, and the"bind": "customerNames"` list example in each of the three guides.
  • Noted, not filed: rules/protocol.md's two measured tables still cite origin/main f1c27f037 as 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 the skill-guide-provider-envelope pin 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 名字的迁移说明会让读者踩正这一脚。

风险与代价(含回滚)

席位意见

(留空,待 os-zhuang / hotlong 定稿)

你要做的

  1. 确认合并顺序:先 feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369,再本 PR。
  2. 作为受管面的人工合并方给出 APPROVED review(这一道是 check-governed-queue-guard 机械要求的)。
  3. clause-② 的 needs:contract-review 需要一份书面复核记录后才清 —— ⛔ 两道保障叠加,人工合并不替代它。
  4. 顺带定一句话:本仓单文件 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

…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>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5df0ee9719178037689ca3fb7c61394eab88ec2d

In-seat review by the domain:skills seat at the contract-review tier (served tier read at seating, objectstack#7623 marker 5738863635) — the lane's record, taken under the reconciliation ask 5739015009 on objectui#9370 (ruling objectstack#18862 item 2) after one working day's silence: ask 2026-09-19T03:31Z, window read at 2026-09-20T03:33Z — no comment from a live holder on the card, and the domain:ui seat post objectui#9771 names neither this card nor this PR. ⛔ No Claim: by this seat on the card: the claim of record stays 5652138683 (restated with its Branch: line by 5689310818), because the claim reader retracts only by the same login (objectstack#19240) — this seat acts as the lane's reviewing and landing seat. Clause-②: yes is declared on that claim; needs:contract-review is on both carriers (check-clause2-carriers --pair 9378 exit 0 at 2026-09-20T03:07Z: 「both carriers agree」; the PR body spells the clause as a heading, which the reader reports as input only). Verified against GitHub (GET /pulls/9378/files: three files — skills/objectui/guides/data-integration.md +40 / −12, skills/objectui/guides/schema-expressions.md +76 / −10, skills/objectui/rules/protocol.md +18 / −5) and the fetched branch (origin/claude/issue-9370-skills-data-root at 5df0ee97, git rev-parse pasted above; base 69aa9c017, 347 commits behind origin/main e86445f5; git merge-tree --write-tree origin/main HEAD clean after deepening to the merge base; REST mergeable: true, mergeable_state: behind), ⛔ not against the report: two commits, no model identifier in either message; ten hunks.

① Derived judgments

  • The public-surface claim the three files now make: expression roots and bind paths resolve against the ambient scope a host publishes with PredicateScopeProvider (@object-ui/react); SchemaRendererProvider's dataSource is the fetch adapter — it publishes no root and answers no bind; the renderer adds record (the bound row) and page, and derives current_user from the scope's user. Read on objectui origin/main: packages/react/src/context/SchemaRendererContext.tsx :113–:114 (useDataScope reads usePredicateScope()); packages/react/src/SchemaRenderer.tsx :866, :940–:953 (the removed data: dataSource binding, named as the decision), :969–:974 (...predicateScope, current_user: predicateScope.user, { record: boundRecord }, page: pageVariables); packages/react/src/hooks/useExpression.ts :36 PredicateScopeProvider and :50 usePredicateScope, exported through hooks/index.ts :9 and packages/react/src/index.ts :11 — so the two new os:check fences' imports ({ PredicateScopeProvider, SchemaRenderer } from @object-ui/react, type { BaseSchema } from @object-ui/types, packages/types/src/index.ts :95) name real exports; packages/components/src/renderers/data-display/list.tsx :11 / :17 still reads bind through useDataScope. Every sentence in the diff is a reading of that code: the guides teach the channel that exists and stop teaching the one that resolves to undefined. Correct per the card's census and objectui#9308 (merged as PR feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369, 85243729, 2026-09-13T09:02Z — the PR body's 「merge after feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369」 condition is met, so the prose is true on main today).
  • The verdict-flip callout (schema-expressions.md, the ⛔ block): one column per layer — the predicate layer fails soft to true, the interpolation layer returns the template's own characters — is the dev's measurement on the built evaluator with three firing controls (report 5652297355, migration_note_states_the_verdict_flip), and the two roots it names match CANONICAL_ROOT_BY_LAYER = { runtime: 'record', metadata: 'data' } (objectstack packages/lint/src/validate-visibility-predicates.ts :894–:897, ADR-0089 D3). The seat did not re-run the evaluator (no built dist here); the code readings above are the seat's, the table is the dev's with its controls stated.
  • The ${data.*} examples are KEPT and made reachable (the wiring fence publishes a root named data; the scope-table row says every such example assumes it), not rewritten as PR feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369 did for content/docs — the dev's stated divergence, with reasons the seat checked: the pin skill-guide-provider-envelope.test.tsx :106 publishes scope={{ data: PROVIDER }} and renders those spellings, and ADR-0089 keeps data at the metadata layer. Residual ${data. on the branch: schema-expressions.md 23 (21 on main; the +2 are the wiring fence and the table row), data-integration.md 2, protocol.md 14 (+1, the re-attributed measurement line). Accepted: no example is false under the prose that now precedes it.
  • protocol.md's two measured tables keep their f1c27f037 outcomes and only re-attribute the wiring, saying so inline; today those outcomes are held by skill-guide-provider-envelope.test.tsx (:218 / :246 read this file). Region fence held: origin/main's one later change to this file (efc1c9c4, :107–:115) and open PR docs(skills,AGENTS): teach action:button + actionType, retire the events bag #9592's hunk (:238–:262) are both disjoint from this PR's two hunks (:134–:140, :208–:235 on its base); the merge is clean.
  • Value density, read from the loading agent's seat: the three pages now name the one channel that works, say what dataSource still is, and tell a reader who authored a data.* gate from the old pages what moved, per layer. Lines: data-integration.md 485 → 513, schema-expressions.md 569 → 635, protocol.md 344 → 354 (+104 by count, +134 / −27 by diff); the dispatching claim set no net-line cap.

② Semver level

None owed — skills/** publishes no package; scripts/check-changeset-presence.mjs exit 0 at this head against merge-base 69aa9c017.

③ Boundary flags

  • open_questions (2): merge ordering (A) — moot, feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel #9369 merged; changeset house style (A) — the gate decides, exit 0. out_of_scope_findings: objectui#9379 and bug(skills): testing.md Pattern 5 gates on a bare userRole root nothing publishes — copy the example and the assertion throws #9380 filed by the dev; the page-builder.md member of the same class landed as its own card (objectui#9672 → PR docs(skills): page-builder.md names the channel that publishes expression roots (objectui#9672) #9997, at its terminal).
  • CI: ⛔ NOT MEASURED on this head — 0 check-runs; all 22 workflow runs of 2026-09-13T08:45Z ended startup_failure or stayed queued (a runner-side failure that day, not this diff). The seat's PUT /pulls/9378/update-branch — the one PM-legal way to obtain a fresh run — was refused by this session's command classifier at 2026-09-20T03:34Z, and MCP writes are outside the seat's channel. Gates the seat ran at the head without a build, in a detached read-only worktree: check-skills-paths exit 0, check-skill-eval-tokens exit 0, check-changeset-presence exit 0. NOT MEASURED here: check-skill-examples (needs the built dist; the dev's reading at the branch: 16 of 16 marked fences pass, floor 13) and the two guide-reading pins (skill-guide-data-table-binding.test.tsx, skill-guide-provider-envelope.test.tsx). The queue builds and tests the merge result on the way in; an approver who wants the PR-level checks first can press 「Update branch」 — one click, and this record stays valid for the content (the merge adds no line to the three files).
  • Attribution: the PR body carries the dev's session footer plus the platform's appended one (the dev's report names it); neither commit carries a model identifier.

Implemented-by: claude/issue-9370-skills-data-root
Reviewed-by: session_01W5y9kRg1YtYaMQYExVLRc2

VERDICT: PASS


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 20, 2026 08:36
@os-zhuang
os-zhuang enabled auto-merge September 20, 2026 08:36
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 5 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/4795-bindable-text-keys.md

  • names skills/objectui/rules/protocol.md → skills/objectui/rules/protocol.md — edited by this change

    Published authoring guidance updated to match: skills/objectui/rules/protocol.md (new "Bindable Text Keys" rule), plus the page-builder, schema-expressions and data-integration guides, which taught the now-retired "never evaluated" statement and its host-pre-resolution workaround.

.changeset/5120-retire-data-table-name-alias.md

  • names skills/objectui/guides/data-integration.md → skills/objectui/guides/data-integration.md — edited by this change

    The two published skill guides that taught the name spelling (skills/objectui/guides/data-integration.md, schema-expressions.md) migrate in this same release, so the platform never refuses a spelling it still ships.

  • names schema-expressions.md → skills/objectui/guides/schema-expressions.md — edited by this change

    The two published skill guides that taught the name spelling (skills/objectui/guides/data-integration.md, schema-expressions.md) migrate in this same release, so the platform never refuses a spelling it still ships.

.changeset/6357-basechema-bind-declaration.md

  • names skills/objectui/rules/protocol.md → skills/objectui/rules/protocol.md — edited by this change

    bind was read by ten production sites and declared by no schema shape. It resolved as any through BaseSchema's index signature and rode .passthrough() on the validator, while three separate documents taught it as an authorable key of every node: this repo's own AGENTS.md §4 ("Every node in the UI tree follows this shape (@object-ui/types)"), the published agent-facing skills/objectui/rules/protocol.md ("Every UI component node MUST follow this shape"), and content/docs/fields/grid.mdx. So the agent-facing protocol told authors to write a key the published types did not know existed.

.changeset/6575-data-table-bind-diagnostic.md

  • names skills/objectui/rules/protocol.md → skills/objectui/rules/protocol.md — edited by this change

    The platform was already paying for this in teaching rather than in diagnostics: skills/objectui/rules/protocol.md documents the pothole verbatim and a pin test locks the behaviour. The warning now also reaches the console, where the author who did not read the docs is standing:

.changeset/6665-data-table-non-array-data-diagnostic.md

  • names skills/objectui/rules/protocol.md → skills/objectui/rules/protocol.md — edited by this change

    The spelling that opened the card is a ${...} expression string, and it is a defect rather than a design because the SAME expression is evaluated one key over. Re-measured on merge-base 5967be095 through the real SchemaRenderer (the table was previously quoted from skills/objectui/rules/protocol.md as a measurement on f1c27f037 and had not been re-run); all four legs reproduced, and they are now pinned as tests rather than prose:

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with c2f0f4832 (merge-base with origin/main): 3 file(s) changed outside .changeset/, read against 1223 pending declaration(s) that publish a body (1783 pending in total). · run

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 20, 2026
@os-tesla
os-tesla marked this pull request as draft September 20, 2026 09:10

Copy link
Copy Markdown
Collaborator Author

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 domain:ui#2 execution seat (PM session session_018HrVaotisyhgmot9o2MLRq) under a direct maintainer instruction, quoted below.

Why it had to leave the queue

This PR entered the merge queue at 2026-09-20T08:56:39Z while both clause-② carriers were still open: the needs:contract-review label is on this PR and on its card objectui#9370. The merge-group job Governed Surface Queue Guard failed on that, exit code 6, and its own output quotes the governing rule verbatim:

「双肢命中任一 ⇒ 无席内条款②复核 PASS 在案 ⛔ 禁止入队」

and names the remedy, which is what this comment records doing:

  1. ⭐ Take the pull request out of the queue: convert it back to DRAFT (disarming auto-merge alone does NOT dequeue it — objectui#6183 measured exactly that), and leave it parked.

⚠️ The approval is not the thing that was missing. os-zhuang submitted an APPROVED review at 2026-09-20T08:36:26Z and flipped the PR ready four seconds later. This guard does ⛔ not read approvals — it reads the clause-② carrier, which is a different gate and is still hung on both carriers.

⛔ What this seat did NOT do

⛔ Did not strip the needs:contract-review label. The guard's own words: 「Stripping the label to get past this check, with no verdict on record, is the defect this leg was built from — not a way through it.」 Both carriers stay exactly as they were.
⛔ Did not touch the change, the branch, the approval, or the card.

Why it mattered to other work

The 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

  • Whose: the maintainer, speaking to this seat directly.
  • Words, quoted and ⛔ not translated: 「6小时之前的所有开发 pr」…「你接手以上所有pr的开发。除了 Version Packages」
  • Where: this PM session's chat, 2026-09-20T09:09Z.

What unparks it

The 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. ⚠️ Note objectui#9370 also carries needs-user-decision, so the card is in the decision inbox as well.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 20, 2026 09:21
Merged via the queue into main with commit 8ec28d7 Sep 20, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9370-skills-data-root branch September 20, 2026 09:32
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants