@assure-one/design-system 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ /**
2
+ * CM-14 — hidden-input mirror finder (class X, report-only; plan §29,
3
+ * registry `C-HIDDEN-MIRRORS`, feeds Wave 4 W4-04).
4
+ *
5
+ * A *mirror* is a hidden `<input name="…">` a consumer added because a
6
+ * controlled design-system control emits no native form value ([CU §18]:
7
+ * "controlled design-system fields (Select, DatePicker) don't emit a native
8
+ * form value"). W4-04 gives those controls native participation, and the
9
+ * moment a call site passes `name` to the control **and** keeps its mirror,
10
+ * the form posts the field twice and the server action reads whichever the
11
+ * platform hands it first (plan §33, the duplicate-submission risk).
12
+ *
13
+ * Deleting a mirror therefore changes what the server receives, which is the
14
+ * one thing plan §29 never automates. CM-14 is report-only for ever: it
15
+ * names the site, the control, the field name and whether the control at
16
+ * that site already takes `name`, and a human decides each one.
17
+ *
18
+ * ## What it detects
19
+ *
20
+ * The applications do **not** put a mirror next to its control — they collect
21
+ * the mirrors at the top of the `<form>` and bind the control 200 lines
22
+ * lower. "Next to" is therefore read as *bound to the same state*:
23
+ *
24
+ * | rule | registry | what it is |
25
+ * | ------------------------- | ----------------- | ---------------------------------------------------------------- |
26
+ * | `mirror-shared-binding` | C-HIDDEN-MIRRORS | the hidden input's `value` reads a binding a DS control is bound to |
27
+ * | `mirror-in-form` | C-HIDDEN-MIRRORS | same `<form>` as a DS control that posts nothing today, no shared binding |
28
+ * | `unregistered-hidden-input` | (unregistered) | a hidden input with no DS control to mirror — a server-supplied field |
29
+ *
30
+ * The third rule is what makes the report reconcile with the audit's census
31
+ * of 64 hidden `<input>`s in PRO [CU §24]: that number counts every hidden
32
+ * input, and most of the auth-form ones carry a token or an email the server
33
+ * sent, not a mirrored control value. CM-14 reports all three classes so the
34
+ * difference is visible rather than asserted.
35
+ *
36
+ * ## What it deliberately does not do
37
+ *
38
+ * - It never writes a file (the runner refuses on class X).
39
+ * - It does not decide which of the two values is correct, and it does not
40
+ * report a `name` collision as an error: `<name>_from`/`_to`
41
+ * (DateRangePicker) and repeated `name`s read with `getAll()` are both
42
+ * legitimate.
43
+ * - It says nothing about hidden inputs the design system renders itself
44
+ * (DatePicker's own `<input type="hidden" name>`): those are inside the
45
+ * package, never in consumer source.
46
+ * - `<input type={expr}>` with a computed type is not resolved; it appears
47
+ * in `notTransformed` so the human sees what the scan could not read.
48
+ * - It does not look across files. A mirror in one file and its control in
49
+ * another is reported as `unregistered-hidden-input`, with the name, and
50
+ * left to the reader.
51
+ */
52
+ import { CHANGE_PROPS, VALUE_PROPS, analyseForms, isWithin, literalValue } from "../lib/forms.mjs";
53
+
54
+ export const meta = {
55
+ id: "CM-14",
56
+ title: "hidden-input mirror finder: hidden <input name> next to a design-system control",
57
+ class: "X",
58
+ oneShot: false,
59
+ requires: { codemods: [], dsVersion: null },
60
+ parses: ["code"],
61
+ includeTests: false,
62
+ usesTypeScript: true,
63
+ usesPostcss: false,
64
+ registryIds: ["C-HIDDEN-MIRRORS"],
65
+ };
66
+
67
+ /** How a finding is weighted in the report and in the Wave 4 work. */
68
+ export const SEVERITY = { high: "high", medium: "medium", low: "low" };
69
+
70
+ /**
71
+ * Design-system controls a consumer mirrors, and whether they take `name`
72
+ * today (design system 1.32.0).
73
+ *
74
+ * - `"yes"` — the control accepts `name` now: `Select`, `DatePicker` and
75
+ * `DateRangePicker` declare it themselves (`DatePicker` renders its own
76
+ * hidden input, `DateRangePicker` renders `<name>_from`/`<name>_to`), and
77
+ * `Checkbox`, `Switch`, `RadioGroup` and `Slider` inherit the Radix root
78
+ * props and spread them, so Radix's own hidden input posts the value.
79
+ * A mirror next to one of these is a *duplicate today* the moment the call
80
+ * site also passes `name`.
81
+ * - `"planned"` — no `name` today; native participation arrives with the
82
+ * Wave 4 item named in the comment. A mirror here is load-bearing until
83
+ * then and must not be removed.
84
+ *
85
+ * `tests/codemods/cm-14.test.mjs` checks this table against the design
86
+ * system's own sources, so it cannot drift from the components it describes.
87
+ */
88
+ export const NAME_CAPABILITY = {
89
+ Select: "yes", // src/primitives/select.tsx — `name?: string`
90
+ DatePicker: "yes", // renders `<input type="hidden" name>` with the ISO value
91
+ DateRangePicker: "yes", // renders `<name>_from` and `<name>_to`
92
+ Checkbox: "yes", // Radix checkbox root props
93
+ Switch: "yes", // Radix switch root props
94
+ RadioGroup: "yes", // Radix radio-group root props
95
+ Slider: "yes", // Radix slider root props
96
+ ClientSelect: "yes", // renders its own hidden `<input name>` (default `client_id`)
97
+ SearchSelect: "planned", // W4-12 (adapter over Combobox)
98
+ TeamMemberSelect: "planned", // W4-08/W4-12
99
+ MultiSelectField: "planned", // W4-08
100
+ ToggleGroup: "planned", // W4-16
101
+ PhoneCountryInput: "planned", // W4-04
102
+ OtpInput: "planned", // W4-04
103
+ Questions: "planned", // W4-04 (answer widgets)
104
+ };
105
+
106
+ /**
107
+ * Controls that already post a native value without any help, so a hidden
108
+ * input in the same form is not a mirror of them. They are still recorded as
109
+ * bindings, because a mirror of a controlled `Input` does exist ([CU §18]:
110
+ * `contact_name`) and is worth reporting — it just is not a design-system
111
+ * gap.
112
+ */
113
+ export const NATIVE_CONTROLS = new Set(["Input", "Textarea", "SearchInput", "SubmitButton"]);
114
+
115
+ const CONTROLS = new Set([...Object.keys(NAME_CAPABILITY), ...NATIVE_CONTROLS]);
116
+
117
+ /** Whether an `<input>` element is `type="hidden"`. */
118
+ const hiddenType = (el) => {
119
+ if (el.tag !== "input") return null;
120
+ const prop = el.props.get("type");
121
+ if (!prop) return null;
122
+ const literal = literalValue(prop);
123
+ if (literal === "hidden") return "literal";
124
+ if (prop.literals.includes("hidden")) return "computed";
125
+ return prop.expression ? "unknown" : null;
126
+ };
127
+
128
+ /** The bindings an attribute set reads, for the given attribute names. */
129
+ const bindingsOf = (el, names) => {
130
+ const out = new Set();
131
+ for (const name of names) {
132
+ const prop = el.props.get(name);
133
+ if (!prop) continue;
134
+ for (const id of prop.identifiers) out.add(id);
135
+ }
136
+ return out;
137
+ };
138
+
139
+ const intersect = (a, b) => [...a].filter((x) => b.has(x)).sort();
140
+
141
+ export function transform(file, { ts }) {
142
+ const facts = analyseForms(ts, file.source, file.rel);
143
+ const findings = [];
144
+ const notTransformed = [];
145
+
146
+ const hidden = [];
147
+ for (const el of facts.elements) {
148
+ const kind = hiddenType(el);
149
+ if (kind === null) continue;
150
+ if (kind === "unknown") {
151
+ notTransformed.push({
152
+ line: el.line,
153
+ reason: "dynamic-type",
154
+ detail: `<input type={${el.props.get("type").text}}> — the type is computed, so the scan cannot tell whether it is hidden`,
155
+ });
156
+ continue;
157
+ }
158
+ hidden.push(el);
159
+ }
160
+ if (!hidden.length) return { findings, notTransformed, parseErrors: facts.parseErrors };
161
+
162
+ // Every design-system control in the file, with the bindings it is bound to.
163
+ const controls = facts.elements
164
+ .filter((el) => el.isDs && CONTROLS.has(el.base))
165
+ .map((el) => ({
166
+ el,
167
+ bindings: new Set([...bindingsOf(el, VALUE_PROPS), ...bindingsOf(el, CHANGE_PROPS)]),
168
+ acceptsName: NAME_CAPABILITY[el.base] ?? null,
169
+ native: NATIVE_CONTROLS.has(el.base),
170
+ named: literalValue(el.props.get("name")) ?? (el.props.has("name") ? "(expression)" : null),
171
+ }));
172
+ /** Controls that post nothing today and are the reason a mirror exists. */
173
+ const needMirror = controls.filter((c) => !c.native);
174
+
175
+ for (const el of hidden) {
176
+ const nameProp = el.props.get("name");
177
+ const name = nameProp ? (literalValue(nameProp) ?? nameProp.text) : null;
178
+ const valueProp = el.props.get("value");
179
+ const reads = new Set(valueProp?.identifiers ?? []);
180
+
181
+ // 1. The strong signal: the mirror and a control read the same binding.
182
+ const shared = controls
183
+ .map((c) => ({ c, common: intersect(reads, c.bindings) }))
184
+ .filter((x) => x.common.length)
185
+ .sort((a, b) => b.common.length - a.common.length);
186
+ if (shared.length) {
187
+ const best = shared[0];
188
+ const duplicate = best.c.named !== null && best.c.acceptsName === "yes";
189
+ add(findings, {
190
+ line: el.line,
191
+ registryId: "C-HIDDEN-MIRRORS",
192
+ severity: duplicate
193
+ ? SEVERITY.high
194
+ : best.c.acceptsName === "yes"
195
+ ? SEVERITY.medium
196
+ : SEVERITY.low,
197
+ rule: "mirror-shared-binding",
198
+ scope: el.form === null ? "no-form" : "form",
199
+ component: best.c.el.component,
200
+ match: name === null ? '<input type="hidden">' : `name=${JSON.stringify(name)}`,
201
+ detail: {
202
+ name,
203
+ binding: best.common.join(", "),
204
+ acceptsName: best.c.acceptsName,
205
+ controlPassesName: best.c.named,
206
+ controlLine: best.c.el.line,
207
+ sameForm: isWithin(facts.elements, best.c.el.index, el.form),
208
+ alsoMatches: shared.slice(1).map((x) => x.c.el.component),
209
+ nativeControl: best.c.native,
210
+ },
211
+ confidence: "high",
212
+ });
213
+ continue;
214
+ }
215
+
216
+ // 2. The weak signal: the same `<form>` holds a control that posts
217
+ // nothing today, but nothing ties this input to it.
218
+ const inForm =
219
+ el.form === null
220
+ ? []
221
+ : needMirror.filter((c) => isWithin(facts.elements, c.el.index, el.form));
222
+ if (inForm.length) {
223
+ add(findings, {
224
+ line: el.line,
225
+ registryId: "C-HIDDEN-MIRRORS",
226
+ severity: SEVERITY.low,
227
+ rule: "mirror-in-form",
228
+ scope: "form",
229
+ component: [...new Set(inForm.map((c) => c.el.component))].sort().join(", "),
230
+ match: name === null ? '<input type="hidden">' : `name=${JSON.stringify(name)}`,
231
+ detail: {
232
+ name,
233
+ binding: null,
234
+ acceptsName: [...new Set(inForm.map((c) => c.acceptsName))].sort().join(", "),
235
+ controlPassesName: null,
236
+ controlLine: inForm[0].el.line,
237
+ sameForm: true,
238
+ },
239
+ confidence: "low",
240
+ });
241
+ continue;
242
+ }
243
+
244
+ // 3. Not a mirror: a hidden field carrying a value the server supplied.
245
+ add(findings, {
246
+ line: el.line,
247
+ registryId: null,
248
+ severity: SEVERITY.low,
249
+ rule: "unregistered-hidden-input",
250
+ scope: el.form === null ? "no-form" : "form",
251
+ component: null,
252
+ match: name === null ? '<input type="hidden">' : `name=${JSON.stringify(name)}`,
253
+ detail: {
254
+ name,
255
+ binding: valueProp?.text ?? null,
256
+ acceptsName: null,
257
+ controlPassesName: null,
258
+ dsControlsInFile: [...new Set(needMirror.map((c) => c.el.component))].sort(),
259
+ },
260
+ confidence: "medium",
261
+ });
262
+ }
263
+
264
+ return {
265
+ findings,
266
+ notTransformed,
267
+ parseErrors: facts.parseErrors,
268
+ context: {
269
+ hiddenInputs: hidden.length,
270
+ controls: [...new Set(controls.map((c) => c.el.component))].sort(),
271
+ },
272
+ };
273
+ }
274
+
275
+ const add = (findings, finding) => findings.push({ gate: null, ...finding });