uidex 0.6.0 → 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 +1510 -1244
  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 +82 -255
  11. package/dist/headless/index.cjs.map +1 -1
  12. package/dist/headless/index.d.cts +5 -11
  13. package/dist/headless/index.d.ts +5 -11
  14. package/dist/headless/index.js +82 -257
  15. package/dist/headless/index.js.map +1 -1
  16. package/dist/index.cjs +721 -1053
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +149 -160
  19. package/dist/index.d.ts +149 -160
  20. package/dist/index.js +741 -1068
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.cjs +729 -1000
  23. package/dist/react/index.cjs.map +1 -1
  24. package/dist/react/index.d.cts +99 -86
  25. package/dist/react/index.d.ts +99 -86
  26. package/dist/react/index.js +745 -1015
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/scan/index.cjs +1518 -1237
  29. package/dist/scan/index.cjs.map +1 -1
  30. package/dist/scan/index.d.cts +209 -12
  31. package/dist/scan/index.d.ts +209 -12
  32. package/dist/scan/index.js +1515 -1236
  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;
@@ -96,7 +98,7 @@ interface Registry {
96
98
  setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
97
99
  getReports(kind: EntityKind, id: string): readonly ReportRecord[];
98
100
  listReportKeys(): readonly string[];
99
- archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
101
+ closeReport?: (reportId: string, status?: string) => void | Promise<void>;
100
102
  onReportsChange(cb: () => void): () => void;
101
103
  }
102
104
 
@@ -118,14 +120,12 @@ interface AuditConfig {
118
120
  coverage?: boolean;
119
121
  acceptance?: boolean;
120
122
  }
121
- type TypeMode = "strict" | "loose";
122
123
  interface UidexConfig {
123
124
  $schema?: string;
124
125
  sources: SourceConfig[];
125
126
  exclude?: string[];
126
127
  output: string;
127
128
  flows?: string[];
128
- typeMode?: TypeMode;
129
129
  audit?: AuditConfig;
130
130
  conventions?: ConventionsConfig;
131
131
  }
@@ -140,7 +140,12 @@ interface ScannedFile {
140
140
  displayPath: string;
141
141
  content: string;
142
142
  }
143
- 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";
144
149
  type DomAttrKind = "element" | "region" | "widget" | "primitive";
145
150
  interface AnnotationAncestor {
146
151
  kind: DomAttrKind;
@@ -151,10 +156,10 @@ interface Annotation {
151
156
  id: string;
152
157
  file: string;
153
158
  line: number;
154
- description?: string;
155
- acceptance?: string[];
156
- /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
159
+ /** JSX ancestor chain, outermost to innermost. */
157
160
  ancestors?: AnnotationAncestor[];
161
+ /** The span of the attribute's value literal. */
162
+ span?: Span;
158
163
  }
159
164
  type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
160
165
  interface MetadataExport {
@@ -168,12 +173,86 @@ interface MetadataExport {
168
173
  widgets?: string[];
169
174
  notFlow?: boolean;
170
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[];
171
245
  }
172
246
  interface ExtractedFile {
173
247
  file: ScannedFile;
174
248
  annotations: Annotation[];
175
249
  metadata?: MetadataExport[];
176
250
  diagnostics?: Diagnostic[];
251
+ flows?: FlowFact[];
252
+ dynamicAttrs?: DynamicAttrFact[];
253
+ unannotatedInteractive?: InteractiveElementFact[];
254
+ landmarks?: LandmarkFact[];
255
+ imports?: ImportFact[];
177
256
  }
178
257
  interface DetectedRoute {
179
258
  id: string;
@@ -181,6 +260,30 @@ interface DetectedRoute {
181
260
  file: string;
182
261
  }
183
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
+ }
184
287
  interface Diagnostic {
185
288
  code: string;
186
289
  severity: DiagnosticSeverity;
@@ -192,6 +295,7 @@ interface Diagnostic {
192
295
  id: string;
193
296
  };
194
297
  hint?: string;
298
+ fix?: DiagnosticFix;
195
299
  }
196
300
  interface AuditSummary {
197
301
  diagnostics: Diagnostic[];
@@ -231,11 +335,36 @@ declare function walk(sources: SourceConfig[], options: WalkOptions): ScannedFil
231
335
 
232
336
  declare function extract(files: ScannedFile[]): ExtractedFile[];
233
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
+
234
363
  interface UidexExportExtractResult {
235
364
  exports: MetadataExport[];
236
365
  diagnostics: Diagnostic[];
237
366
  }
238
- declare function extractUidexExports(file: ScannedFile): UidexExportExtractResult;
367
+ declare function extractUidexExports(file: ScannedFile, parsed?: ParsedSource): UidexExportExtractResult;
239
368
 
240
369
  interface ResolveContext {
241
370
  config: UidexConfig;
@@ -258,6 +387,8 @@ interface AuditOptions {
258
387
  registry: Registry;
259
388
  extracted: ExtractedFile[];
260
389
  files: ScannedFile[];
390
+ /** Extract output for flow spec files; enables per-call reference checks. */
391
+ flowExtracted?: ExtractedFile[];
261
392
  config: UidexConfig;
262
393
  check?: boolean;
263
394
  lint?: boolean;
@@ -276,8 +407,6 @@ interface EmitOptions {
276
407
  gitContext?: GitContext;
277
408
  /** The import source for `createUidex` in the generated preconfigured export. */
278
409
  uidexImport?: string;
279
- /** Controls id-union emission: "strict" emits literal unions, "loose" emits `string`. */
280
- typeMode?: TypeMode;
281
410
  }
282
411
  declare function emit(opts: EmitOptions): string;
283
412
 
@@ -297,6 +426,15 @@ interface GitResolveOptions {
297
426
  }
298
427
  declare function resolveGitContext(opts?: GitResolveOptions): GitContext;
299
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
+ }
300
438
  interface ScaffoldOptions {
301
439
  registry: Registry;
302
440
  widgetId: string;
@@ -311,6 +449,13 @@ interface ScaffoldResult {
311
449
  reason?: string;
312
450
  }
313
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;
314
459
 
315
460
  interface RunScanOptions {
316
461
  cwd?: string;
@@ -330,6 +475,59 @@ interface ScanResult {
330
475
  declare function runScan(opts?: RunScanOptions): ScanResult[];
331
476
  declare function writeScanResult(result: ScanResult): void;
332
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
+
333
531
  interface CliResult {
334
532
  exitCode: number;
335
533
  stdout: string;
@@ -341,7 +539,6 @@ interface CliOptions {
341
539
  }
342
540
  declare function run(opts: CliOptions): Promise<CliResult>;
343
541
 
344
- declare const DEFAULT_TYPE_MODE: TypeMode;
345
542
  declare class ConfigError extends Error {
346
543
  constructor(message: string);
347
544
  }
@@ -402,4 +599,4 @@ declare namespace Uidex {
402
599
  }
403
600
  }
404
601
 
405
- 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;
@@ -96,7 +98,7 @@ interface Registry {
96
98
  setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
97
99
  getReports(kind: EntityKind, id: string): readonly ReportRecord[];
98
100
  listReportKeys(): readonly string[];
99
- archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
101
+ closeReport?: (reportId: string, status?: string) => void | Promise<void>;
100
102
  onReportsChange(cb: () => void): () => void;
101
103
  }
102
104
 
@@ -118,14 +120,12 @@ interface AuditConfig {
118
120
  coverage?: boolean;
119
121
  acceptance?: boolean;
120
122
  }
121
- type TypeMode = "strict" | "loose";
122
123
  interface UidexConfig {
123
124
  $schema?: string;
124
125
  sources: SourceConfig[];
125
126
  exclude?: string[];
126
127
  output: string;
127
128
  flows?: string[];
128
- typeMode?: TypeMode;
129
129
  audit?: AuditConfig;
130
130
  conventions?: ConventionsConfig;
131
131
  }
@@ -140,7 +140,12 @@ interface ScannedFile {
140
140
  displayPath: string;
141
141
  content: string;
142
142
  }
143
- 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";
144
149
  type DomAttrKind = "element" | "region" | "widget" | "primitive";
145
150
  interface AnnotationAncestor {
146
151
  kind: DomAttrKind;
@@ -151,10 +156,10 @@ interface Annotation {
151
156
  id: string;
152
157
  file: string;
153
158
  line: number;
154
- description?: string;
155
- acceptance?: string[];
156
- /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
159
+ /** JSX ancestor chain, outermost to innermost. */
157
160
  ancestors?: AnnotationAncestor[];
161
+ /** The span of the attribute's value literal. */
162
+ span?: Span;
158
163
  }
159
164
  type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
160
165
  interface MetadataExport {
@@ -168,12 +173,86 @@ interface MetadataExport {
168
173
  widgets?: string[];
169
174
  notFlow?: boolean;
170
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[];
171
245
  }
172
246
  interface ExtractedFile {
173
247
  file: ScannedFile;
174
248
  annotations: Annotation[];
175
249
  metadata?: MetadataExport[];
176
250
  diagnostics?: Diagnostic[];
251
+ flows?: FlowFact[];
252
+ dynamicAttrs?: DynamicAttrFact[];
253
+ unannotatedInteractive?: InteractiveElementFact[];
254
+ landmarks?: LandmarkFact[];
255
+ imports?: ImportFact[];
177
256
  }
178
257
  interface DetectedRoute {
179
258
  id: string;
@@ -181,6 +260,30 @@ interface DetectedRoute {
181
260
  file: string;
182
261
  }
183
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
+ }
184
287
  interface Diagnostic {
185
288
  code: string;
186
289
  severity: DiagnosticSeverity;
@@ -192,6 +295,7 @@ interface Diagnostic {
192
295
  id: string;
193
296
  };
194
297
  hint?: string;
298
+ fix?: DiagnosticFix;
195
299
  }
196
300
  interface AuditSummary {
197
301
  diagnostics: Diagnostic[];
@@ -231,11 +335,36 @@ declare function walk(sources: SourceConfig[], options: WalkOptions): ScannedFil
231
335
 
232
336
  declare function extract(files: ScannedFile[]): ExtractedFile[];
233
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
+
234
363
  interface UidexExportExtractResult {
235
364
  exports: MetadataExport[];
236
365
  diagnostics: Diagnostic[];
237
366
  }
238
- declare function extractUidexExports(file: ScannedFile): UidexExportExtractResult;
367
+ declare function extractUidexExports(file: ScannedFile, parsed?: ParsedSource): UidexExportExtractResult;
239
368
 
240
369
  interface ResolveContext {
241
370
  config: UidexConfig;
@@ -258,6 +387,8 @@ interface AuditOptions {
258
387
  registry: Registry;
259
388
  extracted: ExtractedFile[];
260
389
  files: ScannedFile[];
390
+ /** Extract output for flow spec files; enables per-call reference checks. */
391
+ flowExtracted?: ExtractedFile[];
261
392
  config: UidexConfig;
262
393
  check?: boolean;
263
394
  lint?: boolean;
@@ -276,8 +407,6 @@ interface EmitOptions {
276
407
  gitContext?: GitContext;
277
408
  /** The import source for `createUidex` in the generated preconfigured export. */
278
409
  uidexImport?: string;
279
- /** Controls id-union emission: "strict" emits literal unions, "loose" emits `string`. */
280
- typeMode?: TypeMode;
281
410
  }
282
411
  declare function emit(opts: EmitOptions): string;
283
412
 
@@ -297,6 +426,15 @@ interface GitResolveOptions {
297
426
  }
298
427
  declare function resolveGitContext(opts?: GitResolveOptions): GitContext;
299
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
+ }
300
438
  interface ScaffoldOptions {
301
439
  registry: Registry;
302
440
  widgetId: string;
@@ -311,6 +449,13 @@ interface ScaffoldResult {
311
449
  reason?: string;
312
450
  }
313
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;
314
459
 
315
460
  interface RunScanOptions {
316
461
  cwd?: string;
@@ -330,6 +475,59 @@ interface ScanResult {
330
475
  declare function runScan(opts?: RunScanOptions): ScanResult[];
331
476
  declare function writeScanResult(result: ScanResult): void;
332
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
+
333
531
  interface CliResult {
334
532
  exitCode: number;
335
533
  stdout: string;
@@ -341,7 +539,6 @@ interface CliOptions {
341
539
  }
342
540
  declare function run(opts: CliOptions): Promise<CliResult>;
343
541
 
344
- declare const DEFAULT_TYPE_MODE: TypeMode;
345
542
  declare class ConfigError extends Error {
346
543
  constructor(message: string);
347
544
  }
@@ -402,4 +599,4 @@ declare namespace Uidex {
402
599
  }
403
600
  }
404
601
 
405
- 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 };