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): make the bulk-action option item's openness deliberate, not accidental (#4001) (#4909)
* fix(spec): make the bulk-action option item's openness deliberate, not accidental (#4001)
`BulkActionParamSchema`'s `options[]` entry was bare default-strip while the
strictness ledger's prose called it and its parent "both deliberately open" —
one intent, two postures, found by the #4001 2026-08-03 re-measure (#4852).
Per the maintainer's verdict A the code moves to the prose: `.passthrough()` on
the option item. Verified before writing, since the prose itself had just been
proven able to drift — objectui's option TYPE is closed
(`Array<{ label; value }>`, `packages/types/src/objectql.ts:271`), but the type
is not what an authored option meets: `bulkParamToField` SPREADS every entry
(`packages/plugin-grid/src/components/bulkParamToField.ts:131`) into the field
metadata, whose vocabulary is `SelectOptionMetadata`
(`packages/types/src/field-types.ts:288`) — `color` / `icon` / `disabled` /
`visibleWhen` beyond the pair, and read (`packages/fields/src/index.tsx:1089`).
So the strip was deleting widget config the renderer would have honoured.
The new test states the openness is deliberate and names the evidence, so a
future strict-ification sweep stops there instead of "fixing" it.
Ledger: `ui/bulk-action.zod.ts` reaches zero strip sites, so its remaining-strip
row is deleted and the header arithmetic reconciled (124 → 123 of 198). Both
directions of that gate were driven red before the green was believed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
* docs(spec): keep the option shape readable after the passthrough (#4001)
`gen:docs` renders a passthrough object inside an array as `Record<string,
any>[]`, so the reference page lost the `{ label, value }` shape the moment the
entry opened — an author-facing surface reading "shapeless" for something that
still has two required keys, which is the strip-era smell pointed the other way.
The declared pair now lives in the key's own `.describe()`, which the generator
renders verbatim, along with WHY the entry is open and which extra keys the
widget actually reads. Regenerated `content/docs/references/ui/bulk-action.mdx`
accordingly (the one artifact `check:generated` proved stale — not the set).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
---------
Co-authored-by: Claude <noreply@anthropic.com>
|**required**|`boolean`| optional | Blocks the Confirm button until a value is present. |
97
97
|**default**|`any`| optional | Value applied when the dialog opens. (An ActionParam spells this `defaultValue`.) |
98
-
|**options**|`{ label: string; value: string \| number \| boolean }[]`| optional | Static options for select-style widgets. |
98
+
|**options**|`Record<string, any>[]`| optional | Static options for select-style widgets. Each entry is `{ label, value }` plus any extra widget config — the entry is open (`.passthrough()`) because the renderer forwards unknown option keys to the field widget, which reads `color` / `icon` / `disabled` / `visibleWhen` beyond the declared pair. |
99
99
|**object**|`string`| optional | Target object for a `lookup` widget. (An ActionParam spells this `reference`.) |
100
100
|**labelField**|`string`| optional | Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id). |
101
101
|**multiple**|`boolean`| optional | Allow picking multiple values — the param value becomes an array and is written to the patch as-is. |
Copy file name to clipboardExpand all lines: docs/audits/2026-07-unknown-key-strictness-ledger.md
+15-5Lines changed: 15 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -488,7 +488,7 @@ not verdicts).
488
488
|---|---|---|---|
489
489
|`action.zod.ts`| 8 | authorable | param schema strict (#3746); remaining blocks ride later steps. **9 → 8 at the #4001 re-measurement** — no schema changed: the ninth "site" was a `z.object(…)` inside a JSDoc paragraph, which the old textual counter could not tell from code |
490
490
|`view.zod.ts`| 50 | authorable | partially strict (ADR-0089); long tail of sub-blocks. `bulkActionDefs` left this file in #4457 — see the row below |
491
-
|`bulk-action.zod.ts`| 3 | authorable |**strict as of #4457** — `BulkActionDefSchema` (the def itself). It was `z.array(z.record(z.string(), z.any()))` inline in `view.zod.ts`: a selection-bar button with **no shape at all**, so `opeartion` / `excution: 'aggregate'` parsed and shipped as a button that ran the default behaviour. Its two other sites are `BulkActionParamSchema` and that param's `options` entry, both deliberately **open**: objectui's `BulkActionParam` declares a `[key: string]: unknown` catch-all for widget config (min/max/step/format), so `.passthrough()` is the honest mirror and strictness there would reject valid config — same call as `dashboard.zod.ts`'s widget `config`. The def also refuses the combinations the executor never reads (`patch` outside an update, `execution` outside a custom, `batchSize` on an aggregate) and a hand-written `actionDef`, which is renderer-attached |
491
+
| `bulk-action.zod.ts` | 3 | authorable | **strict as of #4457** — `BulkActionDefSchema` (the def itself). It was `z.array(z.record(z.string(), z.any()))` inline in `view.zod.ts`: a selection-bar button with **no shape at all**, so `opeartion` / `excution: 'aggregate'` parsed and shipped as a button that ran the default behaviour. Its two other sites are `BulkActionParamSchema` and that param's `options` entry, both deliberately **open** and both now `.passthrough()` — the param because objectui's `BulkActionParam` declares a `[key: string]: unknown` catch-all for widget config (min/max/step/format), so passthrough is the honest mirror and strictness would reject valid config (same call as `dashboard.zod.ts`'s widget `config`); the OPTION ENTRY on separate measured evidence, since its objectui type is closed and only the runtime path is open — `bulkParamToField` spreads each entry (`plugin-grid/src/components/bulkParamToField.ts:131`) into `SelectOptionMetadata` (`types/src/field-types.ts:288`), which declares and reads `color` / `icon` / `disabled` / `visibleWhen`. **This row said "both deliberately open" while only the parent was `passthrough`** — one intent, two postures, caught by the 2026-08-03 re-measure and closed by the ruling's verdict A (make the code match the prose). The lesson is the campaign's own: prose in this ledger is not a posture reading, which is why the remaining-strip map is gated and this column is not. The def also refuses the combinations the executor never reads (`patch` outside an update, `execution` outside a custom, `batchSize` on an aggregate) and a hand-written `actionDef`, which is renderer-attached |
492
492
|`component.zod.ts`| 29 | authorable |**next candidate** — SDUI component defs; check React-prop open slots first (p) |
|`app.zod.ts`| 18 | authorable |**strict as of #4001 PR B** — `AppSchema` + branding / area / context-selector / contribution, and the nav-item union converted to `z.discriminatedUnion('type', …)` (the union-error question, settled empirically: matched-branch-only errors, exact recursive paths, `toJSONSchema` clean). Per-target `params` stay open. PR A (#4142) tombstoned the seven audit-dead keys first |
@@ -625,7 +625,7 @@ is complete and so nobody re-triages them from scratch next batch.
625
625
626
626
**Authorable strip in `automation/`: 41 of 67.** This is the ruling's "known main body".
627
627
628
-
#### `ui/` — 124 strip of 198
628
+
#### `ui/` — 123 strip of 198
629
629
630
630
| File | Strip | Sites | Class | Batch |
631
631
|---|---|---|---|---|
@@ -647,13 +647,23 @@ is complete and so nobody re-triages them from scratch next batch.
|`action.zod.ts`| 1 | 8 | authorable |`ActionParamSchema.options` — a plain `{ label, value }` pair; the cheapest win in the directory |
649
649
|`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 |
650
-
|`bulk-action.zod.ts`| 1 | 3 | open |`BulkActionParamSchema.options`. ⚠️ **The triage row calls both this and its parent "deliberately open", but only the PARENT is `passthrough` — this one is plain strip.** Same intent, two postures; decide which the intent actually was |
})).optional().describe('Static options for select-style widgets.'),
128
+
}).passthrough()).optional().describe('Static options for select-style widgets. Each entry is `{ label, value }` plus any extra widget config — the entry is open (`.passthrough()`) because the renderer forwards unknown option keys to the field widget, which reads `color` / `icon` / `disabled` / `visibleWhen` beyond the declared pair.'),
113
129
object: SnakeCaseIdentifierSchema.optional().describe("Target object for a `lookup` widget. (An ActionParam spells this `reference`.)"),
114
130
labelField: z.string().optional().describe('Related-object field used as the option label for a `lookup` widget (defaults to name/full_name/email/id).'),
115
131
multiple: z.boolean().optional().describe('Allow picking multiple values — the param value becomes an array and is written to the patch as-is.'),
0 commit comments