uidex 0.9.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 +1190 -335
  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 +1129 -244
  38. package/dist/scan/index.cjs.map +1 -1
  39. package/dist/scan/index.d.cts +379 -100
  40. package/dist/scan/index.d.ts +379 -100
  41. package/dist/scan/index.js +1109 -240
  42. package/dist/scan/index.js.map +1 -1
  43. package/package.json +1 -1
@@ -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;
@@ -178,6 +184,15 @@ interface AuditConfig {
178
184
  pageCoverage?: boolean;
179
185
  /** State-matrix (core-kind) gate (default on when a manifest is present). */
180
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">;
181
196
  }
182
197
  interface UidexConfig {
183
198
  $schema?: string;
@@ -194,11 +209,21 @@ interface UidexConfig {
194
209
  */
195
210
  statesManifest?: string;
196
211
  /**
197
- * Path (relative to the config dir) of the page-coverage baseline — the
198
- * tool-managed, shrink-only backlog written by `uidex scan --update-baseline`.
199
- * 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`.
200
215
  */
201
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;
202
227
  }
203
228
  interface DiscoveredConfig {
204
229
  configPath: string;
@@ -253,16 +278,32 @@ interface MetadataExport {
253
278
  * one adjacent comma) keyed by field name. Used by `--fix` codemods.
254
279
  */
255
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>;
256
287
  /** Spans of the string-literal items in `features`, parallel to `features`. */
257
288
  featureSpans?: Span[];
258
289
  /** Spans of the string-literal items in `widgets`, parallel to `widgets`. */
259
290
  widgetSpans?: Span[];
260
291
  /** Declared render states (page/feature/widget); see ParsedState. */
261
292
  states?: ParsedState[];
262
- /** `capture: false` opts the entity out of the page-coverage gate. */
263
- capture?: boolean;
264
- /** Per-core-kind waivers: kind → reason the route cannot render it. */
265
- 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;
266
307
  }
267
308
  /**
268
309
  * A parsed `states` entry from `export const uidex`. The authored form is a
@@ -405,6 +446,13 @@ declare const CONFIG_FILENAME = ".uidex.json";
405
446
  interface DiscoverOptions {
406
447
  cwd?: string;
407
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;
408
456
  }
409
457
  declare function discover(options?: DiscoverOptions): DiscoveredConfig[];
410
458
 
@@ -474,6 +522,151 @@ interface ResolveOutput {
474
522
  */
475
523
  declare function resolve(ctx: ResolveContext): ResolveOutput;
476
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
+
477
670
  /**
478
671
  * State-capture completeness — the deterministic replacement for a bespoke
479
672
  * `check-states.mjs`. Cross-checks the states DECLARED on registry entities
@@ -487,8 +680,17 @@ declare function resolve(ctx: ResolveContext): ResolveOutput;
487
680
  * - `orphan-state-capture` — a run captured a state the entity never declared,
488
681
  * so the contact sheet shows a state the registry doesn't know about.
489
682
  *
490
- * Pure: same (registry, manifest) same diagnostics. Only runs when a manifest
491
- * 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.
492
694
  */
493
695
  /** One `entity/state` a capture run recorded. */
494
696
  interface CapturedState {
@@ -507,49 +709,21 @@ interface CapturedState {
507
709
  }
508
710
  interface CapturedStatesManifest {
509
711
  captured: CapturedState[];
712
+ /** ISO timestamp the reporter stamped; drives the staleness check. */
713
+ generatedAt?: string;
510
714
  }
511
- declare function checkStateCoverage(registry: Registry, manifest: CapturedStatesManifest): Diagnostic[];
512
-
513
- /**
514
- * Page-capture coverage — the coarse gate above the per-state gate. Every
515
- * derived route must be accounted for: captured (a run drove it), opted out
516
- * (`export const uidex = { page, capture: false }`), or acknowledged backlog (a
517
- * tool-managed, shrink-only baseline). A route in none of those is a *new*
518
- * uncaptured page and fails hard — the ratchet that keeps debt from growing.
519
- *
520
- * uidex derives the route set (route groups stripped, dynamic segments kept), so
521
- * unlike a hand-rolled `src/app/**` walk there is no page list to enumerate or
522
- * keep from rotting: a route exists in the registry or it doesn't. And captures
523
- * observe the URL they ran against, so which route a capture covers is matched,
524
- * never hand-declared.
525
- */
526
- /** The committed, tool-managed backlog. Shrinks as pages/kinds get captured. */
527
- interface PageCoverageBaseline {
528
- /** Route patterns acknowledged as uncaptured (e.g. "/[scope]/collections"). */
529
- uncapturedPages: string[];
530
- /** route → core kinds acknowledged as not-yet-captured (the matrix backlog). */
531
- 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;
532
725
  }
533
- /**
534
- * Turn a Next-style route pattern into an anchored matcher.
535
- * `/[scope]/products/[productId]/edit` → matches `/acme/products/p1/edit`
536
- * `/[...slug]` (catch-all) → matches one or more segments
537
- * Returns the literal-segment count as a specificity score, so the most literal
538
- * matching pattern wins when several match (e.g. `/x/new` beats `/x/[id]`).
539
- */
540
- declare function compileRoute(pattern: string): {
541
- test: (url: string) => boolean;
542
- specificity: number;
543
- };
544
- /** The route pattern (from the given set) that most-specifically matches a URL. */
545
- declare function matchUrlToRoute(url: string, routePaths: string[]): string | null;
546
- declare function checkPageCoverage(registry: Registry, manifest: CapturedStatesManifest, baseline?: PageCoverageBaseline): Diagnostic[];
547
- /**
548
- * Compute the shrink-only baseline for `--update-baseline`: every route with no
549
- * capture and no opt-out. Seeds on first run and prunes on later runs; growth
550
- * only ever happens through this explicit command, visible in the file's diff.
551
- */
552
- declare function computePageBaseline(registry: Registry, manifest: CapturedStatesManifest): PageCoverageBaseline;
726
+ declare function checkStateCoverage(registry: Registry, manifest: CapturedStatesManifest, opts?: StateCoverageOptions): Diagnostic[];
553
727
 
554
728
  interface AuditOptions {
555
729
  registry: Registry;
@@ -581,8 +755,12 @@ interface AuditOptions {
581
755
  locsOutputPath?: string;
582
756
  /** Captured-states manifest for the completeness gate; absent → gate skipped. */
583
757
  capturedStates?: CapturedStatesManifest;
584
- /** Page-coverage baseline (the shrink-only backlog); absent → empty baseline. */
585
- 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;
586
764
  }
587
765
  declare function audit(opts: AuditOptions): AuditSummary;
588
766
 
@@ -688,11 +866,22 @@ interface RunScanOptions {
688
866
  configs?: DiscoveredConfig[];
689
867
  /** Captured-states manifest for the completeness gate; overrides on-disk load. */
690
868
  capturedStates?: CapturedStatesManifest;
691
- /** Page-coverage baseline; overrides on-disk load. */
692
- 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;
693
873
  }
694
- /** Load the page-coverage baseline from disk, if present + well-formed. */
695
- 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;
696
885
  /** One generated artifact: its bytes plus where it lands (abs + repo-relative). */
697
886
  interface ScanArtifact {
698
887
  generated: string;
@@ -724,33 +913,113 @@ declare function runScan(opts?: RunScanOptions): ScanResult[];
724
913
  * actually moved. Returns `true` if any file was written.
725
914
  */
726
915
  declare function writeScanResult(result: ScanResult): boolean;
727
- /** Absolute path of the page-coverage baseline for a scan result. */
728
- declare function pageBaselinePath(result: ScanResult): string;
916
+ /** Absolute path of the coverage baseline for a scan result. */
917
+ declare function coverageBaselinePath(result: ScanResult): string;
729
918
 
730
919
  /**
731
- * State-matrix coverage — the second completeness axis. A capture that only ever
732
- * shoots the happy path leaves most of the harness's value on the table, so
733
- * every *captured* route must render each of the four core kinds
734
- * (loading / empty / populated / error), or account for the gap:
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.
735
926
  *
736
- * - a **waiver** (`export const uidex = { page, waivers: { empty: "…" } }`)
737
- * the route CANNOT render that kind (a create form has no empty state);
738
- * - the **MISSING_KINDS baseline** the route CAN render it and nobody has
739
- * captured it yet (a per-(route, kind) shrink-only backlog).
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;
983
+
984
+ /**
985
+ * Fold a PARTIAL capture manifest into the committed one.
740
986
  *
741
- * Anything else is a `core-kind` error. Only *captured* routes are in scope: a
742
- * route with no capture at all is a page-coverage concern, not a matrix hole.
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.
992
+ *
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.
743
998
  */
744
- /** route → core kinds acknowledged as not-yet-captured (the finer ratchet). */
745
- interface StateMatrixBaseline {
746
- 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);
747
1010
  }
748
- declare function checkStateMatrix(registry: Registry, manifest: CapturedStatesManifest, baseline?: StateMatrixBaseline): Diagnostic[];
749
1011
  /**
750
- * Compute the shrink-only MISSING_KINDS baseline for `--update-baseline`: every
751
- * (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.
752
1018
  */
753
- 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">>;
754
1023
 
755
1024
  /**
756
1025
  * Applies the machine-generated fixes attached to diagnostics.
@@ -816,13 +1085,6 @@ interface CliOptions {
816
1085
  }
817
1086
  declare function run(opts: CliOptions): Promise<CliResult>;
818
1087
 
819
- declare class ConfigError extends Error {
820
- constructor(message: string);
821
- }
822
- declare function validateConfig(raw: unknown): UidexConfig;
823
- declare function parseConfig(json: string): UidexConfig;
824
- declare const DEFAULT_CONVENTIONS: Required<Pick<NonNullable<UidexConfig["conventions"]>, "primitives" | "features" | "pages" | "flows" | "regions">>;
825
-
826
1088
  /**
827
1089
  * Authoring-surface shape types for `export const uidex = { ... }` declarations.
828
1090
  *
@@ -849,8 +1111,24 @@ declare namespace Uidex {
849
1111
  acceptance?: readonly string[];
850
1112
  description?: string;
851
1113
  }
852
- /** Per-core-kind waivers: a kind this route cannot render → reason. */
853
- 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
+ };
854
1132
  interface Page<PageIds extends string = string, FeatureIds extends string = string, WidgetIds extends string = string> {
855
1133
  page: PageIds | false;
856
1134
  name?: string;
@@ -858,8 +1136,7 @@ declare namespace Uidex {
858
1136
  widgets?: readonly WidgetIds[];
859
1137
  acceptance?: readonly string[];
860
1138
  states?: readonly (string | State)[];
861
- capture?: boolean;
862
- waivers?: Waivers;
1139
+ acknowledge?: Acknowledge;
863
1140
  description?: string;
864
1141
  }
865
1142
  interface Feature<FeatureIds extends string = string> {
@@ -868,6 +1145,7 @@ declare namespace Uidex {
868
1145
  features?: readonly FeatureIds[];
869
1146
  acceptance?: readonly string[];
870
1147
  states?: readonly (string | State)[];
1148
+ acknowledge?: ComponentAcknowledge;
871
1149
  description?: string;
872
1150
  }
873
1151
  interface Primitive<PrimitiveIds extends string = string> {
@@ -880,6 +1158,7 @@ declare namespace Uidex {
880
1158
  name?: string;
881
1159
  acceptance?: readonly string[];
882
1160
  states?: readonly (string | State)[];
1161
+ acknowledge?: ComponentAcknowledge;
883
1162
  description?: string;
884
1163
  }
885
1164
  interface Flow<FlowIds extends string = string> {
@@ -892,4 +1171,4 @@ declare namespace Uidex {
892
1171
  }
893
1172
  }
894
1173
 
895
- 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 };