uidex 0.8.0 → 0.10.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 (43) hide show
  1. package/dist/cli/cli.cjs +1273 -343
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +3 -0
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +19 -13
  6. package/dist/headless/index.d.ts +19 -13
  7. package/dist/headless/index.js +3 -0
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +3 -0
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +19 -13
  12. package/dist/index.d.ts +19 -13
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +36 -7
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.js +36 -7
  18. package/dist/playwright/index.js.map +1 -1
  19. package/dist/playwright/states-reporter.cjs +36 -7
  20. package/dist/playwright/states-reporter.cjs.map +1 -1
  21. package/dist/playwright/states-reporter.d.cts +15 -0
  22. package/dist/playwright/states-reporter.d.ts +15 -0
  23. package/dist/playwright/states-reporter.js +36 -7
  24. package/dist/playwright/states-reporter.js.map +1 -1
  25. package/dist/playwright/states.cjs +8 -0
  26. package/dist/playwright/states.cjs.map +1 -1
  27. package/dist/playwright/states.d.cts +44 -1
  28. package/dist/playwright/states.d.ts +44 -1
  29. package/dist/playwright/states.js +7 -0
  30. package/dist/playwright/states.js.map +1 -1
  31. package/dist/react/index.cjs +3 -0
  32. package/dist/react/index.cjs.map +1 -1
  33. package/dist/react/index.d.cts +19 -13
  34. package/dist/react/index.d.ts +19 -13
  35. package/dist/react/index.js +3 -0
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/scan/index.cjs +1217 -257
  38. package/dist/scan/index.cjs.map +1 -1
  39. package/dist/scan/index.d.cts +410 -104
  40. package/dist/scan/index.d.ts +410 -104
  41. package/dist/scan/index.js +1197 -253
  42. package/dist/scan/index.js.map +1 -1
  43. package/package.json +17 -17
@@ -15,8 +15,8 @@ interface EntityRef {
15
15
  /**
16
16
  * The four kinds every captured route is expected to render — the state-matrix
17
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.
18
+ * the table, so a missing core kind must be captured or ACKNOWLEDGED (see
19
+ * `Metadata.acknowledge`).
20
20
  */
21
21
  declare const CORE_STATE_KINDS: readonly ["loading", "empty", "populated", "error"];
22
22
  type CoreStateKind = (typeof CORE_STATE_KINDS)[number];
@@ -50,18 +50,24 @@ interface Metadata {
50
50
  /** Declared render states (see StateDecl); the capture-completeness gate. */
51
51
  states?: StateDecl[];
52
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`.
53
+ * The ONE way to say "don't complain about this gap": scope typed reason.
54
+ *
55
+ * - scope `"*"` covers the whole entity (a redirect page that can never be
56
+ * captured, a component whose states are not gated here);
57
+ * - a core-kind scope covers that one cell of this page's route matrix.
58
+ *
59
+ * `type: "impossible"` is a permanent design statement and needs a `reason`;
60
+ * `type: "backlog"` is debt and does not. Keeping both as values of one field
61
+ * is what makes mislabeling debt as impossible a visible, reviewable edit.
62
+ * `captured-elsewhere` is computed by the workspace and never authored here.
56
63
  */
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>>;
64
+ acknowledge?: Partial<Record<AcknowledgeScope, MetaAcknowledgment>>;
65
+ }
66
+ /** Scope of an acknowledgment: one core kind, or the whole entity. */
67
+ type AcknowledgeScope = CoreStateKind | "*";
68
+ interface MetaAcknowledgment {
69
+ type: "impossible" | "backlog";
70
+ reason?: string;
65
71
  }
66
72
  interface EntityWithMetaBase {
67
73
  id: string;
@@ -153,7 +159,18 @@ interface SourceConfig {
153
159
  interface ConventionsConfig {
154
160
  primitives?: string[] | false;
155
161
  features?: string | false;
156
- pages?: "auto" | false;
162
+ /**
163
+ * How Page entities are derived from route files.
164
+ * - `"auto"` (default): infer the route root from the `app`/`pages`/
165
+ * `routes` directory-name heuristic.
166
+ * - `{ routesDir }`: the route root is exactly this dir (relative to the
167
+ * config dir), detected with the TanStack / React Router `routes/`-style
168
+ * convention. Use for a custom TanStack `routesDirectory`.
169
+ * - `false`: do not derive pages from routes.
170
+ */
171
+ pages?: "auto" | false | {
172
+ routesDir: string;
173
+ };
157
174
  flows?: string[] | false;
158
175
  regions?: "landmarks" | false;
159
176
  }
@@ -167,6 +184,15 @@ interface AuditConfig {
167
184
  pageCoverage?: boolean;
168
185
  /** State-matrix (core-kind) gate (default on when a manifest is present). */
169
186
  stateMatrix?: boolean;
187
+ /**
188
+ * Per-diagnostic-code severity overrides, e.g.
189
+ * `{ "missing-state-capture": "error" }`. A code's built-in severity is a
190
+ * default, not a policy — this is how a repo escalates an advisory gate into
191
+ * a build-failing ratchet, or silences one it has consciously accepted.
192
+ * `"off"` drops the diagnostic entirely. Unknown codes are rejected by
193
+ * config validation, so a typo can't silently disable a gate.
194
+ */
195
+ severity?: Record<string, DiagnosticSeverity | "off">;
170
196
  }
171
197
  interface UidexConfig {
172
198
  $schema?: string;
@@ -183,11 +209,21 @@ interface UidexConfig {
183
209
  */
184
210
  statesManifest?: string;
185
211
  /**
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`.
212
+ * Path (relative to the config dir) of the coverage baseline — the
213
+ * tool-managed, shrink-only `acknowledge` backlog written by
214
+ * `uidex scan --update-baseline`. Defaults to `uidex-coverage-baseline.json`.
189
215
  */
190
216
  coverageBaseline?: string;
217
+ /**
218
+ * The workspace this app belongs to — the level that answers "did a SIBLING
219
+ * app capture this shared declaration?" exactly, instead of guessing.
220
+ *
221
+ * - omitted (default): auto-detect by walking up for a workspace marker
222
+ * (`pnpm-workspace.yaml`, `turbo.json`, `.git`, …). Costs a few JSON reads.
223
+ * - a path (relative to the config dir): use exactly this root.
224
+ * - `false`: closed world — never look at siblings.
225
+ */
226
+ workspace?: string | false;
191
227
  }
192
228
  interface DiscoveredConfig {
193
229
  configPath: string;
@@ -242,16 +278,32 @@ interface MetadataExport {
242
278
  * one adjacent comma) keyed by field name. Used by `--fix` codemods.
243
279
  */
244
280
  fieldSpans?: Record<string, Span>;
281
+ /**
282
+ * Spans of top-level fields WITHOUT the comma extension (key start → value
283
+ * end), keyed by field name. `fieldSpans` deletes a property; this one
284
+ * replaces it in place, which is what the `acknowledge` codemod needs.
285
+ */
286
+ fieldPropSpans?: Record<string, Span>;
245
287
  /** Spans of the string-literal items in `features`, parallel to `features`. */
246
288
  featureSpans?: Span[];
247
289
  /** Spans of the string-literal items in `widgets`, parallel to `widgets`. */
248
290
  widgetSpans?: Span[];
249
291
  /** Declared render states (page/feature/widget); see ParsedState. */
250
292
  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>;
293
+ /** Scope (`"*"` | core kind) typed acknowledgment. See `acknowledge.ts`. */
294
+ acknowledge?: Record<string, ParsedAcknowledgment>;
295
+ /**
296
+ * REMOVED fields, parsed only so the `acknowledge` codemod can rewrite them.
297
+ * No gate reads these — an unmigrated declaration is an error with a fix
298
+ * attached, never a silently different meaning.
299
+ */
300
+ legacyCapture?: boolean;
301
+ legacyWaivers?: Record<string, string>;
302
+ }
303
+ /** A parsed `acknowledge` entry from `export const uidex`. */
304
+ interface ParsedAcknowledgment {
305
+ type: "impossible" | "backlog";
306
+ reason?: string;
255
307
  }
256
308
  /**
257
309
  * A parsed `states` entry from `export const uidex`. The authored form is a
@@ -394,6 +446,13 @@ declare const CONFIG_FILENAME = ".uidex.json";
394
446
  interface DiscoverOptions {
395
447
  cwd?: string;
396
448
  maxDepth?: number;
449
+ /**
450
+ * Keep descending past a config found at `cwd` itself, so the result is EVERY
451
+ * config in the tree rather than the innermost one. The workspace level needs
452
+ * the whole set; a plain `uidex scan` wants the nearest, which is why that
453
+ * stays the default.
454
+ */
455
+ all?: boolean;
397
456
  }
398
457
  declare function discover(options?: DiscoverOptions): DiscoveredConfig[];
399
458
 
@@ -463,6 +522,151 @@ interface ResolveOutput {
463
522
  */
464
523
  declare function resolve(ctx: ResolveContext): ResolveOutput;
465
524
 
525
+ /**
526
+ * ACKNOWLEDGMENT — the single primitive for "don't complain about this".
527
+ *
528
+ * There used to be four mechanisms plus an implicit fifth (`waivers`, the
529
+ * baseline's `missingKinds` / `uncapturedPages`, `capture: false`, and a
530
+ * borrowed-entity inference). They overlapped, lived in unrelated files with
531
+ * unrelated shapes, and the rule keeping them straight — "don't fold capturable
532
+ * debt into a waiver" — was enforced socially, in prose, because nothing
533
+ * structural stopped you reaching for the wrong one.
534
+ *
535
+ * They are now one map from a SCOPE to a typed REASON:
536
+ *
537
+ * acknowledge: {
538
+ * empty: { type: "impossible", reason: "a create form has no empty state" },
539
+ * "*": { type: "backlog" },
540
+ * }
541
+ *
542
+ * SCOPE is a core kind (that kind of this route) or `"*"` (the whole entity —
543
+ * every axis of it). TYPE is why:
544
+ *
545
+ * - `impossible` — human-authored. The surface CANNOT render this; a
546
+ * substantive `reason` is REQUIRED. Contradicted by an actual capture →
547
+ * `stale-acknowledgment`.
548
+ * - `backlog` — debt. It CAN render this and nobody has captured it yet.
549
+ * `reason` is optional. Tool-managed in the coverage baseline
550
+ * (`--update-baseline`, shrink-only), and authorable in source when you want
551
+ * the debt to sit next to the declaration.
552
+ * - `captured-elsewhere` — COMPUTED by the workspace level, never authored: a
553
+ * sibling app that registers this same declaration captured it. Replaces the
554
+ * borrowed-entity guess with an exact answer.
555
+ *
556
+ * The anti-laundering property is now structural rather than social:
557
+ * `impossible` and `backlog` are sibling values of ONE field, so choosing wrongly
558
+ * is a one-token, greppable, reviewable edit — not a choice between two
559
+ * unrelated files. And the reason rule points the lazy path at the honest
560
+ * answer: `impossible` costs you a sentence, `backlog` is free.
561
+ */
562
+ /** The scope key meaning "the whole entity", not one core kind. */
563
+ declare const ACK_ALL = "*";
564
+ /** Every valid scope key: the four core kinds plus `"*"`. */
565
+ declare const ACK_SCOPES: readonly string[];
566
+ type AcknowledgeType = "impossible" | "backlog" | "captured-elsewhere";
567
+ interface Acknowledgment {
568
+ type: AcknowledgeType;
569
+ /** Why. Required for `impossible`, optional for `backlog`. */
570
+ reason?: string;
571
+ /** `captured-elsewhere` only: the workspace app whose capture covers this. */
572
+ by?: string;
573
+ }
574
+ /** scope (`"*"` | core kind) → why it is acknowledged. */
575
+ type AcknowledgeMap = Record<string, Acknowledgment>;
576
+ /** Where an acknowledgment came from, so diagnostics can point at the right file. */
577
+ type AcknowledgeOrigin = "source" | "baseline" | "workspace";
578
+ interface ResolvedAcknowledgment extends Acknowledgment {
579
+ scope: string;
580
+ origin: AcknowledgeOrigin;
581
+ }
582
+ /**
583
+ * The committed, tool-managed coverage backlog. Same `Acknowledgment` shape as
584
+ * the source-authored field, keyed by route pattern — so the two really are the
585
+ * same primitive in two homes, not two lookalikes.
586
+ */
587
+ interface CoverageBaseline {
588
+ version: 2;
589
+ /** route pattern → scope → acknowledgment (always `type: "backlog"`). */
590
+ acknowledge: Record<string, AcknowledgeMap>;
591
+ }
592
+ /** The pre-`acknowledge` baseline shape, recognized only to migrate it. */
593
+ interface LegacyCoverageBaseline {
594
+ uncapturedPages?: string[];
595
+ missingKinds?: Record<string, string[]>;
596
+ }
597
+ /**
598
+ * The acknowledgments in force for one ROUTE: the baseline's entry for the route
599
+ * layered UNDER the page's own `acknowledge`. Source wins — a human statement
600
+ * outranks tool-recorded debt, and `--update-baseline` drops the shadowed entry
601
+ * on its next run.
602
+ */
603
+ declare function routeAcknowledge(registry: Registry, baseline: CoverageBaseline | undefined, route: string, page: string | undefined): Map<string, ResolvedAcknowledgment>;
604
+ /**
605
+ * The acknowledgments in force for one ENTITY (page/feature/widget). Only `"*"`
606
+ * is meaningful here — the core-kind axis is route-scoped. `capturedElsewhere`
607
+ * is the workspace's computed verdict; source still wins, so an explicit
608
+ * `impossible` is never silently reinterpreted.
609
+ */
610
+ declare function entityAcknowledge(meta: Metadata | undefined, capturedElsewhereBy?: string): ResolvedAcknowledgment | undefined;
611
+ /**
612
+ * Parse a v2 baseline. Malformed entries are dropped rather than thrown on: a
613
+ * hand-mangled baseline must not crash the scan, and every dropped entry simply
614
+ * re-appears as an unacknowledged gap the gate reports.
615
+ */
616
+ declare function parseCoverageBaseline(raw: unknown): CoverageBaseline | null;
617
+ /** Whether a parsed baseline JSON is the pre-`acknowledge` shape. */
618
+ declare function isLegacyBaseline(raw: unknown): raw is LegacyCoverageBaseline;
619
+ /** Convert the pre-`acknowledge` baseline shape to v2. Pure; no I/O. */
620
+ declare function migrateBaseline(legacy: LegacyCoverageBaseline): CoverageBaseline;
621
+ /** Serialize a baseline with stable key order, so its diff is reviewable. */
622
+ declare function serializeCoverageBaseline(baseline: CoverageBaseline): string;
623
+
624
+ interface WorkspaceApp {
625
+ /** Display name — the config dir's basename (e.g. "platform-web"). */
626
+ name: string;
627
+ configDir: string;
628
+ configPath: string;
629
+ /** Whether this app's configured sources would register `absFile`. */
630
+ registers: (absFile: string) => boolean;
631
+ /** `captureKey` values this app's manifest captured. */
632
+ captured: ReadonlySet<string>;
633
+ }
634
+ interface Workspace {
635
+ root: string;
636
+ /** Every OTHER app in the workspace; the scanning app is excluded. */
637
+ siblings: WorkspaceApp[];
638
+ }
639
+ interface ResolveWorkspaceOptions {
640
+ configDir: string;
641
+ config: UidexConfig;
642
+ /** Overrides discovery; used by tests to avoid touching the real filesystem. */
643
+ configs?: DiscoveredConfig[];
644
+ }
645
+ /**
646
+ * Walk up from the app looking for a workspace marker. Returns `undefined` when
647
+ * there is none — a standalone app then keeps today's closed-world behavior
648
+ * rather than silently treating its parent directory as a workspace.
649
+ */
650
+ declare function findWorkspaceRoot(configDir: string): string | undefined;
651
+ /**
652
+ * Resolve the workspace an app participates in.
653
+ *
654
+ * `config.workspace` selects the root: a path (relative to the config dir),
655
+ * `false` for an explicit closed world, or omitted for auto-detect via
656
+ * `findWorkspaceRoot`. Returns `undefined` when there is no workspace or no
657
+ * sibling in it — callers then behave exactly as a single-app scan does.
658
+ */
659
+ declare function resolveWorkspace(opts: ResolveWorkspaceOptions): Workspace | undefined;
660
+ /**
661
+ * The sibling app that already captured this declaration, if any — the exact
662
+ * answer that replaces the borrowed-entity guess. `absFile` is the declaring
663
+ * file's real path (a display path cannot be resolved back across a `prefix`).
664
+ *
665
+ * A capture recording no `kind` matches on id alone, mirroring how the
666
+ * state-coverage gate treats an unlabeled capture.
667
+ */
668
+ declare function capturedElsewhere(workspace: Workspace | undefined, absFile: string | undefined, kind: string, id: string): string | undefined;
669
+
466
670
  /**
467
671
  * State-capture completeness — the deterministic replacement for a bespoke
468
672
  * `check-states.mjs`. Cross-checks the states DECLARED on registry entities
@@ -476,8 +680,17 @@ declare function resolve(ctx: ResolveContext): ResolveOutput;
476
680
  * - `orphan-state-capture` — a run captured a state the entity never declared,
477
681
  * so the contact sheet shows a state the registry doesn't know about.
478
682
  *
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.
683
+ * Two things take an entity out of the gate, and both are acknowledgments:
684
+ * a SOURCE `acknowledge: { "*": }`, or the WORKSPACE observing that a sibling
685
+ * app which registers this same declaration captured it (`captured-elsewhere`).
686
+ *
687
+ * The workspace answer replaces a heuristic this module used to carry: a
688
+ * "borrowed" entity (one declared under a shared source `prefix`) that this app
689
+ * captured nothing for was ASSUMED to be a sibling's responsibility. That guess
690
+ * could not tell a genuinely shared component from an id that merely collided
691
+ * with a sibling's, and it silenced real gaps whenever an app captured nothing.
692
+ * The workspace checks the sibling's actual source set and actual manifest, so
693
+ * the answer is exact and the guess is gone.
481
694
  */
482
695
  /** One `entity/state` a capture run recorded. */
483
696
  interface CapturedState {
@@ -496,49 +709,21 @@ interface CapturedState {
496
709
  }
497
710
  interface CapturedStatesManifest {
498
711
  captured: CapturedState[];
712
+ /** ISO timestamp the reporter stamped; drives the staleness check. */
713
+ generatedAt?: string;
499
714
  }
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[]>;
715
+ interface StateCoverageOptions {
716
+ /** The workspace, when one is resolved; enables `captured-elsewhere`. */
717
+ workspace?: Workspace;
718
+ /**
719
+ * Maps an entity's `loc.file` (a DISPLAY path, which a `prefix` makes
720
+ * unresolvable on its own) back to its real path on disk. Supplied by
721
+ * `audit()` from the scanned-file list, so the workspace can test the file
722
+ * against a sibling's source roots.
723
+ */
724
+ resolveSourcePath?: (displayPath: string) => string | undefined;
521
725
  }
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;
726
+ declare function checkStateCoverage(registry: Registry, manifest: CapturedStatesManifest, opts?: StateCoverageOptions): Diagnostic[];
542
727
 
543
728
  interface AuditOptions {
544
729
  registry: Registry;
@@ -570,8 +755,12 @@ interface AuditOptions {
570
755
  locsOutputPath?: string;
571
756
  /** Captured-states manifest for the completeness gate; absent → gate skipped. */
572
757
  capturedStates?: CapturedStatesManifest;
573
- /** Page-coverage baseline (the shrink-only backlog); absent → empty baseline. */
574
- pageBaseline?: PageCoverageBaseline;
758
+ /** Coverage baseline (the shrink-only `acknowledge` backlog). */
759
+ coverageBaseline?: CoverageBaseline;
760
+ /** Migration diagnostic when the baseline on disk is the removed shape. */
761
+ baselineDiagnostic?: Diagnostic;
762
+ /** The resolved workspace; enables `captured-elsewhere`. */
763
+ workspace?: Workspace;
575
764
  }
576
765
  declare function audit(opts: AuditOptions): AuditSummary;
577
766
 
@@ -611,10 +800,26 @@ declare function emit(opts: EmitOptions): EmitResult;
611
800
  * Detect framework-aware routes:
612
801
  * - Next.js App Router: `**\/app/**\/page.tsx` -> `/segments`
613
802
  * - Next.js Pages Router: `**\/pages/**\/*.tsx` -> `/segments` (excludes _app, _document, api/)
614
- * - Vite + React Router / TanStack Router:
615
- * `**\/routes/**\/*.tsx` -> `/segments`
803
+ * - Vite + TanStack Router / React Router: `**\/routes/**\/*.tsx` -> `/segments`.
804
+ * Honors TanStack conventions: pathless layouts (`_layout`) and route
805
+ * groups (`(group)`) drop from the path, the folder route file
806
+ * (`route.tsx`) collapses like `index`, `-`-prefixed and `api/` files are
807
+ * excluded, and flat dot-notation filenames (`a.b.$c.tsx`) split into
808
+ * nested segments. Dynamic segments are normalized to the canonical
809
+ * `[param]` form used everywhere else in the pipeline: `$id` -> `[id]`,
810
+ * the splat `$` -> `[...splat]`, and `[.]`-escaped literals are unwrapped.
616
811
  */
617
- declare function detectRoutes(files: ScannedFile[]): DetectedRoute[];
812
+ interface DetectRoutesOptions {
813
+ /**
814
+ * Explicit routes root, as it appears in `displayPath` (relative to the
815
+ * config dir). When set, only files under this dir are routes and the
816
+ * TanStack / React Router `routes/`-style convention is applied — overriding
817
+ * the `app`/`pages`/`routes` directory-name heuristic. Use for a custom
818
+ * TanStack `routesDirectory`.
819
+ */
820
+ routesDir?: string;
821
+ }
822
+ declare function detectRoutes(files: ScannedFile[], options?: DetectRoutesOptions): DetectedRoute[];
618
823
  declare function pathToId(routePath: string): string;
619
824
 
620
825
  interface GitResolveOptions {
@@ -661,11 +866,22 @@ interface RunScanOptions {
661
866
  configs?: DiscoveredConfig[];
662
867
  /** Captured-states manifest for the completeness gate; overrides on-disk load. */
663
868
  capturedStates?: CapturedStatesManifest;
664
- /** Page-coverage baseline; overrides on-disk load. */
665
- pageBaseline?: PageCoverageBaseline;
869
+ /** Coverage baseline (the shrink-only backlog); overrides on-disk load. */
870
+ coverageBaseline?: CoverageBaseline;
871
+ /** Workspace override; skips auto-detection (used by tests). */
872
+ workspace?: Workspace;
666
873
  }
667
- /** Load the page-coverage baseline from disk, if present + well-formed. */
668
- declare function loadPageBaseline(configDir: string, config: UidexConfig): PageCoverageBaseline | undefined;
874
+ interface LoadedBaseline {
875
+ baseline?: CoverageBaseline;
876
+ /** Present when the file on disk is the pre-`acknowledge` shape. */
877
+ legacy?: Diagnostic;
878
+ }
879
+ /**
880
+ * Load the coverage baseline from disk. A file in the REMOVED shape does not
881
+ * silently read as "no baseline" — that would turn every acknowledged-backlog
882
+ * route into a hard error — it comes back as a migration diagnostic with a fix.
883
+ */
884
+ declare function loadCoverageBaseline(configDir: string, config: UidexConfig): LoadedBaseline;
669
885
  /** One generated artifact: its bytes plus where it lands (abs + repo-relative). */
670
886
  interface ScanArtifact {
671
887
  generated: string;
@@ -697,33 +913,113 @@ declare function runScan(opts?: RunScanOptions): ScanResult[];
697
913
  * actually moved. Returns `true` if any file was written.
698
914
  */
699
915
  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;
916
+ /** Absolute path of the coverage baseline for a scan result. */
917
+ declare function coverageBaselinePath(result: ScanResult): string;
918
+
919
+ /**
920
+ * Page-capture coverage — the coarse gate above the per-state gate. Every
921
+ * derived route must be accounted for: captured (a run drove it), or
922
+ * ACKNOWLEDGED at `"*"` scope — either `impossible` (a redirect, a static
923
+ * notice, an internal target — the old `capture: false`, which now has to say
924
+ * WHY) or `backlog` (tool-managed, shrink-only). A route in none of those is a
925
+ * *new* uncaptured page and fails hard — the ratchet that keeps debt growing.
926
+ *
927
+ * uidex derives the route set (route groups stripped, dynamic segments kept), so
928
+ * unlike a hand-rolled `src/app/**` walk there is no page list to enumerate or
929
+ * keep from rotting: a route exists in the registry or it doesn't. And captures
930
+ * observe the URL they ran against, so which route a capture covers is matched,
931
+ * never hand-declared.
932
+ */
933
+ /**
934
+ * Turn a Next-style route pattern into an anchored matcher.
935
+ * `/[scope]/products/[productId]/edit` → matches `/acme/products/p1/edit`
936
+ * `/[...slug]` (catch-all) → matches one or more segments
937
+ * Returns the literal-segment count as a specificity score, so the most literal
938
+ * matching pattern wins when several match (e.g. `/x/new` beats `/x/[id]`).
939
+ */
940
+ declare function compileRoute(pattern: string): {
941
+ test: (url: string) => boolean;
942
+ specificity: number;
943
+ };
944
+ /** The route pattern (from the given set) that most-specifically matches a URL. */
945
+ declare function matchUrlToRoute(url: string, routePaths: string[]): string | null;
946
+ declare function checkPageCoverage(registry: Registry, manifest: CapturedStatesManifest, baseline?: CoverageBaseline): Diagnostic[];
947
+ /**
948
+ * The page half of the shrink-only baseline for `--update-baseline`: every route
949
+ * with no capture and no SOURCE acknowledgment. Seeds on first run and prunes on
950
+ * later runs; growth only ever happens through this explicit command, visible in
951
+ * the file's diff.
952
+ */
953
+ declare function computePageBacklog(registry: Registry, manifest: CapturedStatesManifest): Record<string, AcknowledgeMap>;
954
+
955
+ declare function checkStateMatrix(registry: Registry, manifest: CapturedStatesManifest, baseline?: CoverageBaseline): Diagnostic[];
956
+ /**
957
+ * The matrix half of the shrink-only baseline for `--update-baseline`: every
958
+ * (captured route, core kind) that is neither captured nor already acknowledged
959
+ * in SOURCE. Source acknowledgments are excluded so the same gap is never
960
+ * recorded twice, in two files, saying two things.
961
+ */
962
+ declare function computeMatrixBacklog(registry: Registry, manifest: CapturedStatesManifest): Record<string, AcknowledgeMap>;
963
+
964
+ declare function checkComponentCoverage(registry: Registry, manifest: CapturedStatesManifest): Diagnostic[];
965
+
966
+ /**
967
+ * Migration diagnostics for one file's `export const uidex` blocks. Emitted as
968
+ * errors: the fields are gone, so the declaration means nothing until rewritten.
969
+ */
970
+ declare function migrateLegacyFields(ef: ExtractedFile): Diagnostic[];
971
+ /**
972
+ * Migration diagnostic for a pre-`acknowledge` coverage baseline. The whole file
973
+ * is replaced, which is safe because the baseline is a generated artifact: the
974
+ * same content `--update-baseline` would produce, just without needing a capture
975
+ * run to regenerate it.
976
+ */
977
+ declare function migrateLegacyBaseline(opts: {
978
+ path: string;
979
+ raw: string;
980
+ parsed: unknown;
981
+ displayPath: string;
982
+ }): Diagnostic | undefined;
702
983
 
703
984
  /**
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:
985
+ * Fold a PARTIAL capture manifest into the committed one.
708
986
  *
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).
987
+ * A filtered run (`--grep`) legitimately captures a subset, so the reporter's
988
+ * shrink guard diverts it to `<name>.partial.json` rather than overwriting a
989
+ * fuller manifest. Without a merge path the only ways forward are re-running the
990
+ * FULL suite or hand-editing JSON — both of which make scoped re-capture
991
+ * expensive enough that people skip it and let the manifest drift.
713
992
  *
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.
993
+ * Merge is by `(kind, entity, state)`: rows in the partial REPLACE their
994
+ * counterparts (that run is the newer evidence) and rows absent from it are
995
+ * preserved (it never claimed to cover them). So merging is purely additive to
996
+ * coverage — it can never drop a state, which is what makes it safe to run
997
+ * without re-reading the whole manifest.
716
998
  */
717
- /** route → core kinds acknowledged as not-yet-captured (the finer ratchet). */
718
- interface StateMatrixBaseline {
719
- missingKinds: Record<string, string[]>;
999
+ interface MergeResult {
1000
+ manifest: CapturedStatesManifest;
1001
+ /** `(kind, entity, state)` keys the partial replaced. */
1002
+ updated: string[];
1003
+ /** Keys the partial contributed that the committed manifest lacked. */
1004
+ added: string[];
1005
+ }
1006
+ declare function mergeStates(committed: CapturedStatesManifest, partial: CapturedStatesManifest): MergeResult;
1007
+
1008
+ declare class ConfigError extends Error {
1009
+ constructor(message: string);
720
1010
  }
721
- declare function checkStateMatrix(registry: Registry, manifest: CapturedStatesManifest, baseline?: StateMatrixBaseline): Diagnostic[];
722
1011
  /**
723
- * Compute the shrink-only MISSING_KINDS baseline for `--update-baseline`: every
724
- * (captured route, core kind) that is neither captured nor waived.
1012
+ * Every diagnostic code the scanner can emit the allow-list `audit.severity`
1013
+ * validates against, so a typo'd key fails loudly instead of silently leaving
1014
+ * the gate it was meant to govern at its default severity.
1015
+ *
1016
+ * Keep in sync when adding a diagnostic; `severity-codes.test.ts` asserts this
1017
+ * set matches the codes actually emitted by the source.
725
1018
  */
726
- declare function computeMissingKinds(registry: Registry, manifest: CapturedStatesManifest): Record<string, string[]>;
1019
+ declare const DIAGNOSTIC_CODES: readonly ["acceptance-uncovered", "acknowledged-elsewhere", "competing-uidex-export", "component-state-uncaptured", "core-kind", "duplicate-id", "dynamic-attr", "dynamic-flow-reference", "gen-missing", "gen-stale", "legacy-acknowledge-field", "legacy-coverage-baseline", "matrix-backlog", "missing-element-annotation", "missing-state-capture", "orphan-state-capture", "page-backlog", "page-captured", "page-no-capture", "parse-error", "placeholder-reason", "prefer-well-known-file", "rename", "scope-leak", "spread-attr", "stale-acknowledgment", "stale-page-baseline", "uidex-export-ambiguous-kind", "uidex-export-duplicate-field", "uidex-export-duplicate-state", "uidex-export-empty-name", "uidex-export-invalid-field", "uidex-export-invalid-literal", "uidex-export-missing-kind", "uidex-export-satisfies-mismatch", "uidex-export-unknown-field", "unknown-reference", "widget-id-mismatch", "widget-missing-acceptance", "widget-missing-dom"];
1020
+ declare function validateConfig(raw: unknown): UidexConfig;
1021
+ declare function parseConfig(json: string): UidexConfig;
1022
+ declare const DEFAULT_CONVENTIONS: Required<Pick<NonNullable<UidexConfig["conventions"]>, "primitives" | "features" | "pages" | "flows" | "regions">>;
727
1023
 
728
1024
  /**
729
1025
  * Applies the machine-generated fixes attached to diagnostics.
@@ -789,13 +1085,6 @@ interface CliOptions {
789
1085
  }
790
1086
  declare function run(opts: CliOptions): Promise<CliResult>;
791
1087
 
792
- declare class ConfigError extends Error {
793
- constructor(message: string);
794
- }
795
- declare function validateConfig(raw: unknown): UidexConfig;
796
- declare function parseConfig(json: string): UidexConfig;
797
- declare const DEFAULT_CONVENTIONS: Required<Pick<NonNullable<UidexConfig["conventions"]>, "primitives" | "features" | "pages" | "flows" | "regions">>;
798
-
799
1088
  /**
800
1089
  * Authoring-surface shape types for `export const uidex = { ... }` declarations.
801
1090
  *
@@ -822,8 +1111,24 @@ declare namespace Uidex {
822
1111
  acceptance?: readonly string[];
823
1112
  description?: string;
824
1113
  }
825
- /** Per-core-kind waivers: a kind this route cannot render → reason. */
826
- type Waivers = Partial<Record<CoreStateKind, string>>;
1114
+ /** Scope of an acknowledgment: one core kind, or the whole entity. */
1115
+ type AcknowledgeScope = CoreStateKind | "*";
1116
+ /** A permanent design statement — so it has to say why. */
1117
+ interface Impossible {
1118
+ type: "impossible";
1119
+ reason: string;
1120
+ }
1121
+ /** Debt: capturable, not captured yet. No reason required. */
1122
+ interface Backlog {
1123
+ type: "backlog";
1124
+ reason?: string;
1125
+ }
1126
+ type Acknowledgment = Impossible | Backlog;
1127
+ type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>;
1128
+ /** A component has no route, so only the `"*"` scope applies to one. */
1129
+ type ComponentAcknowledge = {
1130
+ "*"?: Acknowledgment;
1131
+ };
827
1132
  interface Page<PageIds extends string = string, FeatureIds extends string = string, WidgetIds extends string = string> {
828
1133
  page: PageIds | false;
829
1134
  name?: string;
@@ -831,8 +1136,7 @@ declare namespace Uidex {
831
1136
  widgets?: readonly WidgetIds[];
832
1137
  acceptance?: readonly string[];
833
1138
  states?: readonly (string | State)[];
834
- capture?: boolean;
835
- waivers?: Waivers;
1139
+ acknowledge?: Acknowledge;
836
1140
  description?: string;
837
1141
  }
838
1142
  interface Feature<FeatureIds extends string = string> {
@@ -841,6 +1145,7 @@ declare namespace Uidex {
841
1145
  features?: readonly FeatureIds[];
842
1146
  acceptance?: readonly string[];
843
1147
  states?: readonly (string | State)[];
1148
+ acknowledge?: ComponentAcknowledge;
844
1149
  description?: string;
845
1150
  }
846
1151
  interface Primitive<PrimitiveIds extends string = string> {
@@ -853,6 +1158,7 @@ declare namespace Uidex {
853
1158
  name?: string;
854
1159
  acceptance?: readonly string[];
855
1160
  states?: readonly (string | State)[];
1161
+ acknowledge?: ComponentAcknowledge;
856
1162
  description?: string;
857
1163
  }
858
1164
  interface Flow<FlowIds extends string = string> {
@@ -865,4 +1171,4 @@ declare namespace Uidex {
865
1171
  }
866
1172
  }
867
1173
 
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 };
1174
+ export { ACK_ALL, ACK_SCOPES, type AcknowledgeMap, type AcknowledgeOrigin, type AcknowledgeType, type Acknowledgment, 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, type CoverageBaseline, DEFAULT_CONVENTIONS, DIAGNOSTIC_CODES, type DetectedRoute, type Diagnostic, type DiagnosticFix, type DiagnosticSeverity, type DiscoveredConfig, type ExtractedFile, type FixEdit, type GitContext, type ImportFact, type LandmarkFact, type MergeResult, type MetadataExport, type MetadataExportKind, type ParsedAcknowledgment, type ParsedState, type RenameKind, type RenameOptions, type RenameResult, type ResolvedAcknowledgment, type RunScanOptions, type ScaffoldKind, type ScaffoldOptions, type ScaffoldResult, type ScaffoldSpecOptions, type ScanResult, type ScannedFile, type SourceConfig, type Span, type StateCoverageOptions, Uidex, type UidexConfig, type Workspace, type WorkspaceApp, applyFixes, audit, capturedElsewhere, checkComponentCoverage, checkPageCoverage, checkStateCoverage, checkStateMatrix, compileRoute, computeMatrixBacklog, computePageBacklog, coverageBaselinePath, detectRoutes, discover, emit, entityAcknowledge, extract, extractUidexExports, findWorkspaceRoot, globToRegExp, isLegacyBaseline, loadCoverageBaseline, matchUrlToRoute, mergeStates, migrateBaseline, migrateLegacyBaseline, migrateLegacyFields, parseConfig, parseCoverageBaseline, pathToId, renameEntity, resolve, resolveGitContext, resolveWorkspace, routeAcknowledge, run as runCli, runScan, scaffoldSpec, scaffoldWidgetSpec, serializeCoverageBaseline, validateConfig, walk, writeScanResult };