Repository navigation
fix(plugin-designer): editing an app keeps its stored navigation (objectui#10894) - #10912
Conversation
…on (objectui#10894) Drives the real EditAppPage and wizard to saveItem. Red on the base: leaving the Objects step replaces the stored navigation with generated object entries. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ectui#10894) Leaving the Objects step no longer replaces the draft's navigation with one generated object entry per selected object. An empty navigation is filled as before (the create path); a non-empty one is merged: a newly selected object's entry is appended, a deselected object's entries are dropped wherever they sit, and every other entry is kept as stored, in its position. EditAppPage counts an object as selected when the stored tree has an object entry for it anywhere, a group's children included, so an untouched edit drops nothing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…avigation (objectui#10894) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ 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: ① Derived judgmentsMerge semantics (read against spec
Create path
Pins (read, not run)
Export and lint
Changeset. Every sentence is true against the code except one overstatement.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…el and groups (objectui#10894) The selection walk recurses into group children only, so "anywhere" overstated it for an object entry nested under another object entry's children. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ 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: This is an in-seat delta review of the changeset follow-up ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Seat note: landing paused on
|
…0894-app-edit-keeps-navigation Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN 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
|
… family D input slice) (objectstack-ai#10907) Part of objectstack-ai#9256 Clause-②: yes **Clause-② `yes`, as the claim declared:** `input` accepted an authored `children` on both published faces and rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it. It now refuses it by name. A published accept set narrows, so a contract review is owed before landing. This is the family-D slice that release `5864147075` on objectui#9256 lists, and nothing else: `input`, the family-D test header's superseded remarks, and `PartialSchema`'s docblock claim about generic `Omit` re-spellings. `br`, `hr` and `img` are not touched. objectui#9256 stays open. ## What changed - **`input` narrowed** in the family-D shape PRs objectui#9589 and objectui#10897 used: - TypeScript face (`InputSchema` in `packages/types/src/form.ts`): `body?: never` and `children?: never`, each with a docblock saying what the renderer reads and what it renders instead. - zod mirror (`InputSchema` in `zod/form.zod.ts`): two `retirementTombstone` members fed one string, `INPUT_NEITHER_CHANNEL`, built by `neitherContentChannelGuidance`. Both stay MEMBERS, so `zod-mirror-parity`'s key sets stay equal. - `body` was already refused on both faces by `BaseSchema` (objectui#6771). It is restated because that refusal names `children` as the remedy, and `input` does not read `children` either. - **`email` / `password` (`InputShorthandSchema`)**: no type or accept-set change. See "The shorthand's own pair" below. - **Texts (comments only):** - The family-D test header: 「family E, frozen」 (the `InputSchema` bullet), 「unattributable」 (the `DetailViewSchema` bullet) and the two `ui:calendar` remarks that still said plain `Omit` (the header bullet on the six bare-name hold-outs, and the comment above the slice-2 rows) now say what is true. No assertion in that file moved. - `PartialSchema`'s docblock in `packages/types/src/index.ts`: the sentence reading every generic re-spelling as collapsing is marked false, with `OmitDeclared` named as the counter-example. - **Bounded in-place fix, declared:** `partial-schema-collapse-pin.test.ts`'s header carried the same false claim, and even named the key-remapping spelling as one that collapses. It is corrected the same way, comment only. See "Texts" below for the four conditions and the evidence. - **New pin file** `packages/types/src/__tests__/content-channel-input-9256.test.ts`. - **One changeset** `.changeset/9256-input-content-channels.md`: `@object-ui/types` `minor` with an explicit BREAKING note and a migration line, the spelling PRs objectui#9589 and objectui#10897 used under this repo's no-major rule. - **Four pending changesets are corrected, prose only.** - Round 2 added a dated note to `6397-partial-schema-collapse-pin.md`, `9256-content-channel-family-d.md` and `8499-node-slot-registered-arms.md`. - Round 3 added a second note to the family-D entry and reworded the E3 entry's render claim in place. - Each carries a sentence a later change in this same release made false, or an overstatement, and each ships verbatim into the CHANGELOG. Every frontmatter block is byte-identical. See "Round 2" and "Round 3" below. ## Measurement, taken on `origin/main` `244d516df` before any edit **Instrument.** The TypeScript compiler API (6.0.3), ⛔ not grep. It builds one program per `tsconfig.json`: the 40 workspace packages, `apps/console` and the three examples, 44 programs over 2015 non-test source files. It ran on a BUILT tree (`turbo run build` 43/43, 0 cached) with 0 unresolved-module diagnostics. It files every `.body` / `.children` read (property access, string element access, object destructuring) under the declared type of its receiver: 520 channel reads were filed. - **`InputSchema`: 0 `body` / 0 `children` reads.** `InputShorthandSchema` is never a receiver: the `email` / `password` wrappers are `any`-typed and hand the node to the `input` renderer, which reads it as `InputSchema`. - **Lit controls fire in the same run:** `ButtonSchema` 1, `DivSchema` 1, `CardSchema` 1, `ContainerSchema` 1 (each `children`). - **Receiver reachability:** the same programs see `InputSchema` as the receiver of 23 reads over 17 keys: `defaultValue`, `description`, `error`, `id`, `inputType`, `label`, `max`, `maxLength`, `min`, `name`, `pattern`, `placeholder`, `readOnly`, `required`, `step`, `value`, `wrapperClass`. Neither channel is among them, so the zero is a reading, not blindness. **Every registration that claims `input`, bare and namespaced** (`check:registry-bare-names --json`, 1657 files, 424 claims, 0 contested): - `input`: sole claimant `ui:input`, a literal `ComponentRegistry.register('input', InputRenderer, …)` in `renderers/form/input.tsx`. The hop is typed (`InputRenderer` takes `schema: InputSchema`), not `any`. No `field:input` or other claimant exists; `ai:input` is a different key, a guarded protocol placeholder. - `email` / `password`: `ui:email` / `ui:password` own the bare keys; `field:email` / `field:password` stand down with `skipFallback: true`. **Does any path pass the node's content through? No.** - `SchemaRenderer` destructures `children` and `body` out of the props bag before the spread. - The only pass-through `InputRenderer` gives the native element is `toFormControlDomProps(inputProps)`, a whitelist pick of `SDUI_DOM_PASS_THROUGH_KEYS` plus `name` / `disabled`; `children` is not on it. - The `email` / `password` wrappers only spread `props` and `props.schema` into `InputRenderer`. - The registration declares no `children` slot input (objectui#9910). - No in-source host builds an `input` node by spreading another node. The literal `input` nodes in `form.tsx`, `cli`'s `init` scaffold and the dashboard / designer config panels are assembled key by key, and none carries a channel. - **The 75 `any`-typed channel reads were attributed file by file.** None can receive an `input` node's channel: - app-shell nav / search trees, and HTTP request, error or email `body` payloads; - React `children` destructured off component props (`view:simple`'s renderer, `react-page`'s wrapper), which `SchemaRenderer` never fills from a node; - the metadata-admin preview canvases, which read a page block's `properties.children` or a page draft's root `children` to list and append blocks for selection, not to render a node's channel; - the page-container and sectioning registrations in `containers.tsx`, `page.tsx` and `semantic.tsx`, and the html-elements factory, whose tag list excludes `input` by name (its own comment lists `input` among the tags it never registers); - `core`'s schema builder (card / grid / flex builders; `InputBuilder` has no children setter); - `record:alert`'s own `props.body`, the runner's nav items, and the vscode validator. - **Generic traversers** (`validateChildren` in core, `sdui-parser`'s parse / validate, `cli validate`) walk children to validate. They render nothing and are node-agnostic, as for every family-D row already landed. **Producers.** `pnpm census:body-dialect --keys input,email,password,ui:input,ui:email,ui:password,div,card,page,button` read 9016 files. The key population is printed by the tool itself, so objectui#9545's blindness does not apply. - Nodes on the six input keys: `input` 321, `email` 75, `password` 60, `ui:email` 1, `ui:password` 17, `ui:input` 0. **0 author `body`, 0 author `children`.** - The controls fire in the same pass: `children` on `div` 172, `card` 183, `page` 51, `button` 5. - Real producers: 0. Nothing was migrated. ## The shorthand's own pair: type-redundant now, kept on purpose `InputShorthandSchema` inherits `InputSchema` through `OmitDeclared` on the TypeScript face and `.omit()` on the mirror, so it now inherits the new pair. It already declared its own (PR objectui#10897). - **Redundant as a type:** on the built d.ts both are the same `?: never`, and the new `Eq` pin makes `tsc` hold that. - **Not redundant as text.** The repo's rule for a restated tombstone is the one `neitherContentChannelGuidance`'s docblock gives for restating `body` over `BaseSchema`: restate when the inherited message misdirects this node's author. The helper's contract spells the node and its route into the message. The inherited string names `input` and the `ui:input` route; the shorthand's names `email` / `password` and the wrapper route. - **Decision:** keep both members and rewrite their docblocks (TS) and comment (zod) to say they are type-redundant and why they stay. The shorthand's old `body` docblock gave a reason that became false with this change (that the inherited refusal names `children` as the remedy), and it is replaced. - **Pinned:** a CONTROL row asserts that an author of `email` / `password` reads the message naming those two nodes and not the `input` one, so deleting the restatement turns it red. - Counter-precedents, noted: `UiCalendarSchema` restates nothing and inherits `calendar`'s message, which names it. objectui#8072 deleted the shorthand's `wrapperClass` restatement once `InputSchema` carried the key; that was a live key with one description, not a refusal message naming a node. ## The `OmitDeclared` propagation, measured on the BUILT d.ts A compiler-API probe over `packages/types/dist/form.d.ts` reads the heritage of `InputShorthandSchema` on its own: the base type the checker gives for `OmitDeclared` of `InputSchema` minus `type` / `inputType`, before the interface's own members. | built d.ts | `InputSchema.children` | heritage `.children` | shorthand face `.children` | |---|---|---|---| | base `244d516df` | `SchemaNode \| SchemaNode[]` | `SchemaNode \| SchemaNode[]` | `undefined` (its own restated tombstone) | | this branch | `undefined` | `undefined` | `undefined` | A consumer-side probe resolves `@object-ui/types` the way `packages/components` does (`--traceResolution`: `packages/types/dist/index.d.ts`). There, `input` + `children`, `input` + `body` and `email` + `children` each answer their `@ts-expect-error`, and a lit control (`const n: number = 'x'`) fires. ## Red on base, then green — one-shot, the restore proven by state The predictions were written to a file BEFORE the mutation. The mutation was `git checkout 244d516` of the two declaration files (`form.ts`, `zod/form.zod.ts`), with every test at HEAD. It was proven by both blob hashes equal to BASE and by anchor counts: `INPUT_NEITHER_CHANNEL` 0, the new TS docblock marker 0. The run sat under `trap … EXIT INT TERM` with absolute paths. | reader | mutated (BASE declarations) | restored (HEAD `72dba894a`) | |---|---|---| | vitest, `content-channel-input-9256.test.ts` | **RED** exit 1: 6 failed / 13 passed | **GREEN** 19 / 19 | | `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2: TS2578 on the `inputChildren` pin, TS2322 on the `Eq` pin for `children`; 0 elsewhere (`zod-mirror-parity` included) | **GREEN** exit 0 | | built d.ts probe (types rebuilt; dist markers 0 then 2) | `InputSchema` and heritage `children` accepted | refused | - **The 6 red rows:** `input.children` refused at its path, its message, `.describe()` equality, every-value, and the root + nested `AnyComponentSchema` row; plus the `input.body` message row. The base message is `BaseSchema`'s `Did you mean` pointer. - **Green on base, by design:** `input.body` refused at its path, every-value and `AnyComponentSchema` (`BaseSchema` already refused `body`), and the shorthand message CONTROL (its own pair is unchanged). - **One prediction was recorded as uncertain and read, not assumed:** `input.body` `.describe()` equality is GREEN on base, because `BaseSchema`'s alias refusal also describes with its own message. -⚠️ `?: never` is erased before vitest runs: the TypeScript half is read by `tsc` alone. `tsc --listFiles` counts the new pin file 1 under `tsconfig.test.json` and 0 under `tsconfig.json`, so it is a real assertion under the `type-check` script. - **Restore:** proven by both blob hashes equal to HEAD, `git diff HEAD` empty and `git status` clean. ## Texts - **Family-D test header.** The `InputSchema` bullet now records the plain-`Omit` hold-out ground in the past tense, the E3 slice's `OmitDeclared` repair, and this slice's narrowing, pinned in the new file. The `DetailViewSchema` bullet records that the E3 slice attributed the `any` hop directly and narrowed it there. The two `ui:calendar` remarks name `OmitDeclared` for the TS face, `.extend()` for the mirror, and say which file pins which half. The third `ui:calendar` remark (the old TRIPWIRE note) was already in the past tense and is unchanged. - **`PartialSchema`'s docblock.** The triage's reading is kept as the triage's. The re-spelling claim is marked FALSE, with `OmitDeclared` as the counter-example. The docblock now says that re-spelling the alias would narrow a published type, a contract change not made in a comment correction, and that objectui#6397 is closed. The alias itself is unchanged. - **Bounded in-place fix: `partial-schema-collapse-pin.test.ts` header** (comment only; its assertions are about the alias as written and do not move). All four conditions hold: 1. It is the same defect as the carried item: the same sentence, one hop away. The corrected docblock points readers at this file. 2. The fix is mechanical, and its shape is pinned by the carried correction. 3. No open PR touches the file. 4. Same gate family: types `tsc` and vitest, no new verification surface. **Evidence, a one-shot probe against the built d.ts (not shipped):** the exact spelling the header named as collapsing, `{ type } & { [K in keyof T as K extends 'type' ? never : K]?: T[K] }`, declares 62 properties at `ObjectGridSchema` and 28 at `ButtonSchema`, where the shipped `PartialSchema` declares 1 at each. It refuses `label: 42` on `ButtonSchema` and `objectName: 42` on `ObjectGridSchema` (both `@ts-expect-error` used; tsc exit 0). ## Gates at HEAD `72dba894a` (round 1; round 2's are above) (exit codes captured by redirect-then-capture; heavy runs through the shared verify lock) | gate | result | |---|---| | `@object-ui/types` build (tsc + vite + dist completeness) | exit 0, 134 emitted files verified | | `@object-ui/types` `type-check` (`tsc --noEmit` + examples + `tsconfig.test.json`) | exit 0 | | `pnpm exec vitest run packages/types/` (parity, the family-D / E3 / input pins, the 8072 and 8762 shorthand pins) | 265 files / 5873 tests, exit 0 | | `pnpm exec vitest run scripts/` (every `scripts/__tests__` gate suite, a superset of those whose corpus holds a touched file) | 177 passed + 2 skipped of 179 files / 5322 tests, exit 0 | | downstream consumer type-check: the packages naming `InputSchema` (`components`, `core`) plus those authoring `input` nodes or configs (`cli`, `plugin-dashboard`, `plugin-designer`) and `fields`, `react`, `types`, against the rebuilt `dist` | 8 × `type-check: Done`, exit 0 | | `@object-ui/types` lint (`eslint .`, JSON) | 338 files, 0 errors; the 9 `form.ts` warnings sit outside every added hunk | | `check:handler-key-reads` | exit 0; 128 arms, 50 / 50 reads judged, 0 unjudged | | `check:control-bytes` · `check:new-line-citations` (0 new) · `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` | exit 0 each | | `check:spec-symbols` · `check:component-surface-parity` · `check:readme-exports` · `check:registry-bare-names` · `check:prompt-keys` · `check:doc-types` · `check:test-path-roots` · `check:esm-specifiers` · `check:vi-mock-*` · `type-check:coverage` | exit 0 each | | `check:doc-snippets` (678 / 678) · `check:doc-examples` · `check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` · `check:sdui-registration-pins` · `pnpm check` (CLI self-check) | exit 0 each | | governed guard `--test` over the 7 paths | NOT GOVERNED (lit control `AGENTS.md`: exit 3) | - **The gate list was derived by hand.** objectstack's `dispatch-gates.mjs` refuses an objectui answer by design (exit 2), so the list comes from objectui's `package.json` and `.github/workflows/`. - `check:changeset-claims` is report-only. It names 18 pending changesets that mention a touched file; each was read, and none is falsified as a record of its own change. The two that carry a claim this slice bears on are in the Acceptance notes. - **NOT MEASURED, left to CI:** the 8-way `pnpm test` shards beyond `packages/types` and `scripts/`, `test:dist`, and E2E. `check:sdui-registration-pins` read the console built at base; this diff touches no registration and no `sideEffects` array. ## Round 2: four prose edits, head `6eb3397b4` These come from contract review `5865087439` ③. The seat took them into this PR because each ships verbatim into the CHANGELOG. They are prose only: no code, no test logic, and every frontmatter block is byte-identical (the `name: bump` lines at base and head compare equal). 1. **`.changeset/9256-input-content-channels.md`, and the Clause-② sentence above.** - Old: "an authored child list on it rendered nothing: no error, no warning, no element." - New: "an authored child list on it rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it." - Measured with a one-shot probe, not committed. It built the manifest from the live registry the way `container-declaration-ratchet.test.tsx` does, then ran `validateTree` and `compile`. - `input`, `ui:input`, `email` and `password` carrying `children`: each draws exactly `not-a-container`. - The same four without `children`: none. - `div` carrying `children` (control): none. - The JSX `input` tag with a child: `not-a-container`, severity `warning`. 2. **`.changeset/6397-partial-schema-collapse-pin.md`.** A dated note follows the "Why a pin and not a repair or a retirement" paragraph: a generic re-spelling of this `Omit` does not collapse, objectui#9256. - Spelling: `OmitDeclared` is spelled exactly as quoted, in `packages/types/src/form.ts`. - Counts: the re-spelled alias declares 28 properties at `ButtonSchema` and 62 at `ObjectGridSchema` on this head and on `main` `733fd5ac6`. The shipped alias declares 1 at each. - "Not true when written": at that entry's own commit (`c8ea8af9c`), under the same TypeScript 6.0.3 its lockfile pins, the same spelling declares 27 and 61. Those are the source counts in that entry's own table. Both re-spellings refuse `label: 42` (the `@ts-expect-error` is used), and a lit control on the shipped alias admits it. - "Later in this same release": the E3 entry and the 6397 entry are both pending on `main` (`git cat-file -e` exit 0 for each; control on an absent path: exit 128). 3. **`.changeset/9256-content-channel-family-d.md`.** A dated note follows the AMENDED paragraph, in that file's own convention, naming the hold-outs this card later narrowed in this release. - Six of the nine names, in `nine-holdouts-six-narrow.md`. - `list` and `timeline`, in `9256-list-timeline-content-channels.md`. This is one more than the reviewer's draft, measured. - `DetailViewSchema` and the shorthand faces, in the E3 entry. - `InputSchema`, in this PR. - A compiler-API probe over the source reads `body` / `children` as `undefined` with their own `?: never` members on `TextSchema`, `ImageSchema`, `IconSchema`, `TabsSchema`, `AccordionSchema`, `CalendarSchema`, `ListSchema`, `TimelineSchema`, `DetailViewSchema`, `InputShorthandSchema` and `InputSchema`. The zod faces are pinned in the family-D, E3 and input pin files. - `ButtonSchema` and `AppComponentSchema` still resolve `children` to `SchemaNode | SchemaNode[]` (`ButtonSchema`'s zod mirror declares its own `children` union; `AppComponentSchema`'s extends `BaseSchema`, which declares one). That is the note's "still accept `children`" sentence. 4. **`.changeset/8499-node-slot-registered-arms.md`.** "`inputType` is deliberately NOT declared on this arm" is false on `main`. - `form.zod.ts` declares `inputType: retirementTombstone(SHORTHAND_INPUT_TYPE_REFUSAL)` on `InputShorthandSchema`, and `form.ts` declares `inputType?: never`. Both files are identical on `main` and at the base. - A runtime probe: `inputType` is a member of the mirror shape. `{ type: 'password', inputType: 'text' }` is refused with `invalid_type` at path `inputType`, the message opening with the objectui#8762 guidance. Bare `{ type: 'password' }` (control) parses. - objectui#8762's entry landed a day after 8499's (2026-09-10 against 2026-09-09), and both are pending on `main`. - A dated note in the file's own form follows the `InputShorthandSchema` bullet, the same place its objectui#10756 note sits under the `HtmlElementSchema` bullet. | gate at `6eb3397b4` | result | |---|---| | `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s) added, 3 modified, 0 deleted". Each of the three modified entries prints the same declaration at base and now (`@object-ui/types: patch`, `minor`, `minor`), so no declaration is lost: its case 2, a correction on purpose. | | `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:control-bytes` · `check:new-line-citations` | exit 0 each | | the 24 `scripts/__tests__` suites that name `.changeset` (the changeset gates' own suites, the polarity census, control bytes, pending literals, docs and workflow suites) | 24 files / 913 tests, exit 0 | ## Round 3: two more prose edits, head `876f6d7ea` These come from delta review `5865466392` ③ flags 2 and 3. They are prose only, and both frontmatter blocks are byte-identical. 1. **`.changeset/9256-content-channel-family-d.md`: a second dated note, directly after the AMENDED paragraph** and before round 2's note. The headline is "the two twins no longer take `body` — objectui#6771". - Old, still in the AMENDED paragraph: "The two twins are unchanged and go on inheriting `body` as the content slot". - New note: objectui#6771 retired `body` on `BaseSchema` itself, so both twins refuse `body` on both faces. The TypeScript face does it through `BaseSchema`'s `body?: never`; each mirror does it by name, pointing at `requestBody`. - Measured, TypeScript: a compiler-API probe over the source gives `BaseSchema` its own `body?: never`. `ChatbotEnhancedSchema` and `ChatbotFloatingSchema` resolve `body` to `undefined` and declare only `children?: never` themselves. - Measured, zod, with a one-shot runtime probe (deleted): each twin's mirror refuses `body` with `invalid_type` at path `body`, and the message names `requestBody`. `requestBody` parses (control). - Provenance, from `git log -S`: `BaseSchema`'s `body?: never` and both twins' mirror tombstones were introduced by `2acd8e109` (objectui#6771, 2026-09-19). The AMENDED paragraph was written on 2026-09-17 (`c42554e94`), and `6771-retire-body-child-list-dialect.md` is pending on `main`. So "later in this same release" holds. - The reviewer's draft also cited objectui#9659. That card did not introduce the twin refusals, so the note cites objectui#6771 alone. - Round 2's note opens "most of the hold-outs above have since been narrowed". It is still true beside the new one: it names what this card narrowed, and it ends with `button` and `AppComponentSchema`, which still accept `children`. 2. **`.changeset/9256-e3-residual-content-channels.md`, in place.** - Old: "an authored child list on them rendered nothing: no error, no warning, no element." - New: "an authored child list on them rendered nothing, with no render-time error or warning and no element; only the parser tier's `not-a-container` warning (objectui#9910) noticed it." - Measured for all twelve keys, not just the two in round 2. A one-shot runtime probe (deleted) loaded every plugin registration (`plugin-grid`, `-form`, `-kanban`, `-map`, `-tree`, `-view`, `-gantt`, `-calendar`, `-charts`, `-detail`, and `components`). It built the manifest from the live registry and ran `validateTree`. - Each of the twelve is known to the registry, and none declares a `children` input. - Carrying `children`, each draws `not-a-container` (severity `warning`). Without it, none does. `div` carrying `children` (control) draws nothing. - `object-chart`, `email` and `password` also draw one `unknown-prop` in both legs, from the probe's own `objectName` fixture key. It is not part of the difference. | gate at `876f6d7ea` | result | |---|---| | `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s) added, 4 modified, 0 deleted". Each modified entry prints the same declaration at base and now, so no declaration is lost. | | `changeset:check` · `check-changeset-presence` · `check:changeset-claims` · `check:pending-changeset-literals` · `check:control-bytes` · `check:new-line-citations` | exit 0 each | | the 24 `scripts/__tests__` suites that name `.changeset` | 24 files / 913 tests, exit 0 | ## Serial constraints - **Re-mapped at branch time (`244d516df`), before each round's push, and again before round 3's edits.** At round 3 the open PRs are objectui#10921, objectstack-ai#10915, objectstack-ai#10914, objectstack-ai#10912, objectstack-ai#10911, objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10906, objectstack-ai#10901, objectstack-ai#10777 and objectstack-ai#10278. objectstack-ai#10904, objectstack-ai#10902 and objectstack-ai#10891 have merged since the first map. - None touches any of the five changesets this PR adds or edits. - One touches a source path here: PR objectui#10914 edits `packages/types/src/zod/form.zod.ts`, in two comment / `.describe()` hunks on the `wrapperClass` key (in `InputSchema` and in the `InputShorthandSchema` restatement note). Neither overlaps or touches this PR's hunks, and `git merge-tree` of this head (`876f6d7ea`) against objectstack-ai#10914's head is clean. - PR objectui#10908 edits `zod-mirror-parity.test.ts` and `zod/index.zod.ts`, neither of which this PR touches. - **The release PR objectui#5400** (1732 files, enumerated to the end, last updated 2026-09-18) touches none of the source paths. As release consumption it deletes pending changesets, three of the four edited here among them (not the E3 entry); that is not a hunk overlap. - **`origin/main` moved four commits since the base** (`9f0c84a44`, `7ea8118f7`, `8522396c0`, `733fd5ac6`). None touches `packages/types` or the changesets edited here, and `git merge-tree` of this head (`876f6d7ea`) against `733fd5ac6` is clean. `main` has not moved since. ## Acceptance notes: out of scope, not fixed here - **The `PartialSchema` docblock and its pin file still sequence the alias's repair on objectui#5155.** That card is closed as a duplicate of objectui#5250, whose ruling keeps the tolerant face on renderer props. objectui#8347 (open) is the card on removing the index signature from the authoring face. "Once objectui#5155 removes the root index signature" therefore names a card that will not do it. Noted, not edited: rewriting the sequencing is a judgement about objectui#5250 / objectstack-ai#8347, not a comment correction. Carrier: none. - **The same "no error, no warning, no element" sentence lives in published refusal messages and docblocks,** not only in changesets. Examples: the family-D `retirementTombstone` strings on the zod mirrors (the chatbot twins' included) and the matching TypeScript docblocks. Wherever the node's registration declares no `children` input, the parser tier's `not-a-container` warning did fire. Correcting those strings is a published-message change, not prose, and it is not this PR's. Noted, not edited. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #10894
Clause-②: no — an edit stops destroying stored navigation. No declared type, accepted key or published surface moves.
Dispatched by the
domain:uiseat 2 PM loop, claim comment 5864678970, sessionhttps://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN. Base9f0c84a448, head5b2b8568bb.What changed
AppCreationWizard: leaving the Objects step (handleNext, andhandleStepClickon the step 1 to step 2 jump) no longer setsnavigation: generateNavFromObjects(prev.objects). Both callmergeNavWithObjects(prev.navigation, prev.objects):generateNavFromObjectsreturns (the create path, unchanged);EditAppPage: an object counts as selected when the stored navigation has an object entry for it ANYWHERE, a group's children included (navObjectNames, exported from the wizard module). It counted top-level entries only..changeset/10894-app-edit-keeps-navigation.md:'@object-ui/plugin-designer': patch.H1 — reproduced on
main(9f0c84a448)A throwaway probe (deleted, never committed) drove the real
EditAppPageand wizard tosaveItem, renaming the app only. The stored navigation[nav_accounts (object "Customer Accounts", icon Briefcase), nav_rule (separator), nav_sales (group with the nav_pipeline dashboard child), nav_docs (url)]saved as:[{"id":"account","type":"object","label":"Accounts","icon":"Building","objectName":"account"}]The Navigation step listed
["account"]. Even the kept object entry lost its authored id, label and icon. Create-path control on the same base: an empty draft withaccountandcontactselected saves the two generated object entries (the control row below, green on base).H2 — every writer of
draft.navigationAll writers live in
AppCreationWizard:DEFAULT_DRAFT.navigation([]), then theinitialDraftspread:EditAppPagepasses the storednavigation;CreateAppPagepasses a localStorage draft, which can carry oneaddNavItemremoveNavItemreorderNavItemhandleNext(leaving step 1)generateNavFromObjects(replace); nowmergeNavWithObjectshandleStepClick(step 1 to 2)handleNextThe draft's navigation is read by
NavigationBuilderStep(items), byhandleComplete(onComplete, thenwizardDraftToAppSchema, thensaveIteminCreateAppPage/EditAppPage) and byhandleSaveDraft(localStorage).EditAppPagealso reads the STORED navigation for the Objects step'sselected.NavigationDesigneris a controlled component (items/onChange); the wizard does not render it and it writes nothing to the draft.apps/consolehas no writer: app-shell'sAppContentlazy-loadsCreateAppPage/EditAppPageonto routes, and the console tests mockEditAppPage.The merge rule, and the group-child edge
Leaving the Objects step with a non-empty navigation:
url/dashboard/page/report/component/actionentries, each object entry's authored label, icon and order, and an object entry for an object the Objects step does not list (nothing deselected it).When no entry is dropped the tree is returned as the same array, so an untouched edit saves the stored bytes.
Group-child edge, measured and pinned. An object entry the author placed inside a group (
nav_people, objectcontact, insidenav_salesbeside thenav_pipelinedashboard):EditAppPagenow listscontactas selected;contactdrops the entry from inside the group. The group keeps its place, its label and icon, and its other children;children: []. The specAppSchemaparses that save green.Without the
EditAppPagechange the merge would drop a grouped object entry on an edit that never touched it: ablation B below reddens exactly those rows.Pins, red on base and green on head
packages/plugin-designer/src/__tests__/EditAppPage.keepsNavigation-10894.test.tsx, 9 rows, each through the realEditAppPageorCreateAppPagesave:AppSchemagreen);children;3b93e01369on base code):Tests 8 failed | 1 passed (9). The one pass is row 9.5b2b8568bb:Tests 9 passed (9).Ablation A: restore the unconditional regeneration.
scripts/ablation-replace.mjs(objectstack) swapped bothmergeNavWithObjects(prev.navigation, prev.objects)calls forgenerateNavFromObjects(prev.objects): anchor x2 to x0, blob1669c2e50c50toadac4d10b62c. ResultTests 8 failed | 1 passed (9): rows 1 to 8 red, only the create-path control green. Restored: blob equals HEAD (1669c2e50c50),git diff HEADempty.Ablation B:
EditAppPageselection back to top-level only. Anchor x1 to x0, blob31e1ae7eb312to6744ceabdad7. ResultTests 3 failed | 6 passed (9): exactly rows 5, 6 and 7. Restored: blob equals HEAD,git diff HEADempty.The pins import
EditAppPageandCreateAppPageby relative path intosrc, so nodistsits in the resolution path and neither ablation needed a build leg.Consumer sweep
Population: 27 test files under
packages/plugin-designer/src(git ls-filesandfindagree). The five that touch the wizard,CreateAppPage,EditAppPageorNavigationDesigner(git grep for those names and their registry keys), at head5b2b8568bb:AppCreationWizard.requiredMarker-10367.test.tsx__tests__/AppWizard.specDocument-10867.test.tsx(the objectui#10867 pins)__tests__/EditAppPage.declaredKeys-10842.test.tsx__tests__/EditAppPage.keepsNavigation-10894.test.tsx__tests__/NavigationDesigner.specNavTypes.test.tsxGates (head
5b2b8568bb)Heavy runs went through the shared verify lock (
os-verify-lock.sh,VERDICT command-exitlines quoted).pnpm exec vitest run packages/plugin-designer/:Test Files 27 passed (27),Tests 198 passed (198), VERDICT command-exit 0.pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-designer^...' build(16 packages): VERDICT command-exit 0. Thenpnpm --filter @object-ui/plugin-designer type-check, which echoedtsc --noEmit && tsc -p tsconfig.test.json: VERDICT command-exit 0.tsc -p tsconfig.test.json --listFilesincludes the new pin file.check:new-line-citations:0 new citation(s), exit 0.check:control-bytes: OK, exit 0.check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots: OK, exit 0.check:changeset-claims: "No pending changeset names a file this change touches", exit 0.check:pending-changeset-literals: exit 0.check:metadata-write-doors,check:unreferenced-sources,check:shell-escape-residue: OK, exit 0.scripts/check-changeset-presence.mjs: 1 released package, 1 changeset declared, exit 0.scripts/check-changeset-no-major.mjs: exit 0.scripts/check-governed-queue-guard.mjs --teston the four changed paths: NOT GOVERNED.--format json, 3 files): 0 errors. The repo lint (pnpm lint) is left to CI.Pending changesets that name
AppCreationWizardorEditAppPage(objectui#10367, objectui#10827, objectui#10842, objectui#10867) were read. None has a sentence this change makes false, so none gets a dated note.Serial
The claim read the open-PR file lists at
9f0c84a44: none touchespackages/plugin-designer/src. Re-read before this PR opened: 10 open PRs, none touchespackages/plugin-designer/src. The release PR's file list was read as one capped page of 100; it versions packages. PR objectui#10889, the file's previous holder, is merged.Acceptance notes
navObjectNamesfrom the wizard module keeps the change inside the claimed file surface. It costs onereact-refresh/only-export-componentswarning there (warnings are not a gate). TheEditAppPageedit removes oneno-explicit-anywarning.Generated by Claude Code