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
@@ -30,7 +30,8 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
30
30
  "audit",
31
31
  "conventions",
32
32
  "statesManifest",
33
- "coverageBaseline"
33
+ "coverageBaseline",
34
+ "workspace"
34
35
  ]);
35
36
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
36
37
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -46,19 +47,64 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
46
47
  "acceptance",
47
48
  "states",
48
49
  "pageCoverage",
49
- "stateMatrix"
50
+ "stateMatrix",
51
+ "severity"
50
52
  ]);
53
+ var DIAGNOSTIC_CODES = [
54
+ "acceptance-uncovered",
55
+ "acknowledged-elsewhere",
56
+ "competing-uidex-export",
57
+ "component-state-uncaptured",
58
+ "core-kind",
59
+ "duplicate-id",
60
+ "dynamic-attr",
61
+ "dynamic-flow-reference",
62
+ "gen-missing",
63
+ "gen-stale",
64
+ "legacy-acknowledge-field",
65
+ "legacy-coverage-baseline",
66
+ "matrix-backlog",
67
+ "missing-element-annotation",
68
+ "missing-state-capture",
69
+ "orphan-state-capture",
70
+ "page-backlog",
71
+ "page-captured",
72
+ "page-no-capture",
73
+ "parse-error",
74
+ "placeholder-reason",
75
+ "prefer-well-known-file",
76
+ "rename",
77
+ "scope-leak",
78
+ "spread-attr",
79
+ "stale-acknowledgment",
80
+ "stale-page-baseline",
81
+ "uidex-export-ambiguous-kind",
82
+ "uidex-export-duplicate-field",
83
+ "uidex-export-duplicate-state",
84
+ "uidex-export-empty-name",
85
+ "uidex-export-invalid-field",
86
+ "uidex-export-invalid-literal",
87
+ "uidex-export-missing-kind",
88
+ "uidex-export-satisfies-mismatch",
89
+ "uidex-export-unknown-field",
90
+ "unknown-reference",
91
+ "widget-id-mismatch",
92
+ "widget-missing-acceptance",
93
+ "widget-missing-dom"
94
+ ];
95
+ var DIAGNOSTIC_CODE_SET = new Set(DIAGNOSTIC_CODES);
96
+ var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
51
97
  function fail(msg) {
52
98
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
53
99
  }
54
- function assertObject(value, path12) {
100
+ function assertObject(value, path13) {
55
101
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
56
- fail(`${path12} must be an object`);
102
+ fail(`${path13} must be an object`);
57
103
  }
58
104
  }
59
- function assertStringArray(value, path12) {
105
+ function assertStringArray(value, path13) {
60
106
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
61
- fail(`${path12} must be a string[]`);
107
+ fail(`${path13} must be a string[]`);
62
108
  }
63
109
  }
64
110
  function validateConfig(raw) {
@@ -112,11 +158,30 @@ function validateConfig(raw) {
112
158
  if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
113
159
  fail(`"coverageBaseline" must be a string`);
114
160
  }
161
+ if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
162
+ fail(`"workspace" must be a path string or false`);
163
+ }
115
164
  if (raw.audit !== void 0) {
116
165
  assertObject(raw.audit, "audit");
117
166
  for (const key of Object.keys(raw.audit)) {
118
167
  if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
119
168
  const v = raw.audit[key];
169
+ if (key === "severity") {
170
+ assertObject(v, "audit.severity");
171
+ for (const [code, level] of Object.entries(v)) {
172
+ if (!DIAGNOSTIC_CODE_SET.has(code)) {
173
+ fail(
174
+ `unknown diagnostic code "${code}" in audit.severity (a typo here would silently leave that gate at its default severity)`
175
+ );
176
+ }
177
+ if (typeof level !== "string" || !SEVERITY_LEVELS.has(level)) {
178
+ fail(
179
+ `audit.severity["${code}"] must be one of "error", "warning", "info", "off"`
180
+ );
181
+ }
182
+ }
183
+ continue;
184
+ }
120
185
  if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
121
186
  }
122
187
  }
@@ -159,7 +224,8 @@ function validateConfig(raw) {
159
224
  audit: raw.audit,
160
225
  conventions: raw.conventions,
161
226
  statesManifest: raw.statesManifest,
162
- coverageBaseline: raw.coverageBaseline
227
+ coverageBaseline: raw.coverageBaseline,
228
+ workspace: raw.workspace
163
229
  };
164
230
  }
165
231
  function parseConfig(json) {
@@ -209,9 +275,11 @@ function tryReadConfig(configPath) {
209
275
  function discover(options = {}) {
210
276
  const cwd = options.cwd ?? process.cwd();
211
277
  const maxDepth = options.maxDepth ?? MAX_DEPTH;
278
+ const all = options.all ?? false;
212
279
  const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
213
- if (rootMatch) return [rootMatch];
280
+ if (rootMatch && !all) return [rootMatch];
214
281
  const results = [];
282
+ if (rootMatch) results.push(rootMatch);
215
283
  const queue = [[cwd, 0]];
216
284
  while (queue.length > 0) {
217
285
  const [dir, depth] = queue.shift();
@@ -315,6 +383,26 @@ function toPosix(p2) {
315
383
  function matchesAny(rel, patterns) {
316
384
  return patterns.some((g) => globToRegExp(g).test(rel));
317
385
  }
386
+ function createSourceMatcher(sources, options) {
387
+ const { cwd, globalExcludes = [], includeTests = false } = options;
388
+ const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
389
+ const compiled = sources.map((source) => ({
390
+ absRoot: path2.resolve(cwd, source.rootDir),
391
+ includes: source.include?.length ? source.include : DEFAULT_INCLUDES,
392
+ excludes: [...baseDefaults, ...globalExcludes, ...source.exclude ?? []]
393
+ }));
394
+ return (absFile) => {
395
+ const abs = path2.resolve(absFile);
396
+ return compiled.some(({ absRoot, includes, excludes }) => {
397
+ const rel = toPosix(path2.relative(absRoot, abs));
398
+ if (rel.length === 0 || rel.startsWith("../") || path2.isAbsolute(rel)) {
399
+ return false;
400
+ }
401
+ if (matchesAny(rel, excludes)) return false;
402
+ return matchesAny(rel, includes);
403
+ });
404
+ };
405
+ }
318
406
  function walk(sources, options) {
319
407
  const { cwd, globalExcludes = [], includeTests = false } = options;
320
408
  const out2 = [];
@@ -648,6 +736,132 @@ function createRegistry() {
648
736
  };
649
737
  }
650
738
 
739
+ // src/scanner/scan/acknowledge.ts
740
+ var ACK_ALL = "*";
741
+ var ACK_SCOPES = [ACK_ALL, ...CORE_STATE_KINDS];
742
+ var COMPONENT_ACK_SCOPES = [ACK_ALL];
743
+ var AUTHORABLE_ACK_TYPES = [
744
+ "impossible",
745
+ "backlog"
746
+ ];
747
+ var CORE_SET = new Set(CORE_STATE_KINDS);
748
+ function isCoreKind(scope) {
749
+ return CORE_SET.has(scope);
750
+ }
751
+ var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
752
+ function isPlaceholderReason(reason) {
753
+ return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
754
+ }
755
+ function describeAck(a) {
756
+ if (a.type === "captured-elsewhere") {
757
+ return a.by ? `captured-elsewhere (${a.by})` : "captured-elsewhere";
758
+ }
759
+ return a.reason ? `${a.type} ("${a.reason}")` : a.type;
760
+ }
761
+ function originHint(a, page) {
762
+ if (a.origin === "baseline") {
763
+ return "Prune it with `uidex scan --update-baseline` so the backlog reflects reality.";
764
+ }
765
+ if (a.origin === "workspace") {
766
+ return "This is computed from the workspace; it resolves itself once the sibling capture changes.";
767
+ }
768
+ const where = page ? ` on page "${page}"` : "";
769
+ return `Remove the \`acknowledge.${a.scope}\` entry${where} \u2014 a capture contradicts it.`;
770
+ }
771
+ function toResolved(map, origin, into) {
772
+ for (const [scope, ack] of Object.entries(map ?? {})) {
773
+ into.set(scope, { ...ack, scope, origin });
774
+ }
775
+ }
776
+ function routeAcknowledge(registry, baseline, route, page) {
777
+ const out2 = /* @__PURE__ */ new Map();
778
+ toResolved(baseline?.acknowledge?.[route], "baseline", out2);
779
+ if (page) {
780
+ toResolved(registry.get("page", page)?.meta?.acknowledge, "source", out2);
781
+ }
782
+ return out2;
783
+ }
784
+ function entityAcknowledge(meta, capturedElsewhereBy) {
785
+ const source = meta?.acknowledge?.[ACK_ALL];
786
+ if (source) return { ...source, scope: ACK_ALL, origin: "source" };
787
+ if (capturedElsewhereBy) {
788
+ return {
789
+ type: "captured-elsewhere",
790
+ by: capturedElsewhereBy,
791
+ scope: ACK_ALL,
792
+ origin: "workspace"
793
+ };
794
+ }
795
+ return void 0;
796
+ }
797
+ function readAck(raw) {
798
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
799
+ const r = raw;
800
+ if (r.type !== "impossible" && r.type !== "backlog") return void 0;
801
+ const out2 = { type: r.type };
802
+ if (typeof r.reason === "string" && r.reason.length > 0) out2.reason = r.reason;
803
+ return out2;
804
+ }
805
+ function parseCoverageBaseline(raw) {
806
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
807
+ const r = raw;
808
+ const ackRaw = r.acknowledge;
809
+ if (!ackRaw || typeof ackRaw !== "object" || Array.isArray(ackRaw))
810
+ return null;
811
+ const acknowledge = {};
812
+ for (const [route, scopesRaw] of Object.entries(
813
+ ackRaw
814
+ )) {
815
+ if (!scopesRaw || typeof scopesRaw !== "object" || Array.isArray(scopesRaw))
816
+ continue;
817
+ const scopes = {};
818
+ for (const [scope, ackVal] of Object.entries(
819
+ scopesRaw
820
+ )) {
821
+ if (scope !== ACK_ALL && !isCoreKind(scope)) continue;
822
+ const ack = readAck(ackVal);
823
+ if (ack) scopes[scope] = ack;
824
+ }
825
+ if (Object.keys(scopes).length > 0) acknowledge[route] = scopes;
826
+ }
827
+ return { version: 2, acknowledge };
828
+ }
829
+ function isLegacyBaseline(raw) {
830
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
831
+ const r = raw;
832
+ if ("acknowledge" in r) return false;
833
+ return "uncapturedPages" in r || "missingKinds" in r;
834
+ }
835
+ function migrateBaseline(legacy) {
836
+ const acknowledge = {};
837
+ const at = (route) => acknowledge[route] ??= {};
838
+ for (const route of legacy.uncapturedPages ?? []) {
839
+ if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
840
+ }
841
+ for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
842
+ if (!Array.isArray(kinds)) continue;
843
+ for (const kind of kinds) {
844
+ if (typeof kind === "string" && isCoreKind(kind)) {
845
+ at(route)[kind] = { type: "backlog" };
846
+ }
847
+ }
848
+ }
849
+ return { version: 2, acknowledge };
850
+ }
851
+ function serializeCoverageBaseline(baseline) {
852
+ const routes = Object.keys(baseline.acknowledge).sort();
853
+ const ordered = {};
854
+ for (const route of routes) {
855
+ const scopes = baseline.acknowledge[route];
856
+ const sorted = {};
857
+ for (const scope of ACK_SCOPES) {
858
+ if (scopes[scope]) sorted[scope] = scopes[scope];
859
+ }
860
+ ordered[route] = sorted;
861
+ }
862
+ return JSON.stringify({ version: 2, acknowledge: ordered }, null, 2) + "\n";
863
+ }
864
+
651
865
  // src/scanner/scan/extract-uidex-export.ts
652
866
  var KIND_DISCRIMINATORS = [
653
867
  "page",
@@ -666,26 +880,41 @@ var ALLOWED_FIELDS = {
666
880
  "widgets",
667
881
  "acceptance",
668
882
  "states",
883
+ "acknowledge",
669
884
  "capture",
670
885
  "waivers",
671
886
  "description"
672
887
  ]),
673
- // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
674
- // they are page-only (the gates read them off the page). A feature/widget that
675
- // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
888
+ // A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
889
+ // and a core kind (one cell of its matrix). A feature/widget has no route, so
890
+ // the core-kind matrix is meaningless there and only `"*"` is accepted —
891
+ // giving a component a scoped opt-out instead of forcing you to delete its
892
+ // whole `states` block. `capture` / `waivers` are REMOVED; they stay parseable
893
+ // only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
676
894
  feature: /* @__PURE__ */ new Set([
677
895
  "feature",
678
896
  "name",
679
897
  "features",
680
898
  "acceptance",
681
899
  "states",
900
+ "acknowledge",
901
+ "capture",
682
902
  "description"
683
903
  ]),
684
904
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
685
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
905
+ widget: /* @__PURE__ */ new Set([
906
+ "widget",
907
+ "name",
908
+ "acceptance",
909
+ "states",
910
+ "acknowledge",
911
+ "capture",
912
+ "description"
913
+ ]),
686
914
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
687
915
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
688
916
  };
917
+ var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
689
918
  var STATE_FIELDS = /* @__PURE__ */ new Set([
690
919
  "name",
691
920
  "kind",
@@ -936,7 +1165,8 @@ function objectLit(node, content, p2, pos, span) {
936
1165
  key,
937
1166
  value,
938
1167
  keyPos,
939
- span: removalSpan(content, prop.start, prop.end)
1168
+ span: removalSpan(content, prop.start, prop.end),
1169
+ propSpan: { start: prop.start, end: prop.end }
940
1170
  });
941
1171
  }
942
1172
  return { kind: "object", entries, pos, span };
@@ -1084,8 +1314,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1084
1314
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1085
1315
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1086
1316
  const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
1087
- const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
1088
- const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
1317
+ const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
1318
+ const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
1319
+ const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
1320
+ const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
1089
1321
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1090
1322
  const metadata = {
1091
1323
  source: "ts-export",
@@ -1109,15 +1341,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1109
1341
  metadata.widgetSpans = widgetsField.spans;
1110
1342
  }
1111
1343
  if (states?.length) metadata.states = states;
1112
- if (capture !== void 0) metadata.capture = capture;
1113
- if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
1344
+ if (acknowledge && Object.keys(acknowledge).length > 0) {
1345
+ metadata.acknowledge = acknowledge;
1346
+ }
1347
+ if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
1348
+ if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
1349
+ metadata.legacyWaivers = legacyWaivers;
1350
+ }
1114
1351
  if (notFlow) metadata.notFlow = true;
1115
1352
  if (typeof id === "string" && idValue.kind === "string") {
1116
1353
  metadata.idSpan = idValue.span;
1117
1354
  }
1118
1355
  const fieldSpans = {};
1119
- for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
1356
+ const fieldPropSpans = {};
1357
+ for (const entry of value.entries) {
1358
+ fieldSpans[entry.key] = entry.span;
1359
+ fieldPropSpans[entry.key] = entry.propSpan;
1360
+ }
1120
1361
  metadata.fieldSpans = fieldSpans;
1362
+ metadata.fieldPropSpans = fieldPropSpans;
1121
1363
  return metadata;
1122
1364
  }
1123
1365
  function readIdField(value, kind, fieldName) {
@@ -1147,6 +1389,80 @@ function readIdField(value, kind, fieldName) {
1147
1389
  value.pos
1148
1390
  );
1149
1391
  }
1392
+ function readAcknowledgeField(byKey, isPage) {
1393
+ const entry = byKey.get("acknowledge");
1394
+ if (!entry) return void 0;
1395
+ if (entry.value.kind !== "object") {
1396
+ throw new ExtractError(
1397
+ "uidex-export-invalid-field",
1398
+ '`acknowledge` must be an object mapping a scope (`"*"` or a core kind) to `{ type, reason? }`.',
1399
+ entry.value.pos
1400
+ );
1401
+ }
1402
+ const out2 = {};
1403
+ for (const e of entry.value.entries) {
1404
+ const scopes = isPage ? ACK_SCOPES : COMPONENT_ACK_SCOPES;
1405
+ if (!scopes.includes(e.key)) {
1406
+ throw new ExtractError(
1407
+ "uidex-export-invalid-field",
1408
+ `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)"}.`,
1409
+ e.keyPos
1410
+ );
1411
+ }
1412
+ if (e.value.kind !== "object") {
1413
+ throw new ExtractError(
1414
+ "uidex-export-invalid-field",
1415
+ `acknowledge "${e.key}" must be an object \`{ type, reason? }\`.`,
1416
+ e.value.pos
1417
+ );
1418
+ }
1419
+ const fieldByKey = /* @__PURE__ */ new Map();
1420
+ for (const f of e.value.entries) {
1421
+ if (!ACK_FIELDS.has(f.key)) {
1422
+ throw new ExtractError(
1423
+ "uidex-export-invalid-field",
1424
+ `Unknown field "${f.key}" in an \`acknowledge\` entry. Allowed: ${[...ACK_FIELDS].sort().join(", ")}.`,
1425
+ f.value.pos
1426
+ );
1427
+ }
1428
+ fieldByKey.set(f.key, f);
1429
+ }
1430
+ const type = readStringField(fieldByKey, "type");
1431
+ if (type === void 0) {
1432
+ throw new ExtractError(
1433
+ "uidex-export-invalid-field",
1434
+ `acknowledge "${e.key}" needs a \`type\`: ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")}.`,
1435
+ e.value.pos
1436
+ );
1437
+ }
1438
+ if (type === "captured-elsewhere") {
1439
+ throw new ExtractError(
1440
+ "uidex-export-invalid-field",
1441
+ '`type: "captured-elsewhere"` cannot be authored; the workspace computes it from the sibling app that captured the declaration.',
1442
+ fieldByKey.get("type").value.pos,
1443
+ "Remove the entry \u2014 if a sibling app really captures this, the workspace reports it as `acknowledged-elsewhere` on its own."
1444
+ );
1445
+ }
1446
+ if (type !== "impossible" && type !== "backlog") {
1447
+ throw new ExtractError(
1448
+ "uidex-export-invalid-field",
1449
+ `acknowledge \`type\` must be ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")} (got "${type}").`,
1450
+ fieldByKey.get("type").value.pos
1451
+ );
1452
+ }
1453
+ const reason = readStringField(fieldByKey, "reason");
1454
+ if (type === "impossible" && (reason === void 0 || reason.length === 0)) {
1455
+ throw new ExtractError(
1456
+ "uidex-export-invalid-field",
1457
+ `acknowledge "${e.key}" is \`type: "impossible"\` and needs a non-empty \`reason\` (why this can NEVER be rendered/captured).`,
1458
+ e.value.pos,
1459
+ 'If it merely has not been captured yet, that is `type: "backlog"` \u2014 which needs no reason.'
1460
+ );
1461
+ }
1462
+ out2[e.key] = reason ? { type, reason } : { type };
1463
+ }
1464
+ return out2;
1465
+ }
1150
1466
  function readWaiversField(byKey) {
1151
1467
  const entry = byKey.get("waivers");
1152
1468
  if (!entry) return void 0;
@@ -1995,9 +2311,8 @@ function buildMetaFromExport(exp) {
1995
2311
  ...s.description ? { description: s.description } : {}
1996
2312
  }));
1997
2313
  }
1998
- if (exp.capture === false) meta.capture = false;
1999
- if (exp.waivers && Object.keys(exp.waivers).length > 0) {
2000
- meta.waivers = exp.waivers;
2314
+ if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
2315
+ meta.acknowledge = exp.acknowledge;
2001
2316
  }
2002
2317
  return Object.keys(meta).length > 0 ? meta : void 0;
2003
2318
  }
@@ -2342,6 +2657,45 @@ function resolve2(ctx) {
2342
2657
  };
2343
2658
  registry.add(element);
2344
2659
  }
2660
+ const ancestorsById = /* @__PURE__ */ new Map();
2661
+ const fileById = /* @__PURE__ */ new Map();
2662
+ for (const a of allAnnotations) {
2663
+ if (!fileById.has(a.id)) fileById.set(a.id, a.file);
2664
+ if (a.ancestors?.length) {
2665
+ const prev = ancestorsById.get(a.id) ?? [];
2666
+ ancestorsById.set(a.id, [...prev, ...a.ancestors.map((x) => x.id)]);
2667
+ }
2668
+ }
2669
+ const ownersByFile = /* @__PURE__ */ new Map();
2670
+ const addOwner = (file, id) => {
2671
+ ownersByFile.set(file, [...ownersByFile.get(file) ?? [], id]);
2672
+ };
2673
+ const featureDirs = [];
2674
+ for (const f of registry.list("feature")) {
2675
+ const loc = f.loc?.file;
2676
+ if (!loc) continue;
2677
+ const dir = loc.endsWith("/") ? loc.slice(0, -1) : path4.posix.dirname(loc);
2678
+ featureDirs.push({ dir, id: f.id });
2679
+ }
2680
+ for (const p2 of registry.list("page")) {
2681
+ if (p2.loc?.file) addOwner(p2.loc.file, p2.id);
2682
+ }
2683
+ const ownersFor = (file) => {
2684
+ const out2 = [...ownersByFile.get(file) ?? []];
2685
+ for (const { dir, id } of featureDirs) {
2686
+ if (file === dir || file.startsWith(`${dir}/`)) out2.push(id);
2687
+ }
2688
+ return out2;
2689
+ };
2690
+ const expandTouches = (ids) => {
2691
+ const out2 = new Set(ids);
2692
+ for (const id of ids) {
2693
+ for (const anc of ancestorsById.get(id) ?? []) out2.add(anc);
2694
+ const file = fileById.get(id);
2695
+ if (file) for (const owner of ownersFor(file)) out2.add(owner);
2696
+ }
2697
+ return [...out2];
2698
+ };
2345
2699
  if (ctx.flowFiles && conventions.flows) {
2346
2700
  for (const ff of ctx.flowFiles) {
2347
2701
  const notFlowExport = (ff.metadata ?? []).find(
@@ -2351,7 +2705,10 @@ function resolve2(ctx) {
2351
2705
  const flowExport = (ff.metadata ?? []).find(
2352
2706
  (m) => m.kind === "flow" && typeof m.id === "string"
2353
2707
  );
2354
- const derived = flowsFromFacts(ff);
2708
+ const derived = flowsFromFacts(ff).map((f) => ({
2709
+ ...f,
2710
+ touches: expandTouches(f.touches)
2711
+ }));
2355
2712
  if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
2356
2713
  const base = derived[0];
2357
2714
  const flow = {
@@ -2420,7 +2777,195 @@ function dedupe(arr) {
2420
2777
  }
2421
2778
 
2422
2779
  // src/scanner/scan/audit.ts
2423
- import * as path5 from "path";
2780
+ import * as path6 from "path";
2781
+
2782
+ // src/scanner/scan/component-coverage.ts
2783
+ var COMPOSED_KINDS = ["widget", "feature"];
2784
+ function composedStateEntities(registry, page) {
2785
+ const meta = registry.get("page", page)?.meta;
2786
+ if (!meta) return [];
2787
+ const out2 = [];
2788
+ for (const kind of COMPOSED_KINDS) {
2789
+ const ids = (kind === "widget" ? meta.widgets : meta.features) ?? [];
2790
+ for (const id of ids) {
2791
+ const e = registry.get(kind, id);
2792
+ if (!e?.meta?.states?.length) continue;
2793
+ if (e.meta.acknowledge?.[ACK_ALL]) continue;
2794
+ out2.push({ kind, id });
2795
+ }
2796
+ }
2797
+ return out2;
2798
+ }
2799
+ function capturedStatesFor(captured, kind, id) {
2800
+ const out2 = /* @__PURE__ */ new Set();
2801
+ for (const c of captured) {
2802
+ if (c.entity !== id) continue;
2803
+ if (c.kind !== void 0 && c.kind !== kind && c.kind !== "page") continue;
2804
+ out2.add(c.state);
2805
+ }
2806
+ return out2;
2807
+ }
2808
+ function checkComponentCoverage(registry, manifest) {
2809
+ const routes = registry.list("route");
2810
+ if (routes.length === 0) return [];
2811
+ const captured = manifest.captured ?? [];
2812
+ const diagnostics = [];
2813
+ for (const route of routes) {
2814
+ if (registry.get("page", route.page)?.meta?.acknowledge?.[ACK_ALL]) continue;
2815
+ for (const { kind, id } of composedStateEntities(registry, route.page)) {
2816
+ const entity = registry.get(kind, id);
2817
+ const declared = entity.meta.states.map((s) => s.name);
2818
+ const shot = capturedStatesFor(captured, kind, id);
2819
+ const missing = declared.filter((s) => !shot.has(s));
2820
+ if (missing.length === 0) continue;
2821
+ diagnostics.push({
2822
+ code: "component-state-uncaptured",
2823
+ severity: "info",
2824
+ message: `route "${route.path}" composes ${kind} "${id}", whose state(s) [${missing.join(", ")}] no capture produced`,
2825
+ file: entity.loc?.file,
2826
+ line: entity.loc?.line,
2827
+ entity: { kind, id },
2828
+ 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}".`
2829
+ });
2830
+ }
2831
+ }
2832
+ return diagnostics;
2833
+ }
2834
+
2835
+ // src/scanner/scan/migrate-acknowledge.ts
2836
+ var CORE = CORE_STATE_KINDS;
2837
+ function orderScopes(scopes) {
2838
+ const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
2839
+ return [...scopes].sort((a, b) => rank(a) - rank(b));
2840
+ }
2841
+ function renderKey(scope) {
2842
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
2843
+ }
2844
+ function renderAck(a) {
2845
+ const parts = [`type: ${JSON.stringify(a.type)}`];
2846
+ if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
2847
+ return `{ ${parts.join(", ")} }`;
2848
+ }
2849
+ function renderAcknowledge(entries, indent) {
2850
+ const inner = indent + " ";
2851
+ const lines = orderScopes(Object.keys(entries)).map(
2852
+ (scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
2853
+ );
2854
+ return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
2855
+ }
2856
+ function indentAt(content, offset) {
2857
+ const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
2858
+ const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
2859
+ return m ? m[0] : "";
2860
+ }
2861
+ function plannedEntries(m) {
2862
+ const entries = {};
2863
+ if (m.legacyCapture === false) {
2864
+ entries[ACK_ALL] = {
2865
+ type: "impossible",
2866
+ reason: m.description || PLACEHOLDER_REASON
2867
+ };
2868
+ }
2869
+ for (const kind of CORE) {
2870
+ const reason = m.legacyWaivers?.[kind];
2871
+ if (reason) entries[kind] = { type: "impossible", reason };
2872
+ }
2873
+ return entries;
2874
+ }
2875
+ function migrateLegacyFields(ef) {
2876
+ const out2 = [];
2877
+ const { content, sourcePath, displayPath } = ef.file;
2878
+ for (const m of ef.metadata ?? []) {
2879
+ const legacy = [];
2880
+ if (m.legacyWaivers) legacy.push("waivers");
2881
+ if (m.legacyCapture !== void 0) legacy.push("capture");
2882
+ if (legacy.length === 0) continue;
2883
+ const fields = legacy.join(" and ");
2884
+ const base = {
2885
+ severity: "error",
2886
+ code: "legacy-acknowledge-field",
2887
+ file: displayPath,
2888
+ line: m.loc.line
2889
+ };
2890
+ if (m.acknowledge) {
2891
+ out2.push({
2892
+ ...base,
2893
+ message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
2894
+ hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
2895
+ });
2896
+ continue;
2897
+ }
2898
+ const entries = plannedEntries(m);
2899
+ const edits = [];
2900
+ 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);
2901
+ if (present.length !== legacy.length) {
2902
+ out2.push({
2903
+ ...base,
2904
+ message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
2905
+ hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
2906
+ });
2907
+ continue;
2908
+ }
2909
+ const [anchor, ...rest] = present;
2910
+ if (Object.keys(entries).length === 0) {
2911
+ edits.push({
2912
+ path: sourcePath,
2913
+ start: anchor.del.start,
2914
+ end: anchor.del.end,
2915
+ replacement: ""
2916
+ });
2917
+ } else {
2918
+ edits.push({
2919
+ path: sourcePath,
2920
+ start: anchor.prop.start,
2921
+ end: anchor.prop.end,
2922
+ replacement: renderAcknowledge(
2923
+ entries,
2924
+ indentAt(content, anchor.prop.start)
2925
+ )
2926
+ });
2927
+ }
2928
+ for (const r of rest) {
2929
+ edits.push({
2930
+ path: sourcePath,
2931
+ start: r.del.start,
2932
+ end: r.del.end,
2933
+ replacement: ""
2934
+ });
2935
+ }
2936
+ out2.push({
2937
+ ...base,
2938
+ message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
2939
+ 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" : ""}.`,
2940
+ fix: {
2941
+ description: `Rewrite \`${fields}\` as \`acknowledge\``,
2942
+ edits
2943
+ }
2944
+ });
2945
+ }
2946
+ return out2;
2947
+ }
2948
+ function migrateLegacyBaseline(opts) {
2949
+ if (!isLegacyBaseline(opts.parsed)) return void 0;
2950
+ return {
2951
+ code: "legacy-coverage-baseline",
2952
+ severity: "error",
2953
+ message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
2954
+ file: opts.displayPath,
2955
+ hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
2956
+ fix: {
2957
+ description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
2958
+ edits: [
2959
+ {
2960
+ path: opts.path,
2961
+ start: 0,
2962
+ end: opts.raw.length,
2963
+ replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
2964
+ }
2965
+ ]
2966
+ }
2967
+ };
2968
+ }
2424
2969
 
2425
2970
  // src/scanner/scan/page-coverage.ts
2426
2971
  function compileRoute(pattern) {
@@ -2443,11 +2988,11 @@ function compileRoute(pattern) {
2443
2988
  return { test: (url) => re.test(url), specificity };
2444
2989
  }
2445
2990
  function matchUrlToRoute(url, routePaths) {
2446
- const path12 = stripQuery(url);
2991
+ const path13 = stripQuery(url);
2447
2992
  let best = null;
2448
2993
  for (const rp of routePaths) {
2449
2994
  const { test, specificity } = compileRoute(rp);
2450
- if (!test(path12)) continue;
2995
+ if (!test(path13)) continue;
2451
2996
  if (!best || specificity > best.specificity)
2452
2997
  best = { path: rp, specificity };
2453
2998
  }
@@ -2478,29 +3023,49 @@ function checkPageCoverage(registry, manifest, baseline) {
2478
3023
  if (routes.length === 0) return [];
2479
3024
  const routePaths = routes.map((r) => r.path);
2480
3025
  const captured = manifest.captured ?? [];
2481
- const optedOut = /* @__PURE__ */ new Set();
2482
- for (const r of routes) {
2483
- const page = registry.get("page", r.page);
2484
- if (page?.meta?.capture === false) optedOut.add(r.path);
2485
- }
2486
3026
  const covered = /* @__PURE__ */ new Set();
2487
3027
  for (const c of captured) {
2488
3028
  const route = routeForCapture(c, routes, routePaths);
2489
3029
  if (route) covered.add(route);
2490
3030
  }
2491
- const baselineSet = new Set(baseline?.uncapturedPages ?? []);
2492
3031
  const diagnostics = [];
2493
3032
  for (const r of routes) {
2494
- if (covered.has(r.path) || optedOut.has(r.path)) continue;
3033
+ const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
3034
+ ACK_ALL
3035
+ );
2495
3036
  const loc = registry.get("page", r.page)?.loc;
2496
- if (baselineSet.has(r.path)) {
3037
+ if (covered.has(r.path)) {
3038
+ if (ack?.type === "impossible") {
3039
+ diagnostics.push({
3040
+ code: "stale-acknowledgment",
3041
+ severity: "warning",
3042
+ message: `page "${r.page}" acknowledges "*" as ${describeAck(ack)}, but route "${r.path}" is captured`,
3043
+ file: loc?.file,
3044
+ line: loc?.line,
3045
+ entity: { kind: "page", id: r.page },
3046
+ hint: originHint(ack, r.page)
3047
+ });
3048
+ } else if (ack?.type === "backlog") {
3049
+ diagnostics.push({
3050
+ code: "page-captured",
3051
+ severity: "warning",
3052
+ message: `route "${r.path}" is now captured but is still acknowledged as backlog`,
3053
+ file: loc?.file,
3054
+ line: loc?.line,
3055
+ hint: originHint(ack, r.page)
3056
+ });
3057
+ }
3058
+ continue;
3059
+ }
3060
+ if (ack?.type === "impossible") continue;
3061
+ if (ack?.type === "backlog") {
2497
3062
  diagnostics.push({
2498
3063
  code: "page-backlog",
2499
3064
  severity: "info",
2500
3065
  message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
2501
3066
  file: loc?.file,
2502
3067
  line: loc?.line,
2503
- 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 }\`.`
3068
+ 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}".`
2504
3069
  });
2505
3070
  continue;
2506
3071
  }
@@ -2511,46 +3076,155 @@ function checkPageCoverage(registry, manifest, baseline) {
2511
3076
  file: loc?.file,
2512
3077
  line: loc?.line,
2513
3078
  entity: { kind: "route", id: r.path },
2514
- 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).`
3079
+ 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).`
2515
3080
  });
2516
3081
  }
2517
- for (const b of baselineSet) {
2518
- if (!routePaths.includes(b)) {
2519
- diagnostics.push({
2520
- code: "stale-page-baseline",
2521
- severity: "warning",
2522
- message: `coverage baseline lists "${b}", which is not a route in the registry`,
2523
- hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
2524
- });
2525
- } else if (covered.has(b) || optedOut.has(b)) {
2526
- diagnostics.push({
2527
- code: "page-captured",
2528
- severity: "warning",
2529
- message: `route "${b}" is now captured but is still in the coverage baseline`,
2530
- hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
2531
- });
2532
- }
3082
+ for (const b of Object.keys(baseline?.acknowledge ?? {})) {
3083
+ if (routePaths.includes(b)) continue;
3084
+ diagnostics.push({
3085
+ code: "stale-page-baseline",
3086
+ severity: "warning",
3087
+ message: `coverage baseline acknowledges "${b}", which is not a route in the registry`,
3088
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
3089
+ });
2533
3090
  }
2534
3091
  return diagnostics;
2535
3092
  }
2536
- function computePageBaseline(registry, manifest) {
2537
- const uncaptured = [];
3093
+ function computePageBacklog(registry, manifest) {
3094
+ const out2 = {};
2538
3095
  for (const d of checkPageCoverage(registry, manifest, {
2539
- uncapturedPages: []
3096
+ version: 2,
3097
+ acknowledge: {}
2540
3098
  })) {
2541
- if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
3099
+ if (d.code === "page-no-capture" && d.entity) {
3100
+ out2[d.entity.id] = { [ACK_ALL]: { type: "backlog" } };
3101
+ }
2542
3102
  }
2543
- return { uncapturedPages: uncaptured.sort() };
3103
+ return out2;
3104
+ }
3105
+
3106
+ // src/scanner/scan/workspace.ts
3107
+ import * as fs3 from "fs";
3108
+ import * as path5 from "path";
3109
+ var WORKSPACE_MARKERS = [
3110
+ "pnpm-workspace.yaml",
3111
+ "pnpm-workspace.yml",
3112
+ "turbo.json",
3113
+ "lerna.json",
3114
+ "nx.json",
3115
+ ".git"
3116
+ ];
3117
+ var MAX_ASCENT = 8;
3118
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
3119
+ function captureKey(kind, entity) {
3120
+ return `${kind ?? ""}|${entity}`;
3121
+ }
3122
+ function hasMarker(dir) {
3123
+ return WORKSPACE_MARKERS.some((m) => fs3.existsSync(path5.join(dir, m)));
3124
+ }
3125
+ function findWorkspaceRoot(configDir) {
3126
+ let dir = path5.resolve(configDir);
3127
+ for (let i = 0; i < MAX_ASCENT; i++) {
3128
+ const parent = path5.dirname(dir);
3129
+ if (parent === dir) return void 0;
3130
+ dir = parent;
3131
+ if (hasMarker(dir)) return dir;
3132
+ }
3133
+ return void 0;
3134
+ }
3135
+ function captureKeys(manifest) {
3136
+ const out2 = /* @__PURE__ */ new Set();
3137
+ for (const c of manifest?.captured ?? []) {
3138
+ out2.add(captureKey(c.kind, c.entity));
3139
+ }
3140
+ return out2;
3141
+ }
3142
+ function readManifest(configDir, config) {
3143
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3144
+ let raw;
3145
+ try {
3146
+ raw = fs3.readFileSync(path5.resolve(configDir, rel), "utf8");
3147
+ } catch {
3148
+ return void 0;
3149
+ }
3150
+ try {
3151
+ const parsed = JSON.parse(raw);
3152
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
3153
+ const captured = [];
3154
+ for (const entry of parsed.captured) {
3155
+ if (!entry || typeof entry !== "object") continue;
3156
+ const c = entry;
3157
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
3158
+ const rec = { entity: c.entity, state: c.state };
3159
+ if (typeof c.kind === "string") rec.kind = c.kind;
3160
+ captured.push(rec);
3161
+ }
3162
+ return { captured };
3163
+ } catch {
3164
+ return void 0;
3165
+ }
3166
+ }
3167
+ function toApp(dc) {
3168
+ return {
3169
+ name: path5.basename(dc.configDir),
3170
+ configDir: dc.configDir,
3171
+ configPath: dc.configPath,
3172
+ registers: createSourceMatcher(dc.config.sources, {
3173
+ cwd: dc.configDir,
3174
+ globalExcludes: dc.config.exclude
3175
+ }),
3176
+ captured: captureKeys(readManifest(dc.configDir, dc.config))
3177
+ };
3178
+ }
3179
+ function resolveWorkspace(opts) {
3180
+ const { configDir, config } = opts;
3181
+ if (config.workspace === false) return void 0;
3182
+ const root = typeof config.workspace === "string" ? path5.resolve(configDir, config.workspace) : findWorkspaceRoot(configDir);
3183
+ if (!root) return void 0;
3184
+ const configs = opts.configs ?? discover({ cwd: root, all: true });
3185
+ const self = path5.resolve(configDir, CONFIG_FILENAME);
3186
+ const siblings = configs.filter((dc) => path5.resolve(dc.configPath) !== self).map(toApp);
3187
+ if (siblings.length === 0) return void 0;
3188
+ return { root, siblings };
3189
+ }
3190
+ function capturedElsewhere(workspace, absFile, kind, id) {
3191
+ if (!workspace || !absFile) return void 0;
3192
+ for (const app of workspace.siblings) {
3193
+ if (!app.registers(absFile)) continue;
3194
+ if (app.captured.has(captureKey(kind, id)) || app.captured.has(captureKey(void 0, id))) {
3195
+ return app.name;
3196
+ }
3197
+ }
3198
+ return void 0;
2544
3199
  }
2545
3200
 
2546
3201
  // src/scanner/scan/state-coverage.ts
2547
3202
  var STATE_KINDS2 = ["page", "feature", "widget"];
2548
- function declaredStateEntities(registry) {
3203
+ function declaredStateEntities(registry, opts, ungated) {
2549
3204
  const out2 = [];
2550
3205
  for (const kind of STATE_KINDS2) {
2551
3206
  for (const e of registry.list(kind)) {
2552
3207
  const states = e.meta?.states;
2553
3208
  if (!states || states.length === 0) continue;
3209
+ const absFile = e.loc?.file ? opts.resolveSourcePath?.(e.loc.file) : void 0;
3210
+ const ack = entityAcknowledge(
3211
+ e.meta,
3212
+ capturedElsewhere(opts.workspace, absFile, kind, e.id)
3213
+ );
3214
+ if (ack) {
3215
+ if (ack.origin === "workspace") {
3216
+ ungated.push({
3217
+ code: "acknowledged-elsewhere",
3218
+ severity: "info",
3219
+ 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)})`,
3220
+ file: e.loc?.file,
3221
+ line: e.loc?.line,
3222
+ entity: { kind, id: e.id },
3223
+ 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.`
3224
+ });
3225
+ }
3226
+ continue;
3227
+ }
2554
3228
  out2.push({
2555
3229
  kind,
2556
3230
  id: e.id,
@@ -2561,18 +3235,31 @@ function declaredStateEntities(registry) {
2561
3235
  }
2562
3236
  return out2;
2563
3237
  }
2564
- function captureMatchesEntity(cap, ent) {
3238
+ function unreliablePageKinds(registry, captured) {
3239
+ const out2 = /* @__PURE__ */ new Set();
3240
+ for (const c of captured) {
3241
+ if (c.kind !== "page") continue;
3242
+ if (registry.get("page", c.entity)) continue;
3243
+ out2.add(c.entity);
3244
+ }
3245
+ return out2;
3246
+ }
3247
+ function captureMatchesEntity(cap, ent, unreliable = EMPTY) {
2565
3248
  if (cap.entity !== ent.id) return false;
2566
- return cap.kind === void 0 || cap.kind === ent.kind;
3249
+ if (cap.kind === void 0) return true;
3250
+ if (cap.kind === ent.kind) return true;
3251
+ return cap.kind === "page" && unreliable.has(cap.entity);
2567
3252
  }
2568
- function checkStateCoverage(registry, manifest) {
3253
+ var EMPTY = /* @__PURE__ */ new Set();
3254
+ function checkStateCoverage(registry, manifest, opts = {}) {
2569
3255
  const diagnostics = [];
2570
- const declared = declaredStateEntities(registry);
2571
3256
  const captured = manifest.captured ?? [];
3257
+ const unreliable = unreliablePageKinds(registry, captured);
3258
+ const declared = declaredStateEntities(registry, opts, diagnostics);
2572
3259
  for (const ent of declared) {
2573
3260
  for (const state of ent.states) {
2574
3261
  const hit = captured.some(
2575
- (c) => captureMatchesEntity(c, ent) && c.state === state
3262
+ (c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
2576
3263
  );
2577
3264
  if (hit) continue;
2578
3265
  diagnostics.push({
@@ -2587,7 +3274,9 @@ function checkStateCoverage(registry, manifest) {
2587
3274
  }
2588
3275
  }
2589
3276
  for (const cap of captured) {
2590
- const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
3277
+ const candidates = declared.filter(
3278
+ (ent2) => captureMatchesEntity(cap, ent2, unreliable)
3279
+ );
2591
3280
  if (candidates.length === 0) continue;
2592
3281
  const declaredSomewhere = candidates.some(
2593
3282
  (ent2) => ent2.states.includes(cap.state)
@@ -2631,36 +3320,35 @@ function coreKindsByRoute(registry, manifest) {
2631
3320
  }
2632
3321
  return byRoute;
2633
3322
  }
2634
- function pageWaivers(registry, routePage) {
2635
- return registry.get("page", routePage)?.meta?.waivers ?? {};
2636
- }
2637
- function isOptedOut(registry, page) {
2638
- return page ? registry.get("page", page)?.meta?.capture === false : false;
2639
- }
2640
3323
  function checkStateMatrix(registry, manifest, baseline) {
2641
3324
  const routes = registry.list("route");
2642
3325
  if (routes.length === 0) return [];
2643
3326
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2644
3327
  const captured = coreKindsByRoute(registry, manifest);
2645
- const baselined = baseline?.missingKinds ?? {};
2646
3328
  const diagnostics = [];
2647
3329
  for (const [route, kinds] of captured) {
2648
3330
  const page = pageOf.get(route);
2649
- if (isOptedOut(registry, page)) continue;
2650
- const waivers = page ? pageWaivers(registry, page) : {};
3331
+ const ack = routeAcknowledge(registry, baseline, route, page);
2651
3332
  const loc = page ? registry.get("page", page)?.loc : void 0;
2652
- const baselinedKinds = new Set(baselined[route] ?? []);
3333
+ const all = ack.get(ACK_ALL);
3334
+ if (all) {
3335
+ if (all.type === "impossible") {
3336
+ diagnostics.push(staleDiagnostic(all, route, page, loc));
3337
+ }
3338
+ continue;
3339
+ }
2653
3340
  for (const kind of CORE_KINDS) {
2654
3341
  if (kinds.has(kind)) continue;
2655
- if (waivers[kind]) continue;
2656
- if (baselinedKinds.has(kind)) {
3342
+ const entry = ack.get(kind);
3343
+ if (entry?.type === "impossible") continue;
3344
+ if (entry?.type === "backlog") {
2657
3345
  diagnostics.push({
2658
3346
  code: "matrix-backlog",
2659
3347
  severity: "info",
2660
- message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
3348
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
2661
3349
  file: loc?.file,
2662
3350
  line: loc?.line,
2663
- hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
3351
+ 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" }\`.`
2664
3352
  });
2665
3353
  continue;
2666
3354
  }
@@ -2671,42 +3359,43 @@ function checkStateMatrix(registry, manifest, baseline) {
2671
3359
  file: loc?.file,
2672
3360
  line: loc?.line,
2673
3361
  entity: { kind: "route", id: route },
2674
- 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).`
3362
+ 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).`
2675
3363
  });
2676
3364
  }
2677
- }
2678
- for (const [route, kinds] of captured) {
2679
- const page = pageOf.get(route);
2680
- if (!page || isOptedOut(registry, page)) continue;
2681
- const waivers = pageWaivers(registry, page);
2682
- const loc = registry.get("page", page)?.loc;
2683
- for (const kind of Object.keys(waivers)) {
2684
- if (kinds.has(kind)) {
2685
- diagnostics.push({
2686
- code: "stale-waiver",
2687
- severity: "warning",
2688
- message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
2689
- file: loc?.file,
2690
- line: loc?.line,
2691
- entity: { kind: "page", id: page },
2692
- hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
2693
- });
2694
- }
3365
+ for (const [scope, entry] of ack) {
3366
+ if (scope === ACK_ALL || !kinds.has(scope)) continue;
3367
+ diagnostics.push(staleDiagnostic(entry, route, page, loc));
2695
3368
  }
2696
3369
  }
2697
3370
  return diagnostics;
2698
3371
  }
2699
- function computeMissingKinds(registry, manifest) {
3372
+ function staleDiagnostic(entry, route, page, loc) {
3373
+ const what = entry.scope === ACK_ALL ? `route "${route}" is captured` : `route "${route}" does render core kind "${entry.scope}"`;
3374
+ return {
3375
+ code: "stale-acknowledgment",
3376
+ severity: "warning",
3377
+ message: `${page ? `page "${page}"` : `route "${route}"`} acknowledges "${entry.scope}" as ${describeAck(entry)}, but ${what}`,
3378
+ file: loc?.file,
3379
+ line: loc?.line,
3380
+ ...page ? { entity: { kind: "page", id: page } } : {},
3381
+ hint: originHint(entry, page)
3382
+ };
3383
+ }
3384
+ function computeMatrixBacklog(registry, manifest) {
2700
3385
  const routes = registry.list("route");
2701
3386
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2702
3387
  const captured = coreKindsByRoute(registry, manifest);
2703
3388
  const out2 = {};
2704
3389
  for (const [route, kinds] of captured) {
2705
3390
  const page = pageOf.get(route);
2706
- if (isOptedOut(registry, page)) continue;
2707
- const waivers = page ? pageWaivers(registry, page) : {};
2708
- const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
2709
- if (missing.length > 0) out2[route] = missing;
3391
+ const ack = routeAcknowledge(registry, void 0, route, page);
3392
+ if (ack.has(ACK_ALL)) continue;
3393
+ const scopes = {};
3394
+ for (const kind of CORE_KINDS) {
3395
+ if (kinds.has(kind) || ack.has(kind)) continue;
3396
+ scopes[kind] = { type: "backlog" };
3397
+ }
3398
+ if (Object.keys(scopes).length > 0) out2[route] = scopes;
2710
3399
  }
2711
3400
  return out2;
2712
3401
  }
@@ -2730,6 +3419,23 @@ function audit(opts) {
2730
3419
  for (const ef of opts.flowExtracted ?? []) {
2731
3420
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
2732
3421
  }
3422
+ for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
3423
+ if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
3424
+ for (const ef of extracted) {
3425
+ for (const m of ef.metadata ?? []) {
3426
+ for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
3427
+ if (!isPlaceholderReason(ack.reason)) continue;
3428
+ diagnostics.push({
3429
+ code: "placeholder-reason",
3430
+ severity: "warning",
3431
+ message: `acknowledge "${scope}" still has the migration's placeholder reason ("${ack.reason}")`,
3432
+ file: ef.file.displayPath,
3433
+ line: m.loc.line,
3434
+ 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).`
3435
+ });
3436
+ }
3437
+ }
3438
+ }
2733
3439
  if (check && opts.generated !== void 0) {
2734
3440
  const outRel = opts.outputPath ?? config.output;
2735
3441
  const fresh = normalizeForCheck(opts.generated);
@@ -2843,8 +3549,8 @@ function audit(opts) {
2843
3549
  if (typeof m.id !== "string") continue;
2844
3550
  const filePath = ef.file.displayPath;
2845
3551
  const wellKnownName = WELL_KNOWN_FILES[m.kind];
2846
- if (path5.posix.basename(filePath) === wellKnownName) continue;
2847
- const dir = path5.posix.dirname(filePath);
3552
+ if (path6.posix.basename(filePath) === wellKnownName) continue;
3553
+ const dir = path6.posix.dirname(filePath);
2848
3554
  const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
2849
3555
  if (scannedPaths.has(wellKnownPath)) continue;
2850
3556
  const kindLabel = m.kind === "page" ? "Page" : "Feature";
@@ -3058,25 +3764,41 @@ function audit(opts) {
3058
3764
  }
3059
3765
  }
3060
3766
  if (lint && statesEnabled && opts.capturedStates) {
3061
- diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
3767
+ const sourceByDisplay = new Map(
3768
+ files.map((f) => [f.displayPath, f.sourcePath])
3769
+ );
3770
+ diagnostics.push(
3771
+ ...checkStateCoverage(registry, opts.capturedStates, {
3772
+ workspace: opts.workspace,
3773
+ resolveSourcePath: (displayPath) => sourceByDisplay.get(displayPath)
3774
+ })
3775
+ );
3062
3776
  }
3063
3777
  if (lint && pageCoverageEnabled && opts.capturedStates) {
3064
3778
  diagnostics.push(
3065
- ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
3779
+ ...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
3066
3780
  );
3067
3781
  }
3782
+ if (lint && statesEnabled && opts.capturedStates) {
3783
+ diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
3784
+ }
3068
3785
  if (lint && stateMatrixEnabled && opts.capturedStates) {
3069
3786
  diagnostics.push(
3070
- ...checkStateMatrix(registry, opts.capturedStates, {
3071
- missingKinds: opts.pageBaseline?.missingKinds ?? {}
3072
- })
3787
+ ...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
3073
3788
  );
3074
3789
  }
3790
+ const overrides = config.audit?.severity ?? {};
3791
+ const governed = diagnostics.flatMap((d) => {
3792
+ const level = overrides[d.code];
3793
+ if (level === void 0) return [d];
3794
+ if (level === "off") return [];
3795
+ return [{ ...d, severity: level }];
3796
+ });
3075
3797
  const summary = {
3076
- errors: diagnostics.filter((d) => d.severity === "error").length,
3077
- warnings: diagnostics.filter((d) => d.severity === "warning").length
3798
+ errors: governed.filter((d) => d.severity === "error").length,
3799
+ warnings: governed.filter((d) => d.severity === "warning").length
3078
3800
  };
3079
- return { diagnostics, summary };
3801
+ return { diagnostics: governed, summary };
3080
3802
  }
3081
3803
  function lineOfOffset(content, offset) {
3082
3804
  let line = 1;
@@ -3404,6 +4126,30 @@ ${body}
3404
4126
  `;
3405
4127
  }
3406
4128
  var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
4129
+ function stateEntities(list) {
4130
+ return list.map((e) => ({
4131
+ id: e.id,
4132
+ states: (e.meta?.states ?? []).map((s) => s.name)
4133
+ }));
4134
+ }
4135
+ function emitStateMap(groups) {
4136
+ const lines = ["export interface UidexStates {"];
4137
+ for (const g of groups) {
4138
+ const withStates = g.entities.filter((e) => e.states.length > 0);
4139
+ if (withStates.length === 0) {
4140
+ lines.push(` ${g.kind}: never`);
4141
+ continue;
4142
+ }
4143
+ lines.push(` ${g.kind}: {`);
4144
+ for (const e of [...withStates].sort((a, b) => a.id.localeCompare(b.id))) {
4145
+ const states = [...new Set(e.states)].sort().map((s) => JSON.stringify(s)).join(" | ");
4146
+ lines.push(` ${JSON.stringify(e.id)}: ${states}`);
4147
+ }
4148
+ lines.push(" }");
4149
+ }
4150
+ lines.push("}");
4151
+ return lines.join("\n") + "\n";
4152
+ }
3407
4153
  function emitId(name, ids) {
3408
4154
  if (OPAQUE_ID_UNIONS.has(name)) {
3409
4155
  return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
@@ -3491,6 +4237,14 @@ function emit(opts) {
3491
4237
  );
3492
4238
  t.push(emitId("StateId", [...stateNames]));
3493
4239
  t.push("");
4240
+ t.push("// ---- declared state space (kind \u2192 entity \u2192 its own states) ----");
4241
+ t.push(
4242
+ emitStateMap([
4243
+ { kind: "page", entities: stateEntities(pages) },
4244
+ { kind: "feature", entities: stateEntities(features) },
4245
+ { kind: "widget", entities: stateEntities(widgets) }
4246
+ ])
4247
+ );
3494
4248
  t.push("// ---- authoring-surface shape types ----");
3495
4249
  t.push("export namespace Uidex {");
3496
4250
  t.push(
@@ -3503,7 +4257,20 @@ function emit(opts) {
3503
4257
  t.push(" acceptance?: readonly string[]");
3504
4258
  t.push(" description?: string");
3505
4259
  t.push(" }");
3506
- t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
4260
+ t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
4261
+ t.push(" export interface Impossible {");
4262
+ t.push(' type: "impossible"');
4263
+ t.push(" reason: string");
4264
+ t.push(" }");
4265
+ t.push(" export interface Backlog {");
4266
+ t.push(' type: "backlog"');
4267
+ t.push(" reason?: string");
4268
+ t.push(" }");
4269
+ t.push(" export type Acknowledgment = Impossible | Backlog");
4270
+ t.push(
4271
+ " export type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>"
4272
+ );
4273
+ t.push(' export type ComponentAcknowledge = { "*"?: Acknowledgment }');
3507
4274
  t.push(" export interface Page {");
3508
4275
  t.push(" page: PageId | false");
3509
4276
  t.push(" name?: string");
@@ -3511,8 +4278,7 @@ function emit(opts) {
3511
4278
  t.push(" widgets?: readonly WidgetId[]");
3512
4279
  t.push(" acceptance?: readonly string[]");
3513
4280
  t.push(" states?: readonly (string | State)[]");
3514
- t.push(" capture?: boolean");
3515
- t.push(" waivers?: Waivers");
4281
+ t.push(" acknowledge?: Acknowledge");
3516
4282
  t.push(" description?: string");
3517
4283
  t.push(" }");
3518
4284
  t.push(" export interface Feature {");
@@ -3521,6 +4287,7 @@ function emit(opts) {
3521
4287
  t.push(" features?: readonly FeatureId[]");
3522
4288
  t.push(" acceptance?: readonly string[]");
3523
4289
  t.push(" states?: readonly (string | State)[]");
4290
+ t.push(" acknowledge?: ComponentAcknowledge");
3524
4291
  t.push(" description?: string");
3525
4292
  t.push(" }");
3526
4293
  t.push(" export interface Primitive {");
@@ -3533,6 +4300,7 @@ function emit(opts) {
3533
4300
  t.push(" name?: string");
3534
4301
  t.push(" acceptance?: readonly string[]");
3535
4302
  t.push(" states?: readonly (string | State)[]");
4303
+ t.push(" acknowledge?: ComponentAcknowledge");
3536
4304
  t.push(" description?: string");
3537
4305
  t.push(" }");
3538
4306
  t.push(" export interface Region {");
@@ -3628,8 +4396,8 @@ function parseGitHubRef(ref) {
3628
4396
  }
3629
4397
 
3630
4398
  // src/scanner/scan/scaffold.ts
3631
- import * as fs3 from "fs";
3632
- import * as path6 from "path";
4399
+ import * as fs4 from "fs";
4400
+ import * as path7 from "path";
3633
4401
  function scaffoldWidgetSpec(opts) {
3634
4402
  return scaffoldSpec({
3635
4403
  registry: opts.registry,
@@ -3655,8 +4423,8 @@ function scaffoldSpec(opts) {
3655
4423
  }
3656
4424
  const criteria = entity.meta?.acceptance ?? [];
3657
4425
  const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
3658
- const outputPath = path6.resolve(outDir, filename);
3659
- if (fs3.existsSync(outputPath) && !force) {
4426
+ const outputPath = path7.resolve(outDir, filename);
4427
+ if (fs4.existsSync(outputPath) && !force) {
3660
4428
  return {
3661
4429
  outputPath,
3662
4430
  written: false,
@@ -3665,8 +4433,8 @@ function scaffoldSpec(opts) {
3665
4433
  };
3666
4434
  }
3667
4435
  const content = renderSpec({ id, criteria, fixtureImport });
3668
- fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
3669
- fs3.writeFileSync(outputPath, content, "utf8");
4436
+ fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
4437
+ fs4.writeFileSync(outputPath, content, "utf8");
3670
4438
  return { outputPath, written: true, skipped: false };
3671
4439
  }
3672
4440
  function capitalize(s) {
@@ -3674,25 +4442,27 @@ function capitalize(s) {
3674
4442
  }
3675
4443
  function renderSpec(args) {
3676
4444
  const lines = [];
3677
- lines.push(
3678
- `import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
3679
- );
4445
+ lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
3680
4446
  lines.push("");
3681
4447
  lines.push(
3682
4448
  `test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
3683
4449
  );
3684
- if (args.criteria.length === 0) {
3685
- lines.push(` test("TODO: add acceptance criteria", async () => {`);
3686
- lines.push(` // TODO`);
4450
+ const stub = (title, body) => {
4451
+ lines.push(` test.fixme(${JSON.stringify(title)}, async ({ uidex }) => {`);
4452
+ for (const l of body) lines.push(` ${l}`);
4453
+ lines.push(` void uidex`);
3687
4454
  lines.push(` })`);
4455
+ lines.push("");
4456
+ };
4457
+ if (args.criteria.length === 0) {
4458
+ stub("TODO: add acceptance criteria", [
4459
+ "// Declare `acceptance: [...]` on the entity, then re-run scaffold."
4460
+ ]);
3688
4461
  } else {
3689
4462
  for (const criterion of args.criteria) {
3690
- lines.push(` test(${JSON.stringify(criterion)}, async ({ uidex }) => {`);
3691
- lines.push(` // TODO: implement criterion`);
3692
- lines.push(` void uidex`);
3693
- lines.push(` expect(true).toBe(true)`);
3694
- lines.push(` })`);
3695
- lines.push("");
4463
+ stub(criterion, [
4464
+ "// TODO: drive this criterion, then remove `.fixme` to arm the test."
4465
+ ]);
3696
4466
  }
3697
4467
  }
3698
4468
  lines.push("})");
@@ -3701,47 +4471,40 @@ function renderSpec(args) {
3701
4471
  }
3702
4472
 
3703
4473
  // src/scanner/scan/pipeline.ts
3704
- import * as fs4 from "fs";
3705
- import * as path7 from "path";
3706
- var DEFAULT_STATES_MANIFEST = "uidex-states.json";
4474
+ import * as fs5 from "fs";
4475
+ import * as path8 from "path";
4476
+ var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
3707
4477
  var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
3708
- function loadPageBaseline(configDir, config) {
4478
+ function loadCoverageBaseline(configDir, config) {
3709
4479
  const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3710
- const abs = path7.resolve(configDir, rel);
4480
+ const abs = path8.resolve(configDir, rel);
3711
4481
  let raw;
3712
4482
  try {
3713
- raw = fs4.readFileSync(abs, "utf8");
4483
+ raw = fs5.readFileSync(abs, "utf8");
3714
4484
  } catch {
3715
- return void 0;
4485
+ return {};
3716
4486
  }
4487
+ let parsed;
3717
4488
  try {
3718
- const parsed = JSON.parse(raw);
3719
- if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
3720
- const uncapturedPages = parsed.uncapturedPages.filter(
3721
- (p2) => typeof p2 === "string"
3722
- );
3723
- let missingKinds;
3724
- const mkRaw = parsed.missingKinds;
3725
- if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
3726
- const mk = {};
3727
- for (const [route, kinds] of Object.entries(mkRaw)) {
3728
- if (Array.isArray(kinds)) {
3729
- mk[route] = kinds.filter((k) => typeof k === "string");
3730
- }
3731
- }
3732
- missingKinds = mk;
3733
- }
3734
- return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
4489
+ parsed = JSON.parse(raw);
3735
4490
  } catch {
3736
- return void 0;
4491
+ return {};
3737
4492
  }
4493
+ const legacy = migrateLegacyBaseline({
4494
+ path: abs,
4495
+ raw,
4496
+ parsed,
4497
+ displayPath: rel
4498
+ });
4499
+ if (legacy) return { legacy };
4500
+ return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
3738
4501
  }
3739
4502
  function loadCapturedStates(configDir, config) {
3740
- const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3741
- const abs = path7.resolve(configDir, rel);
4503
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
4504
+ const abs = path8.resolve(configDir, rel);
3742
4505
  let raw;
3743
4506
  try {
3744
- raw = fs4.readFileSync(abs, "utf8");
4507
+ raw = fs5.readFileSync(abs, "utf8");
3745
4508
  } catch {
3746
4509
  return void 0;
3747
4510
  }
@@ -3760,7 +4523,7 @@ function loadCapturedStates(configDir, config) {
3760
4523
  if (typeof c.route === "string") rec.route = c.route;
3761
4524
  captured.push(rec);
3762
4525
  }
3763
- return { captured };
4526
+ return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
3764
4527
  } catch {
3765
4528
  return void 0;
3766
4529
  }
@@ -3796,11 +4559,11 @@ function runOne(dc, opts) {
3796
4559
  gitContext
3797
4560
  });
3798
4561
  const typesRel = config.output;
3799
- const typesPath = path7.resolve(configDir, typesRel);
4562
+ const typesPath = path8.resolve(configDir, typesRel);
3800
4563
  const dataRel = dataOutputPath(config.output);
3801
- const dataPath = path7.resolve(configDir, dataRel);
4564
+ const dataPath = path8.resolve(configDir, dataRel);
3802
4565
  const locsRel = locsOutputPath(config.output);
3803
- const locsPath = path7.resolve(configDir, locsRel);
4566
+ const locsPath = path8.resolve(configDir, locsRel);
3804
4567
  let typesOnDisk = null;
3805
4568
  let dataOnDisk = null;
3806
4569
  let locsOnDisk = null;
@@ -3813,9 +4576,11 @@ function runOne(dc, opts) {
3813
4576
  (ef) => (ef.diagnostics?.length ?? 0) > 0
3814
4577
  );
3815
4578
  const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
3816
- const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
4579
+ const loadedBaseline = opts.coverageBaseline ? { baseline: opts.coverageBaseline } : loadCoverageBaseline(configDir, config);
4580
+ const coverageBaseline = loadedBaseline.baseline;
4581
+ const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
3817
4582
  let auditResult;
3818
- if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
4583
+ if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
3819
4584
  auditResult = audit({
3820
4585
  registry: resolved.registry,
3821
4586
  extracted,
@@ -3838,7 +4603,9 @@ function runOne(dc, opts) {
3838
4603
  locsOnDisk,
3839
4604
  locsOutputPath: locsRel,
3840
4605
  capturedStates,
3841
- pageBaseline
4606
+ coverageBaseline,
4607
+ baselineDiagnostic: loadedBaseline.legacy,
4608
+ workspace
3842
4609
  });
3843
4610
  }
3844
4611
  return {
@@ -3859,15 +4626,15 @@ function runOne(dc, opts) {
3859
4626
  }
3860
4627
  function readOrNull(p2) {
3861
4628
  try {
3862
- return fs4.readFileSync(p2, "utf8");
4629
+ return fs5.readFileSync(p2, "utf8");
3863
4630
  } catch {
3864
4631
  return null;
3865
4632
  }
3866
4633
  }
3867
4634
  function writeArtifact(artifact) {
3868
4635
  if (readOrNull(artifact.outputPath) === artifact.generated) return false;
3869
- fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
3870
- fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4636
+ fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
4637
+ fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
3871
4638
  return true;
3872
4639
  }
3873
4640
  function writeScanResult(result) {
@@ -3876,14 +4643,45 @@ function writeScanResult(result) {
3876
4643
  const wroteLocs = writeArtifact(result.locs);
3877
4644
  return wroteTypes || wroteData || wroteLocs;
3878
4645
  }
3879
- function pageBaselinePath(result) {
4646
+ function coverageBaselinePath(result) {
3880
4647
  const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3881
- return path7.resolve(result.configDir, rel);
4648
+ return path8.resolve(result.configDir, rel);
4649
+ }
4650
+
4651
+ // src/scanner/scan/states-merge.ts
4652
+ function keyOf(c) {
4653
+ return `${c.kind ?? "page"} ${c.entity} ${c.state}`;
4654
+ }
4655
+ function mergeStates(committed, partial) {
4656
+ const byKey = /* @__PURE__ */ new Map();
4657
+ for (const c of committed.captured ?? []) byKey.set(keyOf(c), c);
4658
+ const updated = [];
4659
+ const added = [];
4660
+ for (const c of partial.captured ?? []) {
4661
+ const k = keyOf(c);
4662
+ (byKey.has(k) ? updated : added).push(k);
4663
+ byKey.set(k, c);
4664
+ }
4665
+ const captured = [...byKey.values()].sort(
4666
+ (a, b) => a.entity.localeCompare(b.entity) || a.state.localeCompare(b.state)
4667
+ );
4668
+ return {
4669
+ manifest: {
4670
+ ...committed,
4671
+ captured,
4672
+ // The merged file is only as fresh as its OLDEST evidence: rows carried
4673
+ // over from the committed manifest were not re-verified by this run, so
4674
+ // claiming the partial's newer stamp would defeat the staleness check.
4675
+ ...committed.generatedAt ? { generatedAt: committed.generatedAt } : {}
4676
+ },
4677
+ updated: updated.sort(),
4678
+ added: added.sort()
4679
+ };
3882
4680
  }
3883
4681
 
3884
4682
  // src/scanner/scan/fix.ts
3885
- import * as fs5 from "fs";
3886
- import * as path8 from "path";
4683
+ import * as fs6 from "fs";
4684
+ import * as path9 from "path";
3887
4685
  function applyFixes(diagnostics) {
3888
4686
  const entries = [];
3889
4687
  for (const d of diagnostics) {
@@ -3916,7 +4714,7 @@ function applyFixes(diagnostics) {
3916
4714
  for (const [filePath, edits] of editsByFile) {
3917
4715
  let content;
3918
4716
  try {
3919
- content = fs5.readFileSync(filePath, "utf8");
4717
+ content = fs6.readFileSync(filePath, "utf8");
3920
4718
  } catch {
3921
4719
  for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
3922
4720
  continue;
@@ -3936,22 +4734,22 @@ function applyFixes(diagnostics) {
3936
4734
  const edit = kept[i];
3937
4735
  content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
3938
4736
  }
3939
- if (kept.length > 0) fs5.writeFileSync(filePath, content, "utf8");
4737
+ if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
3940
4738
  }
3941
4739
  for (const entry of entries) {
3942
4740
  if (entry.skippedReason) continue;
3943
4741
  for (const create of entry.createFiles) {
3944
- if (fs5.existsSync(create.path)) {
3945
- entry.skippedReason = `${path8.basename(create.path)} already exists`;
4742
+ if (fs6.existsSync(create.path)) {
4743
+ entry.skippedReason = `${path9.basename(create.path)} already exists`;
3946
4744
  continue;
3947
4745
  }
3948
- fs5.mkdirSync(path8.dirname(create.path), { recursive: true });
3949
- fs5.writeFileSync(create.path, create.content, "utf8");
4746
+ fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
4747
+ fs6.writeFileSync(create.path, create.content, "utf8");
3950
4748
  }
3951
4749
  if (entry.skippedReason) continue;
3952
4750
  for (const del of entry.deleteFiles) {
3953
4751
  try {
3954
- fs5.unlinkSync(del);
4752
+ fs6.unlinkSync(del);
3955
4753
  } catch {
3956
4754
  }
3957
4755
  }
@@ -4129,29 +4927,29 @@ function renameEntity(opts) {
4129
4927
  }
4130
4928
 
4131
4929
  // src/scanner/scan/cli.ts
4132
- import * as fs8 from "fs";
4133
- import * as path11 from "path";
4930
+ import * as fs9 from "fs";
4931
+ import * as path12 from "path";
4134
4932
 
4135
4933
  // src/scanner/scan/ai/index.ts
4136
4934
  import * as p from "@clack/prompts";
4137
4935
 
4138
4936
  // src/scanner/scan/ai/providers/claude.ts
4139
- import * as fs7 from "fs";
4140
- import * as path10 from "path";
4937
+ import * as fs8 from "fs";
4938
+ import * as path11 from "path";
4141
4939
 
4142
4940
  // src/scanner/scan/ai/templates.ts
4143
- import * as fs6 from "fs";
4144
- import * as path9 from "path";
4941
+ import * as fs7 from "fs";
4942
+ import * as path10 from "path";
4145
4943
  function templatePath(rel) {
4146
4944
  const candidates = [
4147
- path9.resolve(__dirname, "../../templates", rel),
4945
+ path10.resolve(__dirname, "../../templates", rel),
4148
4946
  // dist/cli/cli.cjs → ../../templates
4149
- path9.resolve(__dirname, "../../../../templates", rel)
4947
+ path10.resolve(__dirname, "../../../../templates", rel)
4150
4948
  // src/scanner/scan/ai → ../../../../templates
4151
4949
  ];
4152
4950
  for (const c of candidates) {
4153
4951
  try {
4154
- fs6.accessSync(c, fs6.constants.R_OK);
4952
+ fs7.accessSync(c, fs7.constants.R_OK);
4155
4953
  return c;
4156
4954
  } catch {
4157
4955
  continue;
@@ -4163,7 +4961,7 @@ function templatePath(rel) {
4163
4961
  );
4164
4962
  }
4165
4963
  function readTemplate(rel) {
4166
- return fs6.readFileSync(templatePath(rel), "utf8");
4964
+ return fs7.readFileSync(templatePath(rel), "utf8");
4167
4965
  }
4168
4966
 
4169
4967
  // src/scanner/scan/ai/providers/claude.ts
@@ -4194,8 +4992,8 @@ var claudeProvider = {
4194
4992
  async install({ cwd, force }) {
4195
4993
  const changes = [];
4196
4994
  for (const file of SKILL_FILES) {
4197
- const dest = path10.join(cwd, file.dest);
4198
- const exists = fs7.existsSync(dest);
4995
+ const dest = path11.join(cwd, file.dest);
4996
+ const exists = fs8.existsSync(dest);
4199
4997
  if (exists && !force) {
4200
4998
  changes.push({
4201
4999
  path: file.dest,
@@ -4204,56 +5002,56 @@ var claudeProvider = {
4204
5002
  });
4205
5003
  continue;
4206
5004
  }
4207
- fs7.mkdirSync(path10.dirname(dest), { recursive: true });
4208
- fs7.writeFileSync(dest, readTemplate(file.template));
5005
+ fs8.mkdirSync(path11.dirname(dest), { recursive: true });
5006
+ fs8.writeFileSync(dest, readTemplate(file.template));
4209
5007
  changes.push({
4210
5008
  path: file.dest,
4211
5009
  action: exists ? "overwritten" : "created"
4212
5010
  });
4213
5011
  }
4214
5012
  for (const rel of LEGACY_FILES) {
4215
- const dest = path10.join(cwd, rel);
4216
- if (fs7.existsSync(dest)) {
4217
- fs7.unlinkSync(dest);
5013
+ const dest = path11.join(cwd, rel);
5014
+ if (fs8.existsSync(dest)) {
5015
+ fs8.unlinkSync(dest);
4218
5016
  changes.push({ path: rel, action: "removed" });
4219
5017
  }
4220
5018
  }
4221
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4222
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4223
- cleanupEmpty(path10.join(cwd, ".claude/rules"));
5019
+ cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
5020
+ cleanupEmpty(path11.join(cwd, ".claude/commands"));
5021
+ cleanupEmpty(path11.join(cwd, ".claude/rules"));
4224
5022
  return { changes };
4225
5023
  },
4226
5024
  async uninstall({ cwd }) {
4227
5025
  const changes = [];
4228
5026
  for (const file of SKILL_FILES) {
4229
- const dest = path10.join(cwd, file.dest);
4230
- if (!fs7.existsSync(dest)) {
5027
+ const dest = path11.join(cwd, file.dest);
5028
+ if (!fs8.existsSync(dest)) {
4231
5029
  changes.push({ path: file.dest, action: "skipped", reason: "absent" });
4232
5030
  continue;
4233
5031
  }
4234
- fs7.unlinkSync(dest);
5032
+ fs8.unlinkSync(dest);
4235
5033
  changes.push({ path: file.dest, action: "removed" });
4236
5034
  }
4237
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex/references"));
4238
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex"));
4239
- cleanupEmpty(path10.join(cwd, ".claude/skills"));
5035
+ cleanupEmpty(path11.join(cwd, ".claude/skills/uidex/references"));
5036
+ cleanupEmpty(path11.join(cwd, ".claude/skills/uidex"));
5037
+ cleanupEmpty(path11.join(cwd, ".claude/skills"));
4240
5038
  for (const rel of LEGACY_FILES) {
4241
- const dest = path10.join(cwd, rel);
4242
- if (fs7.existsSync(dest)) {
4243
- fs7.unlinkSync(dest);
5039
+ const dest = path11.join(cwd, rel);
5040
+ if (fs8.existsSync(dest)) {
5041
+ fs8.unlinkSync(dest);
4244
5042
  changes.push({ path: rel, action: "removed" });
4245
5043
  }
4246
5044
  }
4247
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4248
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4249
- cleanupEmpty(path10.join(cwd, ".claude/rules"));
5045
+ cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
5046
+ cleanupEmpty(path11.join(cwd, ".claude/commands"));
5047
+ cleanupEmpty(path11.join(cwd, ".claude/rules"));
4250
5048
  return { changes };
4251
5049
  }
4252
5050
  };
4253
5051
  function cleanupEmpty(dir) {
4254
5052
  try {
4255
- const entries = fs7.readdirSync(dir);
4256
- if (entries.length === 0) fs7.rmdirSync(dir);
5053
+ const entries = fs8.readdirSync(dir);
5054
+ if (entries.length === 0) fs8.rmdirSync(dir);
4257
5055
  } catch {
4258
5056
  }
4259
5057
  }
@@ -4422,6 +5220,8 @@ async function run(opts) {
4422
5220
  return runScaffold(cwd, positional.slice(1), flags, writer);
4423
5221
  case "rename":
4424
5222
  return runRename(cwd, positional.slice(1), flags, writer);
5223
+ case "states":
5224
+ return runStates(cwd, positional.slice(1), writer);
4425
5225
  case "ai": {
4426
5226
  const result = await runAiCommand({
4427
5227
  cwd,
@@ -4450,22 +5250,23 @@ function helpText2() {
4450
5250
  " scan [flags] Run the scanner pipeline",
4451
5251
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
4452
5252
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
5253
+ " states merge [partial] Fold a filtered run's *.partial.json into the committed manifest",
4453
5254
  " ai <install|uninstall|providers> Manage AI assistant integrations",
4454
5255
  "",
4455
5256
  "Flags:",
4456
5257
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
4457
5258
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
4458
5259
  " --audit Equivalent to --check --lint (read-only)",
4459
- " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4460
- " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
5260
+ " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names, migrate waivers/capture to acknowledge), then rescan and write",
5261
+ " --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
4461
5262
  " --json Emit JSON diagnostics on stdout",
4462
5263
  " --force (scaffold) overwrite existing spec",
4463
5264
  ""
4464
5265
  ].join("\n");
4465
5266
  }
4466
5267
  function runInit(cwd, w) {
4467
- const configPath = path11.join(cwd, CONFIG_FILENAME);
4468
- if (fs8.existsSync(configPath)) {
5268
+ const configPath = path12.join(cwd, CONFIG_FILENAME);
5269
+ if (fs9.existsSync(configPath)) {
4469
5270
  w.err(`.uidex.json already exists at ${configPath}`);
4470
5271
  return w.result(1);
4471
5272
  }
@@ -4474,16 +5275,16 @@ function runInit(cwd, w) {
4474
5275
  sources: [{ rootDir: "src" }],
4475
5276
  output: "src/uidex.gen.ts"
4476
5277
  };
4477
- fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
5278
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4478
5279
  w.out(`Created ${configPath}`);
4479
- const gitignorePath = path11.join(cwd, ".gitignore");
5280
+ const gitignorePath = path12.join(cwd, ".gitignore");
4480
5281
  const entry = "*.gen.*";
4481
- if (fs8.existsSync(gitignorePath)) {
4482
- const existing = fs8.readFileSync(gitignorePath, "utf8");
5282
+ if (fs9.existsSync(gitignorePath)) {
5283
+ const existing = fs9.readFileSync(gitignorePath, "utf8");
4483
5284
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
4484
5285
  if (!hasEntry) {
4485
5286
  const needsNewline = existing.length > 0 && !existing.endsWith("\n");
4486
- fs8.appendFileSync(
5287
+ fs9.appendFileSync(
4487
5288
  gitignorePath,
4488
5289
  `${needsNewline ? "\n" : ""}${entry}
4489
5290
  `,
@@ -4492,7 +5293,7 @@ function runInit(cwd, w) {
4492
5293
  w.out(`Appended ${entry} to ${gitignorePath}`);
4493
5294
  }
4494
5295
  } else {
4495
- fs8.writeFileSync(gitignorePath, `${entry}
5296
+ fs9.writeFileSync(gitignorePath, `${entry}
4496
5297
  `, "utf8");
4497
5298
  w.out(`Created ${gitignorePath} with ${entry}`);
4498
5299
  }
@@ -4531,20 +5332,25 @@ function runScanCommand(cwd, flags, w) {
4531
5332
  if (updateBaseline) {
4532
5333
  for (const r of results) {
4533
5334
  const manifest = r.capturedStates ?? { captured: [] };
4534
- const pageBaseline = computePageBaseline(r.registry, manifest);
4535
- const missingKinds = computeMissingKinds(r.registry, manifest);
4536
- const baseline = {
4537
- ...pageBaseline,
4538
- ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
5335
+ const acknowledge = {};
5336
+ const merge = (from) => {
5337
+ for (const [route, scopes] of Object.entries(from)) {
5338
+ acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
5339
+ }
4539
5340
  };
4540
- const p2 = pageBaselinePath(r);
4541
- fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4542
- const kindGaps = Object.values(missingKinds).reduce(
4543
- (n, ks) => n + ks.length,
5341
+ merge(computePageBacklog(r.registry, manifest));
5342
+ merge(computeMatrixBacklog(r.registry, manifest));
5343
+ const baseline = { version: 2, acknowledge };
5344
+ const p2 = coverageBaselinePath(r);
5345
+ fs9.writeFileSync(p2, serializeCoverageBaseline(baseline));
5346
+ const entries = Object.values(acknowledge);
5347
+ const pages = entries.filter((s) => s["*"]).length;
5348
+ const kindGaps = entries.reduce(
5349
+ (n, s) => n + Object.keys(s).filter((k) => k !== "*").length,
4544
5350
  0
4545
5351
  );
4546
5352
  w.out(
4547
- `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
5353
+ `Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
4548
5354
  );
4549
5355
  }
4550
5356
  return w.result(0);
@@ -4617,7 +5423,7 @@ function runScaffold(cwd, args, flags, w) {
4617
5423
  for (const r of results) {
4618
5424
  const entity = r.registry.get(scaffoldKind, id);
4619
5425
  if (!entity) continue;
4620
- const outDir = path11.resolve(r.configDir, "e2e");
5426
+ const outDir = path12.resolve(r.configDir, "e2e");
4621
5427
  const result = scaffoldSpec({
4622
5428
  registry: r.registry,
4623
5429
  kind: scaffoldKind,
@@ -4638,6 +5444,53 @@ function runScaffold(cwd, args, flags, w) {
4638
5444
  return w.result(1);
4639
5445
  }
4640
5446
  var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
5447
+ function runStates(cwd, args, w) {
5448
+ const [sub, explicitPartial] = args;
5449
+ if (sub !== "merge") {
5450
+ w.err("Usage: uidex states merge [partial-manifest]");
5451
+ return w.result(1);
5452
+ }
5453
+ const configs = discover({ cwd });
5454
+ if (configs.length === 0) {
5455
+ w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
5456
+ return w.result(1);
5457
+ }
5458
+ let merged = 0;
5459
+ for (const { configDir, config } of configs) {
5460
+ const rel = config.statesManifest ?? "uidex-states.json";
5461
+ const target = path12.resolve(configDir, rel);
5462
+ const partial = explicitPartial ? path12.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
5463
+ const committedRaw = readJson(target);
5464
+ const partialRaw = readJson(partial);
5465
+ if (!partialRaw) continue;
5466
+ if (!committedRaw) {
5467
+ w.err(`No committed manifest at ${rel}; nothing to merge into`);
5468
+ return w.result(1);
5469
+ }
5470
+ const result = mergeStates(committedRaw, partialRaw);
5471
+ fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
5472
+ merged++;
5473
+ w.out(
5474
+ `Merged ${path12.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
5475
+ );
5476
+ for (const k of result.added) w.out(` + ${k}`);
5477
+ for (const k of result.updated) w.out(` ~ ${k}`);
5478
+ }
5479
+ if (merged === 0) {
5480
+ w.out("No partial manifest found; nothing to merge.");
5481
+ }
5482
+ return w.result(0);
5483
+ }
5484
+ function readJson(p2) {
5485
+ try {
5486
+ const parsed = JSON.parse(fs9.readFileSync(p2, "utf8"));
5487
+ if (!parsed || typeof parsed !== "object") return null;
5488
+ const m = parsed;
5489
+ return Array.isArray(m.captured) ? m : null;
5490
+ } catch {
5491
+ return null;
5492
+ }
5493
+ }
4641
5494
  function runRename(cwd, args, flags, w) {
4642
5495
  const [kind, oldId, newId] = args;
4643
5496
  if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
@@ -4685,35 +5538,51 @@ function createWriter() {
4685
5538
  };
4686
5539
  }
4687
5540
  export {
5541
+ ACK_ALL,
5542
+ ACK_SCOPES,
4688
5543
  CONFIG_FILENAME,
4689
5544
  ConfigError,
4690
5545
  DEFAULT_CONVENTIONS,
5546
+ DIAGNOSTIC_CODES,
4691
5547
  applyFixes,
4692
5548
  audit,
5549
+ capturedElsewhere,
5550
+ checkComponentCoverage,
4693
5551
  checkPageCoverage,
4694
5552
  checkStateCoverage,
4695
5553
  checkStateMatrix,
4696
5554
  compileRoute,
4697
- computeMissingKinds,
4698
- computePageBaseline,
5555
+ computeMatrixBacklog,
5556
+ computePageBacklog,
5557
+ coverageBaselinePath,
4699
5558
  detectRoutes,
4700
5559
  discover,
4701
5560
  emit,
5561
+ entityAcknowledge,
4702
5562
  extract,
4703
5563
  extractUidexExports,
5564
+ findWorkspaceRoot,
4704
5565
  globToRegExp,
4705
- loadPageBaseline,
5566
+ isLegacyBaseline,
5567
+ loadCoverageBaseline,
4706
5568
  matchUrlToRoute,
4707
- pageBaselinePath,
5569
+ mergeStates,
5570
+ migrateBaseline,
5571
+ migrateLegacyBaseline,
5572
+ migrateLegacyFields,
4708
5573
  parseConfig,
5574
+ parseCoverageBaseline,
4709
5575
  pathToId,
4710
5576
  renameEntity,
4711
5577
  resolve2 as resolve,
4712
5578
  resolveGitContext,
5579
+ resolveWorkspace,
5580
+ routeAcknowledge,
4713
5581
  run as runCli,
4714
5582
  runScan,
4715
5583
  scaffoldSpec,
4716
5584
  scaffoldWidgetSpec,
5585
+ serializeCoverageBaseline,
4717
5586
  validateConfig,
4718
5587
  walk,
4719
5588
  writeScanResult