uidex 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cli/cli.cjs +1112 -1054
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +4 -448
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +41 -11
  6. package/dist/headless/index.d.ts +41 -11
  7. package/dist/headless/index.js +4 -450
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +147 -3252
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +43 -316
  12. package/dist/index.d.ts +43 -316
  13. package/dist/index.js +133 -3254
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +175 -0
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.d.cts +2 -0
  18. package/dist/playwright/index.d.ts +2 -0
  19. package/dist/playwright/index.js +167 -0
  20. package/dist/playwright/index.js.map +1 -1
  21. package/dist/playwright/states-reporter.cjs +123 -0
  22. package/dist/playwright/states-reporter.cjs.map +1 -0
  23. package/dist/playwright/states-reporter.d.cts +46 -0
  24. package/dist/playwright/states-reporter.d.ts +46 -0
  25. package/dist/playwright/states-reporter.js +88 -0
  26. package/dist/playwright/states-reporter.js.map +1 -0
  27. package/dist/playwright/states.cjs +118 -0
  28. package/dist/playwright/states.cjs.map +1 -0
  29. package/dist/playwright/states.d.cts +120 -0
  30. package/dist/playwright/states.d.ts +120 -0
  31. package/dist/playwright/states.js +88 -0
  32. package/dist/playwright/states.js.map +1 -0
  33. package/dist/react/index.cjs +163 -3255
  34. package/dist/react/index.cjs.map +1 -1
  35. package/dist/react/index.d.cts +62 -275
  36. package/dist/react/index.d.ts +62 -275
  37. package/dist/react/index.js +151 -3268
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/scan/index.cjs +1292 -345
  40. package/dist/scan/index.cjs.map +1 -1
  41. package/dist/scan/index.d.cts +305 -12
  42. package/dist/scan/index.d.ts +305 -12
  43. package/dist/scan/index.js +1283 -345
  44. package/dist/scan/index.js.map +1 -1
  45. package/package.json +12 -16
  46. package/dist/cloud/index.cjs +0 -682
  47. package/dist/cloud/index.cjs.map +0 -1
  48. package/dist/cloud/index.d.cts +0 -270
  49. package/dist/cloud/index.d.ts +0 -270
  50. package/dist/cloud/index.js +0 -645
  51. package/dist/cloud/index.js.map +0 -1
package/dist/cli/cli.cjs CHANGED
@@ -181,14 +181,14 @@ ${helpText()}`);
181
181
  const provider = await selectProvider(opts, flags.provider);
182
182
  if (!provider) return out(0, "Cancelled.\n");
183
183
  if (sub === "install") {
184
- const result3 = await provider.install({
184
+ const result2 = await provider.install({
185
185
  cwd,
186
186
  force: flags.force === true
187
187
  });
188
- return out(0, formatChanges(provider, "Installed", result3));
188
+ return out(0, formatChanges(provider, "Installed", result2));
189
189
  }
190
- const result2 = await provider.uninstall({ cwd });
191
- return out(0, formatChanges(provider, "Uninstalled", result2));
190
+ const result = await provider.uninstall({ cwd });
191
+ return out(0, formatChanges(provider, "Uninstalled", result));
192
192
  }
193
193
  async function selectProvider(opts, explicit) {
194
194
  if (explicit) {
@@ -231,9 +231,9 @@ function parseFlags(args) {
231
231
  }
232
232
  return flags;
233
233
  }
234
- function formatChanges(provider, verb, result2) {
234
+ function formatChanges(provider, verb, result) {
235
235
  const lines = [`${verb} ${provider.label}:`];
236
- for (const c of result2.changes) lines.push(` ${describe(c)}`);
236
+ for (const c of result.changes) lines.push(` ${describe(c)}`);
237
237
  return lines.join("\n") + "\n";
238
238
  }
239
239
  function describe(c) {
@@ -300,7 +300,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
300
300
  "output",
301
301
  "flows",
302
302
  "audit",
303
- "conventions"
303
+ "conventions",
304
+ "statesManifest",
305
+ "coverageBaseline"
304
306
  ]);
305
307
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
306
308
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -310,7 +312,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
310
312
  "flows",
311
313
  "regions"
312
314
  ]);
313
- var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acceptance"]);
315
+ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
316
+ "scopeLeak",
317
+ "coverage",
318
+ "acceptance",
319
+ "states",
320
+ "pageCoverage",
321
+ "stateMatrix"
322
+ ]);
314
323
  function fail(msg) {
315
324
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
316
325
  }
@@ -369,6 +378,12 @@ function validateConfig(raw) {
369
378
  }
370
379
  if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
371
380
  if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
381
+ if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
382
+ fail(`"statesManifest" must be a string`);
383
+ }
384
+ if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
385
+ fail(`"coverageBaseline" must be a string`);
386
+ }
372
387
  if (raw.audit !== void 0) {
373
388
  assertObject(raw.audit, "audit");
374
389
  for (const key of Object.keys(raw.audit)) {
@@ -392,7 +407,13 @@ function validateConfig(raw) {
392
407
  fail(`conventions.features must be a string or false`);
393
408
  }
394
409
  if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
395
- fail(`conventions.pages must be "auto" or false`);
410
+ const p2 = c.pages;
411
+ const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
412
+ if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
413
+ fail(
414
+ `conventions.pages must be "auto", false, or { routesDir: string }`
415
+ );
416
+ }
396
417
  }
397
418
  if (c.flows !== void 0 && c.flows !== false) {
398
419
  assertStringArray(c.flows, "conventions.flows");
@@ -408,7 +429,9 @@ function validateConfig(raw) {
408
429
  output: raw.output,
409
430
  flows: raw.flows,
410
431
  audit: raw.audit,
411
- conventions: raw.conventions
432
+ conventions: raw.conventions,
433
+ statesManifest: raw.statesManifest,
434
+ coverageBaseline: raw.coverageBaseline
412
435
  };
413
436
  }
414
437
  function parseConfig(json) {
@@ -598,6 +621,12 @@ var ENTITY_KINDS = [
598
621
  "primitive",
599
622
  "flow"
600
623
  ];
624
+ var CORE_STATE_KINDS = [
625
+ "loading",
626
+ "empty",
627
+ "populated",
628
+ "error"
629
+ ];
601
630
  function isMetaKind(kind) {
602
631
  return kind !== "route" && kind !== "flow";
603
632
  }
@@ -731,11 +760,11 @@ function createRegistry() {
731
760
  };
732
761
  const query = (predicate) => {
733
762
  const flows = getFlows();
734
- const result2 = [];
763
+ const result = [];
735
764
  for (const entity of allEntities()) {
736
- if (predicate(entity)) result2.push(freezeEntity(entity, flows));
765
+ if (predicate(entity)) result.push(freezeEntity(entity, flows));
737
766
  }
738
- return result2;
767
+ return result;
739
768
  };
740
769
  const byScope = (scope) => query(
741
770
  (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
@@ -776,6 +805,295 @@ function createRegistry() {
776
805
  };
777
806
  }
778
807
 
808
+ // src/scanner/scan/page-coverage.ts
809
+ function compileRoute(pattern) {
810
+ const segs = pattern.split("/").filter(Boolean);
811
+ let specificity = 0;
812
+ const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
813
+ const effective = optionalTail ? segs.slice(0, -1) : segs;
814
+ const parts = effective.map((seg) => {
815
+ const catchAll = /^\[\.{3}.+\]$/.test(seg);
816
+ const dynamic = /^\[.+\]$/.test(seg);
817
+ if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
818
+ if (dynamic) return "[^/]+";
819
+ specificity++;
820
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
821
+ });
822
+ let body = parts.length === 0 ? "" : "/" + parts.join("/");
823
+ if (optionalTail) body += "(?:/[^/]+)*";
824
+ if (body === "") body = "/";
825
+ const re = new RegExp(`^${body}/?$`);
826
+ return { test: (url) => re.test(url), specificity };
827
+ }
828
+ function matchUrlToRoute(url, routePaths) {
829
+ const path12 = stripQuery(url);
830
+ let best = null;
831
+ for (const rp of routePaths) {
832
+ const { test, specificity } = compileRoute(rp);
833
+ if (!test(path12)) continue;
834
+ if (!best || specificity > best.specificity)
835
+ best = { path: rp, specificity };
836
+ }
837
+ return best?.path ?? null;
838
+ }
839
+ function stripQuery(url) {
840
+ const q = url.indexOf("?");
841
+ const h = url.indexOf("#");
842
+ let end = url.length;
843
+ if (q !== -1) end = Math.min(end, q);
844
+ if (h !== -1) end = Math.min(end, h);
845
+ return url.slice(0, end);
846
+ }
847
+ function routeForCapture(c, routes, routePaths) {
848
+ if (c.route && routePaths.includes(c.route)) return c.route;
849
+ if (c.url) {
850
+ const m = matchUrlToRoute(c.url, routePaths);
851
+ if (m) return m;
852
+ }
853
+ if (c.kind === "page") {
854
+ const r = routes.find((r2) => r2.page === c.entity);
855
+ if (r) return r.path;
856
+ }
857
+ return null;
858
+ }
859
+ function checkPageCoverage(registry, manifest, baseline) {
860
+ const routes = registry.list("route");
861
+ if (routes.length === 0) return [];
862
+ const routePaths = routes.map((r) => r.path);
863
+ const captured = manifest.captured ?? [];
864
+ const optedOut = /* @__PURE__ */ new Set();
865
+ for (const r of routes) {
866
+ const page = registry.get("page", r.page);
867
+ if (page?.meta?.capture === false) optedOut.add(r.path);
868
+ }
869
+ const covered = /* @__PURE__ */ new Set();
870
+ for (const c of captured) {
871
+ const route = routeForCapture(c, routes, routePaths);
872
+ if (route) covered.add(route);
873
+ }
874
+ const baselineSet = new Set(baseline?.uncapturedPages ?? []);
875
+ const diagnostics = [];
876
+ for (const r of routes) {
877
+ if (covered.has(r.path) || optedOut.has(r.path)) continue;
878
+ const loc = registry.get("page", r.page)?.loc;
879
+ if (baselineSet.has(r.path)) {
880
+ diagnostics.push({
881
+ code: "page-backlog",
882
+ severity: "info",
883
+ message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
884
+ file: loc?.file,
885
+ line: loc?.line,
886
+ hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`.`
887
+ });
888
+ continue;
889
+ }
890
+ diagnostics.push({
891
+ code: "page-no-capture",
892
+ severity: "error",
893
+ message: `route "${r.path}" has no visual-states capture and is not accounted for`,
894
+ file: loc?.file,
895
+ line: loc?.line,
896
+ entity: { kind: "route", id: r.path },
897
+ hint: `Capture the route in a states spec, opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`, or grandfather it as acknowledged backlog with \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
898
+ });
899
+ }
900
+ for (const b of baselineSet) {
901
+ if (!routePaths.includes(b)) {
902
+ diagnostics.push({
903
+ code: "stale-page-baseline",
904
+ severity: "warning",
905
+ message: `coverage baseline lists "${b}", which is not a route in the registry`,
906
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
907
+ });
908
+ } else if (covered.has(b) || optedOut.has(b)) {
909
+ diagnostics.push({
910
+ code: "page-captured",
911
+ severity: "warning",
912
+ message: `route "${b}" is now captured but is still in the coverage baseline`,
913
+ hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
914
+ });
915
+ }
916
+ }
917
+ return diagnostics;
918
+ }
919
+ function computePageBaseline(registry, manifest) {
920
+ const uncaptured = [];
921
+ for (const d of checkPageCoverage(registry, manifest, {
922
+ uncapturedPages: []
923
+ })) {
924
+ if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
925
+ }
926
+ return { uncapturedPages: uncaptured.sort() };
927
+ }
928
+
929
+ // src/scanner/scan/state-coverage.ts
930
+ var STATE_KINDS = ["page", "feature", "widget"];
931
+ function declaredStateEntities(registry) {
932
+ const out2 = [];
933
+ for (const kind of STATE_KINDS) {
934
+ for (const e of registry.list(kind)) {
935
+ const states = e.meta?.states;
936
+ if (!states || states.length === 0) continue;
937
+ out2.push({
938
+ kind,
939
+ id: e.id,
940
+ states: states.map((s) => s.name),
941
+ loc: e.loc
942
+ });
943
+ }
944
+ }
945
+ return out2;
946
+ }
947
+ function captureMatchesEntity(cap, ent) {
948
+ if (cap.entity !== ent.id) return false;
949
+ return cap.kind === void 0 || cap.kind === ent.kind;
950
+ }
951
+ function checkStateCoverage(registry, manifest) {
952
+ const diagnostics = [];
953
+ const declared = declaredStateEntities(registry);
954
+ const captured = manifest.captured ?? [];
955
+ for (const ent of declared) {
956
+ for (const state of ent.states) {
957
+ const hit = captured.some(
958
+ (c) => captureMatchesEntity(c, ent) && c.state === state
959
+ );
960
+ if (hit) continue;
961
+ diagnostics.push({
962
+ code: "missing-state-capture",
963
+ severity: "warning",
964
+ message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
965
+ file: ent.loc?.file,
966
+ line: ent.loc?.line,
967
+ entity: { kind: ent.kind, id: ent.id },
968
+ hint: `Add a capture for "${ent.id}/${state}" to the states matrix, or remove the state from \`export const uidex = { states: [...] }\` if it no longer exists.`
969
+ });
970
+ }
971
+ }
972
+ for (const cap of captured) {
973
+ const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
974
+ if (candidates.length === 0) continue;
975
+ const declaredSomewhere = candidates.some(
976
+ (ent2) => ent2.states.includes(cap.state)
977
+ );
978
+ if (declaredSomewhere) continue;
979
+ const ent = candidates[0];
980
+ diagnostics.push({
981
+ code: "orphan-state-capture",
982
+ severity: "warning",
983
+ message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
984
+ file: ent.loc?.file,
985
+ line: ent.loc?.line,
986
+ entity: { kind: ent.kind, id: ent.id },
987
+ hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
988
+ });
989
+ }
990
+ return diagnostics;
991
+ }
992
+
993
+ // src/scanner/scan/state-matrix.ts
994
+ var CORE_KINDS = CORE_STATE_KINDS;
995
+ var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
996
+ function resolveMatrixKind(registry, c) {
997
+ if (c.stateKind) return c.stateKind;
998
+ for (const kind of STATE_ENTITY_KINDS) {
999
+ const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
1000
+ if (declared) return declared;
1001
+ }
1002
+ return CORE_KINDS.includes(c.state) ? c.state : "variant";
1003
+ }
1004
+ function coreKindsByRoute(registry, manifest) {
1005
+ const routes = registry.list("route");
1006
+ const routePaths = routes.map((r) => r.path);
1007
+ const byRoute = /* @__PURE__ */ new Map();
1008
+ for (const c of manifest.captured ?? []) {
1009
+ const route = routeForCapture(c, routes, routePaths);
1010
+ if (!route) continue;
1011
+ if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
1012
+ const kind = resolveMatrixKind(registry, c);
1013
+ if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
1014
+ }
1015
+ return byRoute;
1016
+ }
1017
+ function pageWaivers(registry, routePage) {
1018
+ return registry.get("page", routePage)?.meta?.waivers ?? {};
1019
+ }
1020
+ function isOptedOut(registry, page) {
1021
+ return page ? registry.get("page", page)?.meta?.capture === false : false;
1022
+ }
1023
+ function checkStateMatrix(registry, manifest, baseline) {
1024
+ const routes = registry.list("route");
1025
+ if (routes.length === 0) return [];
1026
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1027
+ const captured = coreKindsByRoute(registry, manifest);
1028
+ const baselined = baseline?.missingKinds ?? {};
1029
+ const diagnostics = [];
1030
+ for (const [route, kinds] of captured) {
1031
+ const page = pageOf.get(route);
1032
+ if (isOptedOut(registry, page)) continue;
1033
+ const waivers = page ? pageWaivers(registry, page) : {};
1034
+ const loc = page ? registry.get("page", page)?.loc : void 0;
1035
+ const baselinedKinds = new Set(baselined[route] ?? []);
1036
+ for (const kind of CORE_KINDS) {
1037
+ if (kinds.has(kind)) continue;
1038
+ if (waivers[kind]) continue;
1039
+ if (baselinedKinds.has(kind)) {
1040
+ diagnostics.push({
1041
+ code: "matrix-backlog",
1042
+ severity: "info",
1043
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
1044
+ file: loc?.file,
1045
+ line: loc?.line,
1046
+ hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
1047
+ });
1048
+ continue;
1049
+ }
1050
+ diagnostics.push({
1051
+ code: "core-kind",
1052
+ severity: "error",
1053
+ message: `route "${route}" is captured but never renders core kind "${kind}"`,
1054
+ file: loc?.file,
1055
+ line: loc?.line,
1056
+ entity: { kind: "route", id: route },
1057
+ hint: `Capture the "${kind}" state, or if the route cannot render it add \`export const uidex = { page: "${page}", waivers: { ${kind}: "why it can't" } }\`, or grandfather it via \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
1058
+ });
1059
+ }
1060
+ }
1061
+ for (const [route, kinds] of captured) {
1062
+ const page = pageOf.get(route);
1063
+ if (!page || isOptedOut(registry, page)) continue;
1064
+ const waivers = pageWaivers(registry, page);
1065
+ const loc = registry.get("page", page)?.loc;
1066
+ for (const kind of Object.keys(waivers)) {
1067
+ if (kinds.has(kind)) {
1068
+ diagnostics.push({
1069
+ code: "stale-waiver",
1070
+ severity: "warning",
1071
+ message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
1072
+ file: loc?.file,
1073
+ line: loc?.line,
1074
+ entity: { kind: "page", id: page },
1075
+ hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
1076
+ });
1077
+ }
1078
+ }
1079
+ }
1080
+ return diagnostics;
1081
+ }
1082
+ function computeMissingKinds(registry, manifest) {
1083
+ const routes = registry.list("route");
1084
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1085
+ const captured = coreKindsByRoute(registry, manifest);
1086
+ const out2 = {};
1087
+ for (const [route, kinds] of captured) {
1088
+ const page = pageOf.get(route);
1089
+ if (isOptedOut(registry, page)) continue;
1090
+ const waivers = page ? pageWaivers(registry, page) : {};
1091
+ const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
1092
+ if (missing.length > 0) out2[route] = missing;
1093
+ }
1094
+ return out2;
1095
+ }
1096
+
779
1097
  // src/scanner/scan/audit.ts
780
1098
  function audit(opts) {
781
1099
  const diagnostics = [];
@@ -785,6 +1103,9 @@ function audit(opts) {
785
1103
  const acceptanceEnabled = config.audit?.acceptance ?? true;
786
1104
  const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
787
1105
  const coverageEnabled = config.audit?.coverage ?? true;
1106
+ const statesEnabled = config.audit?.states ?? true;
1107
+ const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
1108
+ const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
788
1109
  if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
789
1110
  for (const ef of extracted) {
790
1111
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
@@ -818,6 +1139,48 @@ function audit(opts) {
818
1139
  }
819
1140
  }
820
1141
  }
1142
+ if (check && opts.typesGenerated !== void 0) {
1143
+ const outRel = opts.typesOutputPath ?? config.output;
1144
+ const fresh = normalizeForCheck(opts.typesGenerated);
1145
+ if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
1146
+ diagnostics.push({
1147
+ code: "gen-missing",
1148
+ severity: "error",
1149
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
1150
+ file: outRel,
1151
+ hint: "Run `uidex scan` (without --check) to regenerate"
1152
+ });
1153
+ } else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
1154
+ diagnostics.push({
1155
+ code: "gen-stale",
1156
+ severity: "error",
1157
+ message: `Generated file "${outRel}" is stale`,
1158
+ file: outRel,
1159
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
1160
+ });
1161
+ }
1162
+ }
1163
+ if (check && opts.locsGenerated !== void 0) {
1164
+ const outRel = opts.locsOutputPath ?? config.output;
1165
+ const fresh = normalizeForCheck(opts.locsGenerated);
1166
+ if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
1167
+ diagnostics.push({
1168
+ code: "gen-missing",
1169
+ severity: "error",
1170
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
1171
+ file: outRel,
1172
+ hint: "Run `uidex scan` (without --check) to regenerate"
1173
+ });
1174
+ } else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
1175
+ diagnostics.push({
1176
+ code: "gen-stale",
1177
+ severity: "error",
1178
+ message: `Generated file "${outRel}" is stale`,
1179
+ file: outRel,
1180
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
1181
+ });
1182
+ }
1183
+ }
821
1184
  if (lint && acceptanceEnabled) {
822
1185
  for (const kind of ["widget", "feature", "page"]) {
823
1186
  for (const e of registry.list(kind)) {
@@ -1077,6 +1440,21 @@ function audit(opts) {
1077
1440
  }
1078
1441
  }
1079
1442
  }
1443
+ if (lint && statesEnabled && opts.capturedStates) {
1444
+ diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
1445
+ }
1446
+ if (lint && pageCoverageEnabled && opts.capturedStates) {
1447
+ diagnostics.push(
1448
+ ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
1449
+ );
1450
+ }
1451
+ if (lint && stateMatrixEnabled && opts.capturedStates) {
1452
+ diagnostics.push(
1453
+ ...checkStateMatrix(registry, opts.capturedStates, {
1454
+ missingKinds: opts.pageBaseline?.missingKinds ?? {}
1455
+ })
1456
+ );
1457
+ }
1080
1458
  const summary = {
1081
1459
  errors: diagnostics.filter((d) => d.severity === "error").length,
1082
1460
  warnings: diagnostics.filter((d) => d.severity === "warning").length
@@ -1241,6 +1619,148 @@ function stableReplacer(_key, value) {
1241
1619
  }
1242
1620
 
1243
1621
  // src/scanner/scan/emit.ts
1622
+ var AUTHORED_KINDS = /* @__PURE__ */ new Set([
1623
+ "route",
1624
+ "page",
1625
+ "feature",
1626
+ "widget",
1627
+ "primitive",
1628
+ "flow"
1629
+ ]);
1630
+ function internedPayload(entities) {
1631
+ const fileIndex = /* @__PURE__ */ new Map();
1632
+ const files = [];
1633
+ const compact = entities.map((e) => {
1634
+ const loc = e.loc;
1635
+ if (!loc || typeof loc.file !== "string") return e;
1636
+ let idx = fileIndex.get(loc.file);
1637
+ if (idx === void 0) {
1638
+ idx = files.length;
1639
+ files.push(loc.file);
1640
+ fileIndex.set(loc.file, idx);
1641
+ }
1642
+ const rest = { ...loc };
1643
+ delete rest.file;
1644
+ return { ...e, loc: { f: idx, ...rest } };
1645
+ });
1646
+ return {
1647
+ filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
1648
+ parseExpr: `(JSON.parse(${JSON.stringify(
1649
+ JSON.stringify(compact, replacerSorted)
1650
+ )}) as any[]).map(hydrate)`
1651
+ };
1652
+ }
1653
+ var HYDRATE_FN = [
1654
+ "function hydrate(e: any): any {",
1655
+ ' if (e.loc && typeof e.loc.f === "number") {',
1656
+ " const { f, ...rest } = e.loc",
1657
+ " e.loc = { file: files[f], ...rest }",
1658
+ " }",
1659
+ " return e",
1660
+ "}"
1661
+ ].join("\n");
1662
+ function compactMeta(meta) {
1663
+ if (!meta || typeof meta !== "object") return meta;
1664
+ const composes = meta.composes;
1665
+ if (!Array.isArray(composes)) return meta;
1666
+ return {
1667
+ ...meta,
1668
+ composes: composes.map(
1669
+ (c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
1670
+ )
1671
+ };
1672
+ }
1673
+ function emitLocsModule(thin, uidexImport) {
1674
+ const kinds = [...new Set(thin.map((e) => e.kind))].sort();
1675
+ const kindIdx = new Map(kinds.map((k, i) => [k, i]));
1676
+ const fileIndex = /* @__PURE__ */ new Map();
1677
+ const files = [];
1678
+ const fi = (f) => {
1679
+ let i = fileIndex.get(f);
1680
+ if (i === void 0) {
1681
+ i = files.length;
1682
+ files.push(f);
1683
+ fileIndex.set(f, i);
1684
+ }
1685
+ return i;
1686
+ };
1687
+ const bare = [];
1688
+ const rich = [];
1689
+ for (const e of thin) {
1690
+ const loc = e.loc;
1691
+ const topOnlyKindIdLoc = Object.keys(e).every(
1692
+ (k) => k === "kind" || k === "id" || k === "loc"
1693
+ );
1694
+ const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
1695
+ (k) => k === "file" || k === "line" || k === "column"
1696
+ );
1697
+ if (topOnlyKindIdLoc && locOnlyKnown) {
1698
+ const row = [
1699
+ kindIdx.get(e.kind),
1700
+ e.id,
1701
+ fi(loc.file)
1702
+ ];
1703
+ if (loc.line != null || loc.column != null)
1704
+ row.push(Number(loc.line ?? 0));
1705
+ if (loc.column != null) row.push(Number(loc.column));
1706
+ bare.push(row);
1707
+ } else {
1708
+ const richEntity = { ...e };
1709
+ if (loc && typeof loc.file === "string") {
1710
+ const { file, ...rest } = loc;
1711
+ richEntity.loc = { f: fi(file), ...rest };
1712
+ }
1713
+ if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
1714
+ rich.push(richEntity);
1715
+ }
1716
+ }
1717
+ const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
1718
+ const l = [];
1719
+ l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1720
+ l.push(
1721
+ "// Deferred element/region source locations for the inspector's copy-path."
1722
+ );
1723
+ l.push("// @ts-nocheck");
1724
+ l.push("/* eslint-disable */");
1725
+ l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
1726
+ l.push("");
1727
+ l.push(`const files: readonly string[] = ${jsonStable(files)}`);
1728
+ l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
1729
+ l.push("function loc(fi: number, line?: number, col?: number) {");
1730
+ l.push(" const o: any = { file: files[fi] }");
1731
+ l.push(" if (line) o.line = line");
1732
+ l.push(" if (col) o.column = col");
1733
+ l.push(" return o");
1734
+ l.push("}");
1735
+ l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
1736
+ l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
1737
+ l.push("");
1738
+ l.push(
1739
+ "/** Add the thin element/region entities (source locations) to a registry. */"
1740
+ );
1741
+ l.push("export function loadLocs(target: Registry): Registry {");
1742
+ l.push(" for (const r of bare)");
1743
+ l.push(
1744
+ " target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
1745
+ );
1746
+ l.push(" for (const e of rich) {");
1747
+ l.push(" const out: any = { ...e }");
1748
+ l.push(' if (e.loc && typeof e.loc.f === "number") {');
1749
+ l.push(" const { f, ...rest } = e.loc");
1750
+ l.push(" out.loc = { file: files[f], ...rest }");
1751
+ l.push(" }");
1752
+ l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
1753
+ l.push(
1754
+ ' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
1755
+ );
1756
+ l.push(" }");
1757
+ l.push(" target.add(out)");
1758
+ l.push(" }");
1759
+ l.push(" return target");
1760
+ l.push("}");
1761
+ l.push("");
1762
+ return l.join("\n");
1763
+ }
1244
1764
  function sortById(arr) {
1245
1765
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
1246
1766
  }
@@ -1266,6 +1786,20 @@ function emitIdUnion(name, ids) {
1266
1786
  ${body}
1267
1787
  `;
1268
1788
  }
1789
+ var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
1790
+ function emitId(name, ids) {
1791
+ if (OPAQUE_ID_UNIONS.has(name)) {
1792
+ return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
1793
+ `;
1794
+ }
1795
+ return emitIdUnion(name, ids);
1796
+ }
1797
+ function dataOutputPath(output) {
1798
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
1799
+ }
1800
+ function locsOutputPath(output) {
1801
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
1802
+ }
1269
1803
  function emit(opts) {
1270
1804
  const { registry, gitContext, uidexImport = "uidex" } = opts;
1271
1805
  const routes = [...registry.list("route")].sort(
@@ -1274,112 +1808,131 @@ function emit(opts) {
1274
1808
  const pages = sortById(registry.list("page"));
1275
1809
  const features = sortById(registry.list("feature"));
1276
1810
  const widgets = sortById(registry.list("widget"));
1811
+ const stateNames = /* @__PURE__ */ new Set();
1812
+ for (const e of [...pages, ...features, ...widgets]) {
1813
+ for (const s of e.meta?.states ?? []) stateNames.add(s.name);
1814
+ }
1277
1815
  const regions = sortById(registry.list("region"));
1278
1816
  const elements = sortById(registry.list("element"));
1279
1817
  const primitives = sortById(registry.list("primitive"));
1280
1818
  const flows = sortById(registry.list("flow"));
1281
- const lines = [];
1282
- lines.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1283
- lines.push("/* eslint-disable */");
1284
- lines.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
1285
- lines.push(
1286
- `import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
1819
+ const t = [];
1820
+ t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1821
+ t.push(
1822
+ "// Types only \u2014 the runtime registry lives in the sibling `.data` module."
1287
1823
  );
1288
- lines.push("");
1289
- lines.push("// ---- id unions ----");
1290
- lines.push(
1291
- emitIdUnion(
1824
+ t.push("/* eslint-disable */");
1825
+ t.push("");
1826
+ t.push("// ---- id unions ----");
1827
+ t.push(
1828
+ emitId(
1292
1829
  "PageId",
1293
1830
  pages.map((e) => e.id)
1294
1831
  )
1295
1832
  );
1296
- lines.push(
1297
- emitIdUnion(
1833
+ t.push(
1834
+ emitId(
1298
1835
  "FeatureId",
1299
1836
  features.map((e) => e.id)
1300
1837
  )
1301
1838
  );
1302
- lines.push(
1303
- emitIdUnion(
1839
+ t.push(
1840
+ emitId(
1304
1841
  "WidgetId",
1305
1842
  widgets.map((e) => e.id)
1306
1843
  )
1307
1844
  );
1308
- lines.push(
1309
- emitIdUnion(
1845
+ t.push(
1846
+ emitId(
1310
1847
  "RegionId",
1311
1848
  regions.map((e) => e.id)
1312
1849
  )
1313
1850
  );
1314
- lines.push(
1315
- emitIdUnion(
1851
+ t.push(
1852
+ emitId(
1316
1853
  "ElementId",
1317
1854
  elements.map((e) => e.id)
1318
1855
  )
1319
1856
  );
1320
- lines.push(
1321
- emitIdUnion(
1857
+ t.push(
1858
+ emitId(
1322
1859
  "PrimitiveId",
1323
1860
  primitives.map((e) => e.id)
1324
1861
  )
1325
1862
  );
1326
- lines.push(
1327
- emitIdUnion(
1863
+ t.push(
1864
+ emitId(
1328
1865
  "FlowId",
1329
1866
  flows.map((e) => e.id)
1330
1867
  )
1331
1868
  );
1332
- lines.push(
1333
- emitIdUnion(
1869
+ t.push(
1870
+ emitId(
1334
1871
  "RouteId",
1335
1872
  routes.map((e) => e.path)
1336
1873
  )
1337
1874
  );
1338
- lines.push("");
1339
- lines.push("// ---- authoring-surface shape types ----");
1340
- lines.push("export namespace Uidex {");
1341
- lines.push(" export interface Page {");
1342
- lines.push(" page: PageId | false");
1343
- lines.push(" name?: string");
1344
- lines.push(" features?: readonly FeatureId[]");
1345
- lines.push(" widgets?: readonly WidgetId[]");
1346
- lines.push(" acceptance?: readonly string[]");
1347
- lines.push(" description?: string");
1348
- lines.push(" }");
1349
- lines.push(" export interface Feature {");
1350
- lines.push(" feature: FeatureId | false");
1351
- lines.push(" name?: string");
1352
- lines.push(" features?: readonly FeatureId[]");
1353
- lines.push(" acceptance?: readonly string[]");
1354
- lines.push(" description?: string");
1355
- lines.push(" }");
1356
- lines.push(" export interface Primitive {");
1357
- lines.push(" primitive: PrimitiveId");
1358
- lines.push(" name?: string");
1359
- lines.push(" description?: string");
1360
- lines.push(" }");
1361
- lines.push(" export interface Widget {");
1362
- lines.push(" widget: WidgetId");
1363
- lines.push(" name?: string");
1364
- lines.push(" acceptance?: readonly string[]");
1365
- lines.push(" description?: string");
1366
- lines.push(" }");
1367
- lines.push(" export interface Region {");
1368
- lines.push(" region: RegionId | false");
1369
- lines.push(" name?: string");
1370
- lines.push(" description?: string");
1371
- lines.push(" }");
1372
- lines.push(" export interface Flow {");
1373
- lines.push(" flow: FlowId");
1374
- lines.push(" name?: string");
1375
- lines.push(" description?: string");
1376
- lines.push(" }");
1377
- lines.push(" export interface NotFlow {");
1378
- lines.push(" notFlow: true");
1379
- lines.push(" }");
1380
- lines.push("}");
1381
- lines.push("");
1382
- lines.push("// ---- entities ----");
1875
+ t.push(emitId("StateId", [...stateNames]));
1876
+ t.push("");
1877
+ t.push("// ---- authoring-surface shape types ----");
1878
+ t.push("export namespace Uidex {");
1879
+ t.push(
1880
+ ' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
1881
+ );
1882
+ t.push(' export type StateKind = CoreStateKind | "variant"');
1883
+ t.push(" export interface State {");
1884
+ t.push(" name: string");
1885
+ t.push(" kind?: StateKind");
1886
+ t.push(" acceptance?: readonly string[]");
1887
+ t.push(" description?: string");
1888
+ t.push(" }");
1889
+ t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
1890
+ t.push(" export interface Page {");
1891
+ t.push(" page: PageId | false");
1892
+ t.push(" name?: string");
1893
+ t.push(" features?: readonly FeatureId[]");
1894
+ t.push(" widgets?: readonly WidgetId[]");
1895
+ t.push(" acceptance?: readonly string[]");
1896
+ t.push(" states?: readonly (string | State)[]");
1897
+ t.push(" capture?: boolean");
1898
+ t.push(" waivers?: Waivers");
1899
+ t.push(" description?: string");
1900
+ t.push(" }");
1901
+ t.push(" export interface Feature {");
1902
+ t.push(" feature: FeatureId | false");
1903
+ t.push(" name?: string");
1904
+ t.push(" features?: readonly FeatureId[]");
1905
+ t.push(" acceptance?: readonly string[]");
1906
+ t.push(" states?: readonly (string | State)[]");
1907
+ t.push(" description?: string");
1908
+ t.push(" }");
1909
+ t.push(" export interface Primitive {");
1910
+ t.push(" primitive: PrimitiveId");
1911
+ t.push(" name?: string");
1912
+ t.push(" description?: string");
1913
+ t.push(" }");
1914
+ t.push(" export interface Widget {");
1915
+ t.push(" widget: WidgetId");
1916
+ t.push(" name?: string");
1917
+ t.push(" acceptance?: readonly string[]");
1918
+ t.push(" states?: readonly (string | State)[]");
1919
+ t.push(" description?: string");
1920
+ t.push(" }");
1921
+ t.push(" export interface Region {");
1922
+ t.push(" region: RegionId | false");
1923
+ t.push(" name?: string");
1924
+ t.push(" description?: string");
1925
+ t.push(" }");
1926
+ t.push(" export interface Flow {");
1927
+ t.push(" flow: FlowId");
1928
+ t.push(" name?: string");
1929
+ t.push(" description?: string");
1930
+ t.push(" }");
1931
+ t.push(" export interface NotFlow {");
1932
+ t.push(" notFlow: true");
1933
+ t.push(" }");
1934
+ t.push("}");
1935
+ t.push("");
1383
1936
  const allEntities = [
1384
1937
  ...routes,
1385
1938
  ...pages,
@@ -1390,25 +1943,39 @@ function emit(opts) {
1390
1943
  ...primitives,
1391
1944
  ...flows
1392
1945
  ];
1393
- lines.push(
1394
- `export const entities: ReadonlyArray<Entity> = ${jsonStable(allEntities)}`
1395
- );
1396
- lines.push("");
1397
- lines.push("// ---- git context ----");
1946
+ const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
1947
+ const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
1398
1948
  const gc = gitContext ?? { branch: null, commit: null, pr: null };
1399
- lines.push(`export const gitContext = ${jsonStable(gc)} as const`);
1400
- lines.push("");
1401
- lines.push("// ---- registry factory ----");
1402
- lines.push("export function loadRegistry(target: Registry): Registry {");
1403
- lines.push(" for (const entity of entities) target.add(entity)");
1404
- lines.push(" return target");
1405
- lines.push("}");
1406
- lines.push("");
1407
- lines.push("// ---- preconfigured uidex instance ----");
1408
- lines.push("export const uidex = createUidex()");
1409
- lines.push("for (const entity of entities) uidex.registry.add(entity)");
1410
- lines.push("");
1411
- return lines.join("\n");
1949
+ const authoredPayload = internedPayload(authored);
1950
+ const d = [];
1951
+ d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1952
+ d.push(
1953
+ "// Authored-metadata registry. The thin element/region entities (source"
1954
+ );
1955
+ d.push(
1956
+ "// locations only) live in the sibling `.locs` module, loaded lazily."
1957
+ );
1958
+ d.push("// @ts-nocheck");
1959
+ d.push("/* eslint-disable */");
1960
+ d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
1961
+ d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
1962
+ d.push("");
1963
+ d.push(authoredPayload.filesDecl);
1964
+ d.push(HYDRATE_FN);
1965
+ d.push(`export const entities = ${authoredPayload.parseExpr}`);
1966
+ d.push("");
1967
+ d.push(`export const gitContext = ${jsonStable(gc)} as const`);
1968
+ d.push("");
1969
+ d.push("export function loadRegistry(target: Registry): Registry {");
1970
+ d.push(" for (const entity of entities) target.add(entity)");
1971
+ d.push(" return target");
1972
+ d.push("}");
1973
+ d.push("");
1974
+ d.push("export const uidex = createUidex()");
1975
+ d.push("for (const entity of entities) uidex.registry.add(entity)");
1976
+ d.push("");
1977
+ const locs = emitLocsModule(thin, uidexImport);
1978
+ return { types: t.join("\n"), data: d.join("\n"), locs };
1412
1979
  }
1413
1980
 
1414
1981
  // src/scanner/scan/ast.ts
@@ -1445,14 +2012,14 @@ function makeLineAt(content) {
1445
2012
  function parseSource(file) {
1446
2013
  const lineAt = makeLineAt(file.content);
1447
2014
  try {
1448
- const result2 = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
2015
+ const result = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
1449
2016
  lang: langFor(file.sourcePath),
1450
2017
  sourceType: "module"
1451
2018
  });
1452
2019
  return {
1453
- program: result2.program,
1454
- hasErrors: result2.errors.length > 0,
1455
- comments: result2.comments ?? [],
2020
+ program: result.program,
2021
+ hasErrors: result.errors.length > 0,
2022
+ comments: result.comments ?? [],
1456
2023
  lineAt
1457
2024
  };
1458
2025
  } catch {
@@ -1512,20 +2079,35 @@ var ALLOWED_FIELDS = {
1512
2079
  "features",
1513
2080
  "widgets",
1514
2081
  "acceptance",
2082
+ "states",
2083
+ "capture",
2084
+ "waivers",
1515
2085
  "description"
1516
2086
  ]),
2087
+ // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
2088
+ // they are page-only (the gates read them off the page). A feature/widget that
2089
+ // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
1517
2090
  feature: /* @__PURE__ */ new Set([
1518
2091
  "feature",
1519
2092
  "name",
1520
2093
  "features",
1521
2094
  "acceptance",
2095
+ "states",
1522
2096
  "description"
1523
2097
  ]),
1524
2098
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
1525
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
2099
+ widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
1526
2100
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
1527
2101
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
1528
2102
  };
2103
+ var STATE_FIELDS = /* @__PURE__ */ new Set([
2104
+ "name",
2105
+ "kind",
2106
+ "acceptance",
2107
+ "description"
2108
+ ]);
2109
+ var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
2110
+ var STATE_KINDS2 = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
1529
2111
  var FALSEABLE = /* @__PURE__ */ new Set([
1530
2112
  "page",
1531
2113
  "feature",
@@ -1915,6 +2497,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1915
2497
  }
1916
2498
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1917
2499
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
2500
+ const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
2501
+ const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
2502
+ const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
1918
2503
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1919
2504
  const metadata = {
1920
2505
  source: "ts-export",
@@ -1937,6 +2522,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1937
2522
  metadata.widgets = widgetsField.values;
1938
2523
  metadata.widgetSpans = widgetsField.spans;
1939
2524
  }
2525
+ if (states?.length) metadata.states = states;
2526
+ if (capture !== void 0) metadata.capture = capture;
2527
+ if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
1940
2528
  if (notFlow) metadata.notFlow = true;
1941
2529
  if (typeof id === "string" && idValue.kind === "string") {
1942
2530
  metadata.idSpan = idValue.span;
@@ -1973,6 +2561,48 @@ function readIdField(value, kind, fieldName) {
1973
2561
  value.pos
1974
2562
  );
1975
2563
  }
2564
+ function readWaiversField(byKey) {
2565
+ const entry = byKey.get("waivers");
2566
+ if (!entry) return void 0;
2567
+ if (entry.value.kind !== "object") {
2568
+ throw new ExtractError(
2569
+ "uidex-export-invalid-field",
2570
+ "`waivers` must be an object mapping a core kind to a reason.",
2571
+ entry.value.pos
2572
+ );
2573
+ }
2574
+ const waivers = {};
2575
+ for (const e of entry.value.entries) {
2576
+ if (!CORE_STATE_KINDS2.has(e.key)) {
2577
+ throw new ExtractError(
2578
+ "uidex-export-invalid-field",
2579
+ `waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
2580
+ e.keyPos
2581
+ );
2582
+ }
2583
+ if (e.value.kind !== "string" || e.value.value.length === 0) {
2584
+ throw new ExtractError(
2585
+ "uidex-export-invalid-field",
2586
+ `waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
2587
+ e.value.pos
2588
+ );
2589
+ }
2590
+ waivers[e.key] = e.value.value;
2591
+ }
2592
+ return waivers;
2593
+ }
2594
+ function readBooleanField(byKey, name) {
2595
+ const entry = byKey.get(name);
2596
+ if (!entry) return void 0;
2597
+ if (entry.value.kind !== "boolean") {
2598
+ throw new ExtractError(
2599
+ "uidex-export-invalid-field",
2600
+ `\`${name}\` must be a boolean.`,
2601
+ entry.value.pos
2602
+ );
2603
+ }
2604
+ return entry.value.value;
2605
+ }
1976
2606
  function readStringField(byKey, name) {
1977
2607
  const entry = byKey.get(name);
1978
2608
  if (!entry) return void 0;
@@ -2010,6 +2640,86 @@ function readStringArrayField(byKey, name) {
2010
2640
  }
2011
2641
  return { values, spans };
2012
2642
  }
2643
+ function readStatesField(byKey) {
2644
+ const entry = byKey.get("states");
2645
+ if (!entry) return void 0;
2646
+ if (entry.value.kind !== "array") {
2647
+ throw new ExtractError(
2648
+ "uidex-export-invalid-field",
2649
+ "`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
2650
+ entry.value.pos
2651
+ );
2652
+ }
2653
+ const states = [];
2654
+ const seen = /* @__PURE__ */ new Set();
2655
+ for (const item of entry.value.items) {
2656
+ let state;
2657
+ if (item.kind === "string") {
2658
+ state = { name: item.value, nameSpan: item.span };
2659
+ } else if (item.kind === "object") {
2660
+ const fieldByKey = /* @__PURE__ */ new Map();
2661
+ for (const e of item.entries) fieldByKey.set(e.key, e);
2662
+ for (const e of item.entries) {
2663
+ if (!STATE_FIELDS.has(e.key)) {
2664
+ throw new ExtractError(
2665
+ "uidex-export-invalid-field",
2666
+ `Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
2667
+ STATE_FIELDS
2668
+ ).sort().join(", ")}.`,
2669
+ e.value.pos
2670
+ );
2671
+ }
2672
+ }
2673
+ const nameEntry = fieldByKey.get("name");
2674
+ if (!nameEntry || nameEntry.value.kind !== "string") {
2675
+ throw new ExtractError(
2676
+ "uidex-export-invalid-field",
2677
+ "Each object `states` entry must have a string `name`.",
2678
+ item.pos
2679
+ );
2680
+ }
2681
+ state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
2682
+ const kind = readStringField(fieldByKey, "kind");
2683
+ if (kind !== void 0) {
2684
+ if (!STATE_KINDS2.has(kind)) {
2685
+ throw new ExtractError(
2686
+ "uidex-export-invalid-field",
2687
+ `state \`kind\` must be one of ${[...STATE_KINDS2].join(", ")} (got "${kind}").`,
2688
+ fieldByKey.get("kind").value.pos
2689
+ );
2690
+ }
2691
+ state.kind = kind;
2692
+ }
2693
+ const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
2694
+ if (acceptance) state.acceptance = acceptance;
2695
+ const description = readStringField(fieldByKey, "description");
2696
+ if (description) state.description = description;
2697
+ } else {
2698
+ throw new ExtractError(
2699
+ "uidex-export-invalid-field",
2700
+ "`states` items must be strings or `{ name, ... }` objects.",
2701
+ item.pos
2702
+ );
2703
+ }
2704
+ if (state.name.length === 0) {
2705
+ throw new ExtractError(
2706
+ "uidex-export-invalid-field",
2707
+ "A `states` entry has an empty `name`.",
2708
+ item.pos
2709
+ );
2710
+ }
2711
+ if (seen.has(state.name)) {
2712
+ throw new ExtractError(
2713
+ "uidex-export-duplicate-state",
2714
+ `Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
2715
+ item.pos
2716
+ );
2717
+ }
2718
+ seen.add(state.name);
2719
+ states.push(state);
2720
+ }
2721
+ return states;
2722
+ }
2013
2723
  function posAt(content, offset, p2) {
2014
2724
  const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
2015
2725
  return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
@@ -2247,22 +2957,22 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
2247
2957
  if (!kind) continue;
2248
2958
  const value = attr.value;
2249
2959
  if (!value) continue;
2250
- let result2 = UNRESOLVED;
2960
+ let result = UNRESOLVED;
2251
2961
  let valueSpan;
2252
2962
  if (value.type === "Literal") {
2253
2963
  const v = staticString(value);
2254
- result2 = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
2255
- if (result2.resolved) valueSpan = { start: value.start, end: value.end };
2964
+ result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
2965
+ if (result.resolved) valueSpan = { start: value.start, end: value.end };
2256
2966
  } else if (value.type === "JSXExpressionContainer") {
2257
2967
  const expr = value.expression;
2258
2968
  if (expr && expr.type !== "JSXEmptyExpression") {
2259
- result2 = evalIdExpression(expr, consts);
2969
+ result = evalIdExpression(expr, consts);
2260
2970
  const inner = unwrapTsExpression(expr);
2261
- if (result2.resolved && staticString(inner) !== null) {
2971
+ if (result.resolved && staticString(inner) !== null) {
2262
2972
  valueSpan = { start: inner.start, end: inner.end };
2263
2973
  }
2264
2974
  }
2265
- if (!result2.resolved) {
2975
+ if (!result.resolved) {
2266
2976
  dynamicAttrs.push({
2267
2977
  kind,
2268
2978
  attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
@@ -2271,15 +2981,15 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
2271
2981
  continue;
2272
2982
  }
2273
2983
  }
2274
- if (!result2.resolved) continue;
2275
- for (const id of result2.ids) {
2984
+ if (!result.resolved) continue;
2985
+ for (const id of result.ids) {
2276
2986
  const resolved = {
2277
2987
  kind,
2278
2988
  id,
2279
2989
  start: attr.start,
2280
2990
  isPattern: id.includes("*"),
2281
2991
  // Only a single plain string literal is renameable in place.
2282
- ...result2.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
2992
+ ...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
2283
2993
  };
2284
2994
  if (resolved.isPattern) patterns.push(resolved);
2285
2995
  else statics.push(resolved);
@@ -2562,13 +3272,26 @@ var path8 = __toESM(require("path"), 1);
2562
3272
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
2563
3273
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
2564
3274
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
2565
- function detectRoutes(files) {
3275
+ function detectRoutes(files, options = {}) {
2566
3276
  const out2 = [];
2567
3277
  const seen = /* @__PURE__ */ new Set();
3278
+ const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
3279
+ const routesDirParts = routesDir ? routesDir.split("/") : null;
2568
3280
  for (const f of files) {
2569
3281
  const rel = f.displayPath;
2570
3282
  const parts = rel.split("/");
2571
3283
  const base = parts[parts.length - 1];
3284
+ if (routesDirParts) {
3285
+ const end = dirEndIndex(parts, routesDirParts);
3286
+ if (end === -1 || end > parts.length - 1) continue;
3287
+ if (!PAGES_ROUTER_BASENAME.test(base)) continue;
3288
+ const routePath = routesStylePath(
3289
+ parts.slice(end, parts.length - 1),
3290
+ base
3291
+ );
3292
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
3293
+ continue;
3294
+ }
2572
3295
  const appIdx = parts.indexOf("app");
2573
3296
  if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
2574
3297
  const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
@@ -2595,15 +3318,11 @@ function detectRoutes(files) {
2595
3318
  }
2596
3319
  const routesIdx = parts.indexOf("routes");
2597
3320
  if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
2598
- const segs = parts.slice(routesIdx + 1);
2599
- const last = segs[segs.length - 1];
2600
- if (last.startsWith("_")) continue;
2601
- const normalized = [
2602
- ...segs.slice(0, -1),
2603
- base.replace(/\.[^.]+$/, "")
2604
- ].filter((s) => s !== "index" && s !== "__root");
2605
- const routePath = formatNextAppPath(normalized);
2606
- push(out2, seen, routePath, f.displayPath);
3321
+ const routePath = routesStylePath(
3322
+ parts.slice(routesIdx + 1, parts.length - 1),
3323
+ base
3324
+ );
3325
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
2607
3326
  continue;
2608
3327
  }
2609
3328
  }
@@ -2619,6 +3338,63 @@ function formatNextAppPath(segments) {
2619
3338
  if (kept.length === 0) return "/";
2620
3339
  return "/" + kept.join("/");
2621
3340
  }
3341
+ function dirEndIndex(parts, dirParts) {
3342
+ if (dirParts.length === 0) return 0;
3343
+ for (let i = 0; i + dirParts.length <= parts.length; i++) {
3344
+ let match = true;
3345
+ for (let j = 0; j < dirParts.length; j++) {
3346
+ if (parts[i + j] !== dirParts[j]) {
3347
+ match = false;
3348
+ break;
3349
+ }
3350
+ }
3351
+ if (match) return i + dirParts.length;
3352
+ }
3353
+ return -1;
3354
+ }
3355
+ function routesStylePath(dirSegs, base) {
3356
+ const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
3357
+ const leaf = fileSegs[fileSegs.length - 1];
3358
+ if (leaf.startsWith("_")) return null;
3359
+ const rawSegs = [...dirSegs, ...fileSegs];
3360
+ if (rawSegs.some((s) => s.startsWith("-"))) return null;
3361
+ if (rawSegs[0] === "api") return null;
3362
+ return formatTanStackPath(rawSegs);
3363
+ }
3364
+ function splitFlatSegments(name) {
3365
+ const segs = [];
3366
+ let cur = "";
3367
+ let depth = 0;
3368
+ for (const ch of name) {
3369
+ if (ch === "[") depth++;
3370
+ else if (ch === "]") depth = Math.max(0, depth - 1);
3371
+ if (ch === "." && depth === 0) {
3372
+ segs.push(cur);
3373
+ cur = "";
3374
+ continue;
3375
+ }
3376
+ cur += ch;
3377
+ }
3378
+ segs.push(cur);
3379
+ return segs;
3380
+ }
3381
+ function formatTanStackPath(segments) {
3382
+ const kept = [];
3383
+ for (const seg of segments) {
3384
+ if (!seg) continue;
3385
+ if (seg === "index" || seg === "route" || seg === "__root") continue;
3386
+ if (seg.startsWith("_")) continue;
3387
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
3388
+ kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
3389
+ }
3390
+ if (kept.length === 0) return "/";
3391
+ return "/" + kept.join("/");
3392
+ }
3393
+ function normalizeTanStackSegment(seg) {
3394
+ if (seg === "$") return "[...splat]";
3395
+ if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
3396
+ return seg.replace(/\[([^\]]*)\]/g, "$1");
3397
+ }
2622
3398
  function pathToId(routePath) {
2623
3399
  if (routePath === "/") return "root";
2624
3400
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -2633,7 +3409,12 @@ var BASE_EXCLUDES = [
2633
3409
  "**/dist/**",
2634
3410
  "**/build/**",
2635
3411
  "**/.next/**",
2636
- "**/*.gen.ts"
3412
+ // Exclude the scanner's own generated siblings so a scan never re-ingests its
3413
+ // output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
3414
+ // a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
3415
+ "**/*.gen.ts",
3416
+ "**/*.gen.data.ts",
3417
+ "**/*.gen.locs.ts"
2637
3418
  ];
2638
3419
  var TEST_EXCLUDES = [
2639
3420
  "**/*.test.ts",
@@ -2782,6 +3563,18 @@ function buildMetaFromExport(exp) {
2782
3563
  if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
2783
3564
  if (exp.features?.length) meta.features = exp.features;
2784
3565
  if (exp.widgets?.length) meta.widgets = exp.widgets;
3566
+ if (exp.states?.length) {
3567
+ meta.states = exp.states.map((s) => ({
3568
+ name: s.name,
3569
+ ...s.kind ? { kind: s.kind } : {},
3570
+ ...s.acceptance?.length ? { acceptance: s.acceptance } : {},
3571
+ ...s.description ? { description: s.description } : {}
3572
+ }));
3573
+ }
3574
+ if (exp.capture === false) meta.capture = false;
3575
+ if (exp.waivers && Object.keys(exp.waivers).length > 0) {
3576
+ meta.waivers = exp.waivers;
3577
+ }
2785
3578
  return Object.keys(meta).length > 0 ? meta : void 0;
2786
3579
  }
2787
3580
  function resolve3(ctx) {
@@ -2827,9 +3620,12 @@ function resolve3(ctx) {
2827
3620
  function metaWithComposes(kind, id, base) {
2828
3621
  const composes = directChildren.get(`${kind}:${id}`);
2829
3622
  if (!composes || composes.length === 0) return base;
2830
- return { ...base ?? {}, composes };
3623
+ return { ...base, composes };
2831
3624
  }
2832
- const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
3625
+ const routes = conventions.pages === false ? [] : detectRoutes(
3626
+ ctx.extracted.map((e) => e.file),
3627
+ conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
3628
+ );
2833
3629
  const handledPageFiles = /* @__PURE__ */ new Set();
2834
3630
  for (const route of routes) {
2835
3631
  const routeDir = path8.posix.dirname(route.file);
@@ -3200,6 +3996,68 @@ function dedupe(arr) {
3200
3996
  }
3201
3997
 
3202
3998
  // src/scanner/scan/pipeline.ts
3999
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
4000
+ var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
4001
+ function loadPageBaseline(configDir, config) {
4002
+ const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
4003
+ const abs = path9.resolve(configDir, rel);
4004
+ let raw;
4005
+ try {
4006
+ raw = fs6.readFileSync(abs, "utf8");
4007
+ } catch {
4008
+ return void 0;
4009
+ }
4010
+ try {
4011
+ const parsed = JSON.parse(raw);
4012
+ if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
4013
+ const uncapturedPages = parsed.uncapturedPages.filter(
4014
+ (p2) => typeof p2 === "string"
4015
+ );
4016
+ let missingKinds;
4017
+ const mkRaw = parsed.missingKinds;
4018
+ if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
4019
+ const mk = {};
4020
+ for (const [route, kinds] of Object.entries(mkRaw)) {
4021
+ if (Array.isArray(kinds)) {
4022
+ mk[route] = kinds.filter((k) => typeof k === "string");
4023
+ }
4024
+ }
4025
+ missingKinds = mk;
4026
+ }
4027
+ return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
4028
+ } catch {
4029
+ return void 0;
4030
+ }
4031
+ }
4032
+ function loadCapturedStates(configDir, config) {
4033
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
4034
+ const abs = path9.resolve(configDir, rel);
4035
+ let raw;
4036
+ try {
4037
+ raw = fs6.readFileSync(abs, "utf8");
4038
+ } catch {
4039
+ return void 0;
4040
+ }
4041
+ try {
4042
+ const parsed = JSON.parse(raw);
4043
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
4044
+ const captured = [];
4045
+ for (const entry of parsed.captured) {
4046
+ if (!entry || typeof entry !== "object") continue;
4047
+ const c = entry;
4048
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
4049
+ const rec = { entity: c.entity, state: c.state };
4050
+ if (typeof c.kind === "string") rec.kind = c.kind;
4051
+ if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
4052
+ if (typeof c.url === "string") rec.url = c.url;
4053
+ if (typeof c.route === "string") rec.route = c.route;
4054
+ captured.push(rec);
4055
+ }
4056
+ return { captured };
4057
+ } catch {
4058
+ return void 0;
4059
+ }
4060
+ }
3203
4061
  function runScan(opts = {}) {
3204
4062
  const cwd = opts.cwd ?? process.cwd();
3205
4063
  const configs = opts.configs ?? discover({ cwd });
@@ -3226,23 +4084,29 @@ function runOne(dc, opts) {
3226
4084
  flowFiles: extractedFlows
3227
4085
  });
3228
4086
  const gitContext = resolveGitContext({ cwd: configDir });
3229
- const generated = emit({
4087
+ const emitted = emit({
3230
4088
  registry: resolved.registry,
3231
4089
  gitContext
3232
4090
  });
3233
- const outputPath = path9.resolve(configDir, config.output);
3234
- const outputRel = config.output;
3235
- let existingOnDisk = null;
4091
+ const typesRel = config.output;
4092
+ const typesPath = path9.resolve(configDir, typesRel);
4093
+ const dataRel = dataOutputPath(config.output);
4094
+ const dataPath = path9.resolve(configDir, dataRel);
4095
+ const locsRel = locsOutputPath(config.output);
4096
+ const locsPath = path9.resolve(configDir, locsRel);
4097
+ let typesOnDisk = null;
4098
+ let dataOnDisk = null;
4099
+ let locsOnDisk = null;
3236
4100
  if (opts.check) {
3237
- try {
3238
- existingOnDisk = fs6.readFileSync(outputPath, "utf8");
3239
- } catch {
3240
- existingOnDisk = null;
3241
- }
4101
+ typesOnDisk = readOrNull(typesPath);
4102
+ dataOnDisk = readOrNull(dataPath);
4103
+ locsOnDisk = readOrNull(locsPath);
3242
4104
  }
3243
4105
  const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
3244
4106
  (ef) => (ef.diagnostics?.length ?? 0) > 0
3245
4107
  );
4108
+ const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
4109
+ const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
3246
4110
  let auditResult;
3247
4111
  if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
3248
4112
  auditResult = audit({
@@ -3254,9 +4118,20 @@ function runOne(dc, opts) {
3254
4118
  check: opts.check,
3255
4119
  lint: opts.lint,
3256
4120
  resolveDiagnostics: resolved.diagnostics,
3257
- generated,
3258
- existingOnDisk,
3259
- outputPath: outputRel
4121
+ // The entity array (and thus the meaningful drift) lives in the data file,
4122
+ // so the entity-aware gen-stale diff runs against it; the types file gets a
4123
+ // plain content compare (see audit()).
4124
+ generated: emitted.data,
4125
+ existingOnDisk: dataOnDisk,
4126
+ outputPath: dataRel,
4127
+ typesGenerated: emitted.types,
4128
+ typesOnDisk,
4129
+ typesOutputPath: typesRel,
4130
+ locsGenerated: emitted.locs,
4131
+ locsOnDisk,
4132
+ locsOutputPath: locsRel,
4133
+ capturedStates,
4134
+ pageBaseline
3260
4135
  });
3261
4136
  }
3262
4137
  return {
@@ -3265,13 +4140,38 @@ function runOne(dc, opts) {
3265
4140
  registry: resolved.registry,
3266
4141
  gitContext,
3267
4142
  audit: auditResult,
3268
- generated,
3269
- outputPath
4143
+ types: {
4144
+ generated: emitted.types,
4145
+ outputPath: typesPath,
4146
+ outputRel: typesRel
4147
+ },
4148
+ data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
4149
+ locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
4150
+ capturedStates
3270
4151
  };
3271
4152
  }
3272
- function writeScanResult(result2) {
3273
- fs6.mkdirSync(path9.dirname(result2.outputPath), { recursive: true });
3274
- fs6.writeFileSync(result2.outputPath, result2.generated, "utf8");
4153
+ function readOrNull(p2) {
4154
+ try {
4155
+ return fs6.readFileSync(p2, "utf8");
4156
+ } catch {
4157
+ return null;
4158
+ }
4159
+ }
4160
+ function writeArtifact(artifact) {
4161
+ if (readOrNull(artifact.outputPath) === artifact.generated) return false;
4162
+ fs6.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
4163
+ fs6.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4164
+ return true;
4165
+ }
4166
+ function writeScanResult(result) {
4167
+ const wroteTypes = writeArtifact(result.types);
4168
+ const wroteData = writeArtifact(result.data);
4169
+ const wroteLocs = writeArtifact(result.locs);
4170
+ return wroteTypes || wroteData || wroteLocs;
4171
+ }
4172
+ function pageBaselinePath(result) {
4173
+ const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
4174
+ return path9.resolve(result.configDir, rel);
3275
4175
  }
3276
4176
 
3277
4177
  // src/scanner/scan/rename.ts
@@ -3411,7 +4311,7 @@ function renameEntity(opts) {
3411
4311
  ]
3412
4312
  };
3413
4313
  }
3414
- const result2 = applyFixes([
4314
+ const result = applyFixes([
3415
4315
  {
3416
4316
  code: "rename",
3417
4317
  severity: "info",
@@ -3422,8 +4322,8 @@ function renameEntity(opts) {
3422
4322
  }
3423
4323
  }
3424
4324
  ]);
3425
- if (result2.skipped.length > 0) {
3426
- errors.push(`Some edits were skipped: ${result2.skipped[0].reason}`);
4325
+ if (result.skipped.length > 0) {
4326
+ errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
3427
4327
  }
3428
4328
  for (const r of runScan({ cwd })) writeScanResult(r);
3429
4329
  return { edits: edits.length, manual, errors };
@@ -3522,10 +4422,10 @@ function parseArgs(args) {
3522
4422
  async function run(opts) {
3523
4423
  const cwd = opts.cwd ?? process.cwd();
3524
4424
  const { positional, flags } = parseArgs(opts.argv);
3525
- const command2 = positional[0] ?? "help";
4425
+ const command = positional[0] ?? "help";
3526
4426
  const writer = createWriter();
3527
4427
  try {
3528
- switch (command2) {
4428
+ switch (command) {
3529
4429
  case "help":
3530
4430
  case "--help":
3531
4431
  case "-h":
@@ -3540,16 +4440,16 @@ async function run(opts) {
3540
4440
  case "rename":
3541
4441
  return runRename(cwd, positional.slice(1), flags, writer);
3542
4442
  case "ai": {
3543
- const result2 = await runAiCommand({
4443
+ const result = await runAiCommand({
3544
4444
  cwd,
3545
4445
  argv: opts.argv.slice(1)
3546
4446
  });
3547
- if (result2.stdout) writer.out(result2.stdout.replace(/\n$/, ""));
3548
- if (result2.stderr) writer.err(result2.stderr.replace(/\n$/, ""));
3549
- return writer.result(result2.exitCode);
4447
+ if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
4448
+ if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
4449
+ return writer.result(result.exitCode);
3550
4450
  }
3551
4451
  default:
3552
- writer.err(`Unknown command: ${command2}`);
4452
+ writer.err(`Unknown command: ${command}`);
3553
4453
  writer.err(helpText2());
3554
4454
  return writer.result(1);
3555
4455
  }
@@ -3568,16 +4468,13 @@ function helpText2() {
3568
4468
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
3569
4469
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
3570
4470
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3571
- " api <METHOD> <PATH> Call the uidex API",
3572
- " api --list Show available API routes",
3573
- " api login Authenticate via browser",
3574
- " api login --token <tok> Store an auth token directly",
3575
4471
  "",
3576
4472
  "Flags:",
3577
4473
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
3578
4474
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
3579
4475
  " --audit Equivalent to --check --lint (read-only)",
3580
4476
  " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4477
+ " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
3581
4478
  " --json Emit JSON diagnostics on stdout",
3582
4479
  " --force (scaffold) overwrite existing spec",
3583
4480
  ""
@@ -3597,7 +4494,7 @@ function runInit(cwd, w) {
3597
4494
  fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3598
4495
  w.out(`Created ${configPath}`);
3599
4496
  const gitignorePath = path11.join(cwd, ".gitignore");
3600
- const entry = "*.gen.ts";
4497
+ const entry = "*.gen.*";
3601
4498
  if (fs8.existsSync(gitignorePath)) {
3602
4499
  const existing = fs8.readFileSync(gitignorePath, "utf8");
3603
4500
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
@@ -3620,8 +4517,9 @@ function runInit(cwd, w) {
3620
4517
  }
3621
4518
  function runScanCommand(cwd, flags, w) {
3622
4519
  const fix = Boolean(flags.fix);
3623
- const check = !fix && Boolean(flags.check || flags.audit);
3624
- const lint = Boolean(flags.lint || flags.audit || fix);
4520
+ const updateBaseline = Boolean(flags["update-baseline"]);
4521
+ const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
4522
+ const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
3625
4523
  const asJson = Boolean(flags.json);
3626
4524
  let configs = discover({ cwd });
3627
4525
  if (configs.length === 0) {
@@ -3632,16 +4530,41 @@ function runScanCommand(cwd, flags, w) {
3632
4530
  let fixSkipped = [];
3633
4531
  if (fix) {
3634
4532
  const discovery = runScan({ cwd, check: true, lint: true, configs });
3635
- const result2 = applyFixes(
4533
+ const result = applyFixes(
3636
4534
  discovery.flatMap((r) => r.audit?.diagnostics ?? [])
3637
4535
  );
3638
- fixed = result2.applied;
3639
- fixSkipped = result2.skipped;
4536
+ fixed = result.applied;
4537
+ fixSkipped = result.skipped;
3640
4538
  configs = discover({ cwd });
3641
4539
  }
3642
4540
  const results = runScan({ cwd, check, lint, configs });
4541
+ const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
4542
+ const wrote = /* @__PURE__ */ new Set();
3643
4543
  if (!check) {
3644
- for (const r of results) writeScanResult(r);
4544
+ for (const a of artifacts) {
4545
+ if (writeArtifact(a)) wrote.add(a);
4546
+ }
4547
+ }
4548
+ if (updateBaseline) {
4549
+ for (const r of results) {
4550
+ const manifest = r.capturedStates ?? { captured: [] };
4551
+ const pageBaseline = computePageBaseline(r.registry, manifest);
4552
+ const missingKinds = computeMissingKinds(r.registry, manifest);
4553
+ const baseline = {
4554
+ ...pageBaseline,
4555
+ ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
4556
+ };
4557
+ const p2 = pageBaselinePath(r);
4558
+ fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4559
+ const kindGaps = Object.values(missingKinds).reduce(
4560
+ (n, ks) => n + ks.length,
4561
+ 0
4562
+ );
4563
+ w.out(
4564
+ `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
4565
+ );
4566
+ }
4567
+ return w.result(0);
3645
4568
  }
3646
4569
  const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
3647
4570
  const summary = results.reduce(
@@ -3669,10 +4592,14 @@ function runScanCommand(cwd, flags, w) {
3669
4592
  );
3670
4593
  }
3671
4594
  for (const r of results) {
3672
- if (check) {
3673
- w.out(`Checked ${r.outputPath}`);
3674
- } else {
3675
- w.out(`Wrote ${r.outputPath}`);
4595
+ for (const art of [r.types, r.data, r.locs]) {
4596
+ if (check) {
4597
+ w.out(`Checked ${art.outputPath}`);
4598
+ } else if (wrote.has(art)) {
4599
+ w.out(`Wrote ${art.outputPath}`);
4600
+ } else {
4601
+ w.out(`Unchanged ${art.outputPath}`);
4602
+ }
3676
4603
  }
3677
4604
  for (const d of r.audit?.diagnostics ?? []) {
3678
4605
  const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
@@ -3708,18 +4635,18 @@ function runScaffold(cwd, args, flags, w) {
3708
4635
  const entity = r.registry.get(scaffoldKind, id);
3709
4636
  if (!entity) continue;
3710
4637
  const outDir = path11.resolve(r.configDir, "e2e");
3711
- const result2 = scaffoldSpec({
4638
+ const result = scaffoldSpec({
3712
4639
  registry: r.registry,
3713
4640
  kind: scaffoldKind,
3714
4641
  id,
3715
4642
  outDir,
3716
4643
  force: Boolean(flags.force)
3717
4644
  });
3718
- if (result2.skipped) {
3719
- w.err(result2.reason ?? "skipped");
4645
+ if (result.skipped) {
4646
+ w.err(result.reason ?? "skipped");
3720
4647
  return w.result(1);
3721
4648
  }
3722
- w.out(`Wrote ${result2.outputPath}`);
4649
+ w.out(`Wrote ${result.outputPath}`);
3723
4650
  return w.result(0);
3724
4651
  }
3725
4652
  w.err(
@@ -3736,24 +4663,24 @@ function runRename(cwd, args, flags, w) {
3736
4663
  );
3737
4664
  return w.result(1);
3738
4665
  }
3739
- const result2 = renameEntity({
4666
+ const result = renameEntity({
3740
4667
  cwd,
3741
4668
  kind,
3742
4669
  oldId,
3743
4670
  newId,
3744
4671
  force: Boolean(flags.force)
3745
4672
  });
3746
- for (const e of result2.errors) w.err(e);
3747
- for (const m of result2.manual) {
4673
+ for (const e of result.errors) w.err(e);
4674
+ for (const m of result.manual) {
3748
4675
  w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
3749
4676
  }
3750
- if (result2.errors.length > 0) return w.result(1);
4677
+ if (result.errors.length > 0) return w.result(1);
3751
4678
  w.out(
3752
- `Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result2.edits} edit(s)); gen file regenerated`
4679
+ `Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
3753
4680
  );
3754
- if (result2.manual.length > 0) {
4681
+ if (result.manual.length > 0) {
3755
4682
  w.err(
3756
- `${result2.manual.length} occurrence(s) need manual follow-up (listed above)`
4683
+ `${result.manual.length} occurrence(s) need manual follow-up (listed above)`
3757
4684
  );
3758
4685
  return w.result(1);
3759
4686
  }
@@ -3775,883 +4702,14 @@ function createWriter() {
3775
4702
  };
3776
4703
  }
3777
4704
 
3778
- // src/scanner/cli/auth.ts
3779
- function createMemoryTokenStorage(initial) {
3780
- let token = initial ?? null;
3781
- return {
3782
- get: () => token,
3783
- set: (t) => {
3784
- token = t;
3785
- },
3786
- clear: () => {
3787
- token = null;
3788
- }
3789
- };
3790
- }
3791
- function createFileTokenStorage(options) {
3792
- const nodeFs = require("fs");
3793
- const nodePath = require("path");
3794
- const file = options.path;
3795
- function read() {
3796
- if (!nodeFs.existsSync(file)) return null;
3797
- try {
3798
- const raw = nodeFs.readFileSync(file, "utf8");
3799
- const parsed = JSON.parse(raw);
3800
- if (parsed && typeof parsed === "object") {
3801
- return parsed;
3802
- }
3803
- } catch {
3804
- return null;
3805
- }
3806
- return null;
3807
- }
3808
- return {
3809
- get: () => read()?.token ?? null,
3810
- set: (token) => {
3811
- nodeFs.mkdirSync(nodePath.dirname(file), { recursive: true });
3812
- nodeFs.writeFileSync(file, JSON.stringify({ token }, null, 2));
3813
- },
3814
- clear: () => {
3815
- if (nodeFs.existsSync(file)) nodeFs.unlinkSync(file);
3816
- }
3817
- };
3818
- }
3819
- function defaultTokenPath() {
3820
- const os = require("os");
3821
- const path12 = require("path");
3822
- return path12.join(os.homedir(), ".uidex", "cloud-token.json");
3823
- }
3824
-
3825
- // src/scanner/cli/http.ts
3826
- function createHttpClient(options) {
3827
- const baseUrl = options.baseUrl.replace(/\/$/, "");
3828
- async function request(path12, opts = {}) {
3829
- let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
3830
- if (opts.query) {
3831
- url += (url.includes("?") ? "&" : "?") + opts.query;
3832
- }
3833
- const headers = {
3834
- Accept: "application/json",
3835
- ...opts.headers
3836
- };
3837
- const token = options.token();
3838
- if (token) headers.Authorization = `Bearer ${token}`;
3839
- if (opts.body !== void 0) {
3840
- headers["Content-Type"] ??= "application/json";
3841
- }
3842
- const res = await fetch(url, {
3843
- method: opts.method ?? (opts.body ? "POST" : "GET"),
3844
- headers,
3845
- body: opts.body
3846
- });
3847
- const raw = await res.text();
3848
- let body = raw;
3849
- try {
3850
- body = JSON.parse(raw);
3851
- } catch {
3852
- }
3853
- return { status: res.status, body, raw };
3854
- }
3855
- return { request };
3856
- }
3857
-
3858
- // src/scanner/cli/json-highlight.ts
3859
- var RESET = "\x1B[0m";
3860
- var BOLD = "\x1B[1m";
3861
- var DIM = "\x1B[2m";
3862
- var RED = "\x1B[31m";
3863
- var CYAN = "\x1B[36m";
3864
- var GREEN = "\x1B[32m";
3865
- var YELLOW = "\x1B[33m";
3866
- var MAGENTA = "\x1B[35m";
3867
- function highlightJson(value, color) {
3868
- const json = JSON.stringify(value, null, 2);
3869
- if (!color) return json;
3870
- return json.replace(
3871
- /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
3872
- (match, key, str, literal, num) => {
3873
- if (key) return `${CYAN}${key}${RESET}:`;
3874
- if (str) return `${GREEN}${str}${RESET}`;
3875
- if (literal) return `${MAGENTA}${literal}${RESET}`;
3876
- if (num) return `${YELLOW}${num}${RESET}`;
3877
- return match;
3878
- }
3879
- );
3880
- }
3881
- function formatStatus(status, color) {
3882
- if (!color) return `HTTP ${status}`;
3883
- const code = status >= 400 ? RED : status >= 300 ? YELLOW : GREEN;
3884
- return `${code}HTTP ${status}${RESET}`;
3885
- }
3886
- function boldText(text, color) {
3887
- if (!color) return text;
3888
- return `${BOLD}${text}${RESET}`;
3889
- }
3890
- function dimText(text, color) {
3891
- if (!color) return text;
3892
- return `${DIM}${text}${RESET}`;
3893
- }
3894
-
3895
- // src/scanner/cli/api-routes.gen.ts
3896
- var API_ROUTES = [
3897
- {
3898
- "method": "POST",
3899
- "path": "/api/ingest",
3900
- "operationId": "submitReport",
3901
- "summary": "Submit feedback from the SDK.",
3902
- "tag": "Ingest",
3903
- "params": []
3904
- },
3905
- {
3906
- "method": "GET",
3907
- "path": "/api/ingest/config",
3908
- "operationId": "getIngestConfig",
3909
- "summary": "Get integration configuration for the project.",
3910
- "tag": "Ingest",
3911
- "params": []
3912
- },
3913
- {
3914
- "method": "GET",
3915
- "path": "/api/ingest/reports",
3916
- "operationId": "listIngestReports",
3917
- "summary": "List feedback for the project (SDK view).",
3918
- "tag": "Ingest",
3919
- "params": []
3920
- },
3921
- {
3922
- "method": "GET",
3923
- "path": "/api/ingest/pins",
3924
- "operationId": "listPins",
3925
- "summary": "List pins by route and/or entity.",
3926
- "tag": "Ingest",
3927
- "params": []
3928
- },
3929
- {
3930
- "method": "POST",
3931
- "path": "/api/ingest/pins/archive",
3932
- "operationId": "archivePin",
3933
- "summary": "Archive a pin.",
3934
- "tag": "Ingest",
3935
- "params": []
3936
- },
3937
- {
3938
- "method": "POST",
3939
- "path": "/api/cli-auth/authorize",
3940
- "operationId": "authorizeCli",
3941
- "summary": "Authorize a CLI session by delivering a token.",
3942
- "tag": "CLI Auth",
3943
- "params": []
3944
- },
3945
- {
3946
- "method": "GET",
3947
- "path": "/api/organizations",
3948
- "operationId": "listOrganizations",
3949
- "summary": "List organizations for the current user.",
3950
- "tag": "Organizations",
3951
- "params": []
3952
- },
3953
- {
3954
- "method": "POST",
3955
- "path": "/api/organizations",
3956
- "operationId": "createOrganization",
3957
- "summary": "Create an organization.",
3958
- "tag": "Organizations",
3959
- "params": []
3960
- },
3961
- {
3962
- "method": "POST",
3963
- "path": "/api/organizations/switch",
3964
- "operationId": "switchOrganization",
3965
- "summary": "Switch the active organization.",
3966
- "tag": "Organizations",
3967
- "params": []
3968
- },
3969
- {
3970
- "method": "GET",
3971
- "path": "/api/organizations/{orgId}",
3972
- "operationId": "getOrganization",
3973
- "summary": "Get organization details.",
3974
- "tag": "Organizations",
3975
- "params": [
3976
- "orgId"
3977
- ]
3978
- },
3979
- {
3980
- "method": "PATCH",
3981
- "path": "/api/organizations/{orgId}",
3982
- "operationId": "updateOrganization",
3983
- "summary": "Update an organization.",
3984
- "tag": "Organizations",
3985
- "params": [
3986
- "orgId"
3987
- ]
3988
- },
3989
- {
3990
- "method": "DELETE",
3991
- "path": "/api/organizations/{orgId}",
3992
- "operationId": "deleteOrganization",
3993
- "summary": "Delete an organization.",
3994
- "tag": "Organizations",
3995
- "params": [
3996
- "orgId"
3997
- ]
3998
- },
3999
- {
4000
- "method": "GET",
4001
- "path": "/api/organizations/{orgId}/members",
4002
- "operationId": "listMembers",
4003
- "summary": "List organization members.",
4004
- "tag": "Members",
4005
- "params": [
4006
- "orgId"
4007
- ]
4008
- },
4009
- {
4010
- "method": "DELETE",
4011
- "path": "/api/organizations/{orgId}/members",
4012
- "operationId": "removeMember",
4013
- "summary": "Remove a member from the organization.",
4014
- "tag": "Members",
4015
- "params": [
4016
- "orgId"
4017
- ]
4018
- },
4019
- {
4020
- "method": "PATCH",
4021
- "path": "/api/organizations/{orgId}/members/{memberId}",
4022
- "operationId": "updateMember",
4023
- "summary": "Update a member's role.",
4024
- "tag": "Members",
4025
- "params": [
4026
- "orgId",
4027
- "memberId"
4028
- ]
4029
- },
4030
- {
4031
- "method": "GET",
4032
- "path": "/api/organizations/{orgId}/invitations",
4033
- "operationId": "listInvitations",
4034
- "summary": "List organization invitations.",
4035
- "tag": "Invitations",
4036
- "params": [
4037
- "orgId"
4038
- ]
4039
- },
4040
- {
4041
- "method": "POST",
4042
- "path": "/api/organizations/{orgId}/invitations",
4043
- "operationId": "createInvitation",
4044
- "summary": "Create an invitation link.",
4045
- "tag": "Invitations",
4046
- "params": [
4047
- "orgId"
4048
- ]
4049
- },
4050
- {
4051
- "method": "DELETE",
4052
- "path": "/api/organizations/{orgId}/invitations/{inviteId}",
4053
- "operationId": "deleteInvitation",
4054
- "summary": "Revoke an invitation.",
4055
- "tag": "Invitations",
4056
- "params": [
4057
- "orgId",
4058
- "inviteId"
4059
- ]
4060
- },
4061
- {
4062
- "method": "GET",
4063
- "path": "/api/organizations/{orgId}/projects",
4064
- "operationId": "listProjects",
4065
- "summary": "List projects in an organization.",
4066
- "tag": "Projects",
4067
- "params": [
4068
- "orgId"
4069
- ]
4070
- },
4071
- {
4072
- "method": "POST",
4073
- "path": "/api/organizations/{orgId}/projects",
4074
- "operationId": "createProject",
4075
- "summary": "Create a project.",
4076
- "tag": "Projects",
4077
- "params": [
4078
- "orgId"
4079
- ]
4080
- },
4081
- {
4082
- "method": "GET",
4083
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4084
- "operationId": "getProject",
4085
- "summary": "Get project details.",
4086
- "tag": "Projects",
4087
- "params": [
4088
- "orgId",
4089
- "projectId"
4090
- ]
4091
- },
4092
- {
4093
- "method": "PATCH",
4094
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4095
- "operationId": "updateProject",
4096
- "summary": "Update a project.",
4097
- "tag": "Projects",
4098
- "params": [
4099
- "orgId",
4100
- "projectId"
4101
- ]
4102
- },
4103
- {
4104
- "method": "DELETE",
4105
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4106
- "operationId": "deleteProject",
4107
- "summary": "Delete a project.",
4108
- "tag": "Projects",
4109
- "params": [
4110
- "orgId",
4111
- "projectId"
4112
- ]
4113
- },
4114
- {
4115
- "method": "GET",
4116
- "path": "/api/projects/resolve",
4117
- "operationId": "resolveProject",
4118
- "summary": "Resolve an API key to its project and organization.",
4119
- "tag": "Projects",
4120
- "params": []
4121
- },
4122
- {
4123
- "method": "GET",
4124
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
4125
- "operationId": "listApiKeys",
4126
- "summary": "List API keys for a project.",
4127
- "tag": "API Keys",
4128
- "params": [
4129
- "orgId",
4130
- "projectId"
4131
- ]
4132
- },
4133
- {
4134
- "method": "POST",
4135
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
4136
- "operationId": "createApiKey",
4137
- "summary": "Create an API key.",
4138
- "tag": "API Keys",
4139
- "params": [
4140
- "orgId",
4141
- "projectId"
4142
- ]
4143
- },
4144
- {
4145
- "method": "PATCH",
4146
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
4147
- "operationId": "revokeApiKey",
4148
- "summary": "Revoke an API key.",
4149
- "tag": "API Keys",
4150
- "params": [
4151
- "orgId",
4152
- "projectId",
4153
- "keyId"
4154
- ]
4155
- },
4156
- {
4157
- "method": "DELETE",
4158
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
4159
- "operationId": "deleteApiKey",
4160
- "summary": "Permanently delete an API key.",
4161
- "tag": "API Keys",
4162
- "params": [
4163
- "orgId",
4164
- "projectId",
4165
- "keyId"
4166
- ]
4167
- },
4168
- {
4169
- "method": "GET",
4170
- "path": "/api/organizations/{orgId}/projects/{projectId}/reports",
4171
- "operationId": "listProjectReports",
4172
- "summary": "List feedback for a project.",
4173
- "tag": "Reports",
4174
- "params": [
4175
- "orgId",
4176
- "projectId"
4177
- ]
4178
- },
4179
- {
4180
- "method": "GET",
4181
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4182
- "operationId": "getReport",
4183
- "summary": "Get feedback details.",
4184
- "tag": "Reports",
4185
- "params": [
4186
- "orgId",
4187
- "projectId",
4188
- "reportId"
4189
- ]
4190
- },
4191
- {
4192
- "method": "PATCH",
4193
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4194
- "operationId": "updateReport",
4195
- "summary": "Update feedback status, priority, or assignment.",
4196
- "tag": "Reports",
4197
- "params": [
4198
- "orgId",
4199
- "projectId",
4200
- "reportId"
4201
- ]
4202
- },
4203
- {
4204
- "method": "DELETE",
4205
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4206
- "operationId": "deleteReport",
4207
- "summary": "Delete a feedback record.",
4208
- "tag": "Reports",
4209
- "params": [
4210
- "orgId",
4211
- "projectId",
4212
- "reportId"
4213
- ]
4214
- },
4215
- {
4216
- "method": "POST",
4217
- "path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
4218
- "operationId": "bulkUpdateReportStatus",
4219
- "summary": "Update status on multiple feedback records.",
4220
- "tag": "Reports",
4221
- "params": [
4222
- "orgId",
4223
- "projectId"
4224
- ]
4225
- },
4226
- {
4227
- "method": "GET",
4228
- "path": "/api/organizations/{orgId}/integrations",
4229
- "operationId": "listIntegrations",
4230
- "summary": "List integrations for an organization.",
4231
- "tag": "Integrations",
4232
- "params": [
4233
- "orgId"
4234
- ]
4235
- },
4236
- {
4237
- "method": "POST",
4238
- "path": "/api/organizations/{orgId}/integrations",
4239
- "operationId": "createIntegration",
4240
- "summary": "Create an integration.",
4241
- "tag": "Integrations",
4242
- "params": [
4243
- "orgId"
4244
- ]
4245
- },
4246
- {
4247
- "method": "GET",
4248
- "path": "/api/organizations/{orgId}/integrations/{integrationId}",
4249
- "operationId": "getIntegration",
4250
- "summary": "Get integration details.",
4251
- "tag": "Integrations",
4252
- "params": [
4253
- "orgId",
4254
- "integrationId"
4255
- ]
4256
- },
4257
- {
4258
- "method": "DELETE",
4259
- "path": "/api/organizations/{orgId}/integrations/{integrationId}",
4260
- "operationId": "deleteIntegration",
4261
- "summary": "Delete an integration.",
4262
- "tag": "Integrations",
4263
- "params": [
4264
- "orgId",
4265
- "integrationId"
4266
- ]
4267
- },
4268
- {
4269
- "method": "GET",
4270
- "path": "/api/organizations/{orgId}/integrations/{integrationId}/targets",
4271
- "operationId": "listIntegrationTargets",
4272
- "summary": "List available targets (e.g. Jira projects/boards).",
4273
- "tag": "Integrations",
4274
- "params": [
4275
- "orgId",
4276
- "integrationId"
4277
- ]
4278
- },
4279
- {
4280
- "method": "POST",
4281
- "path": "/api/organizations/{orgId}/integrations/{integrationId}/test",
4282
- "operationId": "testIntegration",
4283
- "summary": "Test integration connectivity.",
4284
- "tag": "Integrations",
4285
- "params": [
4286
- "orgId",
4287
- "integrationId"
4288
- ]
4289
- },
4290
- {
4291
- "method": "GET",
4292
- "path": "/api/organizations/{orgId}/external-issues",
4293
- "operationId": "listExternalIssues",
4294
- "summary": "List external issues, optionally filtered by feedback.",
4295
- "tag": "External Issues",
4296
- "params": [
4297
- "orgId"
4298
- ]
4299
- },
4300
- {
4301
- "method": "GET",
4302
- "path": "/api/organizations/{orgId}/issue-drafts",
4303
- "operationId": "listIssueDrafts",
4304
- "summary": "List issue drafts for a project.",
4305
- "tag": "Issue Drafts",
4306
- "params": [
4307
- "orgId"
4308
- ]
4309
- },
4310
- {
4311
- "method": "POST",
4312
- "path": "/api/organizations/{orgId}/issue-drafts",
4313
- "operationId": "createIssueDraft",
4314
- "summary": "Manually create an issue draft.",
4315
- "tag": "Issue Drafts",
4316
- "params": [
4317
- "orgId"
4318
- ]
4319
- },
4320
- {
4321
- "method": "PATCH",
4322
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4323
- "operationId": "updateIssueDraft",
4324
- "summary": "Update an issue draft.",
4325
- "tag": "Issue Drafts",
4326
- "params": [
4327
- "orgId",
4328
- "draftId"
4329
- ]
4330
- },
4331
- {
4332
- "method": "DELETE",
4333
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4334
- "operationId": "deleteIssueDraft",
4335
- "summary": "Delete an issue draft.",
4336
- "tag": "Issue Drafts",
4337
- "params": [
4338
- "orgId",
4339
- "draftId"
4340
- ]
4341
- },
4342
- {
4343
- "method": "POST",
4344
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}/submit",
4345
- "operationId": "submitIssueDraft",
4346
- "summary": "Submit a draft to the configured integration.",
4347
- "tag": "Issue Drafts",
4348
- "params": [
4349
- "orgId",
4350
- "draftId"
4351
- ]
4352
- },
4353
- {
4354
- "method": "POST",
4355
- "path": "/api/organizations/{orgId}/projects/{projectId}/triage",
4356
- "operationId": "runTriage",
4357
- "summary": "Run LLM-powered triage on untriaged feedback.",
4358
- "tag": "Triage",
4359
- "params": [
4360
- "orgId",
4361
- "projectId"
4362
- ]
4363
- },
4364
- {
4365
- "method": "GET",
4366
- "path": "/api/user/tokens",
4367
- "operationId": "listUserTokens",
4368
- "summary": "List personal access tokens.",
4369
- "tag": "User Tokens",
4370
- "params": []
4371
- },
4372
- {
4373
- "method": "POST",
4374
- "path": "/api/user/tokens",
4375
- "operationId": "createUserToken",
4376
- "summary": "Create a personal access token.",
4377
- "tag": "User Tokens",
4378
- "params": []
4379
- },
4380
- {
4381
- "method": "DELETE",
4382
- "path": "/api/user/tokens/{tokenId}",
4383
- "operationId": "deleteUserToken",
4384
- "summary": "Revoke and delete a personal access token.",
4385
- "tag": "User Tokens",
4386
- "params": [
4387
- "tokenId"
4388
- ]
4389
- }
4390
- ];
4391
-
4392
- // src/scanner/cli/api.ts
4393
- var METHODS = /* @__PURE__ */ new Set([
4394
- "GET",
4395
- "POST",
4396
- "PUT",
4397
- "PATCH",
4398
- "DELETE",
4399
- "HEAD",
4400
- "OPTIONS"
4401
- ]);
4402
- var HELP = `uidex api \u2014 call the uidex API
4403
-
4404
- Usage:
4405
- uidex api <METHOD> <PATH> [flags] Make an API request
4406
- uidex api --list [--tag <tag>] List available routes
4407
- uidex api login Authenticate via browser
4408
- uidex api login --token <tok> Store an auth token directly
4409
- uidex api status Check auth state
4410
-
4411
- Flags:
4412
- --body <json> Request body (JSON string)
4413
- --query <params> Query string (key=val&key2=val2)
4414
- --base-url <url> Override the API base URL
4415
- --token <tok> Use a specific token (instead of stored)
4416
- --raw Disable JSON pretty-printing
4417
- --no-color Disable colored output
4418
- --list List available API routes
4419
- --tag <tag> Filter --list by tag
4420
- --json Emit --list as JSON
4421
- `;
4422
- async function runApiCommand(opts) {
4423
- const { positional, flags } = parseArgs(opts.argv);
4424
- const stdout = [];
4425
- const stderr = [];
4426
- const color = opts.color ?? (flags["no-color"] ? false : process.stdout.isTTY ?? false);
4427
- const ctx = { flags, opts, color, stdout, stderr };
4428
- const sub = positional[0];
4429
- if (!sub && !flags.list) {
4430
- stdout.push(HELP);
4431
- return result(0, stdout, stderr);
4432
- }
4433
- if (flags.list) return listRoutes(ctx);
4434
- if (sub === "help" || sub === "--help" || sub === "-h") {
4435
- stdout.push(HELP);
4436
- return result(0, stdout, stderr);
4437
- }
4438
- if (sub === "login") return runLogin(ctx);
4439
- if (sub === "status") return runStatus(ctx);
4440
- const method = sub?.toUpperCase();
4441
- const path12 = positional[1];
4442
- if (!method || !METHODS.has(method)) {
4443
- stderr.push(`Unknown command or method: ${sub}`);
4444
- stderr.push(HELP);
4445
- return result(1, stdout, stderr);
4446
- }
4447
- if (!path12) {
4448
- stderr.push("Missing path. Usage: uidex api GET /api/organizations");
4449
- return result(1, stdout, stderr);
4450
- }
4451
- return runRequest(ctx, method, path12);
4452
- }
4453
- function listRoutes(ctx) {
4454
- const { flags, color, stdout, stderr } = ctx;
4455
- let routes = API_ROUTES;
4456
- const tagFilter = flags.tag;
4457
- if (typeof tagFilter === "string") {
4458
- routes = routes.filter(
4459
- (r) => r.tag.toLowerCase() === tagFilter.toLowerCase()
4460
- );
4461
- }
4462
- if (flags.json) {
4463
- stdout.push(JSON.stringify(routes, null, 2));
4464
- return result(0, stdout, stderr);
4465
- }
4466
- const grouped = /* @__PURE__ */ new Map();
4467
- for (const r of routes) {
4468
- const tag = r.tag || "Other";
4469
- let list = grouped.get(tag);
4470
- if (!list) {
4471
- list = [];
4472
- grouped.set(tag, list);
4473
- }
4474
- list.push(r);
4475
- }
4476
- const methodPad = 7;
4477
- for (const [tag, tagRoutes] of grouped) {
4478
- stdout.push("");
4479
- stdout.push(boldText(tag, color));
4480
- for (const r of tagRoutes) {
4481
- const m = r.method.padEnd(methodPad);
4482
- const desc = r.summary ? dimText(` ${r.operationId} \u2014 ${r.summary}`, color) : dimText(` ${r.operationId}`, color);
4483
- stdout.push(` ${m} ${r.path}${desc}`);
4484
- }
4485
- }
4486
- stdout.push("");
4487
- return result(0, stdout, stderr);
4488
- }
4489
- async function runLogin(ctx) {
4490
- const { flags, opts, stdout, stderr } = ctx;
4491
- const token = flags.token;
4492
- if (typeof token === "string" && token.length > 0) {
4493
- const storage = resolveTokenStorage(opts);
4494
- storage.set(token);
4495
- stdout.push("Token stored.");
4496
- return result(0, stdout, stderr);
4497
- }
4498
- return runBrowserLogin(ctx);
4499
- }
4500
- async function runBrowserLogin(ctx) {
4501
- const { flags, opts, color, stdout, stderr } = ctx;
4502
- const http = await import("http");
4503
- const crypto = await import("crypto");
4504
- const { exec } = await import("child_process");
4505
- const baseUrl = resolveBaseUrl(flags, opts);
4506
- const code = crypto.randomBytes(3).toString("hex").toUpperCase();
4507
- return new Promise((resolve7) => {
4508
- let settled = false;
4509
- const settle = (exitCode) => {
4510
- if (settled) return;
4511
- settled = true;
4512
- server.close();
4513
- clearTimeout(timeout);
4514
- resolve7(result(exitCode, stdout, stderr));
4515
- };
4516
- const timeout = setTimeout(() => {
4517
- stderr.push("Timed out waiting for browser authorization.");
4518
- settle(1);
4519
- }, 12e4);
4520
- const server = http.createServer((req, res) => {
4521
- if (!req.url?.startsWith("/callback")) {
4522
- res.writeHead(404);
4523
- res.end();
4524
- return;
4525
- }
4526
- const url = new URL(req.url, `http://127.0.0.1`);
4527
- const receivedToken = url.searchParams.get("token");
4528
- const receivedCode = url.searchParams.get("code");
4529
- if (receivedCode !== code) {
4530
- res.writeHead(400, { "Content-Type": "application/json" });
4531
- res.end(JSON.stringify({ error: "Code mismatch" }));
4532
- stderr.push("Received callback with mismatched code. Aborting.");
4533
- settle(1);
4534
- return;
4535
- }
4536
- if (!receivedToken) {
4537
- res.writeHead(400, { "Content-Type": "application/json" });
4538
- res.end(JSON.stringify({ error: "Missing token" }));
4539
- stderr.push("Received callback without token. Aborting.");
4540
- settle(1);
4541
- return;
4542
- }
4543
- res.writeHead(200, { "Content-Type": "application/json" });
4544
- res.end(JSON.stringify({ ok: true }));
4545
- const storage = resolveTokenStorage(opts);
4546
- storage.set(receivedToken);
4547
- stdout.push(dimText("Authenticated successfully.", color));
4548
- settle(0);
4549
- });
4550
- server.listen(0, "127.0.0.1", () => {
4551
- const addr = server.address();
4552
- if (!addr || typeof addr === "string") {
4553
- stderr.push("Failed to start local server.");
4554
- settle(1);
4555
- return;
4556
- }
4557
- const port = addr.port;
4558
- const authUrl = `${baseUrl}/cli-auth?code=${encodeURIComponent(code)}&port=${port}`;
4559
- stdout.push(dimText("Opening browser to authorize...", color));
4560
- stdout.push("");
4561
- stdout.push(` Confirmation code: ${boldText(code, color)}`);
4562
- stdout.push("");
4563
- stdout.push(
4564
- dimText("If the browser doesn't open, visit this URL manually:", color)
4565
- );
4566
- stdout.push(` ${authUrl}`);
4567
- stdout.push("");
4568
- process.stdout.write(result(0, stdout, stderr).stdout);
4569
- stdout.length = 0;
4570
- openBrowser(authUrl, exec);
4571
- });
4572
- server.on("error", (err2) => {
4573
- stderr.push(`Failed to start local server: ${err2.message}`);
4574
- settle(1);
4575
- });
4576
- });
4577
- }
4578
- function openBrowser(url, exec) {
4579
- const platform = process.platform;
4580
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
4581
- exec(`${cmd} ${JSON.stringify(url)}`);
4582
- }
4583
- function runStatus(ctx) {
4584
- const { flags, opts, stdout, stderr } = ctx;
4585
- const storage = resolveTokenStorage(opts);
4586
- const baseUrl = resolveBaseUrl(flags, opts);
4587
- const token = storage.get();
4588
- stdout.push(`baseUrl: ${baseUrl}`);
4589
- stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
4590
- return result(token ? 0 : 1, stdout, stderr);
4591
- }
4592
- async function runRequest(ctx, method, path12) {
4593
- const { flags, opts, color, stdout, stderr } = ctx;
4594
- const tokenFromFlag = flags.token;
4595
- const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
4596
- if (!token) {
4597
- stderr.push("Not authenticated. Run: uidex api login --token <value>");
4598
- return result(1, stdout, stderr);
4599
- }
4600
- const baseUrl = resolveBaseUrl(flags, opts);
4601
- const http = opts.http ?? createHttpClient({ baseUrl, token: () => token });
4602
- const bodyStr = typeof flags.body === "string" ? flags.body : void 0;
4603
- if (bodyStr !== void 0) {
4604
- try {
4605
- JSON.parse(bodyStr);
4606
- } catch {
4607
- stderr.push("Invalid JSON in --body");
4608
- return result(1, stdout, stderr);
4609
- }
4610
- }
4611
- const res = await http.request(path12, {
4612
- method,
4613
- body: bodyStr,
4614
- query: typeof flags.query === "string" ? flags.query : void 0
4615
- });
4616
- const isSuccess = res.status >= 200 && res.status < 300;
4617
- if (!isSuccess) {
4618
- stderr.push(formatStatus(res.status, color));
4619
- }
4620
- if (res.body !== void 0 && res.body !== null && res.body !== "") {
4621
- if (flags.raw) {
4622
- stdout.push(res.raw);
4623
- } else {
4624
- stdout.push(highlightJson(res.body, color));
4625
- }
4626
- }
4627
- return result(isSuccess ? 0 : 1, stdout, stderr);
4628
- }
4629
- function resolveTokenStorage(opts) {
4630
- if (opts.tokenStorage) return opts.tokenStorage;
4631
- const envToken = opts.env?.UIDEX_TOKEN;
4632
- if (envToken) return createMemoryTokenStorage(envToken);
4633
- return createFileTokenStorage({ path: defaultTokenPath() });
4634
- }
4635
- function resolveBaseUrl(flags, opts) {
4636
- return (typeof flags["base-url"] === "string" ? flags["base-url"] : void 0) ?? opts.baseUrl ?? opts.env?.UIDEX_API_URL ?? "https://app.uidex.dev";
4637
- }
4638
- function result(exitCode, stdout, stderr) {
4639
- return {
4640
- exitCode,
4641
- stdout: stdout.join("\n") + (stdout.length ? "\n" : ""),
4642
- stderr: stderr.join("\n") + (stderr.length ? "\n" : "")
4643
- };
4644
- }
4645
-
4646
4705
  // src/scanner/cli/cli.ts
4647
4706
  var argv = process.argv.slice(2);
4648
- var command = argv[0];
4649
- var resultP = command === "api" ? runApiCommand({ argv: argv.slice(1) }) : run({ argv });
4707
+ var resultP = run({ argv });
4650
4708
  resultP.then(
4651
- (result2) => {
4652
- if (result2.stdout) process.stdout.write(result2.stdout);
4653
- if (result2.stderr) process.stderr.write(result2.stderr);
4654
- process.exit(result2.exitCode);
4709
+ (result) => {
4710
+ if (result.stdout) process.stdout.write(result.stdout);
4711
+ if (result.stderr) process.stderr.write(result.stderr);
4712
+ process.exit(result.exitCode);
4655
4713
  },
4656
4714
  (error) => {
4657
4715
  process.stderr.write(