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
* feat(spec)!: reject unknown keys on the responsive/SDUI-styling shapes (#4001 批 13)
Close all four sites in `ui/responsive.zod.ts` with `strictObject`. The nested
gap is what this fixes: `PageComponentSchema` has been `.strict()` since
ADR-0089 D3a and strictness does not recurse, so a component whose every styling
and layout instruction was written in the wrong breakpoint vocabulary parsed
clean and returned `{ responsiveStyles: {}, responsive: {} }`.
The file carries TWO breakpoint vocabularies sixteen lines apart on the same
component (ADR-0065 buckets vs the Tailwind ramp), so the aliases run both ways
and are anchored to the named sibling rather than to edit distance.
The other five files in this batch are NOT closed, on a measurement: their 22
sites have no authoring door (no carrier key; unreachable by BFS from all 24
metadata-type roots plus defineStack, with three positive controls passing in
the same run; no .parse() anywhere in three repos). ADR-0049 triage is #4988.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
* docs(spec): record the measured binding door for the responsive strictness (#4001 批 13)
The door is getMetadataTypeSchema('page') — MetadataManager.validate, GET
/api/v1/meta, the Studio page form. It is NOT objectstack build/validate: a key
PageComponentSchema has rejected since ADR-0089 D3a passes both and lands in the
built artifact. Pre-existing, filed as #5000; recorded here so the campaign's
usual "three example apps validate" line is not read as evidence for this
surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/audits/2026-07-unknown-key-strictness-ledger.md
+51-13Lines changed: 51 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,6 +20,13 @@ One question decides the class: **who writes this schema's input?**
20
20
|**authorable**| A human or AI author, into `*.object.ts` / `defineStack` config / Studio / MCP |`.strict()` + fixable error (the ratchet target) |
21
21
|**wire**| Another machine: server responses, connector payloads, runtime envelopes, persisted runtime state | stay tolerant (`.strip` / `.passthrough`); strictness here turns an upstream *addition* into our parse crash |
22
22
|**open**| Deliberately schemaless user data (record bodies, per-node-type `config`, React props) | stay open; a *sibling* contract validates it (e.g. a node executor's `configSchema`, #4027/#4040) |
23
+
|**no door**|**Nobody — nothing parses it.** The shape is exported and typed, but no schema declares a carrier key for it, so it is unreachable from every metadata-type root and from `defineStack`. Added at 批 13, when the first run of files resolved its `(p)` this way |**out of this ratchet's scope.**`.strict()` is a property of a PARSE; with no parse it enforces nothing and only makes a dead slot look load-bearing (#4583). The live question is ADR-0049 enforce-or-remove — retire the vocabulary or give it a carrier — so a row here points at an issue, never at a batch |
24
+
25
+
A fourth answer to "who writes this input" is **nobody**, and it is only
26
+
reachable by measurement rather than by reading the file: `no door` was added at
27
+
批 13 after a BFS from every authoring root (with positive controls) came back
28
+
empty on five `ui/` files at once. Reading a schema's exports and JSDoc cannot
29
+
distinguish it from `authorable` — which is exactly why the `(p)` exists.
23
30
24
31
Mixed files carry both — classify per schema, not per file. A **response-side
25
32
extension of an authoring schema** (e.g. `EffectiveObjectPermissionSchema`)
|`chart.zod.ts` / `i18n.zod.ts` / `responsive.zod.ts`| 7+6+4 | authorable (p) | i18n label shapes are wide-open records by design — verify. **`chart` 6 → 7 at the re-measurement** — again no schema changed: `ChartAggregateSchema` is written `z\n .object({`, and the old counter's `z\.object\(` could not match across the line break |
|`offline.zod.ts` / `report.zod.ts`| 3 ea | authorable (p) ||
505
+
|`chart.zod.ts` / `i18n.zod.ts`| 7+6 | authorable (p) | i18n label shapes are wide-open records by design — verify. **`chart` 6 → 7 at the re-measurement** — again no schema changed: `ChartAggregateSchema` is written `z\n .object({`, and the old counter's `z\.object\(` could not match across the line break |
506
+
| `responsive.zod.ts` | 4 | authorable | **strict as of #4001 批 13** — all four sites (`ResponsiveConfig`, `ResponsiveStyles`, and the two per-breakpoint maps). This is the one file of batch 13's six whose `(p)` resolved POSITIVE, and it resolved on the graph rather than on the file's face: `page.components[].responsive` / `.responsiveStyles` put both shapes inside the `page` metadata-type root (`dashboard.widgets[].responsive` was the second carrier until #4876 retired it, same day). What the closure bought is the batch's whole argument in one parse — **`PageComponentSchema` has been `.strict()` since ADR-0089 D3a and that never reached these blocks**, so `{ type:'element:text', responsiveStyles: { lg: {…} }, responsive: { colums: {…}, hideOn: [] } }` parsed CLEAN and returned `responsiveStyles: {}, responsive: {}` — every styling and layout instruction the author wrote, gone, reported valid. A strict shell over strip-mode children is a closed surface's silhouette, not a closed surface. The curation is the file's real hazard rather than typos: it carries TWO breakpoint vocabularies sixteen lines apart on the same component (`responsiveStyles`' `large`/`medium`/`small`/`xsmall`, ADR-0065, against `responsive`'s Tailwind `xs`…`2xl`), so the aliases run BOTH ways between them and are anchored to the named sibling, not to edit distance — batch 12's method, and the only thing that can answer `lg` → `large`. Two entries had to be measured rather than reasoned: `{ columns: { large: 4, lg: 3 } }` used to keep HALF the map (the node laid out, at the wrong width, on breakpoints the author never named — worse than a total loss, which is at least visible); and `hideOn` → `hiddenOn` needed a hand-written alias because the distance fallback provably cannot reach it — it lowercases the input but not the candidates, so a capital in a declared key costs an extra edit against a budget of 2, and the all-lowercase `hiddenon` resolves while the correctly-cased `hideOn` does not. That asymmetry is general to camelCase keys, i.e. to most of the spec, and is filed as **#4990**. `StyleMapSchema` stays deliberately OPEN (its key space is every CSS property; objectui's `declarations()` emits whatever it is handed) — recorded in the schema JSDoc, in a test pin, and in this row |
| `animation.zod.ts` / `dnd.zod.ts` / `keyboard.zod.ts` / `touch.zod.ts` / `offline.zod.ts` | 4+4+4+7+3 | ~~authorable (p)~~ **no door** | **no authoring door (measured, #4001 批 13)** — the `(p)` resolved NEGATIVE and the row is kept only so the arithmetic stays complete. Three independent measurements on 2026-08-03: (1) nothing under `packages/spec/src` imports these modules except the `ui/index.ts` barrel, so no schema anywhere declares a carrier key for them; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` — the closure `build-schemas.ts` uses for the #4650 deletion check — reaches none of the 22 sites, while its three positive controls (`PageSchema`, batch 11's `WebhookSchema`, batch 10's `StateMachineSchema`) all resolve `root-graph` in the same run; (3) no `.parse()` / `.safeParse()` on any of them exists in `objectstack`, `objectui` or the example apps outside their own unit tests — objectui re-exports the inferred TYPES only and says so (#2561). `.strict()` is a property of a PARSE and there is no parse, so closing them would enforce nothing and would spend a v17 breaking change to leave *"a precisely validated dead slot — the more convincing lie"* (the #4583 row below). The live question is ADR-0049 enforce-or-remove, filed as **#4988**; each file's header comment and its test file carry the same verdict (the batch 12 three-places standard). **Do not reschedule these as strictness work** — that is what the `(p)` was for, and it has been answered |
509
+
|`report.zod.ts`| 3 | authorable (p) ||
501
510
|`notification.zod.ts`| 1 | authorable (p) |**#4610 dropped two sites** — the `./ui``Notification` (toast/banner instance) and `NotificationConfig` (toaster global config) shapes were removed: zero importers in all three repos, and both shadowed live names owned elsewhere (`./api` owns the inbox row). What remains is `NotificationActionSchema`, part of the presentation vocabulary the ui entry keeps |
|`action.zod.ts`| 1 | 8 | authorable |`ActionParamSchema.options` — a plain `{ label, value }` pair; the cheapest win in the directory |
688
696
|`app.zod.ts`| 1 | 18 | verify |`BaseNavItemSchema` — the base the strict discriminated-union members extend. Closing a base that is `.extend()`ed is the #4001 trap that bit `view` (finding 16); confirm the members' strictness is not already covering it before touching |
0 commit comments