uidex 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/cli.cjs +1190 -335
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +3 -0
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +19 -13
  6. package/dist/headless/index.d.ts +19 -13
  7. package/dist/headless/index.js +3 -0
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +3 -0
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +19 -13
  12. package/dist/index.d.ts +19 -13
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +36 -7
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.js +36 -7
  18. package/dist/playwright/index.js.map +1 -1
  19. package/dist/playwright/states-reporter.cjs +36 -7
  20. package/dist/playwright/states-reporter.cjs.map +1 -1
  21. package/dist/playwright/states-reporter.d.cts +15 -0
  22. package/dist/playwright/states-reporter.d.ts +15 -0
  23. package/dist/playwright/states-reporter.js +36 -7
  24. package/dist/playwright/states-reporter.js.map +1 -1
  25. package/dist/playwright/states.cjs +8 -0
  26. package/dist/playwright/states.cjs.map +1 -1
  27. package/dist/playwright/states.d.cts +44 -1
  28. package/dist/playwright/states.d.ts +44 -1
  29. package/dist/playwright/states.js +7 -0
  30. package/dist/playwright/states.js.map +1 -1
  31. package/dist/react/index.cjs +3 -0
  32. package/dist/react/index.cjs.map +1 -1
  33. package/dist/react/index.d.cts +19 -13
  34. package/dist/react/index.d.ts +19 -13
  35. package/dist/react/index.js +3 -0
  36. package/dist/react/index.js.map +1 -1
  37. package/dist/scan/index.cjs +1129 -244
  38. package/dist/scan/index.cjs.map +1 -1
  39. package/dist/scan/index.d.cts +379 -100
  40. package/dist/scan/index.d.ts +379 -100
  41. package/dist/scan/index.js +1109 -240
  42. package/dist/scan/index.js.map +1 -1
  43. package/package.json +1 -1
@@ -30,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
  }
@@ -226,7 +307,8 @@ function validateConfig(raw) {
226
307
  audit: raw.audit,
227
308
  conventions: raw.conventions,
228
309
  statesManifest: raw.statesManifest,
229
- coverageBaseline: raw.coverageBaseline
310
+ coverageBaseline: raw.coverageBaseline,
311
+ workspace: raw.workspace
230
312
  };
231
313
  }
232
314
  function parseConfig(json) {
@@ -276,9 +358,11 @@ function tryReadConfig(configPath) {
276
358
  function discover(options = {}) {
277
359
  const cwd = options.cwd ?? process.cwd();
278
360
  const maxDepth = options.maxDepth ?? MAX_DEPTH;
361
+ const all = options.all ?? false;
279
362
  const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
280
- if (rootMatch) return [rootMatch];
363
+ if (rootMatch && !all) return [rootMatch];
281
364
  const results = [];
365
+ if (rootMatch) results.push(rootMatch);
282
366
  const queue = [[cwd, 0]];
283
367
  while (queue.length > 0) {
284
368
  const [dir, depth] = queue.shift();
@@ -382,6 +466,26 @@ function toPosix(p2) {
382
466
  function matchesAny(rel, patterns) {
383
467
  return patterns.some((g) => globToRegExp(g).test(rel));
384
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
+ }
385
489
  function walk(sources, options) {
386
490
  const { cwd, globalExcludes = [], includeTests = false } = options;
387
491
  const out2 = [];
@@ -715,6 +819,132 @@ function createRegistry() {
715
819
  };
716
820
  }
717
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
+
718
948
  // src/scanner/scan/extract-uidex-export.ts
719
949
  var KIND_DISCRIMINATORS = [
720
950
  "page",
@@ -733,26 +963,41 @@ var ALLOWED_FIELDS = {
733
963
  "widgets",
734
964
  "acceptance",
735
965
  "states",
966
+ "acknowledge",
736
967
  "capture",
737
968
  "waivers",
738
969
  "description"
739
970
  ]),
740
- // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
741
- // they are page-only (the gates read them off the page). A feature/widget that
742
- // 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).
743
977
  feature: /* @__PURE__ */ new Set([
744
978
  "feature",
745
979
  "name",
746
980
  "features",
747
981
  "acceptance",
748
982
  "states",
983
+ "acknowledge",
984
+ "capture",
749
985
  "description"
750
986
  ]),
751
987
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
752
- 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
+ ]),
753
997
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
754
998
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
755
999
  };
1000
+ var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
756
1001
  var STATE_FIELDS = /* @__PURE__ */ new Set([
757
1002
  "name",
758
1003
  "kind",
@@ -1003,7 +1248,8 @@ function objectLit(node, content, p2, pos, span) {
1003
1248
  key,
1004
1249
  value,
1005
1250
  keyPos,
1006
- span: removalSpan(content, prop.start, prop.end)
1251
+ span: removalSpan(content, prop.start, prop.end),
1252
+ propSpan: { start: prop.start, end: prop.end }
1007
1253
  });
1008
1254
  }
1009
1255
  return { kind: "object", entries, pos, span };
@@ -1151,8 +1397,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1151
1397
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1152
1398
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1153
1399
  const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
1154
- const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
1155
- 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;
1156
1404
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1157
1405
  const metadata = {
1158
1406
  source: "ts-export",
@@ -1176,15 +1424,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1176
1424
  metadata.widgetSpans = widgetsField.spans;
1177
1425
  }
1178
1426
  if (states?.length) metadata.states = states;
1179
- if (capture !== void 0) metadata.capture = capture;
1180
- 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
+ }
1181
1434
  if (notFlow) metadata.notFlow = true;
1182
1435
  if (typeof id === "string" && idValue.kind === "string") {
1183
1436
  metadata.idSpan = idValue.span;
1184
1437
  }
1185
1438
  const fieldSpans = {};
1186
- 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
+ }
1187
1444
  metadata.fieldSpans = fieldSpans;
1445
+ metadata.fieldPropSpans = fieldPropSpans;
1188
1446
  return metadata;
1189
1447
  }
1190
1448
  function readIdField(value, kind, fieldName) {
@@ -1214,6 +1472,80 @@ function readIdField(value, kind, fieldName) {
1214
1472
  value.pos
1215
1473
  );
1216
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
+ }
1217
1549
  function readWaiversField(byKey) {
1218
1550
  const entry = byKey.get("waivers");
1219
1551
  if (!entry) return void 0;
@@ -2062,9 +2394,8 @@ function buildMetaFromExport(exp) {
2062
2394
  ...s.description ? { description: s.description } : {}
2063
2395
  }));
2064
2396
  }
2065
- if (exp.capture === false) meta.capture = false;
2066
- if (exp.waivers && Object.keys(exp.waivers).length > 0) {
2067
- meta.waivers = exp.waivers;
2397
+ if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
2398
+ meta.acknowledge = exp.acknowledge;
2068
2399
  }
2069
2400
  return Object.keys(meta).length > 0 ? meta : void 0;
2070
2401
  }
@@ -2409,6 +2740,45 @@ function resolve2(ctx) {
2409
2740
  };
2410
2741
  registry.add(element);
2411
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
+ };
2412
2782
  if (ctx.flowFiles && conventions.flows) {
2413
2783
  for (const ff of ctx.flowFiles) {
2414
2784
  const notFlowExport = (ff.metadata ?? []).find(
@@ -2418,7 +2788,10 @@ function resolve2(ctx) {
2418
2788
  const flowExport = (ff.metadata ?? []).find(
2419
2789
  (m) => m.kind === "flow" && typeof m.id === "string"
2420
2790
  );
2421
- const derived = flowsFromFacts(ff);
2791
+ const derived = flowsFromFacts(ff).map((f) => ({
2792
+ ...f,
2793
+ touches: expandTouches(f.touches)
2794
+ }));
2422
2795
  if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
2423
2796
  const base = derived[0];
2424
2797
  const flow = {
@@ -2487,7 +2860,195 @@ function dedupe(arr) {
2487
2860
  }
2488
2861
 
2489
2862
  // src/scanner/scan/audit.ts
2490
- 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
+ }
2491
3052
 
2492
3053
  // src/scanner/scan/page-coverage.ts
2493
3054
  function compileRoute(pattern) {
@@ -2510,11 +3071,11 @@ function compileRoute(pattern) {
2510
3071
  return { test: (url) => re.test(url), specificity };
2511
3072
  }
2512
3073
  function matchUrlToRoute(url, routePaths) {
2513
- const path12 = stripQuery(url);
3074
+ const path13 = stripQuery(url);
2514
3075
  let best = null;
2515
3076
  for (const rp of routePaths) {
2516
3077
  const { test, specificity } = compileRoute(rp);
2517
- if (!test(path12)) continue;
3078
+ if (!test(path13)) continue;
2518
3079
  if (!best || specificity > best.specificity)
2519
3080
  best = { path: rp, specificity };
2520
3081
  }
@@ -2545,29 +3106,49 @@ function checkPageCoverage(registry, manifest, baseline) {
2545
3106
  if (routes.length === 0) return [];
2546
3107
  const routePaths = routes.map((r) => r.path);
2547
3108
  const captured = manifest.captured ?? [];
2548
- const optedOut = /* @__PURE__ */ new Set();
2549
- for (const r of routes) {
2550
- const page = registry.get("page", r.page);
2551
- if (page?.meta?.capture === false) optedOut.add(r.path);
2552
- }
2553
3109
  const covered = /* @__PURE__ */ new Set();
2554
3110
  for (const c of captured) {
2555
3111
  const route = routeForCapture(c, routes, routePaths);
2556
3112
  if (route) covered.add(route);
2557
3113
  }
2558
- const baselineSet = new Set(baseline?.uncapturedPages ?? []);
2559
3114
  const diagnostics = [];
2560
3115
  for (const r of routes) {
2561
- 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
+ );
2562
3119
  const loc = registry.get("page", r.page)?.loc;
2563
- 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") {
2564
3145
  diagnostics.push({
2565
3146
  code: "page-backlog",
2566
3147
  severity: "info",
2567
3148
  message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
2568
3149
  file: loc?.file,
2569
3150
  line: loc?.line,
2570
- 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}".`
2571
3152
  });
2572
3153
  continue;
2573
3154
  }
@@ -2578,46 +3159,155 @@ function checkPageCoverage(registry, manifest, baseline) {
2578
3159
  file: loc?.file,
2579
3160
  line: loc?.line,
2580
3161
  entity: { kind: "route", id: r.path },
2581
- 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).`
2582
3163
  });
2583
3164
  }
2584
- for (const b of baselineSet) {
2585
- if (!routePaths.includes(b)) {
2586
- diagnostics.push({
2587
- code: "stale-page-baseline",
2588
- severity: "warning",
2589
- message: `coverage baseline lists "${b}", which is not a route in the registry`,
2590
- hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
2591
- });
2592
- } else if (covered.has(b) || optedOut.has(b)) {
2593
- diagnostics.push({
2594
- code: "page-captured",
2595
- severity: "warning",
2596
- message: `route "${b}" is now captured but is still in the coverage baseline`,
2597
- hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
2598
- });
2599
- }
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
+ });
2600
3173
  }
2601
3174
  return diagnostics;
2602
3175
  }
2603
- function computePageBaseline(registry, manifest) {
2604
- const uncaptured = [];
3176
+ function computePageBacklog(registry, manifest) {
3177
+ const out2 = {};
2605
3178
  for (const d of checkPageCoverage(registry, manifest, {
2606
- uncapturedPages: []
3179
+ version: 2,
3180
+ acknowledge: {}
2607
3181
  })) {
2608
- 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
+ }
2609
3185
  }
2610
- return { uncapturedPages: uncaptured.sort() };
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;
3248
+ }
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;
2611
3282
  }
2612
3283
 
2613
3284
  // src/scanner/scan/state-coverage.ts
2614
3285
  var STATE_KINDS2 = ["page", "feature", "widget"];
2615
- function declaredStateEntities(registry) {
3286
+ function declaredStateEntities(registry, opts, ungated) {
2616
3287
  const out2 = [];
2617
3288
  for (const kind of STATE_KINDS2) {
2618
3289
  for (const e of registry.list(kind)) {
2619
3290
  const states = e.meta?.states;
2620
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
+ }
2621
3311
  out2.push({
2622
3312
  kind,
2623
3313
  id: e.id,
@@ -2628,18 +3318,31 @@ function declaredStateEntities(registry) {
2628
3318
  }
2629
3319
  return out2;
2630
3320
  }
2631
- 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) {
2632
3331
  if (cap.entity !== ent.id) return false;
2633
- 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);
2634
3335
  }
2635
- function checkStateCoverage(registry, manifest) {
3336
+ var EMPTY = /* @__PURE__ */ new Set();
3337
+ function checkStateCoverage(registry, manifest, opts = {}) {
2636
3338
  const diagnostics = [];
2637
- const declared = declaredStateEntities(registry);
2638
3339
  const captured = manifest.captured ?? [];
3340
+ const unreliable = unreliablePageKinds(registry, captured);
3341
+ const declared = declaredStateEntities(registry, opts, diagnostics);
2639
3342
  for (const ent of declared) {
2640
3343
  for (const state of ent.states) {
2641
3344
  const hit = captured.some(
2642
- (c) => captureMatchesEntity(c, ent) && c.state === state
3345
+ (c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
2643
3346
  );
2644
3347
  if (hit) continue;
2645
3348
  diagnostics.push({
@@ -2654,7 +3357,9 @@ function checkStateCoverage(registry, manifest) {
2654
3357
  }
2655
3358
  }
2656
3359
  for (const cap of captured) {
2657
- const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
3360
+ const candidates = declared.filter(
3361
+ (ent2) => captureMatchesEntity(cap, ent2, unreliable)
3362
+ );
2658
3363
  if (candidates.length === 0) continue;
2659
3364
  const declaredSomewhere = candidates.some(
2660
3365
  (ent2) => ent2.states.includes(cap.state)
@@ -2698,36 +3403,35 @@ function coreKindsByRoute(registry, manifest) {
2698
3403
  }
2699
3404
  return byRoute;
2700
3405
  }
2701
- function pageWaivers(registry, routePage) {
2702
- return registry.get("page", routePage)?.meta?.waivers ?? {};
2703
- }
2704
- function isOptedOut(registry, page) {
2705
- return page ? registry.get("page", page)?.meta?.capture === false : false;
2706
- }
2707
3406
  function checkStateMatrix(registry, manifest, baseline) {
2708
3407
  const routes = registry.list("route");
2709
3408
  if (routes.length === 0) return [];
2710
3409
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2711
3410
  const captured = coreKindsByRoute(registry, manifest);
2712
- const baselined = baseline?.missingKinds ?? {};
2713
3411
  const diagnostics = [];
2714
3412
  for (const [route, kinds] of captured) {
2715
3413
  const page = pageOf.get(route);
2716
- if (isOptedOut(registry, page)) continue;
2717
- const waivers = page ? pageWaivers(registry, page) : {};
3414
+ const ack = routeAcknowledge(registry, baseline, route, page);
2718
3415
  const loc = page ? registry.get("page", page)?.loc : void 0;
2719
- 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
+ }
2720
3423
  for (const kind of CORE_KINDS) {
2721
3424
  if (kinds.has(kind)) continue;
2722
- if (waivers[kind]) continue;
2723
- if (baselinedKinds.has(kind)) {
3425
+ const entry = ack.get(kind);
3426
+ if (entry?.type === "impossible") continue;
3427
+ if (entry?.type === "backlog") {
2724
3428
  diagnostics.push({
2725
3429
  code: "matrix-backlog",
2726
3430
  severity: "info",
2727
- 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)`,
2728
3432
  file: loc?.file,
2729
3433
  line: loc?.line,
2730
- 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" }\`.`
2731
3435
  });
2732
3436
  continue;
2733
3437
  }
@@ -2738,42 +3442,43 @@ function checkStateMatrix(registry, manifest, baseline) {
2738
3442
  file: loc?.file,
2739
3443
  line: loc?.line,
2740
3444
  entity: { kind: "route", id: route },
2741
- 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).`
2742
3446
  });
2743
3447
  }
2744
- }
2745
- for (const [route, kinds] of captured) {
2746
- const page = pageOf.get(route);
2747
- if (!page || isOptedOut(registry, page)) continue;
2748
- const waivers = pageWaivers(registry, page);
2749
- const loc = registry.get("page", page)?.loc;
2750
- for (const kind of Object.keys(waivers)) {
2751
- if (kinds.has(kind)) {
2752
- diagnostics.push({
2753
- code: "stale-waiver",
2754
- severity: "warning",
2755
- message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
2756
- file: loc?.file,
2757
- line: loc?.line,
2758
- entity: { kind: "page", id: page },
2759
- hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
2760
- });
2761
- }
3448
+ for (const [scope, entry] of ack) {
3449
+ if (scope === ACK_ALL || !kinds.has(scope)) continue;
3450
+ diagnostics.push(staleDiagnostic(entry, route, page, loc));
2762
3451
  }
2763
3452
  }
2764
3453
  return diagnostics;
2765
3454
  }
2766
- 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) {
2767
3468
  const routes = registry.list("route");
2768
3469
  const pageOf = new Map(routes.map((r) => [r.path, r.page]));
2769
3470
  const captured = coreKindsByRoute(registry, manifest);
2770
3471
  const out2 = {};
2771
3472
  for (const [route, kinds] of captured) {
2772
3473
  const page = pageOf.get(route);
2773
- if (isOptedOut(registry, page)) continue;
2774
- const waivers = page ? pageWaivers(registry, page) : {};
2775
- const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
2776
- 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;
2777
3482
  }
2778
3483
  return out2;
2779
3484
  }
@@ -2797,6 +3502,23 @@ function audit(opts) {
2797
3502
  for (const ef of opts.flowExtracted ?? []) {
2798
3503
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
2799
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
+ }
2800
3522
  if (check && opts.generated !== void 0) {
2801
3523
  const outRel = opts.outputPath ?? config.output;
2802
3524
  const fresh = normalizeForCheck(opts.generated);
@@ -2910,8 +3632,8 @@ function audit(opts) {
2910
3632
  if (typeof m.id !== "string") continue;
2911
3633
  const filePath = ef.file.displayPath;
2912
3634
  const wellKnownName = WELL_KNOWN_FILES[m.kind];
2913
- if (path5.posix.basename(filePath) === wellKnownName) continue;
2914
- const dir = path5.posix.dirname(filePath);
3635
+ if (path6.posix.basename(filePath) === wellKnownName) continue;
3636
+ const dir = path6.posix.dirname(filePath);
2915
3637
  const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
2916
3638
  if (scannedPaths.has(wellKnownPath)) continue;
2917
3639
  const kindLabel = m.kind === "page" ? "Page" : "Feature";
@@ -3125,25 +3847,41 @@ function audit(opts) {
3125
3847
  }
3126
3848
  }
3127
3849
  if (lint && statesEnabled && opts.capturedStates) {
3128
- 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
+ );
3129
3859
  }
3130
3860
  if (lint && pageCoverageEnabled && opts.capturedStates) {
3131
3861
  diagnostics.push(
3132
- ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
3862
+ ...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
3133
3863
  );
3134
3864
  }
3865
+ if (lint && statesEnabled && opts.capturedStates) {
3866
+ diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
3867
+ }
3135
3868
  if (lint && stateMatrixEnabled && opts.capturedStates) {
3136
3869
  diagnostics.push(
3137
- ...checkStateMatrix(registry, opts.capturedStates, {
3138
- missingKinds: opts.pageBaseline?.missingKinds ?? {}
3139
- })
3870
+ ...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
3140
3871
  );
3141
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
+ });
3142
3880
  const summary = {
3143
- errors: diagnostics.filter((d) => d.severity === "error").length,
3144
- 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
3145
3883
  };
3146
- return { diagnostics, summary };
3884
+ return { diagnostics: governed, summary };
3147
3885
  }
3148
3886
  function lineOfOffset(content, offset) {
3149
3887
  let line = 1;
@@ -3471,6 +4209,30 @@ ${body}
3471
4209
  `;
3472
4210
  }
3473
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
+ }
3474
4236
  function emitId(name, ids) {
3475
4237
  if (OPAQUE_ID_UNIONS.has(name)) {
3476
4238
  return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
@@ -3558,6 +4320,14 @@ function emit(opts) {
3558
4320
  );
3559
4321
  t.push(emitId("StateId", [...stateNames]));
3560
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
+ );
3561
4331
  t.push("// ---- authoring-surface shape types ----");
3562
4332
  t.push("export namespace Uidex {");
3563
4333
  t.push(
@@ -3570,7 +4340,20 @@ function emit(opts) {
3570
4340
  t.push(" acceptance?: readonly string[]");
3571
4341
  t.push(" description?: string");
3572
4342
  t.push(" }");
3573
- 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 }');
3574
4357
  t.push(" export interface Page {");
3575
4358
  t.push(" page: PageId | false");
3576
4359
  t.push(" name?: string");
@@ -3578,8 +4361,7 @@ function emit(opts) {
3578
4361
  t.push(" widgets?: readonly WidgetId[]");
3579
4362
  t.push(" acceptance?: readonly string[]");
3580
4363
  t.push(" states?: readonly (string | State)[]");
3581
- t.push(" capture?: boolean");
3582
- t.push(" waivers?: Waivers");
4364
+ t.push(" acknowledge?: Acknowledge");
3583
4365
  t.push(" description?: string");
3584
4366
  t.push(" }");
3585
4367
  t.push(" export interface Feature {");
@@ -3588,6 +4370,7 @@ function emit(opts) {
3588
4370
  t.push(" features?: readonly FeatureId[]");
3589
4371
  t.push(" acceptance?: readonly string[]");
3590
4372
  t.push(" states?: readonly (string | State)[]");
4373
+ t.push(" acknowledge?: ComponentAcknowledge");
3591
4374
  t.push(" description?: string");
3592
4375
  t.push(" }");
3593
4376
  t.push(" export interface Primitive {");
@@ -3600,6 +4383,7 @@ function emit(opts) {
3600
4383
  t.push(" name?: string");
3601
4384
  t.push(" acceptance?: readonly string[]");
3602
4385
  t.push(" states?: readonly (string | State)[]");
4386
+ t.push(" acknowledge?: ComponentAcknowledge");
3603
4387
  t.push(" description?: string");
3604
4388
  t.push(" }");
3605
4389
  t.push(" export interface Region {");
@@ -3695,8 +4479,8 @@ function parseGitHubRef(ref) {
3695
4479
  }
3696
4480
 
3697
4481
  // src/scanner/scan/scaffold.ts
3698
- var fs3 = __toESM(require("fs"), 1);
3699
- var path6 = __toESM(require("path"), 1);
4482
+ var fs4 = __toESM(require("fs"), 1);
4483
+ var path7 = __toESM(require("path"), 1);
3700
4484
  function scaffoldWidgetSpec(opts) {
3701
4485
  return scaffoldSpec({
3702
4486
  registry: opts.registry,
@@ -3722,8 +4506,8 @@ function scaffoldSpec(opts) {
3722
4506
  }
3723
4507
  const criteria = entity.meta?.acceptance ?? [];
3724
4508
  const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
3725
- const outputPath = path6.resolve(outDir, filename);
3726
- if (fs3.existsSync(outputPath) && !force) {
4509
+ const outputPath = path7.resolve(outDir, filename);
4510
+ if (fs4.existsSync(outputPath) && !force) {
3727
4511
  return {
3728
4512
  outputPath,
3729
4513
  written: false,
@@ -3732,8 +4516,8 @@ function scaffoldSpec(opts) {
3732
4516
  };
3733
4517
  }
3734
4518
  const content = renderSpec({ id, criteria, fixtureImport });
3735
- fs3.mkdirSync(path6.dirname(outputPath), { recursive: true });
3736
- fs3.writeFileSync(outputPath, content, "utf8");
4519
+ fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
4520
+ fs4.writeFileSync(outputPath, content, "utf8");
3737
4521
  return { outputPath, written: true, skipped: false };
3738
4522
  }
3739
4523
  function capitalize(s) {
@@ -3741,25 +4525,27 @@ function capitalize(s) {
3741
4525
  }
3742
4526
  function renderSpec(args) {
3743
4527
  const lines = [];
3744
- lines.push(
3745
- `import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
3746
- );
4528
+ lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
3747
4529
  lines.push("");
3748
4530
  lines.push(
3749
4531
  `test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
3750
4532
  );
3751
- if (args.criteria.length === 0) {
3752
- lines.push(` test("TODO: add acceptance criteria", async () => {`);
3753
- 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`);
3754
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
+ ]);
3755
4544
  } else {
3756
4545
  for (const criterion of args.criteria) {
3757
- lines.push(` test(${JSON.stringify(criterion)}, async ({ uidex }) => {`);
3758
- lines.push(` // TODO: implement criterion`);
3759
- lines.push(` void uidex`);
3760
- lines.push(` expect(true).toBe(true)`);
3761
- lines.push(` })`);
3762
- lines.push("");
4546
+ stub(criterion, [
4547
+ "// TODO: drive this criterion, then remove `.fixme` to arm the test."
4548
+ ]);
3763
4549
  }
3764
4550
  }
3765
4551
  lines.push("})");
@@ -3768,47 +4554,40 @@ function renderSpec(args) {
3768
4554
  }
3769
4555
 
3770
4556
  // src/scanner/scan/pipeline.ts
3771
- var fs4 = __toESM(require("fs"), 1);
3772
- var path7 = __toESM(require("path"), 1);
3773
- 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";
3774
4560
  var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
3775
- function loadPageBaseline(configDir, config) {
4561
+ function loadCoverageBaseline(configDir, config) {
3776
4562
  const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3777
- const abs = path7.resolve(configDir, rel);
4563
+ const abs = path8.resolve(configDir, rel);
3778
4564
  let raw;
3779
4565
  try {
3780
- raw = fs4.readFileSync(abs, "utf8");
4566
+ raw = fs5.readFileSync(abs, "utf8");
3781
4567
  } catch {
3782
- return void 0;
4568
+ return {};
3783
4569
  }
4570
+ let parsed;
3784
4571
  try {
3785
- const parsed = JSON.parse(raw);
3786
- if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
3787
- const uncapturedPages = parsed.uncapturedPages.filter(
3788
- (p2) => typeof p2 === "string"
3789
- );
3790
- let missingKinds;
3791
- const mkRaw = parsed.missingKinds;
3792
- if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
3793
- const mk = {};
3794
- for (const [route, kinds] of Object.entries(mkRaw)) {
3795
- if (Array.isArray(kinds)) {
3796
- mk[route] = kinds.filter((k) => typeof k === "string");
3797
- }
3798
- }
3799
- missingKinds = mk;
3800
- }
3801
- return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
4572
+ parsed = JSON.parse(raw);
3802
4573
  } catch {
3803
- return void 0;
4574
+ return {};
3804
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 };
3805
4584
  }
3806
4585
  function loadCapturedStates(configDir, config) {
3807
- const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3808
- const abs = path7.resolve(configDir, rel);
4586
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
4587
+ const abs = path8.resolve(configDir, rel);
3809
4588
  let raw;
3810
4589
  try {
3811
- raw = fs4.readFileSync(abs, "utf8");
4590
+ raw = fs5.readFileSync(abs, "utf8");
3812
4591
  } catch {
3813
4592
  return void 0;
3814
4593
  }
@@ -3827,7 +4606,7 @@ function loadCapturedStates(configDir, config) {
3827
4606
  if (typeof c.route === "string") rec.route = c.route;
3828
4607
  captured.push(rec);
3829
4608
  }
3830
- return { captured };
4609
+ return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
3831
4610
  } catch {
3832
4611
  return void 0;
3833
4612
  }
@@ -3863,11 +4642,11 @@ function runOne(dc, opts) {
3863
4642
  gitContext
3864
4643
  });
3865
4644
  const typesRel = config.output;
3866
- const typesPath = path7.resolve(configDir, typesRel);
4645
+ const typesPath = path8.resolve(configDir, typesRel);
3867
4646
  const dataRel = dataOutputPath(config.output);
3868
- const dataPath = path7.resolve(configDir, dataRel);
4647
+ const dataPath = path8.resolve(configDir, dataRel);
3869
4648
  const locsRel = locsOutputPath(config.output);
3870
- const locsPath = path7.resolve(configDir, locsRel);
4649
+ const locsPath = path8.resolve(configDir, locsRel);
3871
4650
  let typesOnDisk = null;
3872
4651
  let dataOnDisk = null;
3873
4652
  let locsOnDisk = null;
@@ -3880,9 +4659,11 @@ function runOne(dc, opts) {
3880
4659
  (ef) => (ef.diagnostics?.length ?? 0) > 0
3881
4660
  );
3882
4661
  const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
3883
- 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 });
3884
4665
  let auditResult;
3885
- if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
4666
+ if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
3886
4667
  auditResult = audit({
3887
4668
  registry: resolved.registry,
3888
4669
  extracted,
@@ -3905,7 +4686,9 @@ function runOne(dc, opts) {
3905
4686
  locsOnDisk,
3906
4687
  locsOutputPath: locsRel,
3907
4688
  capturedStates,
3908
- pageBaseline
4689
+ coverageBaseline,
4690
+ baselineDiagnostic: loadedBaseline.legacy,
4691
+ workspace
3909
4692
  });
3910
4693
  }
3911
4694
  return {
@@ -3926,15 +4709,15 @@ function runOne(dc, opts) {
3926
4709
  }
3927
4710
  function readOrNull(p2) {
3928
4711
  try {
3929
- return fs4.readFileSync(p2, "utf8");
4712
+ return fs5.readFileSync(p2, "utf8");
3930
4713
  } catch {
3931
4714
  return null;
3932
4715
  }
3933
4716
  }
3934
4717
  function writeArtifact(artifact) {
3935
4718
  if (readOrNull(artifact.outputPath) === artifact.generated) return false;
3936
- fs4.mkdirSync(path7.dirname(artifact.outputPath), { recursive: true });
3937
- fs4.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4719
+ fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
4720
+ fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
3938
4721
  return true;
3939
4722
  }
3940
4723
  function writeScanResult(result) {
@@ -3943,14 +4726,45 @@ function writeScanResult(result) {
3943
4726
  const wroteLocs = writeArtifact(result.locs);
3944
4727
  return wroteTypes || wroteData || wroteLocs;
3945
4728
  }
3946
- function pageBaselinePath(result) {
4729
+ function coverageBaselinePath(result) {
3947
4730
  const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3948
- 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
+ };
3949
4763
  }
3950
4764
 
3951
4765
  // src/scanner/scan/fix.ts
3952
- var fs5 = __toESM(require("fs"), 1);
3953
- var path8 = __toESM(require("path"), 1);
4766
+ var fs6 = __toESM(require("fs"), 1);
4767
+ var path9 = __toESM(require("path"), 1);
3954
4768
  function applyFixes(diagnostics) {
3955
4769
  const entries = [];
3956
4770
  for (const d of diagnostics) {
@@ -3983,7 +4797,7 @@ function applyFixes(diagnostics) {
3983
4797
  for (const [filePath, edits] of editsByFile) {
3984
4798
  let content;
3985
4799
  try {
3986
- content = fs5.readFileSync(filePath, "utf8");
4800
+ content = fs6.readFileSync(filePath, "utf8");
3987
4801
  } catch {
3988
4802
  for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
3989
4803
  continue;
@@ -4003,22 +4817,22 @@ function applyFixes(diagnostics) {
4003
4817
  const edit = kept[i];
4004
4818
  content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
4005
4819
  }
4006
- if (kept.length > 0) fs5.writeFileSync(filePath, content, "utf8");
4820
+ if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
4007
4821
  }
4008
4822
  for (const entry of entries) {
4009
4823
  if (entry.skippedReason) continue;
4010
4824
  for (const create of entry.createFiles) {
4011
- if (fs5.existsSync(create.path)) {
4012
- entry.skippedReason = `${path8.basename(create.path)} already exists`;
4825
+ if (fs6.existsSync(create.path)) {
4826
+ entry.skippedReason = `${path9.basename(create.path)} already exists`;
4013
4827
  continue;
4014
4828
  }
4015
- fs5.mkdirSync(path8.dirname(create.path), { recursive: true });
4016
- fs5.writeFileSync(create.path, create.content, "utf8");
4829
+ fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
4830
+ fs6.writeFileSync(create.path, create.content, "utf8");
4017
4831
  }
4018
4832
  if (entry.skippedReason) continue;
4019
4833
  for (const del of entry.deleteFiles) {
4020
4834
  try {
4021
- fs5.unlinkSync(del);
4835
+ fs6.unlinkSync(del);
4022
4836
  } catch {
4023
4837
  }
4024
4838
  }
@@ -4196,29 +5010,29 @@ function renameEntity(opts) {
4196
5010
  }
4197
5011
 
4198
5012
  // src/scanner/scan/cli.ts
4199
- var fs8 = __toESM(require("fs"), 1);
4200
- var path11 = __toESM(require("path"), 1);
5013
+ var fs9 = __toESM(require("fs"), 1);
5014
+ var path12 = __toESM(require("path"), 1);
4201
5015
 
4202
5016
  // src/scanner/scan/ai/index.ts
4203
5017
  var p = __toESM(require("@clack/prompts"), 1);
4204
5018
 
4205
5019
  // src/scanner/scan/ai/providers/claude.ts
4206
- var fs7 = __toESM(require("fs"), 1);
4207
- var path10 = __toESM(require("path"), 1);
5020
+ var fs8 = __toESM(require("fs"), 1);
5021
+ var path11 = __toESM(require("path"), 1);
4208
5022
 
4209
5023
  // src/scanner/scan/ai/templates.ts
4210
- var fs6 = __toESM(require("fs"), 1);
4211
- var path9 = __toESM(require("path"), 1);
5024
+ var fs7 = __toESM(require("fs"), 1);
5025
+ var path10 = __toESM(require("path"), 1);
4212
5026
  function templatePath(rel) {
4213
5027
  const candidates = [
4214
- path9.resolve(__dirname, "../../templates", rel),
5028
+ path10.resolve(__dirname, "../../templates", rel),
4215
5029
  // dist/cli/cli.cjs → ../../templates
4216
- path9.resolve(__dirname, "../../../../templates", rel)
5030
+ path10.resolve(__dirname, "../../../../templates", rel)
4217
5031
  // src/scanner/scan/ai → ../../../../templates
4218
5032
  ];
4219
5033
  for (const c of candidates) {
4220
5034
  try {
4221
- fs6.accessSync(c, fs6.constants.R_OK);
5035
+ fs7.accessSync(c, fs7.constants.R_OK);
4222
5036
  return c;
4223
5037
  } catch {
4224
5038
  continue;
@@ -4230,7 +5044,7 @@ function templatePath(rel) {
4230
5044
  );
4231
5045
  }
4232
5046
  function readTemplate(rel) {
4233
- return fs6.readFileSync(templatePath(rel), "utf8");
5047
+ return fs7.readFileSync(templatePath(rel), "utf8");
4234
5048
  }
4235
5049
 
4236
5050
  // src/scanner/scan/ai/providers/claude.ts
@@ -4261,8 +5075,8 @@ var claudeProvider = {
4261
5075
  async install({ cwd, force }) {
4262
5076
  const changes = [];
4263
5077
  for (const file of SKILL_FILES) {
4264
- const dest = path10.join(cwd, file.dest);
4265
- const exists = fs7.existsSync(dest);
5078
+ const dest = path11.join(cwd, file.dest);
5079
+ const exists = fs8.existsSync(dest);
4266
5080
  if (exists && !force) {
4267
5081
  changes.push({
4268
5082
  path: file.dest,
@@ -4271,56 +5085,56 @@ var claudeProvider = {
4271
5085
  });
4272
5086
  continue;
4273
5087
  }
4274
- fs7.mkdirSync(path10.dirname(dest), { recursive: true });
4275
- fs7.writeFileSync(dest, readTemplate(file.template));
5088
+ fs8.mkdirSync(path11.dirname(dest), { recursive: true });
5089
+ fs8.writeFileSync(dest, readTemplate(file.template));
4276
5090
  changes.push({
4277
5091
  path: file.dest,
4278
5092
  action: exists ? "overwritten" : "created"
4279
5093
  });
4280
5094
  }
4281
5095
  for (const rel of LEGACY_FILES) {
4282
- const dest = path10.join(cwd, rel);
4283
- if (fs7.existsSync(dest)) {
4284
- fs7.unlinkSync(dest);
5096
+ const dest = path11.join(cwd, rel);
5097
+ if (fs8.existsSync(dest)) {
5098
+ fs8.unlinkSync(dest);
4285
5099
  changes.push({ path: rel, action: "removed" });
4286
5100
  }
4287
5101
  }
4288
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4289
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4290
- 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"));
4291
5105
  return { changes };
4292
5106
  },
4293
5107
  async uninstall({ cwd }) {
4294
5108
  const changes = [];
4295
5109
  for (const file of SKILL_FILES) {
4296
- const dest = path10.join(cwd, file.dest);
4297
- if (!fs7.existsSync(dest)) {
5110
+ const dest = path11.join(cwd, file.dest);
5111
+ if (!fs8.existsSync(dest)) {
4298
5112
  changes.push({ path: file.dest, action: "skipped", reason: "absent" });
4299
5113
  continue;
4300
5114
  }
4301
- fs7.unlinkSync(dest);
5115
+ fs8.unlinkSync(dest);
4302
5116
  changes.push({ path: file.dest, action: "removed" });
4303
5117
  }
4304
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex/references"));
4305
- cleanupEmpty(path10.join(cwd, ".claude/skills/uidex"));
4306
- 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"));
4307
5121
  for (const rel of LEGACY_FILES) {
4308
- const dest = path10.join(cwd, rel);
4309
- if (fs7.existsSync(dest)) {
4310
- fs7.unlinkSync(dest);
5122
+ const dest = path11.join(cwd, rel);
5123
+ if (fs8.existsSync(dest)) {
5124
+ fs8.unlinkSync(dest);
4311
5125
  changes.push({ path: rel, action: "removed" });
4312
5126
  }
4313
5127
  }
4314
- cleanupEmpty(path10.join(cwd, ".claude/commands/uidex"));
4315
- cleanupEmpty(path10.join(cwd, ".claude/commands"));
4316
- 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"));
4317
5131
  return { changes };
4318
5132
  }
4319
5133
  };
4320
5134
  function cleanupEmpty(dir) {
4321
5135
  try {
4322
- const entries = fs7.readdirSync(dir);
4323
- if (entries.length === 0) fs7.rmdirSync(dir);
5136
+ const entries = fs8.readdirSync(dir);
5137
+ if (entries.length === 0) fs8.rmdirSync(dir);
4324
5138
  } catch {
4325
5139
  }
4326
5140
  }
@@ -4489,6 +5303,8 @@ async function run(opts) {
4489
5303
  return runScaffold(cwd, positional.slice(1), flags, writer);
4490
5304
  case "rename":
4491
5305
  return runRename(cwd, positional.slice(1), flags, writer);
5306
+ case "states":
5307
+ return runStates(cwd, positional.slice(1), writer);
4492
5308
  case "ai": {
4493
5309
  const result = await runAiCommand({
4494
5310
  cwd,
@@ -4517,22 +5333,23 @@ function helpText2() {
4517
5333
  " scan [flags] Run the scanner pipeline",
4518
5334
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
4519
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",
4520
5337
  " ai <install|uninstall|providers> Manage AI assistant integrations",
4521
5338
  "",
4522
5339
  "Flags:",
4523
5340
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
4524
5341
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
4525
5342
  " --audit Equivalent to --check --lint (read-only)",
4526
- " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4527
- " --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",
4528
5345
  " --json Emit JSON diagnostics on stdout",
4529
5346
  " --force (scaffold) overwrite existing spec",
4530
5347
  ""
4531
5348
  ].join("\n");
4532
5349
  }
4533
5350
  function runInit(cwd, w) {
4534
- const configPath = path11.join(cwd, CONFIG_FILENAME);
4535
- if (fs8.existsSync(configPath)) {
5351
+ const configPath = path12.join(cwd, CONFIG_FILENAME);
5352
+ if (fs9.existsSync(configPath)) {
4536
5353
  w.err(`.uidex.json already exists at ${configPath}`);
4537
5354
  return w.result(1);
4538
5355
  }
@@ -4541,16 +5358,16 @@ function runInit(cwd, w) {
4541
5358
  sources: [{ rootDir: "src" }],
4542
5359
  output: "src/uidex.gen.ts"
4543
5360
  };
4544
- fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
5361
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4545
5362
  w.out(`Created ${configPath}`);
4546
- const gitignorePath = path11.join(cwd, ".gitignore");
5363
+ const gitignorePath = path12.join(cwd, ".gitignore");
4547
5364
  const entry = "*.gen.*";
4548
- if (fs8.existsSync(gitignorePath)) {
4549
- const existing = fs8.readFileSync(gitignorePath, "utf8");
5365
+ if (fs9.existsSync(gitignorePath)) {
5366
+ const existing = fs9.readFileSync(gitignorePath, "utf8");
4550
5367
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
4551
5368
  if (!hasEntry) {
4552
5369
  const needsNewline = existing.length > 0 && !existing.endsWith("\n");
4553
- fs8.appendFileSync(
5370
+ fs9.appendFileSync(
4554
5371
  gitignorePath,
4555
5372
  `${needsNewline ? "\n" : ""}${entry}
4556
5373
  `,
@@ -4559,7 +5376,7 @@ function runInit(cwd, w) {
4559
5376
  w.out(`Appended ${entry} to ${gitignorePath}`);
4560
5377
  }
4561
5378
  } else {
4562
- fs8.writeFileSync(gitignorePath, `${entry}
5379
+ fs9.writeFileSync(gitignorePath, `${entry}
4563
5380
  `, "utf8");
4564
5381
  w.out(`Created ${gitignorePath} with ${entry}`);
4565
5382
  }
@@ -4598,20 +5415,25 @@ function runScanCommand(cwd, flags, w) {
4598
5415
  if (updateBaseline) {
4599
5416
  for (const r of results) {
4600
5417
  const manifest = r.capturedStates ?? { captured: [] };
4601
- const pageBaseline = computePageBaseline(r.registry, manifest);
4602
- const missingKinds = computeMissingKinds(r.registry, manifest);
4603
- const baseline = {
4604
- ...pageBaseline,
4605
- ...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
+ }
4606
5423
  };
4607
- const p2 = pageBaselinePath(r);
4608
- fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4609
- const kindGaps = Object.values(missingKinds).reduce(
4610
- (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,
4611
5433
  0
4612
5434
  );
4613
5435
  w.out(
4614
- `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`
4615
5437
  );
4616
5438
  }
4617
5439
  return w.result(0);
@@ -4684,7 +5506,7 @@ function runScaffold(cwd, args, flags, w) {
4684
5506
  for (const r of results) {
4685
5507
  const entity = r.registry.get(scaffoldKind, id);
4686
5508
  if (!entity) continue;
4687
- const outDir = path11.resolve(r.configDir, "e2e");
5509
+ const outDir = path12.resolve(r.configDir, "e2e");
4688
5510
  const result = scaffoldSpec({
4689
5511
  registry: r.registry,
4690
5512
  kind: scaffoldKind,
@@ -4705,6 +5527,53 @@ function runScaffold(cwd, args, flags, w) {
4705
5527
  return w.result(1);
4706
5528
  }
4707
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
+ }
4708
5577
  function runRename(cwd, args, flags, w) {
4709
5578
  const [kind, oldId, newId] = args;
4710
5579
  if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
@@ -4753,35 +5622,51 @@ function createWriter() {
4753
5622
  }
4754
5623
  // Annotate the CommonJS export names for ESM import in node:
4755
5624
  0 && (module.exports = {
5625
+ ACK_ALL,
5626
+ ACK_SCOPES,
4756
5627
  CONFIG_FILENAME,
4757
5628
  ConfigError,
4758
5629
  DEFAULT_CONVENTIONS,
5630
+ DIAGNOSTIC_CODES,
4759
5631
  applyFixes,
4760
5632
  audit,
5633
+ capturedElsewhere,
5634
+ checkComponentCoverage,
4761
5635
  checkPageCoverage,
4762
5636
  checkStateCoverage,
4763
5637
  checkStateMatrix,
4764
5638
  compileRoute,
4765
- computeMissingKinds,
4766
- computePageBaseline,
5639
+ computeMatrixBacklog,
5640
+ computePageBacklog,
5641
+ coverageBaselinePath,
4767
5642
  detectRoutes,
4768
5643
  discover,
4769
5644
  emit,
5645
+ entityAcknowledge,
4770
5646
  extract,
4771
5647
  extractUidexExports,
5648
+ findWorkspaceRoot,
4772
5649
  globToRegExp,
4773
- loadPageBaseline,
5650
+ isLegacyBaseline,
5651
+ loadCoverageBaseline,
4774
5652
  matchUrlToRoute,
4775
- pageBaselinePath,
5653
+ mergeStates,
5654
+ migrateBaseline,
5655
+ migrateLegacyBaseline,
5656
+ migrateLegacyFields,
4776
5657
  parseConfig,
5658
+ parseCoverageBaseline,
4777
5659
  pathToId,
4778
5660
  renameEntity,
4779
5661
  resolve,
4780
5662
  resolveGitContext,
5663
+ resolveWorkspace,
5664
+ routeAcknowledge,
4781
5665
  runCli,
4782
5666
  runScan,
4783
5667
  scaffoldSpec,
4784
5668
  scaffoldWidgetSpec,
5669
+ serializeCoverageBaseline,
4785
5670
  validateConfig,
4786
5671
  walk,
4787
5672
  writeScanResult