Repository navigation
fix(layout,types): an app-schema-renderer node draws the app document it carries under schema (objectui#11494) - #11501
Conversation
… it carries under `schema` The registration published a `schema` input no node could deliver: SchemaRenderer strips a node's `schema` key and hands the registered component the node itself, so AppSchemaRenderer took the node for its app document. A document nested under `schema` drew an empty shell, and only undeclared app keys written flat on the node drew. Under triage ruling A, `registerLayout()` now registers the key against an adapter (the DetailSectionNode shape) that hands `node.schema` to AppSchemaRenderer, and AppSchemaRendererNodeSchema declares `schema` as AppComponentSchema by reference, optional. The document keeps its own refusals (mobileNavMode inside it), the flat spelling is refused by the strict face and no longer read, and basePath / mobileNavMode are unchanged. The layout guide says what the node does, its `children` sentence included. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
…ral the actions census counts The handler-keys census counts files that name `AppComponentSchema` and read `.actions`; the README already carries `properties.actions`, so naming the type there added a third prose file to its population. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
… page entry The doc component-type gate reads every `type` literal in a fence, and `object` is a navigation kind the gate exempts per page; layout.md carries no such exemption. A `page` entry teaches the same nesting with a key the gate resolves. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
…ped app document The fixture's document had no `type: 'app'`, which AppSchemaRendererNodeSchema now judges as the app document and refuses at `schema.type`. The manifest reading the test makes is unchanged; the node is now clean on every face. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: PR objectui#11501, Check-runs on the head: 43, every one completed as of 2026-10-02T19:24Z (polled seven times at ≥60 s intervals from 2026-10-02T19:13Z; the aggregate ① Derived judgments
② Semver level
③ Boundary flagsDev
Dev deviations:
Dev
Why FAIL, in one line: every contract judgment above is right, and one gate on the head is red on a line this diff feeds (① item 12). The rework is the precedent's named type, on this branch; a new head gets a new record, and this one stands until then. Serial: Implemented-by: VERDICT: FAIL |
…it refers to the nested app document by name Nesting AppComponentSchema into the app-schema-renderer arm made declaration emit re-serialize the whole app document a second time inside AnyComponentSchema, and tsc refused the union with TS7056 against @objectstack/spec built from objectstack main (Spec Main Shape Gate). The arm is now annotated with AppSchemaRendererNodeSchemaType, written by reference to AppComponentSchema, on the PageKindNodeSchemaType precedent. Type only: the schema and its accept set are unchanged. Appends a dated note to the pending objectui#11440 changeset: its sentence that the registration's `schema` input is not declared no longer holds. Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
…ma-input Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: PR objectui#11501, Check-runs on the head: 43, every one completed, read twice at 2026-10-02T20:06Z and 2026-10-02T20:09Z with identical sets (the aggregate ① Derived judgmentsItems 1 to 11 of
② Semver levelUnchanged from ③ Boundary flags
Serial: the merge base is Implemented-by: VERDICT: PASS |
Fixes #11494
Clause-②: yes
An
app-schema-renderernode now draws the app document it carries underschema, andAppSchemaRendererNodeSchemadeclaresschemaas that document, by reference. This executes triage ruling A (comment5958227972) on the card. The standing ruling that this node is "the one JSON door for 'render a whole shell from a schema'" (objectui#4841,5307574602) is unchanged. Builds on objectui#11440 (PR objectui#11493, landed as00ccdf742, this branch's base).What was wrong (measured at the base,
00ccdf742)SchemaRendererstrips a node'sschemakey out of the props it spreads, and hands every registered component the node itself as itsschemaprop.registerLayout()registeredAppSchemaRendererdirectly, so the component took the node for its app document. Rendered through the realSchemaRendererand registry at the base, with this PR's new layout pin file and the source untouched:{ type: 'app-schema-renderer', schema: { type: 'app', name: 'crm', navigation: NAV } }drew no navigation:expected 'App' to contain 'Accounts11494'.expected 'crmAccounts11494' not to contain 'Accounts11494'.What changed
@object-ui/layout(packages/layout/src/index.ts):app-schema-rendereris registered againstAppSchemaRendererNode, a module-private adapter in theDetailSectionNodeshape. It readsnode.schemaoff the nodeSchemaRendereralready hands it, and gives that toAppSchemaRenderer. Every other prop (basePath,mobileNavMode, host props) passes through unchanged. There is no fallback to the node's own keys. A node withoutschemahands the component an empty{ type: 'app' }document, which draws the shell with no branding and no navigation, as such a node drew before.AppSchemaRenderer, its props and the registration'sinputsare unchanged.@object-ui/types(packages/types/src/zod/app.zod.ts):AppSchemaRendererNodeSchemagainsschema: AppComponentSchema.optional(). It is the same schema object, with no copy of its members.content/docs/guide/layout.md: the passage now says what the node does, with a JSON example (checked once by hand on both faces, see Gates). Thechildrensentence ("takes the page content as itschildren"), which the probe did not reproduce, is replaced by what holds: the node's children are not rendered, andobjectui validaterefuses them on this node.@object-ui/typesminor and@object-ui/layoutminor, the latter marked breaking for a node that writes app keys flat on itself.Contract notes (for the at-tier review)
props.schema.schema.SchemaRenderer's strip list is not touched.SchemaRendereralready delivers the whole node asschema, so no wider seam was needed. Nothing new is exported: the adapter is private toindex.ts."type": "app".AppComponentSchemarequires theappliteral, so{ schema: { name: 'crm' } }is refused atschema.type(invalid_value). A typeless variant would be a derived schema (.omit/.partial), not the document by reference, so it was not taken. The TypeScript propAppSchemaRendererProps.schemaalready requirestype: 'app'of JSX hosts.StrictAnyComponentSchema): widens. A node with its document underschemavalidates; it was refused withunrecognized_keysnamingschema. The nested document is closed like every object on this face (schema.bradingis refused by name).safeValidateSchema, whichobjectui validateruns):schemaused to pass unjudged and is now judged as the app document. A malformed nested value is refused: notype: 'app', a non-object, ormobileNavModeinside the document (the objectui#11363 tombstone). That is a narrowing for a malformed nested document on this face. The producer reading below found no such document.unrecognized_keys(navigation,title), and the adapter does not read them. The tolerant face keeps its passthrough for them, soobjectui validatealone does not refuse them. The guide says the strict face does, and nothing more.required, and the node the governed mobile guide teaches ({ type: 'app-schema-renderer', mobileNavMode: 'bottom_nav' }) carries no document. It still validates on both faces, and still draws: both are pinned.4e6dc233:git grep app-schema-rendererfinds one hit, a CHANGELOG line, and no node producer (control wordpage:headerhits in the same tree). objectui: no doc, example or catalog document authors the node with a document, flat or nested. The only authored nodes are test fixtures, two of them respelled here..changeset/11440-arm-passing-types.md(unreleased) says the registration's third input,schema, "is not declared". This PR's types changeset says that sentence no longer holds. The 11440 body is outside this claim's file surface and was not edited; the report names the correction.Pins
packages/layout/src/__tests__/app-schema-renderer-schema-input-11494.test.tsx, through the realSchemaRendererand registry:schemadraws, and the same navigation flat on the node draws nothing;titleunderschemaheads the sidebar, and flat it does not;basePathandmobileNavModeare unchanged: the bottom bar's tab is/apps/crm/account, and thedrawercontrol draws no bar;schemadraws the empty shell with no error banner;schemaas an object input, and the arm declares it.packages/types/src/__tests__/app-schema-renderer-schema-input-11494.test.ts, on both faces, each refusal with a lit control:AppComponentSchemaitself, and is optional;basePathandmobileNavMode, and a node withoutschemavalidates;unrecognized_keys(the same keys nested pass);mobileNavModeinsideschemais the app document's tombstone (on the node it is clean);schema.type, and the strict face closes the nested document.schemais an input the arm does not declare — and a node never delivers it to the component" (its file header now points at the 11494 file), and the types row "the registration'sschemainput is not declared" (now a one-linetoContain('schema')with a pointer).app-mobile-nav-mode-refusal-11363.test.tsnests its document, so its comparisons run against a clean baseline on both faces. The schema-catalog control node's document gainedtype: 'app'; without it, that node is refused on both faces.Reverse verification
00ccdf742, source untouched, the new pin files only):Tests 13 failed | 4 passed (17).scripts/ablation-replace.mjs, which reverts the registration toAppSchemaRenderer:d66737e2b8e7to1391f799016e;Tests 4 failed | 2 passed (6): the four drawing rows, as predicted;blob == HEAD (d66737e2b8e7) and git diff HEAD is empty.Gates (final head
44a67d8fc)pnpm exec vitest run --maxWorkers=2 packages/layout/ packages/types/, plus the 15 tests outside those two packages that nameapp-schema-renderer,AppSchemaRenderer,AppComponentSchema,mobileNavModeorlayout.md, plusscripts/__tests__/check-doc-component-types.test.ts):Test Files 391 passed (391),Tests 9710 passed (9710). That set includes the ratchetregistered-types-validate-ratchet-10859.test.ts, which this PR does not touch, and the zod-parity and strict-face suites inpackages/types.@object-ui/typesand@object-ui/layout: both scripts echoedtype-check: Done, exit 0. They ran onebb7d21ba;git diff --stat ebb7d21ba HEAD -- packages/is empty.--listFilesshows both test programs include the touched test files.@object-ui/example-schema-catalogtype-check: exit 0 on44a67d8fc.pnpm check:doc-snippets, after its--build-filterclosure build:Semantic phase: 776 of 776 block(s) judged, 0 failed.It ran on25c91e58b; only a test file changed after that.pnpm check:doc-examples:Every covered @example compiles, or fails exactly as its ledger row declares.pnpm check:doc-types:Every documented component type is registered.node scripts/markdown-test-inputs.mjs --audit:115 candidate test files, all adjudicated; 82 declared entries, all present.pnpm check:control-bytes:OK (scanned 10092 tracked text file(s); skipped 85 binary).pnpm check:new-line-citations:0 new citation(s).node scripts/check-changeset-presence.mjs:9 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s).pnpm check:changeset-claims: report-only, exit 0. It names 21 pending changesets that cite a touched file; each paragraph was read, and none is falsified. The one this PR does falsify (11440, above) names no file, so this gate cannot see it..ts/.tsxfiles (--no-inline-config --format json): 9 files, 0 errors, 4 warnings, all on lines this PR does not touch (blame). Three facts prove the narrowing:.mdfiles;--print-configshows noparserOptions.projectorprojectService(not type-aware), and no rule ineslint-rules/reads the filesystem, so this diff cannot move any untouched file's verdict.@object-ui/typesdist (not a permanent test): toleranttrue, stricttrue. With the document flat on the node: strictfalse(unrecognized_keysontitle,navigation), toleranttrue, which is what the guide says.node scripts/check-governed-queue-guard.mjs --testover the 13 paths:NOT GOVERNED.6d47fdb9e):AppSchemaRendererNodeSchemais annotated withAppSchemaRendererNodeSchemaType, written by reference toAppComponentSchema(thePageKindNodeSchemaTypeprecedent), so declaration emit names the nested document instead of re-serializing it insideAnyComponentSchema. Type only; the accept set is unchanged.Spec Main Shape Gatereproduced locally against objectstack9b7a0ef3faf1: red without the annotation (index.zod.ts590 TS7056 x2), green with it. The pending objectui#11440 changeset carries a dated note.Acceptance notes
.changeset/11440-arm-passing-types.mdshould drop the sentence that says the registration's third input,schema, "is not declared" and the strict face refuses it. Out of this claim's surface; the report names the replacement.children/bodyrefusal message on this node saysSchemaRendererstrips both "out of the props it handsAppSchemaRenderer". The props now reach the adapter, which forwards no node children, so the sentence still holds in substance. It is published refusal text, and is left as it is.packages/layout/README.md("the key for that isapp-schema-renderer") stays true and is not edited.objectui validateruns the tolerant face, so flat app keys on this node pass it, as any undeclared key passes on any node. That is the two-face design objectui#5250 ruled, not this card's.Session:
https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37(thedomain:uiseat 1 dispatch; dev run under the PM's claim5958336981).Generated by Claude Code