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