uidex 0.7.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.
Files changed (51) hide show
  1. package/dist/cli/cli.cjs +1024 -1041
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +4 -448
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +41 -11
  6. package/dist/headless/index.d.ts +41 -11
  7. package/dist/headless/index.js +4 -450
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +147 -3252
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +43 -316
  12. package/dist/index.d.ts +43 -316
  13. package/dist/index.js +133 -3254
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +175 -0
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.d.cts +2 -0
  18. package/dist/playwright/index.d.ts +2 -0
  19. package/dist/playwright/index.js +167 -0
  20. package/dist/playwright/index.js.map +1 -1
  21. package/dist/playwright/states-reporter.cjs +123 -0
  22. package/dist/playwright/states-reporter.cjs.map +1 -0
  23. package/dist/playwright/states-reporter.d.cts +46 -0
  24. package/dist/playwright/states-reporter.d.ts +46 -0
  25. package/dist/playwright/states-reporter.js +88 -0
  26. package/dist/playwright/states-reporter.js.map +1 -0
  27. package/dist/playwright/states.cjs +118 -0
  28. package/dist/playwright/states.cjs.map +1 -0
  29. package/dist/playwright/states.d.cts +120 -0
  30. package/dist/playwright/states.d.ts +120 -0
  31. package/dist/playwright/states.js +88 -0
  32. package/dist/playwright/states.js.map +1 -0
  33. package/dist/react/index.cjs +163 -3255
  34. package/dist/react/index.cjs.map +1 -1
  35. package/dist/react/index.d.cts +62 -275
  36. package/dist/react/index.d.ts +62 -275
  37. package/dist/react/index.js +151 -3268
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/scan/index.cjs +1194 -322
  40. package/dist/scan/index.cjs.map +1 -1
  41. package/dist/scan/index.d.cts +274 -8
  42. package/dist/scan/index.d.ts +274 -8
  43. package/dist/scan/index.js +1185 -322
  44. package/dist/scan/index.js.map +1 -1
  45. package/package.json +27 -31
  46. package/dist/cloud/index.cjs +0 -682
  47. package/dist/cloud/index.cjs.map +0 -1
  48. package/dist/cloud/index.d.cts +0 -270
  49. package/dist/cloud/index.d.ts +0 -270
  50. package/dist/cloud/index.js +0 -645
  51. package/dist/cloud/index.js.map +0 -1
@@ -28,7 +28,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
28
28
  "output",
29
29
  "flows",
30
30
  "audit",
31
- "conventions"
31
+ "conventions",
32
+ "statesManifest",
33
+ "coverageBaseline"
32
34
  ]);
33
35
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
34
36
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -38,7 +40,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
38
40
  "flows",
39
41
  "regions"
40
42
  ]);
41
- var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acceptance"]);
43
+ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
44
+ "scopeLeak",
45
+ "coverage",
46
+ "acceptance",
47
+ "states",
48
+ "pageCoverage",
49
+ "stateMatrix"
50
+ ]);
42
51
  function fail(msg) {
43
52
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
44
53
  }
@@ -97,6 +106,12 @@ function validateConfig(raw) {
97
106
  }
98
107
  if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
99
108
  if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
109
+ if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
110
+ fail(`"statesManifest" must be a string`);
111
+ }
112
+ if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
113
+ fail(`"coverageBaseline" must be a string`);
114
+ }
100
115
  if (raw.audit !== void 0) {
101
116
  assertObject(raw.audit, "audit");
102
117
  for (const key of Object.keys(raw.audit)) {
@@ -136,7 +151,9 @@ function validateConfig(raw) {
136
151
  output: raw.output,
137
152
  flows: raw.flows,
138
153
  audit: raw.audit,
139
- conventions: raw.conventions
154
+ conventions: raw.conventions,
155
+ statesManifest: raw.statesManifest,
156
+ coverageBaseline: raw.coverageBaseline
140
157
  };
141
158
  }
142
159
  function parseConfig(json) {
@@ -225,7 +242,12 @@ var BASE_EXCLUDES = [
225
242
  "**/dist/**",
226
243
  "**/build/**",
227
244
  "**/.next/**",
228
- "**/*.gen.ts"
245
+ // Exclude the scanner's own generated siblings so a scan never re-ingests its
246
+ // output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
247
+ // a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
248
+ "**/*.gen.ts",
249
+ "**/*.gen.data.ts",
250
+ "**/*.gen.locs.ts"
229
251
  ];
230
252
  var TEST_EXCLUDES = [
231
253
  "**/*.test.ts",
@@ -425,6 +447,201 @@ function unwrapTsExpression(expr) {
425
447
  }
426
448
  }
427
449
 
450
+ // src/shared/entities/types.ts
451
+ var ENTITY_KINDS = [
452
+ "route",
453
+ "page",
454
+ "feature",
455
+ "widget",
456
+ "region",
457
+ "element",
458
+ "primitive",
459
+ "flow"
460
+ ];
461
+ var CORE_STATE_KINDS = [
462
+ "loading",
463
+ "empty",
464
+ "populated",
465
+ "error"
466
+ ];
467
+ function isMetaKind(kind) {
468
+ return kind !== "route" && kind !== "flow";
469
+ }
470
+ function isMetaEntity(entity) {
471
+ return isMetaKind(entity.kind);
472
+ }
473
+ function entityKey(entity) {
474
+ return entity.kind === "route" ? entity.path : entity.id;
475
+ }
476
+ var UnknownEntityKindError = class extends Error {
477
+ kind;
478
+ constructor(kind) {
479
+ super(`Unknown entity kind: ${kind}`);
480
+ this.name = "UnknownEntityKindError";
481
+ this.kind = kind;
482
+ }
483
+ };
484
+ var KIND_SET = new Set(ENTITY_KINDS);
485
+ function assertEntityKind(kind) {
486
+ if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
487
+ }
488
+
489
+ // src/shared/entities/registry.ts
490
+ function emptyStore() {
491
+ return {
492
+ route: /* @__PURE__ */ new Map(),
493
+ page: /* @__PURE__ */ new Map(),
494
+ feature: /* @__PURE__ */ new Map(),
495
+ widget: /* @__PURE__ */ new Map(),
496
+ region: /* @__PURE__ */ new Map(),
497
+ element: /* @__PURE__ */ new Map(),
498
+ primitive: /* @__PURE__ */ new Map(),
499
+ flow: /* @__PURE__ */ new Map()
500
+ };
501
+ }
502
+ function computeFlowIds(flows, targetId) {
503
+ const ids = [];
504
+ for (const flow of flows) {
505
+ if (flow.touches.includes(targetId)) ids.push(flow.id);
506
+ }
507
+ return ids;
508
+ }
509
+ function freezeEntity(entity, flows) {
510
+ if (!isMetaKind(entity.kind)) return entity;
511
+ const withMeta = entity;
512
+ if (withMeta.meta === void 0) return entity;
513
+ const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
514
+ const mergedMeta = { ...withMeta.meta, flows: computedFlows };
515
+ return { ...entity, meta: Object.freeze(mergedMeta) };
516
+ }
517
+ function createRegistry() {
518
+ const store = emptyStore();
519
+ let flowsCache = null;
520
+ const patternCache = /* @__PURE__ */ new Map();
521
+ const getFlows = () => {
522
+ if (flowsCache === null) flowsCache = Array.from(store.flow.values());
523
+ return flowsCache;
524
+ };
525
+ const add = (entity) => {
526
+ assertEntityKind(entity.kind);
527
+ const key = entityKey(entity);
528
+ store[entity.kind].set(key, entity);
529
+ flowsCache = null;
530
+ patternCache.delete(entity.kind);
531
+ };
532
+ const get = (kind, id) => {
533
+ assertEntityKind(kind);
534
+ const raw = store[kind].get(id);
535
+ if (raw === void 0) return void 0;
536
+ return freezeEntity(raw, getFlows());
537
+ };
538
+ const getPatternsForKind = (kind) => {
539
+ const cached = patternCache.get(kind);
540
+ if (cached !== void 0) return cached;
541
+ const patterns = [];
542
+ for (const [key, entity] of store[kind]) {
543
+ if (key.includes("*")) {
544
+ const segments = key.split("*");
545
+ patterns.push({
546
+ segments,
547
+ staticLength: segments.reduce((n, s) => n + s.length, 0),
548
+ entity
549
+ });
550
+ }
551
+ }
552
+ patternCache.set(
553
+ kind,
554
+ patterns
555
+ );
556
+ return patterns;
557
+ };
558
+ const matchesSegments = (segments, id) => {
559
+ const first = segments[0];
560
+ const last = segments[segments.length - 1];
561
+ if (!id.startsWith(first)) return false;
562
+ let pos = first.length;
563
+ for (let i = 1; i < segments.length - 1; i++) {
564
+ const idx = id.indexOf(segments[i], pos);
565
+ if (idx === -1) return false;
566
+ pos = idx + segments[i].length;
567
+ }
568
+ return id.endsWith(last) && id.length - last.length >= pos;
569
+ };
570
+ const matchPattern = (kind, id) => {
571
+ assertEntityKind(kind);
572
+ const patterns = getPatternsForKind(kind);
573
+ if (patterns.length === 0) return void 0;
574
+ let best;
575
+ for (const entry of patterns) {
576
+ if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
577
+ best = entry;
578
+ }
579
+ }
580
+ if (best === void 0) return void 0;
581
+ return freezeEntity(best.entity, getFlows());
582
+ };
583
+ const list = (kind) => {
584
+ assertEntityKind(kind);
585
+ const flows = getFlows();
586
+ return Array.from(
587
+ store[kind].values(),
588
+ (e) => freezeEntity(e, flows)
589
+ );
590
+ };
591
+ const allEntities = function* () {
592
+ for (const kind of Object.keys(store)) {
593
+ for (const entity of store[kind].values()) {
594
+ yield entity;
595
+ }
596
+ }
597
+ };
598
+ const query = (predicate) => {
599
+ const flows = getFlows();
600
+ const result = [];
601
+ for (const entity of allEntities()) {
602
+ if (predicate(entity)) result.push(freezeEntity(entity, flows));
603
+ }
604
+ return result;
605
+ };
606
+ const byScope = (scope) => query(
607
+ (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
608
+ );
609
+ const touchedBy = (flowId) => {
610
+ const flow = store.flow.get(flowId);
611
+ if (flow === void 0) return [];
612
+ const ids = new Set(flow.touches);
613
+ return query((entity) => {
614
+ if (!isMetaEntity(entity)) return false;
615
+ return ids.has(entity.id);
616
+ });
617
+ };
618
+ const reports = /* @__PURE__ */ new Map();
619
+ const reportsCbs = /* @__PURE__ */ new Set();
620
+ const setReports = (kind, id, records) => {
621
+ reports.set(`${kind}:${id}`, records);
622
+ for (const cb of reportsCbs) cb();
623
+ };
624
+ const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
625
+ const listReportKeys = () => Array.from(reports.keys());
626
+ const onReportsChange = (cb) => {
627
+ reportsCbs.add(cb);
628
+ return () => reportsCbs.delete(cb);
629
+ };
630
+ return {
631
+ add,
632
+ get,
633
+ matchPattern,
634
+ list,
635
+ query,
636
+ byScope,
637
+ touchedBy,
638
+ setReports,
639
+ getReports,
640
+ listReportKeys,
641
+ onReportsChange
642
+ };
643
+ }
644
+
428
645
  // src/scanner/scan/extract-uidex-export.ts
429
646
  var KIND_DISCRIMINATORS = [
430
647
  "page",
@@ -442,20 +659,35 @@ var ALLOWED_FIELDS = {
442
659
  "features",
443
660
  "widgets",
444
661
  "acceptance",
662
+ "states",
663
+ "capture",
664
+ "waivers",
445
665
  "description"
446
666
  ]),
667
+ // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
668
+ // they are page-only (the gates read them off the page). A feature/widget that
669
+ // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
447
670
  feature: /* @__PURE__ */ new Set([
448
671
  "feature",
449
672
  "name",
450
673
  "features",
451
674
  "acceptance",
675
+ "states",
452
676
  "description"
453
677
  ]),
454
678
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
455
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
679
+ widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
456
680
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
457
681
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
458
682
  };
683
+ var STATE_FIELDS = /* @__PURE__ */ new Set([
684
+ "name",
685
+ "kind",
686
+ "acceptance",
687
+ "description"
688
+ ]);
689
+ var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
690
+ var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
459
691
  var FALSEABLE = /* @__PURE__ */ new Set([
460
692
  "page",
461
693
  "feature",
@@ -845,6 +1077,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
845
1077
  }
846
1078
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
847
1079
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1080
+ const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
1081
+ const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
1082
+ const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
848
1083
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
849
1084
  const metadata = {
850
1085
  source: "ts-export",
@@ -867,6 +1102,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
867
1102
  metadata.widgets = widgetsField.values;
868
1103
  metadata.widgetSpans = widgetsField.spans;
869
1104
  }
1105
+ if (states?.length) metadata.states = states;
1106
+ if (capture !== void 0) metadata.capture = capture;
1107
+ if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
870
1108
  if (notFlow) metadata.notFlow = true;
871
1109
  if (typeof id === "string" && idValue.kind === "string") {
872
1110
  metadata.idSpan = idValue.span;
@@ -903,6 +1141,48 @@ function readIdField(value, kind, fieldName) {
903
1141
  value.pos
904
1142
  );
905
1143
  }
1144
+ function readWaiversField(byKey) {
1145
+ const entry = byKey.get("waivers");
1146
+ if (!entry) return void 0;
1147
+ if (entry.value.kind !== "object") {
1148
+ throw new ExtractError(
1149
+ "uidex-export-invalid-field",
1150
+ "`waivers` must be an object mapping a core kind to a reason.",
1151
+ entry.value.pos
1152
+ );
1153
+ }
1154
+ const waivers = {};
1155
+ for (const e of entry.value.entries) {
1156
+ if (!CORE_STATE_KINDS2.has(e.key)) {
1157
+ throw new ExtractError(
1158
+ "uidex-export-invalid-field",
1159
+ `waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
1160
+ e.keyPos
1161
+ );
1162
+ }
1163
+ if (e.value.kind !== "string" || e.value.value.length === 0) {
1164
+ throw new ExtractError(
1165
+ "uidex-export-invalid-field",
1166
+ `waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
1167
+ e.value.pos
1168
+ );
1169
+ }
1170
+ waivers[e.key] = e.value.value;
1171
+ }
1172
+ return waivers;
1173
+ }
1174
+ function readBooleanField(byKey, name) {
1175
+ const entry = byKey.get(name);
1176
+ if (!entry) return void 0;
1177
+ if (entry.value.kind !== "boolean") {
1178
+ throw new ExtractError(
1179
+ "uidex-export-invalid-field",
1180
+ `\`${name}\` must be a boolean.`,
1181
+ entry.value.pos
1182
+ );
1183
+ }
1184
+ return entry.value.value;
1185
+ }
906
1186
  function readStringField(byKey, name) {
907
1187
  const entry = byKey.get(name);
908
1188
  if (!entry) return void 0;
@@ -940,6 +1220,86 @@ function readStringArrayField(byKey, name) {
940
1220
  }
941
1221
  return { values, spans };
942
1222
  }
1223
+ function readStatesField(byKey) {
1224
+ const entry = byKey.get("states");
1225
+ if (!entry) return void 0;
1226
+ if (entry.value.kind !== "array") {
1227
+ throw new ExtractError(
1228
+ "uidex-export-invalid-field",
1229
+ "`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
1230
+ entry.value.pos
1231
+ );
1232
+ }
1233
+ const states = [];
1234
+ const seen = /* @__PURE__ */ new Set();
1235
+ for (const item of entry.value.items) {
1236
+ let state;
1237
+ if (item.kind === "string") {
1238
+ state = { name: item.value, nameSpan: item.span };
1239
+ } else if (item.kind === "object") {
1240
+ const fieldByKey = /* @__PURE__ */ new Map();
1241
+ for (const e of item.entries) fieldByKey.set(e.key, e);
1242
+ for (const e of item.entries) {
1243
+ if (!STATE_FIELDS.has(e.key)) {
1244
+ throw new ExtractError(
1245
+ "uidex-export-invalid-field",
1246
+ `Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
1247
+ STATE_FIELDS
1248
+ ).sort().join(", ")}.`,
1249
+ e.value.pos
1250
+ );
1251
+ }
1252
+ }
1253
+ const nameEntry = fieldByKey.get("name");
1254
+ if (!nameEntry || nameEntry.value.kind !== "string") {
1255
+ throw new ExtractError(
1256
+ "uidex-export-invalid-field",
1257
+ "Each object `states` entry must have a string `name`.",
1258
+ item.pos
1259
+ );
1260
+ }
1261
+ state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
1262
+ const kind = readStringField(fieldByKey, "kind");
1263
+ if (kind !== void 0) {
1264
+ if (!STATE_KINDS.has(kind)) {
1265
+ throw new ExtractError(
1266
+ "uidex-export-invalid-field",
1267
+ `state \`kind\` must be one of ${[...STATE_KINDS].join(", ")} (got "${kind}").`,
1268
+ fieldByKey.get("kind").value.pos
1269
+ );
1270
+ }
1271
+ state.kind = kind;
1272
+ }
1273
+ const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
1274
+ if (acceptance) state.acceptance = acceptance;
1275
+ const description = readStringField(fieldByKey, "description");
1276
+ if (description) state.description = description;
1277
+ } else {
1278
+ throw new ExtractError(
1279
+ "uidex-export-invalid-field",
1280
+ "`states` items must be strings or `{ name, ... }` objects.",
1281
+ item.pos
1282
+ );
1283
+ }
1284
+ if (state.name.length === 0) {
1285
+ throw new ExtractError(
1286
+ "uidex-export-invalid-field",
1287
+ "A `states` entry has an empty `name`.",
1288
+ item.pos
1289
+ );
1290
+ }
1291
+ if (seen.has(state.name)) {
1292
+ throw new ExtractError(
1293
+ "uidex-export-duplicate-state",
1294
+ `Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
1295
+ item.pos
1296
+ );
1297
+ }
1298
+ seen.add(state.name);
1299
+ states.push(state);
1300
+ }
1301
+ return states;
1302
+ }
943
1303
  function posAt(content, offset, p2) {
944
1304
  const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
945
1305
  return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
@@ -1440,211 +1800,22 @@ function collectImportFacts(parsed) {
1440
1800
  } else {
1441
1801
  continue;
1442
1802
  }
1443
- if (!source || source.type !== "Literal") continue;
1444
- if (typeof source.value !== "string") continue;
1445
- out2.push({
1446
- specifier: source.value,
1447
- line: parsed.lineAt(stmt.start),
1448
- span: { start: stmt.start, end: stmt.end },
1449
- isTypeOnly,
1450
- names
1451
- });
1452
- }
1453
- return out2;
1454
- }
1455
-
1456
- // src/scanner/scan/resolve.ts
1457
- import * as path4 from "path";
1458
-
1459
- // src/shared/entities/types.ts
1460
- var ENTITY_KINDS = [
1461
- "route",
1462
- "page",
1463
- "feature",
1464
- "widget",
1465
- "region",
1466
- "element",
1467
- "primitive",
1468
- "flow"
1469
- ];
1470
- function isMetaKind(kind) {
1471
- return kind !== "route" && kind !== "flow";
1472
- }
1473
- function isMetaEntity(entity) {
1474
- return isMetaKind(entity.kind);
1475
- }
1476
- function entityKey(entity) {
1477
- return entity.kind === "route" ? entity.path : entity.id;
1478
- }
1479
- var UnknownEntityKindError = class extends Error {
1480
- kind;
1481
- constructor(kind) {
1482
- super(`Unknown entity kind: ${kind}`);
1483
- this.name = "UnknownEntityKindError";
1484
- this.kind = kind;
1485
- }
1486
- };
1487
- var KIND_SET = new Set(ENTITY_KINDS);
1488
- function assertEntityKind(kind) {
1489
- if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
1490
- }
1491
-
1492
- // src/shared/entities/registry.ts
1493
- function emptyStore() {
1494
- return {
1495
- route: /* @__PURE__ */ new Map(),
1496
- page: /* @__PURE__ */ new Map(),
1497
- feature: /* @__PURE__ */ new Map(),
1498
- widget: /* @__PURE__ */ new Map(),
1499
- region: /* @__PURE__ */ new Map(),
1500
- element: /* @__PURE__ */ new Map(),
1501
- primitive: /* @__PURE__ */ new Map(),
1502
- flow: /* @__PURE__ */ new Map()
1503
- };
1504
- }
1505
- function computeFlowIds(flows, targetId) {
1506
- const ids = [];
1507
- for (const flow of flows) {
1508
- if (flow.touches.includes(targetId)) ids.push(flow.id);
1509
- }
1510
- return ids;
1511
- }
1512
- function freezeEntity(entity, flows) {
1513
- if (!isMetaKind(entity.kind)) return entity;
1514
- const withMeta = entity;
1515
- if (withMeta.meta === void 0) return entity;
1516
- const computedFlows = Object.freeze(computeFlowIds(flows, withMeta.id));
1517
- const mergedMeta = { ...withMeta.meta, flows: computedFlows };
1518
- return { ...entity, meta: Object.freeze(mergedMeta) };
1519
- }
1520
- function createRegistry() {
1521
- const store = emptyStore();
1522
- let flowsCache = null;
1523
- const patternCache = /* @__PURE__ */ new Map();
1524
- const getFlows = () => {
1525
- if (flowsCache === null) flowsCache = Array.from(store.flow.values());
1526
- return flowsCache;
1527
- };
1528
- const add = (entity) => {
1529
- assertEntityKind(entity.kind);
1530
- const key = entityKey(entity);
1531
- store[entity.kind].set(key, entity);
1532
- flowsCache = null;
1533
- patternCache.delete(entity.kind);
1534
- };
1535
- const get = (kind, id) => {
1536
- assertEntityKind(kind);
1537
- const raw = store[kind].get(id);
1538
- if (raw === void 0) return void 0;
1539
- return freezeEntity(raw, getFlows());
1540
- };
1541
- const getPatternsForKind = (kind) => {
1542
- const cached = patternCache.get(kind);
1543
- if (cached !== void 0) return cached;
1544
- const patterns = [];
1545
- for (const [key, entity] of store[kind]) {
1546
- if (key.includes("*")) {
1547
- const segments = key.split("*");
1548
- patterns.push({
1549
- segments,
1550
- staticLength: segments.reduce((n, s) => n + s.length, 0),
1551
- entity
1552
- });
1553
- }
1554
- }
1555
- patternCache.set(
1556
- kind,
1557
- patterns
1558
- );
1559
- return patterns;
1560
- };
1561
- const matchesSegments = (segments, id) => {
1562
- const first = segments[0];
1563
- const last = segments[segments.length - 1];
1564
- if (!id.startsWith(first)) return false;
1565
- let pos = first.length;
1566
- for (let i = 1; i < segments.length - 1; i++) {
1567
- const idx = id.indexOf(segments[i], pos);
1568
- if (idx === -1) return false;
1569
- pos = idx + segments[i].length;
1570
- }
1571
- return id.endsWith(last) && id.length - last.length >= pos;
1572
- };
1573
- const matchPattern = (kind, id) => {
1574
- assertEntityKind(kind);
1575
- const patterns = getPatternsForKind(kind);
1576
- if (patterns.length === 0) return void 0;
1577
- let best;
1578
- for (const entry of patterns) {
1579
- if (matchesSegments(entry.segments, id) && (best === void 0 || entry.staticLength > best.staticLength)) {
1580
- best = entry;
1581
- }
1582
- }
1583
- if (best === void 0) return void 0;
1584
- return freezeEntity(best.entity, getFlows());
1585
- };
1586
- const list = (kind) => {
1587
- assertEntityKind(kind);
1588
- const flows = getFlows();
1589
- return Array.from(
1590
- store[kind].values(),
1591
- (e) => freezeEntity(e, flows)
1592
- );
1593
- };
1594
- const allEntities = function* () {
1595
- for (const kind of Object.keys(store)) {
1596
- for (const entity of store[kind].values()) {
1597
- yield entity;
1598
- }
1599
- }
1600
- };
1601
- const query = (predicate) => {
1602
- const flows = getFlows();
1603
- const result = [];
1604
- for (const entity of allEntities()) {
1605
- if (predicate(entity)) result.push(freezeEntity(entity, flows));
1606
- }
1607
- return result;
1608
- };
1609
- const byScope = (scope) => query(
1610
- (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
1611
- );
1612
- const touchedBy = (flowId) => {
1613
- const flow = store.flow.get(flowId);
1614
- if (flow === void 0) return [];
1615
- const ids = new Set(flow.touches);
1616
- return query((entity) => {
1617
- if (!isMetaEntity(entity)) return false;
1618
- return ids.has(entity.id);
1803
+ if (!source || source.type !== "Literal") continue;
1804
+ if (typeof source.value !== "string") continue;
1805
+ out2.push({
1806
+ specifier: source.value,
1807
+ line: parsed.lineAt(stmt.start),
1808
+ span: { start: stmt.start, end: stmt.end },
1809
+ isTypeOnly,
1810
+ names
1619
1811
  });
1620
- };
1621
- const reports = /* @__PURE__ */ new Map();
1622
- const reportsCbs = /* @__PURE__ */ new Set();
1623
- const setReports = (kind, id, records) => {
1624
- reports.set(`${kind}:${id}`, records);
1625
- for (const cb of reportsCbs) cb();
1626
- };
1627
- const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
1628
- const listReportKeys = () => Array.from(reports.keys());
1629
- const onReportsChange = (cb) => {
1630
- reportsCbs.add(cb);
1631
- return () => reportsCbs.delete(cb);
1632
- };
1633
- return {
1634
- add,
1635
- get,
1636
- matchPattern,
1637
- list,
1638
- query,
1639
- byScope,
1640
- touchedBy,
1641
- setReports,
1642
- getReports,
1643
- listReportKeys,
1644
- onReportsChange
1645
- };
1812
+ }
1813
+ return out2;
1646
1814
  }
1647
1815
 
1816
+ // src/scanner/scan/resolve.ts
1817
+ import * as path4 from "path";
1818
+
1648
1819
  // src/scanner/scan/routes.ts
1649
1820
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
1650
1821
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
@@ -1744,6 +1915,18 @@ function buildMetaFromExport(exp) {
1744
1915
  if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
1745
1916
  if (exp.features?.length) meta.features = exp.features;
1746
1917
  if (exp.widgets?.length) meta.widgets = exp.widgets;
1918
+ if (exp.states?.length) {
1919
+ meta.states = exp.states.map((s) => ({
1920
+ name: s.name,
1921
+ ...s.kind ? { kind: s.kind } : {},
1922
+ ...s.acceptance?.length ? { acceptance: s.acceptance } : {},
1923
+ ...s.description ? { description: s.description } : {}
1924
+ }));
1925
+ }
1926
+ if (exp.capture === false) meta.capture = false;
1927
+ if (exp.waivers && Object.keys(exp.waivers).length > 0) {
1928
+ meta.waivers = exp.waivers;
1929
+ }
1747
1930
  return Object.keys(meta).length > 0 ? meta : void 0;
1748
1931
  }
1749
1932
  function resolve2(ctx) {
@@ -2163,6 +2346,297 @@ function dedupe(arr) {
2163
2346
 
2164
2347
  // src/scanner/scan/audit.ts
2165
2348
  import * as path5 from "path";
2349
+
2350
+ // src/scanner/scan/page-coverage.ts
2351
+ function compileRoute(pattern) {
2352
+ const segs = pattern.split("/").filter(Boolean);
2353
+ let specificity = 0;
2354
+ const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
2355
+ const effective = optionalTail ? segs.slice(0, -1) : segs;
2356
+ const parts = effective.map((seg) => {
2357
+ const catchAll = /^\[\.{3}.+\]$/.test(seg);
2358
+ const dynamic = /^\[.+\]$/.test(seg);
2359
+ if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
2360
+ if (dynamic) return "[^/]+";
2361
+ specificity++;
2362
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2363
+ });
2364
+ let body = parts.length === 0 ? "" : "/" + parts.join("/");
2365
+ if (optionalTail) body += "(?:/[^/]+)*";
2366
+ if (body === "") body = "/";
2367
+ const re = new RegExp(`^${body}/?$`);
2368
+ return { test: (url) => re.test(url), specificity };
2369
+ }
2370
+ function matchUrlToRoute(url, routePaths) {
2371
+ const path12 = stripQuery(url);
2372
+ let best = null;
2373
+ for (const rp of routePaths) {
2374
+ const { test, specificity } = compileRoute(rp);
2375
+ if (!test(path12)) continue;
2376
+ if (!best || specificity > best.specificity)
2377
+ best = { path: rp, specificity };
2378
+ }
2379
+ return best?.path ?? null;
2380
+ }
2381
+ function stripQuery(url) {
2382
+ const q = url.indexOf("?");
2383
+ const h = url.indexOf("#");
2384
+ let end = url.length;
2385
+ if (q !== -1) end = Math.min(end, q);
2386
+ if (h !== -1) end = Math.min(end, h);
2387
+ return url.slice(0, end);
2388
+ }
2389
+ function routeForCapture(c, routes, routePaths) {
2390
+ if (c.route && routePaths.includes(c.route)) return c.route;
2391
+ if (c.url) {
2392
+ const m = matchUrlToRoute(c.url, routePaths);
2393
+ if (m) return m;
2394
+ }
2395
+ if (c.kind === "page") {
2396
+ const r = routes.find((r2) => r2.page === c.entity);
2397
+ if (r) return r.path;
2398
+ }
2399
+ return null;
2400
+ }
2401
+ function checkPageCoverage(registry, manifest, baseline) {
2402
+ const routes = registry.list("route");
2403
+ if (routes.length === 0) return [];
2404
+ const routePaths = routes.map((r) => r.path);
2405
+ const captured = manifest.captured ?? [];
2406
+ const optedOut = /* @__PURE__ */ new Set();
2407
+ for (const r of routes) {
2408
+ const page = registry.get("page", r.page);
2409
+ if (page?.meta?.capture === false) optedOut.add(r.path);
2410
+ }
2411
+ const covered = /* @__PURE__ */ new Set();
2412
+ for (const c of captured) {
2413
+ const route = routeForCapture(c, routes, routePaths);
2414
+ if (route) covered.add(route);
2415
+ }
2416
+ const baselineSet = new Set(baseline?.uncapturedPages ?? []);
2417
+ const diagnostics = [];
2418
+ for (const r of routes) {
2419
+ if (covered.has(r.path) || optedOut.has(r.path)) continue;
2420
+ const loc = registry.get("page", r.page)?.loc;
2421
+ if (baselineSet.has(r.path)) {
2422
+ diagnostics.push({
2423
+ code: "page-backlog",
2424
+ severity: "info",
2425
+ message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
2426
+ file: loc?.file,
2427
+ line: loc?.line,
2428
+ hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`.`
2429
+ });
2430
+ continue;
2431
+ }
2432
+ diagnostics.push({
2433
+ code: "page-no-capture",
2434
+ severity: "error",
2435
+ message: `route "${r.path}" has no visual-states capture and is not accounted for`,
2436
+ file: loc?.file,
2437
+ line: loc?.line,
2438
+ entity: { kind: "route", id: r.path },
2439
+ hint: `Capture the route in a states spec, opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`, or grandfather it as acknowledged backlog with \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
2440
+ });
2441
+ }
2442
+ for (const b of baselineSet) {
2443
+ if (!routePaths.includes(b)) {
2444
+ diagnostics.push({
2445
+ code: "stale-page-baseline",
2446
+ severity: "warning",
2447
+ message: `coverage baseline lists "${b}", which is not a route in the registry`,
2448
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
2449
+ });
2450
+ } else if (covered.has(b) || optedOut.has(b)) {
2451
+ diagnostics.push({
2452
+ code: "page-captured",
2453
+ severity: "warning",
2454
+ message: `route "${b}" is now captured but is still in the coverage baseline`,
2455
+ hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
2456
+ });
2457
+ }
2458
+ }
2459
+ return diagnostics;
2460
+ }
2461
+ function computePageBaseline(registry, manifest) {
2462
+ const uncaptured = [];
2463
+ for (const d of checkPageCoverage(registry, manifest, {
2464
+ uncapturedPages: []
2465
+ })) {
2466
+ if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
2467
+ }
2468
+ return { uncapturedPages: uncaptured.sort() };
2469
+ }
2470
+
2471
+ // src/scanner/scan/state-coverage.ts
2472
+ var STATE_KINDS2 = ["page", "feature", "widget"];
2473
+ function declaredStateEntities(registry) {
2474
+ const out2 = [];
2475
+ for (const kind of STATE_KINDS2) {
2476
+ for (const e of registry.list(kind)) {
2477
+ const states = e.meta?.states;
2478
+ if (!states || states.length === 0) continue;
2479
+ out2.push({
2480
+ kind,
2481
+ id: e.id,
2482
+ states: states.map((s) => s.name),
2483
+ loc: e.loc
2484
+ });
2485
+ }
2486
+ }
2487
+ return out2;
2488
+ }
2489
+ function captureMatchesEntity(cap, ent) {
2490
+ if (cap.entity !== ent.id) return false;
2491
+ return cap.kind === void 0 || cap.kind === ent.kind;
2492
+ }
2493
+ function checkStateCoverage(registry, manifest) {
2494
+ const diagnostics = [];
2495
+ const declared = declaredStateEntities(registry);
2496
+ const captured = manifest.captured ?? [];
2497
+ for (const ent of declared) {
2498
+ for (const state of ent.states) {
2499
+ const hit = captured.some(
2500
+ (c) => captureMatchesEntity(c, ent) && c.state === state
2501
+ );
2502
+ if (hit) continue;
2503
+ diagnostics.push({
2504
+ code: "missing-state-capture",
2505
+ severity: "warning",
2506
+ message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
2507
+ file: ent.loc?.file,
2508
+ line: ent.loc?.line,
2509
+ entity: { kind: ent.kind, id: ent.id },
2510
+ hint: `Add a capture for "${ent.id}/${state}" to the states matrix, or remove the state from \`export const uidex = { states: [...] }\` if it no longer exists.`
2511
+ });
2512
+ }
2513
+ }
2514
+ for (const cap of captured) {
2515
+ const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
2516
+ if (candidates.length === 0) continue;
2517
+ const declaredSomewhere = candidates.some(
2518
+ (ent2) => ent2.states.includes(cap.state)
2519
+ );
2520
+ if (declaredSomewhere) continue;
2521
+ const ent = candidates[0];
2522
+ diagnostics.push({
2523
+ code: "orphan-state-capture",
2524
+ severity: "warning",
2525
+ message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
2526
+ file: ent.loc?.file,
2527
+ line: ent.loc?.line,
2528
+ entity: { kind: ent.kind, id: ent.id },
2529
+ hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
2530
+ });
2531
+ }
2532
+ return diagnostics;
2533
+ }
2534
+
2535
+ // src/scanner/scan/state-matrix.ts
2536
+ var CORE_KINDS = CORE_STATE_KINDS;
2537
+ var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
2538
+ function resolveMatrixKind(registry, c) {
2539
+ if (c.stateKind) return c.stateKind;
2540
+ for (const kind of STATE_ENTITY_KINDS) {
2541
+ const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
2542
+ if (declared) return declared;
2543
+ }
2544
+ return CORE_KINDS.includes(c.state) ? c.state : "variant";
2545
+ }
2546
+ function coreKindsByRoute(registry, manifest) {
2547
+ const routes = registry.list("route");
2548
+ const routePaths = routes.map((r) => r.path);
2549
+ const byRoute = /* @__PURE__ */ new Map();
2550
+ for (const c of manifest.captured ?? []) {
2551
+ const route = routeForCapture(c, routes, routePaths);
2552
+ if (!route) continue;
2553
+ if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
2554
+ const kind = resolveMatrixKind(registry, c);
2555
+ if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
2556
+ }
2557
+ return byRoute;
2558
+ }
2559
+ function pageWaivers(registry, routePage) {
2560
+ return registry.get("page", routePage)?.meta?.waivers ?? {};
2561
+ }
2562
+ function isOptedOut(registry, page) {
2563
+ return page ? registry.get("page", page)?.meta?.capture === false : false;
2564
+ }
2565
+ function checkStateMatrix(registry, manifest, baseline) {
2566
+ const routes = registry.list("route");
2567
+ if (routes.length === 0) return [];
2568
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2569
+ const captured = coreKindsByRoute(registry, manifest);
2570
+ const baselined = baseline?.missingKinds ?? {};
2571
+ const diagnostics = [];
2572
+ for (const [route, kinds] of captured) {
2573
+ const page = pageOf.get(route);
2574
+ if (isOptedOut(registry, page)) continue;
2575
+ const waivers = page ? pageWaivers(registry, page) : {};
2576
+ const loc = page ? registry.get("page", page)?.loc : void 0;
2577
+ const baselinedKinds = new Set(baselined[route] ?? []);
2578
+ for (const kind of CORE_KINDS) {
2579
+ if (kinds.has(kind)) continue;
2580
+ if (waivers[kind]) continue;
2581
+ if (baselinedKinds.has(kind)) {
2582
+ diagnostics.push({
2583
+ code: "matrix-backlog",
2584
+ severity: "info",
2585
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
2586
+ file: loc?.file,
2587
+ line: loc?.line,
2588
+ hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
2589
+ });
2590
+ continue;
2591
+ }
2592
+ diagnostics.push({
2593
+ code: "core-kind",
2594
+ severity: "error",
2595
+ message: `route "${route}" is captured but never renders core kind "${kind}"`,
2596
+ file: loc?.file,
2597
+ line: loc?.line,
2598
+ entity: { kind: "route", id: route },
2599
+ hint: `Capture the "${kind}" state, or if the route cannot render it add \`export const uidex = { page: "${page}", waivers: { ${kind}: "why it can't" } }\`, or grandfather it via \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
2600
+ });
2601
+ }
2602
+ }
2603
+ for (const [route, kinds] of captured) {
2604
+ const page = pageOf.get(route);
2605
+ if (!page || isOptedOut(registry, page)) continue;
2606
+ const waivers = pageWaivers(registry, page);
2607
+ const loc = registry.get("page", page)?.loc;
2608
+ for (const kind of Object.keys(waivers)) {
2609
+ if (kinds.has(kind)) {
2610
+ diagnostics.push({
2611
+ code: "stale-waiver",
2612
+ severity: "warning",
2613
+ message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
2614
+ file: loc?.file,
2615
+ line: loc?.line,
2616
+ entity: { kind: "page", id: page },
2617
+ hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
2618
+ });
2619
+ }
2620
+ }
2621
+ }
2622
+ return diagnostics;
2623
+ }
2624
+ function computeMissingKinds(registry, manifest) {
2625
+ const routes = registry.list("route");
2626
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2627
+ const captured = coreKindsByRoute(registry, manifest);
2628
+ const out2 = {};
2629
+ for (const [route, kinds] of captured) {
2630
+ const page = pageOf.get(route);
2631
+ if (isOptedOut(registry, page)) continue;
2632
+ const waivers = page ? pageWaivers(registry, page) : {};
2633
+ const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
2634
+ if (missing.length > 0) out2[route] = missing;
2635
+ }
2636
+ return out2;
2637
+ }
2638
+
2639
+ // src/scanner/scan/audit.ts
2166
2640
  function audit(opts) {
2167
2641
  const diagnostics = [];
2168
2642
  const { registry, extracted, files, config } = opts;
@@ -2171,6 +2645,9 @@ function audit(opts) {
2171
2645
  const acceptanceEnabled = config.audit?.acceptance ?? true;
2172
2646
  const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
2173
2647
  const coverageEnabled = config.audit?.coverage ?? true;
2648
+ const statesEnabled = config.audit?.states ?? true;
2649
+ const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
2650
+ const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
2174
2651
  if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
2175
2652
  for (const ef of extracted) {
2176
2653
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
@@ -2204,6 +2681,48 @@ function audit(opts) {
2204
2681
  }
2205
2682
  }
2206
2683
  }
2684
+ if (check && opts.typesGenerated !== void 0) {
2685
+ const outRel = opts.typesOutputPath ?? config.output;
2686
+ const fresh = normalizeForCheck(opts.typesGenerated);
2687
+ if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
2688
+ diagnostics.push({
2689
+ code: "gen-missing",
2690
+ severity: "error",
2691
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
2692
+ file: outRel,
2693
+ hint: "Run `uidex scan` (without --check) to regenerate"
2694
+ });
2695
+ } else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
2696
+ diagnostics.push({
2697
+ code: "gen-stale",
2698
+ severity: "error",
2699
+ message: `Generated file "${outRel}" is stale`,
2700
+ file: outRel,
2701
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
2702
+ });
2703
+ }
2704
+ }
2705
+ if (check && opts.locsGenerated !== void 0) {
2706
+ const outRel = opts.locsOutputPath ?? config.output;
2707
+ const fresh = normalizeForCheck(opts.locsGenerated);
2708
+ if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
2709
+ diagnostics.push({
2710
+ code: "gen-missing",
2711
+ severity: "error",
2712
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
2713
+ file: outRel,
2714
+ hint: "Run `uidex scan` (without --check) to regenerate"
2715
+ });
2716
+ } else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
2717
+ diagnostics.push({
2718
+ code: "gen-stale",
2719
+ severity: "error",
2720
+ message: `Generated file "${outRel}" is stale`,
2721
+ file: outRel,
2722
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
2723
+ });
2724
+ }
2725
+ }
2207
2726
  if (lint && acceptanceEnabled) {
2208
2727
  for (const kind of ["widget", "feature", "page"]) {
2209
2728
  for (const e of registry.list(kind)) {
@@ -2463,6 +2982,21 @@ function audit(opts) {
2463
2982
  }
2464
2983
  }
2465
2984
  }
2985
+ if (lint && statesEnabled && opts.capturedStates) {
2986
+ diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
2987
+ }
2988
+ if (lint && pageCoverageEnabled && opts.capturedStates) {
2989
+ diagnostics.push(
2990
+ ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
2991
+ );
2992
+ }
2993
+ if (lint && stateMatrixEnabled && opts.capturedStates) {
2994
+ diagnostics.push(
2995
+ ...checkStateMatrix(registry, opts.capturedStates, {
2996
+ missingKinds: opts.pageBaseline?.missingKinds ?? {}
2997
+ })
2998
+ );
2999
+ }
2466
3000
  const summary = {
2467
3001
  errors: diagnostics.filter((d) => d.severity === "error").length,
2468
3002
  warnings: diagnostics.filter((d) => d.severity === "warning").length
@@ -2627,6 +3161,148 @@ function stableReplacer(_key, value) {
2627
3161
  }
2628
3162
 
2629
3163
  // src/scanner/scan/emit.ts
3164
+ var AUTHORED_KINDS = /* @__PURE__ */ new Set([
3165
+ "route",
3166
+ "page",
3167
+ "feature",
3168
+ "widget",
3169
+ "primitive",
3170
+ "flow"
3171
+ ]);
3172
+ function internedPayload(entities) {
3173
+ const fileIndex = /* @__PURE__ */ new Map();
3174
+ const files = [];
3175
+ const compact = entities.map((e) => {
3176
+ const loc = e.loc;
3177
+ if (!loc || typeof loc.file !== "string") return e;
3178
+ let idx = fileIndex.get(loc.file);
3179
+ if (idx === void 0) {
3180
+ idx = files.length;
3181
+ files.push(loc.file);
3182
+ fileIndex.set(loc.file, idx);
3183
+ }
3184
+ const rest = { ...loc };
3185
+ delete rest.file;
3186
+ return { ...e, loc: { f: idx, ...rest } };
3187
+ });
3188
+ return {
3189
+ filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
3190
+ parseExpr: `(JSON.parse(${JSON.stringify(
3191
+ JSON.stringify(compact, replacerSorted)
3192
+ )}) as any[]).map(hydrate)`
3193
+ };
3194
+ }
3195
+ var HYDRATE_FN = [
3196
+ "function hydrate(e: any): any {",
3197
+ ' if (e.loc && typeof e.loc.f === "number") {',
3198
+ " const { f, ...rest } = e.loc",
3199
+ " e.loc = { file: files[f], ...rest }",
3200
+ " }",
3201
+ " return e",
3202
+ "}"
3203
+ ].join("\n");
3204
+ function compactMeta(meta) {
3205
+ if (!meta || typeof meta !== "object") return meta;
3206
+ const composes = meta.composes;
3207
+ if (!Array.isArray(composes)) return meta;
3208
+ return {
3209
+ ...meta,
3210
+ composes: composes.map(
3211
+ (c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
3212
+ )
3213
+ };
3214
+ }
3215
+ function emitLocsModule(thin, uidexImport) {
3216
+ const kinds = [...new Set(thin.map((e) => e.kind))].sort();
3217
+ const kindIdx = new Map(kinds.map((k, i) => [k, i]));
3218
+ const fileIndex = /* @__PURE__ */ new Map();
3219
+ const files = [];
3220
+ const fi = (f) => {
3221
+ let i = fileIndex.get(f);
3222
+ if (i === void 0) {
3223
+ i = files.length;
3224
+ files.push(f);
3225
+ fileIndex.set(f, i);
3226
+ }
3227
+ return i;
3228
+ };
3229
+ const bare = [];
3230
+ const rich = [];
3231
+ for (const e of thin) {
3232
+ const loc = e.loc;
3233
+ const topOnlyKindIdLoc = Object.keys(e).every(
3234
+ (k) => k === "kind" || k === "id" || k === "loc"
3235
+ );
3236
+ const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
3237
+ (k) => k === "file" || k === "line" || k === "column"
3238
+ );
3239
+ if (topOnlyKindIdLoc && locOnlyKnown) {
3240
+ const row = [
3241
+ kindIdx.get(e.kind),
3242
+ e.id,
3243
+ fi(loc.file)
3244
+ ];
3245
+ if (loc.line != null || loc.column != null)
3246
+ row.push(Number(loc.line ?? 0));
3247
+ if (loc.column != null) row.push(Number(loc.column));
3248
+ bare.push(row);
3249
+ } else {
3250
+ const richEntity = { ...e };
3251
+ if (loc && typeof loc.file === "string") {
3252
+ const { file, ...rest } = loc;
3253
+ richEntity.loc = { f: fi(file), ...rest };
3254
+ }
3255
+ if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
3256
+ rich.push(richEntity);
3257
+ }
3258
+ }
3259
+ const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
3260
+ const l = [];
3261
+ l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
3262
+ l.push(
3263
+ "// Deferred element/region source locations for the inspector's copy-path."
3264
+ );
3265
+ l.push("// @ts-nocheck");
3266
+ l.push("/* eslint-disable */");
3267
+ l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
3268
+ l.push("");
3269
+ l.push(`const files: readonly string[] = ${jsonStable(files)}`);
3270
+ l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
3271
+ l.push("function loc(fi: number, line?: number, col?: number) {");
3272
+ l.push(" const o: any = { file: files[fi] }");
3273
+ l.push(" if (line) o.line = line");
3274
+ l.push(" if (col) o.column = col");
3275
+ l.push(" return o");
3276
+ l.push("}");
3277
+ l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
3278
+ l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
3279
+ l.push("");
3280
+ l.push(
3281
+ "/** Add the thin element/region entities (source locations) to a registry. */"
3282
+ );
3283
+ l.push("export function loadLocs(target: Registry): Registry {");
3284
+ l.push(" for (const r of bare)");
3285
+ l.push(
3286
+ " target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
3287
+ );
3288
+ l.push(" for (const e of rich) {");
3289
+ l.push(" const out: any = { ...e }");
3290
+ l.push(' if (e.loc && typeof e.loc.f === "number") {');
3291
+ l.push(" const { f, ...rest } = e.loc");
3292
+ l.push(" out.loc = { file: files[f], ...rest }");
3293
+ l.push(" }");
3294
+ l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
3295
+ l.push(
3296
+ ' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
3297
+ );
3298
+ l.push(" }");
3299
+ l.push(" target.add(out)");
3300
+ l.push(" }");
3301
+ l.push(" return target");
3302
+ l.push("}");
3303
+ l.push("");
3304
+ return l.join("\n");
3305
+ }
2630
3306
  function sortById(arr) {
2631
3307
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
2632
3308
  }
@@ -2652,6 +3328,20 @@ function emitIdUnion(name, ids) {
2652
3328
  ${body}
2653
3329
  `;
2654
3330
  }
3331
+ var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
3332
+ function emitId(name, ids) {
3333
+ if (OPAQUE_ID_UNIONS.has(name)) {
3334
+ return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
3335
+ `;
3336
+ }
3337
+ return emitIdUnion(name, ids);
3338
+ }
3339
+ function dataOutputPath(output) {
3340
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
3341
+ }
3342
+ function locsOutputPath(output) {
3343
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
3344
+ }
2655
3345
  function emit(opts) {
2656
3346
  const { registry, gitContext, uidexImport = "uidex" } = opts;
2657
3347
  const routes = [...registry.list("route")].sort(
@@ -2660,112 +3350,131 @@ function emit(opts) {
2660
3350
  const pages = sortById(registry.list("page"));
2661
3351
  const features = sortById(registry.list("feature"));
2662
3352
  const widgets = sortById(registry.list("widget"));
3353
+ const stateNames = /* @__PURE__ */ new Set();
3354
+ for (const e of [...pages, ...features, ...widgets]) {
3355
+ for (const s of e.meta?.states ?? []) stateNames.add(s.name);
3356
+ }
2663
3357
  const regions = sortById(registry.list("region"));
2664
3358
  const elements = sortById(registry.list("element"));
2665
3359
  const primitives = sortById(registry.list("primitive"));
2666
3360
  const flows = sortById(registry.list("flow"));
2667
- const lines = [];
2668
- lines.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
2669
- lines.push("/* eslint-disable */");
2670
- lines.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
2671
- lines.push(
2672
- `import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
3361
+ const t = [];
3362
+ t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
3363
+ t.push(
3364
+ "// Types only \u2014 the runtime registry lives in the sibling `.data` module."
2673
3365
  );
2674
- lines.push("");
2675
- lines.push("// ---- id unions ----");
2676
- lines.push(
2677
- emitIdUnion(
3366
+ t.push("/* eslint-disable */");
3367
+ t.push("");
3368
+ t.push("// ---- id unions ----");
3369
+ t.push(
3370
+ emitId(
2678
3371
  "PageId",
2679
3372
  pages.map((e) => e.id)
2680
3373
  )
2681
3374
  );
2682
- lines.push(
2683
- emitIdUnion(
3375
+ t.push(
3376
+ emitId(
2684
3377
  "FeatureId",
2685
3378
  features.map((e) => e.id)
2686
3379
  )
2687
3380
  );
2688
- lines.push(
2689
- emitIdUnion(
3381
+ t.push(
3382
+ emitId(
2690
3383
  "WidgetId",
2691
3384
  widgets.map((e) => e.id)
2692
3385
  )
2693
3386
  );
2694
- lines.push(
2695
- emitIdUnion(
3387
+ t.push(
3388
+ emitId(
2696
3389
  "RegionId",
2697
3390
  regions.map((e) => e.id)
2698
3391
  )
2699
3392
  );
2700
- lines.push(
2701
- emitIdUnion(
3393
+ t.push(
3394
+ emitId(
2702
3395
  "ElementId",
2703
3396
  elements.map((e) => e.id)
2704
3397
  )
2705
3398
  );
2706
- lines.push(
2707
- emitIdUnion(
3399
+ t.push(
3400
+ emitId(
2708
3401
  "PrimitiveId",
2709
3402
  primitives.map((e) => e.id)
2710
3403
  )
2711
3404
  );
2712
- lines.push(
2713
- emitIdUnion(
3405
+ t.push(
3406
+ emitId(
2714
3407
  "FlowId",
2715
3408
  flows.map((e) => e.id)
2716
3409
  )
2717
3410
  );
2718
- lines.push(
2719
- emitIdUnion(
3411
+ t.push(
3412
+ emitId(
2720
3413
  "RouteId",
2721
3414
  routes.map((e) => e.path)
2722
3415
  )
2723
3416
  );
2724
- lines.push("");
2725
- lines.push("// ---- authoring-surface shape types ----");
2726
- lines.push("export namespace Uidex {");
2727
- lines.push(" export interface Page {");
2728
- lines.push(" page: PageId | false");
2729
- lines.push(" name?: string");
2730
- lines.push(" features?: readonly FeatureId[]");
2731
- lines.push(" widgets?: readonly WidgetId[]");
2732
- lines.push(" acceptance?: readonly string[]");
2733
- lines.push(" description?: string");
2734
- lines.push(" }");
2735
- lines.push(" export interface Feature {");
2736
- lines.push(" feature: FeatureId | false");
2737
- lines.push(" name?: string");
2738
- lines.push(" features?: readonly FeatureId[]");
2739
- lines.push(" acceptance?: readonly string[]");
2740
- lines.push(" description?: string");
2741
- lines.push(" }");
2742
- lines.push(" export interface Primitive {");
2743
- lines.push(" primitive: PrimitiveId");
2744
- lines.push(" name?: string");
2745
- lines.push(" description?: string");
2746
- lines.push(" }");
2747
- lines.push(" export interface Widget {");
2748
- lines.push(" widget: WidgetId");
2749
- lines.push(" name?: string");
2750
- lines.push(" acceptance?: readonly string[]");
2751
- lines.push(" description?: string");
2752
- lines.push(" }");
2753
- lines.push(" export interface Region {");
2754
- lines.push(" region: RegionId | false");
2755
- lines.push(" name?: string");
2756
- lines.push(" description?: string");
2757
- lines.push(" }");
2758
- lines.push(" export interface Flow {");
2759
- lines.push(" flow: FlowId");
2760
- lines.push(" name?: string");
2761
- lines.push(" description?: string");
2762
- lines.push(" }");
2763
- lines.push(" export interface NotFlow {");
2764
- lines.push(" notFlow: true");
2765
- lines.push(" }");
2766
- lines.push("}");
2767
- lines.push("");
2768
- lines.push("// ---- entities ----");
3417
+ t.push(emitId("StateId", [...stateNames]));
3418
+ t.push("");
3419
+ t.push("// ---- authoring-surface shape types ----");
3420
+ t.push("export namespace Uidex {");
3421
+ t.push(
3422
+ ' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
3423
+ );
3424
+ t.push(' export type StateKind = CoreStateKind | "variant"');
3425
+ t.push(" export interface State {");
3426
+ t.push(" name: string");
3427
+ t.push(" kind?: StateKind");
3428
+ t.push(" acceptance?: readonly string[]");
3429
+ t.push(" description?: string");
3430
+ t.push(" }");
3431
+ t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
3432
+ t.push(" export interface Page {");
3433
+ t.push(" page: PageId | false");
3434
+ t.push(" name?: string");
3435
+ t.push(" features?: readonly FeatureId[]");
3436
+ t.push(" widgets?: readonly WidgetId[]");
3437
+ t.push(" acceptance?: readonly string[]");
3438
+ t.push(" states?: readonly (string | State)[]");
3439
+ t.push(" capture?: boolean");
3440
+ t.push(" waivers?: Waivers");
3441
+ t.push(" description?: string");
3442
+ t.push(" }");
3443
+ t.push(" export interface Feature {");
3444
+ t.push(" feature: FeatureId | false");
3445
+ t.push(" name?: string");
3446
+ t.push(" features?: readonly FeatureId[]");
3447
+ t.push(" acceptance?: readonly string[]");
3448
+ t.push(" states?: readonly (string | State)[]");
3449
+ t.push(" description?: string");
3450
+ t.push(" }");
3451
+ t.push(" export interface Primitive {");
3452
+ t.push(" primitive: PrimitiveId");
3453
+ t.push(" name?: string");
3454
+ t.push(" description?: string");
3455
+ t.push(" }");
3456
+ t.push(" export interface Widget {");
3457
+ t.push(" widget: WidgetId");
3458
+ t.push(" name?: string");
3459
+ t.push(" acceptance?: readonly string[]");
3460
+ t.push(" states?: readonly (string | State)[]");
3461
+ t.push(" description?: string");
3462
+ t.push(" }");
3463
+ t.push(" export interface Region {");
3464
+ t.push(" region: RegionId | false");
3465
+ t.push(" name?: string");
3466
+ t.push(" description?: string");
3467
+ t.push(" }");
3468
+ t.push(" export interface Flow {");
3469
+ t.push(" flow: FlowId");
3470
+ t.push(" name?: string");
3471
+ t.push(" description?: string");
3472
+ t.push(" }");
3473
+ t.push(" export interface NotFlow {");
3474
+ t.push(" notFlow: true");
3475
+ t.push(" }");
3476
+ t.push("}");
3477
+ t.push("");
2769
3478
  const allEntities = [
2770
3479
  ...routes,
2771
3480
  ...pages,
@@ -2776,25 +3485,39 @@ function emit(opts) {
2776
3485
  ...primitives,
2777
3486
  ...flows
2778
3487
  ];
2779
- lines.push(
2780
- `export const entities: ReadonlyArray<Entity> = ${jsonStable(allEntities)}`
2781
- );
2782
- lines.push("");
2783
- lines.push("// ---- git context ----");
3488
+ const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
3489
+ const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
2784
3490
  const gc = gitContext ?? { branch: null, commit: null, pr: null };
2785
- lines.push(`export const gitContext = ${jsonStable(gc)} as const`);
2786
- lines.push("");
2787
- lines.push("// ---- registry factory ----");
2788
- lines.push("export function loadRegistry(target: Registry): Registry {");
2789
- lines.push(" for (const entity of entities) target.add(entity)");
2790
- lines.push(" return target");
2791
- lines.push("}");
2792
- lines.push("");
2793
- lines.push("// ---- preconfigured uidex instance ----");
2794
- lines.push("export const uidex = createUidex()");
2795
- lines.push("for (const entity of entities) uidex.registry.add(entity)");
2796
- lines.push("");
2797
- return lines.join("\n");
3491
+ const authoredPayload = internedPayload(authored);
3492
+ const d = [];
3493
+ d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
3494
+ d.push(
3495
+ "// Authored-metadata registry. The thin element/region entities (source"
3496
+ );
3497
+ d.push(
3498
+ "// locations only) live in the sibling `.locs` module, loaded lazily."
3499
+ );
3500
+ d.push("// @ts-nocheck");
3501
+ d.push("/* eslint-disable */");
3502
+ d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
3503
+ d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
3504
+ d.push("");
3505
+ d.push(authoredPayload.filesDecl);
3506
+ d.push(HYDRATE_FN);
3507
+ d.push(`export const entities = ${authoredPayload.parseExpr}`);
3508
+ d.push("");
3509
+ d.push(`export const gitContext = ${jsonStable(gc)} as const`);
3510
+ d.push("");
3511
+ d.push("export function loadRegistry(target: Registry): Registry {");
3512
+ d.push(" for (const entity of entities) target.add(entity)");
3513
+ d.push(" return target");
3514
+ d.push("}");
3515
+ d.push("");
3516
+ d.push("export const uidex = createUidex()");
3517
+ d.push("for (const entity of entities) uidex.registry.add(entity)");
3518
+ d.push("");
3519
+ const locs = emitLocsModule(thin, uidexImport);
3520
+ return { types: t.join("\n"), data: d.join("\n"), locs };
2798
3521
  }
2799
3522
 
2800
3523
  // src/scanner/scan/git.ts
@@ -2905,6 +3628,68 @@ function renderSpec(args) {
2905
3628
  // src/scanner/scan/pipeline.ts
2906
3629
  import * as fs4 from "fs";
2907
3630
  import * as path7 from "path";
3631
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
3632
+ var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
3633
+ function loadPageBaseline(configDir, config) {
3634
+ const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3635
+ const abs = path7.resolve(configDir, rel);
3636
+ let raw;
3637
+ try {
3638
+ raw = fs4.readFileSync(abs, "utf8");
3639
+ } catch {
3640
+ return void 0;
3641
+ }
3642
+ try {
3643
+ const parsed = JSON.parse(raw);
3644
+ if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
3645
+ const uncapturedPages = parsed.uncapturedPages.filter(
3646
+ (p2) => typeof p2 === "string"
3647
+ );
3648
+ let missingKinds;
3649
+ const mkRaw = parsed.missingKinds;
3650
+ if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
3651
+ const mk = {};
3652
+ for (const [route, kinds] of Object.entries(mkRaw)) {
3653
+ if (Array.isArray(kinds)) {
3654
+ mk[route] = kinds.filter((k) => typeof k === "string");
3655
+ }
3656
+ }
3657
+ missingKinds = mk;
3658
+ }
3659
+ return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
3660
+ } catch {
3661
+ return void 0;
3662
+ }
3663
+ }
3664
+ function loadCapturedStates(configDir, config) {
3665
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3666
+ const abs = path7.resolve(configDir, rel);
3667
+ let raw;
3668
+ try {
3669
+ raw = fs4.readFileSync(abs, "utf8");
3670
+ } catch {
3671
+ return void 0;
3672
+ }
3673
+ try {
3674
+ const parsed = JSON.parse(raw);
3675
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
3676
+ const captured = [];
3677
+ for (const entry of parsed.captured) {
3678
+ if (!entry || typeof entry !== "object") continue;
3679
+ const c = entry;
3680
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
3681
+ const rec = { entity: c.entity, state: c.state };
3682
+ if (typeof c.kind === "string") rec.kind = c.kind;
3683
+ if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
3684
+ if (typeof c.url === "string") rec.url = c.url;
3685
+ if (typeof c.route === "string") rec.route = c.route;
3686
+ captured.push(rec);
3687
+ }
3688
+ return { captured };
3689
+ } catch {
3690
+ return void 0;
3691
+ }
3692
+ }
2908
3693
  function runScan(opts = {}) {
2909
3694
  const cwd = opts.cwd ?? process.cwd();
2910
3695
  const configs = opts.configs ?? discover({ cwd });
@@ -2931,23 +3716,29 @@ function runOne(dc, opts) {
2931
3716
  flowFiles: extractedFlows
2932
3717
  });
2933
3718
  const gitContext = resolveGitContext({ cwd: configDir });
2934
- const generated = emit({
3719
+ const emitted = emit({
2935
3720
  registry: resolved.registry,
2936
3721
  gitContext
2937
3722
  });
2938
- const outputPath = path7.resolve(configDir, config.output);
2939
- const outputRel = config.output;
2940
- let existingOnDisk = null;
3723
+ const typesRel = config.output;
3724
+ const typesPath = path7.resolve(configDir, typesRel);
3725
+ const dataRel = dataOutputPath(config.output);
3726
+ const dataPath = path7.resolve(configDir, dataRel);
3727
+ const locsRel = locsOutputPath(config.output);
3728
+ const locsPath = path7.resolve(configDir, locsRel);
3729
+ let typesOnDisk = null;
3730
+ let dataOnDisk = null;
3731
+ let locsOnDisk = null;
2941
3732
  if (opts.check) {
2942
- try {
2943
- existingOnDisk = fs4.readFileSync(outputPath, "utf8");
2944
- } catch {
2945
- existingOnDisk = null;
2946
- }
3733
+ typesOnDisk = readOrNull(typesPath);
3734
+ dataOnDisk = readOrNull(dataPath);
3735
+ locsOnDisk = readOrNull(locsPath);
2947
3736
  }
2948
3737
  const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
2949
3738
  (ef) => (ef.diagnostics?.length ?? 0) > 0
2950
3739
  );
3740
+ const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
3741
+ const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
2951
3742
  let auditResult;
2952
3743
  if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
2953
3744
  auditResult = audit({
@@ -2959,9 +3750,20 @@ function runOne(dc, opts) {
2959
3750
  check: opts.check,
2960
3751
  lint: opts.lint,
2961
3752
  resolveDiagnostics: resolved.diagnostics,
2962
- generated,
2963
- existingOnDisk,
2964
- outputPath: outputRel
3753
+ // The entity array (and thus the meaningful drift) lives in the data file,
3754
+ // so the entity-aware gen-stale diff runs against it; the types file gets a
3755
+ // plain content compare (see audit()).
3756
+ generated: emitted.data,
3757
+ existingOnDisk: dataOnDisk,
3758
+ outputPath: dataRel,
3759
+ typesGenerated: emitted.types,
3760
+ typesOnDisk,
3761
+ typesOutputPath: typesRel,
3762
+ locsGenerated: emitted.locs,
3763
+ locsOnDisk,
3764
+ locsOutputPath: locsRel,
3765
+ capturedStates,
3766
+ pageBaseline
2965
3767
  });
2966
3768
  }
2967
3769
  return {
@@ -2970,13 +3772,38 @@ function runOne(dc, opts) {
2970
3772
  registry: resolved.registry,
2971
3773
  gitContext,
2972
3774
  audit: auditResult,
2973
- generated,
2974
- outputPath
3775
+ types: {
3776
+ generated: emitted.types,
3777
+ outputPath: typesPath,
3778
+ outputRel: typesRel
3779
+ },
3780
+ data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
3781
+ locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
3782
+ capturedStates
2975
3783
  };
2976
3784
  }
3785
+ function readOrNull(p2) {
3786
+ try {
3787
+ return fs4.readFileSync(p2, "utf8");
3788
+ } catch {
3789
+ return null;
3790
+ }
3791
+ }
3792
+ function writeArtifact(artifact) {
3793
+ if (readOrNull(artifact.outputPath) === artifact.generated) return false;
3794
+ fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
3795
+ fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
3796
+ return true;
3797
+ }
2977
3798
  function writeScanResult(result) {
2978
- fs4.mkdirSync(path7.dirname(result.outputPath), { recursive: true });
2979
- fs4.writeFileSync(result.outputPath, result.generated, "utf8");
3799
+ const wroteTypes = writeArtifact(result.types);
3800
+ const wroteData = writeArtifact(result.data);
3801
+ const wroteLocs = writeArtifact(result.locs);
3802
+ return wroteTypes || wroteData || wroteLocs;
3803
+ }
3804
+ function pageBaselinePath(result) {
3805
+ const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3806
+ return path7.resolve(result.configDir, rel);
2980
3807
  }
2981
3808
 
2982
3809
  // src/scanner/scan/fix.ts
@@ -3549,16 +4376,13 @@ function helpText2() {
3549
4376
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
3550
4377
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
3551
4378
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3552
- " api <METHOD> <PATH> Call the uidex API",
3553
- " api --list Show available API routes",
3554
- " api login Authenticate via browser",
3555
- " api login --token <tok> Store an auth token directly",
3556
4379
  "",
3557
4380
  "Flags:",
3558
4381
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
3559
4382
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
3560
4383
  " --audit Equivalent to --check --lint (read-only)",
3561
4384
  " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4385
+ " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
3562
4386
  " --json Emit JSON diagnostics on stdout",
3563
4387
  " --force (scaffold) overwrite existing spec",
3564
4388
  ""
@@ -3578,7 +4402,7 @@ function runInit(cwd, w) {
3578
4402
  fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3579
4403
  w.out(`Created ${configPath}`);
3580
4404
  const gitignorePath = path11.join(cwd, ".gitignore");
3581
- const entry = "*.gen.ts";
4405
+ const entry = "*.gen.*";
3582
4406
  if (fs8.existsSync(gitignorePath)) {
3583
4407
  const existing = fs8.readFileSync(gitignorePath, "utf8");
3584
4408
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
@@ -3601,8 +4425,9 @@ function runInit(cwd, w) {
3601
4425
  }
3602
4426
  function runScanCommand(cwd, flags, w) {
3603
4427
  const fix = Boolean(flags.fix);
3604
- const check = !fix && Boolean(flags.check || flags.audit);
3605
- const lint = Boolean(flags.lint || flags.audit || fix);
4428
+ const updateBaseline = Boolean(flags["update-baseline"]);
4429
+ const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
4430
+ const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
3606
4431
  const asJson = Boolean(flags.json);
3607
4432
  let configs = discover({ cwd });
3608
4433
  if (configs.length === 0) {
@@ -3621,8 +4446,33 @@ function runScanCommand(cwd, flags, w) {
3621
4446
  configs = discover({ cwd });
3622
4447
  }
3623
4448
  const results = runScan({ cwd, check, lint, configs });
4449
+ const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
4450
+ const wrote = /* @__PURE__ */ new Set();
3624
4451
  if (!check) {
3625
- for (const r of results) writeScanResult(r);
4452
+ for (const a of artifacts) {
4453
+ if (writeArtifact(a)) wrote.add(a);
4454
+ }
4455
+ }
4456
+ if (updateBaseline) {
4457
+ for (const r of results) {
4458
+ const manifest = r.capturedStates ?? { captured: [] };
4459
+ const pageBaseline = computePageBaseline(r.registry, manifest);
4460
+ const missingKinds = computeMissingKinds(r.registry, manifest);
4461
+ const baseline = {
4462
+ ...pageBaseline,
4463
+ ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
4464
+ };
4465
+ const p2 = pageBaselinePath(r);
4466
+ fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4467
+ const kindGaps = Object.values(missingKinds).reduce(
4468
+ (n, ks) => n + ks.length,
4469
+ 0
4470
+ );
4471
+ w.out(
4472
+ `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
4473
+ );
4474
+ }
4475
+ return w.result(0);
3626
4476
  }
3627
4477
  const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
3628
4478
  const summary = results.reduce(
@@ -3650,10 +4500,14 @@ function runScanCommand(cwd, flags, w) {
3650
4500
  );
3651
4501
  }
3652
4502
  for (const r of results) {
3653
- if (check) {
3654
- w.out(`Checked ${r.outputPath}`);
3655
- } else {
3656
- w.out(`Wrote ${r.outputPath}`);
4503
+ for (const art of [r.types, r.data, r.locs]) {
4504
+ if (check) {
4505
+ w.out(`Checked ${art.outputPath}`);
4506
+ } else if (wrote.has(art)) {
4507
+ w.out(`Wrote ${art.outputPath}`);
4508
+ } else {
4509
+ w.out(`Unchanged ${art.outputPath}`);
4510
+ }
3657
4511
  }
3658
4512
  for (const d of r.audit?.diagnostics ?? []) {
3659
4513
  const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
@@ -3761,12 +4615,21 @@ export {
3761
4615
  DEFAULT_CONVENTIONS,
3762
4616
  applyFixes,
3763
4617
  audit,
4618
+ checkPageCoverage,
4619
+ checkStateCoverage,
4620
+ checkStateMatrix,
4621
+ compileRoute,
4622
+ computeMissingKinds,
4623
+ computePageBaseline,
3764
4624
  detectRoutes,
3765
4625
  discover,
3766
4626
  emit,
3767
4627
  extract,
3768
4628
  extractUidexExports,
3769
4629
  globToRegExp,
4630
+ loadPageBaseline,
4631
+ matchUrlToRoute,
4632
+ pageBaselinePath,
3770
4633
  parseConfig,
3771
4634
  pathToId,
3772
4635
  renameEntity,