uidex 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1273 -343
- 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 +1217 -257
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +410 -104
- package/dist/scan/index.d.ts +410 -104
- package/dist/scan/index.js +1197 -253
- package/dist/scan/index.js.map +1 -1
- package/package.json +17 -17
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
|
}
|
|
@@ -407,7 +472,13 @@ function validateConfig(raw) {
|
|
|
407
472
|
fail(`conventions.features must be a string or false`);
|
|
408
473
|
}
|
|
409
474
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
410
|
-
|
|
475
|
+
const p2 = c.pages;
|
|
476
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
477
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
478
|
+
fail(
|
|
479
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
411
482
|
}
|
|
412
483
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
413
484
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -425,7 +496,8 @@ function validateConfig(raw) {
|
|
|
425
496
|
audit: raw.audit,
|
|
426
497
|
conventions: raw.conventions,
|
|
427
498
|
statesManifest: raw.statesManifest,
|
|
428
|
-
coverageBaseline: raw.coverageBaseline
|
|
499
|
+
coverageBaseline: raw.coverageBaseline,
|
|
500
|
+
workspace: raw.workspace
|
|
429
501
|
};
|
|
430
502
|
}
|
|
431
503
|
function parseConfig(json) {
|
|
@@ -475,9 +547,11 @@ function tryReadConfig(configPath) {
|
|
|
475
547
|
function discover(options = {}) {
|
|
476
548
|
const cwd = options.cwd ?? process.cwd();
|
|
477
549
|
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
550
|
+
const all = options.all ?? false;
|
|
478
551
|
const rootMatch = tryReadConfig(path3.join(cwd, CONFIG_FILENAME));
|
|
479
|
-
if (rootMatch) return [rootMatch];
|
|
552
|
+
if (rootMatch && !all) return [rootMatch];
|
|
480
553
|
const results = [];
|
|
554
|
+
if (rootMatch) results.push(rootMatch);
|
|
481
555
|
const queue = [[cwd, 0]];
|
|
482
556
|
while (queue.length > 0) {
|
|
483
557
|
const [dir, depth] = queue.shift();
|
|
@@ -598,11 +672,11 @@ function applyFixes(diagnostics) {
|
|
|
598
672
|
}
|
|
599
673
|
|
|
600
674
|
// src/scanner/scan/pipeline.ts
|
|
601
|
-
var
|
|
602
|
-
var
|
|
675
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
676
|
+
var path10 = __toESM(require("path"), 1);
|
|
603
677
|
|
|
604
678
|
// src/scanner/scan/audit.ts
|
|
605
|
-
var
|
|
679
|
+
var path7 = __toESM(require("path"), 1);
|
|
606
680
|
|
|
607
681
|
// src/shared/entities/types.ts
|
|
608
682
|
var ENTITY_KINDS = [
|
|
@@ -799,6 +873,320 @@ function createRegistry() {
|
|
|
799
873
|
};
|
|
800
874
|
}
|
|
801
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
|
+
|
|
802
1190
|
// src/scanner/scan/page-coverage.ts
|
|
803
1191
|
function compileRoute(pattern) {
|
|
804
1192
|
const segs = pattern.split("/").filter(Boolean);
|
|
@@ -820,11 +1208,11 @@ function compileRoute(pattern) {
|
|
|
820
1208
|
return { test: (url) => re.test(url), specificity };
|
|
821
1209
|
}
|
|
822
1210
|
function matchUrlToRoute(url, routePaths) {
|
|
823
|
-
const
|
|
1211
|
+
const path13 = stripQuery(url);
|
|
824
1212
|
let best = null;
|
|
825
1213
|
for (const rp of routePaths) {
|
|
826
1214
|
const { test, specificity } = compileRoute(rp);
|
|
827
|
-
if (!test(
|
|
1215
|
+
if (!test(path13)) continue;
|
|
828
1216
|
if (!best || specificity > best.specificity)
|
|
829
1217
|
best = { path: rp, specificity };
|
|
830
1218
|
}
|
|
@@ -855,29 +1243,49 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
855
1243
|
if (routes.length === 0) return [];
|
|
856
1244
|
const routePaths = routes.map((r) => r.path);
|
|
857
1245
|
const captured = manifest.captured ?? [];
|
|
858
|
-
const optedOut = /* @__PURE__ */ new Set();
|
|
859
|
-
for (const r of routes) {
|
|
860
|
-
const page = registry.get("page", r.page);
|
|
861
|
-
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
862
|
-
}
|
|
863
1246
|
const covered = /* @__PURE__ */ new Set();
|
|
864
1247
|
for (const c of captured) {
|
|
865
1248
|
const route = routeForCapture(c, routes, routePaths);
|
|
866
1249
|
if (route) covered.add(route);
|
|
867
1250
|
}
|
|
868
|
-
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
869
1251
|
const diagnostics = [];
|
|
870
1252
|
for (const r of routes) {
|
|
871
|
-
|
|
1253
|
+
const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
|
|
1254
|
+
ACK_ALL
|
|
1255
|
+
);
|
|
872
1256
|
const loc = registry.get("page", r.page)?.loc;
|
|
873
|
-
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") {
|
|
874
1282
|
diagnostics.push({
|
|
875
1283
|
code: "page-backlog",
|
|
876
1284
|
severity: "info",
|
|
877
1285
|
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
878
1286
|
file: loc?.file,
|
|
879
1287
|
line: loc?.line,
|
|
880
|
-
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}".`
|
|
881
1289
|
});
|
|
882
1290
|
continue;
|
|
883
1291
|
}
|
|
@@ -888,46 +1296,307 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
888
1296
|
file: loc?.file,
|
|
889
1297
|
line: loc?.line,
|
|
890
1298
|
entity: { kind: "route", id: r.path },
|
|
891
|
-
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).`
|
|
892
1300
|
});
|
|
893
1301
|
}
|
|
894
|
-
for (const b of
|
|
895
|
-
if (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
} else if (covered.has(b) || optedOut.has(b)) {
|
|
903
|
-
diagnostics.push({
|
|
904
|
-
code: "page-captured",
|
|
905
|
-
severity: "warning",
|
|
906
|
-
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
907
|
-
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
908
|
-
});
|
|
909
|
-
}
|
|
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
|
+
});
|
|
910
1310
|
}
|
|
911
1311
|
return diagnostics;
|
|
912
1312
|
}
|
|
913
|
-
function
|
|
914
|
-
const
|
|
1313
|
+
function computePageBacklog(registry, manifest) {
|
|
1314
|
+
const out2 = {};
|
|
915
1315
|
for (const d of checkPageCoverage(registry, manifest, {
|
|
916
|
-
|
|
1316
|
+
version: 2,
|
|
1317
|
+
acknowledge: {}
|
|
917
1318
|
})) {
|
|
918
|
-
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;
|
|
919
1537
|
}
|
|
920
|
-
|
|
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;
|
|
921
1571
|
}
|
|
922
1572
|
|
|
923
1573
|
// src/scanner/scan/state-coverage.ts
|
|
924
1574
|
var STATE_KINDS = ["page", "feature", "widget"];
|
|
925
|
-
function declaredStateEntities(registry) {
|
|
1575
|
+
function declaredStateEntities(registry, opts, ungated) {
|
|
926
1576
|
const out2 = [];
|
|
927
1577
|
for (const kind of STATE_KINDS) {
|
|
928
1578
|
for (const e of registry.list(kind)) {
|
|
929
1579
|
const states = e.meta?.states;
|
|
930
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
|
+
}
|
|
931
1600
|
out2.push({
|
|
932
1601
|
kind,
|
|
933
1602
|
id: e.id,
|
|
@@ -938,18 +1607,31 @@ function declaredStateEntities(registry) {
|
|
|
938
1607
|
}
|
|
939
1608
|
return out2;
|
|
940
1609
|
}
|
|
941
|
-
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) {
|
|
942
1620
|
if (cap.entity !== ent.id) return false;
|
|
943
|
-
|
|
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);
|
|
944
1624
|
}
|
|
945
|
-
|
|
1625
|
+
var EMPTY = /* @__PURE__ */ new Set();
|
|
1626
|
+
function checkStateCoverage(registry, manifest, opts = {}) {
|
|
946
1627
|
const diagnostics = [];
|
|
947
|
-
const declared = declaredStateEntities(registry);
|
|
948
1628
|
const captured = manifest.captured ?? [];
|
|
1629
|
+
const unreliable = unreliablePageKinds(registry, captured);
|
|
1630
|
+
const declared = declaredStateEntities(registry, opts, diagnostics);
|
|
949
1631
|
for (const ent of declared) {
|
|
950
1632
|
for (const state of ent.states) {
|
|
951
1633
|
const hit = captured.some(
|
|
952
|
-
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
1634
|
+
(c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
|
|
953
1635
|
);
|
|
954
1636
|
if (hit) continue;
|
|
955
1637
|
diagnostics.push({
|
|
@@ -964,7 +1646,9 @@ function checkStateCoverage(registry, manifest) {
|
|
|
964
1646
|
}
|
|
965
1647
|
}
|
|
966
1648
|
for (const cap of captured) {
|
|
967
|
-
const candidates = declared.filter(
|
|
1649
|
+
const candidates = declared.filter(
|
|
1650
|
+
(ent2) => captureMatchesEntity(cap, ent2, unreliable)
|
|
1651
|
+
);
|
|
968
1652
|
if (candidates.length === 0) continue;
|
|
969
1653
|
const declaredSomewhere = candidates.some(
|
|
970
1654
|
(ent2) => ent2.states.includes(cap.state)
|
|
@@ -1008,36 +1692,35 @@ function coreKindsByRoute(registry, manifest) {
|
|
|
1008
1692
|
}
|
|
1009
1693
|
return byRoute;
|
|
1010
1694
|
}
|
|
1011
|
-
function pageWaivers(registry, routePage) {
|
|
1012
|
-
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
1013
|
-
}
|
|
1014
|
-
function isOptedOut(registry, page) {
|
|
1015
|
-
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
1016
|
-
}
|
|
1017
1695
|
function checkStateMatrix(registry, manifest, baseline) {
|
|
1018
1696
|
const routes = registry.list("route");
|
|
1019
1697
|
if (routes.length === 0) return [];
|
|
1020
1698
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1021
1699
|
const captured = coreKindsByRoute(registry, manifest);
|
|
1022
|
-
const baselined = baseline?.missingKinds ?? {};
|
|
1023
1700
|
const diagnostics = [];
|
|
1024
1701
|
for (const [route, kinds] of captured) {
|
|
1025
1702
|
const page = pageOf.get(route);
|
|
1026
|
-
|
|
1027
|
-
const waivers = page ? pageWaivers(registry, page) : {};
|
|
1703
|
+
const ack = routeAcknowledge(registry, baseline, route, page);
|
|
1028
1704
|
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
1029
|
-
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
|
+
}
|
|
1030
1712
|
for (const kind of CORE_KINDS) {
|
|
1031
1713
|
if (kinds.has(kind)) continue;
|
|
1032
|
-
|
|
1033
|
-
if (
|
|
1714
|
+
const entry = ack.get(kind);
|
|
1715
|
+
if (entry?.type === "impossible") continue;
|
|
1716
|
+
if (entry?.type === "backlog") {
|
|
1034
1717
|
diagnostics.push({
|
|
1035
1718
|
code: "matrix-backlog",
|
|
1036
1719
|
severity: "info",
|
|
1037
|
-
message: `route "${route}" does not render core kind "${kind}" (acknowledged
|
|
1720
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
|
|
1038
1721
|
file: loc?.file,
|
|
1039
1722
|
line: loc?.line,
|
|
1040
|
-
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" }\`.`
|
|
1041
1724
|
});
|
|
1042
1725
|
continue;
|
|
1043
1726
|
}
|
|
@@ -1048,42 +1731,43 @@ function checkStateMatrix(registry, manifest, baseline) {
|
|
|
1048
1731
|
file: loc?.file,
|
|
1049
1732
|
line: loc?.line,
|
|
1050
1733
|
entity: { kind: "route", id: route },
|
|
1051
|
-
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).`
|
|
1052
1735
|
});
|
|
1053
1736
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (!page || isOptedOut(registry, page)) continue;
|
|
1058
|
-
const waivers = pageWaivers(registry, page);
|
|
1059
|
-
const loc = registry.get("page", page)?.loc;
|
|
1060
|
-
for (const kind of Object.keys(waivers)) {
|
|
1061
|
-
if (kinds.has(kind)) {
|
|
1062
|
-
diagnostics.push({
|
|
1063
|
-
code: "stale-waiver",
|
|
1064
|
-
severity: "warning",
|
|
1065
|
-
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
1066
|
-
file: loc?.file,
|
|
1067
|
-
line: loc?.line,
|
|
1068
|
-
entity: { kind: "page", id: page },
|
|
1069
|
-
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1737
|
+
for (const [scope, entry] of ack) {
|
|
1738
|
+
if (scope === ACK_ALL || !kinds.has(scope)) continue;
|
|
1739
|
+
diagnostics.push(staleDiagnostic(entry, route, page, loc));
|
|
1072
1740
|
}
|
|
1073
1741
|
}
|
|
1074
1742
|
return diagnostics;
|
|
1075
1743
|
}
|
|
1076
|
-
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) {
|
|
1077
1757
|
const routes = registry.list("route");
|
|
1078
1758
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1079
1759
|
const captured = coreKindsByRoute(registry, manifest);
|
|
1080
1760
|
const out2 = {};
|
|
1081
1761
|
for (const [route, kinds] of captured) {
|
|
1082
1762
|
const page = pageOf.get(route);
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
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;
|
|
1087
1771
|
}
|
|
1088
1772
|
return out2;
|
|
1089
1773
|
}
|
|
@@ -1107,6 +1791,23 @@ function audit(opts) {
|
|
|
1107
1791
|
for (const ef of opts.flowExtracted ?? []) {
|
|
1108
1792
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
1109
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
|
+
}
|
|
1110
1811
|
if (check && opts.generated !== void 0) {
|
|
1111
1812
|
const outRel = opts.outputPath ?? config.output;
|
|
1112
1813
|
const fresh = normalizeForCheck(opts.generated);
|
|
@@ -1220,8 +1921,8 @@ function audit(opts) {
|
|
|
1220
1921
|
if (typeof m.id !== "string") continue;
|
|
1221
1922
|
const filePath = ef.file.displayPath;
|
|
1222
1923
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
1223
|
-
if (
|
|
1224
|
-
const dir =
|
|
1924
|
+
if (path7.posix.basename(filePath) === wellKnownName) continue;
|
|
1925
|
+
const dir = path7.posix.dirname(filePath);
|
|
1225
1926
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
1226
1927
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
1227
1928
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -1435,25 +2136,41 @@ function audit(opts) {
|
|
|
1435
2136
|
}
|
|
1436
2137
|
}
|
|
1437
2138
|
if (lint && statesEnabled && opts.capturedStates) {
|
|
1438
|
-
|
|
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
|
+
);
|
|
1439
2148
|
}
|
|
1440
2149
|
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
1441
2150
|
diagnostics.push(
|
|
1442
|
-
...checkPageCoverage(registry, opts.capturedStates, opts.
|
|
2151
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
|
|
1443
2152
|
);
|
|
1444
2153
|
}
|
|
2154
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
2155
|
+
diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
|
|
2156
|
+
}
|
|
1445
2157
|
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
1446
2158
|
diagnostics.push(
|
|
1447
|
-
...checkStateMatrix(registry, opts.capturedStates,
|
|
1448
|
-
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
1449
|
-
})
|
|
2159
|
+
...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
|
|
1450
2160
|
);
|
|
1451
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
|
+
});
|
|
1452
2169
|
const summary = {
|
|
1453
|
-
errors:
|
|
1454
|
-
warnings:
|
|
2170
|
+
errors: governed.filter((d) => d.severity === "error").length,
|
|
2171
|
+
warnings: governed.filter((d) => d.severity === "warning").length
|
|
1455
2172
|
};
|
|
1456
|
-
return { diagnostics, summary };
|
|
2173
|
+
return { diagnostics: governed, summary };
|
|
1457
2174
|
}
|
|
1458
2175
|
function lineOfOffset(content, offset) {
|
|
1459
2176
|
let line = 1;
|
|
@@ -1781,6 +2498,30 @@ ${body}
|
|
|
1781
2498
|
`;
|
|
1782
2499
|
}
|
|
1783
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
|
+
}
|
|
1784
2525
|
function emitId(name, ids) {
|
|
1785
2526
|
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
1786
2527
|
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
@@ -1868,6 +2609,14 @@ function emit(opts) {
|
|
|
1868
2609
|
);
|
|
1869
2610
|
t.push(emitId("StateId", [...stateNames]));
|
|
1870
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
|
+
);
|
|
1871
2620
|
t.push("// ---- authoring-surface shape types ----");
|
|
1872
2621
|
t.push("export namespace Uidex {");
|
|
1873
2622
|
t.push(
|
|
@@ -1880,7 +2629,20 @@ function emit(opts) {
|
|
|
1880
2629
|
t.push(" acceptance?: readonly string[]");
|
|
1881
2630
|
t.push(" description?: string");
|
|
1882
2631
|
t.push(" }");
|
|
1883
|
-
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 }');
|
|
1884
2646
|
t.push(" export interface Page {");
|
|
1885
2647
|
t.push(" page: PageId | false");
|
|
1886
2648
|
t.push(" name?: string");
|
|
@@ -1888,8 +2650,7 @@ function emit(opts) {
|
|
|
1888
2650
|
t.push(" widgets?: readonly WidgetId[]");
|
|
1889
2651
|
t.push(" acceptance?: readonly string[]");
|
|
1890
2652
|
t.push(" states?: readonly (string | State)[]");
|
|
1891
|
-
t.push("
|
|
1892
|
-
t.push(" waivers?: Waivers");
|
|
2653
|
+
t.push(" acknowledge?: Acknowledge");
|
|
1893
2654
|
t.push(" description?: string");
|
|
1894
2655
|
t.push(" }");
|
|
1895
2656
|
t.push(" export interface Feature {");
|
|
@@ -1898,6 +2659,7 @@ function emit(opts) {
|
|
|
1898
2659
|
t.push(" features?: readonly FeatureId[]");
|
|
1899
2660
|
t.push(" acceptance?: readonly string[]");
|
|
1900
2661
|
t.push(" states?: readonly (string | State)[]");
|
|
2662
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
1901
2663
|
t.push(" description?: string");
|
|
1902
2664
|
t.push(" }");
|
|
1903
2665
|
t.push(" export interface Primitive {");
|
|
@@ -1910,6 +2672,7 @@ function emit(opts) {
|
|
|
1910
2672
|
t.push(" name?: string");
|
|
1911
2673
|
t.push(" acceptance?: readonly string[]");
|
|
1912
2674
|
t.push(" states?: readonly (string | State)[]");
|
|
2675
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
1913
2676
|
t.push(" description?: string");
|
|
1914
2677
|
t.push(" }");
|
|
1915
2678
|
t.push(" export interface Region {");
|
|
@@ -1973,10 +2736,10 @@ function emit(opts) {
|
|
|
1973
2736
|
}
|
|
1974
2737
|
|
|
1975
2738
|
// src/scanner/scan/ast.ts
|
|
1976
|
-
var
|
|
2739
|
+
var path8 = __toESM(require("path"), 1);
|
|
1977
2740
|
var import_oxc_parser = require("oxc-parser");
|
|
1978
2741
|
function langFor(sourcePath) {
|
|
1979
|
-
switch (
|
|
2742
|
+
switch (path8.extname(sourcePath)) {
|
|
1980
2743
|
case ".tsx":
|
|
1981
2744
|
return "tsx";
|
|
1982
2745
|
case ".ts":
|
|
@@ -2074,26 +2837,41 @@ var ALLOWED_FIELDS = {
|
|
|
2074
2837
|
"widgets",
|
|
2075
2838
|
"acceptance",
|
|
2076
2839
|
"states",
|
|
2840
|
+
"acknowledge",
|
|
2077
2841
|
"capture",
|
|
2078
2842
|
"waivers",
|
|
2079
2843
|
"description"
|
|
2080
2844
|
]),
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
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).
|
|
2084
2851
|
feature: /* @__PURE__ */ new Set([
|
|
2085
2852
|
"feature",
|
|
2086
2853
|
"name",
|
|
2087
|
-
"features",
|
|
2854
|
+
"features",
|
|
2855
|
+
"acceptance",
|
|
2856
|
+
"states",
|
|
2857
|
+
"acknowledge",
|
|
2858
|
+
"capture",
|
|
2859
|
+
"description"
|
|
2860
|
+
]),
|
|
2861
|
+
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
2862
|
+
widget: /* @__PURE__ */ new Set([
|
|
2863
|
+
"widget",
|
|
2864
|
+
"name",
|
|
2088
2865
|
"acceptance",
|
|
2089
2866
|
"states",
|
|
2867
|
+
"acknowledge",
|
|
2868
|
+
"capture",
|
|
2090
2869
|
"description"
|
|
2091
2870
|
]),
|
|
2092
|
-
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
2093
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
2094
2871
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
2095
2872
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
2096
2873
|
};
|
|
2874
|
+
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
2097
2875
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
2098
2876
|
"name",
|
|
2099
2877
|
"kind",
|
|
@@ -2344,7 +3122,8 @@ function objectLit(node, content, p2, pos, span) {
|
|
|
2344
3122
|
key,
|
|
2345
3123
|
value,
|
|
2346
3124
|
keyPos,
|
|
2347
|
-
span: removalSpan(content, prop.start, prop.end)
|
|
3125
|
+
span: removalSpan(content, prop.start, prop.end),
|
|
3126
|
+
propSpan: { start: prop.start, end: prop.end }
|
|
2348
3127
|
});
|
|
2349
3128
|
}
|
|
2350
3129
|
return { kind: "object", entries, pos, span };
|
|
@@ -2492,8 +3271,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
2492
3271
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
2493
3272
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
2494
3273
|
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
2495
|
-
const
|
|
2496
|
-
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;
|
|
2497
3278
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
2498
3279
|
const metadata = {
|
|
2499
3280
|
source: "ts-export",
|
|
@@ -2517,15 +3298,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
2517
3298
|
metadata.widgetSpans = widgetsField.spans;
|
|
2518
3299
|
}
|
|
2519
3300
|
if (states?.length) metadata.states = states;
|
|
2520
|
-
if (
|
|
2521
|
-
|
|
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
|
+
}
|
|
2522
3308
|
if (notFlow) metadata.notFlow = true;
|
|
2523
3309
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
2524
3310
|
metadata.idSpan = idValue.span;
|
|
2525
3311
|
}
|
|
2526
3312
|
const fieldSpans = {};
|
|
2527
|
-
|
|
3313
|
+
const fieldPropSpans = {};
|
|
3314
|
+
for (const entry of value.entries) {
|
|
3315
|
+
fieldSpans[entry.key] = entry.span;
|
|
3316
|
+
fieldPropSpans[entry.key] = entry.propSpan;
|
|
3317
|
+
}
|
|
2528
3318
|
metadata.fieldSpans = fieldSpans;
|
|
3319
|
+
metadata.fieldPropSpans = fieldPropSpans;
|
|
2529
3320
|
return metadata;
|
|
2530
3321
|
}
|
|
2531
3322
|
function readIdField(value, kind, fieldName) {
|
|
@@ -2555,6 +3346,80 @@ function readIdField(value, kind, fieldName) {
|
|
|
2555
3346
|
value.pos
|
|
2556
3347
|
);
|
|
2557
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
|
+
}
|
|
2558
3423
|
function readWaiversField(byKey) {
|
|
2559
3424
|
const entry = byKey.get("waivers");
|
|
2560
3425
|
if (!entry) return void 0;
|
|
@@ -3260,19 +4125,32 @@ function parseGitHubRef(ref) {
|
|
|
3260
4125
|
}
|
|
3261
4126
|
|
|
3262
4127
|
// src/scanner/scan/resolve.ts
|
|
3263
|
-
var
|
|
4128
|
+
var path9 = __toESM(require("path"), 1);
|
|
3264
4129
|
|
|
3265
4130
|
// src/scanner/scan/routes.ts
|
|
3266
4131
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
3267
4132
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
3268
4133
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
3269
|
-
function detectRoutes(files) {
|
|
4134
|
+
function detectRoutes(files, options = {}) {
|
|
3270
4135
|
const out2 = [];
|
|
3271
4136
|
const seen = /* @__PURE__ */ new Set();
|
|
4137
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
4138
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
3272
4139
|
for (const f of files) {
|
|
3273
4140
|
const rel = f.displayPath;
|
|
3274
4141
|
const parts = rel.split("/");
|
|
3275
4142
|
const base = parts[parts.length - 1];
|
|
4143
|
+
if (routesDirParts) {
|
|
4144
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
4145
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
4146
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
4147
|
+
const routePath = routesStylePath(
|
|
4148
|
+
parts.slice(end, parts.length - 1),
|
|
4149
|
+
base
|
|
4150
|
+
);
|
|
4151
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
3276
4154
|
const appIdx = parts.indexOf("app");
|
|
3277
4155
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
3278
4156
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -3299,15 +4177,11 @@ function detectRoutes(files) {
|
|
|
3299
4177
|
}
|
|
3300
4178
|
const routesIdx = parts.indexOf("routes");
|
|
3301
4179
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
3302
|
-
const
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
base.replace(/\.[^.]+$/, "")
|
|
3308
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
3309
|
-
const routePath = formatNextAppPath(normalized);
|
|
3310
|
-
push(out2, seen, routePath, f.displayPath);
|
|
4180
|
+
const routePath = routesStylePath(
|
|
4181
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
4182
|
+
base
|
|
4183
|
+
);
|
|
4184
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
3311
4185
|
continue;
|
|
3312
4186
|
}
|
|
3313
4187
|
}
|
|
@@ -3323,139 +4197,66 @@ function formatNextAppPath(segments) {
|
|
|
3323
4197
|
if (kept.length === 0) return "/";
|
|
3324
4198
|
return "/" + kept.join("/");
|
|
3325
4199
|
}
|
|
3326
|
-
function
|
|
3327
|
-
if (
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
|
|
3335
|
-
var BASE_EXCLUDES = [
|
|
3336
|
-
"**/node_modules/**",
|
|
3337
|
-
"**/dist/**",
|
|
3338
|
-
"**/build/**",
|
|
3339
|
-
"**/.next/**",
|
|
3340
|
-
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
3341
|
-
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
3342
|
-
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
3343
|
-
"**/*.gen.ts",
|
|
3344
|
-
"**/*.gen.data.ts",
|
|
3345
|
-
"**/*.gen.locs.ts"
|
|
3346
|
-
];
|
|
3347
|
-
var TEST_EXCLUDES = [
|
|
3348
|
-
"**/*.test.ts",
|
|
3349
|
-
"**/*.test.tsx",
|
|
3350
|
-
"**/*.spec.ts",
|
|
3351
|
-
"**/*.spec.tsx"
|
|
3352
|
-
];
|
|
3353
|
-
var DEFAULT_EXCLUDES = [...BASE_EXCLUDES, ...TEST_EXCLUDES];
|
|
3354
|
-
function globToRegExp(glob) {
|
|
3355
|
-
let i = 0;
|
|
3356
|
-
let out2 = "";
|
|
3357
|
-
while (i < glob.length) {
|
|
3358
|
-
const c = glob[i];
|
|
3359
|
-
if (c === "*" && glob[i + 1] === "*") {
|
|
3360
|
-
if (glob[i + 2] === "/") {
|
|
3361
|
-
out2 += "(?:.*/)?";
|
|
3362
|
-
i += 3;
|
|
3363
|
-
continue;
|
|
3364
|
-
}
|
|
3365
|
-
out2 += ".*";
|
|
3366
|
-
i += 2;
|
|
3367
|
-
continue;
|
|
3368
|
-
}
|
|
3369
|
-
if (c === "*") {
|
|
3370
|
-
out2 += "[^/]*";
|
|
3371
|
-
i++;
|
|
3372
|
-
continue;
|
|
3373
|
-
}
|
|
3374
|
-
if (c === "?") {
|
|
3375
|
-
out2 += "[^/]";
|
|
3376
|
-
i++;
|
|
3377
|
-
continue;
|
|
3378
|
-
}
|
|
3379
|
-
if (c === "{") {
|
|
3380
|
-
const end = glob.indexOf("}", i);
|
|
3381
|
-
if (end === -1) {
|
|
3382
|
-
out2 += "\\{";
|
|
3383
|
-
i++;
|
|
3384
|
-
continue;
|
|
4200
|
+
function dirEndIndex(parts, dirParts) {
|
|
4201
|
+
if (dirParts.length === 0) return 0;
|
|
4202
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
4203
|
+
let match = true;
|
|
4204
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
4205
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
4206
|
+
match = false;
|
|
4207
|
+
break;
|
|
3385
4208
|
}
|
|
3386
|
-
const parts = glob.slice(i + 1, end).split(",").map((p2) => p2.replace(/[.+^$()|\\]/g, "\\$&"));
|
|
3387
|
-
out2 += `(?:${parts.join("|")})`;
|
|
3388
|
-
i = end + 1;
|
|
3389
|
-
continue;
|
|
3390
|
-
}
|
|
3391
|
-
if (/[.+^$()|\\]/.test(c)) {
|
|
3392
|
-
out2 += "\\" + c;
|
|
3393
|
-
i++;
|
|
3394
|
-
continue;
|
|
3395
4209
|
}
|
|
3396
|
-
|
|
3397
|
-
i++;
|
|
4210
|
+
if (match) return i + dirParts.length;
|
|
3398
4211
|
}
|
|
3399
|
-
return
|
|
3400
|
-
}
|
|
3401
|
-
function toPosix(p2) {
|
|
3402
|
-
return p2.split(path7.sep).join("/");
|
|
4212
|
+
return -1;
|
|
3403
4213
|
}
|
|
3404
|
-
function
|
|
3405
|
-
|
|
4214
|
+
function routesStylePath(dirSegs, base) {
|
|
4215
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
4216
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
4217
|
+
if (leaf.startsWith("_")) return null;
|
|
4218
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
4219
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
4220
|
+
if (rawSegs[0] === "api") return null;
|
|
4221
|
+
return formatTanStackPath(rawSegs);
|
|
3406
4222
|
}
|
|
3407
|
-
function
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
for (const
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
const absRoot = path7.resolve(cwd, source.rootDir);
|
|
3419
|
-
for (const filePath of walkDir(absRoot, absRoot)) {
|
|
3420
|
-
const rel = toPosix(path7.relative(absRoot, filePath));
|
|
3421
|
-
if (matchesAny(rel, excludes)) continue;
|
|
3422
|
-
if (!matchesAny(rel, includes)) continue;
|
|
3423
|
-
let content;
|
|
3424
|
-
try {
|
|
3425
|
-
content = fs5.readFileSync(filePath, "utf8");
|
|
3426
|
-
} catch {
|
|
3427
|
-
continue;
|
|
3428
|
-
}
|
|
3429
|
-
const relFromCwd = toPosix(path7.relative(cwd, filePath));
|
|
3430
|
-
const displayPath = source.prefix ? `${source.prefix.replace(/\/$/, "")}/${rel}` : relFromCwd;
|
|
3431
|
-
out2.push({
|
|
3432
|
-
sourcePath: filePath,
|
|
3433
|
-
relativePath: rel,
|
|
3434
|
-
displayPath,
|
|
3435
|
-
content
|
|
3436
|
-
});
|
|
4223
|
+
function splitFlatSegments(name) {
|
|
4224
|
+
const segs = [];
|
|
4225
|
+
let cur = "";
|
|
4226
|
+
let depth = 0;
|
|
4227
|
+
for (const ch of name) {
|
|
4228
|
+
if (ch === "[") depth++;
|
|
4229
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
4230
|
+
if (ch === "." && depth === 0) {
|
|
4231
|
+
segs.push(cur);
|
|
4232
|
+
cur = "";
|
|
4233
|
+
continue;
|
|
3437
4234
|
}
|
|
4235
|
+
cur += ch;
|
|
3438
4236
|
}
|
|
3439
|
-
|
|
4237
|
+
segs.push(cur);
|
|
4238
|
+
return segs;
|
|
3440
4239
|
}
|
|
3441
|
-
function
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
const full = path7.join(dir, entry.name);
|
|
3450
|
-
if (entry.isDirectory()) {
|
|
3451
|
-
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "build" || entry.name === ".next") {
|
|
3452
|
-
continue;
|
|
3453
|
-
}
|
|
3454
|
-
yield* walkDir(root, full);
|
|
3455
|
-
} else if (entry.isFile()) {
|
|
3456
|
-
yield full;
|
|
3457
|
-
}
|
|
4240
|
+
function formatTanStackPath(segments) {
|
|
4241
|
+
const kept = [];
|
|
4242
|
+
for (const seg of segments) {
|
|
4243
|
+
if (!seg) continue;
|
|
4244
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
4245
|
+
if (seg.startsWith("_")) continue;
|
|
4246
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
4247
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
3458
4248
|
}
|
|
4249
|
+
if (kept.length === 0) return "/";
|
|
4250
|
+
return "/" + kept.join("/");
|
|
4251
|
+
}
|
|
4252
|
+
function normalizeTanStackSegment(seg) {
|
|
4253
|
+
if (seg === "$") return "[...splat]";
|
|
4254
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
4255
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
4256
|
+
}
|
|
4257
|
+
function pathToId(routePath) {
|
|
4258
|
+
if (routePath === "/") return "root";
|
|
4259
|
+
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
3459
4260
|
}
|
|
3460
4261
|
|
|
3461
4262
|
// src/scanner/scan/resolve.ts
|
|
@@ -3478,7 +4279,7 @@ function kebab(str) {
|
|
|
3478
4279
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase();
|
|
3479
4280
|
}
|
|
3480
4281
|
function baseName(file) {
|
|
3481
|
-
const b =
|
|
4282
|
+
const b = path9.posix.basename(file);
|
|
3482
4283
|
return b.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "");
|
|
3483
4284
|
}
|
|
3484
4285
|
function fileMatchesAny(displayPath, patterns) {
|
|
@@ -3499,13 +4300,12 @@ function buildMetaFromExport(exp) {
|
|
|
3499
4300
|
...s.description ? { description: s.description } : {}
|
|
3500
4301
|
}));
|
|
3501
4302
|
}
|
|
3502
|
-
if (exp.
|
|
3503
|
-
|
|
3504
|
-
meta.waivers = exp.waivers;
|
|
4303
|
+
if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
|
|
4304
|
+
meta.acknowledge = exp.acknowledge;
|
|
3505
4305
|
}
|
|
3506
4306
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
3507
4307
|
}
|
|
3508
|
-
function
|
|
4308
|
+
function resolve4(ctx) {
|
|
3509
4309
|
const conventions = resolveConventions(ctx.config.conventions);
|
|
3510
4310
|
const registry = createRegistry();
|
|
3511
4311
|
const diagnostics = [];
|
|
@@ -3548,12 +4348,15 @@ function resolve3(ctx) {
|
|
|
3548
4348
|
function metaWithComposes(kind, id, base) {
|
|
3549
4349
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
3550
4350
|
if (!composes || composes.length === 0) return base;
|
|
3551
|
-
return { ...base
|
|
4351
|
+
return { ...base, composes };
|
|
3552
4352
|
}
|
|
3553
|
-
const routes = conventions.pages ===
|
|
4353
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
4354
|
+
ctx.extracted.map((e) => e.file),
|
|
4355
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
4356
|
+
);
|
|
3554
4357
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
3555
4358
|
for (const route of routes) {
|
|
3556
|
-
const routeDir =
|
|
4359
|
+
const routeDir = path9.posix.dirname(route.file);
|
|
3557
4360
|
const wellKnownPath = `${routeDir}/${WELL_KNOWN_FILES.page}`;
|
|
3558
4361
|
const wellKnownExp = exportFor(wellKnownPath, "page");
|
|
3559
4362
|
const routeExp = exportFor(route.file, "page");
|
|
@@ -3607,7 +4410,7 @@ function resolve3(ctx) {
|
|
|
3607
4410
|
const dir = extractFeatureDir(ef.file.displayPath, featureGlob);
|
|
3608
4411
|
if (!dir) continue;
|
|
3609
4412
|
conventionalFeatureDirs.add(dir);
|
|
3610
|
-
const isWellKnown =
|
|
4413
|
+
const isWellKnown = path9.posix.basename(ef.file.displayPath) === WELL_KNOWN_FILES.feature;
|
|
3611
4414
|
if (isWellKnown) wellKnownFeatureFileByDir.set(dir, ef.file.displayPath);
|
|
3612
4415
|
const exp = exportFor(ef.file.displayPath, "feature");
|
|
3613
4416
|
if (exp) {
|
|
@@ -3644,7 +4447,7 @@ function resolve3(ctx) {
|
|
|
3644
4447
|
} else if (allExports.length > 0) {
|
|
3645
4448
|
exp = allExports[0].exp;
|
|
3646
4449
|
}
|
|
3647
|
-
const id = exp && typeof exp.id === "string" ? exp.id :
|
|
4450
|
+
const id = exp && typeof exp.id === "string" ? exp.id : path9.posix.basename(dir);
|
|
3648
4451
|
const meta = exp ? buildMetaFromExport(exp) : void 0;
|
|
3649
4452
|
const feature = {
|
|
3650
4453
|
kind: "feature",
|
|
@@ -3843,6 +4646,45 @@ function resolve3(ctx) {
|
|
|
3843
4646
|
};
|
|
3844
4647
|
registry.add(element);
|
|
3845
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
|
+
};
|
|
3846
4688
|
if (ctx.flowFiles && conventions.flows) {
|
|
3847
4689
|
for (const ff of ctx.flowFiles) {
|
|
3848
4690
|
const notFlowExport = (ff.metadata ?? []).find(
|
|
@@ -3852,7 +4694,10 @@ function resolve3(ctx) {
|
|
|
3852
4694
|
const flowExport = (ff.metadata ?? []).find(
|
|
3853
4695
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
3854
4696
|
);
|
|
3855
|
-
const derived = flowsFromFacts(ff)
|
|
4697
|
+
const derived = flowsFromFacts(ff).map((f) => ({
|
|
4698
|
+
...f,
|
|
4699
|
+
touches: expandTouches(f.touches)
|
|
4700
|
+
}));
|
|
3856
4701
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
3857
4702
|
const base = derived[0];
|
|
3858
4703
|
const flow = {
|
|
@@ -3921,45 +4766,38 @@ function dedupe(arr) {
|
|
|
3921
4766
|
}
|
|
3922
4767
|
|
|
3923
4768
|
// src/scanner/scan/pipeline.ts
|
|
3924
|
-
var
|
|
4769
|
+
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
3925
4770
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3926
|
-
function
|
|
4771
|
+
function loadCoverageBaseline(configDir, config) {
|
|
3927
4772
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3928
|
-
const abs =
|
|
4773
|
+
const abs = path10.resolve(configDir, rel);
|
|
3929
4774
|
let raw;
|
|
3930
4775
|
try {
|
|
3931
|
-
raw =
|
|
4776
|
+
raw = fs7.readFileSync(abs, "utf8");
|
|
3932
4777
|
} catch {
|
|
3933
|
-
return
|
|
4778
|
+
return {};
|
|
3934
4779
|
}
|
|
4780
|
+
let parsed;
|
|
3935
4781
|
try {
|
|
3936
|
-
|
|
3937
|
-
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3938
|
-
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3939
|
-
(p2) => typeof p2 === "string"
|
|
3940
|
-
);
|
|
3941
|
-
let missingKinds;
|
|
3942
|
-
const mkRaw = parsed.missingKinds;
|
|
3943
|
-
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3944
|
-
const mk = {};
|
|
3945
|
-
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3946
|
-
if (Array.isArray(kinds)) {
|
|
3947
|
-
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3948
|
-
}
|
|
3949
|
-
}
|
|
3950
|
-
missingKinds = mk;
|
|
3951
|
-
}
|
|
3952
|
-
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
4782
|
+
parsed = JSON.parse(raw);
|
|
3953
4783
|
} catch {
|
|
3954
|
-
return
|
|
4784
|
+
return {};
|
|
3955
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 };
|
|
3956
4794
|
}
|
|
3957
4795
|
function loadCapturedStates(configDir, config) {
|
|
3958
|
-
const rel = config.statesManifest ??
|
|
3959
|
-
const abs =
|
|
4796
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4797
|
+
const abs = path10.resolve(configDir, rel);
|
|
3960
4798
|
let raw;
|
|
3961
4799
|
try {
|
|
3962
|
-
raw =
|
|
4800
|
+
raw = fs7.readFileSync(abs, "utf8");
|
|
3963
4801
|
} catch {
|
|
3964
4802
|
return void 0;
|
|
3965
4803
|
}
|
|
@@ -3978,7 +4816,7 @@ function loadCapturedStates(configDir, config) {
|
|
|
3978
4816
|
if (typeof c.route === "string") rec.route = c.route;
|
|
3979
4817
|
captured.push(rec);
|
|
3980
4818
|
}
|
|
3981
|
-
return { captured };
|
|
4819
|
+
return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
|
|
3982
4820
|
} catch {
|
|
3983
4821
|
return void 0;
|
|
3984
4822
|
}
|
|
@@ -4003,7 +4841,7 @@ function runOne(dc, opts) {
|
|
|
4003
4841
|
{ cwd: configDir, includeTests: true }
|
|
4004
4842
|
) : [];
|
|
4005
4843
|
const extractedFlows = extract(flowFiles);
|
|
4006
|
-
const resolved =
|
|
4844
|
+
const resolved = resolve4({
|
|
4007
4845
|
config,
|
|
4008
4846
|
extracted,
|
|
4009
4847
|
flowFiles: extractedFlows
|
|
@@ -4014,11 +4852,11 @@ function runOne(dc, opts) {
|
|
|
4014
4852
|
gitContext
|
|
4015
4853
|
});
|
|
4016
4854
|
const typesRel = config.output;
|
|
4017
|
-
const typesPath =
|
|
4855
|
+
const typesPath = path10.resolve(configDir, typesRel);
|
|
4018
4856
|
const dataRel = dataOutputPath(config.output);
|
|
4019
|
-
const dataPath =
|
|
4857
|
+
const dataPath = path10.resolve(configDir, dataRel);
|
|
4020
4858
|
const locsRel = locsOutputPath(config.output);
|
|
4021
|
-
const locsPath =
|
|
4859
|
+
const locsPath = path10.resolve(configDir, locsRel);
|
|
4022
4860
|
let typesOnDisk = null;
|
|
4023
4861
|
let dataOnDisk = null;
|
|
4024
4862
|
let locsOnDisk = null;
|
|
@@ -4031,9 +4869,11 @@ function runOne(dc, opts) {
|
|
|
4031
4869
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
4032
4870
|
);
|
|
4033
4871
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
4034
|
-
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 });
|
|
4035
4875
|
let auditResult;
|
|
4036
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4876
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
|
|
4037
4877
|
auditResult = audit({
|
|
4038
4878
|
registry: resolved.registry,
|
|
4039
4879
|
extracted,
|
|
@@ -4056,7 +4896,9 @@ function runOne(dc, opts) {
|
|
|
4056
4896
|
locsOnDisk,
|
|
4057
4897
|
locsOutputPath: locsRel,
|
|
4058
4898
|
capturedStates,
|
|
4059
|
-
|
|
4899
|
+
coverageBaseline,
|
|
4900
|
+
baselineDiagnostic: loadedBaseline.legacy,
|
|
4901
|
+
workspace
|
|
4060
4902
|
});
|
|
4061
4903
|
}
|
|
4062
4904
|
return {
|
|
@@ -4077,15 +4919,15 @@ function runOne(dc, opts) {
|
|
|
4077
4919
|
}
|
|
4078
4920
|
function readOrNull(p2) {
|
|
4079
4921
|
try {
|
|
4080
|
-
return
|
|
4922
|
+
return fs7.readFileSync(p2, "utf8");
|
|
4081
4923
|
} catch {
|
|
4082
4924
|
return null;
|
|
4083
4925
|
}
|
|
4084
4926
|
}
|
|
4085
4927
|
function writeArtifact(artifact) {
|
|
4086
4928
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
4087
|
-
|
|
4088
|
-
|
|
4929
|
+
fs7.mkdirSync(path10.dirname(artifact.outputPath), { recursive: true });
|
|
4930
|
+
fs7.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
4089
4931
|
return true;
|
|
4090
4932
|
}
|
|
4091
4933
|
function writeScanResult(result) {
|
|
@@ -4094,9 +4936,9 @@ function writeScanResult(result) {
|
|
|
4094
4936
|
const wroteLocs = writeArtifact(result.locs);
|
|
4095
4937
|
return wroteTypes || wroteData || wroteLocs;
|
|
4096
4938
|
}
|
|
4097
|
-
function
|
|
4939
|
+
function coverageBaselinePath(result) {
|
|
4098
4940
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4099
|
-
return
|
|
4941
|
+
return path10.resolve(result.configDir, rel);
|
|
4100
4942
|
}
|
|
4101
4943
|
|
|
4102
4944
|
// src/scanner/scan/rename.ts
|
|
@@ -4255,8 +5097,8 @@ function renameEntity(opts) {
|
|
|
4255
5097
|
}
|
|
4256
5098
|
|
|
4257
5099
|
// src/scanner/scan/scaffold.ts
|
|
4258
|
-
var
|
|
4259
|
-
var
|
|
5100
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
5101
|
+
var path11 = __toESM(require("path"), 1);
|
|
4260
5102
|
function scaffoldSpec(opts) {
|
|
4261
5103
|
const {
|
|
4262
5104
|
registry,
|
|
@@ -4272,8 +5114,8 @@ function scaffoldSpec(opts) {
|
|
|
4272
5114
|
}
|
|
4273
5115
|
const criteria = entity.meta?.acceptance ?? [];
|
|
4274
5116
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
4275
|
-
const outputPath =
|
|
4276
|
-
if (
|
|
5117
|
+
const outputPath = path11.resolve(outDir, filename);
|
|
5118
|
+
if (fs8.existsSync(outputPath) && !force) {
|
|
4277
5119
|
return {
|
|
4278
5120
|
outputPath,
|
|
4279
5121
|
written: false,
|
|
@@ -4282,8 +5124,8 @@ function scaffoldSpec(opts) {
|
|
|
4282
5124
|
};
|
|
4283
5125
|
}
|
|
4284
5126
|
const content = renderSpec({ id, criteria, fixtureImport });
|
|
4285
|
-
|
|
4286
|
-
|
|
5127
|
+
fs8.mkdirSync(path11.dirname(outputPath), { recursive: true });
|
|
5128
|
+
fs8.writeFileSync(outputPath, content, "utf8");
|
|
4287
5129
|
return { outputPath, written: true, skipped: false };
|
|
4288
5130
|
}
|
|
4289
5131
|
function capitalize(s) {
|
|
@@ -4291,25 +5133,27 @@ function capitalize(s) {
|
|
|
4291
5133
|
}
|
|
4292
5134
|
function renderSpec(args) {
|
|
4293
5135
|
const lines = [];
|
|
4294
|
-
lines.push(
|
|
4295
|
-
`import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
|
|
4296
|
-
);
|
|
5136
|
+
lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
|
|
4297
5137
|
lines.push("");
|
|
4298
5138
|
lines.push(
|
|
4299
5139
|
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
4300
5140
|
);
|
|
4301
|
-
|
|
4302
|
-
lines.push(` test(
|
|
4303
|
-
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`);
|
|
4304
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
|
+
]);
|
|
4305
5152
|
} else {
|
|
4306
5153
|
for (const criterion of args.criteria) {
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
lines.push(` expect(true).toBe(true)`);
|
|
4311
|
-
lines.push(` })`);
|
|
4312
|
-
lines.push("");
|
|
5154
|
+
stub(criterion, [
|
|
5155
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test."
|
|
5156
|
+
]);
|
|
4313
5157
|
}
|
|
4314
5158
|
}
|
|
4315
5159
|
lines.push("})");
|
|
@@ -4317,6 +5161,37 @@ function renderSpec(args) {
|
|
|
4317
5161
|
return lines.join("\n");
|
|
4318
5162
|
}
|
|
4319
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
|
+
|
|
4320
5195
|
// src/scanner/cli/parse-args.ts
|
|
4321
5196
|
function parseArgs(args) {
|
|
4322
5197
|
const positional = [];
|
|
@@ -4364,6 +5239,8 @@ async function run(opts) {
|
|
|
4364
5239
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
4365
5240
|
case "rename":
|
|
4366
5241
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
5242
|
+
case "states":
|
|
5243
|
+
return runStates(cwd, positional.slice(1), writer);
|
|
4367
5244
|
case "ai": {
|
|
4368
5245
|
const result = await runAiCommand({
|
|
4369
5246
|
cwd,
|
|
@@ -4392,22 +5269,23 @@ function helpText2() {
|
|
|
4392
5269
|
" scan [flags] Run the scanner pipeline",
|
|
4393
5270
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
4394
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",
|
|
4395
5273
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
4396
5274
|
"",
|
|
4397
5275
|
"Flags:",
|
|
4398
5276
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
4399
5277
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
4400
5278
|
" --audit Equivalent to --check --lint (read-only)",
|
|
4401
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4402
|
-
" --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",
|
|
4403
5281
|
" --json Emit JSON diagnostics on stdout",
|
|
4404
5282
|
" --force (scaffold) overwrite existing spec",
|
|
4405
5283
|
""
|
|
4406
5284
|
].join("\n");
|
|
4407
5285
|
}
|
|
4408
5286
|
function runInit(cwd, w) {
|
|
4409
|
-
const configPath =
|
|
4410
|
-
if (
|
|
5287
|
+
const configPath = path12.join(cwd, CONFIG_FILENAME);
|
|
5288
|
+
if (fs9.existsSync(configPath)) {
|
|
4411
5289
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
4412
5290
|
return w.result(1);
|
|
4413
5291
|
}
|
|
@@ -4416,16 +5294,16 @@ function runInit(cwd, w) {
|
|
|
4416
5294
|
sources: [{ rootDir: "src" }],
|
|
4417
5295
|
output: "src/uidex.gen.ts"
|
|
4418
5296
|
};
|
|
4419
|
-
|
|
5297
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
4420
5298
|
w.out(`Created ${configPath}`);
|
|
4421
|
-
const gitignorePath =
|
|
5299
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
4422
5300
|
const entry = "*.gen.*";
|
|
4423
|
-
if (
|
|
4424
|
-
const existing =
|
|
5301
|
+
if (fs9.existsSync(gitignorePath)) {
|
|
5302
|
+
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
4425
5303
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
4426
5304
|
if (!hasEntry) {
|
|
4427
5305
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4428
|
-
|
|
5306
|
+
fs9.appendFileSync(
|
|
4429
5307
|
gitignorePath,
|
|
4430
5308
|
`${needsNewline ? "\n" : ""}${entry}
|
|
4431
5309
|
`,
|
|
@@ -4434,7 +5312,7 @@ function runInit(cwd, w) {
|
|
|
4434
5312
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
4435
5313
|
}
|
|
4436
5314
|
} else {
|
|
4437
|
-
|
|
5315
|
+
fs9.writeFileSync(gitignorePath, `${entry}
|
|
4438
5316
|
`, "utf8");
|
|
4439
5317
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
4440
5318
|
}
|
|
@@ -4473,20 +5351,25 @@ function runScanCommand(cwd, flags, w) {
|
|
|
4473
5351
|
if (updateBaseline) {
|
|
4474
5352
|
for (const r of results) {
|
|
4475
5353
|
const manifest = r.capturedStates ?? { captured: [] };
|
|
4476
|
-
const
|
|
4477
|
-
const
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
5354
|
+
const acknowledge = {};
|
|
5355
|
+
const merge = (from) => {
|
|
5356
|
+
for (const [route, scopes] of Object.entries(from)) {
|
|
5357
|
+
acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
|
|
5358
|
+
}
|
|
4481
5359
|
};
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
const
|
|
4485
|
-
|
|
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,
|
|
4486
5369
|
0
|
|
4487
5370
|
);
|
|
4488
5371
|
w.out(
|
|
4489
|
-
`Wrote ${p2} \u2014 ${
|
|
5372
|
+
`Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
|
|
4490
5373
|
);
|
|
4491
5374
|
}
|
|
4492
5375
|
return w.result(0);
|
|
@@ -4559,7 +5442,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4559
5442
|
for (const r of results) {
|
|
4560
5443
|
const entity = r.registry.get(scaffoldKind, id);
|
|
4561
5444
|
if (!entity) continue;
|
|
4562
|
-
const outDir =
|
|
5445
|
+
const outDir = path12.resolve(r.configDir, "e2e");
|
|
4563
5446
|
const result = scaffoldSpec({
|
|
4564
5447
|
registry: r.registry,
|
|
4565
5448
|
kind: scaffoldKind,
|
|
@@ -4580,6 +5463,53 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4580
5463
|
return w.result(1);
|
|
4581
5464
|
}
|
|
4582
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
|
+
}
|
|
4583
5513
|
function runRename(cwd, args, flags, w) {
|
|
4584
5514
|
const [kind, oldId, newId] = args;
|
|
4585
5515
|
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|