Skip to content

skills(objectui): record the measured envelope reality instead of contradicting it - #5641

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5372-protocol-envelope-teaching
Aug 21, 2026
Merged

skills(objectui): record the measured envelope reality instead of contradicting it#5641
os-sales merged 1 commit into
mainfrom
claude/issue-5372-protocol-envelope-teaching

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5372

The published rules told authors that the properties / props envelope belonged to the element:* namespace and that every other key "lives on the node". Measured against the real renderer, only half of that is true — and the wrong half is the half an author hits when wiring provider data into a data-table. The guides now record the measurement instead of contradicting it.

Draft on purpose, and it should stay one. The seat dispatched this as a governed surface: human merge only, no ready flip, no auto-merge, no queue. See the one conflict note at the bottom before acting on that.

Re-measured first — the card's claim is a runtime statement, and the tip had moved

The card was filed against 99d8721a3; triage annotated it at 2026-08-21T10:33Z. Re-measured on origin/mainf1c27f037, real SchemaRenderer inside a SchemaRendererProvider holding { customers: [ 2 records ] }, identical columns in every leg, reading tbody td:

legnoderendered body cells
A{ "type": "data-table", "data": "${data.customers}", "columns": [...] }["No results foundTry adjusting your filters or search query."]
B{ "type": "data-table", "properties": { "data": "${data.customers}" }, ... }["Ada Lovelace","ada@example.com","Grace Hopper","grace@example.com"]
C{ "type": "data-table", "props": { "data": "${data.customers}" }, ... }["No results foundTry adjusting your filters or search query."]
D{ "type": "data-table", "data": [ 2 literal records ], ... }["Ada Lovelace","ada@example.com","Grace Hopper","grace@example.com"]
F{ "type": "data-table", "bind": "customers", ... }["No results foundTry adjusting your filters or search query."]

The card's claim holds on the current tip, unchanged. Exactly one of the three spellings reaches the provider's data, and it is the one the rules told authors not to reach for.

What the measurement actually shows — the rule was wrong about the namespace, not just about data-table

The card explains the data-table case. Probing a ui:*card shows the same mechanism is not namespace-scoped, which is what makes the published sentence false rather than merely incomplete (same tip, dataSource = { label: "Evaluated Title" }):

noderendered card header
{ "type": "card", "title": "Customer Summary" }Customer Summary
{ "type": "card", "props": { "title": "Customer Summary" } }no header element at all
{ "type": "card", "properties": { "title": "Customer Summary" } }Customer Summary
{ "type": "card", "title": "${data.label}" }${data.label} — read, never evaluated
{ "type": "card", "properties": { "title": "${data.label}" } }Evaluated Title

So there are two envelopes with two different fates, and the rules had merged them into one:

  • props.* — evaluated, then spread as React props. A ui:* / page:* renderer reads schema.* and never sees it. element:*'s readProps() does. The old rule is right about this one.
  • properties.* — the spec spelling; evaluated, then hoisted onto the node by the COMPAT hoist (type / id excepted), so it lands where every renderer reads, in every namespace. The old rule was wrong about this one.
  • a node key — read, but never expression-evaluated.

Recorded, not recommended — and why that line is where it is

The taught route is unchanged: keys on the node, values resolved in the host before the schema reaches SchemaRenderer. Whether properties is an official ui:* authoring channel is #4795's open sub-question ②, still awaiting the maintainer's one-liner (comment 5328302083: "Still awaiting the maintainer's one-liner on the two parked sub-questions: ② whether the diagnostic copy names the properties channel"). Promoting it to a taught spelling here would settle that from a guide edit, so the guides state the measurement and name the open question instead.

What is not open, and is now stated plainly: a ${...} on the node is never evaluated, a key under props never reaches a ui:* / page:* renderer, and neither failure says anything about whether the provider has data.

⛔ Out of scope, and neither was prepared for

Swept by claim, not by phrase — two sites had none of the searchable tokens

Grepping for the envelope wording finds protocol.md, page-builder.md and schema-expressions.md. Sweeping for the claim found two more that a phrase grep misses, both prescribing the workaround #4786 retired from the prose:

  1. SKILL.md:138 and its twin page-builder.md:481 — a Common Mistakes bullet reading "Putting expression values in top-level value / label fields instead of props.*." It names the envelope as the fix. page-builder.md therefore contradicted itself: line 188 of the same file already said "Moving it under props does not help." Measured false above — props.title renders no header at all.
  2. skills/objectui/evals/schema-expressions.json — the graded form of the same false rule, and the one with no prose tokens at all. Evals 1 and 2 listed props.value / props.label in must_contain, so an assistant answering with the spelling that renders blank was scored correct, and one answering correctly was scored wrong. Both now require the working answer and list the dead spelling under must_not_contain.

Fixing (2) is the in-place bounded exemption, declared: same defect class as this card, mechanical with the correct shape already pinned by the sibling prose and the 2026-08-17 ruling ("Direction 2 permanently rejected"; working channels are content or host pre-resolution), no other claim on the file (no open PR touches skills/, no open issue names the evals), and the same gate family. Full file surface: skills/objectui/{SKILL.md, rules/protocol.md, guides/page-builder.md, guides/schema-expressions.md, guides/data-integration.md, evals/schema-expressions.json} plus one new test and one changeset.

Published-skill net-addition budget

Preference honoured: the false teaching is replaced in place, not appended to. SKILL.md is +0 — a same-length swap.

filebefore → after (lines)delta
skills/objectui/SKILL.md155 → 155+0
skills/objectui/rules/protocol.md231 → 275+44
skills/objectui/guides/schema-expressions.md612 → 621+9
skills/objectui/guides/page-builder.md502 → 510+8
skills/objectui/guides/data-integration.md430 → 437+7
skills/objectui/evals/schema-expressions.json51 → 53+2
whole package, published .md5608 → 5676+68 (+1.2%)
whole skills/ tree incl. eval JSON6216 → 6286+70

The +44 is concentrated in the one file whose central rule was false, and is mostly two measured tables standing in for prose that was wrong. The other four sites are pointers into it rather than restatements, which is what keeps the package delta at +1.2%. No sibling gate defines a token count for skills/ (scripts/ carries only check-skills-paths.mjs and its baseline), so lines are the reading.

The pin

packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx renders all five legs through the realSchemaRenderer and guards the class, in the same three-part shape as its sibling skill-guide-data-table-binding.test.tsx:

  • counter-probe — the rules file is readable and still carries a known-present term, so the "the retired sentence is gone" assertions below it are readings rather than vacuous passes; the eval sweep counts graded assertions and fails on zero.
  • doc-sameness — lifted from the real published bytes at run time, so re-introducing the claim fails here.
  • behaviour — the corrected teaching is verified against the renderer, not against a reading of it.
  • class guard — no published guide may prescribe the envelope (instead of \props.`), and **no published eval may require a props.*` spelling in a correct answer**.

Verification — all at the final commit 2c03db447

check:skills-paths ✅ check-skills-paths: OK (95/96 stated path(s) resolve across 18 guide file(s); 1 baselined).
check:control-bytes ✅ check-control-bytes: OK (scanned 4690 tracked text file(s); skipped 85 binary).
docs:check-links Links are valid across 13 scan roots.
check:doc-types ✅ Every documented component type is registered.
changeset:check ✅ All workspace packages are in the changeset fixed group.
✅ No changeset declares a `major` bump.
quick-reference ✅ QUICK_REFERENCE.md's "Current Release" block already states every anchor.

Changeset gate, quoted rather than assumed — it rules that none is owed, and an explicit empty-frontmatter declaration is added anyway (objectui's first-class "publishes nothing" form; this repo has no skip-changeset label):

Compared the working tree with f1c27f037 (merge-base with origin/main): 6 file(s) changed,
0 of them under the src/ of a package the release covers, 0 under a package changesets ignores,
1 changeset(s) added.
✅ No source of a released package changed in this range, so no changeset is owed.

Build, type-check and tests, exit codes captured before any pipe:

BUILD_EXIT=0 pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build
TYPECHECK_EXIT=0 > @object-ui/components@17.6.0 type-check
> tsc --noEmit && tsc -p tsconfig.test.json
TEST_EXIT=0 Test Files 2 passed (2)
Tests 29 passed (29)

The type-check line is quoted because a pnpm --filter that matches no script exits 0 having run nothing; the echoed script name shows this one really ran (objectui spells it type-check, hyphenated).

Reverse verification. Both retired sites were re-introduced on the committed tree and the mutation was confirmed on disk by anchored greps before the run — instead of \props.*`present (1),renders nothing at allabsent (0), and eval 1'smust_containre-parsed from JSON as['props.value', 'props.label', '${data.']`. Predicted direction was red on exactly the two class-guard cases, and that is what happened:

× no guide tells an author to move an expression "instead of" onto `props`
× no published eval REQUIRES a `props.*` spelling in a correct answer
Tests 2 failed | 12 passed (14)

The mutation script carried a trap … EXIT INT TERM restore; the restore leg is proven by git status --porcelain returning empty, i.e. byte-identical to the commit that ran 29/29 green. No rebuild leg is claimed because none applies here: the pin reads the guides through fs.readFileSync on source paths, and the renderer legs resolve through vitest's workspace alias map to packages/*/src, never dist.

Lint — a measured narrowing, not a skipped run. ESLint's own config is the population: it refuses skills/ outright ("all of the files matching the glob pattern skills/ are ignored") and reports markdown as "File ignored because no matching configuration was supplied", with files: ['**/*.{ts,tsx}'] as its only source glob. So exactly one of the eight changed files is in its population, and --format json reports files eslint linted: 1 | errors: 0 | warnings: 0. grep -c 'projectService\|project:' eslint.config.js is 0 — no type-aware linting is configured, so nothing in this diff can move a verdict on a file it does not touch. CI runs the farm regardless.

check:doc-snippets is not quoted: it exits 1 as a broken gauge until the workspace is built, and its scan surface does not include skills/ (#5465).

One conflict to relay, not to act on

The dispatch flagged skills/** as a governed surface. AGENTS.md §受管面 says the opposite in as many words: .claude/skills/** is governed, while **skills/** at the repo root — the published tree, e.g. skills/objectui/ — is explicitly not, and it warns that misjudging conservatively is still misjudging, because it leaves work parked. This PR is left as a draft either way, because a draft is where a dev seat's PR ends. Flagging it so the seat can reconcile the two before this sits waiting for a merge nobody is obliged to perform.

Out-of-scope finding


Generated by Claude Code

…tradicting it
The published rules told authors that the `properties` / `props` envelope
belonged to the `element:*` namespace and that every other key "lives on the
node". Measured against a real `SchemaRenderer` inside a
`SchemaRendererProvider`, only half of that is true, and the wrong half is the
half an author hits when wiring provider data into a `data-table`.
`props` is evaluated and then spread as React props, so a `ui:*` / `page:*`
renderer never sees it. `properties` is evaluated and then hoisted onto the
node (`type` / `id` excepted), so it reaches every renderer in every namespace.
A node key is read but never evaluated. The consequence the rules got backwards:
the one envelope they told an author not to reach for was the only spelling that
put a provider's rows into a `data-table`, while the two they endorsed both
rendered a correct header over the empty state with nothing thrown and nothing
logged.
The guides now record that measurement. The taught route is unchanged — keys on
the node, values resolved in the host — because whether `properties` is an
official `ui:*` authoring channel is an open contract question (#4795 ②); it is
recorded, not recommended. Two directions stay explicitly untouched: node-level
evaluation is not widened, and `data-table` does not gain a `bind` read (#5125
declined that for its sibling renderer).
Swept by claim rather than by phrase, which found two sites carrying the retired
`props.*` workaround with none of the envelope tokens: the Common Mistakes bullet
in `SKILL.md` and its twin in `page-builder.md`, and the graded eval fixtures,
whose `must_contain` marked `props.value` / `props.label` as the correct answer.
A new pin renders all three spellings through the real renderer and guards the
class: no published guide may prescribe the envelope, and no published eval may
require a `props.*` spelling in a correct answer.
Part of #5372
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.8 KB3867.2 KB
Main entry chunk (gzip)151.7 KB350 KB
Entry fileindex-B__lABZE.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.00KB113.72KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.33KB32.93KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.16KB60.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 22:40
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit b1bc9c5Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5372-protocol-envelope-teaching branch August 21, 2026 22:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@os-sales@claude