You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec): revert ListView.sort strictness — the console stamps a UI row id
The full suite caught this: `view-metadata-schema.test.ts` pins
`sort: [{ id, field, order }]` as "the exact shape normalizeViewMetadata
persists on a console column-sort PUT", and objectui stamps that `id` per row
(components/src/custom/sort-builder.tsx:68/:94, crypto.randomUUID()). Closing
the sort entry made a console column-sort a 422.
The mechanism is the finding, and it governs every nested block in this file:
`.strip()` does NOT recurse, any more than `.strict()` does. ViewMetadataSchema
rescues Studio's round-trip keys by making its flattened members `.strip()`, but
that re-opens the TOP level only — a nested block closed inside ListViewSchema is
still reached through that member, so a console-stamped key inside it 422s
regardless of the member's own posture. The union's block comment read as though
the rescue reached all the way down; it does not, and it now says so.
`id` was deliberately NOT declared to silence the rejection. It is a React list
key, not protocol: declaring it would put a UI artifact on the authorable surface
and teach an AI author to emit a UUID. The end state is #5074's authoring/wire
split applied one level down.
Also adds the variant-docs entry my discriminatedUnion conversion made visible.
`FormView.submitBehavior`'s four variants were always authorable; as a plain
z.union the gate could not see them. GOVERNED (ui/forms.mdx names all four), not
exempt.
Ledger: ui/ 74 -> 75 strip, authorable 34 -> 35, recomputed from surviving rows.
Filed from this: #5114 — ViewFilterRuleSchema (closed by an EARLIER batch, live
on main) rejects the same class of console-stamped `id` on filter rows.
Part of #4001
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Copy file name to clipboardExpand all lines: docs/audits/2026-07-unknown-key-strictness-ledger.md
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -755,12 +755,12 @@ it the same way: the decision is also written beside the schema and pinned in a
755
755
test (`flow.test.ts`, `etl.test.ts`), because a row in a table is not where the
756
756
next person to open that file will look.
757
757
758
-
#### `ui/` — 74 strip of 198
758
+
#### `ui/` — 75 strip of 198
759
759
760
760
| File | Strip | Sites | Class | Batch |
761
761
|---|---|---|---|---|
762
762
|`component.zod.ts`| 29 | 29 | authorable (p) | Largest single block left. SDUI component props — **verify the React-prop open slots first**; `check:react-declaration-parity` compares two DECLARATIONS and cannot tell you which props a renderer reads |
763
-
| `view.zod.ts` | 4 | 50 | mixed | **16 of 20 closed at #4001 批 18**; the 4 that remain are each measured, and none is unfinished work. Closed: `ViewDataSchema`'s four provider arms, `UserFilterField.options`, `GanttQuickFilter.options`, `GanttConfig.tooltipFields`, `ListView.sort` / `.conditionalFormatting` / `.emptyState`, `FormFieldBase.keyField`, `FormView.subforms`, and `submitBehavior`'s four arms. Reachability was measured, not assumed: a BFS from all 24 metadata-type roots plus `ObjectStackSchema` resolves every one `root-graph`, with `ViewSchema`/`FormViewSchema`/`ViewItemSchema`/`PageSchema` as positive controls and 批 13's no-door shapes UNREACHABLE **in the same run** — and the instrument had to be fixed first: `lazySchema` returns a Proxy, but a carrier writes `X.optional()`, which RESOLVES it, so the closure holds the real instance and comparing the Proxy alone false-negatived `ViewDataSchema` (caught by cross-checking its two literal carrier keys, not by trusting the reading). ⚠️ **Re-checked against #5056**: every 批 18 target is `root-graph` by **identity**, so **none** of the sixteen rests on the `derived-clone` bridge that 批 16 found can mark a dead shape reachable. The one `derived-clone` verdict in the run is `ListViewSchema` — a positive CONTROL, not a target, and independently identity-reachable via `ObjectListViewSchema`. Every closed shape also has a literal carrier key in this file and a named parse door (`defineView` / `defineViewItem` / the `view` metadata-type schema / objectui's `GanttConfigSchema.safeParse` at `plugin-gantt/src/ObjectGantt.tsx:408`) — the strong-evidence class #5056 leaves standing. Curation anchored to named siblings: `direction → order` is the alias #4721 put on `SortNodeSchema` for the identical tuple (it measured `{field, direction:'desc'}` parsing to `{field, order:'asc'}` — a silently REVERSED sort); an option `count` gets a wrong-layer pointer to `showCount` because objectui COMPUTES it per render; and a bare `name` on the `object` data source is deliberately NOT aliased — it is a real key on the view ITEM, so a rename would be finding 7 again. `submitBehavior` became a `discriminatedUnion` on the `kind` literal it already required: as a plain union of four strict members the rejection is an `invalid_union` whose prescription #5014 measured the renderers flattening away. ⚠️ **`GanttConfigSchema` / `TreeConfigSchema` are `strictObject(…).passthrough()`** — open at the parent by design, and this ledger's own counter reads them as `strict` because `postureOf` returns early on the `strictObject` idiom without walking the chain (**#5072**); it inflates the strict count and does not affect this row's strip count. **Still open, all four measured:** `UserFiltersSchema` — closing it would 422 `allowAddTab`, which objectui's renderer reads (`plugin-list/src/UserFilters.tsx:182`/`:742`) and the spec never declared; `saveMetaItem` validates but persists the ORIGINAL body, so the stripped key still reaches the renderer and the capability WORKS today — closing removes a capability rather than making a silent failure loud (**#5073**). The 批 6e reliance question IS answered: `ObjectUserFiltersSchema` is `.omit()`ed off this base and `.omit()` inherits posture, so the pin flips from "drops" to "rejects" — that flip is wanted, and gated only on `allowAddTab`. `ViewItemSchema` ×2 — **wire, not authorable**: objectui's pin control PUTs `{...storedItem, isPinned}` (`ObjectView.tsx:882` → `data-objectstack/src/index.ts:2801`); a stored ViewItem record carries `viewKind` AND `config`, so it lands on THIS member (the flattened members are excluded by their `config: z.undefined()` guard) and closing it would 422 pinning a saved view (**#5074**). `FormFieldBaseSchema` — a module-private BASE whose sole consumer already applies `.strict()` plus the ADR-0089 `strictVisibilityError` map; the door is closed, the ledger counts the base. Each verdict is recorded in three places (schema JSDoc + `view-strictness-batch18.test.ts` + this row) |
763
+
| `view.zod.ts` | 5 | 50 | mixed | **15 of 20 closed at #4001 批 18**; the 5 that remain are each measured, and none is unfinished work. Closed: `ViewDataSchema`'s four provider arms, `UserFilterField.options`, `GanttQuickFilter.options`, `GanttConfig.tooltipFields`, `ListView.conditionalFormatting` / `.emptyState`, `FormFieldBase.keyField`, `FormView.subforms`, and `submitBehavior`'s four arms. Reachability was measured, not assumed: a BFS from all 24 metadata-type roots plus `ObjectStackSchema` resolves every one `root-graph`, with `ViewSchema`/`FormViewSchema`/`ViewItemSchema`/`PageSchema` as positive controls and 批 13's no-door shapes UNREACHABLE **in the same run** — and the instrument had to be fixed first: `lazySchema` returns a Proxy, but a carrier writes `X.optional()`, which RESOLVES it, so the closure holds the real instance and comparing the Proxy alone false-negatived `ViewDataSchema` (caught by cross-checking its two literal carrier keys, not by trusting the reading). ⚠️ **Re-checked against #5056**: every 批 18 target is `root-graph` by **identity**, so **none** of the fifteen rests on the `derived-clone` bridge that 批 16 found can mark a dead shape reachable. The one `derived-clone` verdict in the run is `ListViewSchema` — a positive CONTROL, not a target, and independently identity-reachable via `ObjectListViewSchema`. Every closed shape also has a literal carrier key in this file and a named parse door (`defineView` / `defineViewItem` / the `view` metadata-type schema / objectui's `GanttConfigSchema.safeParse` at `plugin-gantt/src/ObjectGantt.tsx:408`) — the strong-evidence class #5056 leaves standing. ⚠️ **`ListView.sort` was closed and then REVERTED, and that is the batch's most useful finding.** It carried `direction → order`, the #4721 alias for the identical tuple (`{field, direction:'desc'}` parsed to `{field, order:'asc'}` — a silently REVERSED sort). The full suite then failed one case: `view-metadata-schema.test.ts` pins `sort: [{ id, field, order }]` as the exact body a console column-sort PUT persists, and objectui stamps that `id` per row (`components/src/custom/sort-builder.tsx:68`/`:94`, `crypto.randomUUID()`). **The mechanism governs every nested block in this file and is the opposite of what the union's own comment implies: `.strip()` does NOT recurse.** `ViewMetadataSchema` rescues Studio's round-trip keys by making its flattened members `.strip()`, but that re-opens the TOP level only — a nested block closed inside `ListViewSchema` is still reached through that member, so a console-stamped key inside it becomes a 422 regardless. `id` was deliberately NOT declared to silence it: it is a React list key, and declaring it would put a UI artifact on the authorable surface and tell an AI author to emit one. The end state is #5074's authoring/wire split applied one level down; until then the shape stays open rather than half-closed against the platform's own writes. Curation on what DID close is anchored to named siblings: an option `count` gets a wrong-layer pointer to `showCount` because objectui COMPUTES it per render; and a bare `name` on the `object` data source is deliberately NOT aliased — it is a real key on the view ITEM, so a rename would be finding 7 again. `submitBehavior` became a `discriminatedUnion` on the `kind` literal it already required: as a plain union of four strict members the rejection is an `invalid_union` whose prescription #5014 measured the renderers flattening away. ⚠️ **`GanttConfigSchema` / `TreeConfigSchema` are `strictObject(…).passthrough()`** — open at the parent by design, and this ledger's own counter reads them as `strict` because `postureOf` returns early on the `strictObject` idiom without walking the chain (**#5072**); it inflates the strict count and does not affect this row's strip count. **Still open, all five measured:** `UserFiltersSchema` — closing it would 422 `allowAddTab`, which objectui's renderer reads (`plugin-list/src/UserFilters.tsx:182`/`:742`) and the spec never declared; `saveMetaItem` validates but persists the ORIGINAL body, so the stripped key still reaches the renderer and the capability WORKS today — closing removes a capability rather than making a silent failure loud (**#5073**). The 批 6e reliance question IS answered: `ObjectUserFiltersSchema` is `.omit()`ed off this base and `.omit()` inherits posture, so the pin flips from "drops" to "rejects" — that flip is wanted, and gated only on `allowAddTab`. `ViewItemSchema` ×2 — **wire, not authorable**: objectui's pin control PUTs `{...storedItem, isPinned}` (`ObjectView.tsx:882` → `data-objectstack/src/index.ts:2801`); a stored ViewItem record carries `viewKind` AND `config`, so it lands on THIS member (the flattened members are excluded by their `config: z.undefined()` guard) and closing it would 422 pinning a saved view (**#5074**). `FormFieldBaseSchema` — a module-private BASE whose sole consumer already applies `.strict()` plus the ADR-0089 `strictVisibilityError` map; the door is closed, the ledger counts the base. `ListView.sort` — reverted, see above. Each verdict is recorded in three places (schema JSDoc + `view-strictness-batch18.test.ts` + this row) |
764
764
|`widget.zod.ts`| 9 | 9 |**no door**| ⛔ **not strictness work** — the whole file measured unreachable from every authoring root (#4001 批 16), with no carrier key and zero parse in all three repos. ADR-0049 triage is **#5055**. See the triage row above, including why the campaign's own BFS said otherwise first (**#5056**) |
765
765
|`chart.zod.ts`| 2 | 7 |**no gate**|`ChartAggregateSchema` + `ChartGroupBySchema`'s object arm. Config / axis / series / annotation / interaction closed at 批 15; these two are NOT unfinished work — their carrier (`<ObjectChart aggregate>`) is live but nothing parses them, so closing them would gate nothing (#4583). Blocked on wiring the react-page publish gate to parse the schema instead of re-deriving it — see the triage row |
766
766
|`touch.zod.ts`| 7 | 7 |**no door**| ⛔ **not strictness work** — measured unreachable from every authoring root (#4001 批 13); ADR-0049 triage is #4988. See the triage row above |
@@ -819,20 +819,20 @@ metadata-type roots), and it was returning `aria: {}` for a legacy-spelled block
819
819
820
820
**批 18 is the ninth instance.** It computed 84 against a tree where 批 16's
821
821
rows still existed (`widget` still `authorable` at 9, `i18n` still 6) — right
822
-
against its own branch, wrong against the merge, which is **74**: a number
822
+
against its own branch, wrong against the merge, which is **75**: a number
823
823
neither side wrote down. Git conflicted three regions here (header, the
824
824
`view`/`widget` row pair, and this paragraph) and every row from both sides was
825
825
kept before the arithmetic was redone from them. Worth naming because 批 16 and
826
826
批 18 moved the same two numbers for OPPOSITE reasons — 批 16 by reclassifying 14
827
-
sites it did not touch, 批 18 by closing 16 it did — and the merged subtotal is
827
+
sites it did not touch, 批 18 by closing 15 it did — and the merged subtotal is
828
828
not reachable by applying either delta to the other's base.
829
829
830
-
**Authorable strip in `ui/`: 34 of 74** (was 123 of 123 when the ruling was
830
+
**Authorable strip in `ui/`: 35 of 75** (was 123 of 123 when the ruling was
831
831
written). Recomputed from the surviving rows at 批 18, not decremented:
832
-
29+4+9+2+7+5+4+4+4+3+1+1+1 = 74, of which 40 are the two no-parse classes, so
833
-
the authorable half is `component` 29 + `view`4 + `app` 1 = 34. `app.zod.ts`'s
832
+
29+5+9+2+7+5+4+4+4+3+1+1+1 = 75, of which 40 are the two no-parse classes, so
833
+
the authorable half is `component` 29 + `view`5 + `app` 1 = 35. `app.zod.ts`'s
834
834
single site is held pending the finding-16 `.extend()` check rather than counted
835
-
as ready. **40 of the 74 are the two no-parse classes**: 38 `no door` — `touch`
835
+
as ready. **40 of the 75 are the two no-parse classes**: 38 `no door` — `touch`
836
836
(7), `animation` (4), `dnd` (4), `keyboard` (4) and `offline` (3) from 批 13,
837
837
`sharing.zod.ts`'s `EmbedConfig` and `notification.zod.ts`'s `NotificationAction`
838
838
from 批 14, and `widget.zod.ts` (9) plus `i18n.zod.ts`'s remaining 5 from 批 16
0 commit comments