Skip to content

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

Draft
os-tesla wants to merge 2 commits into
mainfrom
claude/issue-9370-skills-data-root
Draft

docs(skills): move the three published guides off the retired dataSource expression root#9378
os-tesla wants to merge 2 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 69aa9c017every 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 就是表达式根 databinddataSource」改成 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants