uidex 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/cli.cjs +1273 -343
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +3 -0
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +19 -13
  6. package/dist/headless/index.d.ts +19 -13
  7. package/dist/headless/index.js +3 -0
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +3 -0
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +19 -13
  12. package/dist/index.d.ts +19 -13
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +36 -7
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.js +36 -7
  18. package/dist/playwright/index.js.map +1 -1
  19. package/dist/playwright/states-reporter.cjs +36 -7
  20. package/dist/playwright/states-reporter.cjs.map +1 -1
  21. package/dist/playwright/states-reporter.d.cts +15 -0
  22. package/dist/playwright/states-reporter.d.ts +15 -0
  23. package/dist/playwright/states-reporter.js +36 -7
  24. package/dist/playwright/states-reporter.js.map +1 -1
  25. package/dist/playwright/states.cjs +8 -0
  26. package/dist/playwright/states.cjs.map +1 -1
  27. package/dist/playwright/states.d.cts +44 -1
  28. package/dist/playwright/states.d.ts +44 -1
  29. package/dist/playwright/states.js +7 -0
  30. package/dist/playwright/states.js.map +1 -1
  31. package/dist/react/index.cjs +3 -0
  32. package/dist/react/index.cjs.map +1 -1
  33. package/dist/react/index.d.cts +19 -13
  34. package/dist/react/index.d.ts +19 -13
  35. package/dist/react/index.js +3 -0
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/scan/index.cjs +1217 -257
  38. package/dist/scan/index.cjs.map +1 -1
  39. package/dist/scan/index.d.cts +410 -104
  40. package/dist/scan/index.d.ts +410 -104
  41. package/dist/scan/index.js +1197 -253
  42. package/dist/scan/index.js.map +1 -1
  43. package/package.json +17 -17
@@ -30,35 +30,51 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/scanner/scan/index.ts
31
31
  var scan_exports = {};
32
32
  __export(scan_exports, {
33
+ ACK_ALL: () => ACK_ALL,
34
+ ACK_SCOPES: () => ACK_SCOPES,
33
35
  CONFIG_FILENAME: () => CONFIG_FILENAME,
34
36
  ConfigError: () => ConfigError,
35
37
  DEFAULT_CONVENTIONS: () => DEFAULT_CONVENTIONS,
38
+ DIAGNOSTIC_CODES: () => DIAGNOSTIC_CODES,
36
39
  applyFixes: () => applyFixes,
37
40
  audit: () => audit,
41
+ capturedElsewhere: () => capturedElsewhere,
42
+ checkComponentCoverage: () => checkComponentCoverage,
38
43
  checkPageCoverage: () => checkPageCoverage,
39
44
  checkStateCoverage: () => checkStateCoverage,
40
45
  checkStateMatrix: () => checkStateMatrix,
41
46
  compileRoute: () => compileRoute,
42
- computeMissingKinds: () => computeMissingKinds,
43
- computePageBaseline: () => computePageBaseline,
47
+ computeMatrixBacklog: () => computeMatrixBacklog,
48
+ computePageBacklog: () => computePageBacklog,
49
+ coverageBaselinePath: () => coverageBaselinePath,
44
50
  detectRoutes: () => detectRoutes,
45
51
  discover: () => discover,
46
52
  emit: () => emit,
53
+ entityAcknowledge: () => entityAcknowledge,
47
54
  extract: () => extract,
48
55
  extractUidexExports: () => extractUidexExports,
56
+ findWorkspaceRoot: () => findWorkspaceRoot,
49
57
  globToRegExp: () => globToRegExp,
50
- loadPageBaseline: () => loadPageBaseline,
58
+ isLegacyBaseline: () => isLegacyBaseline,
59
+ loadCoverageBaseline: () => loadCoverageBaseline,
51
60
  matchUrlToRoute: () => matchUrlToRoute,
52
- pageBaselinePath: () => pageBaselinePath,
61
+ mergeStates: () => mergeStates,
62
+ migrateBaseline: () => migrateBaseline,
63
+ migrateLegacyBaseline: () => migrateLegacyBaseline,
64
+ migrateLegacyFields: () => migrateLegacyFields,
53
65
  parseConfig: () => parseConfig,
66
+ parseCoverageBaseline: () => parseCoverageBaseline,
54
67
  pathToId: () => pathToId,
55
68
  renameEntity: () => renameEntity,
56
69
  resolve: () => resolve2,
57
70
  resolveGitContext: () => resolveGitContext,
71
+ resolveWorkspace: () => resolveWorkspace,
72
+ routeAcknowledge: () => routeAcknowledge,
58
73
  runCli: () => run,
59
74
  runScan: () => runScan,
60
75
  scaffoldSpec: () => scaffoldSpec,
61
76
  scaffoldWidgetSpec: () => scaffoldWidgetSpec,
77
+ serializeCoverageBaseline: () => serializeCoverageBaseline,
62
78
  validateConfig: () => validateConfig,
63
79
  walk: () => walk,
64
80
  writeScanResult: () => writeScanResult
@@ -97,7 +113,8 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
97
113
  "audit",
98
114
  "conventions",
99
115
  "statesManifest",
100
- "coverageBaseline"
116
+ "coverageBaseline",
117
+ "workspace"
101
118
  ]);
102
119
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
103
120
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -113,19 +130,64 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
113
130
  "acceptance",
114
131
  "states",
115
132
  "pageCoverage",
116
- "stateMatrix"
133
+ "stateMatrix",
134
+ "severity"
117
135
  ]);
136
+ var DIAGNOSTIC_CODES = [
137
+ "acceptance-uncovered",
138
+ "acknowledged-elsewhere",
139
+ "competing-uidex-export",
140
+ "component-state-uncaptured",
141
+ "core-kind",
142
+ "duplicate-id",
143
+ "dynamic-attr",
144
+ "dynamic-flow-reference",
145
+ "gen-missing",
146
+ "gen-stale",
147
+ "legacy-acknowledge-field",
148
+ "legacy-coverage-baseline",
149
+ "matrix-backlog",
150
+ "missing-element-annotation",
151
+ "missing-state-capture",
152
+ "orphan-state-capture",
153
+ "page-backlog",
154
+ "page-captured",
155
+ "page-no-capture",
156
+ "parse-error",
157
+ "placeholder-reason",
158
+ "prefer-well-known-file",
159
+ "rename",
160
+ "scope-leak",
161
+ "spread-attr",
162
+ "stale-acknowledgment",
163
+ "stale-page-baseline",
164
+ "uidex-export-ambiguous-kind",
165
+ "uidex-export-duplicate-field",
166
+ "uidex-export-duplicate-state",
167
+ "uidex-export-empty-name",
168
+ "uidex-export-invalid-field",
169
+ "uidex-export-invalid-literal",
170
+ "uidex-export-missing-kind",
171
+ "uidex-export-satisfies-mismatch",
172
+ "uidex-export-unknown-field",
173
+ "unknown-reference",
174
+ "widget-id-mismatch",
175
+ "widget-missing-acceptance",
176
+ "widget-missing-dom"
177
+ ];
178
+ var DIAGNOSTIC_CODE_SET = new Set(DIAGNOSTIC_CODES);
179
+ var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
118
180
  function fail(msg) {
119
181
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
120
182
  }
121
- function assertObject(value, path12) {
183
+ function assertObject(value, path13) {
122
184
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
123
- fail(`${path12} must be an object`);
185
+ fail(`${path13} must be an object`);
124
186
  }
125
187
  }
126
- function assertStringArray(value, path12) {
188
+ function assertStringArray(value, path13) {
127
189
  if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
128
- fail(`${path12} must be a string[]`);
190
+ fail(`${path13} must be a string[]`);
129
191
  }
130
192
  }
131
193
  function validateConfig(raw) {
@@ -179,11 +241,30 @@ function validateConfig(raw) {
179
241
  if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
180
242
  fail(`"coverageBaseline" must be a string`);
181
243
  }
244
+ if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
245
+ fail(`"workspace" must be a path string or false`);
246
+ }
182
247
  if (raw.audit !== void 0) {
183
248
  assertObject(raw.audit, "audit");
184
249
  for (const key of Object.keys(raw.audit)) {
185
250
  if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
186
251
  const v = raw.audit[key];
252
+ if (key === "severity") {
253
+ assertObject(v, "audit.severity");
254
+ for (const [code, level] of Object.entries(v)) {
255
+ if (!DIAGNOSTIC_CODE_SET.has(code)) {
256
+ fail(
257
+ `unknown diagnostic code "${code}" in audit.severity (a typo here would silently leave that gate at its default severity)`
258
+ );
259
+ }
260
+ if (typeof level !== "string" || !SEVERITY_LEVELS.has(level)) {
261
+ fail(
262
+ `audit.severity["${code}"] must be one of "error", "warning", "info", "off"`
263
+ );
264
+ }
265
+ }
266
+ continue;
267
+ }
187
268
  if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
188
269
  }
189
270
  }
@@ -202,7 +283,13 @@ function validateConfig(raw) {
202
283
  fail(`conventions.features must be a string or false`);
203
284
  }
204
285
  if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
205
- fail(`conventions.pages must be "auto" or false`);
286
+ const p2 = c.pages;
287
+ const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
288
+ if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
289
+ fail(
290
+ `conventions.pages must be "auto", false, or { routesDir: string }`
291
+ );
292
+ }
206
293
  }
207
294
  if (c.flows !== void 0 && c.flows !== false) {
208
295
  assertStringArray(c.flows, "conventions.flows");
@@ -220,7 +307,8 @@ function validateConfig(raw) {
220
307
  audit: raw.audit,
221
308
  conventions: raw.conventions,
222
309
  statesManifest: raw.statesManifest,
223
- coverageBaseline: raw.coverageBaseline
310
+ coverageBaseline: raw.coverageBaseline,
311
+ workspace: raw.workspace
224
312
  };
225
313
  }
226
314
  function parseConfig(json) {
@@ -270,9 +358,11 @@ function tryReadConfig(configPath) {
270
358
  function discover(options = {}) {
271
359
  const cwd = options.cwd ?? process.cwd();
272
360
  const maxDepth = options.maxDepth ?? MAX_DEPTH;
361
+ const all = options.all ?? false;
273
362
  const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
274
- if (rootMatch) return [rootMatch];
363
+ if (rootMatch && !all) return [rootMatch];
275
364
  const results = [];
365
+ if (rootMatch) results.push(rootMatch);
276
366
  const queue = [[cwd, 0]];
277
367
  while (queue.length > 0) {
278
368
  const [dir, depth] = queue.shift();
@@ -376,6 +466,26 @@ function toPosix(p2) {
376
466
  function matchesAny(rel, patterns) {
377
467
  return patterns.some((g) => globToRegExp(g).test(rel));
378
468
  }
469
+ function createSourceMatcher(sources, options) {
470
+ const { cwd, globalExcludes = [], includeTests = false } = options;
471
+ const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
472
+ const compiled = sources.map((source) => ({
473
+ absRoot: path2.resolve(cwd, source.rootDir),
474
+ includes: source.include?.length ? source.include : DEFAULT_INCLUDES,
475
+ excludes: [...baseDefaults, ...globalExcludes, ...source.exclude ?? []]
476
+ }));
477
+ return (absFile) => {
478
+ const abs = path2.resolve(absFile);
479
+ return compiled.some(({ absRoot, includes, excludes }) => {
480
+ const rel = toPosix(path2.relative(absRoot, abs));
481
+ if (rel.length === 0 || rel.startsWith("../") || path2.isAbsolute(rel)) {
482
+ return false;
483
+ }
484
+ if (matchesAny(rel, excludes)) return false;
485
+ return matchesAny(rel, includes);
486
+ });
487
+ };
488
+ }
379
489
  function walk(sources, options) {
380
490
  const { cwd, globalExcludes = [], includeTests = false } = options;
381
491
  const out2 = [];
@@ -709,6 +819,132 @@ function createRegistry() {
709
819
  };
710
820
  }
711
821
 
822
+ // src/scanner/scan/acknowledge.ts
823
+ var ACK_ALL = "*";
824
+ var ACK_SCOPES = [ACK_ALL, ...CORE_STATE_KINDS];
825
+ var COMPONENT_ACK_SCOPES = [ACK_ALL];
826
+ var AUTHORABLE_ACK_TYPES = [
827
+ "impossible",
828
+ "backlog"
829
+ ];
830
+ var CORE_SET = new Set(CORE_STATE_KINDS);
831
+ function isCoreKind(scope) {
832
+ return CORE_SET.has(scope);
833
+ }
834
+ var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
835
+ function isPlaceholderReason(reason) {
836
+ return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
837
+ }
838
+ function describeAck(a) {
839
+ if (a.type === "captured-elsewhere") {
840
+ return a.by ? `captured-elsewhere (${a.by})` : "captured-elsewhere";
841
+ }
842
+ return a.reason ? `${a.type} ("${a.reason}")` : a.type;
843
+ }
844
+ function originHint(a, page) {
845
+ if (a.origin === "baseline") {
846
+ return "Prune it with `uidex scan --update-baseline` so the backlog reflects reality.";
847
+ }
848
+ if (a.origin === "workspace") {
849
+ return "This is computed from the workspace; it resolves itself once the sibling capture changes.";
850
+ }
851
+ const where = page ? ` on page "${page}"` : "";
852
+ return `Remove the \`acknowledge.${a.scope}\` entry${where} \u2014 a capture contradicts it.`;
853
+ }
854
+ function toResolved(map, origin, into) {
855
+ for (const [scope, ack] of Object.entries(map ?? {})) {
856
+ into.set(scope, { ...ack, scope, origin });
857
+ }
858
+ }
859
+ function routeAcknowledge(registry, baseline, route, page) {
860
+ const out2 = /* @__PURE__ */ new Map();
861
+ toResolved(baseline?.acknowledge?.[route], "baseline", out2);
862
+ if (page) {
863
+ toResolved(registry.get("page", page)?.meta?.acknowledge, "source", out2);
864
+ }
865
+ return out2;
866
+ }
867
+ function entityAcknowledge(meta, capturedElsewhereBy) {
868
+ const source = meta?.acknowledge?.[ACK_ALL];
869
+ if (source) return { ...source, scope: ACK_ALL, origin: "source" };
870
+ if (capturedElsewhereBy) {
871
+ return {
872
+ type: "captured-elsewhere",
873
+ by: capturedElsewhereBy,
874
+ scope: ACK_ALL,
875
+ origin: "workspace"
876
+ };
877
+ }
878
+ return void 0;
879
+ }
880
+ function readAck(raw) {
881
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
882
+ const r = raw;
883
+ if (r.type !== "impossible" && r.type !== "backlog") return void 0;
884
+ const out2 = { type: r.type };
885
+ if (typeof r.reason === "string" && r.reason.length > 0) out2.reason = r.reason;
886
+ return out2;
887
+ }
888
+ function parseCoverageBaseline(raw) {
889
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
890
+ const r = raw;
891
+ const ackRaw = r.acknowledge;
892
+ if (!ackRaw || typeof ackRaw !== "object" || Array.isArray(ackRaw))
893
+ return null;
894
+ const acknowledge = {};
895
+ for (const [route, scopesRaw] of Object.entries(
896
+ ackRaw
897
+ )) {
898
+ if (!scopesRaw || typeof scopesRaw !== "object" || Array.isArray(scopesRaw))
899
+ continue;
900
+ const scopes = {};
901
+ for (const [scope, ackVal] of Object.entries(
902
+ scopesRaw
903
+ )) {
904
+ if (scope !== ACK_ALL && !isCoreKind(scope)) continue;
905
+ const ack = readAck(ackVal);
906
+ if (ack) scopes[scope] = ack;
907
+ }
908
+ if (Object.keys(scopes).length > 0) acknowledge[route] = scopes;
909
+ }
910
+ return { version: 2, acknowledge };
911
+ }
912
+ function isLegacyBaseline(raw) {
913
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
914
+ const r = raw;
915
+ if ("acknowledge" in r) return false;
916
+ return "uncapturedPages" in r || "missingKinds" in r;
917
+ }
918
+ function migrateBaseline(legacy) {
919
+ const acknowledge = {};
920
+ const at = (route) => acknowledge[route] ??= {};
921
+ for (const route of legacy.uncapturedPages ?? []) {
922
+ if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
923
+ }
924
+ for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
925
+ if (!Array.isArray(kinds)) continue;
926
+ for (const kind of kinds) {
927
+ if (typeof kind === "string" && isCoreKind(kind)) {
928
+ at(route)[kind] = { type: "backlog" };
929
+ }
930
+ }
931
+ }
932
+ return { version: 2, acknowledge };
933
+ }
934
+ function serializeCoverageBaseline(baseline) {
935
+ const routes = Object.keys(baseline.acknowledge).sort();
936
+ const ordered = {};
937
+ for (const route of routes) {
938
+ const scopes = baseline.acknowledge[route];
939
+ const sorted = {};
940
+ for (const scope of ACK_SCOPES) {
941
+ if (scopes[scope]) sorted[scope] = scopes[scope];
942
+ }
943
+ ordered[route] = sorted;
944
+ }
945
+ return JSON.stringify({ version: 2, acknowledge: ordered }, null, 2) + "\n";
946
+ }
947
+
712
948
  // src/scanner/scan/extract-uidex-export.ts
713
949
  var KIND_DISCRIMINATORS = [
714
950
  "page",
@@ -727,26 +963,41 @@ var ALLOWED_FIELDS = {
727
963
  "widgets",
728
964
  "acceptance",
729
965
  "states",
966
+ "acknowledge",
730
967
  "capture",
731
968
  "waivers",
732
969
  "description"
733
970
  ]),
734
- // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
735
- // they are page-only (the gates read them off the page). A feature/widget that
736
- // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
971
+ // A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
972
+ // and a core kind (one cell of its matrix). A feature/widget has no route, so
973
+ // the core-kind matrix is meaningless there and only `"*"` is accepted —
974
+ // giving a component a scoped opt-out instead of forcing you to delete its
975
+ // whole `states` block. `capture` / `waivers` are REMOVED; they stay parseable
976
+ // only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
737
977
  feature: /* @__PURE__ */ new Set([
738
978
  "feature",
739
979
  "name",
740
980
  "features",
741
981
  "acceptance",
742
982
  "states",
983
+ "acknowledge",
984
+ "capture",
743
985
  "description"
744
986
  ]),
745
987
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
746
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
988
+ widget: /* @__PURE__ */ new Set([
989
+ "widget",
990
+ "name",
991
+ "acceptance",
992
+ "states",
993
+ "acknowledge",
994
+ "capture",
995
+ "description"
996
+ ]),
747
997
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
748
998
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
749
999
  };
1000
+ var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
750
1001
  var STATE_FIELDS = /* @__PURE__ */ new Set([
751
1002
  "name",
752
1003
  "kind",
@@ -997,7 +1248,8 @@ function objectLit(node, content, p2, pos, span) {
997
1248
  key,
998
1249
  value,
999
1250
  keyPos,
1000
- span: removalSpan(content, prop.start, prop.end)
1251
+ span: removalSpan(content, prop.start, prop.end),
1252
+ propSpan: { start: prop.start, end: prop.end }
1001
1253
  });
1002
1254
  }
1003
1255
  return { kind: "object", entries, pos, span };
@@ -1145,8 +1397,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1145
1397
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1146
1398
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1147
1399
  const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
1148
- const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
1149
- const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
1400
+ const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
1401
+ const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
1402
+ const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
1403
+ const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
1150
1404
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1151
1405
  const metadata = {
1152
1406
  source: "ts-export",
@@ -1170,15 +1424,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1170
1424
  metadata.widgetSpans = widgetsField.spans;
1171
1425
  }
1172
1426
  if (states?.length) metadata.states = states;
1173
- if (capture !== void 0) metadata.capture = capture;
1174
- if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
1427
+ if (acknowledge && Object.keys(acknowledge).length > 0) {
1428
+ metadata.acknowledge = acknowledge;
1429
+ }
1430
+ if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
1431
+ if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
1432
+ metadata.legacyWaivers = legacyWaivers;
1433
+ }
1175
1434
  if (notFlow) metadata.notFlow = true;
1176
1435
  if (typeof id === "string" && idValue.kind === "string") {
1177
1436
  metadata.idSpan = idValue.span;
1178
1437
  }
1179
1438
  const fieldSpans = {};
1180
- for (const entry of value.entries) fieldSpans[entry.key] = entry.span;
1439
+ const fieldPropSpans = {};
1440
+ for (const entry of value.entries) {
1441
+ fieldSpans[entry.key] = entry.span;
1442
+ fieldPropSpans[entry.key] = entry.propSpan;
1443
+ }
1181
1444
  metadata.fieldSpans = fieldSpans;
1445
+ metadata.fieldPropSpans = fieldPropSpans;
1182
1446
  return metadata;
1183
1447
  }
1184
1448
  function readIdField(value, kind, fieldName) {
@@ -1208,6 +1472,80 @@ function readIdField(value, kind, fieldName) {
1208
1472
  value.pos
1209
1473
  );
1210
1474
  }
1475
+ function readAcknowledgeField(byKey, isPage) {
1476
+ const entry = byKey.get("acknowledge");
1477
+ if (!entry) return void 0;
1478
+ if (entry.value.kind !== "object") {
1479
+ throw new ExtractError(
1480
+ "uidex-export-invalid-field",
1481
+ '`acknowledge` must be an object mapping a scope (`"*"` or a core kind) to `{ type, reason? }`.',
1482
+ entry.value.pos
1483
+ );
1484
+ }
1485
+ const out2 = {};
1486
+ for (const e of entry.value.entries) {
1487
+ const scopes = isPage ? ACK_SCOPES : COMPONENT_ACK_SCOPES;
1488
+ if (!scopes.includes(e.key)) {
1489
+ throw new ExtractError(
1490
+ "uidex-export-invalid-field",
1491
+ `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)"}.`,
1492
+ e.keyPos
1493
+ );
1494
+ }
1495
+ if (e.value.kind !== "object") {
1496
+ throw new ExtractError(
1497
+ "uidex-export-invalid-field",
1498
+ `acknowledge "${e.key}" must be an object \`{ type, reason? }\`.`,
1499
+ e.value.pos
1500
+ );
1501
+ }
1502
+ const fieldByKey = /* @__PURE__ */ new Map();
1503
+ for (const f of e.value.entries) {
1504
+ if (!ACK_FIELDS.has(f.key)) {
1505
+ throw new ExtractError(
1506
+ "uidex-export-invalid-field",
1507
+ `Unknown field "${f.key}" in an \`acknowledge\` entry. Allowed: ${[...ACK_FIELDS].sort().join(", ")}.`,
1508
+ f.value.pos
1509
+ );
1510
+ }
1511
+ fieldByKey.set(f.key, f);
1512
+ }
1513
+ const type = readStringField(fieldByKey, "type");
1514
+ if (type === void 0) {
1515
+ throw new ExtractError(
1516
+ "uidex-export-invalid-field",
1517
+ `acknowledge "${e.key}" needs a \`type\`: ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")}.`,
1518
+ e.value.pos
1519
+ );
1520
+ }
1521
+ if (type === "captured-elsewhere") {
1522
+ throw new ExtractError(
1523
+ "uidex-export-invalid-field",
1524
+ '`type: "captured-elsewhere"` cannot be authored; the workspace computes it from the sibling app that captured the declaration.',
1525
+ fieldByKey.get("type").value.pos,
1526
+ "Remove the entry \u2014 if a sibling app really captures this, the workspace reports it as `acknowledged-elsewhere` on its own."
1527
+ );
1528
+ }
1529
+ if (type !== "impossible" && type !== "backlog") {
1530
+ throw new ExtractError(
1531
+ "uidex-export-invalid-field",
1532
+ `acknowledge \`type\` must be ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")} (got "${type}").`,
1533
+ fieldByKey.get("type").value.pos
1534
+ );
1535
+ }
1536
+ const reason = readStringField(fieldByKey, "reason");
1537
+ if (type === "impossible" && (reason === void 0 || reason.length === 0)) {
1538
+ throw new ExtractError(
1539
+ "uidex-export-invalid-field",
1540
+ `acknowledge "${e.key}" is \`type: "impossible"\` and needs a non-empty \`reason\` (why this can NEVER be rendered/captured).`,
1541
+ e.value.pos,
1542
+ 'If it merely has not been captured yet, that is `type: "backlog"` \u2014 which needs no reason.'
1543
+ );
1544
+ }
1545
+ out2[e.key] = reason ? { type, reason } : { type };
1546
+ }
1547
+ return out2;
1548
+ }
1211
1549
  function readWaiversField(byKey) {
1212
1550
  const entry = byKey.get("waivers");
1213
1551
  if (!entry) return void 0;
@@ -1887,13 +2225,26 @@ var path4 = __toESM(require("path"), 1);
1887
2225
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
1888
2226
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
1889
2227
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
1890
- function detectRoutes(files) {
2228
+ function detectRoutes(files, options = {}) {
1891
2229
  const out2 = [];
1892
2230
  const seen = /* @__PURE__ */ new Set();
2231
+ const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
2232
+ const routesDirParts = routesDir ? routesDir.split("/") : null;
1893
2233
  for (const f of files) {
1894
2234
  const rel = f.displayPath;
1895
2235
  const parts = rel.split("/");
1896
2236
  const base = parts[parts.length - 1];
2237
+ if (routesDirParts) {
2238
+ const end = dirEndIndex(parts, routesDirParts);
2239
+ if (end === -1 || end > parts.length - 1) continue;
2240
+ if (!PAGES_ROUTER_BASENAME.test(base)) continue;
2241
+ const routePath = routesStylePath(
2242
+ parts.slice(end, parts.length - 1),
2243
+ base
2244
+ );
2245
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
2246
+ continue;
2247
+ }
1897
2248
  const appIdx = parts.indexOf("app");
1898
2249
  if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
1899
2250
  const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
@@ -1920,15 +2271,11 @@ function detectRoutes(files) {
1920
2271
  }
1921
2272
  const routesIdx = parts.indexOf("routes");
1922
2273
  if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
1923
- const segs = parts.slice(routesIdx + 1);
1924
- const last = segs[segs.length - 1];
1925
- if (last.startsWith("_")) continue;
1926
- const normalized = [
1927
- ...segs.slice(0, -1),
1928
- base.replace(/\.[^.]+$/, "")
1929
- ].filter((s) => s !== "index" && s !== "__root");
1930
- const routePath = formatNextAppPath(normalized);
1931
- push(out2, seen, routePath, f.displayPath);
2274
+ const routePath = routesStylePath(
2275
+ parts.slice(routesIdx + 1, parts.length - 1),
2276
+ base
2277
+ );
2278
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
1932
2279
  continue;
1933
2280
  }
1934
2281
  }
@@ -1944,6 +2291,63 @@ function formatNextAppPath(segments) {
1944
2291
  if (kept.length === 0) return "/";
1945
2292
  return "/" + kept.join("/");
1946
2293
  }
2294
+ function dirEndIndex(parts, dirParts) {
2295
+ if (dirParts.length === 0) return 0;
2296
+ for (let i = 0; i + dirParts.length <= parts.length; i++) {
2297
+ let match = true;
2298
+ for (let j = 0; j < dirParts.length; j++) {
2299
+ if (parts[i + j] !== dirParts[j]) {
2300
+ match = false;
2301
+ break;
2302
+ }
2303
+ }
2304
+ if (match) return i + dirParts.length;
2305
+ }
2306
+ return -1;
2307
+ }
2308
+ function routesStylePath(dirSegs, base) {
2309
+ const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
2310
+ const leaf = fileSegs[fileSegs.length - 1];
2311
+ if (leaf.startsWith("_")) return null;
2312
+ const rawSegs = [...dirSegs, ...fileSegs];
2313
+ if (rawSegs.some((s) => s.startsWith("-"))) return null;
2314
+ if (rawSegs[0] === "api") return null;
2315
+ return formatTanStackPath(rawSegs);
2316
+ }
2317
+ function splitFlatSegments(name) {
2318
+ const segs = [];
2319
+ let cur = "";
2320
+ let depth = 0;
2321
+ for (const ch of name) {
2322
+ if (ch === "[") depth++;
2323
+ else if (ch === "]") depth = Math.max(0, depth - 1);
2324
+ if (ch === "." && depth === 0) {
2325
+ segs.push(cur);
2326
+ cur = "";
2327
+ continue;
2328
+ }
2329
+ cur += ch;
2330
+ }
2331
+ segs.push(cur);
2332
+ return segs;
2333
+ }
2334
+ function formatTanStackPath(segments) {
2335
+ const kept = [];
2336
+ for (const seg of segments) {
2337
+ if (!seg) continue;
2338
+ if (seg === "index" || seg === "route" || seg === "__root") continue;
2339
+ if (seg.startsWith("_")) continue;
2340
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
2341
+ kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
2342
+ }
2343
+ if (kept.length === 0) return "/";
2344
+ return "/" + kept.join("/");
2345
+ }
2346
+ function normalizeTanStackSegment(seg) {
2347
+ if (seg === "$") return "[...splat]";
2348
+ if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
2349
+ return seg.replace(/\[([^\]]*)\]/g, "$1");
2350
+ }
1947
2351
  function pathToId(routePath) {
1948
2352
  if (routePath === "/") return "root";
1949
2353
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -1990,9 +2394,8 @@ function buildMetaFromExport(exp) {
1990
2394
  ...s.description ? { description: s.description } : {}
1991
2395
  }));
1992
2396
  }
1993
- if (exp.capture === false) meta.capture = false;
1994
- if (exp.waivers && Object.keys(exp.waivers).length > 0) {
1995
- meta.waivers = exp.waivers;
2397
+ if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
2398
+ meta.acknowledge = exp.acknowledge;
1996
2399
  }
1997
2400
  return Object.keys(meta).length > 0 ? meta : void 0;
1998
2401
  }
@@ -2039,9 +2442,12 @@ function resolve2(ctx) {
2039
2442
  function metaWithComposes(kind, id, base) {
2040
2443
  const composes = directChildren.get(`${kind}:${id}`);
2041
2444
  if (!composes || composes.length === 0) return base;
2042
- return { ...base ?? {}, composes };
2445
+ return { ...base, composes };
2043
2446
  }
2044
- const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
2447
+ const routes = conventions.pages === false ? [] : detectRoutes(
2448
+ ctx.extracted.map((e) => e.file),
2449
+ conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
2450
+ );
2045
2451
  const handledPageFiles = /* @__PURE__ */ new Set();
2046
2452
  for (const route of routes) {
2047
2453
  const routeDir = path4.posix.dirname(route.file);
@@ -2334,6 +2740,45 @@ function resolve2(ctx) {
2334
2740
  };
2335
2741
  registry.add(element);
2336
2742
  }
2743
+ const ancestorsById = /* @__PURE__ */ new Map();
2744
+ const fileById = /* @__PURE__ */ new Map();
2745
+ for (const a of allAnnotations) {
2746
+ if (!fileById.has(a.id)) fileById.set(a.id, a.file);
2747
+ if (a.ancestors?.length) {
2748
+ const prev = ancestorsById.get(a.id) ?? [];
2749
+ ancestorsById.set(a.id, [...prev, ...a.ancestors.map((x) => x.id)]);
2750
+ }
2751
+ }
2752
+ const ownersByFile = /* @__PURE__ */ new Map();
2753
+ const addOwner = (file, id) => {
2754
+ ownersByFile.set(file, [...ownersByFile.get(file) ?? [], id]);
2755
+ };
2756
+ const featureDirs = [];
2757
+ for (const f of registry.list("feature")) {
2758
+ const loc = f.loc?.file;
2759
+ if (!loc) continue;
2760
+ const dir = loc.endsWith("/") ? loc.slice(0, -1) : path4.posix.dirname(loc);
2761
+ featureDirs.push({ dir, id: f.id });
2762
+ }
2763
+ for (const p2 of registry.list("page")) {
2764
+ if (p2.loc?.file) addOwner(p2.loc.file, p2.id);
2765
+ }
2766
+ const ownersFor = (file) => {
2767
+ const out2 = [...ownersByFile.get(file) ?? []];
2768
+ for (const { dir, id } of featureDirs) {
2769
+ if (file === dir || file.startsWith(`${dir}/`)) out2.push(id);
2770
+ }
2771
+ return out2;
2772
+ };
2773
+ const expandTouches = (ids) => {
2774
+ const out2 = new Set(ids);
2775
+ for (const id of ids) {
2776
+ for (const anc of ancestorsById.get(id) ?? []) out2.add(anc);
2777
+ const file = fileById.get(id);
2778
+ if (file) for (const owner of ownersFor(file)) out2.add(owner);
2779
+ }
2780
+ return [...out2];
2781
+ };
2337
2782
  if (ctx.flowFiles && conventions.flows) {
2338
2783
  for (const ff of ctx.flowFiles) {
2339
2784
  const notFlowExport = (ff.metadata ?? []).find(
@@ -2343,7 +2788,10 @@ function resolve2(ctx) {
2343
2788
  const flowExport = (ff.metadata ?? []).find(
2344
2789
  (m) => m.kind === "flow" && typeof m.id === "string"
2345
2790
  );
2346
- const derived = flowsFromFacts(ff);
2791
+ const derived = flowsFromFacts(ff).map((f) => ({
2792
+ ...f,
2793
+ touches: expandTouches(f.touches)
2794
+ }));
2347
2795
  if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
2348
2796
  const base = derived[0];
2349
2797
  const flow = {
@@ -2412,7 +2860,195 @@ function dedupe(arr) {
2412
2860
  }
2413
2861
 
2414
2862
  // src/scanner/scan/audit.ts
2415
- var path5 = __toESM(require("path"), 1);
2863
+ var path6 = __toESM(require("path"), 1);
2864
+
2865
+ // src/scanner/scan/component-coverage.ts
2866
+ var COMPOSED_KINDS = ["widget", "feature"];
2867
+ function composedStateEntities(registry, page) {
2868
+ const meta = registry.get("page", page)?.meta;
2869
+ if (!meta) return [];
2870
+ const out2 = [];
2871
+ for (const kind of COMPOSED_KINDS) {
2872
+ const ids = (kind === "widget" ? meta.widgets : meta.features) ?? [];
2873
+ for (const id of ids) {
2874
+ const e = registry.get(kind, id);
2875
+ if (!e?.meta?.states?.length) continue;
2876
+ if (e.meta.acknowledge?.[ACK_ALL]) continue;
2877
+ out2.push({ kind, id });
2878
+ }
2879
+ }
2880
+ return out2;
2881
+ }
2882
+ function capturedStatesFor(captured, kind, id) {
2883
+ const out2 = /* @__PURE__ */ new Set();
2884
+ for (const c of captured) {
2885
+ if (c.entity !== id) continue;
2886
+ if (c.kind !== void 0 && c.kind !== kind && c.kind !== "page") continue;
2887
+ out2.add(c.state);
2888
+ }
2889
+ return out2;
2890
+ }
2891
+ function checkComponentCoverage(registry, manifest) {
2892
+ const routes = registry.list("route");
2893
+ if (routes.length === 0) return [];
2894
+ const captured = manifest.captured ?? [];
2895
+ const diagnostics = [];
2896
+ for (const route of routes) {
2897
+ if (registry.get("page", route.page)?.meta?.acknowledge?.[ACK_ALL]) continue;
2898
+ for (const { kind, id } of composedStateEntities(registry, route.page)) {
2899
+ const entity = registry.get(kind, id);
2900
+ const declared = entity.meta.states.map((s) => s.name);
2901
+ const shot = capturedStatesFor(captured, kind, id);
2902
+ const missing = declared.filter((s) => !shot.has(s));
2903
+ if (missing.length === 0) continue;
2904
+ diagnostics.push({
2905
+ code: "component-state-uncaptured",
2906
+ severity: "info",
2907
+ message: `route "${route.path}" composes ${kind} "${id}", whose state(s) [${missing.join(", ")}] no capture produced`,
2908
+ file: entity.loc?.file,
2909
+ line: entity.loc?.line,
2910
+ entity: { kind, id },
2911
+ 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}".`
2912
+ });
2913
+ }
2914
+ }
2915
+ return diagnostics;
2916
+ }
2917
+
2918
+ // src/scanner/scan/migrate-acknowledge.ts
2919
+ var CORE = CORE_STATE_KINDS;
2920
+ function orderScopes(scopes) {
2921
+ const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
2922
+ return [...scopes].sort((a, b) => rank(a) - rank(b));
2923
+ }
2924
+ function renderKey(scope) {
2925
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
2926
+ }
2927
+ function renderAck(a) {
2928
+ const parts = [`type: ${JSON.stringify(a.type)}`];
2929
+ if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
2930
+ return `{ ${parts.join(", ")} }`;
2931
+ }
2932
+ function renderAcknowledge(entries, indent) {
2933
+ const inner = indent + " ";
2934
+ const lines = orderScopes(Object.keys(entries)).map(
2935
+ (scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
2936
+ );
2937
+ return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
2938
+ }
2939
+ function indentAt(content, offset) {
2940
+ const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
2941
+ const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
2942
+ return m ? m[0] : "";
2943
+ }
2944
+ function plannedEntries(m) {
2945
+ const entries = {};
2946
+ if (m.legacyCapture === false) {
2947
+ entries[ACK_ALL] = {
2948
+ type: "impossible",
2949
+ reason: m.description || PLACEHOLDER_REASON
2950
+ };
2951
+ }
2952
+ for (const kind of CORE) {
2953
+ const reason = m.legacyWaivers?.[kind];
2954
+ if (reason) entries[kind] = { type: "impossible", reason };
2955
+ }
2956
+ return entries;
2957
+ }
2958
+ function migrateLegacyFields(ef) {
2959
+ const out2 = [];
2960
+ const { content, sourcePath, displayPath } = ef.file;
2961
+ for (const m of ef.metadata ?? []) {
2962
+ const legacy = [];
2963
+ if (m.legacyWaivers) legacy.push("waivers");
2964
+ if (m.legacyCapture !== void 0) legacy.push("capture");
2965
+ if (legacy.length === 0) continue;
2966
+ const fields = legacy.join(" and ");
2967
+ const base = {
2968
+ severity: "error",
2969
+ code: "legacy-acknowledge-field",
2970
+ file: displayPath,
2971
+ line: m.loc.line
2972
+ };
2973
+ if (m.acknowledge) {
2974
+ out2.push({
2975
+ ...base,
2976
+ message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
2977
+ hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
2978
+ });
2979
+ continue;
2980
+ }
2981
+ const entries = plannedEntries(m);
2982
+ const edits = [];
2983
+ 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);
2984
+ if (present.length !== legacy.length) {
2985
+ out2.push({
2986
+ ...base,
2987
+ message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
2988
+ hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
2989
+ });
2990
+ continue;
2991
+ }
2992
+ const [anchor, ...rest] = present;
2993
+ if (Object.keys(entries).length === 0) {
2994
+ edits.push({
2995
+ path: sourcePath,
2996
+ start: anchor.del.start,
2997
+ end: anchor.del.end,
2998
+ replacement: ""
2999
+ });
3000
+ } else {
3001
+ edits.push({
3002
+ path: sourcePath,
3003
+ start: anchor.prop.start,
3004
+ end: anchor.prop.end,
3005
+ replacement: renderAcknowledge(
3006
+ entries,
3007
+ indentAt(content, anchor.prop.start)
3008
+ )
3009
+ });
3010
+ }
3011
+ for (const r of rest) {
3012
+ edits.push({
3013
+ path: sourcePath,
3014
+ start: r.del.start,
3015
+ end: r.del.end,
3016
+ replacement: ""
3017
+ });
3018
+ }
3019
+ out2.push({
3020
+ ...base,
3021
+ message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
3022
+ 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" : ""}.`,
3023
+ fix: {
3024
+ description: `Rewrite \`${fields}\` as \`acknowledge\``,
3025
+ edits
3026
+ }
3027
+ });
3028
+ }
3029
+ return out2;
3030
+ }
3031
+ function migrateLegacyBaseline(opts) {
3032
+ if (!isLegacyBaseline(opts.parsed)) return void 0;
3033
+ return {
3034
+ code: "legacy-coverage-baseline",
3035
+ severity: "error",
3036
+ message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
3037
+ file: opts.displayPath,
3038
+ hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
3039
+ fix: {
3040
+ description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
3041
+ edits: [
3042
+ {
3043
+ path: opts.path,
3044
+ start: 0,
3045
+ end: opts.raw.length,
3046
+ replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
3047
+ }
3048
+ ]
3049
+ }
3050
+ };
3051
+ }
2416
3052
 
2417
3053
  // src/scanner/scan/page-coverage.ts
2418
3054
  function compileRoute(pattern) {
@@ -2435,11 +3071,11 @@ function compileRoute(pattern) {
2435
3071
  return { test: (url) => re.test(url), specificity };
2436
3072
  }
2437
3073
  function matchUrlToRoute(url, routePaths) {
2438
- const path12 = stripQuery(url);
3074
+ const path13 = stripQuery(url);
2439
3075
  let best = null;
2440
3076
  for (const rp of routePaths) {
2441
3077
  const { test, specificity } = compileRoute(rp);
2442
- if (!test(path12)) continue;
3078
+ if (!test(path13)) continue;
2443
3079
  if (!best || specificity > best.specificity)
2444
3080
  best = { path: rp, specificity };
2445
3081
  }
@@ -2470,29 +3106,49 @@ function checkPageCoverage(registry, manifest, baseline) {
2470
3106
  if (routes.length === 0) return [];
2471
3107
  const routePaths = routes.map((r) => r.path);
2472
3108
  const captured = manifest.captured ?? [];
2473
- const optedOut = /* @__PURE__ */ new Set();
2474
- for (const r of routes) {
2475
- const page = registry.get("page", r.page);
2476
- if (page?.meta?.capture === false) optedOut.add(r.path);
2477
- }
2478
3109
  const covered = /* @__PURE__ */ new Set();
2479
3110
  for (const c of captured) {
2480
3111
  const route = routeForCapture(c, routes, routePaths);
2481
3112
  if (route) covered.add(route);
2482
3113
  }
2483
- const baselineSet = new Set(baseline?.uncapturedPages ?? []);
2484
3114
  const diagnostics = [];
2485
3115
  for (const r of routes) {
2486
- if (covered.has(r.path) || optedOut.has(r.path)) continue;
3116
+ const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
3117
+ ACK_ALL
3118
+ );
2487
3119
  const loc = registry.get("page", r.page)?.loc;
2488
- if (baselineSet.has(r.path)) {
3120
+ if (covered.has(r.path)) {
3121
+ if (ack?.type === "impossible") {
3122
+ diagnostics.push({
3123
+ code: "stale-acknowledgment",
3124
+ severity: "warning",
3125
+ message: `page "${r.page}" acknowledges "*" as ${describeAck(ack)}, but route "${r.path}" is captured`,
3126
+ file: loc?.file,
3127
+ line: loc?.line,
3128
+ entity: { kind: "page", id: r.page },
3129
+ hint: originHint(ack, r.page)
3130
+ });
3131
+ } else if (ack?.type === "backlog") {
3132
+ diagnostics.push({
3133
+ code: "page-captured",
3134
+ severity: "warning",
3135
+ message: `route "${r.path}" is now captured but is still acknowledged as backlog`,
3136
+ file: loc?.file,
3137
+ line: loc?.line,
3138
+ hint: originHint(ack, r.page)
3139
+ });
3140
+ }
3141
+ continue;
3142
+ }
3143
+ if (ack?.type === "impossible") continue;
3144
+ if (ack?.type === "backlog") {
2489
3145
  diagnostics.push({
2490
3146
  code: "page-backlog",
2491
3147
  severity: "info",
2492
3148
  message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
2493
3149
  file: loc?.file,
2494
3150
  line: loc?.line,
2495
- 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 }\`.`
3151
+ 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}".`
2496
3152
  });
2497
3153
  continue;
2498
3154
  }
@@ -2503,46 +3159,155 @@ function checkPageCoverage(registry, manifest, baseline) {
2503
3159
  file: loc?.file,
2504
3160
  line: loc?.line,
2505
3161
  entity: { kind: "route", id: r.path },
2506
- 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).`
3162
+ 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).`
2507
3163
  });
2508
3164
  }
2509
- for (const b of baselineSet) {
2510
- if (!routePaths.includes(b)) {
2511
- diagnostics.push({
2512
- code: "stale-page-baseline",
2513
- severity: "warning",
2514
- message: `coverage baseline lists "${b}", which is not a route in the registry`,
2515
- hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
2516
- });
2517
- } else if (covered.has(b) || optedOut.has(b)) {
2518
- diagnostics.push({
2519
- code: "page-captured",
2520
- severity: "warning",
2521
- message: `route "${b}" is now captured but is still in the coverage baseline`,
2522
- hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
2523
- });
2524
- }
3165
+ for (const b of Object.keys(baseline?.acknowledge ?? {})) {
3166
+ if (routePaths.includes(b)) continue;
3167
+ diagnostics.push({
3168
+ code: "stale-page-baseline",
3169
+ severity: "warning",
3170
+ message: `coverage baseline acknowledges "${b}", which is not a route in the registry`,
3171
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
3172
+ });
2525
3173
  }
2526
3174
  return diagnostics;
2527
3175
  }
2528
- function computePageBaseline(registry, manifest) {
2529
- const uncaptured = [];
3176
+ function computePageBacklog(registry, manifest) {
3177
+ const out2 = {};
2530
3178
  for (const d of checkPageCoverage(registry, manifest, {
2531
- uncapturedPages: []
3179
+ version: 2,
3180
+ acknowledge: {}
2532
3181
  })) {
2533
- if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
3182
+ if (d.code === "page-no-capture" && d.entity) {
3183
+ out2[d.entity.id] = { [ACK_ALL]: { type: "backlog" } };
3184
+ }
3185
+ }
3186
+ return out2;
3187
+ }
3188
+
3189
+ // src/scanner/scan/workspace.ts
3190
+ var fs3 = __toESM(require("fs"), 1);
3191
+ var path5 = __toESM(require("path"), 1);
3192
+ var WORKSPACE_MARKERS = [
3193
+ "pnpm-workspace.yaml",
3194
+ "pnpm-workspace.yml",
3195
+ "turbo.json",
3196
+ "lerna.json",
3197
+ "nx.json",
3198
+ ".git"
3199
+ ];
3200
+ var MAX_ASCENT = 8;
3201
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
3202
+ function captureKey(kind, entity) {
3203
+ return `${kind ?? ""}|${entity}`;
3204
+ }
3205
+ function hasMarker(dir) {
3206
+ return WORKSPACE_MARKERS.some((m) => fs3.existsSync(path5.join(dir, m)));
3207
+ }
3208
+ function findWorkspaceRoot(configDir) {
3209
+ let dir = path5.resolve(configDir);
3210
+ for (let i = 0; i < MAX_ASCENT; i++) {
3211
+ const parent = path5.dirname(dir);
3212
+ if (parent === dir) return void 0;
3213
+ dir = parent;
3214
+ if (hasMarker(dir)) return dir;
3215
+ }
3216
+ return void 0;
3217
+ }
3218
+ function captureKeys(manifest) {
3219
+ const out2 = /* @__PURE__ */ new Set();
3220
+ for (const c of manifest?.captured ?? []) {
3221
+ out2.add(captureKey(c.kind, c.entity));
3222
+ }
3223
+ return out2;
3224
+ }
3225
+ function readManifest(configDir, config) {
3226
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3227
+ let raw;
3228
+ try {
3229
+ raw = fs3.readFileSync(path5.resolve(configDir, rel), "utf8");
3230
+ } catch {
3231
+ return void 0;
3232
+ }
3233
+ try {
3234
+ const parsed = JSON.parse(raw);
3235
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
3236
+ const captured = [];
3237
+ for (const entry of parsed.captured) {
3238
+ if (!entry || typeof entry !== "object") continue;
3239
+ const c = entry;
3240
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
3241
+ const rec = { entity: c.entity, state: c.state };
3242
+ if (typeof c.kind === "string") rec.kind = c.kind;
3243
+ captured.push(rec);
3244
+ }
3245
+ return { captured };
3246
+ } catch {
3247
+ return void 0;
2534
3248
  }
2535
- return { uncapturedPages: uncaptured.sort() };
3249
+ }
3250
+ function toApp(dc) {
3251
+ return {
3252
+ name: path5.basename(dc.configDir),
3253
+ configDir: dc.configDir,
3254
+ configPath: dc.configPath,
3255
+ registers: createSourceMatcher(dc.config.sources, {
3256
+ cwd: dc.configDir,
3257
+ globalExcludes: dc.config.exclude
3258
+ }),
3259
+ captured: captureKeys(readManifest(dc.configDir, dc.config))
3260
+ };
3261
+ }
3262
+ function resolveWorkspace(opts) {
3263
+ const { configDir, config } = opts;
3264
+ if (config.workspace === false) return void 0;
3265
+ const root = typeof config.workspace === "string" ? path5.resolve(configDir, config.workspace) : findWorkspaceRoot(configDir);
3266
+ if (!root) return void 0;
3267
+ const configs = opts.configs ?? discover({ cwd: root, all: true });
3268
+ const self = path5.resolve(configDir, CONFIG_FILENAME);
3269
+ const siblings = configs.filter((dc) => path5.resolve(dc.configPath) !== self).map(toApp);
3270
+ if (siblings.length === 0) return void 0;
3271
+ return { root, siblings };
3272
+ }
3273
+ function capturedElsewhere(workspace, absFile, kind, id) {
3274
+ if (!workspace || !absFile) return void 0;
3275
+ for (const app of workspace.siblings) {
3276
+ if (!app.registers(absFile)) continue;
3277
+ if (app.captured.has(captureKey(kind, id)) || app.captured.has(captureKey(void 0, id))) {
3278
+ return app.name;
3279
+ }
3280
+ }
3281
+ return void 0;
2536
3282
  }
2537
3283
 
2538
3284
  // src/scanner/scan/state-coverage.ts
2539
3285
  var STATE_KINDS2 = ["page", "feature", "widget"];
2540
- function declaredStateEntities(registry) {
3286
+ function declaredStateEntities(registry, opts, ungated) {
2541
3287
  const out2 = [];
2542
3288
  for (const kind of STATE_KINDS2) {
2543
3289
  for (const e of registry.list(kind)) {
2544
3290
  const states = e.meta?.states;
2545
3291
  if (!states || states.length === 0) continue;
3292
+ const absFile = e.loc?.file ? opts.resolveSourcePath?.(e.loc.file) : void 0;
3293
+ const ack = entityAcknowledge(
3294
+ e.meta,
3295
+ capturedElsewhere(opts.workspace, absFile, kind, e.id)
3296
+ );
3297
+ if (ack) {
3298
+ if (ack.origin === "workspace") {
3299
+ ungated.push({
3300
+ code: "acknowledged-elsewhere",
3301
+ severity: "info",
3302
+ 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)})`,
3303
+ file: e.loc?.file,
3304
+ line: e.loc?.line,
3305
+ entity: { kind, id: e.id },
3306
+ 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.`
3307
+ });
3308
+ }
3309
+ continue;
3310
+ }
2546
3311
  out2.push({
2547
3312
  kind,
2548
3313
  id: e.id,
@@ -2553,18 +3318,31 @@ function declaredStateEntities(registry) {
2553
3318
  }
2554
3319
  return out2;
2555
3320
  }
2556
- function captureMatchesEntity(cap, ent) {
3321
+ function unreliablePageKinds(registry, captured) {
3322
+ const out2 = /* @__PURE__ */ new Set();
3323
+ for (const c of captured) {
3324
+ if (c.kind !== "page") continue;
3325
+ if (registry.get("page", c.entity)) continue;
3326
+ out2.add(c.entity);
3327
+ }
3328
+ return out2;
3329
+ }
3330
+ function captureMatchesEntity(cap, ent, unreliable = EMPTY) {
2557
3331
  if (cap.entity !== ent.id) return false;
2558
- return cap.kind === void 0 || cap.kind === ent.kind;
3332
+ if (cap.kind === void 0) return true;
3333
+ if (cap.kind === ent.kind) return true;
3334
+ return cap.kind === "page" && unreliable.has(cap.entity);
2559
3335
  }
2560
- function checkStateCoverage(registry, manifest) {
3336
+ var EMPTY = /* @__PURE__ */ new Set();
3337
+ function checkStateCoverage(registry, manifest, opts = {}) {
2561
3338
  const diagnostics = [];
2562
- const declared = declaredStateEntities(registry);
2563
3339
  const captured = manifest.captured ?? [];
3340
+ const unreliable = unreliablePageKinds(registry, captured);
3341
+ const declared = declaredStateEntities(registry, opts, diagnostics);
2564
3342
  for (const ent of declared) {
2565
3343
  for (const state of ent.states) {
2566
3344
  const hit = captured.some(
2567
- (c) => captureMatchesEntity(c, ent) && c.state === state
3345
+ (c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
2568
3346
  );
2569
3347
  if (hit) continue;
2570
3348
  diagnostics.push({
@@ -2579,7 +3357,9 @@ function checkStateCoverage(registry, manifest) {
2579
3357
  }
2580
3358
  }
2581
3359
  for (const cap of captured) {
2582
- const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
3360
+ const candidates = declared.filter(
3361
+ (ent2) => captureMatchesEntity(cap, ent2, unreliable)
3362
+ );
2583
3363
  if (candidates.length === 0) continue;
2584
3364
  const declaredSomewhere = candidates.some(
2585
3365
  (ent2) => ent2.states.includes(cap.state)
@@ -2623,36 +3403,35 @@ function coreKindsByRoute(registry, manifest) {
2623
3403
  }
2624
3404
  return byRoute;
2625
3405
  }
2626
- function pageWaivers(registry, routePage) {
2627
- return registry.get("page", routePage)?.meta?.waivers ?? {};
2628
- }
2629
- function isOptedOut(registry, page) {
2630
- return page ? registry.get("page", page)?.meta?.capture === false : false;
2631
- }
2632
3406
  function checkStateMatrix(registry, manifest, baseline) {
2633
3407
  const routes = registry.list("route");
2634
3408
  if (routes.length === 0) return [];
2635
3409
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2636
3410
  const captured = coreKindsByRoute(registry, manifest);
2637
- const baselined = baseline?.missingKinds ?? {};
2638
3411
  const diagnostics = [];
2639
3412
  for (const [route, kinds] of captured) {
2640
3413
  const page = pageOf.get(route);
2641
- if (isOptedOut(registry, page)) continue;
2642
- const waivers = page ? pageWaivers(registry, page) : {};
3414
+ const ack = routeAcknowledge(registry, baseline, route, page);
2643
3415
  const loc = page ? registry.get("page", page)?.loc : void 0;
2644
- const baselinedKinds = new Set(baselined[route] ?? []);
3416
+ const all = ack.get(ACK_ALL);
3417
+ if (all) {
3418
+ if (all.type === "impossible") {
3419
+ diagnostics.push(staleDiagnostic(all, route, page, loc));
3420
+ }
3421
+ continue;
3422
+ }
2645
3423
  for (const kind of CORE_KINDS) {
2646
3424
  if (kinds.has(kind)) continue;
2647
- if (waivers[kind]) continue;
2648
- if (baselinedKinds.has(kind)) {
3425
+ const entry = ack.get(kind);
3426
+ if (entry?.type === "impossible") continue;
3427
+ if (entry?.type === "backlog") {
2649
3428
  diagnostics.push({
2650
3429
  code: "matrix-backlog",
2651
3430
  severity: "info",
2652
- message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
3431
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
2653
3432
  file: loc?.file,
2654
3433
  line: loc?.line,
2655
- hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
3434
+ 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" }\`.`
2656
3435
  });
2657
3436
  continue;
2658
3437
  }
@@ -2663,42 +3442,43 @@ function checkStateMatrix(registry, manifest, baseline) {
2663
3442
  file: loc?.file,
2664
3443
  line: loc?.line,
2665
3444
  entity: { kind: "route", id: route },
2666
- 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).`
3445
+ 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).`
2667
3446
  });
2668
3447
  }
2669
- }
2670
- for (const [route, kinds] of captured) {
2671
- const page = pageOf.get(route);
2672
- if (!page || isOptedOut(registry, page)) continue;
2673
- const waivers = pageWaivers(registry, page);
2674
- const loc = registry.get("page", page)?.loc;
2675
- for (const kind of Object.keys(waivers)) {
2676
- if (kinds.has(kind)) {
2677
- diagnostics.push({
2678
- code: "stale-waiver",
2679
- severity: "warning",
2680
- message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
2681
- file: loc?.file,
2682
- line: loc?.line,
2683
- entity: { kind: "page", id: page },
2684
- hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
2685
- });
2686
- }
3448
+ for (const [scope, entry] of ack) {
3449
+ if (scope === ACK_ALL || !kinds.has(scope)) continue;
3450
+ diagnostics.push(staleDiagnostic(entry, route, page, loc));
2687
3451
  }
2688
3452
  }
2689
3453
  return diagnostics;
2690
3454
  }
2691
- function computeMissingKinds(registry, manifest) {
3455
+ function staleDiagnostic(entry, route, page, loc) {
3456
+ const what = entry.scope === ACK_ALL ? `route "${route}" is captured` : `route "${route}" does render core kind "${entry.scope}"`;
3457
+ return {
3458
+ code: "stale-acknowledgment",
3459
+ severity: "warning",
3460
+ message: `${page ? `page "${page}"` : `route "${route}"`} acknowledges "${entry.scope}" as ${describeAck(entry)}, but ${what}`,
3461
+ file: loc?.file,
3462
+ line: loc?.line,
3463
+ ...page ? { entity: { kind: "page", id: page } } : {},
3464
+ hint: originHint(entry, page)
3465
+ };
3466
+ }
3467
+ function computeMatrixBacklog(registry, manifest) {
2692
3468
  const routes = registry.list("route");
2693
3469
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2694
3470
  const captured = coreKindsByRoute(registry, manifest);
2695
3471
  const out2 = {};
2696
3472
  for (const [route, kinds] of captured) {
2697
3473
  const page = pageOf.get(route);
2698
- if (isOptedOut(registry, page)) continue;
2699
- const waivers = page ? pageWaivers(registry, page) : {};
2700
- const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
2701
- if (missing.length > 0) out2[route] = missing;
3474
+ const ack = routeAcknowledge(registry, void 0, route, page);
3475
+ if (ack.has(ACK_ALL)) continue;
3476
+ const scopes = {};
3477
+ for (const kind of CORE_KINDS) {
3478
+ if (kinds.has(kind) || ack.has(kind)) continue;
3479
+ scopes[kind] = { type: "backlog" };
3480
+ }
3481
+ if (Object.keys(scopes).length > 0) out2[route] = scopes;
2702
3482
  }
2703
3483
  return out2;
2704
3484
  }
@@ -2722,6 +3502,23 @@ function audit(opts) {
2722
3502
  for (const ef of opts.flowExtracted ?? []) {
2723
3503
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
2724
3504
  }
3505
+ for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
3506
+ if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
3507
+ for (const ef of extracted) {
3508
+ for (const m of ef.metadata ?? []) {
3509
+ for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
3510
+ if (!isPlaceholderReason(ack.reason)) continue;
3511
+ diagnostics.push({
3512
+ code: "placeholder-reason",
3513
+ severity: "warning",
3514
+ message: `acknowledge "${scope}" still has the migration's placeholder reason ("${ack.reason}")`,
3515
+ file: ef.file.displayPath,
3516
+ line: m.loc.line,
3517
+ 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).`
3518
+ });
3519
+ }
3520
+ }
3521
+ }
2725
3522
  if (check && opts.generated !== void 0) {
2726
3523
  const outRel = opts.outputPath ?? config.output;
2727
3524
  const fresh = normalizeForCheck(opts.generated);
@@ -2835,8 +3632,8 @@ function audit(opts) {
2835
3632
  if (typeof m.id !== "string") continue;
2836
3633
  const filePath = ef.file.displayPath;
2837
3634
  const wellKnownName = WELL_KNOWN_FILES[m.kind];
2838
- if (path5.posix.basename(filePath) === wellKnownName) continue;
2839
- const dir = path5.posix.dirname(filePath);
3635
+ if (path6.posix.basename(filePath) === wellKnownName) continue;
3636
+ const dir = path6.posix.dirname(filePath);
2840
3637
  const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
2841
3638
  if (scannedPaths.has(wellKnownPath)) continue;
2842
3639
  const kindLabel = m.kind === "page" ? "Page" : "Feature";
@@ -3050,25 +3847,41 @@ function audit(opts) {
3050
3847
  }
3051
3848
  }
3052
3849
  if (lint && statesEnabled && opts.capturedStates) {
3053
- diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
3850
+ const sourceByDisplay = new Map(
3851
+ files.map((f) => [f.displayPath, f.sourcePath])
3852
+ );
3853
+ diagnostics.push(
3854
+ ...checkStateCoverage(registry, opts.capturedStates, {
3855
+ workspace: opts.workspace,
3856
+ resolveSourcePath: (displayPath) => sourceByDisplay.get(displayPath)
3857
+ })
3858
+ );
3054
3859
  }
3055
3860
  if (lint && pageCoverageEnabled && opts.capturedStates) {
3056
3861
  diagnostics.push(
3057
- ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
3862
+ ...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
3058
3863
  );
3059
3864
  }
3865
+ if (lint && statesEnabled && opts.capturedStates) {
3866
+ diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
3867
+ }
3060
3868
  if (lint && stateMatrixEnabled && opts.capturedStates) {
3061
3869
  diagnostics.push(
3062
- ...checkStateMatrix(registry, opts.capturedStates, {
3063
- missingKinds: opts.pageBaseline?.missingKinds ?? {}
3064
- })
3870
+ ...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
3065
3871
  );
3066
3872
  }
3873
+ const overrides = config.audit?.severity ?? {};
3874
+ const governed = diagnostics.flatMap((d) => {
3875
+ const level = overrides[d.code];
3876
+ if (level === void 0) return [d];
3877
+ if (level === "off") return [];
3878
+ return [{ ...d, severity: level }];
3879
+ });
3067
3880
  const summary = {
3068
- errors: diagnostics.filter((d) => d.severity === "error").length,
3069
- warnings: diagnostics.filter((d) => d.severity === "warning").length
3881
+ errors: governed.filter((d) => d.severity === "error").length,
3882
+ warnings: governed.filter((d) => d.severity === "warning").length
3070
3883
  };
3071
- return { diagnostics, summary };
3884
+ return { diagnostics: governed, summary };
3072
3885
  }
3073
3886
  function lineOfOffset(content, offset) {
3074
3887
  let line = 1;
@@ -3396,6 +4209,30 @@ ${body}
3396
4209
  `;
3397
4210
  }
3398
4211
  var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
4212
+ function stateEntities(list) {
4213
+ return list.map((e) => ({
4214
+ id: e.id,
4215
+ states: (e.meta?.states ?? []).map((s) => s.name)
4216
+ }));
4217
+ }
4218
+ function emitStateMap(groups) {
4219
+ const lines = ["export interface UidexStates {"];
4220
+ for (const g of groups) {
4221
+ const withStates = g.entities.filter((e) => e.states.length > 0);
4222
+ if (withStates.length === 0) {
4223
+ lines.push(` ${g.kind}: never`);
4224
+ continue;
4225
+ }
4226
+ lines.push(` ${g.kind}: {`);
4227
+ for (const e of [...withStates].sort((a, b) => a.id.localeCompare(b.id))) {
4228
+ const states = [...new Set(e.states)].sort().map((s) => JSON.stringify(s)).join(" | ");
4229
+ lines.push(` ${JSON.stringify(e.id)}: ${states}`);
4230
+ }
4231
+ lines.push(" }");
4232
+ }
4233
+ lines.push("}");
4234
+ return lines.join("\n") + "\n";
4235
+ }
3399
4236
  function emitId(name, ids) {
3400
4237
  if (OPAQUE_ID_UNIONS.has(name)) {
3401
4238
  return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
@@ -3483,6 +4320,14 @@ function emit(opts) {
3483
4320
  );
3484
4321
  t.push(emitId("StateId", [...stateNames]));
3485
4322
  t.push("");
4323
+ t.push("// ---- declared state space (kind \u2192 entity \u2192 its own states) ----");
4324
+ t.push(
4325
+ emitStateMap([
4326
+ { kind: "page", entities: stateEntities(pages) },
4327
+ { kind: "feature", entities: stateEntities(features) },
4328
+ { kind: "widget", entities: stateEntities(widgets) }
4329
+ ])
4330
+ );
3486
4331
  t.push("// ---- authoring-surface shape types ----");
3487
4332
  t.push("export namespace Uidex {");
3488
4333
  t.push(
@@ -3495,7 +4340,20 @@ function emit(opts) {
3495
4340
  t.push(" acceptance?: readonly string[]");
3496
4341
  t.push(" description?: string");
3497
4342
  t.push(" }");
3498
- t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
4343
+ t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
4344
+ t.push(" export interface Impossible {");
4345
+ t.push(' type: "impossible"');
4346
+ t.push(" reason: string");
4347
+ t.push(" }");
4348
+ t.push(" export interface Backlog {");
4349
+ t.push(' type: "backlog"');
4350
+ t.push(" reason?: string");
4351
+ t.push(" }");
4352
+ t.push(" export type Acknowledgment = Impossible | Backlog");
4353
+ t.push(
4354
+ " export type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>"
4355
+ );
4356
+ t.push(' export type ComponentAcknowledge = { "*"?: Acknowledgment }');
3499
4357
  t.push(" export interface Page {");
3500
4358
  t.push(" page: PageId | false");
3501
4359
  t.push(" name?: string");
@@ -3503,8 +4361,7 @@ function emit(opts) {
3503
4361
  t.push(" widgets?: readonly WidgetId[]");
3504
4362
  t.push(" acceptance?: readonly string[]");
3505
4363
  t.push(" states?: readonly (string | State)[]");
3506
- t.push(" capture?: boolean");
3507
- t.push(" waivers?: Waivers");
4364
+ t.push(" acknowledge?: Acknowledge");
3508
4365
  t.push(" description?: string");
3509
4366
  t.push(" }");
3510
4367
  t.push(" export interface Feature {");
@@ -3513,6 +4370,7 @@ function emit(opts) {
3513
4370
  t.push(" features?: readonly FeatureId[]");
3514
4371
  t.push(" acceptance?: readonly string[]");
3515
4372
  t.push(" states?: readonly (string | State)[]");
4373
+ t.push(" acknowledge?: ComponentAcknowledge");
3516
4374
  t.push(" description?: string");
3517
4375
  t.push(" }");
3518
4376
  t.push(" export interface Primitive {");
@@ -3525,6 +4383,7 @@ function emit(opts) {
3525
4383
  t.push(" name?: string");
3526
4384
  t.push(" acceptance?: readonly string[]");
3527
4385
  t.push(" states?: readonly (string | State)[]");
4386
+ t.push(" acknowledge?: ComponentAcknowledge");
3528
4387
  t.push(" description?: string");
3529
4388
  t.push(" }");
3530
4389
  t.push(" export interface Region {");
@@ -3620,8 +4479,8 @@ function parseGitHubRef(ref) {
3620
4479
  }
3621
4480
 
3622
4481
  // src/scanner/scan/scaffold.ts
3623
- var fs3 = __toESM(require("fs"), 1);
3624
- var path6 = __toESM(require("path"), 1);
4482
+ var fs4 = __toESM(require("fs"), 1);
4483
+ var path7 = __toESM(require("path"), 1);
3625
4484
  function scaffoldWidgetSpec(opts) {
3626
4485
  return scaffoldSpec({
3627
4486
  registry: opts.registry,
@@ -3647,8 +4506,8 @@ function scaffoldSpec(opts) {
3647
4506
  }
3648
4507
  const criteria = entity.meta?.acceptance ?? [];
3649
4508
  const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
3650
- const outputPath = path6.resolve(outDir, filename);
3651
- if (fs3.existsSync(outputPath) && !force) {
4509
+ const outputPath = path7.resolve(outDir, filename);
4510
+ if (fs4.existsSync(outputPath) && !force) {
3652
4511
  return {
3653
4512
  outputPath,
3654
4513
  written: false,
@@ -3657,8 +4516,8 @@ function scaffoldSpec(opts) {
3657
4516
  };
3658
4517
  }
3659
4518
  const content = renderSpec({ id, criteria, fixtureImport });
3660
- fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
3661
- fs3.writeFileSync(outputPath, content, "utf8");
4519
+ fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
4520
+ fs4.writeFileSync(outputPath, content, "utf8");
3662
4521
  return { outputPath, written: true, skipped: false };
3663
4522
  }
3664
4523
  function capitalize(s) {
@@ -3666,25 +4525,27 @@ function capitalize(s) {
3666
4525
  }
3667
4526
  function renderSpec(args) {
3668
4527
  const lines = [];
3669
- lines.push(
3670
- `import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
3671
- );
4528
+ lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
3672
4529
  lines.push("");
3673
4530
  lines.push(
3674
4531
  `test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
3675
4532
  );
3676
- if (args.criteria.length === 0) {
3677
- lines.push(` test("TODO: add acceptance criteria", async () => {`);
3678
- lines.push(` // TODO`);
4533
+ const stub = (title, body) => {
4534
+ lines.push(` test.fixme(${JSON.stringify(title)}, async ({ uidex }) => {`);
4535
+ for (const l of body) lines.push(` ${l}`);
4536
+ lines.push(` void uidex`);
3679
4537
  lines.push(` })`);
4538
+ lines.push("");
4539
+ };
4540
+ if (args.criteria.length === 0) {
4541
+ stub("TODO: add acceptance criteria", [
4542
+ "// Declare `acceptance: [...]` on the entity, then re-run scaffold."
4543
+ ]);
3680
4544
  } else {
3681
4545
  for (const criterion of args.criteria) {
3682
- lines.push(` test(${JSON.stringify(criterion)}, async ({ uidex }) => {`);
3683
- lines.push(` // TODO: implement criterion`);
3684
- lines.push(` void uidex`);
3685
- lines.push(` expect(true).toBe(true)`);
3686
- lines.push(` })`);
3687
- lines.push("");
4546
+ stub(criterion, [
4547
+ "// TODO: drive this criterion, then remove `.fixme` to arm the test."
4548
+ ]);
3688
4549
  }
3689
4550
  }
3690
4551
  lines.push("})");
@@ -3693,47 +4554,40 @@ function renderSpec(args) {
3693
4554
  }
3694
4555
 
3695
4556
  // src/scanner/scan/pipeline.ts
3696
- var fs4 = __toESM(require("fs"), 1);
3697
- var path7 = __toESM(require("path"), 1);
3698
- var DEFAULT_STATES_MANIFEST = "uidex-states.json";
4557
+ var fs5 = __toESM(require("fs"), 1);
4558
+ var path8 = __toESM(require("path"), 1);
4559
+ var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
3699
4560
  var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
3700
- function loadPageBaseline(configDir, config) {
4561
+ function loadCoverageBaseline(configDir, config) {
3701
4562
  const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3702
- const abs = path7.resolve(configDir, rel);
4563
+ const abs = path8.resolve(configDir, rel);
3703
4564
  let raw;
3704
4565
  try {
3705
- raw = fs4.readFileSync(abs, "utf8");
4566
+ raw = fs5.readFileSync(abs, "utf8");
3706
4567
  } catch {
3707
- return void 0;
4568
+ return {};
3708
4569
  }
4570
+ let parsed;
3709
4571
  try {
3710
- const parsed = JSON.parse(raw);
3711
- if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
3712
- const uncapturedPages = parsed.uncapturedPages.filter(
3713
- (p2) => typeof p2 === "string"
3714
- );
3715
- let missingKinds;
3716
- const mkRaw = parsed.missingKinds;
3717
- if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
3718
- const mk = {};
3719
- for (const [route, kinds] of Object.entries(mkRaw)) {
3720
- if (Array.isArray(kinds)) {
3721
- mk[route] = kinds.filter((k) => typeof k === "string");
3722
- }
3723
- }
3724
- missingKinds = mk;
3725
- }
3726
- return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
4572
+ parsed = JSON.parse(raw);
3727
4573
  } catch {
3728
- return void 0;
4574
+ return {};
3729
4575
  }
4576
+ const legacy = migrateLegacyBaseline({
4577
+ path: abs,
4578
+ raw,
4579
+ parsed,
4580
+ displayPath: rel
4581
+ });
4582
+ if (legacy) return { legacy };
4583
+ return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
3730
4584
  }
3731
4585
  function loadCapturedStates(configDir, config) {
3732
- const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3733
- const abs = path7.resolve(configDir, rel);
4586
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
4587
+ const abs = path8.resolve(configDir, rel);
3734
4588
  let raw;
3735
4589
  try {
3736
- raw = fs4.readFileSync(abs, "utf8");
4590
+ raw = fs5.readFileSync(abs, "utf8");
3737
4591
  } catch {
3738
4592
  return void 0;
3739
4593
  }
@@ -3752,7 +4606,7 @@ function loadCapturedStates(configDir, config) {
3752
4606
  if (typeof c.route === "string") rec.route = c.route;
3753
4607
  captured.push(rec);
3754
4608
  }
3755
- return { captured };
4609
+ return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
3756
4610
  } catch {
3757
4611
  return void 0;
3758
4612
  }
@@ -3788,11 +4642,11 @@ function runOne(dc, opts) {
3788
4642
  gitContext
3789
4643
  });
3790
4644
  const typesRel = config.output;
3791
- const typesPath = path7.resolve(configDir, typesRel);
4645
+ const typesPath = path8.resolve(configDir, typesRel);
3792
4646
  const dataRel = dataOutputPath(config.output);
3793
- const dataPath = path7.resolve(configDir, dataRel);
4647
+ const dataPath = path8.resolve(configDir, dataRel);
3794
4648
  const locsRel = locsOutputPath(config.output);
3795
- const locsPath = path7.resolve(configDir, locsRel);
4649
+ const locsPath = path8.resolve(configDir, locsRel);
3796
4650
  let typesOnDisk = null;
3797
4651
  let dataOnDisk = null;
3798
4652
  let locsOnDisk = null;
@@ -3805,9 +4659,11 @@ function runOne(dc, opts) {
3805
4659
  (ef) => (ef.diagnostics?.length ?? 0) > 0
3806
4660
  );
3807
4661
  const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
3808
- const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
4662
+ const loadedBaseline = opts.coverageBaseline ? { baseline: opts.coverageBaseline } : loadCoverageBaseline(configDir, config);
4663
+ const coverageBaseline = loadedBaseline.baseline;
4664
+ const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
3809
4665
  let auditResult;
3810
- if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
4666
+ if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
3811
4667
  auditResult = audit({
3812
4668
  registry: resolved.registry,
3813
4669
  extracted,
@@ -3830,7 +4686,9 @@ function runOne(dc, opts) {
3830
4686
  locsOnDisk,
3831
4687
  locsOutputPath: locsRel,
3832
4688
  capturedStates,
3833
- pageBaseline
4689
+ coverageBaseline,
4690
+ baselineDiagnostic: loadedBaseline.legacy,
4691
+ workspace
3834
4692
  });
3835
4693
  }
3836
4694
  return {
@@ -3851,15 +4709,15 @@ function runOne(dc, opts) {
3851
4709
  }
3852
4710
  function readOrNull(p2) {
3853
4711
  try {
3854
- return fs4.readFileSync(p2, "utf8");
4712
+ return fs5.readFileSync(p2, "utf8");
3855
4713
  } catch {
3856
4714
  return null;
3857
4715
  }
3858
4716
  }
3859
4717
  function writeArtifact(artifact) {
3860
4718
  if (readOrNull(artifact.outputPath) === artifact.generated) return false;
3861
- fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
3862
- fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4719
+ fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
4720
+ fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
3863
4721
  return true;
3864
4722
  }
3865
4723
  function writeScanResult(result) {
@@ -3868,14 +4726,45 @@ function writeScanResult(result) {
3868
4726
  const wroteLocs = writeArtifact(result.locs);
3869
4727
  return wroteTypes || wroteData || wroteLocs;
3870
4728
  }
3871
- function pageBaselinePath(result) {
4729
+ function coverageBaselinePath(result) {
3872
4730
  const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3873
- return path7.resolve(result.configDir, rel);
4731
+ return path8.resolve(result.configDir, rel);
4732
+ }
4733
+
4734
+ // src/scanner/scan/states-merge.ts
4735
+ function keyOf(c) {
4736
+ return `${c.kind ?? "page"} ${c.entity} ${c.state}`;
4737
+ }
4738
+ function mergeStates(committed, partial) {
4739
+ const byKey = /* @__PURE__ */ new Map();
4740
+ for (const c of committed.captured ?? []) byKey.set(keyOf(c), c);
4741
+ const updated = [];
4742
+ const added = [];
4743
+ for (const c of partial.captured ?? []) {
4744
+ const k = keyOf(c);
4745
+ (byKey.has(k) ? updated : added).push(k);
4746
+ byKey.set(k, c);
4747
+ }
4748
+ const captured = [...byKey.values()].sort(
4749
+ (a, b) => a.entity.localeCompare(b.entity) || a.state.localeCompare(b.state)
4750
+ );
4751
+ return {
4752
+ manifest: {
4753
+ ...committed,
4754
+ captured,
4755
+ // The merged file is only as fresh as its OLDEST evidence: rows carried
4756
+ // over from the committed manifest were not re-verified by this run, so
4757
+ // claiming the partial's newer stamp would defeat the staleness check.
4758
+ ...committed.generatedAt ? { generatedAt: committed.generatedAt } : {}
4759
+ },
4760
+ updated: updated.sort(),
4761
+ added: added.sort()
4762
+ };
3874
4763
  }
3875
4764
 
3876
4765
  // src/scanner/scan/fix.ts
3877
- var fs5 = __toESM(require("fs"), 1);
3878
- var path8 = __toESM(require("path"), 1);
4766
+ var fs6 = __toESM(require("fs"), 1);
4767
+ var path9 = __toESM(require("path"), 1);
3879
4768
  function applyFixes(diagnostics) {
3880
4769
  const entries = [];
3881
4770
  for (const d of diagnostics) {
@@ -3908,7 +4797,7 @@ function applyFixes(diagnostics) {
3908
4797
  for (const [filePath, edits] of editsByFile) {
3909
4798
  let content;
3910
4799
  try {
3911
- content = fs5.readFileSync(filePath, "utf8");
4800
+ content = fs6.readFileSync(filePath, "utf8");
3912
4801
  } catch {
3913
4802
  for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
3914
4803
  continue;
@@ -3928,22 +4817,22 @@ function applyFixes(diagnostics) {
3928
4817
  const edit = kept[i];
3929
4818
  content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
3930
4819
  }
3931
- if (kept.length > 0) fs5.writeFileSync(filePath, content, "utf8");
4820
+ if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
3932
4821
  }
3933
4822
  for (const entry of entries) {
3934
4823
  if (entry.skippedReason) continue;
3935
4824
  for (const create of entry.createFiles) {
3936
- if (fs5.existsSync(create.path)) {
3937
- entry.skippedReason = `${path8.basename(create.path)} already exists`;
4825
+ if (fs6.existsSync(create.path)) {
4826
+ entry.skippedReason = `${path9.basename(create.path)} already exists`;
3938
4827
  continue;
3939
4828
  }
3940
- fs5.mkdirSync(path8.dirname(create.path), { recursive: true });
3941
- fs5.writeFileSync(create.path, create.content, "utf8");
4829
+ fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
4830
+ fs6.writeFileSync(create.path, create.content, "utf8");
3942
4831
  }
3943
4832
  if (entry.skippedReason) continue;
3944
4833
  for (const del of entry.deleteFiles) {
3945
4834
  try {
3946
- fs5.unlinkSync(del);
4835
+ fs6.unlinkSync(del);
3947
4836
  } catch {
3948
4837
  }
3949
4838
  }
@@ -4121,29 +5010,29 @@ function renameEntity(opts) {
4121
5010
  }
4122
5011
 
4123
5012
  // src/scanner/scan/cli.ts
4124
- var fs8 = __toESM(require("fs"), 1);
4125
- var path11 = __toESM(require("path"), 1);
5013
+ var fs9 = __toESM(require("fs"), 1);
5014
+ var path12 = __toESM(require("path"), 1);
4126
5015
 
4127
5016
  // src/scanner/scan/ai/index.ts
4128
5017
  var p = __toESM(require("@clack/prompts"), 1);
4129
5018
 
4130
5019
  // src/scanner/scan/ai/providers/claude.ts
4131
- var fs7 = __toESM(require("fs"), 1);
4132
- var path10 = __toESM(require("path"), 1);
5020
+ var fs8 = __toESM(require("fs"), 1);
5021
+ var path11 = __toESM(require("path"), 1);
4133
5022
 
4134
5023
  // src/scanner/scan/ai/templates.ts
4135
- var fs6 = __toESM(require("fs"), 1);
4136
- var path9 = __toESM(require("path"), 1);
5024
+ var fs7 = __toESM(require("fs"), 1);
5025
+ var path10 = __toESM(require("path"), 1);
4137
5026
  function templatePath(rel) {
4138
5027
  const candidates = [
4139
- path9.resolve(__dirname, "../../templates", rel),
5028
+ path10.resolve(__dirname, "../../templates", rel),
4140
5029
  // dist/cli/cli.cjs → ../../templates
4141
- path9.resolve(__dirname, "../../../../templates", rel)
5030
+ path10.resolve(__dirname, "../../../../templates", rel)
4142
5031
  // src/scanner/scan/ai → ../../../../templates
4143
5032
  ];
4144
5033
  for (const c of candidates) {
4145
5034
  try {
4146
- fs6.accessSync(c, fs6.constants.R_OK);
5035
+ fs7.accessSync(c, fs7.constants.R_OK);
4147
5036
  return c;
4148
5037
  } catch {
4149
5038
  continue;
@@ -4155,7 +5044,7 @@ function templatePath(rel) {
4155
5044
  );
4156
5045
  }
4157
5046
  function readTemplate(rel) {
4158
- return fs6.readFileSync(templatePath(rel), "utf8");
5047
+ return fs7.readFileSync(templatePath(rel), "utf8");
4159
5048
  }
4160
5049
 
4161
5050
  // src/scanner/scan/ai/providers/claude.ts
@@ -4186,8 +5075,8 @@ var claudeProvider = {
4186
5075
  async install({ cwd, force }) {
4187
5076
  const changes = [];
4188
5077
  for (const file of SKILL_FILES) {
4189
- const dest = path10.join(cwd, file.dest);
4190
- const exists = fs7.existsSync(dest);
5078
+ const dest = path11.join(cwd, file.dest);
5079
+ const exists = fs8.existsSync(dest);
4191
5080
  if (exists && !force) {
4192
5081
  changes.push({
4193
5082
  path: file.dest,
@@ -4196,56 +5085,56 @@ var claudeProvider = {
4196
5085
  });
4197
5086
  continue;
4198
5087
  }
4199
- fs7.mkdirSync(path10.dirname(dest), { recursive: true });
4200
- fs7.writeFileSync(dest, readTemplate(file.template));
5088
+ fs8.mkdirSync(path11.dirname(dest), { recursive: true });
5089
+ fs8.writeFileSync(dest, readTemplate(file.template));
4201
5090
  changes.push({
4202
5091
  path: file.dest,
4203
5092
  action: exists ? "overwritten" : "created"
4204
5093
  });
4205
5094
  }
4206
5095
  for (const rel of LEGACY_FILES) {
4207
- const dest = path10.join(cwd, rel);
4208
- if (fs7.existsSync(dest)) {
4209
- fs7.unlinkSync(dest);
5096
+ const dest = path11.join(cwd, rel);
5097
+ if (fs8.existsSync(dest)) {
5098
+ fs8.unlinkSync(dest);
4210
5099
  changes.push({ path: rel, action: "removed" });
4211
5100
  }
4212
5101
  }
4213
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4214
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4215
- cleanupEmpty(path10.join(cwd, ".claude/rules"));
5102
+ cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
5103
+ cleanupEmpty(path11.join(cwd, ".claude/commands"));
5104
+ cleanupEmpty(path11.join(cwd, ".claude/rules"));
4216
5105
  return { changes };
4217
5106
  },
4218
5107
  async uninstall({ cwd }) {
4219
5108
  const changes = [];
4220
5109
  for (const file of SKILL_FILES) {
4221
- const dest = path10.join(cwd, file.dest);
4222
- if (!fs7.existsSync(dest)) {
5110
+ const dest = path11.join(cwd, file.dest);
5111
+ if (!fs8.existsSync(dest)) {
4223
5112
  changes.push({ path: file.dest, action: "skipped", reason: "absent" });
4224
5113
  continue;
4225
5114
  }
4226
- fs7.unlinkSync(dest);
5115
+ fs8.unlinkSync(dest);
4227
5116
  changes.push({ path: file.dest, action: "removed" });
4228
5117
  }
4229
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex/references"));
4230
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex"));
4231
- cleanupEmpty(path10.join(cwd, ".claude/skills"));
5118
+ cleanupEmpty(path11.join(cwd, ".claude/skills/uidex/references"));
5119
+ cleanupEmpty(path11.join(cwd, ".claude/skills/uidex"));
5120
+ cleanupEmpty(path11.join(cwd, ".claude/skills"));
4232
5121
  for (const rel of LEGACY_FILES) {
4233
- const dest = path10.join(cwd, rel);
4234
- if (fs7.existsSync(dest)) {
4235
- fs7.unlinkSync(dest);
5122
+ const dest = path11.join(cwd, rel);
5123
+ if (fs8.existsSync(dest)) {
5124
+ fs8.unlinkSync(dest);
4236
5125
  changes.push({ path: rel, action: "removed" });
4237
5126
  }
4238
5127
  }
4239
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4240
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4241
- cleanupEmpty(path10.join(cwd, ".claude/rules"));
5128
+ cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
5129
+ cleanupEmpty(path11.join(cwd, ".claude/commands"));
5130
+ cleanupEmpty(path11.join(cwd, ".claude/rules"));
4242
5131
  return { changes };
4243
5132
  }
4244
5133
  };
4245
5134
  function cleanupEmpty(dir) {
4246
5135
  try {
4247
- const entries = fs7.readdirSync(dir);
4248
- if (entries.length === 0) fs7.rmdirSync(dir);
5136
+ const entries = fs8.readdirSync(dir);
5137
+ if (entries.length === 0) fs8.rmdirSync(dir);
4249
5138
  } catch {
4250
5139
  }
4251
5140
  }
@@ -4414,6 +5303,8 @@ async function run(opts) {
4414
5303
  return runScaffold(cwd, positional.slice(1), flags, writer);
4415
5304
  case "rename":
4416
5305
  return runRename(cwd, positional.slice(1), flags, writer);
5306
+ case "states":
5307
+ return runStates(cwd, positional.slice(1), writer);
4417
5308
  case "ai": {
4418
5309
  const result = await runAiCommand({
4419
5310
  cwd,
@@ -4442,22 +5333,23 @@ function helpText2() {
4442
5333
  " scan [flags] Run the scanner pipeline",
4443
5334
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
4444
5335
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
5336
+ " states merge [partial] Fold a filtered run's *.partial.json into the committed manifest",
4445
5337
  " ai <install|uninstall|providers> Manage AI assistant integrations",
4446
5338
  "",
4447
5339
  "Flags:",
4448
5340
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
4449
5341
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
4450
5342
  " --audit Equivalent to --check --lint (read-only)",
4451
- " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4452
- " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
5343
+ " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names, migrate waivers/capture to acknowledge), then rescan and write",
5344
+ " --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
4453
5345
  " --json Emit JSON diagnostics on stdout",
4454
5346
  " --force (scaffold) overwrite existing spec",
4455
5347
  ""
4456
5348
  ].join("\n");
4457
5349
  }
4458
5350
  function runInit(cwd, w) {
4459
- const configPath = path11.join(cwd, CONFIG_FILENAME);
4460
- if (fs8.existsSync(configPath)) {
5351
+ const configPath = path12.join(cwd, CONFIG_FILENAME);
5352
+ if (fs9.existsSync(configPath)) {
4461
5353
  w.err(`.uidex.json already exists at ${configPath}`);
4462
5354
  return w.result(1);
4463
5355
  }
@@ -4466,16 +5358,16 @@ function runInit(cwd, w) {
4466
5358
  sources: [{ rootDir: "src" }],
4467
5359
  output: "src/uidex.gen.ts"
4468
5360
  };
4469
- fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
5361
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4470
5362
  w.out(`Created ${configPath}`);
4471
- const gitignorePath = path11.join(cwd, ".gitignore");
5363
+ const gitignorePath = path12.join(cwd, ".gitignore");
4472
5364
  const entry = "*.gen.*";
4473
- if (fs8.existsSync(gitignorePath)) {
4474
- const existing = fs8.readFileSync(gitignorePath, "utf8");
5365
+ if (fs9.existsSync(gitignorePath)) {
5366
+ const existing = fs9.readFileSync(gitignorePath, "utf8");
4475
5367
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
4476
5368
  if (!hasEntry) {
4477
5369
  const needsNewline = existing.length > 0 && !existing.endsWith("\n");
4478
- fs8.appendFileSync(
5370
+ fs9.appendFileSync(
4479
5371
  gitignorePath,
4480
5372
  `${needsNewline ? "\n" : ""}${entry}
4481
5373
  `,
@@ -4484,7 +5376,7 @@ function runInit(cwd, w) {
4484
5376
  w.out(`Appended ${entry} to ${gitignorePath}`);
4485
5377
  }
4486
5378
  } else {
4487
- fs8.writeFileSync(gitignorePath, `${entry}
5379
+ fs9.writeFileSync(gitignorePath, `${entry}
4488
5380
  `, "utf8");
4489
5381
  w.out(`Created ${gitignorePath} with ${entry}`);
4490
5382
  }
@@ -4523,20 +5415,25 @@ function runScanCommand(cwd, flags, w) {
4523
5415
  if (updateBaseline) {
4524
5416
  for (const r of results) {
4525
5417
  const manifest = r.capturedStates ?? { captured: [] };
4526
- const pageBaseline = computePageBaseline(r.registry, manifest);
4527
- const missingKinds = computeMissingKinds(r.registry, manifest);
4528
- const baseline = {
4529
- ...pageBaseline,
4530
- ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
5418
+ const acknowledge = {};
5419
+ const merge = (from) => {
5420
+ for (const [route, scopes] of Object.entries(from)) {
5421
+ acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
5422
+ }
4531
5423
  };
4532
- const p2 = pageBaselinePath(r);
4533
- fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4534
- const kindGaps = Object.values(missingKinds).reduce(
4535
- (n, ks) => n + ks.length,
5424
+ merge(computePageBacklog(r.registry, manifest));
5425
+ merge(computeMatrixBacklog(r.registry, manifest));
5426
+ const baseline = { version: 2, acknowledge };
5427
+ const p2 = coverageBaselinePath(r);
5428
+ fs9.writeFileSync(p2, serializeCoverageBaseline(baseline));
5429
+ const entries = Object.values(acknowledge);
5430
+ const pages = entries.filter((s) => s["*"]).length;
5431
+ const kindGaps = entries.reduce(
5432
+ (n, s) => n + Object.keys(s).filter((k) => k !== "*").length,
4536
5433
  0
4537
5434
  );
4538
5435
  w.out(
4539
- `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
5436
+ `Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
4540
5437
  );
4541
5438
  }
4542
5439
  return w.result(0);
@@ -4609,7 +5506,7 @@ function runScaffold(cwd, args, flags, w) {
4609
5506
  for (const r of results) {
4610
5507
  const entity = r.registry.get(scaffoldKind, id);
4611
5508
  if (!entity) continue;
4612
- const outDir = path11.resolve(r.configDir, "e2e");
5509
+ const outDir = path12.resolve(r.configDir, "e2e");
4613
5510
  const result = scaffoldSpec({
4614
5511
  registry: r.registry,
4615
5512
  kind: scaffoldKind,
@@ -4630,6 +5527,53 @@ function runScaffold(cwd, args, flags, w) {
4630
5527
  return w.result(1);
4631
5528
  }
4632
5529
  var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
5530
+ function runStates(cwd, args, w) {
5531
+ const [sub, explicitPartial] = args;
5532
+ if (sub !== "merge") {
5533
+ w.err("Usage: uidex states merge [partial-manifest]");
5534
+ return w.result(1);
5535
+ }
5536
+ const configs = discover({ cwd });
5537
+ if (configs.length === 0) {
5538
+ w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
5539
+ return w.result(1);
5540
+ }
5541
+ let merged = 0;
5542
+ for (const { configDir, config } of configs) {
5543
+ const rel = config.statesManifest ?? "uidex-states.json";
5544
+ const target = path12.resolve(configDir, rel);
5545
+ const partial = explicitPartial ? path12.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
5546
+ const committedRaw = readJson(target);
5547
+ const partialRaw = readJson(partial);
5548
+ if (!partialRaw) continue;
5549
+ if (!committedRaw) {
5550
+ w.err(`No committed manifest at ${rel}; nothing to merge into`);
5551
+ return w.result(1);
5552
+ }
5553
+ const result = mergeStates(committedRaw, partialRaw);
5554
+ fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
5555
+ merged++;
5556
+ w.out(
5557
+ `Merged ${path12.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
5558
+ );
5559
+ for (const k of result.added) w.out(` + ${k}`);
5560
+ for (const k of result.updated) w.out(` ~ ${k}`);
5561
+ }
5562
+ if (merged === 0) {
5563
+ w.out("No partial manifest found; nothing to merge.");
5564
+ }
5565
+ return w.result(0);
5566
+ }
5567
+ function readJson(p2) {
5568
+ try {
5569
+ const parsed = JSON.parse(fs9.readFileSync(p2, "utf8"));
5570
+ if (!parsed || typeof parsed !== "object") return null;
5571
+ const m = parsed;
5572
+ return Array.isArray(m.captured) ? m : null;
5573
+ } catch {
5574
+ return null;
5575
+ }
5576
+ }
4633
5577
  function runRename(cwd, args, flags, w) {
4634
5578
  const [kind, oldId, newId] = args;
4635
5579
  if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
@@ -4678,35 +5622,51 @@ function createWriter() {
4678
5622
  }
4679
5623
  // Annotate the CommonJS export names for ESM import in node:
4680
5624
  0 && (module.exports = {
5625
+ ACK_ALL,
5626
+ ACK_SCOPES,
4681
5627
  CONFIG_FILENAME,
4682
5628
  ConfigError,
4683
5629
  DEFAULT_CONVENTIONS,
5630
+ DIAGNOSTIC_CODES,
4684
5631
  applyFixes,
4685
5632
  audit,
5633
+ capturedElsewhere,
5634
+ checkComponentCoverage,
4686
5635
  checkPageCoverage,
4687
5636
  checkStateCoverage,
4688
5637
  checkStateMatrix,
4689
5638
  compileRoute,
4690
- computeMissingKinds,
4691
- computePageBaseline,
5639
+ computeMatrixBacklog,
5640
+ computePageBacklog,
5641
+ coverageBaselinePath,
4692
5642
  detectRoutes,
4693
5643
  discover,
4694
5644
  emit,
5645
+ entityAcknowledge,
4695
5646
  extract,
4696
5647
  extractUidexExports,
5648
+ findWorkspaceRoot,
4697
5649
  globToRegExp,
4698
- loadPageBaseline,
5650
+ isLegacyBaseline,
5651
+ loadCoverageBaseline,
4699
5652
  matchUrlToRoute,
4700
- pageBaselinePath,
5653
+ mergeStates,
5654
+ migrateBaseline,
5655
+ migrateLegacyBaseline,
5656
+ migrateLegacyFields,
4701
5657
  parseConfig,
5658
+ parseCoverageBaseline,
4702
5659
  pathToId,
4703
5660
  renameEntity,
4704
5661
  resolve,
4705
5662
  resolveGitContext,
5663
+ resolveWorkspace,
5664
+ routeAcknowledge,
4706
5665
  runCli,
4707
5666
  runScan,
4708
5667
  scaffoldSpec,
4709
5668
  scaffoldWidgetSpec,
5669
+ serializeCoverageBaseline,
4710
5670
  validateConfig,
4711
5671
  walk,
4712
5672
  writeScanResult