uidex 0.6.0 → 0.8.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.
- package/README.md +3 -3
- package/dist/cli/cli.cjs +2502 -2253
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +86 -703
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +46 -22
- package/dist/headless/index.d.ts +46 -22
- package/dist/headless/index.js +86 -707
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +712 -4149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -366
- package/dist/index.d.ts +82 -366
- package/dist/index.js +708 -4156
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +175 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +2 -0
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.js +167 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +123 -0
- package/dist/playwright/states-reporter.cjs.map +1 -0
- package/dist/playwright/states-reporter.d.cts +46 -0
- package/dist/playwright/states-reporter.d.ts +46 -0
- package/dist/playwright/states-reporter.js +88 -0
- package/dist/playwright/states-reporter.js.map +1 -0
- package/dist/playwright/states.cjs +118 -0
- package/dist/playwright/states.cjs.map +1 -0
- package/dist/playwright/states.d.cts +120 -0
- package/dist/playwright/states.d.ts +120 -0
- package/dist/playwright/states.js +88 -0
- package/dist/playwright/states.js.map +1 -0
- package/dist/react/index.cjs +750 -4113
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +78 -278
- package/dist/react/index.d.ts +78 -278
- package/dist/react/index.js +748 -4135
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +2694 -1541
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +482 -19
- package/dist/scan/index.d.ts +482 -19
- package/dist/scan/index.js +2682 -1540
- package/dist/scan/index.js.map +1 -1
- package/package.json +14 -17
- package/templates/claude/SKILL.md +71 -0
- package/templates/claude/references/audit.md +43 -0
- package/templates/claude/{rules.md → references/conventions.md} +25 -28
- package/dist/cloud/index.cjs +0 -472
- package/dist/cloud/index.cjs.map +0 -1
- package/dist/cloud/index.d.cts +0 -82
- package/dist/cloud/index.d.ts +0 -82
- package/dist/cloud/index.js +0 -445
- package/dist/cloud/index.js.map +0 -1
- package/templates/claude/audit.md +0 -43
- /package/templates/claude/{api.md → references/api.md} +0 -0
package/dist/scan/index.d.cts
CHANGED
|
@@ -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;
|
|
@@ -10,6 +12,32 @@ interface EntityRef {
|
|
|
10
12
|
kind: EntityKind;
|
|
11
13
|
id: string;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The four kinds every captured route is expected to render — the state-matrix
|
|
17
|
+
* axis. A capture that only shoots the happy path leaves most of the value on
|
|
18
|
+
* the table, so a missing core kind must be captured, waived (the page CANNOT
|
|
19
|
+
* render it), or acknowledged in the MISSING_KINDS baseline.
|
|
20
|
+
*/
|
|
21
|
+
declare const CORE_STATE_KINDS: readonly ["loading", "empty", "populated", "error"];
|
|
22
|
+
type CoreStateKind = (typeof CORE_STATE_KINDS)[number];
|
|
23
|
+
/** `variant` is any state outside the matrix (a dialog, wizard step, multi-currency…). */
|
|
24
|
+
type StateKind = CoreStateKind | "variant";
|
|
25
|
+
/**
|
|
26
|
+
* A declared render state of an entity — the "story"-like unit: a named
|
|
27
|
+
* condition the page/feature/widget can render in (loading / empty / error /
|
|
28
|
+
* multi-currency / …). Enumerated on the entity (not derived from syntax), it
|
|
29
|
+
* is the contract the visual-states capture must satisfy and the anchor for
|
|
30
|
+
* per-state acceptance criteria. Fixtures + capture mechanics stay in the spec;
|
|
31
|
+
* the registry holds only the name (+ optional kind / per-state acceptance).
|
|
32
|
+
*/
|
|
33
|
+
interface StateDecl {
|
|
34
|
+
name: string;
|
|
35
|
+
/** Canonical kind for the state-matrix axis (defaults to `variant`). */
|
|
36
|
+
kind?: StateKind;
|
|
37
|
+
/** Acceptance criteria that only this state can evidence. */
|
|
38
|
+
acceptance?: string[];
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
13
41
|
interface Metadata {
|
|
14
42
|
name?: string;
|
|
15
43
|
description?: string;
|
|
@@ -19,6 +47,21 @@ interface Metadata {
|
|
|
19
47
|
flows?: readonly string[];
|
|
20
48
|
features?: string[];
|
|
21
49
|
widgets?: string[];
|
|
50
|
+
/** Declared render states (see StateDecl); the capture-completeness gate. */
|
|
51
|
+
states?: StateDecl[];
|
|
52
|
+
/**
|
|
53
|
+
* `false` opts this page/feature/widget out of visual-states capture forever
|
|
54
|
+
* (a redirect, a static notice, an internal target). The page-coverage gate
|
|
55
|
+
* treats an opted-out route as accounted-for. Reason belongs in `description`.
|
|
56
|
+
*/
|
|
57
|
+
capture?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Per-core-kind waivers: a core kind this page's route legitimately CANNOT
|
|
60
|
+
* render (a create form has no `empty`), mapped to a substantive reason. The
|
|
61
|
+
* state-matrix gate treats a waived kind as accounted-for. A waiver is a
|
|
62
|
+
* permanent design statement — unlike MISSING_KINDS backlog, which is debt.
|
|
63
|
+
*/
|
|
64
|
+
waivers?: Partial<Record<CoreStateKind, string>>;
|
|
22
65
|
}
|
|
23
66
|
interface EntityWithMetaBase {
|
|
24
67
|
id: string;
|
|
@@ -64,6 +107,7 @@ type Entity = Route | Page | Feature | Widget | Region | Element | Primitive | F
|
|
|
64
107
|
type EntityByKind<K extends EntityKind> = Extract<Entity, {
|
|
65
108
|
kind: K;
|
|
66
109
|
}>;
|
|
110
|
+
type MetaEntityKind = Exclude<EntityKind, "route" | "flow">;
|
|
67
111
|
|
|
68
112
|
interface ReportRecord {
|
|
69
113
|
id: string;
|
|
@@ -96,7 +140,7 @@ interface Registry {
|
|
|
96
140
|
setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
|
|
97
141
|
getReports(kind: EntityKind, id: string): readonly ReportRecord[];
|
|
98
142
|
listReportKeys(): readonly string[];
|
|
99
|
-
|
|
143
|
+
closeReport?: (reportId: string, status?: string) => void | Promise<void>;
|
|
100
144
|
onReportsChange(cb: () => void): () => void;
|
|
101
145
|
}
|
|
102
146
|
|
|
@@ -117,17 +161,33 @@ interface AuditConfig {
|
|
|
117
161
|
scopeLeak?: boolean;
|
|
118
162
|
coverage?: boolean;
|
|
119
163
|
acceptance?: boolean;
|
|
164
|
+
/** State-capture completeness gate (default on when a manifest is present). */
|
|
165
|
+
states?: boolean;
|
|
166
|
+
/** Page-capture coverage gate (default on when a manifest is present). */
|
|
167
|
+
pageCoverage?: boolean;
|
|
168
|
+
/** State-matrix (core-kind) gate (default on when a manifest is present). */
|
|
169
|
+
stateMatrix?: boolean;
|
|
120
170
|
}
|
|
121
|
-
type TypeMode = "strict" | "loose";
|
|
122
171
|
interface UidexConfig {
|
|
123
172
|
$schema?: string;
|
|
124
173
|
sources: SourceConfig[];
|
|
125
174
|
exclude?: string[];
|
|
126
175
|
output: string;
|
|
127
176
|
flows?: string[];
|
|
128
|
-
typeMode?: TypeMode;
|
|
129
177
|
audit?: AuditConfig;
|
|
130
178
|
conventions?: ConventionsConfig;
|
|
179
|
+
/**
|
|
180
|
+
* Path (relative to the config dir) of the captured-states manifest the
|
|
181
|
+
* Playwright states reporter writes. The completeness gate reads it when
|
|
182
|
+
* present. Defaults to `uidex-states.json`.
|
|
183
|
+
*/
|
|
184
|
+
statesManifest?: string;
|
|
185
|
+
/**
|
|
186
|
+
* Path (relative to the config dir) of the page-coverage baseline — the
|
|
187
|
+
* tool-managed, shrink-only backlog written by `uidex scan --update-baseline`.
|
|
188
|
+
* Defaults to `uidex-coverage-baseline.json`.
|
|
189
|
+
*/
|
|
190
|
+
coverageBaseline?: string;
|
|
131
191
|
}
|
|
132
192
|
interface DiscoveredConfig {
|
|
133
193
|
configPath: string;
|
|
@@ -140,7 +200,12 @@ interface ScannedFile {
|
|
|
140
200
|
displayPath: string;
|
|
141
201
|
content: string;
|
|
142
202
|
}
|
|
143
|
-
|
|
203
|
+
/** Byte-offset range into a ScannedFile's content (UTF-16 code units, like oxc spans). */
|
|
204
|
+
interface Span {
|
|
205
|
+
start: number;
|
|
206
|
+
end: number;
|
|
207
|
+
}
|
|
208
|
+
type AnnotationKind = "element" | "region" | "widget" | "primitive";
|
|
144
209
|
type DomAttrKind = "element" | "region" | "widget" | "primitive";
|
|
145
210
|
interface AnnotationAncestor {
|
|
146
211
|
kind: DomAttrKind;
|
|
@@ -151,10 +216,10 @@ interface Annotation {
|
|
|
151
216
|
id: string;
|
|
152
217
|
file: string;
|
|
153
218
|
line: number;
|
|
154
|
-
|
|
155
|
-
acceptance?: string[];
|
|
156
|
-
/** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
|
|
219
|
+
/** JSX ancestor chain, outermost to innermost. */
|
|
157
220
|
ancestors?: AnnotationAncestor[];
|
|
221
|
+
/** The span of the attribute's value literal. */
|
|
222
|
+
span?: Span;
|
|
158
223
|
}
|
|
159
224
|
type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
|
|
160
225
|
interface MetadataExport {
|
|
@@ -168,12 +233,106 @@ interface MetadataExport {
|
|
|
168
233
|
widgets?: string[];
|
|
169
234
|
notFlow?: boolean;
|
|
170
235
|
loc: Location;
|
|
236
|
+
/** Span of the whole `export const uidex = ...` statement (incl. trailing `;`). */
|
|
237
|
+
span?: Span;
|
|
238
|
+
/** Span of the id string literal (the discriminator field's value). */
|
|
239
|
+
idSpan?: Span;
|
|
240
|
+
/**
|
|
241
|
+
* Removal-ready spans of top-level fields (key through value, extended over
|
|
242
|
+
* one adjacent comma) keyed by field name. Used by `--fix` codemods.
|
|
243
|
+
*/
|
|
244
|
+
fieldSpans?: Record<string, Span>;
|
|
245
|
+
/** Spans of the string-literal items in `features`, parallel to `features`. */
|
|
246
|
+
featureSpans?: Span[];
|
|
247
|
+
/** Spans of the string-literal items in `widgets`, parallel to `widgets`. */
|
|
248
|
+
widgetSpans?: Span[];
|
|
249
|
+
/** Declared render states (page/feature/widget); see ParsedState. */
|
|
250
|
+
states?: ParsedState[];
|
|
251
|
+
/** `capture: false` opts the entity out of the page-coverage gate. */
|
|
252
|
+
capture?: boolean;
|
|
253
|
+
/** Per-core-kind waivers: kind → reason the route cannot render it. */
|
|
254
|
+
waivers?: Record<string, string>;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* A parsed `states` entry from `export const uidex`. The authored form is a
|
|
258
|
+
* string (bare name) or an object `{ name, acceptance?, description? }`; both
|
|
259
|
+
* normalize to this. `nameSpan` powers a future `uidex rename state`.
|
|
260
|
+
*/
|
|
261
|
+
interface ParsedState {
|
|
262
|
+
name: string;
|
|
263
|
+
/** Canonical kind: loading | empty | populated | error | variant. */
|
|
264
|
+
kind?: string;
|
|
265
|
+
acceptance?: string[];
|
|
266
|
+
description?: string;
|
|
267
|
+
/** Span of the state's name string literal. */
|
|
268
|
+
nameSpan?: Span;
|
|
269
|
+
}
|
|
270
|
+
/** A data-uidex* attribute whose value the scanner could not resolve. */
|
|
271
|
+
interface DynamicAttrFact {
|
|
272
|
+
kind: DomAttrKind;
|
|
273
|
+
attrName: string;
|
|
274
|
+
line: number;
|
|
275
|
+
}
|
|
276
|
+
/** An HTML5 landmark element (`<header>`, `<nav>`, …, or `role="region"`). */
|
|
277
|
+
interface LandmarkFact {
|
|
278
|
+
/** the host tag, or "region" for role="region" */
|
|
279
|
+
tag: string;
|
|
280
|
+
line: number;
|
|
281
|
+
}
|
|
282
|
+
/** A static import binding (import / export-from), for the scope-leak check. */
|
|
283
|
+
interface ImportFact {
|
|
284
|
+
specifier: string;
|
|
285
|
+
line: number;
|
|
286
|
+
/** span of the whole import/export statement */
|
|
287
|
+
span: Span;
|
|
288
|
+
/** `import type` or an export-from with type-only kind */
|
|
289
|
+
isTypeOnly: boolean;
|
|
290
|
+
/** imported local binding names (default + named + namespace) */
|
|
291
|
+
names: string[];
|
|
292
|
+
}
|
|
293
|
+
/** An interactive host element (`button`, `a`, …) with no data-uidex* attribute. */
|
|
294
|
+
interface InteractiveElementFact {
|
|
295
|
+
tag: string;
|
|
296
|
+
line: number;
|
|
297
|
+
/** the element carries a `{...props}` spread that may deliver the annotation */
|
|
298
|
+
hasSpread: boolean;
|
|
299
|
+
/** offset just after the tag name; where `--fix` inserts the generated attribute */
|
|
300
|
+
nameEnd: number;
|
|
301
|
+
/**
|
|
302
|
+
* Best static label for deriving an id (aria-label, visible text, title,
|
|
303
|
+
* name, placeholder). Absent when nothing usable is statically available.
|
|
304
|
+
*/
|
|
305
|
+
nameHint?: string;
|
|
306
|
+
}
|
|
307
|
+
interface FlowCallFact {
|
|
308
|
+
id: string;
|
|
309
|
+
action?: string;
|
|
310
|
+
line: number;
|
|
311
|
+
/** span of the id string literal argument */
|
|
312
|
+
span?: Span;
|
|
313
|
+
}
|
|
314
|
+
/** A `uidex(expr)` call whose argument is not a static string literal. */
|
|
315
|
+
interface DynamicFlowCallFact {
|
|
316
|
+
line: number;
|
|
317
|
+
}
|
|
318
|
+
/** A tagged `test.describe(..., { tag: "@uidex:flow" }, ...)` occurrence. */
|
|
319
|
+
interface FlowFact {
|
|
320
|
+
title: string;
|
|
321
|
+
line: number;
|
|
322
|
+
calls: FlowCallFact[];
|
|
323
|
+
/** uidex() calls inside this describe whose ids could not be resolved */
|
|
324
|
+
dynamicCalls?: DynamicFlowCallFact[];
|
|
171
325
|
}
|
|
172
326
|
interface ExtractedFile {
|
|
173
327
|
file: ScannedFile;
|
|
174
328
|
annotations: Annotation[];
|
|
175
329
|
metadata?: MetadataExport[];
|
|
176
330
|
diagnostics?: Diagnostic[];
|
|
331
|
+
flows?: FlowFact[];
|
|
332
|
+
dynamicAttrs?: DynamicAttrFact[];
|
|
333
|
+
unannotatedInteractive?: InteractiveElementFact[];
|
|
334
|
+
landmarks?: LandmarkFact[];
|
|
335
|
+
imports?: ImportFact[];
|
|
177
336
|
}
|
|
178
337
|
interface DetectedRoute {
|
|
179
338
|
id: string;
|
|
@@ -181,6 +340,30 @@ interface DetectedRoute {
|
|
|
181
340
|
file: string;
|
|
182
341
|
}
|
|
183
342
|
type DiagnosticSeverity = "error" | "warning" | "info";
|
|
343
|
+
/** A single text replacement; offsets index the file's content at scan time. */
|
|
344
|
+
interface FixEdit {
|
|
345
|
+
/** absolute path of the file to edit */
|
|
346
|
+
path: string;
|
|
347
|
+
start: number;
|
|
348
|
+
end: number;
|
|
349
|
+
replacement: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* A machine-applicable fix attached to a diagnostic. Applied by
|
|
353
|
+
* `uidex scan --fix`; edits are computed against the exact content the
|
|
354
|
+
* scanner read, so they must be applied before any other modification.
|
|
355
|
+
*/
|
|
356
|
+
interface DiagnosticFix {
|
|
357
|
+
description: string;
|
|
358
|
+
edits?: FixEdit[];
|
|
359
|
+
/** files to create (absolute path → content); skipped if the file exists */
|
|
360
|
+
createFiles?: Array<{
|
|
361
|
+
path: string;
|
|
362
|
+
content: string;
|
|
363
|
+
}>;
|
|
364
|
+
/** absolute paths of files to delete */
|
|
365
|
+
deleteFiles?: string[];
|
|
366
|
+
}
|
|
184
367
|
interface Diagnostic {
|
|
185
368
|
code: string;
|
|
186
369
|
severity: DiagnosticSeverity;
|
|
@@ -192,6 +375,7 @@ interface Diagnostic {
|
|
|
192
375
|
id: string;
|
|
193
376
|
};
|
|
194
377
|
hint?: string;
|
|
378
|
+
fix?: DiagnosticFix;
|
|
195
379
|
}
|
|
196
380
|
interface AuditSummary {
|
|
197
381
|
diagnostics: Diagnostic[];
|
|
@@ -231,11 +415,36 @@ declare function walk(sources: SourceConfig[], options: WalkOptions): ScannedFil
|
|
|
231
415
|
|
|
232
416
|
declare function extract(files: ScannedFile[]): ExtractedFile[];
|
|
233
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Shared parse layer for the scanner. Each file is parsed exactly once in
|
|
420
|
+
* the extract phase; the resulting AST never leaves it (ExtractedFile must
|
|
421
|
+
* stay JSON-serializable for the bundler-plugin watchers).
|
|
422
|
+
*
|
|
423
|
+
* oxc spans are UTF-16 code-unit offsets, i.e. they index directly into the
|
|
424
|
+
* JS source string (verified against oxc-parser@0.135.0).
|
|
425
|
+
*/
|
|
426
|
+
interface ParsedComment {
|
|
427
|
+
type: "Line" | "Block";
|
|
428
|
+
value: string;
|
|
429
|
+
start: number;
|
|
430
|
+
end: number;
|
|
431
|
+
}
|
|
432
|
+
interface ParsedSource {
|
|
433
|
+
/** null when parsing failed catastrophically — callers degrade to empty facts */
|
|
434
|
+
program: Program | null;
|
|
435
|
+
/** true when oxc reported recoverable errors (program is still usable) */
|
|
436
|
+
hasErrors: boolean;
|
|
437
|
+
/** all comments in source order; empty when parsing failed */
|
|
438
|
+
comments: ParsedComment[];
|
|
439
|
+
/** offset → 1-based line */
|
|
440
|
+
lineAt(offset: number): number;
|
|
441
|
+
}
|
|
442
|
+
|
|
234
443
|
interface UidexExportExtractResult {
|
|
235
444
|
exports: MetadataExport[];
|
|
236
445
|
diagnostics: Diagnostic[];
|
|
237
446
|
}
|
|
238
|
-
declare function extractUidexExports(file: ScannedFile): UidexExportExtractResult;
|
|
447
|
+
declare function extractUidexExports(file: ScannedFile, parsed?: ParsedSource): UidexExportExtractResult;
|
|
239
448
|
|
|
240
449
|
interface ResolveContext {
|
|
241
450
|
config: UidexConfig;
|
|
@@ -254,20 +463,115 @@ interface ResolveOutput {
|
|
|
254
463
|
*/
|
|
255
464
|
declare function resolve(ctx: ResolveContext): ResolveOutput;
|
|
256
465
|
|
|
466
|
+
/**
|
|
467
|
+
* State-capture completeness — the deterministic replacement for a bespoke
|
|
468
|
+
* `check-states.mjs`. Cross-checks the states DECLARED on registry entities
|
|
469
|
+
* (`export const uidex = { states: [...] }`) against the states a capture run
|
|
470
|
+
* actually produced (a `uidex-states.json` manifest written by the Playwright
|
|
471
|
+
* states reporter):
|
|
472
|
+
*
|
|
473
|
+
* - `missing-state-capture` — a declared state no run captured. The gap the
|
|
474
|
+
* acceptance-grounded analysis surfaces (a promised behavior with no pixel
|
|
475
|
+
* evidence) becomes a mechanical audit failure.
|
|
476
|
+
* - `orphan-state-capture` — a run captured a state the entity never declared,
|
|
477
|
+
* so the contact sheet shows a state the registry doesn't know about.
|
|
478
|
+
*
|
|
479
|
+
* Pure: same (registry, manifest) → same diagnostics. Only runs when a manifest
|
|
480
|
+
* is present, so a plain `uidex scan` (no capture run) never fails on it.
|
|
481
|
+
*/
|
|
482
|
+
/** One `entity/state` a capture run recorded. */
|
|
483
|
+
interface CapturedState {
|
|
484
|
+
/** Registry entity id the capture rendered (e.g. "products"). */
|
|
485
|
+
entity: string;
|
|
486
|
+
/** Entity kind, when the capture recorded it (page/feature/widget). */
|
|
487
|
+
kind?: MetaEntityKind;
|
|
488
|
+
/** The declared state name captured (e.g. "new-filled"). */
|
|
489
|
+
state: string;
|
|
490
|
+
/** Canonical matrix kind (loading/empty/populated/error/variant). */
|
|
491
|
+
stateKind?: string;
|
|
492
|
+
/** URL pathname the capture ran against (page-coverage gate matches it to a route). */
|
|
493
|
+
url?: string;
|
|
494
|
+
/** Explicit route pattern the capture covers, when supplied. */
|
|
495
|
+
route?: string;
|
|
496
|
+
}
|
|
497
|
+
interface CapturedStatesManifest {
|
|
498
|
+
captured: CapturedState[];
|
|
499
|
+
}
|
|
500
|
+
declare function checkStateCoverage(registry: Registry, manifest: CapturedStatesManifest): Diagnostic[];
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Page-capture coverage — the coarse gate above the per-state gate. Every
|
|
504
|
+
* derived route must be accounted for: captured (a run drove it), opted out
|
|
505
|
+
* (`export const uidex = { page, capture: false }`), or acknowledged backlog (a
|
|
506
|
+
* tool-managed, shrink-only baseline). A route in none of those is a *new*
|
|
507
|
+
* uncaptured page and fails hard — the ratchet that keeps debt from growing.
|
|
508
|
+
*
|
|
509
|
+
* uidex derives the route set (route groups stripped, dynamic segments kept), so
|
|
510
|
+
* unlike a hand-rolled `src/app/**` walk there is no page list to enumerate or
|
|
511
|
+
* keep from rotting: a route exists in the registry or it doesn't. And captures
|
|
512
|
+
* observe the URL they ran against, so which route a capture covers is matched,
|
|
513
|
+
* never hand-declared.
|
|
514
|
+
*/
|
|
515
|
+
/** The committed, tool-managed backlog. Shrinks as pages/kinds get captured. */
|
|
516
|
+
interface PageCoverageBaseline {
|
|
517
|
+
/** Route patterns acknowledged as uncaptured (e.g. "/[scope]/collections"). */
|
|
518
|
+
uncapturedPages: string[];
|
|
519
|
+
/** route → core kinds acknowledged as not-yet-captured (the matrix backlog). */
|
|
520
|
+
missingKinds?: Record<string, string[]>;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Turn a Next-style route pattern into an anchored matcher.
|
|
524
|
+
* `/[scope]/products/[productId]/edit` → matches `/acme/products/p1/edit`
|
|
525
|
+
* `/[...slug]` (catch-all) → matches one or more segments
|
|
526
|
+
* Returns the literal-segment count as a specificity score, so the most literal
|
|
527
|
+
* matching pattern wins when several match (e.g. `/x/new` beats `/x/[id]`).
|
|
528
|
+
*/
|
|
529
|
+
declare function compileRoute(pattern: string): {
|
|
530
|
+
test: (url: string) => boolean;
|
|
531
|
+
specificity: number;
|
|
532
|
+
};
|
|
533
|
+
/** The route pattern (from the given set) that most-specifically matches a URL. */
|
|
534
|
+
declare function matchUrlToRoute(url: string, routePaths: string[]): string | null;
|
|
535
|
+
declare function checkPageCoverage(registry: Registry, manifest: CapturedStatesManifest, baseline?: PageCoverageBaseline): Diagnostic[];
|
|
536
|
+
/**
|
|
537
|
+
* Compute the shrink-only baseline for `--update-baseline`: every route with no
|
|
538
|
+
* capture and no opt-out. Seeds on first run and prunes on later runs; growth
|
|
539
|
+
* only ever happens through this explicit command, visible in the file's diff.
|
|
540
|
+
*/
|
|
541
|
+
declare function computePageBaseline(registry: Registry, manifest: CapturedStatesManifest): PageCoverageBaseline;
|
|
542
|
+
|
|
257
543
|
interface AuditOptions {
|
|
258
544
|
registry: Registry;
|
|
259
545
|
extracted: ExtractedFile[];
|
|
260
546
|
files: ScannedFile[];
|
|
547
|
+
/** Extract output for flow spec files; enables per-call reference checks. */
|
|
548
|
+
flowExtracted?: ExtractedFile[];
|
|
261
549
|
config: UidexConfig;
|
|
262
550
|
check?: boolean;
|
|
263
551
|
lint?: boolean;
|
|
264
552
|
resolveDiagnostics?: Diagnostic[];
|
|
265
|
-
/** Freshly-emitted
|
|
553
|
+
/** Freshly-emitted DATA-file bytes (the entity array), required for --check. */
|
|
266
554
|
generated?: string;
|
|
267
|
-
/** On-disk contents of the
|
|
555
|
+
/** On-disk contents of the data file at scan time; `null` if it does not exist. */
|
|
268
556
|
existingOnDisk?: string | null;
|
|
269
|
-
/** Relative path of the
|
|
557
|
+
/** Relative path of the data file; used in diagnostics. */
|
|
270
558
|
outputPath?: string;
|
|
559
|
+
/** Freshly-emitted TYPES-file bytes; enables the types-file --check compare. */
|
|
560
|
+
typesGenerated?: string;
|
|
561
|
+
/** On-disk contents of the types file at scan time; `null` if it does not exist. */
|
|
562
|
+
typesOnDisk?: string | null;
|
|
563
|
+
/** Relative path of the configured `output` (types) file; used in diagnostics. */
|
|
564
|
+
typesOutputPath?: string;
|
|
565
|
+
/** Freshly-emitted LOCS-file bytes; enables the locs-file --check compare. */
|
|
566
|
+
locsGenerated?: string;
|
|
567
|
+
/** On-disk contents of the locs file at scan time; `null` if it does not exist. */
|
|
568
|
+
locsOnDisk?: string | null;
|
|
569
|
+
/** Relative path of the `.locs` file; used in diagnostics. */
|
|
570
|
+
locsOutputPath?: string;
|
|
571
|
+
/** Captured-states manifest for the completeness gate; absent → gate skipped. */
|
|
572
|
+
capturedStates?: CapturedStatesManifest;
|
|
573
|
+
/** Page-coverage baseline (the shrink-only backlog); absent → empty baseline. */
|
|
574
|
+
pageBaseline?: PageCoverageBaseline;
|
|
271
575
|
}
|
|
272
576
|
declare function audit(opts: AuditOptions): AuditSummary;
|
|
273
577
|
|
|
@@ -276,10 +580,32 @@ interface EmitOptions {
|
|
|
276
580
|
gitContext?: GitContext;
|
|
277
581
|
/** The import source for `createUidex` in the generated preconfigured export. */
|
|
278
582
|
uidexImport?: string;
|
|
279
|
-
/** Controls id-union emission: "strict" emits literal unions, "loose" emits `string`. */
|
|
280
|
-
typeMode?: TypeMode;
|
|
281
583
|
}
|
|
282
|
-
|
|
584
|
+
/**
|
|
585
|
+
* The scanner emits THREE artifacts, split by consumer:
|
|
586
|
+
*
|
|
587
|
+
* - `types` — the id unions + the `Uidex` authoring-shape namespace. Pure types,
|
|
588
|
+
* no runtime, no `@ts-nocheck`. This is the file the ~dozens of
|
|
589
|
+
* `import type { Uidex } from "@/uidex.gen"` sites resolve, so it must stay
|
|
590
|
+
* small: an `import type` still forces tsc/tsserver to fully parse the target
|
|
591
|
+
* module, so keeping the runtime entity data out of it is what keeps the editor
|
|
592
|
+
* and type-checker responsive across every consumer file.
|
|
593
|
+
* - `data` — the AUTHORED registry (route/page/feature/widget/primitive/flow) +
|
|
594
|
+
* `gitContext` + `loadRegistry` + the preconfigured `uidex` instance. Runtime,
|
|
595
|
+
* `@ts-nocheck`. Loaded eagerly by the devtools on mount.
|
|
596
|
+
* - `locs` — the thin element/region `{ id, loc }` entities, emitted as positional
|
|
597
|
+
* tuples (see `emitLocsModule`). Loaded lazily by the devtools (idle / first
|
|
598
|
+
* inspect) via `loadLocs`, so ~90% of the entity count stays out of the initial
|
|
599
|
+
* devtools chunk.
|
|
600
|
+
*/
|
|
601
|
+
interface EmitResult {
|
|
602
|
+
types: string;
|
|
603
|
+
/** Authored-metadata registry (pages/features/widgets/flows/…). */
|
|
604
|
+
data: string;
|
|
605
|
+
/** Deferred `id → loc` module for the thin element/region entities. */
|
|
606
|
+
locs: string;
|
|
607
|
+
}
|
|
608
|
+
declare function emit(opts: EmitOptions): EmitResult;
|
|
283
609
|
|
|
284
610
|
/**
|
|
285
611
|
* Detect framework-aware routes:
|
|
@@ -297,6 +623,15 @@ interface GitResolveOptions {
|
|
|
297
623
|
}
|
|
298
624
|
declare function resolveGitContext(opts?: GitResolveOptions): GitContext;
|
|
299
625
|
|
|
626
|
+
type ScaffoldKind = "widget" | "page" | "feature";
|
|
627
|
+
interface ScaffoldSpecOptions {
|
|
628
|
+
registry: Registry;
|
|
629
|
+
kind: ScaffoldKind;
|
|
630
|
+
id: string;
|
|
631
|
+
outDir: string;
|
|
632
|
+
force?: boolean;
|
|
633
|
+
fixtureImport?: string;
|
|
634
|
+
}
|
|
300
635
|
interface ScaffoldOptions {
|
|
301
636
|
registry: Registry;
|
|
302
637
|
widgetId: string;
|
|
@@ -311,12 +646,33 @@ interface ScaffoldResult {
|
|
|
311
646
|
reason?: string;
|
|
312
647
|
}
|
|
313
648
|
declare function scaffoldWidgetSpec(opts: ScaffoldOptions): ScaffoldResult;
|
|
649
|
+
/**
|
|
650
|
+
* Emits a tagged Playwright stub from an entity's declared acceptance
|
|
651
|
+
* criteria — one `test()` per criterion. Widgets keep the historical
|
|
652
|
+
* `widget-<id>.spec.ts` name; pages and features emit `flow-<id>.spec.ts`
|
|
653
|
+
* per the one-tagged-describe-per-flow-spec convention.
|
|
654
|
+
*/
|
|
655
|
+
declare function scaffoldSpec(opts: ScaffoldSpecOptions): ScaffoldResult;
|
|
314
656
|
|
|
315
657
|
interface RunScanOptions {
|
|
316
658
|
cwd?: string;
|
|
317
659
|
check?: boolean;
|
|
318
660
|
lint?: boolean;
|
|
319
661
|
configs?: DiscoveredConfig[];
|
|
662
|
+
/** Captured-states manifest for the completeness gate; overrides on-disk load. */
|
|
663
|
+
capturedStates?: CapturedStatesManifest;
|
|
664
|
+
/** Page-coverage baseline; overrides on-disk load. */
|
|
665
|
+
pageBaseline?: PageCoverageBaseline;
|
|
666
|
+
}
|
|
667
|
+
/** Load the page-coverage baseline from disk, if present + well-formed. */
|
|
668
|
+
declare function loadPageBaseline(configDir: string, config: UidexConfig): PageCoverageBaseline | undefined;
|
|
669
|
+
/** One generated artifact: its bytes plus where it lands (abs + repo-relative). */
|
|
670
|
+
interface ScanArtifact {
|
|
671
|
+
generated: string;
|
|
672
|
+
/** Absolute path on disk. */
|
|
673
|
+
outputPath: string;
|
|
674
|
+
/** Path relative to the config dir, for diagnostics. */
|
|
675
|
+
outputRel: string;
|
|
320
676
|
}
|
|
321
677
|
interface ScanResult {
|
|
322
678
|
config: UidexConfig;
|
|
@@ -324,11 +680,103 @@ interface ScanResult {
|
|
|
324
680
|
registry: Registry;
|
|
325
681
|
gitContext: GitContext;
|
|
326
682
|
audit?: AuditSummary;
|
|
327
|
-
|
|
328
|
-
|
|
683
|
+
/** Pure-types artifact (`config.output`, e.g. `uidex.gen.ts`). */
|
|
684
|
+
types: ScanArtifact;
|
|
685
|
+
/** Authored-registry artifact (the sibling `.data` module). */
|
|
686
|
+
data: ScanArtifact;
|
|
687
|
+
/** Deferred element/region source-location artifact (the sibling `.locs` module). */
|
|
688
|
+
locs: ScanArtifact;
|
|
689
|
+
/** The captured-states manifest in effect this run, if any (for --update-baseline). */
|
|
690
|
+
capturedStates?: CapturedStatesManifest;
|
|
329
691
|
}
|
|
330
692
|
declare function runScan(opts?: RunScanOptions): ScanResult[];
|
|
331
|
-
|
|
693
|
+
/**
|
|
694
|
+
* Write all three generated artifacts (types + data + locs), each only if
|
|
695
|
+
* changed. Splitting them means a change confined to one (e.g. adding an element
|
|
696
|
+
* only touches `.locs`) never rewrites the others, so watchers recompile only what
|
|
697
|
+
* actually moved. Returns `true` if any file was written.
|
|
698
|
+
*/
|
|
699
|
+
declare function writeScanResult(result: ScanResult): boolean;
|
|
700
|
+
/** Absolute path of the page-coverage baseline for a scan result. */
|
|
701
|
+
declare function pageBaselinePath(result: ScanResult): string;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* State-matrix coverage — the second completeness axis. A capture that only ever
|
|
705
|
+
* shoots the happy path leaves most of the harness's value on the table, so
|
|
706
|
+
* every *captured* route must render each of the four core kinds
|
|
707
|
+
* (loading / empty / populated / error), or account for the gap:
|
|
708
|
+
*
|
|
709
|
+
* - a **waiver** (`export const uidex = { page, waivers: { empty: "…" } }`) —
|
|
710
|
+
* the route CANNOT render that kind (a create form has no empty state);
|
|
711
|
+
* - the **MISSING_KINDS baseline** — the route CAN render it and nobody has
|
|
712
|
+
* captured it yet (a per-(route, kind) shrink-only backlog).
|
|
713
|
+
*
|
|
714
|
+
* Anything else is a `core-kind` error. Only *captured* routes are in scope: a
|
|
715
|
+
* route with no capture at all is a page-coverage concern, not a matrix hole.
|
|
716
|
+
*/
|
|
717
|
+
/** route → core kinds acknowledged as not-yet-captured (the finer ratchet). */
|
|
718
|
+
interface StateMatrixBaseline {
|
|
719
|
+
missingKinds: Record<string, string[]>;
|
|
720
|
+
}
|
|
721
|
+
declare function checkStateMatrix(registry: Registry, manifest: CapturedStatesManifest, baseline?: StateMatrixBaseline): Diagnostic[];
|
|
722
|
+
/**
|
|
723
|
+
* Compute the shrink-only MISSING_KINDS baseline for `--update-baseline`: every
|
|
724
|
+
* (captured route, core kind) that is neither captured nor waived.
|
|
725
|
+
*/
|
|
726
|
+
declare function computeMissingKinds(registry: Registry, manifest: CapturedStatesManifest): Record<string, string[]>;
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Applies the machine-generated fixes attached to diagnostics.
|
|
730
|
+
*
|
|
731
|
+
* Edits are offset-based against the content the scanner read, so this must
|
|
732
|
+
* run before anything else touches the files. Within a file, edits apply
|
|
733
|
+
* last-to-first; an edit overlapping one already applied is skipped. Identical
|
|
734
|
+
* edits are deduped — two fixes in one file emitting the same edit collapse to
|
|
735
|
+
* one application.
|
|
736
|
+
*/
|
|
737
|
+
interface AppliedFix {
|
|
738
|
+
code: string;
|
|
739
|
+
description: string;
|
|
740
|
+
file?: string;
|
|
741
|
+
}
|
|
742
|
+
interface ApplyFixesResult {
|
|
743
|
+
applied: AppliedFix[];
|
|
744
|
+
/** fixes skipped because of overlapping edits or existing target files */
|
|
745
|
+
skipped: Array<AppliedFix & {
|
|
746
|
+
reason: string;
|
|
747
|
+
}>;
|
|
748
|
+
}
|
|
749
|
+
declare function applyFixes(diagnostics: Diagnostic[]): ApplyFixesResult;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Cross-file id rename. The registry knows every reference surface for an
|
|
753
|
+
* id — the DOM attribute, `uidex("…")` calls in flow specs, the widget
|
|
754
|
+
* export's discriminator, and `widgets:` arrays — so the rename rewrites all
|
|
755
|
+
* of them in one pass and regenerates the gen file. Occurrences the scanner
|
|
756
|
+
* can't edit mechanically (const indirection, ternaries, patterns) are
|
|
757
|
+
* reported for manual follow-up instead of being guessed at.
|
|
758
|
+
*/
|
|
759
|
+
type RenameKind = "element" | "widget" | "region";
|
|
760
|
+
interface RenameOptions {
|
|
761
|
+
cwd: string;
|
|
762
|
+
kind: RenameKind;
|
|
763
|
+
oldId: string;
|
|
764
|
+
newId: string;
|
|
765
|
+
/** allow renaming onto an id that already exists */
|
|
766
|
+
force?: boolean;
|
|
767
|
+
}
|
|
768
|
+
interface RenameResult {
|
|
769
|
+
/** number of source edits applied */
|
|
770
|
+
edits: number;
|
|
771
|
+
/** occurrences that need a human (no static literal to rewrite) */
|
|
772
|
+
manual: Array<{
|
|
773
|
+
file: string;
|
|
774
|
+
line: number;
|
|
775
|
+
reason: string;
|
|
776
|
+
}>;
|
|
777
|
+
errors: string[];
|
|
778
|
+
}
|
|
779
|
+
declare function renameEntity(opts: RenameOptions): RenameResult;
|
|
332
780
|
|
|
333
781
|
interface CliResult {
|
|
334
782
|
exitCode: number;
|
|
@@ -341,7 +789,6 @@ interface CliOptions {
|
|
|
341
789
|
}
|
|
342
790
|
declare function run(opts: CliOptions): Promise<CliResult>;
|
|
343
791
|
|
|
344
|
-
declare const DEFAULT_TYPE_MODE: TypeMode;
|
|
345
792
|
declare class ConfigError extends Error {
|
|
346
793
|
constructor(message: string);
|
|
347
794
|
}
|
|
@@ -366,12 +813,26 @@ declare const DEFAULT_CONVENTIONS: Required<Pick<NonNullable<UidexConfig["conven
|
|
|
366
813
|
* bootstrapping before the first scan).
|
|
367
814
|
*/
|
|
368
815
|
declare namespace Uidex {
|
|
816
|
+
type CoreStateKind = "loading" | "empty" | "populated" | "error";
|
|
817
|
+
type StateKind = CoreStateKind | "variant";
|
|
818
|
+
/** A declared render state, as authored in `states: [{ name, ... }]`. */
|
|
819
|
+
interface State {
|
|
820
|
+
name: string;
|
|
821
|
+
kind?: StateKind;
|
|
822
|
+
acceptance?: readonly string[];
|
|
823
|
+
description?: string;
|
|
824
|
+
}
|
|
825
|
+
/** Per-core-kind waivers: a kind this route cannot render → reason. */
|
|
826
|
+
type Waivers = Partial<Record<CoreStateKind, string>>;
|
|
369
827
|
interface Page<PageIds extends string = string, FeatureIds extends string = string, WidgetIds extends string = string> {
|
|
370
828
|
page: PageIds | false;
|
|
371
829
|
name?: string;
|
|
372
830
|
features?: readonly FeatureIds[];
|
|
373
831
|
widgets?: readonly WidgetIds[];
|
|
374
832
|
acceptance?: readonly string[];
|
|
833
|
+
states?: readonly (string | State)[];
|
|
834
|
+
capture?: boolean;
|
|
835
|
+
waivers?: Waivers;
|
|
375
836
|
description?: string;
|
|
376
837
|
}
|
|
377
838
|
interface Feature<FeatureIds extends string = string> {
|
|
@@ -379,6 +840,7 @@ declare namespace Uidex {
|
|
|
379
840
|
name?: string;
|
|
380
841
|
features?: readonly FeatureIds[];
|
|
381
842
|
acceptance?: readonly string[];
|
|
843
|
+
states?: readonly (string | State)[];
|
|
382
844
|
description?: string;
|
|
383
845
|
}
|
|
384
846
|
interface Primitive<PrimitiveIds extends string = string> {
|
|
@@ -390,6 +852,7 @@ declare namespace Uidex {
|
|
|
390
852
|
widget: WidgetIds;
|
|
391
853
|
name?: string;
|
|
392
854
|
acceptance?: readonly string[];
|
|
855
|
+
states?: readonly (string | State)[];
|
|
393
856
|
description?: string;
|
|
394
857
|
}
|
|
395
858
|
interface Flow<FlowIds extends string = string> {
|
|
@@ -402,4 +865,4 @@ declare namespace Uidex {
|
|
|
402
865
|
}
|
|
403
866
|
}
|
|
404
867
|
|
|
405
|
-
export { type Annotation, type AnnotationKind, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CliOptions, type CliResult, ConfigError, type ConventionsConfig, DEFAULT_CONVENTIONS,
|
|
868
|
+
export { type Annotation, type AnnotationKind, type AppliedFix, type ApplyFixesResult, type AuditConfig, type AuditSummary, CONFIG_FILENAME, type CapturedState, type CapturedStatesManifest, 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 PageCoverageBaseline, type ParsedState, type RenameKind, type RenameOptions, type RenameResult, type RunScanOptions, type ScaffoldKind, type ScaffoldOptions, type ScaffoldResult, type ScaffoldSpecOptions, type ScanResult, type ScannedFile, type SourceConfig, type Span, type StateMatrixBaseline, Uidex, type UidexConfig, applyFixes, audit, checkPageCoverage, checkStateCoverage, checkStateMatrix, compileRoute, computeMissingKinds, computePageBaseline, detectRoutes, discover, emit, extract, extractUidexExports, globToRegExp, loadPageBaseline, matchUrlToRoute, pageBaselinePath, parseConfig, pathToId, renameEntity, resolve, resolveGitContext, run as runCli, runScan, scaffoldSpec, scaffoldWidgetSpec, validateConfig, walk, writeScanResult };
|