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
package/dist/cli/cli.cjs CHANGED
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/scanner/scan/cli.ts
27
- var fs8 = __toESM(require("fs"), 1);
28
- var path11 = __toESM(require("path"), 1);
27
+ var fs9 = __toESM(require("fs"), 1);
28
+ var path12 = __toESM(require("path"), 1);
29
29
 
30
30
  // src/scanner/scan/ai/index.ts
31
31
  var p = __toESM(require("@clack/prompts"), 1);
@@ -302,7 +302,8 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
302
302
  "audit",
303
303
  "conventions",
304
304
  "statesManifest",
305
- "coverageBaseline"
305
+ "coverageBaseline",
306
+ "workspace"
306
307
  ]);
307
308
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
308
309
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -318,19 +319,64 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
318
319
  "acceptance",
319
320
  "states",
320
321
  "pageCoverage",
321
- "stateMatrix"
322
+ "stateMatrix",
323
+ "severity"
322
324
  ]);
325
+ var DIAGNOSTIC_CODES = [
326
+ "acceptance-uncovered",
327
+ "acknowledged-elsewhere",
328
+ "competing-uidex-export",
329
+ "component-state-uncaptured",
330
+ "core-kind",
331
+ "duplicate-id",
332
+ "dynamic-attr",
333
+ "dynamic-flow-reference",
334
+ "gen-missing",
335
+ "gen-stale",
336
+ "legacy-acknowledge-field",
337
+ "legacy-coverage-baseline",
338
+ "matrix-backlog",
339
+ "missing-element-annotation",
340
+ "missing-state-capture",
341
+ "orphan-state-capture",
342
+ "page-backlog",
343
+ "page-captured",
344
+ "page-no-capture",
345
+ "parse-error",
346
+ "placeholder-reason",
347
+ "prefer-well-known-file",
348
+ "rename",
349
+ "scope-leak",
350
+ "spread-attr",
351
+ "stale-acknowledgment",
352
+ "stale-page-baseline",
353
+ "uidex-export-ambiguous-kind",
354
+ "uidex-export-duplicate-field",
355
+ "uidex-export-duplicate-state",
356
+ "uidex-export-empty-name",
357
+ "uidex-export-invalid-field",
358
+ "uidex-export-invalid-literal",
359
+ "uidex-export-missing-kind",
360
+ "uidex-export-satisfies-mismatch",
361
+ "uidex-export-unknown-field",
362
+ "unknown-reference",
363
+ "widget-id-mismatch",
364
+ "widget-missing-acceptance",
365
+ "widget-missing-dom"
366
+ ];
367
+ var DIAGNOSTIC_CODE_SET = new Set(DIAGNOSTIC_CODES);
368
+ var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
323
369
  function fail(msg) {
324
370
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
325
371
  }
326
- function assertObject(value, path12) {
372
+ function assertObject(value, path13) {
327
373
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
328
- fail(`${path12} must be an object`);
374
+ fail(`${path13} must be an object`);
329
375
  }
330
376
  }
331
- function assertStringArray(value, path12) {
377
+ function assertStringArray(value, path13) {
332
378
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
333
- fail(`${path12} must be a string[]`);
379
+ fail(`${path13} must be a string[]`);
334
380
  }
335
381
  }
336
382
  function validateConfig(raw) {
@@ -384,11 +430,30 @@ function validateConfig(raw) {
384
430
  if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
385
431
  fail(`"coverageBaseline" must be a string`);
386
432
  }
433
+ if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
434
+ fail(`"workspace" must be a path string or false`);
435
+ }
387
436
  if (raw.audit !== void 0) {
388
437
  assertObject(raw.audit, "audit");
389
438
  for (const key of Object.keys(raw.audit)) {
390
439
  if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
391
440
  const v = raw.audit[key];
441
+ if (key === "severity") {
442
+ assertObject(v, "audit.severity");
443
+ for (const [code, level] of Object.entries(v)) {
444
+ if (!DIAGNOSTIC_CODE_SET.has(code)) {
445
+ fail(
446
+ `unknown diagnostic code "${code}" in audit.severity (a typo here would silently leave that gate at its default severity)`
447
+ );
448
+ }
449
+ if (typeof level !== "string" || !SEVERITY_LEVELS.has(level)) {
450
+ fail(
451
+ `audit.severity["${code}"] must be one of "error", "warning", "info", "off"`
452
+ );
453
+ }
454
+ }
455
+ continue;
456
+ }
392
457
  if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
393
458
  }
394
459
  }
@@ -431,7 +496,8 @@ function validateConfig(raw) {
431
496
  audit: raw.audit,
432
497
  conventions: raw.conventions,
433
498
  statesManifest: raw.statesManifest,
434
- coverageBaseline: raw.coverageBaseline
499
+ coverageBaseline: raw.coverageBaseline,
500
+ workspace: raw.workspace
435
501
  };
436
502
  }
437
503
  function parseConfig(json) {
@@ -481,9 +547,11 @@ function tryReadConfig(configPath) {
481
547
  function discover(options = {}) {
482
548
  const cwd = options.cwd ?? process.cwd();
483
549
  const maxDepth = options.maxDepth ?? MAX_DEPTH;
550
+ const all = options.all ?? false;
484
551
  const rootMatch = tryReadConfig(path3.join(cwd, CONFIG_FILENAME));
485
- if (rootMatch) return [rootMatch];
552
+ if (rootMatch && !all) return [rootMatch];
486
553
  const results = [];
554
+ if (rootMatch) results.push(rootMatch);
487
555
  const queue = [[cwd, 0]];
488
556
  while (queue.length > 0) {
489
557
  const [dir, depth] = queue.shift();
@@ -604,11 +672,11 @@ function applyFixes(diagnostics) {
604
672
  }
605
673
 
606
674
  // src/scanner/scan/pipeline.ts
607
- var fs6 = __toESM(require("fs"), 1);
608
- var path9 = __toESM(require("path"), 1);
675
+ var fs7 = __toESM(require("fs"), 1);
676
+ var path10 = __toESM(require("path"), 1);
609
677
 
610
678
  // src/scanner/scan/audit.ts
611
- var path5 = __toESM(require("path"), 1);
679
+ var path7 = __toESM(require("path"), 1);
612
680
 
613
681
  // src/shared/entities/types.ts
614
682
  var ENTITY_KINDS = [
@@ -805,6 +873,320 @@ function createRegistry() {
805
873
  };
806
874
  }
807
875
 
876
+ // src/scanner/scan/acknowledge.ts
877
+ var ACK_ALL = "*";
878
+ var ACK_SCOPES = [ACK_ALL, ...CORE_STATE_KINDS];
879
+ var COMPONENT_ACK_SCOPES = [ACK_ALL];
880
+ var AUTHORABLE_ACK_TYPES = [
881
+ "impossible",
882
+ "backlog"
883
+ ];
884
+ var CORE_SET = new Set(CORE_STATE_KINDS);
885
+ function isCoreKind(scope) {
886
+ return CORE_SET.has(scope);
887
+ }
888
+ var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
889
+ function isPlaceholderReason(reason) {
890
+ return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
891
+ }
892
+ function describeAck(a) {
893
+ if (a.type === "captured-elsewhere") {
894
+ return a.by ? `captured-elsewhere (${a.by})` : "captured-elsewhere";
895
+ }
896
+ return a.reason ? `${a.type} ("${a.reason}")` : a.type;
897
+ }
898
+ function originHint(a, page) {
899
+ if (a.origin === "baseline") {
900
+ return "Prune it with `uidex scan --update-baseline` so the backlog reflects reality.";
901
+ }
902
+ if (a.origin === "workspace") {
903
+ return "This is computed from the workspace; it resolves itself once the sibling capture changes.";
904
+ }
905
+ const where = page ? ` on page "${page}"` : "";
906
+ return `Remove the \`acknowledge.${a.scope}\` entry${where} \u2014 a capture contradicts it.`;
907
+ }
908
+ function toResolved(map, origin, into) {
909
+ for (const [scope, ack] of Object.entries(map ?? {})) {
910
+ into.set(scope, { ...ack, scope, origin });
911
+ }
912
+ }
913
+ function routeAcknowledge(registry, baseline, route, page) {
914
+ const out2 = /* @__PURE__ */ new Map();
915
+ toResolved(baseline?.acknowledge?.[route], "baseline", out2);
916
+ if (page) {
917
+ toResolved(registry.get("page", page)?.meta?.acknowledge, "source", out2);
918
+ }
919
+ return out2;
920
+ }
921
+ function entityAcknowledge(meta, capturedElsewhereBy) {
922
+ const source = meta?.acknowledge?.[ACK_ALL];
923
+ if (source) return { ...source, scope: ACK_ALL, origin: "source" };
924
+ if (capturedElsewhereBy) {
925
+ return {
926
+ type: "captured-elsewhere",
927
+ by: capturedElsewhereBy,
928
+ scope: ACK_ALL,
929
+ origin: "workspace"
930
+ };
931
+ }
932
+ return void 0;
933
+ }
934
+ function readAck(raw) {
935
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
936
+ const r = raw;
937
+ if (r.type !== "impossible" && r.type !== "backlog") return void 0;
938
+ const out2 = { type: r.type };
939
+ if (typeof r.reason === "string" && r.reason.length > 0) out2.reason = r.reason;
940
+ return out2;
941
+ }
942
+ function parseCoverageBaseline(raw) {
943
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
944
+ const r = raw;
945
+ const ackRaw = r.acknowledge;
946
+ if (!ackRaw || typeof ackRaw !== "object" || Array.isArray(ackRaw))
947
+ return null;
948
+ const acknowledge = {};
949
+ for (const [route, scopesRaw] of Object.entries(
950
+ ackRaw
951
+ )) {
952
+ if (!scopesRaw || typeof scopesRaw !== "object" || Array.isArray(scopesRaw))
953
+ continue;
954
+ const scopes = {};
955
+ for (const [scope, ackVal] of Object.entries(
956
+ scopesRaw
957
+ )) {
958
+ if (scope !== ACK_ALL && !isCoreKind(scope)) continue;
959
+ const ack = readAck(ackVal);
960
+ if (ack) scopes[scope] = ack;
961
+ }
962
+ if (Object.keys(scopes).length > 0) acknowledge[route] = scopes;
963
+ }
964
+ return { version: 2, acknowledge };
965
+ }
966
+ function isLegacyBaseline(raw) {
967
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
968
+ const r = raw;
969
+ if ("acknowledge" in r) return false;
970
+ return "uncapturedPages" in r || "missingKinds" in r;
971
+ }
972
+ function migrateBaseline(legacy) {
973
+ const acknowledge = {};
974
+ const at = (route) => acknowledge[route] ??= {};
975
+ for (const route of legacy.uncapturedPages ?? []) {
976
+ if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
977
+ }
978
+ for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
979
+ if (!Array.isArray(kinds)) continue;
980
+ for (const kind of kinds) {
981
+ if (typeof kind === "string" && isCoreKind(kind)) {
982
+ at(route)[kind] = { type: "backlog" };
983
+ }
984
+ }
985
+ }
986
+ return { version: 2, acknowledge };
987
+ }
988
+ function serializeCoverageBaseline(baseline) {
989
+ const routes = Object.keys(baseline.acknowledge).sort();
990
+ const ordered = {};
991
+ for (const route of routes) {
992
+ const scopes = baseline.acknowledge[route];
993
+ const sorted = {};
994
+ for (const scope of ACK_SCOPES) {
995
+ if (scopes[scope]) sorted[scope] = scopes[scope];
996
+ }
997
+ ordered[route] = sorted;
998
+ }
999
+ return JSON.stringify({ version: 2, acknowledge: ordered }, null, 2) + "\n";
1000
+ }
1001
+
1002
+ // src/scanner/scan/component-coverage.ts
1003
+ var COMPOSED_KINDS = ["widget", "feature"];
1004
+ function composedStateEntities(registry, page) {
1005
+ const meta = registry.get("page", page)?.meta;
1006
+ if (!meta) return [];
1007
+ const out2 = [];
1008
+ for (const kind of COMPOSED_KINDS) {
1009
+ const ids = (kind === "widget" ? meta.widgets : meta.features) ?? [];
1010
+ for (const id of ids) {
1011
+ const e = registry.get(kind, id);
1012
+ if (!e?.meta?.states?.length) continue;
1013
+ if (e.meta.acknowledge?.[ACK_ALL]) continue;
1014
+ out2.push({ kind, id });
1015
+ }
1016
+ }
1017
+ return out2;
1018
+ }
1019
+ function capturedStatesFor(captured, kind, id) {
1020
+ const out2 = /* @__PURE__ */ new Set();
1021
+ for (const c of captured) {
1022
+ if (c.entity !== id) continue;
1023
+ if (c.kind !== void 0 && c.kind !== kind && c.kind !== "page") continue;
1024
+ out2.add(c.state);
1025
+ }
1026
+ return out2;
1027
+ }
1028
+ function checkComponentCoverage(registry, manifest) {
1029
+ const routes = registry.list("route");
1030
+ if (routes.length === 0) return [];
1031
+ const captured = manifest.captured ?? [];
1032
+ const diagnostics = [];
1033
+ for (const route of routes) {
1034
+ if (registry.get("page", route.page)?.meta?.acknowledge?.[ACK_ALL]) continue;
1035
+ for (const { kind, id } of composedStateEntities(registry, route.page)) {
1036
+ const entity = registry.get(kind, id);
1037
+ const declared = entity.meta.states.map((s) => s.name);
1038
+ const shot = capturedStatesFor(captured, kind, id);
1039
+ const missing = declared.filter((s) => !shot.has(s));
1040
+ if (missing.length === 0) continue;
1041
+ diagnostics.push({
1042
+ code: "component-state-uncaptured",
1043
+ severity: "info",
1044
+ message: `route "${route.path}" composes ${kind} "${id}", whose state(s) [${missing.join(", ")}] no capture produced`,
1045
+ file: entity.loc?.file,
1046
+ line: entity.loc?.line,
1047
+ entity: { kind, id },
1048
+ hint: `Capture the missing state(s) for "${id}", or drop them from its \`states\` if they no longer exist. This is the integration view: an isolated component capture never proves the state renders correctly INSIDE "${route.path}".`
1049
+ });
1050
+ }
1051
+ }
1052
+ return diagnostics;
1053
+ }
1054
+
1055
+ // src/scanner/scan/migrate-acknowledge.ts
1056
+ var CORE = CORE_STATE_KINDS;
1057
+ function orderScopes(scopes) {
1058
+ const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
1059
+ return [...scopes].sort((a, b) => rank(a) - rank(b));
1060
+ }
1061
+ function renderKey(scope) {
1062
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
1063
+ }
1064
+ function renderAck(a) {
1065
+ const parts = [`type: ${JSON.stringify(a.type)}`];
1066
+ if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
1067
+ return `{ ${parts.join(", ")} }`;
1068
+ }
1069
+ function renderAcknowledge(entries, indent) {
1070
+ const inner = indent + " ";
1071
+ const lines = orderScopes(Object.keys(entries)).map(
1072
+ (scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
1073
+ );
1074
+ return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
1075
+ }
1076
+ function indentAt(content, offset) {
1077
+ const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
1078
+ const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
1079
+ return m ? m[0] : "";
1080
+ }
1081
+ function plannedEntries(m) {
1082
+ const entries = {};
1083
+ if (m.legacyCapture === false) {
1084
+ entries[ACK_ALL] = {
1085
+ type: "impossible",
1086
+ reason: m.description || PLACEHOLDER_REASON
1087
+ };
1088
+ }
1089
+ for (const kind of CORE) {
1090
+ const reason = m.legacyWaivers?.[kind];
1091
+ if (reason) entries[kind] = { type: "impossible", reason };
1092
+ }
1093
+ return entries;
1094
+ }
1095
+ function migrateLegacyFields(ef) {
1096
+ const out2 = [];
1097
+ const { content, sourcePath, displayPath } = ef.file;
1098
+ for (const m of ef.metadata ?? []) {
1099
+ const legacy = [];
1100
+ if (m.legacyWaivers) legacy.push("waivers");
1101
+ if (m.legacyCapture !== void 0) legacy.push("capture");
1102
+ if (legacy.length === 0) continue;
1103
+ const fields = legacy.join(" and ");
1104
+ const base = {
1105
+ severity: "error",
1106
+ code: "legacy-acknowledge-field",
1107
+ file: displayPath,
1108
+ line: m.loc.line
1109
+ };
1110
+ if (m.acknowledge) {
1111
+ out2.push({
1112
+ ...base,
1113
+ message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
1114
+ hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
1115
+ });
1116
+ continue;
1117
+ }
1118
+ const entries = plannedEntries(m);
1119
+ const edits = [];
1120
+ const present = legacy.map((f) => ({ f, prop: m.fieldPropSpans?.[f], del: m.fieldSpans?.[f] })).filter((x) => x.prop && x.del).sort((a, b) => a.prop.start - b.prop.start);
1121
+ if (present.length !== legacy.length) {
1122
+ out2.push({
1123
+ ...base,
1124
+ message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
1125
+ hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
1126
+ });
1127
+ continue;
1128
+ }
1129
+ const [anchor, ...rest] = present;
1130
+ if (Object.keys(entries).length === 0) {
1131
+ edits.push({
1132
+ path: sourcePath,
1133
+ start: anchor.del.start,
1134
+ end: anchor.del.end,
1135
+ replacement: ""
1136
+ });
1137
+ } else {
1138
+ edits.push({
1139
+ path: sourcePath,
1140
+ start: anchor.prop.start,
1141
+ end: anchor.prop.end,
1142
+ replacement: renderAcknowledge(
1143
+ entries,
1144
+ indentAt(content, anchor.prop.start)
1145
+ )
1146
+ });
1147
+ }
1148
+ for (const r of rest) {
1149
+ edits.push({
1150
+ path: sourcePath,
1151
+ start: r.del.start,
1152
+ end: r.del.end,
1153
+ replacement: ""
1154
+ });
1155
+ }
1156
+ out2.push({
1157
+ ...base,
1158
+ message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
1159
+ hint: `Run \`uidex scan --fix\` to rewrite it${m.legacyCapture === false && !m.description ? ", then replace the TODO reason it plants \u2014 `capture: false` carried no explanation and `impossible` requires one" : ""}.`,
1160
+ fix: {
1161
+ description: `Rewrite \`${fields}\` as \`acknowledge\``,
1162
+ edits
1163
+ }
1164
+ });
1165
+ }
1166
+ return out2;
1167
+ }
1168
+ function migrateLegacyBaseline(opts) {
1169
+ if (!isLegacyBaseline(opts.parsed)) return void 0;
1170
+ return {
1171
+ code: "legacy-coverage-baseline",
1172
+ severity: "error",
1173
+ message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
1174
+ file: opts.displayPath,
1175
+ hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
1176
+ fix: {
1177
+ description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
1178
+ edits: [
1179
+ {
1180
+ path: opts.path,
1181
+ start: 0,
1182
+ end: opts.raw.length,
1183
+ replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
1184
+ }
1185
+ ]
1186
+ }
1187
+ };
1188
+ }
1189
+
808
1190
  // src/scanner/scan/page-coverage.ts
809
1191
  function compileRoute(pattern) {
810
1192
  const segs = pattern.split("/").filter(Boolean);
@@ -826,11 +1208,11 @@ function compileRoute(pattern) {
826
1208
  return { test: (url) => re.test(url), specificity };
827
1209
  }
828
1210
  function matchUrlToRoute(url, routePaths) {
829
- const path12 = stripQuery(url);
1211
+ const path13 = stripQuery(url);
830
1212
  let best = null;
831
1213
  for (const rp of routePaths) {
832
1214
  const { test, specificity } = compileRoute(rp);
833
- if (!test(path12)) continue;
1215
+ if (!test(path13)) continue;
834
1216
  if (!best || specificity > best.specificity)
835
1217
  best = { path: rp, specificity };
836
1218
  }
@@ -861,29 +1243,49 @@ function checkPageCoverage(registry, manifest, baseline) {
861
1243
  if (routes.length === 0) return [];
862
1244
  const routePaths = routes.map((r) => r.path);
863
1245
  const captured = manifest.captured ?? [];
864
- const optedOut = /* @__PURE__ */ new Set();
865
- for (const r of routes) {
866
- const page = registry.get("page", r.page);
867
- if (page?.meta?.capture === false) optedOut.add(r.path);
868
- }
869
1246
  const covered = /* @__PURE__ */ new Set();
870
1247
  for (const c of captured) {
871
1248
  const route = routeForCapture(c, routes, routePaths);
872
1249
  if (route) covered.add(route);
873
1250
  }
874
- const baselineSet = new Set(baseline?.uncapturedPages ?? []);
875
1251
  const diagnostics = [];
876
1252
  for (const r of routes) {
877
- if (covered.has(r.path) || optedOut.has(r.path)) continue;
1253
+ const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
1254
+ ACK_ALL
1255
+ );
878
1256
  const loc = registry.get("page", r.page)?.loc;
879
- if (baselineSet.has(r.path)) {
1257
+ if (covered.has(r.path)) {
1258
+ if (ack?.type === "impossible") {
1259
+ diagnostics.push({
1260
+ code: "stale-acknowledgment",
1261
+ severity: "warning",
1262
+ message: `page "${r.page}" acknowledges "*" as ${describeAck(ack)}, but route "${r.path}" is captured`,
1263
+ file: loc?.file,
1264
+ line: loc?.line,
1265
+ entity: { kind: "page", id: r.page },
1266
+ hint: originHint(ack, r.page)
1267
+ });
1268
+ } else if (ack?.type === "backlog") {
1269
+ diagnostics.push({
1270
+ code: "page-captured",
1271
+ severity: "warning",
1272
+ message: `route "${r.path}" is now captured but is still acknowledged as backlog`,
1273
+ file: loc?.file,
1274
+ line: loc?.line,
1275
+ hint: originHint(ack, r.page)
1276
+ });
1277
+ }
1278
+ continue;
1279
+ }
1280
+ if (ack?.type === "impossible") continue;
1281
+ if (ack?.type === "backlog") {
880
1282
  diagnostics.push({
881
1283
  code: "page-backlog",
882
1284
  severity: "info",
883
1285
  message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
884
1286
  file: loc?.file,
885
1287
  line: loc?.line,
886
- 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 }\`.`
1288
+ hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or \u2014 if it can NEVER be captured \u2014 say so with \`acknowledge: { "*": { type: "impossible", reason: "\u2026" } }\` on page "${r.page}".`
887
1289
  });
888
1290
  continue;
889
1291
  }
@@ -894,46 +1296,307 @@ function checkPageCoverage(registry, manifest, baseline) {
894
1296
  file: loc?.file,
895
1297
  line: loc?.line,
896
1298
  entity: { kind: "route", id: r.path },
897
- 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).`
1299
+ hint: `Capture the route in a states spec; or if it can NEVER be captured add \`acknowledge: { "*": { type: "impossible", reason: "why" } }\` to page "${r.page}"; or record it as debt with \`uidex scan --update-baseline\` (which writes \`{ type: "backlog" }\`, visible in the diff for review).`
898
1300
  });
899
1301
  }
900
- for (const b of baselineSet) {
901
- if (!routePaths.includes(b)) {
902
- diagnostics.push({
903
- code: "stale-page-baseline",
904
- severity: "warning",
905
- message: `coverage baseline lists "${b}", which is not a route in the registry`,
906
- hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
907
- });
908
- } else if (covered.has(b) || optedOut.has(b)) {
909
- diagnostics.push({
910
- code: "page-captured",
911
- severity: "warning",
912
- message: `route "${b}" is now captured but is still in the coverage baseline`,
913
- hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
914
- });
915
- }
1302
+ for (const b of Object.keys(baseline?.acknowledge ?? {})) {
1303
+ if (routePaths.includes(b)) continue;
1304
+ diagnostics.push({
1305
+ code: "stale-page-baseline",
1306
+ severity: "warning",
1307
+ message: `coverage baseline acknowledges "${b}", which is not a route in the registry`,
1308
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
1309
+ });
916
1310
  }
917
1311
  return diagnostics;
918
1312
  }
919
- function computePageBaseline(registry, manifest) {
920
- const uncaptured = [];
1313
+ function computePageBacklog(registry, manifest) {
1314
+ const out2 = {};
921
1315
  for (const d of checkPageCoverage(registry, manifest, {
922
- uncapturedPages: []
1316
+ version: 2,
1317
+ acknowledge: {}
923
1318
  })) {
924
- if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
1319
+ if (d.code === "page-no-capture" && d.entity) {
1320
+ out2[d.entity.id] = { [ACK_ALL]: { type: "backlog" } };
1321
+ }
1322
+ }
1323
+ return out2;
1324
+ }
1325
+
1326
+ // src/scanner/scan/workspace.ts
1327
+ var fs6 = __toESM(require("fs"), 1);
1328
+ var path6 = __toESM(require("path"), 1);
1329
+
1330
+ // src/scanner/scan/walk.ts
1331
+ var fs5 = __toESM(require("fs"), 1);
1332
+ var path5 = __toESM(require("path"), 1);
1333
+ var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
1334
+ var BASE_EXCLUDES = [
1335
+ "**/node_modules/**",
1336
+ "**/dist/**",
1337
+ "**/build/**",
1338
+ "**/.next/**",
1339
+ // Exclude the scanner's own generated siblings so a scan never re-ingests its
1340
+ // output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
1341
+ // a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
1342
+ "**/*.gen.ts",
1343
+ "**/*.gen.data.ts",
1344
+ "**/*.gen.locs.ts"
1345
+ ];
1346
+ var TEST_EXCLUDES = [
1347
+ "**/*.test.ts",
1348
+ "**/*.test.tsx",
1349
+ "**/*.spec.ts",
1350
+ "**/*.spec.tsx"
1351
+ ];
1352
+ var DEFAULT_EXCLUDES = [...BASE_EXCLUDES, ...TEST_EXCLUDES];
1353
+ function globToRegExp(glob) {
1354
+ let i = 0;
1355
+ let out2 = "";
1356
+ while (i < glob.length) {
1357
+ const c = glob[i];
1358
+ if (c === "*" && glob[i + 1] === "*") {
1359
+ if (glob[i + 2] === "/") {
1360
+ out2 += "(?:.*/)?";
1361
+ i += 3;
1362
+ continue;
1363
+ }
1364
+ out2 += ".*";
1365
+ i += 2;
1366
+ continue;
1367
+ }
1368
+ if (c === "*") {
1369
+ out2 += "[^/]*";
1370
+ i++;
1371
+ continue;
1372
+ }
1373
+ if (c === "?") {
1374
+ out2 += "[^/]";
1375
+ i++;
1376
+ continue;
1377
+ }
1378
+ if (c === "{") {
1379
+ const end = glob.indexOf("}", i);
1380
+ if (end === -1) {
1381
+ out2 += "\\{";
1382
+ i++;
1383
+ continue;
1384
+ }
1385
+ const parts = glob.slice(i + 1, end).split(",").map((p2) => p2.replace(/[.+^$()|\\]/g, "\\$&"));
1386
+ out2 += `(?:${parts.join("|")})`;
1387
+ i = end + 1;
1388
+ continue;
1389
+ }
1390
+ if (/[.+^$()|\\]/.test(c)) {
1391
+ out2 += "\\" + c;
1392
+ i++;
1393
+ continue;
1394
+ }
1395
+ out2 += c;
1396
+ i++;
1397
+ }
1398
+ return new RegExp(`^${out2}$`);
1399
+ }
1400
+ function toPosix(p2) {
1401
+ return p2.split(path5.sep).join("/");
1402
+ }
1403
+ function matchesAny(rel, patterns) {
1404
+ return patterns.some((g) => globToRegExp(g).test(rel));
1405
+ }
1406
+ function createSourceMatcher(sources, options) {
1407
+ const { cwd, globalExcludes = [], includeTests = false } = options;
1408
+ const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
1409
+ const compiled = sources.map((source) => ({
1410
+ absRoot: path5.resolve(cwd, source.rootDir),
1411
+ includes: source.include?.length ? source.include : DEFAULT_INCLUDES,
1412
+ excludes: [...baseDefaults, ...globalExcludes, ...source.exclude ?? []]
1413
+ }));
1414
+ return (absFile) => {
1415
+ const abs = path5.resolve(absFile);
1416
+ return compiled.some(({ absRoot, includes, excludes }) => {
1417
+ const rel = toPosix(path5.relative(absRoot, abs));
1418
+ if (rel.length === 0 || rel.startsWith("../") || path5.isAbsolute(rel)) {
1419
+ return false;
1420
+ }
1421
+ if (matchesAny(rel, excludes)) return false;
1422
+ return matchesAny(rel, includes);
1423
+ });
1424
+ };
1425
+ }
1426
+ function walk(sources, options) {
1427
+ const { cwd, globalExcludes = [], includeTests = false } = options;
1428
+ const out2 = [];
1429
+ const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
1430
+ for (const source of sources) {
1431
+ const includes = source.include?.length ? source.include : DEFAULT_INCLUDES;
1432
+ const excludes = [
1433
+ ...baseDefaults,
1434
+ ...globalExcludes,
1435
+ ...source.exclude ?? []
1436
+ ];
1437
+ const absRoot = path5.resolve(cwd, source.rootDir);
1438
+ for (const filePath of walkDir(absRoot, absRoot)) {
1439
+ const rel = toPosix(path5.relative(absRoot, filePath));
1440
+ if (matchesAny(rel, excludes)) continue;
1441
+ if (!matchesAny(rel, includes)) continue;
1442
+ let content;
1443
+ try {
1444
+ content = fs5.readFileSync(filePath, "utf8");
1445
+ } catch {
1446
+ continue;
1447
+ }
1448
+ const relFromCwd = toPosix(path5.relative(cwd, filePath));
1449
+ const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
1450
+ out2.push({
1451
+ sourcePath: filePath,
1452
+ relativePath: rel,
1453
+ displayPath,
1454
+ content
1455
+ });
1456
+ }
1457
+ }
1458
+ return out2.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
1459
+ }
1460
+ function* walkDir(root, dir) {
1461
+ let entries;
1462
+ try {
1463
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
1464
+ } catch {
1465
+ return;
1466
+ }
1467
+ for (const entry of entries) {
1468
+ const full = path5.join(dir, entry.name);
1469
+ if (entry.isDirectory()) {
1470
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
1471
+ continue;
1472
+ }
1473
+ yield* walkDir(root, full);
1474
+ } else if (entry.isFile()) {
1475
+ yield full;
1476
+ }
1477
+ }
1478
+ }
1479
+
1480
+ // src/scanner/scan/workspace.ts
1481
+ var WORKSPACE_MARKERS = [
1482
+ "pnpm-workspace.yaml",
1483
+ "pnpm-workspace.yml",
1484
+ "turbo.json",
1485
+ "lerna.json",
1486
+ "nx.json",
1487
+ ".git"
1488
+ ];
1489
+ var MAX_ASCENT = 8;
1490
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
1491
+ function captureKey(kind, entity) {
1492
+ return `${kind ?? ""}|${entity}`;
1493
+ }
1494
+ function hasMarker(dir) {
1495
+ return WORKSPACE_MARKERS.some((m) => fs6.existsSync(path6.join(dir, m)));
1496
+ }
1497
+ function findWorkspaceRoot(configDir) {
1498
+ let dir = path6.resolve(configDir);
1499
+ for (let i = 0; i < MAX_ASCENT; i++) {
1500
+ const parent = path6.dirname(dir);
1501
+ if (parent === dir) return void 0;
1502
+ dir = parent;
1503
+ if (hasMarker(dir)) return dir;
1504
+ }
1505
+ return void 0;
1506
+ }
1507
+ function captureKeys(manifest) {
1508
+ const out2 = /* @__PURE__ */ new Set();
1509
+ for (const c of manifest?.captured ?? []) {
1510
+ out2.add(captureKey(c.kind, c.entity));
1511
+ }
1512
+ return out2;
1513
+ }
1514
+ function readManifest(configDir, config) {
1515
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
1516
+ let raw;
1517
+ try {
1518
+ raw = fs6.readFileSync(path6.resolve(configDir, rel), "utf8");
1519
+ } catch {
1520
+ return void 0;
1521
+ }
1522
+ try {
1523
+ const parsed = JSON.parse(raw);
1524
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
1525
+ const captured = [];
1526
+ for (const entry of parsed.captured) {
1527
+ if (!entry || typeof entry !== "object") continue;
1528
+ const c = entry;
1529
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
1530
+ const rec = { entity: c.entity, state: c.state };
1531
+ if (typeof c.kind === "string") rec.kind = c.kind;
1532
+ captured.push(rec);
1533
+ }
1534
+ return { captured };
1535
+ } catch {
1536
+ return void 0;
925
1537
  }
926
- return { uncapturedPages: uncaptured.sort() };
1538
+ }
1539
+ function toApp(dc) {
1540
+ return {
1541
+ name: path6.basename(dc.configDir),
1542
+ configDir: dc.configDir,
1543
+ configPath: dc.configPath,
1544
+ registers: createSourceMatcher(dc.config.sources, {
1545
+ cwd: dc.configDir,
1546
+ globalExcludes: dc.config.exclude
1547
+ }),
1548
+ captured: captureKeys(readManifest(dc.configDir, dc.config))
1549
+ };
1550
+ }
1551
+ function resolveWorkspace(opts) {
1552
+ const { configDir, config } = opts;
1553
+ if (config.workspace === false) return void 0;
1554
+ const root = typeof config.workspace === "string" ? path6.resolve(configDir, config.workspace) : findWorkspaceRoot(configDir);
1555
+ if (!root) return void 0;
1556
+ const configs = opts.configs ?? discover({ cwd: root, all: true });
1557
+ const self = path6.resolve(configDir, CONFIG_FILENAME);
1558
+ const siblings = configs.filter((dc) => path6.resolve(dc.configPath) !== self).map(toApp);
1559
+ if (siblings.length === 0) return void 0;
1560
+ return { root, siblings };
1561
+ }
1562
+ function capturedElsewhere(workspace, absFile, kind, id) {
1563
+ if (!workspace || !absFile) return void 0;
1564
+ for (const app of workspace.siblings) {
1565
+ if (!app.registers(absFile)) continue;
1566
+ if (app.captured.has(captureKey(kind, id)) || app.captured.has(captureKey(void 0, id))) {
1567
+ return app.name;
1568
+ }
1569
+ }
1570
+ return void 0;
927
1571
  }
928
1572
 
929
1573
  // src/scanner/scan/state-coverage.ts
930
1574
  var STATE_KINDS = ["page", "feature", "widget"];
931
- function declaredStateEntities(registry) {
1575
+ function declaredStateEntities(registry, opts, ungated) {
932
1576
  const out2 = [];
933
1577
  for (const kind of STATE_KINDS) {
934
1578
  for (const e of registry.list(kind)) {
935
1579
  const states = e.meta?.states;
936
1580
  if (!states || states.length === 0) continue;
1581
+ const absFile = e.loc?.file ? opts.resolveSourcePath?.(e.loc.file) : void 0;
1582
+ const ack = entityAcknowledge(
1583
+ e.meta,
1584
+ capturedElsewhere(opts.workspace, absFile, kind, e.id)
1585
+ );
1586
+ if (ack) {
1587
+ if (ack.origin === "workspace") {
1588
+ ungated.push({
1589
+ code: "acknowledged-elsewhere",
1590
+ severity: "info",
1591
+ message: `${kind} "${e.id}" declares states that no capture here produced, but sibling app "${ack.by}" registers the same declaration and captured it (${describeAck(ack)})`,
1592
+ file: e.loc?.file,
1593
+ line: e.loc?.line,
1594
+ entity: { kind, id: e.id },
1595
+ hint: `This is the workspace answering, not a guess: "${ack.by}" sources the declaring file and its manifest has a capture for "${e.id}". Set \`workspace: false\` in .uidex.json to gate it here anyway.`
1596
+ });
1597
+ }
1598
+ continue;
1599
+ }
937
1600
  out2.push({
938
1601
  kind,
939
1602
  id: e.id,
@@ -944,18 +1607,31 @@ function declaredStateEntities(registry) {
944
1607
  }
945
1608
  return out2;
946
1609
  }
947
- function captureMatchesEntity(cap, ent) {
1610
+ function unreliablePageKinds(registry, captured) {
1611
+ const out2 = /* @__PURE__ */ new Set();
1612
+ for (const c of captured) {
1613
+ if (c.kind !== "page") continue;
1614
+ if (registry.get("page", c.entity)) continue;
1615
+ out2.add(c.entity);
1616
+ }
1617
+ return out2;
1618
+ }
1619
+ function captureMatchesEntity(cap, ent, unreliable = EMPTY) {
948
1620
  if (cap.entity !== ent.id) return false;
949
- return cap.kind === void 0 || cap.kind === ent.kind;
1621
+ if (cap.kind === void 0) return true;
1622
+ if (cap.kind === ent.kind) return true;
1623
+ return cap.kind === "page" && unreliable.has(cap.entity);
950
1624
  }
951
- function checkStateCoverage(registry, manifest) {
1625
+ var EMPTY = /* @__PURE__ */ new Set();
1626
+ function checkStateCoverage(registry, manifest, opts = {}) {
952
1627
  const diagnostics = [];
953
- const declared = declaredStateEntities(registry);
954
1628
  const captured = manifest.captured ?? [];
1629
+ const unreliable = unreliablePageKinds(registry, captured);
1630
+ const declared = declaredStateEntities(registry, opts, diagnostics);
955
1631
  for (const ent of declared) {
956
1632
  for (const state of ent.states) {
957
1633
  const hit = captured.some(
958
- (c) => captureMatchesEntity(c, ent) && c.state === state
1634
+ (c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
959
1635
  );
960
1636
  if (hit) continue;
961
1637
  diagnostics.push({
@@ -970,7 +1646,9 @@ function checkStateCoverage(registry, manifest) {
970
1646
  }
971
1647
  }
972
1648
  for (const cap of captured) {
973
- const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
1649
+ const candidates = declared.filter(
1650
+ (ent2) => captureMatchesEntity(cap, ent2, unreliable)
1651
+ );
974
1652
  if (candidates.length === 0) continue;
975
1653
  const declaredSomewhere = candidates.some(
976
1654
  (ent2) => ent2.states.includes(cap.state)
@@ -1014,36 +1692,35 @@ function coreKindsByRoute(registry, manifest) {
1014
1692
  }
1015
1693
  return byRoute;
1016
1694
  }
1017
- function pageWaivers(registry, routePage) {
1018
- return registry.get("page", routePage)?.meta?.waivers ?? {};
1019
- }
1020
- function isOptedOut(registry, page) {
1021
- return page ? registry.get("page", page)?.meta?.capture === false : false;
1022
- }
1023
1695
  function checkStateMatrix(registry, manifest, baseline) {
1024
1696
  const routes = registry.list("route");
1025
1697
  if (routes.length === 0) return [];
1026
1698
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1027
1699
  const captured = coreKindsByRoute(registry, manifest);
1028
- const baselined = baseline?.missingKinds ?? {};
1029
1700
  const diagnostics = [];
1030
1701
  for (const [route, kinds] of captured) {
1031
1702
  const page = pageOf.get(route);
1032
- if (isOptedOut(registry, page)) continue;
1033
- const waivers = page ? pageWaivers(registry, page) : {};
1703
+ const ack = routeAcknowledge(registry, baseline, route, page);
1034
1704
  const loc = page ? registry.get("page", page)?.loc : void 0;
1035
- const baselinedKinds = new Set(baselined[route] ?? []);
1705
+ const all = ack.get(ACK_ALL);
1706
+ if (all) {
1707
+ if (all.type === "impossible") {
1708
+ diagnostics.push(staleDiagnostic(all, route, page, loc));
1709
+ }
1710
+ continue;
1711
+ }
1036
1712
  for (const kind of CORE_KINDS) {
1037
1713
  if (kinds.has(kind)) continue;
1038
- if (waivers[kind]) continue;
1039
- if (baselinedKinds.has(kind)) {
1714
+ const entry = ack.get(kind);
1715
+ if (entry?.type === "impossible") continue;
1716
+ if (entry?.type === "backlog") {
1040
1717
  diagnostics.push({
1041
1718
  code: "matrix-backlog",
1042
1719
  severity: "info",
1043
- message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
1720
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
1044
1721
  file: loc?.file,
1045
1722
  line: loc?.line,
1046
- hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
1723
+ hint: `Capture the "${kind}" state for "${route}", or if the route CANNOT render it change the acknowledgment to \`{ type: "impossible", reason: "why it can't" }\`.`
1047
1724
  });
1048
1725
  continue;
1049
1726
  }
@@ -1054,42 +1731,43 @@ function checkStateMatrix(registry, manifest, baseline) {
1054
1731
  file: loc?.file,
1055
1732
  line: loc?.line,
1056
1733
  entity: { kind: "route", id: route },
1057
- 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).`
1734
+ hint: `Capture the "${kind}" state; or if the route CANNOT render it, add \`acknowledge: { ${kind}: { type: "impossible", reason: "why it can't" } }\` to page "${page}"; or record it as debt with \`uidex scan --update-baseline\` (which writes \`{ type: "backlog" }\`, visible in the diff for review).`
1058
1735
  });
1059
1736
  }
1060
- }
1061
- for (const [route, kinds] of captured) {
1062
- const page = pageOf.get(route);
1063
- if (!page || isOptedOut(registry, page)) continue;
1064
- const waivers = pageWaivers(registry, page);
1065
- const loc = registry.get("page", page)?.loc;
1066
- for (const kind of Object.keys(waivers)) {
1067
- if (kinds.has(kind)) {
1068
- diagnostics.push({
1069
- code: "stale-waiver",
1070
- severity: "warning",
1071
- message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
1072
- file: loc?.file,
1073
- line: loc?.line,
1074
- entity: { kind: "page", id: page },
1075
- hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
1076
- });
1077
- }
1737
+ for (const [scope, entry] of ack) {
1738
+ if (scope === ACK_ALL || !kinds.has(scope)) continue;
1739
+ diagnostics.push(staleDiagnostic(entry, route, page, loc));
1078
1740
  }
1079
1741
  }
1080
1742
  return diagnostics;
1081
1743
  }
1082
- function computeMissingKinds(registry, manifest) {
1744
+ function staleDiagnostic(entry, route, page, loc) {
1745
+ const what = entry.scope === ACK_ALL ? `route "${route}" is captured` : `route "${route}" does render core kind "${entry.scope}"`;
1746
+ return {
1747
+ code: "stale-acknowledgment",
1748
+ severity: "warning",
1749
+ message: `${page ? `page "${page}"` : `route "${route}"`} acknowledges "${entry.scope}" as ${describeAck(entry)}, but ${what}`,
1750
+ file: loc?.file,
1751
+ line: loc?.line,
1752
+ ...page ? { entity: { kind: "page", id: page } } : {},
1753
+ hint: originHint(entry, page)
1754
+ };
1755
+ }
1756
+ function computeMatrixBacklog(registry, manifest) {
1083
1757
  const routes = registry.list("route");
1084
1758
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1085
1759
  const captured = coreKindsByRoute(registry, manifest);
1086
1760
  const out2 = {};
1087
1761
  for (const [route, kinds] of captured) {
1088
1762
  const page = pageOf.get(route);
1089
- if (isOptedOut(registry, page)) continue;
1090
- const waivers = page ? pageWaivers(registry, page) : {};
1091
- const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
1092
- if (missing.length > 0) out2[route] = missing;
1763
+ const ack = routeAcknowledge(registry, void 0, route, page);
1764
+ if (ack.has(ACK_ALL)) continue;
1765
+ const scopes = {};
1766
+ for (const kind of CORE_KINDS) {
1767
+ if (kinds.has(kind) || ack.has(kind)) continue;
1768
+ scopes[kind] = { type: "backlog" };
1769
+ }
1770
+ if (Object.keys(scopes).length > 0) out2[route] = scopes;
1093
1771
  }
1094
1772
  return out2;
1095
1773
  }
@@ -1113,6 +1791,23 @@ function audit(opts) {
1113
1791
  for (const ef of opts.flowExtracted ?? []) {
1114
1792
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
1115
1793
  }
1794
+ for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
1795
+ if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
1796
+ for (const ef of extracted) {
1797
+ for (const m of ef.metadata ?? []) {
1798
+ for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
1799
+ if (!isPlaceholderReason(ack.reason)) continue;
1800
+ diagnostics.push({
1801
+ code: "placeholder-reason",
1802
+ severity: "warning",
1803
+ message: `acknowledge "${scope}" still has the migration's placeholder reason ("${ack.reason}")`,
1804
+ file: ef.file.displayPath,
1805
+ line: m.loc.line,
1806
+ hint: `Replace it with why this can NEVER be captured \u2014 or, if it merely has not been captured yet, change the type to "backlog" (which needs no reason).`
1807
+ });
1808
+ }
1809
+ }
1810
+ }
1116
1811
  if (check && opts.generated !== void 0) {
1117
1812
  const outRel = opts.outputPath ?? config.output;
1118
1813
  const fresh = normalizeForCheck(opts.generated);
@@ -1226,8 +1921,8 @@ function audit(opts) {
1226
1921
  if (typeof m.id !== "string") continue;
1227
1922
  const filePath = ef.file.displayPath;
1228
1923
  const wellKnownName = WELL_KNOWN_FILES[m.kind];
1229
- if (path5.posix.basename(filePath) === wellKnownName) continue;
1230
- const dir = path5.posix.dirname(filePath);
1924
+ if (path7.posix.basename(filePath) === wellKnownName) continue;
1925
+ const dir = path7.posix.dirname(filePath);
1231
1926
  const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
1232
1927
  if (scannedPaths.has(wellKnownPath)) continue;
1233
1928
  const kindLabel = m.kind === "page" ? "Page" : "Feature";
@@ -1441,25 +2136,41 @@ function audit(opts) {
1441
2136
  }
1442
2137
  }
1443
2138
  if (lint && statesEnabled && opts.capturedStates) {
1444
- diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
2139
+ const sourceByDisplay = new Map(
2140
+ files.map((f) => [f.displayPath, f.sourcePath])
2141
+ );
2142
+ diagnostics.push(
2143
+ ...checkStateCoverage(registry, opts.capturedStates, {
2144
+ workspace: opts.workspace,
2145
+ resolveSourcePath: (displayPath) => sourceByDisplay.get(displayPath)
2146
+ })
2147
+ );
1445
2148
  }
1446
2149
  if (lint && pageCoverageEnabled && opts.capturedStates) {
1447
2150
  diagnostics.push(
1448
- ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
2151
+ ...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
1449
2152
  );
1450
2153
  }
2154
+ if (lint && statesEnabled && opts.capturedStates) {
2155
+ diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
2156
+ }
1451
2157
  if (lint && stateMatrixEnabled && opts.capturedStates) {
1452
2158
  diagnostics.push(
1453
- ...checkStateMatrix(registry, opts.capturedStates, {
1454
- missingKinds: opts.pageBaseline?.missingKinds ?? {}
1455
- })
2159
+ ...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
1456
2160
  );
1457
2161
  }
2162
+ const overrides = config.audit?.severity ?? {};
2163
+ const governed = diagnostics.flatMap((d) => {
2164
+ const level = overrides[d.code];
2165
+ if (level === void 0) return [d];
2166
+ if (level === "off") return [];
2167
+ return [{ ...d, severity: level }];
2168
+ });
1458
2169
  const summary = {
1459
- errors: diagnostics.filter((d) => d.severity === "error").length,
1460
- warnings: diagnostics.filter((d) => d.severity === "warning").length
2170
+ errors: governed.filter((d) => d.severity === "error").length,
2171
+ warnings: governed.filter((d) => d.severity === "warning").length
1461
2172
  };
1462
- return { diagnostics, summary };
2173
+ return { diagnostics: governed, summary };
1463
2174
  }
1464
2175
  function lineOfOffset(content, offset) {
1465
2176
  let line = 1;
@@ -1786,7 +2497,31 @@ function emitIdUnion(name, ids) {
1786
2497
  ${body}
1787
2498
  `;
1788
2499
  }
1789
- var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
2500
+ var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
2501
+ function stateEntities(list) {
2502
+ return list.map((e) => ({
2503
+ id: e.id,
2504
+ states: (e.meta?.states ?? []).map((s) => s.name)
2505
+ }));
2506
+ }
2507
+ function emitStateMap(groups) {
2508
+ const lines = ["export interface UidexStates {"];
2509
+ for (const g of groups) {
2510
+ const withStates = g.entities.filter((e) => e.states.length > 0);
2511
+ if (withStates.length === 0) {
2512
+ lines.push(` ${g.kind}: never`);
2513
+ continue;
2514
+ }
2515
+ lines.push(` ${g.kind}: {`);
2516
+ for (const e of [...withStates].sort((a, b) => a.id.localeCompare(b.id))) {
2517
+ const states = [...new Set(e.states)].sort().map((s) => JSON.stringify(s)).join(" | ");
2518
+ lines.push(` ${JSON.stringify(e.id)}: ${states}`);
2519
+ }
2520
+ lines.push(" }");
2521
+ }
2522
+ lines.push("}");
2523
+ return lines.join("\n") + "\n";
2524
+ }
1790
2525
  function emitId(name, ids) {
1791
2526
  if (OPAQUE_ID_UNIONS.has(name)) {
1792
2527
  return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
@@ -1874,6 +2609,14 @@ function emit(opts) {
1874
2609
  );
1875
2610
  t.push(emitId("StateId", [...stateNames]));
1876
2611
  t.push("");
2612
+ t.push("// ---- declared state space (kind \u2192 entity \u2192 its own states) ----");
2613
+ t.push(
2614
+ emitStateMap([
2615
+ { kind: "page", entities: stateEntities(pages) },
2616
+ { kind: "feature", entities: stateEntities(features) },
2617
+ { kind: "widget", entities: stateEntities(widgets) }
2618
+ ])
2619
+ );
1877
2620
  t.push("// ---- authoring-surface shape types ----");
1878
2621
  t.push("export namespace Uidex {");
1879
2622
  t.push(
@@ -1886,7 +2629,20 @@ function emit(opts) {
1886
2629
  t.push(" acceptance?: readonly string[]");
1887
2630
  t.push(" description?: string");
1888
2631
  t.push(" }");
1889
- t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
2632
+ t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
2633
+ t.push(" export interface Impossible {");
2634
+ t.push(' type: "impossible"');
2635
+ t.push(" reason: string");
2636
+ t.push(" }");
2637
+ t.push(" export interface Backlog {");
2638
+ t.push(' type: "backlog"');
2639
+ t.push(" reason?: string");
2640
+ t.push(" }");
2641
+ t.push(" export type Acknowledgment = Impossible | Backlog");
2642
+ t.push(
2643
+ " export type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>"
2644
+ );
2645
+ t.push(' export type ComponentAcknowledge = { "*"?: Acknowledgment }');
1890
2646
  t.push(" export interface Page {");
1891
2647
  t.push(" page: PageId | false");
1892
2648
  t.push(" name?: string");
@@ -1894,8 +2650,7 @@ function emit(opts) {
1894
2650
  t.push(" widgets?: readonly WidgetId[]");
1895
2651
  t.push(" acceptance?: readonly string[]");
1896
2652
  t.push(" states?: readonly (string | State)[]");
1897
- t.push(" capture?: boolean");
1898
- t.push(" waivers?: Waivers");
2653
+ t.push(" acknowledge?: Acknowledge");
1899
2654
  t.push(" description?: string");
1900
2655
  t.push(" }");
1901
2656
  t.push(" export interface Feature {");
@@ -1904,6 +2659,7 @@ function emit(opts) {
1904
2659
  t.push(" features?: readonly FeatureId[]");
1905
2660
  t.push(" acceptance?: readonly string[]");
1906
2661
  t.push(" states?: readonly (string | State)[]");
2662
+ t.push(" acknowledge?: ComponentAcknowledge");
1907
2663
  t.push(" description?: string");
1908
2664
  t.push(" }");
1909
2665
  t.push(" export interface Primitive {");
@@ -1916,6 +2672,7 @@ function emit(opts) {
1916
2672
  t.push(" name?: string");
1917
2673
  t.push(" acceptance?: readonly string[]");
1918
2674
  t.push(" states?: readonly (string | State)[]");
2675
+ t.push(" acknowledge?: ComponentAcknowledge");
1919
2676
  t.push(" description?: string");
1920
2677
  t.push(" }");
1921
2678
  t.push(" export interface Region {");
@@ -1979,10 +2736,10 @@ function emit(opts) {
1979
2736
  }
1980
2737
 
1981
2738
  // src/scanner/scan/ast.ts
1982
- var path6 = __toESM(require("path"), 1);
2739
+ var path8 = __toESM(require("path"), 1);
1983
2740
  var import_oxc_parser = require("oxc-parser");
1984
2741
  function langFor(sourcePath) {
1985
- switch (path6.extname(sourcePath)) {
2742
+ switch (path8.extname(sourcePath)) {
1986
2743
  case ".tsx":
1987
2744
  return "tsx";
1988
2745
  case ".ts":
@@ -2080,26 +2837,41 @@ var ALLOWED_FIELDS = {
2080
2837
  "widgets",
2081
2838
  "acceptance",
2082
2839
  "states",
2840
+ "acknowledge",
2083
2841
  "capture",
2084
2842
  "waivers",
2085
2843
  "description"
2086
2844
  ]),
2087
- // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
2088
- // they are page-only (the gates read them off the page). A feature/widget that
2089
- // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
2845
+ // A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
2846
+ // and a core kind (one cell of its matrix). A feature/widget has no route, so
2847
+ // the core-kind matrix is meaningless there and only `"*"` is accepted —
2848
+ // giving a component a scoped opt-out instead of forcing you to delete its
2849
+ // whole `states` block. `capture` / `waivers` are REMOVED; they stay parseable
2850
+ // only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
2090
2851
  feature: /* @__PURE__ */ new Set([
2091
2852
  "feature",
2092
2853
  "name",
2093
2854
  "features",
2094
2855
  "acceptance",
2095
2856
  "states",
2857
+ "acknowledge",
2858
+ "capture",
2096
2859
  "description"
2097
2860
  ]),
2098
2861
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
2099
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
2862
+ widget: /* @__PURE__ */ new Set([
2863
+ "widget",
2864
+ "name",
2865
+ "acceptance",
2866
+ "states",
2867
+ "acknowledge",
2868
+ "capture",
2869
+ "description"
2870
+ ]),
2100
2871
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
2101
2872
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
2102
2873
  };
2874
+ var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
2103
2875
  var STATE_FIELDS = /* @__PURE__ */ new Set([
2104
2876
  "name",
2105
2877
  "kind",
@@ -2350,7 +3122,8 @@ function objectLit(node, content, p2, pos, span) {
2350
3122
  key,
2351
3123
  value,
2352
3124
  keyPos,
2353
- span: removalSpan(content, prop.start, prop.end)
3125
+ span: removalSpan(content, prop.start, prop.end),
3126
+ propSpan: { start: prop.start, end: prop.end }
2354
3127
  });
2355
3128
  }
2356
3129
  return { kind: "object", entries, pos, span };
@@ -2498,8 +3271,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
2498
3271
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
2499
3272
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
2500
3273
  const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
2501
- const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
2502
- const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
3274
+ const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
3275
+ const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
3276
+ const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
3277
+ const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
2503
3278
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
2504
3279
  const metadata = {
2505
3280
  source: "ts-export",
@@ -2523,15 +3298,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
2523
3298
  metadata.widgetSpans = widgetsField.spans;
2524
3299
  }
2525
3300
  if (states?.length) metadata.states = states;
2526
- if (capture !== void 0) metadata.capture = capture;
2527
- if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
3301
+ if (acknowledge && Object.keys(acknowledge).length > 0) {
3302
+ metadata.acknowledge = acknowledge;
3303
+ }
3304
+ if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
3305
+ if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
3306
+ metadata.legacyWaivers = legacyWaivers;
3307
+ }
2528
3308
  if (notFlow) metadata.notFlow = true;
2529
3309
  if (typeof id === "string" && idValue.kind === "string") {
2530
3310
  metadata.idSpan = idValue.span;
2531
3311
  }
2532
3312
  const fieldSpans = {};
2533
- for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
3313
+ const fieldPropSpans = {};
3314
+ for (const entry of value.entries) {
3315
+ fieldSpans[entry.key] = entry.span;
3316
+ fieldPropSpans[entry.key] = entry.propSpan;
3317
+ }
2534
3318
  metadata.fieldSpans = fieldSpans;
3319
+ metadata.fieldPropSpans = fieldPropSpans;
2535
3320
  return metadata;
2536
3321
  }
2537
3322
  function readIdField(value, kind, fieldName) {
@@ -2561,6 +3346,80 @@ function readIdField(value, kind, fieldName) {
2561
3346
  value.pos
2562
3347
  );
2563
3348
  }
3349
+ function readAcknowledgeField(byKey, isPage) {
3350
+ const entry = byKey.get("acknowledge");
3351
+ if (!entry) return void 0;
3352
+ if (entry.value.kind !== "object") {
3353
+ throw new ExtractError(
3354
+ "uidex-export-invalid-field",
3355
+ '`acknowledge` must be an object mapping a scope (`"*"` or a core kind) to `{ type, reason? }`.',
3356
+ entry.value.pos
3357
+ );
3358
+ }
3359
+ const out2 = {};
3360
+ for (const e of entry.value.entries) {
3361
+ const scopes = isPage ? ACK_SCOPES : COMPONENT_ACK_SCOPES;
3362
+ if (!scopes.includes(e.key)) {
3363
+ throw new ExtractError(
3364
+ "uidex-export-invalid-field",
3365
+ `acknowledge scope "${e.key}" is not valid here; expected ${scopes.map((s) => `"${s}"`).join(", ")}${isPage ? "" : " (core-kind scopes are page-only \u2014 the state matrix is route-scoped)"}.`,
3366
+ e.keyPos
3367
+ );
3368
+ }
3369
+ if (e.value.kind !== "object") {
3370
+ throw new ExtractError(
3371
+ "uidex-export-invalid-field",
3372
+ `acknowledge "${e.key}" must be an object \`{ type, reason? }\`.`,
3373
+ e.value.pos
3374
+ );
3375
+ }
3376
+ const fieldByKey = /* @__PURE__ */ new Map();
3377
+ for (const f of e.value.entries) {
3378
+ if (!ACK_FIELDS.has(f.key)) {
3379
+ throw new ExtractError(
3380
+ "uidex-export-invalid-field",
3381
+ `Unknown field "${f.key}" in an \`acknowledge\` entry. Allowed: ${[...ACK_FIELDS].sort().join(", ")}.`,
3382
+ f.value.pos
3383
+ );
3384
+ }
3385
+ fieldByKey.set(f.key, f);
3386
+ }
3387
+ const type = readStringField(fieldByKey, "type");
3388
+ if (type === void 0) {
3389
+ throw new ExtractError(
3390
+ "uidex-export-invalid-field",
3391
+ `acknowledge "${e.key}" needs a \`type\`: ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")}.`,
3392
+ e.value.pos
3393
+ );
3394
+ }
3395
+ if (type === "captured-elsewhere") {
3396
+ throw new ExtractError(
3397
+ "uidex-export-invalid-field",
3398
+ '`type: "captured-elsewhere"` cannot be authored; the workspace computes it from the sibling app that captured the declaration.',
3399
+ fieldByKey.get("type").value.pos,
3400
+ "Remove the entry \u2014 if a sibling app really captures this, the workspace reports it as `acknowledged-elsewhere` on its own."
3401
+ );
3402
+ }
3403
+ if (type !== "impossible" && type !== "backlog") {
3404
+ throw new ExtractError(
3405
+ "uidex-export-invalid-field",
3406
+ `acknowledge \`type\` must be ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")} (got "${type}").`,
3407
+ fieldByKey.get("type").value.pos
3408
+ );
3409
+ }
3410
+ const reason = readStringField(fieldByKey, "reason");
3411
+ if (type === "impossible" && (reason === void 0 || reason.length === 0)) {
3412
+ throw new ExtractError(
3413
+ "uidex-export-invalid-field",
3414
+ `acknowledge "${e.key}" is \`type: "impossible"\` and needs a non-empty \`reason\` (why this can NEVER be rendered/captured).`,
3415
+ e.value.pos,
3416
+ 'If it merely has not been captured yet, that is `type: "backlog"` \u2014 which needs no reason.'
3417
+ );
3418
+ }
3419
+ out2[e.key] = reason ? { type, reason } : { type };
3420
+ }
3421
+ return out2;
3422
+ }
2564
3423
  function readWaiversField(byKey) {
2565
3424
  const entry = byKey.get("waivers");
2566
3425
  if (!entry) return void 0;
@@ -3266,7 +4125,7 @@ function parseGitHubRef(ref) {
3266
4125
  }
3267
4126
 
3268
4127
  // src/scanner/scan/resolve.ts
3269
- var path8 = __toESM(require("path"), 1);
4128
+ var path9 = __toESM(require("path"), 1);
3270
4129
 
3271
4130
  // src/scanner/scan/routes.ts
3272
4131
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
@@ -3400,136 +4259,6 @@ function pathToId(routePath) {
3400
4259
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3401
4260
  }
3402
4261
 
3403
- // src/scanner/scan/walk.ts
3404
- var fs5 = __toESM(require("fs"), 1);
3405
- var path7 = __toESM(require("path"), 1);
3406
- var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
3407
- var BASE_EXCLUDES = [
3408
- "**/node_modules/**",
3409
- "**/dist/**",
3410
- "**/build/**",
3411
- "**/.next/**",
3412
- // Exclude the scanner's own generated siblings so a scan never re-ingests its
3413
- // output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
3414
- // a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
3415
- "**/*.gen.ts",
3416
- "**/*.gen.data.ts",
3417
- "**/*.gen.locs.ts"
3418
- ];
3419
- var TEST_EXCLUDES = [
3420
- "**/*.test.ts",
3421
- "**/*.test.tsx",
3422
- "**/*.spec.ts",
3423
- "**/*.spec.tsx"
3424
- ];
3425
- var DEFAULT_EXCLUDES = [...BASE_EXCLUDES, ...TEST_EXCLUDES];
3426
- function globToRegExp(glob) {
3427
- let i = 0;
3428
- let out2 = "";
3429
- while (i < glob.length) {
3430
- const c = glob[i];
3431
- if (c === "*" && glob[i + 1] === "*") {
3432
- if (glob[i + 2] === "/") {
3433
- out2 += "(?:.*/)?";
3434
- i += 3;
3435
- continue;
3436
- }
3437
- out2 += ".*";
3438
- i += 2;
3439
- continue;
3440
- }
3441
- if (c === "*") {
3442
- out2 += "[^/]*";
3443
- i++;
3444
- continue;
3445
- }
3446
- if (c === "?") {
3447
- out2 += "[^/]";
3448
- i++;
3449
- continue;
3450
- }
3451
- if (c === "{") {
3452
- const end = glob.indexOf("}", i);
3453
- if (end === -1) {
3454
- out2 += "\\{";
3455
- i++;
3456
- continue;
3457
- }
3458
- const parts = glob.slice(i + 1, end).split(",").map((p2) => p2.replace(/[.+^$()|\\]/g, "\\$&"));
3459
- out2 += `(?:${parts.join("|")})`;
3460
- i = end + 1;
3461
- continue;
3462
- }
3463
- if (/[.+^$()|\\]/.test(c)) {
3464
- out2 += "\\" + c;
3465
- i++;
3466
- continue;
3467
- }
3468
- out2 += c;
3469
- i++;
3470
- }
3471
- return new RegExp(`^${out2}$`);
3472
- }
3473
- function toPosix(p2) {
3474
- return p2.split(path7.sep).join("/");
3475
- }
3476
- function matchesAny(rel, patterns) {
3477
- return patterns.some((g) => globToRegExp(g).test(rel));
3478
- }
3479
- function walk(sources, options) {
3480
- const { cwd, globalExcludes = [], includeTests = false } = options;
3481
- const out2 = [];
3482
- const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
3483
- for (const source of sources) {
3484
- const includes = source.include?.length ? source.include : DEFAULT_INCLUDES;
3485
- const excludes = [
3486
- ...baseDefaults,
3487
- ...globalExcludes,
3488
- ...source.exclude ?? []
3489
- ];
3490
- const absRoot = path7.resolve(cwd, source.rootDir);
3491
- for (const filePath of walkDir(absRoot, absRoot)) {
3492
- const rel = toPosix(path7.relative(absRoot, filePath));
3493
- if (matchesAny(rel, excludes)) continue;
3494
- if (!matchesAny(rel, includes)) continue;
3495
- let content;
3496
- try {
3497
- content = fs5.readFileSync(filePath, "utf8");
3498
- } catch {
3499
- continue;
3500
- }
3501
- const relFromCwd = toPosix(path7.relative(cwd, filePath));
3502
- const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
3503
- out2.push({
3504
- sourcePath: filePath,
3505
- relativePath: rel,
3506
- displayPath,
3507
- content
3508
- });
3509
- }
3510
- }
3511
- return out2.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
3512
- }
3513
- function* walkDir(root, dir) {
3514
- let entries;
3515
- try {
3516
- entries = fs5.readdirSync(dir, { withFileTypes: true });
3517
- } catch {
3518
- return;
3519
- }
3520
- for (const entry of entries) {
3521
- const full = path7.join(dir, entry.name);
3522
- if (entry.isDirectory()) {
3523
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
3524
- continue;
3525
- }
3526
- yield* walkDir(root, full);
3527
- } else if (entry.isFile()) {
3528
- yield full;
3529
- }
3530
- }
3531
- }
3532
-
3533
4262
  // src/scanner/scan/resolve.ts
3534
4263
  var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
3535
4264
  "element",
@@ -3550,7 +4279,7 @@ function kebab(str) {
3550
4279
  return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
3551
4280
  }
3552
4281
  function baseName(file) {
3553
- const b = path8.posix.basename(file);
4282
+ const b = path9.posix.basename(file);
3554
4283
  return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
3555
4284
  }
3556
4285
  function fileMatchesAny(displayPath, patterns) {
@@ -3571,13 +4300,12 @@ function buildMetaFromExport(exp) {
3571
4300
  ...s.description ? { description: s.description } : {}
3572
4301
  }));
3573
4302
  }
3574
- if (exp.capture === false) meta.capture = false;
3575
- if (exp.waivers && Object.keys(exp.waivers).length > 0) {
3576
- meta.waivers = exp.waivers;
4303
+ if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
4304
+ meta.acknowledge = exp.acknowledge;
3577
4305
  }
3578
4306
  return Object.keys(meta).length > 0 ? meta : void 0;
3579
4307
  }
3580
- function resolve3(ctx) {
4308
+ function resolve4(ctx) {
3581
4309
  const conventions = resolveConventions(ctx.config.conventions);
3582
4310
  const registry = createRegistry();
3583
4311
  const diagnostics = [];
@@ -3628,7 +4356,7 @@ function resolve3(ctx) {
3628
4356
  );
3629
4357
  const handledPageFiles = /* @__PURE__ */ new Set();
3630
4358
  for (const route of routes) {
3631
- const routeDir = path8.posix.dirname(route.file);
4359
+ const routeDir = path9.posix.dirname(route.file);
3632
4360
  const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
3633
4361
  const wellKnownExp = exportFor(wellKnownPath, "page");
3634
4362
  const routeExp = exportFor(route.file, "page");
@@ -3682,7 +4410,7 @@ function resolve3(ctx) {
3682
4410
  const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
3683
4411
  if (!dir) continue;
3684
4412
  conventionalFeatureDirs.add(dir);
3685
- const isWellKnown = path8.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
4413
+ const isWellKnown = path9.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
3686
4414
  if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
3687
4415
  const exp = exportFor(ef.file.displayPath, "feature");
3688
4416
  if (exp) {
@@ -3719,7 +4447,7 @@ function resolve3(ctx) {
3719
4447
  } else if (allExports.length > 0) {
3720
4448
  exp = allExports[0].exp;
3721
4449
  }
3722
- const id = exp && typeof exp.id === "string" ? exp.id : path8.posix.basename(dir);
4450
+ const id = exp && typeof exp.id === "string" ? exp.id : path9.posix.basename(dir);
3723
4451
  const meta = exp ? buildMetaFromExport(exp) : void 0;
3724
4452
  const feature = {
3725
4453
  kind: "feature",
@@ -3918,6 +4646,45 @@ function resolve3(ctx) {
3918
4646
  };
3919
4647
  registry.add(element);
3920
4648
  }
4649
+ const ancestorsById = /* @__PURE__ */ new Map();
4650
+ const fileById = /* @__PURE__ */ new Map();
4651
+ for (const a of allAnnotations) {
4652
+ if (!fileById.has(a.id)) fileById.set(a.id, a.file);
4653
+ if (a.ancestors?.length) {
4654
+ const prev = ancestorsById.get(a.id) ?? [];
4655
+ ancestorsById.set(a.id, [...prev, ...a.ancestors.map((x) => x.id)]);
4656
+ }
4657
+ }
4658
+ const ownersByFile = /* @__PURE__ */ new Map();
4659
+ const addOwner = (file, id) => {
4660
+ ownersByFile.set(file, [...ownersByFile.get(file) ?? [], id]);
4661
+ };
4662
+ const featureDirs = [];
4663
+ for (const f of registry.list("feature")) {
4664
+ const loc = f.loc?.file;
4665
+ if (!loc) continue;
4666
+ const dir = loc.endsWith("/") ? loc.slice(0, -1) : path9.posix.dirname(loc);
4667
+ featureDirs.push({ dir, id: f.id });
4668
+ }
4669
+ for (const p2 of registry.list("page")) {
4670
+ if (p2.loc?.file) addOwner(p2.loc.file, p2.id);
4671
+ }
4672
+ const ownersFor = (file) => {
4673
+ const out2 = [...ownersByFile.get(file) ?? []];
4674
+ for (const { dir, id } of featureDirs) {
4675
+ if (file === dir || file.startsWith(`${dir}/`)) out2.push(id);
4676
+ }
4677
+ return out2;
4678
+ };
4679
+ const expandTouches = (ids) => {
4680
+ const out2 = new Set(ids);
4681
+ for (const id of ids) {
4682
+ for (const anc of ancestorsById.get(id) ?? []) out2.add(anc);
4683
+ const file = fileById.get(id);
4684
+ if (file) for (const owner of ownersFor(file)) out2.add(owner);
4685
+ }
4686
+ return [...out2];
4687
+ };
3921
4688
  if (ctx.flowFiles && conventions.flows) {
3922
4689
  for (const ff of ctx.flowFiles) {
3923
4690
  const notFlowExport = (ff.metadata ?? []).find(
@@ -3927,7 +4694,10 @@ function resolve3(ctx) {
3927
4694
  const flowExport = (ff.metadata ?? []).find(
3928
4695
  (m) => m.kind === "flow" && typeof m.id === "string"
3929
4696
  );
3930
- const derived = flowsFromFacts(ff);
4697
+ const derived = flowsFromFacts(ff).map((f) => ({
4698
+ ...f,
4699
+ touches: expandTouches(f.touches)
4700
+ }));
3931
4701
  if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
3932
4702
  const base = derived[0];
3933
4703
  const flow = {
@@ -3996,45 +4766,38 @@ function dedupe(arr) {
3996
4766
  }
3997
4767
 
3998
4768
  // src/scanner/scan/pipeline.ts
3999
- var DEFAULT_STATES_MANIFEST = "uidex-states.json";
4769
+ var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
4000
4770
  var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
4001
- function loadPageBaseline(configDir, config) {
4771
+ function loadCoverageBaseline(configDir, config) {
4002
4772
  const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
4003
- const abs = path9.resolve(configDir, rel);
4773
+ const abs = path10.resolve(configDir, rel);
4004
4774
  let raw;
4005
4775
  try {
4006
- raw = fs6.readFileSync(abs, "utf8");
4776
+ raw = fs7.readFileSync(abs, "utf8");
4007
4777
  } catch {
4008
- return void 0;
4778
+ return {};
4009
4779
  }
4780
+ let parsed;
4010
4781
  try {
4011
- const parsed = JSON.parse(raw);
4012
- if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
4013
- const uncapturedPages = parsed.uncapturedPages.filter(
4014
- (p2) => typeof p2 === "string"
4015
- );
4016
- let missingKinds;
4017
- const mkRaw = parsed.missingKinds;
4018
- if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
4019
- const mk = {};
4020
- for (const [route, kinds] of Object.entries(mkRaw)) {
4021
- if (Array.isArray(kinds)) {
4022
- mk[route] = kinds.filter((k) => typeof k === "string");
4023
- }
4024
- }
4025
- missingKinds = mk;
4026
- }
4027
- return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
4782
+ parsed = JSON.parse(raw);
4028
4783
  } catch {
4029
- return void 0;
4784
+ return {};
4030
4785
  }
4786
+ const legacy = migrateLegacyBaseline({
4787
+ path: abs,
4788
+ raw,
4789
+ parsed,
4790
+ displayPath: rel
4791
+ });
4792
+ if (legacy) return { legacy };
4793
+ return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
4031
4794
  }
4032
4795
  function loadCapturedStates(configDir, config) {
4033
- const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
4034
- const abs = path9.resolve(configDir, rel);
4796
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
4797
+ const abs = path10.resolve(configDir, rel);
4035
4798
  let raw;
4036
4799
  try {
4037
- raw = fs6.readFileSync(abs, "utf8");
4800
+ raw = fs7.readFileSync(abs, "utf8");
4038
4801
  } catch {
4039
4802
  return void 0;
4040
4803
  }
@@ -4053,7 +4816,7 @@ function loadCapturedStates(configDir, config) {
4053
4816
  if (typeof c.route === "string") rec.route = c.route;
4054
4817
  captured.push(rec);
4055
4818
  }
4056
- return { captured };
4819
+ return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
4057
4820
  } catch {
4058
4821
  return void 0;
4059
4822
  }
@@ -4078,7 +4841,7 @@ function runOne(dc, opts) {
4078
4841
  { cwd: configDir, includeTests: true }
4079
4842
  ) : [];
4080
4843
  const extractedFlows = extract(flowFiles);
4081
- const resolved = resolve3({
4844
+ const resolved = resolve4({
4082
4845
  config,
4083
4846
  extracted,
4084
4847
  flowFiles: extractedFlows
@@ -4089,11 +4852,11 @@ function runOne(dc, opts) {
4089
4852
  gitContext
4090
4853
  });
4091
4854
  const typesRel = config.output;
4092
- const typesPath = path9.resolve(configDir, typesRel);
4855
+ const typesPath = path10.resolve(configDir, typesRel);
4093
4856
  const dataRel = dataOutputPath(config.output);
4094
- const dataPath = path9.resolve(configDir, dataRel);
4857
+ const dataPath = path10.resolve(configDir, dataRel);
4095
4858
  const locsRel = locsOutputPath(config.output);
4096
- const locsPath = path9.resolve(configDir, locsRel);
4859
+ const locsPath = path10.resolve(configDir, locsRel);
4097
4860
  let typesOnDisk = null;
4098
4861
  let dataOnDisk = null;
4099
4862
  let locsOnDisk = null;
@@ -4106,9 +4869,11 @@ function runOne(dc, opts) {
4106
4869
  (ef) => (ef.diagnostics?.length ?? 0) > 0
4107
4870
  );
4108
4871
  const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
4109
- const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
4872
+ const loadedBaseline = opts.coverageBaseline ? { baseline: opts.coverageBaseline } : loadCoverageBaseline(configDir, config);
4873
+ const coverageBaseline = loadedBaseline.baseline;
4874
+ const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
4110
4875
  let auditResult;
4111
- if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
4876
+ if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
4112
4877
  auditResult = audit({
4113
4878
  registry: resolved.registry,
4114
4879
  extracted,
@@ -4131,7 +4896,9 @@ function runOne(dc, opts) {
4131
4896
  locsOnDisk,
4132
4897
  locsOutputPath: locsRel,
4133
4898
  capturedStates,
4134
- pageBaseline
4899
+ coverageBaseline,
4900
+ baselineDiagnostic: loadedBaseline.legacy,
4901
+ workspace
4135
4902
  });
4136
4903
  }
4137
4904
  return {
@@ -4152,15 +4919,15 @@ function runOne(dc, opts) {
4152
4919
  }
4153
4920
  function readOrNull(p2) {
4154
4921
  try {
4155
- return fs6.readFileSync(p2, "utf8");
4922
+ return fs7.readFileSync(p2, "utf8");
4156
4923
  } catch {
4157
4924
  return null;
4158
4925
  }
4159
4926
  }
4160
4927
  function writeArtifact(artifact) {
4161
4928
  if (readOrNull(artifact.outputPath) === artifact.generated) return false;
4162
- fs6.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
4163
- fs6.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4929
+ fs7.mkdirSync(path10.dirname(artifact.outputPath), { recursive: true });
4930
+ fs7.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4164
4931
  return true;
4165
4932
  }
4166
4933
  function writeScanResult(result) {
@@ -4169,9 +4936,9 @@ function writeScanResult(result) {
4169
4936
  const wroteLocs = writeArtifact(result.locs);
4170
4937
  return wroteTypes || wroteData || wroteLocs;
4171
4938
  }
4172
- function pageBaselinePath(result) {
4939
+ function coverageBaselinePath(result) {
4173
4940
  const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
4174
- return path9.resolve(result.configDir, rel);
4941
+ return path10.resolve(result.configDir, rel);
4175
4942
  }
4176
4943
 
4177
4944
  // src/scanner/scan/rename.ts
@@ -4330,8 +5097,8 @@ function renameEntity(opts) {
4330
5097
  }
4331
5098
 
4332
5099
  // src/scanner/scan/scaffold.ts
4333
- var fs7 = __toESM(require("fs"), 1);
4334
- var path10 = __toESM(require("path"), 1);
5100
+ var fs8 = __toESM(require("fs"), 1);
5101
+ var path11 = __toESM(require("path"), 1);
4335
5102
  function scaffoldSpec(opts) {
4336
5103
  const {
4337
5104
  registry,
@@ -4347,8 +5114,8 @@ function scaffoldSpec(opts) {
4347
5114
  }
4348
5115
  const criteria = entity.meta?.acceptance ?? [];
4349
5116
  const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
4350
- const outputPath = path10.resolve(outDir, filename);
4351
- if (fs7.existsSync(outputPath) && !force) {
5117
+ const outputPath = path11.resolve(outDir, filename);
5118
+ if (fs8.existsSync(outputPath) && !force) {
4352
5119
  return {
4353
5120
  outputPath,
4354
5121
  written: false,
@@ -4357,8 +5124,8 @@ function scaffoldSpec(opts) {
4357
5124
  };
4358
5125
  }
4359
5126
  const content = renderSpec({ id, criteria, fixtureImport });
4360
- fs7.mkdirSync(path10.dirname(outputPath), { recursive: true });
4361
- fs7.writeFileSync(outputPath, content, "utf8");
5127
+ fs8.mkdirSync(path11.dirname(outputPath), { recursive: true });
5128
+ fs8.writeFileSync(outputPath, content, "utf8");
4362
5129
  return { outputPath, written: true, skipped: false };
4363
5130
  }
4364
5131
  function capitalize(s) {
@@ -4366,25 +5133,27 @@ function capitalize(s) {
4366
5133
  }
4367
5134
  function renderSpec(args) {
4368
5135
  const lines = [];
4369
- lines.push(
4370
- `import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
4371
- );
5136
+ lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
4372
5137
  lines.push("");
4373
5138
  lines.push(
4374
5139
  `test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
4375
5140
  );
4376
- if (args.criteria.length === 0) {
4377
- lines.push(` test("TODO: add acceptance criteria", async () => {`);
4378
- lines.push(` // TODO`);
5141
+ const stub = (title, body) => {
5142
+ lines.push(` test.fixme(${JSON.stringify(title)}, async ({ uidex }) => {`);
5143
+ for (const l of body) lines.push(` ${l}`);
5144
+ lines.push(` void uidex`);
4379
5145
  lines.push(` })`);
5146
+ lines.push("");
5147
+ };
5148
+ if (args.criteria.length === 0) {
5149
+ stub("TODO: add acceptance criteria", [
5150
+ "// Declare `acceptance: [...]` on the entity, then re-run scaffold."
5151
+ ]);
4380
5152
  } else {
4381
5153
  for (const criterion of args.criteria) {
4382
- lines.push(` test(${JSON.stringify(criterion)}, async ({ uidex }) => {`);
4383
- lines.push(` // TODO: implement criterion`);
4384
- lines.push(` void uidex`);
4385
- lines.push(` expect(true).toBe(true)`);
4386
- lines.push(` })`);
4387
- lines.push("");
5154
+ stub(criterion, [
5155
+ "// TODO: drive this criterion, then remove `.fixme` to arm the test."
5156
+ ]);
4388
5157
  }
4389
5158
  }
4390
5159
  lines.push("})");
@@ -4392,6 +5161,37 @@ function renderSpec(args) {
4392
5161
  return lines.join("\n");
4393
5162
  }
4394
5163
 
5164
+ // src/scanner/scan/states-merge.ts
5165
+ function keyOf(c) {
5166
+ return `${c.kind ?? "page"} ${c.entity} ${c.state}`;
5167
+ }
5168
+ function mergeStates(committed, partial) {
5169
+ const byKey = /* @__PURE__ */ new Map();
5170
+ for (const c of committed.captured ?? []) byKey.set(keyOf(c), c);
5171
+ const updated = [];
5172
+ const added = [];
5173
+ for (const c of partial.captured ?? []) {
5174
+ const k = keyOf(c);
5175
+ (byKey.has(k) ? updated : added).push(k);
5176
+ byKey.set(k, c);
5177
+ }
5178
+ const captured = [...byKey.values()].sort(
5179
+ (a, b) => a.entity.localeCompare(b.entity) || a.state.localeCompare(b.state)
5180
+ );
5181
+ return {
5182
+ manifest: {
5183
+ ...committed,
5184
+ captured,
5185
+ // The merged file is only as fresh as its OLDEST evidence: rows carried
5186
+ // over from the committed manifest were not re-verified by this run, so
5187
+ // claiming the partial's newer stamp would defeat the staleness check.
5188
+ ...committed.generatedAt ? { generatedAt: committed.generatedAt } : {}
5189
+ },
5190
+ updated: updated.sort(),
5191
+ added: added.sort()
5192
+ };
5193
+ }
5194
+
4395
5195
  // src/scanner/cli/parse-args.ts
4396
5196
  function parseArgs(args) {
4397
5197
  const positional = [];
@@ -4439,6 +5239,8 @@ async function run(opts) {
4439
5239
  return runScaffold(cwd, positional.slice(1), flags, writer);
4440
5240
  case "rename":
4441
5241
  return runRename(cwd, positional.slice(1), flags, writer);
5242
+ case "states":
5243
+ return runStates(cwd, positional.slice(1), writer);
4442
5244
  case "ai": {
4443
5245
  const result = await runAiCommand({
4444
5246
  cwd,
@@ -4467,22 +5269,23 @@ function helpText2() {
4467
5269
  " scan [flags] Run the scanner pipeline",
4468
5270
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
4469
5271
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
5272
+ " states merge [partial] Fold a filtered run's *.partial.json into the committed manifest",
4470
5273
  " ai <install|uninstall|providers> Manage AI assistant integrations",
4471
5274
  "",
4472
5275
  "Flags:",
4473
5276
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
4474
5277
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
4475
5278
  " --audit Equivalent to --check --lint (read-only)",
4476
- " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4477
- " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
5279
+ " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names, migrate waivers/capture to acknowledge), then rescan and write",
5280
+ " --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
4478
5281
  " --json Emit JSON diagnostics on stdout",
4479
5282
  " --force (scaffold) overwrite existing spec",
4480
5283
  ""
4481
5284
  ].join("\n");
4482
5285
  }
4483
5286
  function runInit(cwd, w) {
4484
- const configPath = path11.join(cwd, CONFIG_FILENAME);
4485
- if (fs8.existsSync(configPath)) {
5287
+ const configPath = path12.join(cwd, CONFIG_FILENAME);
5288
+ if (fs9.existsSync(configPath)) {
4486
5289
  w.err(`.uidex.json already exists at ${configPath}`);
4487
5290
  return w.result(1);
4488
5291
  }
@@ -4491,16 +5294,16 @@ function runInit(cwd, w) {
4491
5294
  sources: [{ rootDir: "src" }],
4492
5295
  output: "src/uidex.gen.ts"
4493
5296
  };
4494
- fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
5297
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4495
5298
  w.out(`Created ${configPath}`);
4496
- const gitignorePath = path11.join(cwd, ".gitignore");
5299
+ const gitignorePath = path12.join(cwd, ".gitignore");
4497
5300
  const entry = "*.gen.*";
4498
- if (fs8.existsSync(gitignorePath)) {
4499
- const existing = fs8.readFileSync(gitignorePath, "utf8");
5301
+ if (fs9.existsSync(gitignorePath)) {
5302
+ const existing = fs9.readFileSync(gitignorePath, "utf8");
4500
5303
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
4501
5304
  if (!hasEntry) {
4502
5305
  const needsNewline = existing.length > 0 && !existing.endsWith("\n");
4503
- fs8.appendFileSync(
5306
+ fs9.appendFileSync(
4504
5307
  gitignorePath,
4505
5308
  `${needsNewline ? "\n" : ""}${entry}
4506
5309
  `,
@@ -4509,7 +5312,7 @@ function runInit(cwd, w) {
4509
5312
  w.out(`Appended ${entry} to ${gitignorePath}`);
4510
5313
  }
4511
5314
  } else {
4512
- fs8.writeFileSync(gitignorePath, `${entry}
5315
+ fs9.writeFileSync(gitignorePath, `${entry}
4513
5316
  `, "utf8");
4514
5317
  w.out(`Created ${gitignorePath} with ${entry}`);
4515
5318
  }
@@ -4548,20 +5351,25 @@ function runScanCommand(cwd, flags, w) {
4548
5351
  if (updateBaseline) {
4549
5352
  for (const r of results) {
4550
5353
  const manifest = r.capturedStates ?? { captured: [] };
4551
- const pageBaseline = computePageBaseline(r.registry, manifest);
4552
- const missingKinds = computeMissingKinds(r.registry, manifest);
4553
- const baseline = {
4554
- ...pageBaseline,
4555
- ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
5354
+ const acknowledge = {};
5355
+ const merge = (from) => {
5356
+ for (const [route, scopes] of Object.entries(from)) {
5357
+ acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
5358
+ }
4556
5359
  };
4557
- const p2 = pageBaselinePath(r);
4558
- fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4559
- const kindGaps = Object.values(missingKinds).reduce(
4560
- (n, ks) => n + ks.length,
5360
+ merge(computePageBacklog(r.registry, manifest));
5361
+ merge(computeMatrixBacklog(r.registry, manifest));
5362
+ const baseline = { version: 2, acknowledge };
5363
+ const p2 = coverageBaselinePath(r);
5364
+ fs9.writeFileSync(p2, serializeCoverageBaseline(baseline));
5365
+ const entries = Object.values(acknowledge);
5366
+ const pages = entries.filter((s) => s["*"]).length;
5367
+ const kindGaps = entries.reduce(
5368
+ (n, s) => n + Object.keys(s).filter((k) => k !== "*").length,
4561
5369
  0
4562
5370
  );
4563
5371
  w.out(
4564
- `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
5372
+ `Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
4565
5373
  );
4566
5374
  }
4567
5375
  return w.result(0);
@@ -4634,7 +5442,7 @@ function runScaffold(cwd, args, flags, w) {
4634
5442
  for (const r of results) {
4635
5443
  const entity = r.registry.get(scaffoldKind, id);
4636
5444
  if (!entity) continue;
4637
- const outDir = path11.resolve(r.configDir, "e2e");
5445
+ const outDir = path12.resolve(r.configDir, "e2e");
4638
5446
  const result = scaffoldSpec({
4639
5447
  registry: r.registry,
4640
5448
  kind: scaffoldKind,
@@ -4655,6 +5463,53 @@ function runScaffold(cwd, args, flags, w) {
4655
5463
  return w.result(1);
4656
5464
  }
4657
5465
  var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
5466
+ function runStates(cwd, args, w) {
5467
+ const [sub, explicitPartial] = args;
5468
+ if (sub !== "merge") {
5469
+ w.err("Usage: uidex states merge [partial-manifest]");
5470
+ return w.result(1);
5471
+ }
5472
+ const configs = discover({ cwd });
5473
+ if (configs.length === 0) {
5474
+ w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
5475
+ return w.result(1);
5476
+ }
5477
+ let merged = 0;
5478
+ for (const { configDir, config } of configs) {
5479
+ const rel = config.statesManifest ?? "uidex-states.json";
5480
+ const target = path12.resolve(configDir, rel);
5481
+ const partial = explicitPartial ? path12.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
5482
+ const committedRaw = readJson(target);
5483
+ const partialRaw = readJson(partial);
5484
+ if (!partialRaw) continue;
5485
+ if (!committedRaw) {
5486
+ w.err(`No committed manifest at ${rel}; nothing to merge into`);
5487
+ return w.result(1);
5488
+ }
5489
+ const result = mergeStates(committedRaw, partialRaw);
5490
+ fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
5491
+ merged++;
5492
+ w.out(
5493
+ `Merged ${path12.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
5494
+ );
5495
+ for (const k of result.added) w.out(` + ${k}`);
5496
+ for (const k of result.updated) w.out(` ~ ${k}`);
5497
+ }
5498
+ if (merged === 0) {
5499
+ w.out("No partial manifest found; nothing to merge.");
5500
+ }
5501
+ return w.result(0);
5502
+ }
5503
+ function readJson(p2) {
5504
+ try {
5505
+ const parsed = JSON.parse(fs9.readFileSync(p2, "utf8"));
5506
+ if (!parsed || typeof parsed !== "object") return null;
5507
+ const m = parsed;
5508
+ return Array.isArray(m.captured) ? m : null;
5509
+ } catch {
5510
+ return null;
5511
+ }
5512
+ }
4658
5513
  function runRename(cwd, args, flags, w) {
4659
5514
  const [kind, oldId, newId] = args;
4660
5515
  if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {