Skip to content

Commit ffbab85

Browse files
committed
feat(spec)!: close AriaProps against unknown keys, reclassify widget + five i18n shapes as no-door (#4001 批 16)
Fifteen strip sites measured per the #4852 remeasure; the counts held exactly (widget 9/9, i18n 6/6, confirmed against the ledger gate's own AST counter). Per-schema door measurement then split them one / fourteen. Closed with strictObject: ui/i18n.zod.ts AriaPropsSchema The highest-fan-out single site the ui/ wave has closed — carried as `aria:` on ~30 shapes under six metadata-type roots (ListView, Page, PageComponent, DashboardWidget, ChartConfig, Action, 20 SDUI component defs). Through the view root, `aria: { label, describedBy }` parsed clean and came back `aria: {}`: the accessible name existed in the source file and nowhere else. Those two spellings are objectui's own ARIA_KEY_ALIASES, i.e. what stored view metadata carries. Reclassified as no door, NOT tightened (14 sites): ui/widget.zod.ts all six shapes / nine sites ui/i18n.zod.ts I18nObject, PluralRule, NumberFormat, DateFormat, LocaleConfig Measured three ways with positive AND negative controls in the same run: no carrier key (nothing imports widget.zod but the barrel), unreachable in a 4766-node BFS from the 24 roots + defineStack, zero parse in all three repos (cloud checked by code search against a proven-working index). A synthetic carrier flips all of them, so the verdict is a fact about the graph. Curation anchored on named sibling contracts, not edit distance: the two legacy spellings on objectui's normalizer, and `ariaRole` on this shape's own inconsistent prefix (two of three keys carry `aria`, `role` does not). The four the distance fallback already reaches were measured first and left to it. `live` and `ariaLabelledBy` get a prescription rather than a rename — finding 7 discipline. `live` is real and rendered by objectui's ListView alone, which declares it via `AriaPropsSchema.extend({ live })`, so that surface keeps accepting it and now inherits this error map; promoting it into the shared shape would advertise aria-live on 29 renderers that do not implement it (#5058). A `.strip()` was added to four files this batch does not otherwise touch: animation, dnd (x2), keyboard, touch build their shapes as `z.object({…}).merge(AriaPropsSchema.partial())`, and `.merge()` adopts the incoming posture — closing AriaProps would have silently closed all five of those no-door shapes with zod's generic message and no changeset. Pinned. Ledger finding 20: the campaign's own BFS reported widget.zod.ts as REACHABLE. Zod's `.describe()` returns a clone sharing the original `_zod.def`, so a bridge firing on any ONE shared property under a shared name links unrelated shapes via `name`/`label`. 2 keys of 20. The error is one-directional — it can only make a batch tighten something dead (#4583). Corrected to whole-shape overlap in ui/door-reachability.testkit.ts, pinned as a regression test, filed as #5056. Instruments proven red before trusting green: six sabotages, one per assertion class (walker, clone bridge, strictness, curation, finding-7 guidance, merge-strip), each reverted. Ledger: ui/ 118 strip of 198, authorable subtotal 82 — both recomputed from the surviving rows, never decremented by this batch's count. Refs #4001. Filed out of scope: #5055 (ADR-0049 for the 14 no-door sites), #5056, #5058, #5059.
1 parent c5e7bd9 commit ffbab85

11 files changed

Lines changed: 770 additions & 19 deletions
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
Close `AriaProps` against unknown keys, and reclassify `widget` + five `i18n` shapes as no-door (#4001 batch 16, ADR-0078)
6+
7+
zod's default is `.strip`: a key a schema does not declare is silently discarded
8+
and the parse still succeeds. On an authoring surface that is the worst failure
9+
mode — the author (increasingly, an AI) gets a success envelope and ships
10+
metadata that quietly ignores what they wrote.
11+
12+
**BREAKING — one shape.** `AriaPropsSchema` (`ui/i18n.zod.ts`) now raises a
13+
named, fixable error instead of dropping the key. It is carried as `aria:` on
14+
roughly thirty live shapes under six metadata-type roots — `ListViewSchema`,
15+
`PageSchema`, `PageComponentSchema`, `DashboardWidgetSchema`, `ChartConfigSchema`,
16+
`ActionSchema`, and twenty SDUI component defs — so this is the highest-fan-out
17+
single site the `ui/` wave has closed.
18+
19+
**What it was doing.** Through the `view` metadata root, this parsed **clean**:
20+
21+
```ts
22+
getMetadataTypeSchema('view').parse({
23+
listViews: { my_view: { type: 'grid', columns: ['name'],
24+
aria: { label: 'Accounts', describedBy: 'accounts-help' } } },
25+
})
26+
// → aria: {}
27+
```
28+
29+
Both keys gone, reported valid. The accessible name existed in the source file
30+
and nowhere else — a screen-reader user hears the DOM default, and nothing in the
31+
toolchain ever said so. Those two spellings are not hypothetical: they are what
32+
objectui's `ARIA_KEY_ALIASES` normalizer folds at the `ListView` boundary
33+
(objectui#2890), i.e. what stored view metadata actually carries.
34+
35+
**The renames, each anchored to a named sibling contract.**
36+
37+
| you wrote | write instead | where the wrong word comes from |
38+
|---|---|---|
39+
| `label` | `ariaLabel` | objectui's stored legacy spelling, folded by `normalizeListViewSchema` |
40+
| `describedBy` | `ariaDescribedBy` | same |
41+
| `ariaRole` | `role` | this shape's own inconsistency — two of its three keys carry the `aria` prefix and `role` does not |
42+
43+
`arialabel`, `ariaLabell`, `ariadescribedby`, `aria-label` and `roles` are left to
44+
the edit-distance fallback, measured before anything was hand-written: an alias
45+
for a key the fallback already reaches is transcription, not judgement.
46+
47+
**Two keys get a prescription instead of a rename**, because renaming them would
48+
be wrong (the ledger's finding 7 — this campaign's own fix once signposting the
49+
way into the failure it exists to kill):
50+
51+
- `live` is real and rendered — by objectui's `ListView` alone, which reads
52+
`schema.aria?.live` and emits `aria-live`. objectui declares it as
53+
`AriaPropsSchema.extend({ live })`, so **that surface keeps accepting it** (and
54+
now inherits this error map for everything else). On any other surface the
55+
message says where `live` IS valid rather than pointing at a declared key that
56+
means something else. Promoting it into the shared shape would advertise
57+
`aria-live` on twenty-nine renderers that do not implement it; the promotion
58+
question is **#5058**.
59+
- `ariaLabelledBy` / `labelledBy``aria-labelledby` references another
60+
element's id, which is not the same thing as `ariaLabel` (a literal string), so
61+
there is nothing to rename it to. The gap is named, and is also #5058.
62+
63+
**A `.strip()` was added to four files this batch did not otherwise touch.**
64+
`animation.zod.ts`, `dnd.zod.ts` (×2), `keyboard.zod.ts` and `touch.zod.ts` build
65+
their config shapes as `z.object({…}).merge(AriaPropsSchema.partial())`, and
66+
`.merge()` adopts the incoming schema's unknown-key posture — so closing
67+
`AriaProps` would have silently closed all five of those shapes too, with zod's
68+
generic message and against #4988's measured verdict that nothing parses them.
69+
The explicit `.strip()` holds their posture; `i18n.test.ts` pins it.
70+
71+
**Nothing in `ui/widget.zod.ts` changed, and five of `ui/i18n.zod.ts`'s six
72+
shapes were left open** — deliberately, on measurement. The ledger scheduled
73+
`widget` as `authorable (p)` / 9 sites and warned that `i18n`'s label shapes were
74+
"wide-open records by design"; resolving both found something more specific.
75+
`widget.zod.ts` has no authoring door at all: nothing under `packages/spec/src`
76+
imports it except the barrel, a BFS from all 24 metadata-type roots plus
77+
`defineStack` never reaches it, and no `.parse()` on any of its shapes exists in
78+
`objectstack`, `objectui` or `cloud` outside its own tests. The same holds for
79+
`I18nObjectSchema`, `PluralRuleSchema`, `NumberFormatSchema`, `DateFormatSchema`
80+
and `LocaleConfigSchema`. `.strict()` is a property of a parse; there is no parse.
81+
Retiring them or giving them a carrier is ADR-0049 enforce-or-remove, tracked in
82+
**#5055** — not a breaking change to spend here.
83+
84+
The warning about the open record was aimed one level off, and both levels are
85+
now recorded: `I18nObject.params` is a `z.record` interpolation bag whose key
86+
space is whatever the message template names — openness there is the contract, and
87+
it was never a site this ratchet could close. The config block the map assumed was
88+
open alongside it (`AriaProps`) turned out to be the directory's most widely
89+
carried live shape.
90+
91+
Zero-breakage evidence: full `@objectstack/spec` suite, `tsc --noEmit`, all ten
92+
spec `check:*` gates, `objectstack validate` on app-showcase / app-crm / app-todo,
93+
and an ADR-0087 direct-parse probe over the three apps' **built** artifacts —
94+
zero `aria` slots present, with the probe's negative control proven red on a
95+
legacy-spelled block.

0 commit comments

Comments
 (0)