Skip to content

feat(react)!: unbind the data-source adapter from the expression scope, and point bind at the scope channel - #9369

Merged
os-tesla merged 4 commits into
mainfrom
claude/issue-9308-unbind-data-root
Sep 13, 2026
Merged

os-tesla merged 4 commits into
mainfrom
claude/issue-9308-unbind-data-root

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9308
Fixes #9346

Maintainer ruling 2026-09-13, verbatim 「同意」 on option B, both halves. Option A (a new published provider prop) and option C were not taken.

What landed

b1 — data: dataSource leaves the evaluator scope. packages/react/src/SchemaRenderer.tsx no longer publishes the injected adapter as an expression root. @object-ui/app-shell's ExpressionProvider states the governing principle — every root a tier binds must be one the engine accepts AND one that tier can actually answer — and objectui#8155 (app) / objectui#8166 (data) already applied it one tier up. Against a conformant DataSource adapter every data.* path resolved undefined, so this tier bound a root it could not answer.

b2 — useDataScope reads usePredicateScope(). A node's bind now resolves a path in the ambient scope a host publishes through PredicateScopeProvider — the channel ExpressionProvider already feeds — instead of walking the adapter. No published key is added; that is what makes B strictly better than A.

Side effect the ruling's own appendix named: ...predicateScope was spread before data: dataSource, so a host that legitimately published data through the documented scope channel was silently overwritten by the adapter. Removing the line un-shadows it. Pinned as leg 3 of the new test.

Measurements the dispatch asked for

1. The verdict DOES move — the dispatch's premise here was falsified

The dispatch predicted the verdict would not move "because data.* already resolved undefined against an adapter". Measured on the built evaluator, that is wrong, and the distinction is present-but-undefined key vs absent key:

scope data.status == 'draft' with throwOnError
{ data: undefined } false false
no data key at all true throws data is not defined

So a page/component visible gate reading data.* was a clean constant false (node hidden on every row) and is now unevaluable, which this fail-soft surface answers true (node shown on every row). It stops being silent in the same stroke: objectui#5454's reportUnresolvableVisibilityPredicate names it, in production as well as development, with a message that is true for it. That is the breaking half of this change, it is stated plainly in the changeset, and it is pinned as leg 7 of SchemaRenderer.dataRootUnbound-9308.test.tsx rather than left to be discovered in a console.

2. reportAdapterOnlyDataPredicate — measured, then re-aimed

It does not flood: its trigger reads the object passed as boundData, not the evaluator scope, so b1 changed its firing set for a conformant host by exactly nothing (every data.* path was already undefined on an adapter). What b1 broke is its truth, in both directions at once:

  • left pointing at the adapter it would have gone silent for precisely the hosts this change breaks — a bag injected as dataSource still answers the read, while the evaluator no longer sees it;
  • and loud for hosts whose scope-channel data answers perfectly.

⇒ Decision: keep the diagnostic, keep its severity and its dev-only gate, and hand it the data the evaluator actually bound (predicateScope?.data). Its copy moves off the "at the node tier data is the DATA-SOURCE ADAPTER" claim, which this PR makes false. Reachability is now stated in the source rather than assumed: with no host data, a data.* predicate throws and objectui#5454 takes it, so this leg speaks only for a host that published a data root which does not answer — the population it can still speak for.

3. celAuthoring's rowCanonAdvisory — more correct, and untouched

Its message says a wrong-layer data.* "faults at runtime with Unknown variable: data and the rule never fires". Before this PR the node tier contradicted that by resolving data to the adapter; now it agrees. ⛔ Severity unchanged (warning) — every save gate on that tier counts severity === 'error', and promoting it would refuse predicates already in customer metadata.

4. dataTableBindDiagnostic's data-vs-bind precedence question — DISSOLVED

Confirmed, and said so in the module's own docblock. While useDataScope walked the adapter, bind and the node's data were two spellings of one idea and a reader of both would have needed a precedence rule. Now bind resolves the ambient scope and data is an inline array on the node: different channels, no precedence to decide. ⛔ data-table still does not read bind (objectui#6575) and does not start.

Blast radius — all nine readers re-verified per reader, not inherited

Each already falls back, so the behaviour against a conformant host is unchanged (the fallback is what was running):

reader resolution
components/.../list.tsx Array.isArray(boundData) ? boundData : Array.isArray(schema.items) ? schema.items : []
components/.../tree-view.tsx boundData || schema.nodes || []
plugin-charts/ObjectChart.tsx boundData || schema.data || fetchedData
plugin-dashboard/ObjectDataTable.tsx boundData || schema.data || fetchedData
plugin-dashboard/ObjectPivotTable.tsx boundData || schema.data || fetchedData
plugin-grid/ObjectGrid.tsx if (boundData && Array.isArray(boundData)) → else falls through to getDataConfig(schema)
plugin-kanban/ObjectKanban.tsx external || boundData || schema.data || fetchedData
plugin-list/ObjectGallery.tsx props.data || boundData || schema.data || fetchedData || []
plugin-timeline/ObjectTimeline.tsx props.data || boundData || fetchedData

None treats boundData as its sole source, so the shape of b2 is unchanged. The card body's "five components" is wrong; nine is right.

The documentation half

Doc Snippet Type Check was red on main with three fences, all teaching meaning 2 — publish page values through dataSource, read them back under data.*:

content/docs/guide/schema-rendering.md:99:29    TS2740
content/docs/guide/schema-rendering.md:434:29   TS2740
packages/react/README.md:73:29                  TS2740

All three move to PredicateScopeProvider, and so does the prose around them — including packages/react/README.md's sentence that dataSource "is what the evaluator sees under the name data", which the gate cannot see and which b1 deletes outright. packages/types/src/base.ts's bind example comment (// → dataSource.customerNames) moves with it.

The pin, written first and run red on the unmodified tree

packages/react/src/__tests__/SchemaRenderer.dataRootUnbound-9308.test.tsx, red-first reading verbatim:

 ❯ |dom| packages/react/src/__tests__/SchemaRenderer.dataRootUnbound-9308.test.tsx (7 tests | 5 failed)
     × leg 1: `${data.stats.total}` against an injected bag does NOT resolve 32ms
     × leg 3: a host-published `data` is no longer shadowed by the adapter 5ms
     × leg 4: a path resolves against the ambient scope 3ms
     × leg 5: the same path against the ADAPTER seam resolves to nothing 2ms
     × leg 7: a `data.*` visibility gate becomes unresolvable — shown, and LOUD 4ms

AssertionError: expected '99' to be '${data.stats.total}' // Object.is equality
      Tests  5 failed | 2 passed (7)

Legs 2 and 6 are the lit controls and pass on the unmodified tree, which is what makes the other five a reading. After b1 + b2: Tests 7 passed (7).

objectui#8021's pin was re-derived, never weakened or skipped. Leg A still READS the Data Context fence off the page on every run (it now branches over three carriers instead of two, so moving the fence back to either of the others changes what it renders); leg C's wiring and leg D's expression swapped to the new teaching; and a new leg E pins the adapter seam directly — provider dataSource plus ${data.user.name} now renders the characters the author typed. The FORWARDED_LOOKALIKES sweep gained scope= so the new carrier's own look-alike is covered.

Two stale claims were corrected while re-deriving neighbouring pins: three test files carried /** The ambient scope app-shell's `ExpressionProvider` really mounts. */ over a bag containing data: {}. buildExpressionScope has published no data since objectui#8166, so the comment was false before this PR and would have hidden the verdict move after it. Those bags are kept — they are now the only way the adapter-only leg is reachable at all — and re-documented as deliberate HOST publications.

Verification

See the report comment on #9308 for the full command list and VERDICT lines.

Acceptance notes

  • skills/objectui/** now teaches a false wiring — guides/schema-expressions.md (| `data` | Alias for dataSource root |, "dataSource = { customerNames: [...] }, list calls useDataScope("customerNames")", ""bind": "app.settings.users" resolves dataSource.app.settings.users"), guides/data-integration.md and rules/protocol.md. ⛔ Not touched here: skills/** is a governed surface, and one path would park this whole PR in the draft-and-wait lane while it is the PR unblocking a red main. Filed separately; the six affected pins in packages/components publish their host values through the scope channel, which keeps them measuring their own subject.
  • objectui#7206 (the formValues / data tail) is pointed the same way by this ruling but is ⛔ not ruled and ⛔ not addressed here; it remains open.
  • objectui#6575 — data-table not reading bind — is untouched.

维护者速读(草稿)

改了什么。 渲染器不再把宿主注入的 dataSource(适配器)当作表达式根 data 发布;节点的 bind 改为在宿主通过 PredicateScopeProvider 发布的环境作用域里解析。文档与 README 随之改教作用域通道。

为什么改。 这是仓库自己已裁过两次的原则(objectui#8155 / objectui#8166)贯彻到最后一层:一个层只能绑定它答得出的根。面对合规适配器,data.* 恒为 undefined,这一层答不出。

风险与代价(含回滚)。 已在客户元数据里的 data.* 可见性判据,行为从「恒隐藏」变为「恒显示」,并在控制台响亮报出。这是唯一的行为断点,已在 changeset 正文点名。回滚只需还原 SchemaRenderer.tsx 的一行与 useDataScope 的一处读取,其余为文档与钉子。

席位意见。

你要做的。 确认「data.* 判据由恒隐藏翻为恒显示」这一取舍可接受;若希望改为保守方向(保留 data 为一个恒空对象以维持旧裁决),请说,那是另一张卡。

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

objectui#9308, maintainer ruling 2026-09-13, option B.

b1 — `SchemaRenderer` no longer writes `data: dataSource` into the evaluator
scope. Against a conformant `DataSource` adapter every `data.*` path already
resolved `undefined`, so this tier published a root it could not answer. Same
principle `ExpressionProvider` states and objectui#8155 / objectui#8166 already
applied one tier up.

b2 — `useDataScope` resolves `bind` against `usePredicateScope()`, the ambient
scope a host publishes through `PredicateScopeProvider`, instead of walking the
injected adapter. No published key is added; the channel already existed.

Also re-aims `reportAdapterOnlyDataPredicate` at the `data` the evaluator
really bound, and moves its copy off the "data is the adapter" claim. Left on
the adapter it would have gone silent for exactly the hosts this change breaks
and loud for the ones whose scope-channel `data` answers.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…d it

objectui#9308 + objectui#9346.

`Doc Snippet Type Check` was red on `main` with three fences that taught
publishing page values through `dataSource` and reading them back under
`data.*`. Under the 2026-09-13 ruling that teaching is wrong, so the Data
Context passages of `content/docs/guide/schema-rendering.md` and
`packages/react/README.md` move to `PredicateScopeProvider`, and the README
sentence asserting that `dataSource` "is what the evaluator sees under the name
`data`" — which the gate cannot see — goes with them.

objectui#8021's pin is RE-DERIVED, not relaxed: leg A still reads the page on
every run, leg C's wiring and leg D's expression swap to the new teaching, and
a new leg E pins the adapter seam as a non-root.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.25KB 13.99KB
plugin-charts (index.js) 71.51KB 19.97KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.01KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 96.00KB 31.71KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.25KB 13.99KB
plugin-charts (index.js) 71.51KB 19.97KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.01KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 96.00KB 31.71KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…h through the scope channel

objectui#9308 b2 — `useDataScope` resolves the ambient predicate scope, so a
pin that fed the bound rows through `SchemaRendererProvider.dataSource` was
measuring a channel the hook no longer reads. Both keep the adapter seam
mounted, where it is now inert, and neither assertion moved.

Found by narrowing the dependent packages to the tests that can reach b1 or b2
at all: 1987 candidate test files, 7 reachable, 4 failures — all in this pair.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.02KB 115.16KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.25KB 13.99KB
plugin-charts (index.js) 71.51KB 19.97KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.01KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 96.00KB 31.71KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review

Head judged: 4de2534f25e67873d88f16ac5bfe8864395e4919 (base 69aa9c017).

⛔ First: this seat falsified TWO premises I wrote into its dispatch

1. reportAdapterOnlyDataPredicate would flood. I told it to measure whether removing the data root makes every data.* predicate unresolved and floods the console. False, and for a reason I had not looked at: that diagnostic resolves against the argument passed as boundData, not against the evaluator scope. b1 changed its firing set for a conformant host by zero. What b1 broke is not its volume but its truth, in both directions — pointed at the adapter it would go SILENT for exactly the hosts this change breaks and LOUD for hosts whose scope-channel data answers. The seat re-aimed it at predicateScope?.data and rewrote the copy off the now-false "data is the adapter" claim.

2. "Confirm the verdict does not move for a conformant host — it should not." ⭐ Falsified, and this is the load-bearing measurement of the whole round:

{ data: undefined }      data.status == 'draft'  ->  false
no `data` key at all     data.status == 'draft'  ->  true   (and THROWS under throwOnError)

A present-but-undefined root and an absent root are not the same to the engine. So a data.* visibility gate flips from constant-HIDE to constant-SHOW. I predicted no movement because I reasoned "it resolved undefined before, it resolves nothing now" — which conflates exactly those two states. Pinned as leg 7, stated in the changeset and the PR body.

① Derived judgments

# move judged
1 data: dataSource removed from the evaluator scope correct — verified by content, not by count
2 useDataScope reads usePredicateScope() correct
3 the shadowing defect retired as a side effect correct, and it was a real latent bug
4 nine bind readers, per-reader fallback re-verified correct
5 objectui#8021's pin re-derived, not weakened correct
6 the three fences + the README sentence the gate cannot see correct

1 — enumerated rather than counted. grep -c "data: dataSource" returns 2 on this branch, which looks like a failed removal. Both hits are comments explaining the removal (:835, :848). The live scope reads:

new ExpressionEvaluator({
  ...predicateScope,
  current_user: (predicateScope as any)?.user,
  ...(boundRecord && … ? { record: boundRecord } : null),
  page: pageVariables,
});

No data. ⭐ Recording the method because the count alone would have been a false alarm, and I have made that mistake three times this session.

3 — I raised the shadowing hazard independently during the analysis: ...predicateScope at :842 followed by data: dataSource at :847 meant a host publishing data through the documented scope channel had it silently overwritten by the adapter. b1 retires it, and :848's comment names it rather than fixing it in silence.

4 — the reason b2 is safe, re-read per reader on this branch: list.tsx Array.isArray(boundData) ? boundData : …schema.items; tree-view boundData || schema.nodes || []; ObjectChart / ObjectDataTable / ObjectPivotTable boundData || schema.data || fetchedData; ObjectGrid falls through to getDataConfig(schema); ObjectKanban external || boundData || schema.data || fetchedData; ObjectGallery props.data || boundData || …; ObjectTimeline props.data || boundData || fetchedData. None treats boundData as its sole source. (The card said five readers; there are nine — my dispatch carried that correction and the seat re-verified all nine rather than inheriting it.)

6 — the deliverable, and CI is the witness, not the seat. check:doc-snippets exit 0 with Semantic phase: 649 of 649 block(s) judged, 0 failed, and the ablation reproduces the card's exact three diagnostics by taking only the two documents back to base. Confirmed independently: PR CI shows Doc Snippet Type Check GREEN on this head, and the only red is Bundle Analysis, which is inherited. All three fences moved to PredicateScopeProvider scope={…}, and packages/react/README.md:44's \`dataSource\` is what the evaluator sees under the name \`data\`. — which no gate could see — is deleted.

② Semver

minor across @object-ui/react, @object-ui/components, @object-ui/types, and the note leads with the break and who it breaks:

Breaking, deliberately — and minor only because this repo's fixed group of 40 packages may not carry a major. Read the migration note below before upgrading if any of your metadata reads data.* at the page/component tier.

Correct, and it carries the verdict-flip. ⛔ Note the changeset was not edited by me for a spelling convention — there is none, as I had to retract to four seats this round.

③ Boundary flags — and the two questions, ruled

Q2 — the verdict flip. RULING: A, as ruled. Proceed.

The conservative variant (data: EMPTY instead of removing the key) keeps verdicts byte-identical and nothing goes loud. ⛔ Refused, and the seat's reason is the right one: it keeps publishing a root this tier cannot answer — the exact defect objectui#8155, objectui#8166 and this ruling each removed — and trades a loud failure for a silent one.

⚠️ But the maintainer ruled B without this consequence in hand, so it is stated rather than buried. For any host that authored a data.* gate, an element that was constantly HIDDEN becomes constantly SHOWN. The gate was never actually gating on data — it was a constant hide either way — but what a user sees changes. It is diagnosable (objectui#5454 makes it loud), pinned, and in the changeset's migration note. ⇒ this is a statement to the maintainer, not a request to re-rule; A is what B means.

Q1 — the published skills/** guides this ruling makes false. RULING: A, with C as the follow-up.

The seat hit two instructions pointing opposite ways — "fix what this round makes false" vs "⛔ do not touch skills/**" — and took A: filed objectui#9370 with a per-line census rather than folding them in. Correct. Folding three governed files in would have parked the PR that clears a red on main behind a human merge. ⇒ the coverage hole is a filed card, not a silence. C (hand objectui#9370 to the seat already in the governed skills lane) is the follow-up and costs one human review instead of two.

other flag disposition
objectui#9297's docs census accepted — this ruling makes those pages more wrong rather than newly wrong; that card already carries them.
objectui#7206 (formValues / data tail) accepted — pointed the same way, ⛔ not ruled, not in this PR.
three test files claimed data: {} was "the ambient scope ExpressionProvider really mounts" accepted — false since objectui#8166 and load-bearing after this change; fixed in place because those files were being re-derived anyway.
dataTableBindDiagnostic's data-vs-bind precedence accepted — DISSOLVED as predicted, and said so in the module docblock. objectui#6575 untouched; data-table still does not read bind.
celAuthoring's rowCanonAdvisory accepted — becomes more correct, severity left at warning, untouched.

Independence

Implemented-by: claude/issue-9308-unbind-data-root   (mode:subagent — a subagent has no session of its own)
Reviewed-by:    session_01UzHd6hDYatoDn17BuwKxnZ     (domain:ui PM seat)

⛔ For this lane a clause-② review is default-tier self-review plus the gates. The readings carry the verdict.

Verdict

PASS. Both carriers cleared in the same stroke as this comment.

⚠️ On landing pre-check ③, and stated because I broke this rule once today: this PR's only red is Bundle Analysis, inherited from main via objectui#9316. ⛔ I am not inventing an exception for convenience — the argument is specific and it is about the rule's own satisfiability: a PR whose purpose is to clear a red on main cannot be gated on main being green. Holding this one would leave Doc Snippet Type Check red until a different, unrelated red is fixed by the maintainer, which is a deadlock on the rule's own repair path. Recorded as a concrete sub-case on objectstack#17934, which is the card asking for ③ to state what it does about base-inherited reds.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Bundle Analysis on this PR is the standing ui-components debt — taken to the chunk's own decision, as the checker instructs

Reproduced locally at origin/main 69aa9c017 (a full package build plus a console build, then node scripts/check-eager-closure-budget.mjs), because the Actions job-log blob host is refused by this container's egress policy and the check-run carries no file-anchored annotation:

❌ chunk `ui-components`   387.8 KB measured / 389.6 KB ceiling
   (headroom 1.9 KB = 0.02x the 89.0 KB regression, under the 0.10x floor and held open
    by its declared 4289-byte allowance, which may only be paid DOWN; reds below 3378 bytes)
chunk `ui-components` was ALREADY declared exhausted before this run.
BUDGET_EXIT=2

Every other row is green: aggregate 3105.8 / 3134.8 KB (0.32x), vendor-objectstack 0.15x, i18n-locale-en 0.10x, framework 0.61x.

⭐ The checker's own verdict text says what to do with it, and this comment is that instruction being followed:

This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk — measured at three gzipped bytes across five unrelated merges. So this verdict is NOT an accusation that your diff spent the bytes … ⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance. Paying the row down is the open decision on the chunk, ⛔ not a task for whichever change the queue happened to weigh — take it there, and say on this pull request that you did.

⛔ No ceiling was moved, no allowance was moved, no exemption was added. Taken to objectui#9204, the chunk's own decision card, where the domain:ui PM seat is carrying it.

This diff cannot have spent those bytes, and that is checked rather than asserted

This PR removes an expression root and re-points a hook; it adds no dependency and no module to the console's eager closure. The row read 387.8 KB on main itself, before this branch existed — the same figure this PR's check reports.

Why this PR is armed anyway

Its own deliverable is green: check:doc-snippets reaches exit 0 here (Semantic phase: 649 of 649 block(s) judged, 0 failed), and CI confirms Doc Snippet Type Check green on this head. That is the check this PR exists to clear on main.

⚠️ Stated plainly because I broke a landing rule once today: Bundle Analysis is the only red on this head and it is inherited from main via objectui#9316. Holding this PR would leave Doc Snippet Type Check red on main until an unrelated standing debt is paid — a deadlock on the rule's own repair path. The general form of that gap is filed as objectstack#17934; this instance is resolved by the checker's own instruction above, which tells the weighed pull request to route the debt and proceed.


Generated by Claude Code

Merged via the queue into main with commit 8524372 Sep 13, 2026
37 of 38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9308-unbind-data-root branch September 13, 2026 09:02
os-try-charles pushed a commit that referenced this pull request Sep 17, 2026
…ta` expression root (objectui#9379)

The 2026-09-13 maintainer ruling on objectui#9308 (option B) stopped
`SchemaRenderer` publishing the injected `DataSource` adapter as the
expression root `data`. This guide still taught both halves: flags
derived "in the dataSource object", a scope table sourcing `data` from
"the `dataSource` passed to `SchemaRendererProvider`", and a trap
paragraph whose recommended spelling is the one that now fails.

The document is what is wrong here, not the runtime: the guide is moved
onto the channel that does publish roots, `PredicateScopeProvider`, in
the same shape PR #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 states that `data` is a root only when the
  host publishes one;
- the trap paragraph is re-measured on the built evaluator: a name
  nothing published makes a predicate fail soft to `true` (the button is
  hidden for every user), while a text key hands back its own source
  characters.

One bounded repair inside the same paragraph: the flag example derived
its booleans from `check(...)`, which answers a `{ allowed, … }` object.
An object is truthy, so `${!flag}` was permanently `false` and the gate
showed the button to everyone. The example now publishes `can(...)`,
which answers a boolean.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
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
…urce` expression root (objectstack-ai#9378)

Fixes objectstack-ai#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 objectstack-ai#9352 / card objectstack-ai#9311, merged as `28be0786d`.

⚠️ **Merge after objectstack-ai#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** — objectstack-ai#9369 is open, not merged. So these pages become true
the moment objectstack-ai#9369 lands and are ahead of the code until then. Every
evaluator measurement below is from that same pre-objectstack-ai#9369 tree, which is
the right place to take it: the evaluator itself is what objectstack-ai#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 objectstack-ai#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 objectstack-ai#9369's `content/docs`
shape:

1. **A pin renders those exact spellings.**
`skill-guide-provider-envelope.test.tsx`, as re-derived in objectstack-ai#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 | \`SchemaRendererProvider dataSource\`
| …` | exact |
| 113 | `| \`data\` | Alias for dataSource root | …` | exact |
| 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:

- **`guides/auth-permissions.md`** carries the same retired claim in its
own words — a scope table row `| \`data\` | the \`dataSource\` passed to
\`SchemaRendererProvider\` |`, the sentence "Keys of the `dataSource`
object are reachable only under the `data.` root", and "With no host
scope mounted, `data` and `page` are all you get". ⛔ Out of scope here:
**PR objectstack-ai#9374 holds that file**, so an in-place edit would collide. Filed
as objectstack-ai#9379.
- **`guides/testing.md` Pattern 5** asserts `getByText('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 on `main` today. Filed as objectstack-ai#9380.

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:

```text
$ 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:

```text
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` (objectstack-ai#9352), changed
one `skills/` file and carried no changeset.

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

```text
$ 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

- `Bundle Analysis` is red on `main` (arrived with objectstack-ai#9316; a maintainer
decision).
- `Doc Snippet Type Check` is red on `main` until objectstack-ai#9369 lands. This diff
touches no document that gate scans (`content/docs` + package READMEs +
root `README.md`, explicitly not `skills/**`).

## 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 objectstack-ai#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
名字的迁移说明会让读者踩正这一脚。

### 风险与代价(含回滚)
- **排序风险(主要):** objectstack-ai#9369 还没合。在它合进去之前,这三页描述的是尚未落地的行为。⚠️ 建议排在 objectstack-ai#9369 之后合。
- 保留了全部 `${data.*}` 示例原文,只让接线示例发布一个叫 `data` 的根 —— 因为
`skill-guide-provider-envelope` 这个 pin 正在渲染这些拼写,改写它们会在另一个 PR 的盲区里挪动 pin
的被测主体。
- 回滚代价:纯文档,`git revert` 一笔即可,不影响任何已发布包(本仓 43 个 manifest 无一发布 `skills/`)。

### 席位意见
(留空,待 `os-zhuang` / `hotlong` 定稿)

### 你要做的
1. 确认合并顺序:先 objectstack-ai#9369,再本 PR。
2. 作为受管面的人工合并方给出 APPROVED review(这一道是 `check-governed-queue-guard`
机械要求的)。
3. clause-② 的 `needs:contract-review` 需要一份书面复核记录后才清 —— ⛔
两道保障叠加,人工合并不替代它。
4. 顺带定一句话:本仓单文件 `skills/` 改动到底要不要写空 frontmatter changeset(objectstack-ai#9352 没写、objectstack-ai#9374
写了,门禁两边都放行)。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ)_


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: os-zhuang <jack@objectstack.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment