uidex 0.5.2 → 0.7.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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/cli.cjs +1542 -1227
  3. package/dist/cli/cli.cjs.map +1 -1
  4. package/dist/cloud/index.cjs +385 -175
  5. package/dist/cloud/index.cjs.map +1 -1
  6. package/dist/cloud/index.d.cts +192 -4
  7. package/dist/cloud/index.d.ts +192 -4
  8. package/dist/cloud/index.js +377 -177
  9. package/dist/cloud/index.js.map +1 -1
  10. package/dist/headless/index.cjs +116 -251
  11. package/dist/headless/index.cjs.map +1 -1
  12. package/dist/headless/index.d.cts +6 -11
  13. package/dist/headless/index.d.ts +6 -11
  14. package/dist/headless/index.js +116 -253
  15. package/dist/headless/index.js.map +1 -1
  16. package/dist/index.cjs +776 -1055
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +152 -160
  19. package/dist/index.d.ts +152 -160
  20. package/dist/index.js +792 -1066
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.cjs +801 -1019
  23. package/dist/react/index.cjs.map +1 -1
  24. package/dist/react/index.d.cts +102 -86
  25. package/dist/react/index.d.ts +102 -86
  26. package/dist/react/index.js +821 -1038
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/scan/index.cjs +1550 -1220
  29. package/dist/scan/index.cjs.map +1 -1
  30. package/dist/scan/index.d.cts +210 -12
  31. package/dist/scan/index.d.ts +210 -12
  32. package/dist/scan/index.js +1547 -1219
  33. package/dist/scan/index.js.map +1 -1
  34. package/package.json +22 -21
  35. package/templates/claude/SKILL.md +71 -0
  36. package/templates/claude/references/audit.md +43 -0
  37. package/templates/claude/{rules.md → references/conventions.md} +25 -28
  38. package/templates/claude/audit.md +0 -43
  39. /package/templates/claude/{api.md → references/api.md} +0 -0
@@ -1,3 +1,5 @@
1
+ import { Program } from 'oxc-parser';
2
+
1
3
  declare const ENTITY_KINDS: readonly ["route", "page", "feature", "widget", "region", "element", "primitive", "flow"];
2
4
  type EntityKind = (typeof ENTITY_KINDS)[number];
3
5
  type Scope = string;
@@ -88,6 +90,7 @@ interface ReportRecord {
88
90
  interface Registry {
89
91
  add(entity: Entity): void;
90
92
  get<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
93
+ matchPattern<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
91
94
  list<K extends EntityKind>(kind: K): ReadonlyArray<EntityByKind<K>>;
92
95
  query(predicate: (entity: Entity) => boolean): Entity[];
93
96
  byScope(scope: Scope): Entity[];
@@ -95,7 +98,7 @@ interface Registry {
95
98
  setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
96
99
  getReports(kind: EntityKind, id: string): readonly ReportRecord[];
97
100
  listReportKeys(): readonly string[];
98
- archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
101
+ closeReport?: (reportId: string, status?: string) => void | Promise<void>;
99
102
  onReportsChange(cb: () => void): () => void;
100
103
  }
101
104
 
@@ -117,14 +120,12 @@ interface AuditConfig {
117
120
  coverage?: boolean;
118
121
  acceptance?: boolean;
119
122
  }
120
- type TypeMode = "strict" | "loose";
121
123
  interface UidexConfig {
122
124
  $schema?: string;
123
125
  sources: SourceConfig[];
124
126
  exclude?: string[];
125
127
  output: string;
126
128
  flows?: string[];
127
- typeMode?: TypeMode;
128
129
  audit?: AuditConfig;
129
130
  conventions?: ConventionsConfig;
130
131
  }
@@ -139,7 +140,12 @@ interface ScannedFile {
139
140
  displayPath: string;
140
141
  content: string;
141
142
  }
142
- type AnnotationKind = "element" | "region" | "widget" | "primitive" | "page-doc" | "feature-doc" | "widget-doc" | "not-flow" | "orphan-acceptance";
143
+ /** Byte-offset range into a ScannedFile's content (UTF-16 code units, like oxc spans). */
144
+ interface Span {
145
+ start: number;
146
+ end: number;
147
+ }
148
+ type AnnotationKind = "element" | "region" | "widget" | "primitive";
143
149
  type DomAttrKind = "element" | "region" | "widget" | "primitive";
144
150
  interface AnnotationAncestor {
145
151
  kind: DomAttrKind;
@@ -150,10 +156,10 @@ interface Annotation {
150
156
  id: string;
151
157
  file: string;
152
158
  line: number;
153
- description?: string;
154
- acceptance?: string[];
155
- /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
159
+ /** JSX ancestor chain, outermost to innermost. */
156
160
  ancestors?: AnnotationAncestor[];
161
+ /** The span of the attribute's value literal. */
162
+ span?: Span;
157
163
  }
158
164
  type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
159
165
  interface MetadataExport {
@@ -167,12 +173,86 @@ interface MetadataExport {
167
173
  widgets?: string[];
168
174
  notFlow?: boolean;
169
175
  loc: Location;
176
+ /** Span of the whole `export const uidex = ...` statement (incl. trailing `;`). */
177
+ span?: Span;
178
+ /** Span of the id string literal (the discriminator field's value). */
179
+ idSpan?: Span;
180
+ /**
181
+ * Removal-ready spans of top-level fields (key through value, extended over
182
+ * one adjacent comma) keyed by field name. Used by `--fix` codemods.
183
+ */
184
+ fieldSpans?: Record<string, Span>;
185
+ /** Spans of the string-literal items in `features`, parallel to `features`. */
186
+ featureSpans?: Span[];
187
+ /** Spans of the string-literal items in `widgets`, parallel to `widgets`. */
188
+ widgetSpans?: Span[];
189
+ }
190
+ /** A data-uidex* attribute whose value the scanner could not resolve. */
191
+ interface DynamicAttrFact {
192
+ kind: DomAttrKind;
193
+ attrName: string;
194
+ line: number;
195
+ }
196
+ /** An HTML5 landmark element (`<header>`, `<nav>`, …, or `role="region"`). */
197
+ interface LandmarkFact {
198
+ /** the host tag, or "region" for role="region" */
199
+ tag: string;
200
+ line: number;
201
+ }
202
+ /** A static import binding (import / export-from), for the scope-leak check. */
203
+ interface ImportFact {
204
+ specifier: string;
205
+ line: number;
206
+ /** span of the whole import/export statement */
207
+ span: Span;
208
+ /** `import type` or an export-from with type-only kind */
209
+ isTypeOnly: boolean;
210
+ /** imported local binding names (default + named + namespace) */
211
+ names: string[];
212
+ }
213
+ /** An interactive host element (`button`, `a`, …) with no data-uidex* attribute. */
214
+ interface InteractiveElementFact {
215
+ tag: string;
216
+ line: number;
217
+ /** the element carries a `{...props}` spread that may deliver the annotation */
218
+ hasSpread: boolean;
219
+ /** offset just after the tag name; where `--fix` inserts the generated attribute */
220
+ nameEnd: number;
221
+ /**
222
+ * Best static label for deriving an id (aria-label, visible text, title,
223
+ * name, placeholder). Absent when nothing usable is statically available.
224
+ */
225
+ nameHint?: string;
226
+ }
227
+ interface FlowCallFact {
228
+ id: string;
229
+ action?: string;
230
+ line: number;
231
+ /** span of the id string literal argument */
232
+ span?: Span;
233
+ }
234
+ /** A `uidex(expr)` call whose argument is not a static string literal. */
235
+ interface DynamicFlowCallFact {
236
+ line: number;
237
+ }
238
+ /** A tagged `test.describe(..., { tag: "@uidex:flow" }, ...)` occurrence. */
239
+ interface FlowFact {
240
+ title: string;
241
+ line: number;
242
+ calls: FlowCallFact[];
243
+ /** uidex() calls inside this describe whose ids could not be resolved */
244
+ dynamicCalls?: DynamicFlowCallFact[];
170
245
  }
171
246
  interface ExtractedFile {
172
247
  file: ScannedFile;
173
248
  annotations: Annotation[];
174
249
  metadata?: MetadataExport[];
175
250
  diagnostics?: Diagnostic[];
251
+ flows?: FlowFact[];
252
+ dynamicAttrs?: DynamicAttrFact[];
253
+ unannotatedInteractive?: InteractiveElementFact[];
254
+ landmarks?: LandmarkFact[];
255
+ imports?: ImportFact[];
176
256
  }
177
257
  interface DetectedRoute {
178
258
  id: string;
@@ -180,6 +260,30 @@ interface DetectedRoute {
180
260
  file: string;
181
261
  }
182
262
  type DiagnosticSeverity = "error" | "warning" | "info";
263
+ /** A single text replacement; offsets index the file's content at scan time. */
264
+ interface FixEdit {
265
+ /** absolute path of the file to edit */
266
+ path: string;
267
+ start: number;
268
+ end: number;
269
+ replacement: string;
270
+ }
271
+ /**
272
+ * A machine-applicable fix attached to a diagnostic. Applied by
273
+ * `uidex scan --fix`; edits are computed against the exact content the
274
+ * scanner read, so they must be applied before any other modification.
275
+ */
276
+ interface DiagnosticFix {
277
+ description: string;
278
+ edits?: FixEdit[];
279
+ /** files to create (absolute path → content); skipped if the file exists */
280
+ createFiles?: Array<{
281
+ path: string;
282
+ content: string;
283
+ }>;
284
+ /** absolute paths of files to delete */
285
+ deleteFiles?: string[];
286
+ }
183
287
  interface Diagnostic {
184
288
  code: string;
185
289
  severity: DiagnosticSeverity;
@@ -191,6 +295,7 @@ interface Diagnostic {
191
295
  id: string;
192
296
  };
193
297
  hint?: string;
298
+ fix?: DiagnosticFix;
194
299
  }
195
300
  interface AuditSummary {
196
301
  diagnostics: Diagnostic[];
@@ -230,11 +335,36 @@ declare function walk(sources: SourceConfig[], options: WalkOptions): ScannedFil
230
335
 
231
336
  declare function extract(files: ScannedFile[]): ExtractedFile[];
232
337
 
338
+ /**
339
+ * Shared parse layer for the scanner. Each file is parsed exactly once in
340
+ * the extract phase; the resulting AST never leaves it (ExtractedFile must
341
+ * stay JSON-serializable for the bundler-plugin watchers).
342
+ *
343
+ * oxc spans are UTF-16 code-unit offsets, i.e. they index directly into the
344
+ * JS source string (verified against oxc-parser@0.135.0).
345
+ */
346
+ interface ParsedComment {
347
+ type: "Line" | "Block";
348
+ value: string;
349
+ start: number;
350
+ end: number;
351
+ }
352
+ interface ParsedSource {
353
+ /** null when parsing failed catastrophically — callers degrade to empty facts */
354
+ program: Program | null;
355
+ /** true when oxc reported recoverable errors (program is still usable) */
356
+ hasErrors: boolean;
357
+ /** all comments in source order; empty when parsing failed */
358
+ comments: ParsedComment[];
359
+ /** offset → 1-based line */
360
+ lineAt(offset: number): number;
361
+ }
362
+
233
363
  interface UidexExportExtractResult {
234
364
  exports: MetadataExport[];
235
365
  diagnostics: Diagnostic[];
236
366
  }
237
- declare function extractUidexExports(file: ScannedFile): UidexExportExtractResult;
367
+ declare function extractUidexExports(file: ScannedFile, parsed?: ParsedSource): UidexExportExtractResult;
238
368
 
239
369
  interface ResolveContext {
240
370
  config: UidexConfig;
@@ -257,6 +387,8 @@ interface AuditOptions {
257
387
  registry: Registry;
258
388
  extracted: ExtractedFile[];
259
389
  files: ScannedFile[];
390
+ /** Extract output for flow spec files; enables per-call reference checks. */
391
+ flowExtracted?: ExtractedFile[];
260
392
  config: UidexConfig;
261
393
  check?: boolean;
262
394
  lint?: boolean;
@@ -275,8 +407,6 @@ interface EmitOptions {
275
407
  gitContext?: GitContext;
276
408
  /** The import source for `createUidex` in the generated preconfigured export. */
277
409
  uidexImport?: string;
278
- /** Controls id-union emission: "strict" emits literal unions, "loose" emits `string`. */
279
- typeMode?: TypeMode;
280
410
  }
281
411
  declare function emit(opts: EmitOptions): string;
282
412
 
@@ -296,6 +426,15 @@ interface GitResolveOptions {
296
426
  }
297
427
  declare function resolveGitContext(opts?: GitResolveOptions): GitContext;
298
428
 
429
+ type ScaffoldKind = "widget" | "page" | "feature";
430
+ interface ScaffoldSpecOptions {
431
+ registry: Registry;
432
+ kind: ScaffoldKind;
433
+ id: string;
434
+ outDir: string;
435
+ force?: boolean;
436
+ fixtureImport?: string;
437
+ }
299
438
  interface ScaffoldOptions {
300
439
  registry: Registry;
301
440
  widgetId: string;
@@ -310,6 +449,13 @@ interface ScaffoldResult {
310
449
  reason?: string;
311
450
  }
312
451
  declare function scaffoldWidgetSpec(opts: ScaffoldOptions): ScaffoldResult;
452
+ /**
453
+ * Emits a tagged Playwright stub from an entity's declared acceptance
454
+ * criteria — one `test()` per criterion. Widgets keep the historical
455
+ * `widget-<id>.spec.ts` name; pages and features emit `flow-<id>.spec.ts`
456
+ * per the one-tagged-describe-per-flow-spec convention.
457
+ */
458
+ declare function scaffoldSpec(opts: ScaffoldSpecOptions): ScaffoldResult;
313
459
 
314
460
  interface RunScanOptions {
315
461
  cwd?: string;
@@ -329,6 +475,59 @@ interface ScanResult {
329
475
  declare function runScan(opts?: RunScanOptions): ScanResult[];
330
476
  declare function writeScanResult(result: ScanResult): void;
331
477
 
478
+ /**
479
+ * Applies the machine-generated fixes attached to diagnostics.
480
+ *
481
+ * Edits are offset-based against the content the scanner read, so this must
482
+ * run before anything else touches the files. Within a file, edits apply
483
+ * last-to-first; an edit overlapping one already applied is skipped. Identical
484
+ * edits are deduped — two fixes in one file emitting the same edit collapse to
485
+ * one application.
486
+ */
487
+ interface AppliedFix {
488
+ code: string;
489
+ description: string;
490
+ file?: string;
491
+ }
492
+ interface ApplyFixesResult {
493
+ applied: AppliedFix[];
494
+ /** fixes skipped because of overlapping edits or existing target files */
495
+ skipped: Array<AppliedFix & {
496
+ reason: string;
497
+ }>;
498
+ }
499
+ declare function applyFixes(diagnostics: Diagnostic[]): ApplyFixesResult;
500
+
501
+ /**
502
+ * Cross-file id rename. The registry knows every reference surface for an
503
+ * id — the DOM attribute, `uidex("…")` calls in flow specs, the widget
504
+ * export's discriminator, and `widgets:` arrays — so the rename rewrites all
505
+ * of them in one pass and regenerates the gen file. Occurrences the scanner
506
+ * can't edit mechanically (const indirection, ternaries, patterns) are
507
+ * reported for manual follow-up instead of being guessed at.
508
+ */
509
+ type RenameKind = "element" | "widget" | "region";
510
+ interface RenameOptions {
511
+ cwd: string;
512
+ kind: RenameKind;
513
+ oldId: string;
514
+ newId: string;
515
+ /** allow renaming onto an id that already exists */
516
+ force?: boolean;
517
+ }
518
+ interface RenameResult {
519
+ /** number of source edits applied */
520
+ edits: number;
521
+ /** occurrences that need a human (no static literal to rewrite) */
522
+ manual: Array<{
523
+ file: string;
524
+ line: number;
525
+ reason: string;
526
+ }>;
527
+ errors: string[];
528
+ }
529
+ declare function renameEntity(opts: RenameOptions): RenameResult;
530
+
332
531
  interface CliResult {
333
532
  exitCode: number;
334
533
  stdout: string;
@@ -340,7 +539,6 @@ interface CliOptions {
340
539
  }
341
540
  declare function run(opts: CliOptions): Promise<CliResult>;
342
541
 
343
- declare const DEFAULT_TYPE_MODE: TypeMode;
344
542
  declare class ConfigError extends Error {
345
543
  constructor(message: string);
346
544
  }
@@ -401,4 +599,4 @@ declare namespace Uidex {
401
599
  }
402
600
  }
403
601
 
404
- export { type Annotation, type AnnotationKind, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CliOptions, type CliResult, ConfigError, type ConventionsConfig, DEFAULT_CONVENTIONS, DEFAULT_TYPE_MODE, type DetectedRoute, type Diagnostic, type DiagnosticSeverity, type DiscoveredConfig, type ExtractedFile, type GitContext, type MetadataExport, type MetadataExportKind, type RunScanOptions, type ScaffoldOptions, type ScaffoldResult, type ScanResult, type ScannedFile, type SourceConfig, type TypeMode, Uidex, type UidexConfig, audit, detectRoutes, discover, emit, extract, extractUidexExports, globToRegExp, parseConfig, pathToId, resolve, resolveGitContext, run as runCli, runScan, scaffoldWidgetSpec, validateConfig, walk, writeScanResult };
602
+ export { type Annotation, type AnnotationKind, type AppliedFix, type ApplyFixesResult, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CliOptions, type CliResult, ConfigError, type ConventionsConfig, DEFAULT_CONVENTIONS, type DetectedRoute, type Diagnostic, type DiagnosticFix, type DiagnosticSeverity, type DiscoveredConfig, type ExtractedFile, type FixEdit, type GitContext, type ImportFact, type LandmarkFact, type MetadataExport, type MetadataExportKind, type RenameKind, type RenameOptions, type RenameResult, type RunScanOptions, type ScaffoldKind, type ScaffoldOptions, type ScaffoldResult, type ScaffoldSpecOptions, type ScanResult, type ScannedFile, type SourceConfig, type Span, Uidex, type UidexConfig, applyFixes, audit, detectRoutes, discover, emit, extract, extractUidexExports, globToRegExp, parseConfig, pathToId, renameEntity, resolve, resolveGitContext, run as runCli, runScan, scaffoldSpec, scaffoldWidgetSpec, validateConfig, walk, writeScanResult };
@@ -1,3 +1,5 @@
1
+ import { Program } from 'oxc-parser';
2
+
1
3
  declare const ENTITY_KINDS: readonly ["route", "page", "feature", "widget", "region", "element", "primitive", "flow"];
2
4
  type EntityKind = (typeof ENTITY_KINDS)[number];
3
5
  type Scope = string;
@@ -88,6 +90,7 @@ interface ReportRecord {
88
90
  interface Registry {
89
91
  add(entity: Entity): void;
90
92
  get<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
93
+ matchPattern<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
91
94
  list<K extends EntityKind>(kind: K): ReadonlyArray<EntityByKind<K>>;
92
95
  query(predicate: (entity: Entity) => boolean): Entity[];
93
96
  byScope(scope: Scope): Entity[];
@@ -95,7 +98,7 @@ interface Registry {
95
98
  setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
96
99
  getReports(kind: EntityKind, id: string): readonly ReportRecord[];
97
100
  listReportKeys(): readonly string[];
98
- archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
101
+ closeReport?: (reportId: string, status?: string) => void | Promise<void>;
99
102
  onReportsChange(cb: () => void): () => void;
100
103
  }
101
104
 
@@ -117,14 +120,12 @@ interface AuditConfig {
117
120
  coverage?: boolean;
118
121
  acceptance?: boolean;
119
122
  }
120
- type TypeMode = "strict" | "loose";
121
123
  interface UidexConfig {
122
124
  $schema?: string;
123
125
  sources: SourceConfig[];
124
126
  exclude?: string[];
125
127
  output: string;
126
128
  flows?: string[];
127
- typeMode?: TypeMode;
128
129
  audit?: AuditConfig;
129
130
  conventions?: ConventionsConfig;
130
131
  }
@@ -139,7 +140,12 @@ interface ScannedFile {
139
140
  displayPath: string;
140
141
  content: string;
141
142
  }
142
- type AnnotationKind = "element" | "region" | "widget" | "primitive" | "page-doc" | "feature-doc" | "widget-doc" | "not-flow" | "orphan-acceptance";
143
+ /** Byte-offset range into a ScannedFile's content (UTF-16 code units, like oxc spans). */
144
+ interface Span {
145
+ start: number;
146
+ end: number;
147
+ }
148
+ type AnnotationKind = "element" | "region" | "widget" | "primitive";
143
149
  type DomAttrKind = "element" | "region" | "widget" | "primitive";
144
150
  interface AnnotationAncestor {
145
151
  kind: DomAttrKind;
@@ -150,10 +156,10 @@ interface Annotation {
150
156
  id: string;
151
157
  file: string;
152
158
  line: number;
153
- description?: string;
154
- acceptance?: string[];
155
- /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
159
+ /** JSX ancestor chain, outermost to innermost. */
156
160
  ancestors?: AnnotationAncestor[];
161
+ /** The span of the attribute's value literal. */
162
+ span?: Span;
157
163
  }
158
164
  type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
159
165
  interface MetadataExport {
@@ -167,12 +173,86 @@ interface MetadataExport {
167
173
  widgets?: string[];
168
174
  notFlow?: boolean;
169
175
  loc: Location;
176
+ /** Span of the whole `export const uidex = ...` statement (incl. trailing `;`). */
177
+ span?: Span;
178
+ /** Span of the id string literal (the discriminator field's value). */
179
+ idSpan?: Span;
180
+ /**
181
+ * Removal-ready spans of top-level fields (key through value, extended over
182
+ * one adjacent comma) keyed by field name. Used by `--fix` codemods.
183
+ */
184
+ fieldSpans?: Record<string, Span>;
185
+ /** Spans of the string-literal items in `features`, parallel to `features`. */
186
+ featureSpans?: Span[];
187
+ /** Spans of the string-literal items in `widgets`, parallel to `widgets`. */
188
+ widgetSpans?: Span[];
189
+ }
190
+ /** A data-uidex* attribute whose value the scanner could not resolve. */
191
+ interface DynamicAttrFact {
192
+ kind: DomAttrKind;
193
+ attrName: string;
194
+ line: number;
195
+ }
196
+ /** An HTML5 landmark element (`<header>`, `<nav>`, …, or `role="region"`). */
197
+ interface LandmarkFact {
198
+ /** the host tag, or "region" for role="region" */
199
+ tag: string;
200
+ line: number;
201
+ }
202
+ /** A static import binding (import / export-from), for the scope-leak check. */
203
+ interface ImportFact {
204
+ specifier: string;
205
+ line: number;
206
+ /** span of the whole import/export statement */
207
+ span: Span;
208
+ /** `import type` or an export-from with type-only kind */
209
+ isTypeOnly: boolean;
210
+ /** imported local binding names (default + named + namespace) */
211
+ names: string[];
212
+ }
213
+ /** An interactive host element (`button`, `a`, …) with no data-uidex* attribute. */
214
+ interface InteractiveElementFact {
215
+ tag: string;
216
+ line: number;
217
+ /** the element carries a `{...props}` spread that may deliver the annotation */
218
+ hasSpread: boolean;
219
+ /** offset just after the tag name; where `--fix` inserts the generated attribute */
220
+ nameEnd: number;
221
+ /**
222
+ * Best static label for deriving an id (aria-label, visible text, title,
223
+ * name, placeholder). Absent when nothing usable is statically available.
224
+ */
225
+ nameHint?: string;
226
+ }
227
+ interface FlowCallFact {
228
+ id: string;
229
+ action?: string;
230
+ line: number;
231
+ /** span of the id string literal argument */
232
+ span?: Span;
233
+ }
234
+ /** A `uidex(expr)` call whose argument is not a static string literal. */
235
+ interface DynamicFlowCallFact {
236
+ line: number;
237
+ }
238
+ /** A tagged `test.describe(..., { tag: "@uidex:flow" }, ...)` occurrence. */
239
+ interface FlowFact {
240
+ title: string;
241
+ line: number;
242
+ calls: FlowCallFact[];
243
+ /** uidex() calls inside this describe whose ids could not be resolved */
244
+ dynamicCalls?: DynamicFlowCallFact[];
170
245
  }
171
246
  interface ExtractedFile {
172
247
  file: ScannedFile;
173
248
  annotations: Annotation[];
174
249
  metadata?: MetadataExport[];
175
250
  diagnostics?: Diagnostic[];
251
+ flows?: FlowFact[];
252
+ dynamicAttrs?: DynamicAttrFact[];
253
+ unannotatedInteractive?: InteractiveElementFact[];
254
+ landmarks?: LandmarkFact[];
255
+ imports?: ImportFact[];
176
256
  }
177
257
  interface DetectedRoute {
178
258
  id: string;
@@ -180,6 +260,30 @@ interface DetectedRoute {
180
260
  file: string;
181
261
  }
182
262
  type DiagnosticSeverity = "error" | "warning" | "info";
263
+ /** A single text replacement; offsets index the file's content at scan time. */
264
+ interface FixEdit {
265
+ /** absolute path of the file to edit */
266
+ path: string;
267
+ start: number;
268
+ end: number;
269
+ replacement: string;
270
+ }
271
+ /**
272
+ * A machine-applicable fix attached to a diagnostic. Applied by
273
+ * `uidex scan --fix`; edits are computed against the exact content the
274
+ * scanner read, so they must be applied before any other modification.
275
+ */
276
+ interface DiagnosticFix {
277
+ description: string;
278
+ edits?: FixEdit[];
279
+ /** files to create (absolute path → content); skipped if the file exists */
280
+ createFiles?: Array<{
281
+ path: string;
282
+ content: string;
283
+ }>;
284
+ /** absolute paths of files to delete */
285
+ deleteFiles?: string[];
286
+ }
183
287
  interface Diagnostic {
184
288
  code: string;
185
289
  severity: DiagnosticSeverity;
@@ -191,6 +295,7 @@ interface Diagnostic {
191
295
  id: string;
192
296
  };
193
297
  hint?: string;
298
+ fix?: DiagnosticFix;
194
299
  }
195
300
  interface AuditSummary {
196
301
  diagnostics: Diagnostic[];
@@ -230,11 +335,36 @@ declare function walk(sources: SourceConfig[], options: WalkOptions): ScannedFil
230
335
 
231
336
  declare function extract(files: ScannedFile[]): ExtractedFile[];
232
337
 
338
+ /**
339
+ * Shared parse layer for the scanner. Each file is parsed exactly once in
340
+ * the extract phase; the resulting AST never leaves it (ExtractedFile must
341
+ * stay JSON-serializable for the bundler-plugin watchers).
342
+ *
343
+ * oxc spans are UTF-16 code-unit offsets, i.e. they index directly into the
344
+ * JS source string (verified against oxc-parser@0.135.0).
345
+ */
346
+ interface ParsedComment {
347
+ type: "Line" | "Block";
348
+ value: string;
349
+ start: number;
350
+ end: number;
351
+ }
352
+ interface ParsedSource {
353
+ /** null when parsing failed catastrophically — callers degrade to empty facts */
354
+ program: Program | null;
355
+ /** true when oxc reported recoverable errors (program is still usable) */
356
+ hasErrors: boolean;
357
+ /** all comments in source order; empty when parsing failed */
358
+ comments: ParsedComment[];
359
+ /** offset → 1-based line */
360
+ lineAt(offset: number): number;
361
+ }
362
+
233
363
  interface UidexExportExtractResult {
234
364
  exports: MetadataExport[];
235
365
  diagnostics: Diagnostic[];
236
366
  }
237
- declare function extractUidexExports(file: ScannedFile): UidexExportExtractResult;
367
+ declare function extractUidexExports(file: ScannedFile, parsed?: ParsedSource): UidexExportExtractResult;
238
368
 
239
369
  interface ResolveContext {
240
370
  config: UidexConfig;
@@ -257,6 +387,8 @@ interface AuditOptions {
257
387
  registry: Registry;
258
388
  extracted: ExtractedFile[];
259
389
  files: ScannedFile[];
390
+ /** Extract output for flow spec files; enables per-call reference checks. */
391
+ flowExtracted?: ExtractedFile[];
260
392
  config: UidexConfig;
261
393
  check?: boolean;
262
394
  lint?: boolean;
@@ -275,8 +407,6 @@ interface EmitOptions {
275
407
  gitContext?: GitContext;
276
408
  /** The import source for `createUidex` in the generated preconfigured export. */
277
409
  uidexImport?: string;
278
- /** Controls id-union emission: "strict" emits literal unions, "loose" emits `string`. */
279
- typeMode?: TypeMode;
280
410
  }
281
411
  declare function emit(opts: EmitOptions): string;
282
412
 
@@ -296,6 +426,15 @@ interface GitResolveOptions {
296
426
  }
297
427
  declare function resolveGitContext(opts?: GitResolveOptions): GitContext;
298
428
 
429
+ type ScaffoldKind = "widget" | "page" | "feature";
430
+ interface ScaffoldSpecOptions {
431
+ registry: Registry;
432
+ kind: ScaffoldKind;
433
+ id: string;
434
+ outDir: string;
435
+ force?: boolean;
436
+ fixtureImport?: string;
437
+ }
299
438
  interface ScaffoldOptions {
300
439
  registry: Registry;
301
440
  widgetId: string;
@@ -310,6 +449,13 @@ interface ScaffoldResult {
310
449
  reason?: string;
311
450
  }
312
451
  declare function scaffoldWidgetSpec(opts: ScaffoldOptions): ScaffoldResult;
452
+ /**
453
+ * Emits a tagged Playwright stub from an entity's declared acceptance
454
+ * criteria — one `test()` per criterion. Widgets keep the historical
455
+ * `widget-<id>.spec.ts` name; pages and features emit `flow-<id>.spec.ts`
456
+ * per the one-tagged-describe-per-flow-spec convention.
457
+ */
458
+ declare function scaffoldSpec(opts: ScaffoldSpecOptions): ScaffoldResult;
313
459
 
314
460
  interface RunScanOptions {
315
461
  cwd?: string;
@@ -329,6 +475,59 @@ interface ScanResult {
329
475
  declare function runScan(opts?: RunScanOptions): ScanResult[];
330
476
  declare function writeScanResult(result: ScanResult): void;
331
477
 
478
+ /**
479
+ * Applies the machine-generated fixes attached to diagnostics.
480
+ *
481
+ * Edits are offset-based against the content the scanner read, so this must
482
+ * run before anything else touches the files. Within a file, edits apply
483
+ * last-to-first; an edit overlapping one already applied is skipped. Identical
484
+ * edits are deduped — two fixes in one file emitting the same edit collapse to
485
+ * one application.
486
+ */
487
+ interface AppliedFix {
488
+ code: string;
489
+ description: string;
490
+ file?: string;
491
+ }
492
+ interface ApplyFixesResult {
493
+ applied: AppliedFix[];
494
+ /** fixes skipped because of overlapping edits or existing target files */
495
+ skipped: Array<AppliedFix & {
496
+ reason: string;
497
+ }>;
498
+ }
499
+ declare function applyFixes(diagnostics: Diagnostic[]): ApplyFixesResult;
500
+
501
+ /**
502
+ * Cross-file id rename. The registry knows every reference surface for an
503
+ * id — the DOM attribute, `uidex("…")` calls in flow specs, the widget
504
+ * export's discriminator, and `widgets:` arrays — so the rename rewrites all
505
+ * of them in one pass and regenerates the gen file. Occurrences the scanner
506
+ * can't edit mechanically (const indirection, ternaries, patterns) are
507
+ * reported for manual follow-up instead of being guessed at.
508
+ */
509
+ type RenameKind = "element" | "widget" | "region";
510
+ interface RenameOptions {
511
+ cwd: string;
512
+ kind: RenameKind;
513
+ oldId: string;
514
+ newId: string;
515
+ /** allow renaming onto an id that already exists */
516
+ force?: boolean;
517
+ }
518
+ interface RenameResult {
519
+ /** number of source edits applied */
520
+ edits: number;
521
+ /** occurrences that need a human (no static literal to rewrite) */
522
+ manual: Array<{
523
+ file: string;
524
+ line: number;
525
+ reason: string;
526
+ }>;
527
+ errors: string[];
528
+ }
529
+ declare function renameEntity(opts: RenameOptions): RenameResult;
530
+
332
531
  interface CliResult {
333
532
  exitCode: number;
334
533
  stdout: string;
@@ -340,7 +539,6 @@ interface CliOptions {
340
539
  }
341
540
  declare function run(opts: CliOptions): Promise<CliResult>;
342
541
 
343
- declare const DEFAULT_TYPE_MODE: TypeMode;
344
542
  declare class ConfigError extends Error {
345
543
  constructor(message: string);
346
544
  }
@@ -401,4 +599,4 @@ declare namespace Uidex {
401
599
  }
402
600
  }
403
601
 
404
- export { type Annotation, type AnnotationKind, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CliOptions, type CliResult, ConfigError, type ConventionsConfig, DEFAULT_CONVENTIONS, DEFAULT_TYPE_MODE, type DetectedRoute, type Diagnostic, type DiagnosticSeverity, type DiscoveredConfig, type ExtractedFile, type GitContext, type MetadataExport, type MetadataExportKind, type RunScanOptions, type ScaffoldOptions, type ScaffoldResult, type ScanResult, type ScannedFile, type SourceConfig, type TypeMode, Uidex, type UidexConfig, audit, detectRoutes, discover, emit, extract, extractUidexExports, globToRegExp, parseConfig, pathToId, resolve, resolveGitContext, run as runCli, runScan, scaffoldWidgetSpec, validateConfig, walk, writeScanResult };
602
+ export { type Annotation, type AnnotationKind, type AppliedFix, type ApplyFixesResult, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CliOptions, type CliResult, ConfigError, type ConventionsConfig, DEFAULT_CONVENTIONS, type DetectedRoute, type Diagnostic, type DiagnosticFix, type DiagnosticSeverity, type DiscoveredConfig, type ExtractedFile, type FixEdit, type GitContext, type ImportFact, type LandmarkFact, type MetadataExport, type MetadataExportKind, type RenameKind, type RenameOptions, type RenameResult, type RunScanOptions, type ScaffoldKind, type ScaffoldOptions, type ScaffoldResult, type ScaffoldSpecOptions, type ScanResult, type ScannedFile, type SourceConfig, type Span, Uidex, type UidexConfig, applyFixes, audit, detectRoutes, discover, emit, extract, extractUidexExports, globToRegExp, parseConfig, pathToId, renameEntity, resolve, resolveGitContext, run as runCli, runScan, scaffoldSpec, scaffoldWidgetSpec, validateConfig, walk, writeScanResult };