uidex 0.10.0 → 0.11.1
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 +3561 -3201
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +36 -3
- package/dist/headless/index.d.ts +36 -3
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +4 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +23 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +15 -1
- package/dist/playwright/index.d.ts +15 -1
- package/dist/playwright/index.js +22 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/reporter.cjs +19 -0
- package/dist/playwright/reporter.cjs.map +1 -1
- package/dist/playwright/reporter.js +19 -0
- package/dist/playwright/reporter.js.map +1 -1
- package/dist/react/index.cjs +4 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +36 -3
- package/dist/react/index.d.ts +36 -3
- package/dist/react/index.js +4 -1
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +666 -312
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +131 -63
- package/dist/scan/index.d.ts +131 -63
- package/dist/scan/index.js +666 -308
- package/dist/scan/index.js.map +1 -1
- package/package.json +3 -3
package/dist/scan/index.js
CHANGED
|
@@ -27,6 +27,7 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
27
27
|
"exclude",
|
|
28
28
|
"output",
|
|
29
29
|
"flows",
|
|
30
|
+
"resolve",
|
|
30
31
|
"audit",
|
|
31
32
|
"conventions",
|
|
32
33
|
"statesManifest",
|
|
@@ -45,24 +46,32 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
|
45
46
|
"scopeLeak",
|
|
46
47
|
"coverage",
|
|
47
48
|
"acceptance",
|
|
49
|
+
"surfaces",
|
|
50
|
+
"requireAcceptance",
|
|
48
51
|
"states",
|
|
49
52
|
"pageCoverage",
|
|
50
53
|
"stateMatrix",
|
|
51
54
|
"severity"
|
|
52
55
|
]);
|
|
53
56
|
var DIAGNOSTIC_CODES = [
|
|
57
|
+
"acceptance-ambiguous",
|
|
58
|
+
"acceptance-duplicate-id",
|
|
59
|
+
"acceptance-orphaned",
|
|
60
|
+
"acceptance-outdated",
|
|
54
61
|
"acceptance-uncovered",
|
|
55
62
|
"acknowledged-elsewhere",
|
|
56
63
|
"competing-uidex-export",
|
|
57
64
|
"component-state-uncaptured",
|
|
58
65
|
"core-kind",
|
|
66
|
+
"covers-missing-rev",
|
|
59
67
|
"duplicate-id",
|
|
60
68
|
"dynamic-attr",
|
|
61
69
|
"dynamic-flow-reference",
|
|
70
|
+
"feature-missing-acceptance",
|
|
71
|
+
"feature-surface-uncaptured",
|
|
72
|
+
"feature-surface-unreferenced",
|
|
62
73
|
"gen-missing",
|
|
63
74
|
"gen-stale",
|
|
64
|
-
"legacy-acknowledge-field",
|
|
65
|
-
"legacy-coverage-baseline",
|
|
66
75
|
"matrix-backlog",
|
|
67
76
|
"missing-element-annotation",
|
|
68
77
|
"missing-state-capture",
|
|
@@ -97,14 +106,14 @@ var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"
|
|
|
97
106
|
function fail(msg) {
|
|
98
107
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
99
108
|
}
|
|
100
|
-
function assertObject(value,
|
|
109
|
+
function assertObject(value, path14) {
|
|
101
110
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
102
|
-
fail(`${
|
|
111
|
+
fail(`${path14} must be an object`);
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
|
-
function assertStringArray(value,
|
|
114
|
+
function assertStringArray(value, path14) {
|
|
106
115
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
107
|
-
fail(`${
|
|
116
|
+
fail(`${path14} must be a string[]`);
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
function validateConfig(raw) {
|
|
@@ -161,6 +170,23 @@ function validateConfig(raw) {
|
|
|
161
170
|
if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
|
|
162
171
|
fail(`"workspace" must be a path string or false`);
|
|
163
172
|
}
|
|
173
|
+
if (raw.resolve !== void 0) {
|
|
174
|
+
assertObject(raw.resolve, "resolve");
|
|
175
|
+
for (const key of Object.keys(raw.resolve)) {
|
|
176
|
+
if (key !== "aliases") fail(`unknown key "${key}" in resolve`);
|
|
177
|
+
}
|
|
178
|
+
const aliases = raw.resolve.aliases;
|
|
179
|
+
if (aliases === void 0) {
|
|
180
|
+
fail(`resolve.aliases is required when "resolve" is present`);
|
|
181
|
+
}
|
|
182
|
+
assertObject(aliases, "resolve.aliases");
|
|
183
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
184
|
+
if (alias.length === 0) fail(`resolve.aliases keys must be non-empty`);
|
|
185
|
+
if (typeof target !== "string") {
|
|
186
|
+
fail(`resolve.aliases["${alias}"] must be a string`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
164
190
|
if (raw.audit !== void 0) {
|
|
165
191
|
assertObject(raw.audit, "audit");
|
|
166
192
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -221,6 +247,7 @@ function validateConfig(raw) {
|
|
|
221
247
|
exclude: raw.exclude,
|
|
222
248
|
output: raw.output,
|
|
223
249
|
flows: raw.flows,
|
|
250
|
+
resolve: raw.resolve,
|
|
224
251
|
audit: raw.audit,
|
|
225
252
|
conventions: raw.conventions,
|
|
226
253
|
statesManifest: raw.statesManifest,
|
|
@@ -748,7 +775,6 @@ var CORE_SET = new Set(CORE_STATE_KINDS);
|
|
|
748
775
|
function isCoreKind(scope) {
|
|
749
776
|
return CORE_SET.has(scope);
|
|
750
777
|
}
|
|
751
|
-
var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
|
|
752
778
|
function isPlaceholderReason(reason) {
|
|
753
779
|
return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
|
|
754
780
|
}
|
|
@@ -826,28 +852,6 @@ function parseCoverageBaseline(raw) {
|
|
|
826
852
|
}
|
|
827
853
|
return { version: 2, acknowledge };
|
|
828
854
|
}
|
|
829
|
-
function isLegacyBaseline(raw) {
|
|
830
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
831
|
-
const r = raw;
|
|
832
|
-
if ("acknowledge" in r) return false;
|
|
833
|
-
return "uncapturedPages" in r || "missingKinds" in r;
|
|
834
|
-
}
|
|
835
|
-
function migrateBaseline(legacy) {
|
|
836
|
-
const acknowledge = {};
|
|
837
|
-
const at = (route) => acknowledge[route] ??= {};
|
|
838
|
-
for (const route of legacy.uncapturedPages ?? []) {
|
|
839
|
-
if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
|
|
840
|
-
}
|
|
841
|
-
for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
|
|
842
|
-
if (!Array.isArray(kinds)) continue;
|
|
843
|
-
for (const kind of kinds) {
|
|
844
|
-
if (typeof kind === "string" && isCoreKind(kind)) {
|
|
845
|
-
at(route)[kind] = { type: "backlog" };
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return { version: 2, acknowledge };
|
|
850
|
-
}
|
|
851
855
|
function serializeCoverageBaseline(baseline) {
|
|
852
856
|
const routes = Object.keys(baseline.acknowledge).sort();
|
|
853
857
|
const ordered = {};
|
|
@@ -881,24 +885,21 @@ var ALLOWED_FIELDS = {
|
|
|
881
885
|
"acceptance",
|
|
882
886
|
"states",
|
|
883
887
|
"acknowledge",
|
|
884
|
-
"capture",
|
|
885
|
-
"waivers",
|
|
886
888
|
"description"
|
|
887
889
|
]),
|
|
888
890
|
// A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
|
|
889
891
|
// and a core kind (one cell of its matrix). A feature/widget has no route, so
|
|
890
892
|
// the core-kind matrix is meaningless there and only `"*"` is accepted —
|
|
891
893
|
// giving a component a scoped opt-out instead of forcing you to delete its
|
|
892
|
-
// whole `states` block.
|
|
893
|
-
// only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
|
|
894
|
+
// whole `states` block.
|
|
894
895
|
feature: /* @__PURE__ */ new Set([
|
|
895
896
|
"feature",
|
|
896
897
|
"name",
|
|
897
898
|
"features",
|
|
899
|
+
"surfaces",
|
|
898
900
|
"acceptance",
|
|
899
901
|
"states",
|
|
900
902
|
"acknowledge",
|
|
901
|
-
"capture",
|
|
902
903
|
"description"
|
|
903
904
|
]),
|
|
904
905
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
@@ -908,20 +909,19 @@ var ALLOWED_FIELDS = {
|
|
|
908
909
|
"acceptance",
|
|
909
910
|
"states",
|
|
910
911
|
"acknowledge",
|
|
911
|
-
"capture",
|
|
912
912
|
"description"
|
|
913
913
|
]),
|
|
914
914
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
915
915
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
916
916
|
};
|
|
917
917
|
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
918
|
+
var ACCEPTANCE_FIELDS = /* @__PURE__ */ new Set(["id", "rev", "text"]);
|
|
918
919
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
919
920
|
"name",
|
|
920
921
|
"kind",
|
|
921
922
|
"acceptance",
|
|
922
923
|
"description"
|
|
923
924
|
]);
|
|
924
|
-
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
925
925
|
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
926
926
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
927
927
|
"page",
|
|
@@ -1287,7 +1287,8 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1287
1287
|
} else {
|
|
1288
1288
|
id = readIdField(idValue, kind, idField);
|
|
1289
1289
|
}
|
|
1290
|
-
const
|
|
1290
|
+
const criterionIds = /* @__PURE__ */ new Set();
|
|
1291
|
+
const acceptance = readAcceptanceField(byKey, criterionIds);
|
|
1291
1292
|
const description = readStringField(byKey, "description");
|
|
1292
1293
|
const name = readStringField(byKey, "name");
|
|
1293
1294
|
if (name === "") {
|
|
@@ -1313,11 +1314,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1313
1314
|
}
|
|
1314
1315
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1315
1316
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1316
|
-
const
|
|
1317
|
+
const surfacesField = kind === "feature" ? readStringArrayField(byKey, "surfaces") : void 0;
|
|
1318
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey, criterionIds) : void 0;
|
|
1317
1319
|
const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
|
|
1318
1320
|
const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
|
|
1319
|
-
const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
|
|
1320
|
-
const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
1321
1321
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1322
1322
|
const metadata = {
|
|
1323
1323
|
source: "ts-export",
|
|
@@ -1340,14 +1340,14 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1340
1340
|
metadata.widgets = widgetsField.values;
|
|
1341
1341
|
metadata.widgetSpans = widgetsField.spans;
|
|
1342
1342
|
}
|
|
1343
|
+
if (surfacesField) {
|
|
1344
|
+
metadata.surfaces = surfacesField.values;
|
|
1345
|
+
metadata.surfaceSpans = surfacesField.spans;
|
|
1346
|
+
}
|
|
1343
1347
|
if (states?.length) metadata.states = states;
|
|
1344
1348
|
if (acknowledge && Object.keys(acknowledge).length > 0) {
|
|
1345
1349
|
metadata.acknowledge = acknowledge;
|
|
1346
1350
|
}
|
|
1347
|
-
if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
|
|
1348
|
-
if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
|
|
1349
|
-
metadata.legacyWaivers = legacyWaivers;
|
|
1350
|
-
}
|
|
1351
1351
|
if (notFlow) metadata.notFlow = true;
|
|
1352
1352
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1353
1353
|
metadata.idSpan = idValue.span;
|
|
@@ -1463,47 +1463,111 @@ function readAcknowledgeField(byKey, isPage) {
|
|
|
1463
1463
|
}
|
|
1464
1464
|
return out2;
|
|
1465
1465
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1466
|
+
function acceptanceSlug(text) {
|
|
1467
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).slice(0, 8).join("-");
|
|
1468
|
+
}
|
|
1469
|
+
function readAcceptanceField(byKey, seen) {
|
|
1470
|
+
const entry = byKey.get("acceptance");
|
|
1468
1471
|
if (!entry) return void 0;
|
|
1469
|
-
|
|
1472
|
+
return readAcceptanceItems(entry, seen);
|
|
1473
|
+
}
|
|
1474
|
+
function readAcceptanceItems(entry, seen) {
|
|
1475
|
+
if (entry.value.kind !== "array") {
|
|
1470
1476
|
throw new ExtractError(
|
|
1471
1477
|
"uidex-export-invalid-field",
|
|
1472
|
-
"`
|
|
1478
|
+
"`acceptance` must be an array of strings or `{ id, rev?, text }` objects.",
|
|
1473
1479
|
entry.value.pos
|
|
1474
1480
|
);
|
|
1475
1481
|
}
|
|
1476
|
-
const
|
|
1477
|
-
for (const
|
|
1478
|
-
|
|
1482
|
+
const criteria = [];
|
|
1483
|
+
for (const item of entry.value.items) {
|
|
1484
|
+
let criterion;
|
|
1485
|
+
if (item.kind === "string") {
|
|
1486
|
+
if (item.value.length === 0) {
|
|
1487
|
+
throw new ExtractError(
|
|
1488
|
+
"uidex-export-invalid-field",
|
|
1489
|
+
"An `acceptance` entry is an empty string.",
|
|
1490
|
+
item.pos
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
const id = acceptanceSlug(item.value);
|
|
1494
|
+
if (!id) {
|
|
1495
|
+
throw new ExtractError(
|
|
1496
|
+
"uidex-export-invalid-field",
|
|
1497
|
+
`Acceptance text "${item.value}" contains no alphanumeric characters to derive an id from; use the object form with an explicit \`id\`.`,
|
|
1498
|
+
item.pos
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
criterion = { id, rev: 1, text: item.value };
|
|
1502
|
+
} else if (item.kind === "object") {
|
|
1503
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1504
|
+
for (const e of item.entries) {
|
|
1505
|
+
if (!ACCEPTANCE_FIELDS.has(e.key)) {
|
|
1506
|
+
throw new ExtractError(
|
|
1507
|
+
"uidex-export-invalid-field",
|
|
1508
|
+
`Unknown field "${e.key}" in an \`acceptance\` entry. Allowed: ${[
|
|
1509
|
+
...ACCEPTANCE_FIELDS
|
|
1510
|
+
].sort().join(", ")}.`,
|
|
1511
|
+
e.value.pos
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
fieldByKey.set(e.key, e);
|
|
1515
|
+
}
|
|
1516
|
+
const id = readStringField(fieldByKey, "id");
|
|
1517
|
+
if (id === void 0 || id.length === 0) {
|
|
1518
|
+
throw new ExtractError(
|
|
1519
|
+
"uidex-export-invalid-field",
|
|
1520
|
+
"An object `acceptance` entry needs a non-empty string `id`.",
|
|
1521
|
+
item.pos
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (id.includes("@")) {
|
|
1525
|
+
throw new ExtractError(
|
|
1526
|
+
"uidex-export-invalid-field",
|
|
1527
|
+
`acceptance id "${id}" must not contain "@" \u2014 it is reserved for the \`@<rev>\` pin in \`uidexCovers\` claims.`,
|
|
1528
|
+
item.pos
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const text = readStringField(fieldByKey, "text");
|
|
1532
|
+
if (text === void 0 || text.length === 0) {
|
|
1533
|
+
throw new ExtractError(
|
|
1534
|
+
"uidex-export-invalid-field",
|
|
1535
|
+
`acceptance "${id}" needs a non-empty string \`text\`.`,
|
|
1536
|
+
item.pos
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
let rev = 1;
|
|
1540
|
+
const revEntry = fieldByKey.get("rev");
|
|
1541
|
+
if (revEntry) {
|
|
1542
|
+
if (revEntry.value.kind !== "number" || !Number.isInteger(revEntry.value.value) || revEntry.value.value < 1) {
|
|
1543
|
+
throw new ExtractError(
|
|
1544
|
+
"uidex-export-invalid-field",
|
|
1545
|
+
`acceptance "${id}" \`rev\` must be a positive integer.`,
|
|
1546
|
+
revEntry.value.pos
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
rev = revEntry.value.value;
|
|
1550
|
+
}
|
|
1551
|
+
criterion = { id, rev, text };
|
|
1552
|
+
} else {
|
|
1479
1553
|
throw new ExtractError(
|
|
1480
1554
|
"uidex-export-invalid-field",
|
|
1481
|
-
`
|
|
1482
|
-
|
|
1555
|
+
"`acceptance` items must be strings or `{ id, rev?, text }` objects.",
|
|
1556
|
+
item.pos
|
|
1483
1557
|
);
|
|
1484
1558
|
}
|
|
1485
|
-
if (
|
|
1559
|
+
if (seen.has(criterion.id)) {
|
|
1486
1560
|
throw new ExtractError(
|
|
1487
|
-
"
|
|
1488
|
-
`
|
|
1489
|
-
|
|
1561
|
+
"acceptance-duplicate-id",
|
|
1562
|
+
`Duplicate acceptance criterion id "${criterion.id}"; each criterion id must be unique within the entity.`,
|
|
1563
|
+
item.pos,
|
|
1564
|
+
"Give the criterion an explicit distinct `id` (object form) \u2014 two string criteria can slug to the same id."
|
|
1490
1565
|
);
|
|
1491
1566
|
}
|
|
1492
|
-
|
|
1567
|
+
seen.add(criterion.id);
|
|
1568
|
+
criteria.push(criterion);
|
|
1493
1569
|
}
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
function readBooleanField(byKey, name) {
|
|
1497
|
-
const entry = byKey.get(name);
|
|
1498
|
-
if (!entry) return void 0;
|
|
1499
|
-
if (entry.value.kind !== "boolean") {
|
|
1500
|
-
throw new ExtractError(
|
|
1501
|
-
"uidex-export-invalid-field",
|
|
1502
|
-
`\`${name}\` must be a boolean.`,
|
|
1503
|
-
entry.value.pos
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
return entry.value.value;
|
|
1570
|
+
return criteria;
|
|
1507
1571
|
}
|
|
1508
1572
|
function readStringField(byKey, name) {
|
|
1509
1573
|
const entry = byKey.get(name);
|
|
@@ -1542,7 +1606,7 @@ function readStringArrayField(byKey, name) {
|
|
|
1542
1606
|
}
|
|
1543
1607
|
return { values, spans };
|
|
1544
1608
|
}
|
|
1545
|
-
function readStatesField(byKey) {
|
|
1609
|
+
function readStatesField(byKey, criterionIds) {
|
|
1546
1610
|
const entry = byKey.get("states");
|
|
1547
1611
|
if (!entry) return void 0;
|
|
1548
1612
|
if (entry.value.kind !== "array") {
|
|
@@ -1592,8 +1656,11 @@ function readStatesField(byKey) {
|
|
|
1592
1656
|
}
|
|
1593
1657
|
state.kind = kind;
|
|
1594
1658
|
}
|
|
1595
|
-
const
|
|
1596
|
-
if (
|
|
1659
|
+
const accEntry = fieldByKey.get("acceptance");
|
|
1660
|
+
if (accEntry) {
|
|
1661
|
+
const acceptance = readAcceptanceItems(accEntry, criterionIds);
|
|
1662
|
+
if (acceptance.length > 0) state.acceptance = acceptance;
|
|
1663
|
+
}
|
|
1597
1664
|
const description = readStringField(fieldByKey, "description");
|
|
1598
1665
|
if (description) state.description = description;
|
|
1599
1666
|
} else {
|
|
@@ -1649,20 +1716,21 @@ function isNode2(value) {
|
|
|
1649
1716
|
}
|
|
1650
1717
|
|
|
1651
1718
|
// src/scanner/scan/flow-facts.ts
|
|
1652
|
-
function collectFlowFacts(parsed, content) {
|
|
1719
|
+
function collectFlowFacts(parsed, content, displayPath) {
|
|
1653
1720
|
if (parsed.program === null || !content.includes("test.describe")) {
|
|
1654
|
-
return [];
|
|
1721
|
+
return { facts: [], diagnostics: [] };
|
|
1655
1722
|
}
|
|
1656
1723
|
const facts = [];
|
|
1724
|
+
const diagnostics = [];
|
|
1657
1725
|
walkAst(parsed.program, (node) => {
|
|
1658
1726
|
if (node.type !== "CallExpression") return void 0;
|
|
1659
|
-
const fact = readTaggedDescribe(node, parsed);
|
|
1727
|
+
const fact = readTaggedDescribe(node, parsed, diagnostics, displayPath);
|
|
1660
1728
|
if (fact) facts.push(fact);
|
|
1661
1729
|
return void 0;
|
|
1662
1730
|
});
|
|
1663
|
-
return facts;
|
|
1731
|
+
return { facts, diagnostics };
|
|
1664
1732
|
}
|
|
1665
|
-
function readTaggedDescribe(call, parsed) {
|
|
1733
|
+
function readTaggedDescribe(call, parsed, diagnostics, displayPath) {
|
|
1666
1734
|
const callee = call.callee;
|
|
1667
1735
|
if (!callee || callee.type !== "MemberExpression" || !isIdentifier(callee.object, "test") || !isIdentifier(callee.property, "describe")) {
|
|
1668
1736
|
return null;
|
|
@@ -1673,13 +1741,83 @@ function readTaggedDescribe(call, parsed) {
|
|
|
1673
1741
|
if (!hasFlowTag(args[1])) return null;
|
|
1674
1742
|
const body = args[2];
|
|
1675
1743
|
const { calls, dynamicCalls } = body ? collectUidexCalls(body, parsed) : { calls: [], dynamicCalls: [] };
|
|
1744
|
+
const covers = body ? collectCoversClaims(body, parsed, title, diagnostics, displayPath) : [];
|
|
1676
1745
|
return {
|
|
1677
1746
|
title,
|
|
1678
1747
|
line: parsed.lineAt(call.start),
|
|
1679
1748
|
calls,
|
|
1680
|
-
...dynamicCalls.length > 0 ? { dynamicCalls } : {}
|
|
1749
|
+
...dynamicCalls.length > 0 ? { dynamicCalls } : {},
|
|
1750
|
+
...covers.length > 0 ? { covers } : {}
|
|
1681
1751
|
};
|
|
1682
1752
|
}
|
|
1753
|
+
function collectCoversClaims(body, parsed, flowTitle, diagnostics, displayPath) {
|
|
1754
|
+
const covers = [];
|
|
1755
|
+
walkAst(body, (node) => {
|
|
1756
|
+
if (node.type !== "CallExpression") return void 0;
|
|
1757
|
+
if (!isIdentifier(node.callee, "uidexCovers")) return void 0;
|
|
1758
|
+
const line = parsed.lineAt(node.start);
|
|
1759
|
+
const args = node.arguments ?? [];
|
|
1760
|
+
const entity = stringLiteralValue(args[0]);
|
|
1761
|
+
if (entity === null) {
|
|
1762
|
+
diagnostics.push({
|
|
1763
|
+
code: "covers-missing-rev",
|
|
1764
|
+
severity: "error",
|
|
1765
|
+
message: `\`uidexCovers(\u2026)\` in flow "${flowTitle}" needs a static string entity id as its first argument.`,
|
|
1766
|
+
file: displayPath,
|
|
1767
|
+
line,
|
|
1768
|
+
hint: 'Write `uidexCovers("<entityId>", "<criterionId>@<rev>")` with string literals.'
|
|
1769
|
+
});
|
|
1770
|
+
return void 0;
|
|
1771
|
+
}
|
|
1772
|
+
if (args.length < 2) {
|
|
1773
|
+
diagnostics.push({
|
|
1774
|
+
code: "covers-missing-rev",
|
|
1775
|
+
severity: "error",
|
|
1776
|
+
message: `\`uidexCovers("${entity}")\` in flow "${flowTitle}" names no criterion; a claim covers nothing without at least one \`"<criterionId>@<rev>"\` argument.`,
|
|
1777
|
+
file: displayPath,
|
|
1778
|
+
line,
|
|
1779
|
+
hint: 'List the criteria this flow verifies: `uidexCovers("<entityId>", "<criterionId>@<rev>", ...)`.'
|
|
1780
|
+
});
|
|
1781
|
+
return void 0;
|
|
1782
|
+
}
|
|
1783
|
+
for (const arg of args.slice(1)) {
|
|
1784
|
+
const claimLine = parsed.lineAt(arg.start);
|
|
1785
|
+
const raw = stringLiteralValue(arg);
|
|
1786
|
+
const at = raw === null ? -1 : raw.lastIndexOf("@");
|
|
1787
|
+
if (raw === null || at <= 0) {
|
|
1788
|
+
diagnostics.push({
|
|
1789
|
+
code: "covers-missing-rev",
|
|
1790
|
+
severity: "error",
|
|
1791
|
+
message: `\`uidexCovers(\u2026)\` claim ${raw === null ? "" : `"${raw}" `}in flow "${flowTitle}" is missing its \`@<rev>\` pin.`,
|
|
1792
|
+
file: displayPath,
|
|
1793
|
+
line: claimLine,
|
|
1794
|
+
hint: 'Pin the criterion rev \u2014 `uidexCovers("<entityId>", "<criterionId>@<rev>")` \u2014 so a later rev bump surfaces this claim as outdated.'
|
|
1795
|
+
});
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
const revStr = raw.slice(at + 1);
|
|
1799
|
+
if (!/^[1-9]\d*$/.test(revStr)) {
|
|
1800
|
+
diagnostics.push({
|
|
1801
|
+
code: "covers-missing-rev",
|
|
1802
|
+
severity: "error",
|
|
1803
|
+
message: `\`uidexCovers(\u2026)\` claim "${raw}" in flow "${flowTitle}" has an invalid rev "@${revStr}"; the rev must be a positive integer (no leading zeros).`,
|
|
1804
|
+
file: displayPath,
|
|
1805
|
+
line: claimLine,
|
|
1806
|
+
hint: 'Write the pin as `"<criterionId>@<rev>"` with a positive integer rev, e.g. `"gated@1"`.'
|
|
1807
|
+
});
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
covers.push({
|
|
1811
|
+
entity,
|
|
1812
|
+
criterion: raw.slice(0, at),
|
|
1813
|
+
rev: Number(revStr),
|
|
1814
|
+
line: claimLine
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
return void 0;
|
|
1818
|
+
});
|
|
1819
|
+
return covers;
|
|
1820
|
+
}
|
|
1683
1821
|
function hasFlowTag(node) {
|
|
1684
1822
|
if (!node || node.type !== "ObjectExpression") return false;
|
|
1685
1823
|
for (const prop of node.properties ?? []) {
|
|
@@ -2071,11 +2209,13 @@ function extract(files) {
|
|
|
2071
2209
|
const out2 = { file, annotations: [] };
|
|
2072
2210
|
out2.annotations = extractOne(file, parsed, out2);
|
|
2073
2211
|
if (exports.length > 0) out2.metadata = exports;
|
|
2212
|
+
const flowFacts = collectFlowFacts(parsed, file.content, file.displayPath);
|
|
2213
|
+
diagnostics.push(...flowFacts.diagnostics);
|
|
2074
2214
|
if (diagnostics.length > 0) out2.diagnostics = diagnostics;
|
|
2075
|
-
|
|
2076
|
-
if (flows.length > 0) out2.flows = flows;
|
|
2215
|
+
if (flowFacts.facts.length > 0) out2.flows = flowFacts.facts;
|
|
2077
2216
|
const imports = collectImportFacts(parsed);
|
|
2078
2217
|
if (imports.length > 0) out2.imports = imports;
|
|
2218
|
+
if (containsJsx(parsed)) out2.hasJsx = true;
|
|
2079
2219
|
return out2;
|
|
2080
2220
|
});
|
|
2081
2221
|
}
|
|
@@ -2100,6 +2240,32 @@ function extractOne(file, parsed, out2) {
|
|
|
2100
2240
|
}
|
|
2101
2241
|
return annotations;
|
|
2102
2242
|
}
|
|
2243
|
+
function containsJsx(parsed) {
|
|
2244
|
+
if (parsed.program === null) return false;
|
|
2245
|
+
let found = false;
|
|
2246
|
+
walkAst(parsed.program, (node) => {
|
|
2247
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
2248
|
+
found = true;
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
if (node.type === "CallExpression" && isCreateElementCallee(node.callee)) {
|
|
2252
|
+
found = true;
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
return void 0;
|
|
2256
|
+
});
|
|
2257
|
+
return found;
|
|
2258
|
+
}
|
|
2259
|
+
function isCreateElementCallee(callee) {
|
|
2260
|
+
const node = callee;
|
|
2261
|
+
if (!node) return false;
|
|
2262
|
+
if (node.type === "Identifier") return String(node.name) === "createElement";
|
|
2263
|
+
if (node.type === "MemberExpression") {
|
|
2264
|
+
const prop = node.property;
|
|
2265
|
+
return prop?.type === "Identifier" && String(prop.name) === "createElement";
|
|
2266
|
+
}
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2103
2269
|
function collectImportFacts(parsed) {
|
|
2104
2270
|
if (parsed.program === null) return [];
|
|
2105
2271
|
const out2 = [];
|
|
@@ -2132,6 +2298,21 @@ function collectImportFacts(parsed) {
|
|
|
2132
2298
|
names
|
|
2133
2299
|
});
|
|
2134
2300
|
}
|
|
2301
|
+
walkAst(parsed.program, (node) => {
|
|
2302
|
+
if (node.type !== "ImportExpression") return void 0;
|
|
2303
|
+
const source = node.source;
|
|
2304
|
+
if (!source || source.type !== "Literal") return void 0;
|
|
2305
|
+
if (typeof source.value !== "string") return void 0;
|
|
2306
|
+
out2.push({
|
|
2307
|
+
specifier: source.value,
|
|
2308
|
+
line: parsed.lineAt(node.start),
|
|
2309
|
+
span: { start: node.start, end: node.end },
|
|
2310
|
+
isTypeOnly: false,
|
|
2311
|
+
names: [],
|
|
2312
|
+
dynamic: true
|
|
2313
|
+
});
|
|
2314
|
+
return void 0;
|
|
2315
|
+
});
|
|
2135
2316
|
return out2;
|
|
2136
2317
|
}
|
|
2137
2318
|
|
|
@@ -2303,6 +2484,7 @@ function buildMetaFromExport(exp) {
|
|
|
2303
2484
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
2304
2485
|
if (exp.features?.length) meta.features = exp.features;
|
|
2305
2486
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
2487
|
+
if (exp.surfaces?.length) meta.surfaces = exp.surfaces;
|
|
2306
2488
|
if (exp.states?.length) {
|
|
2307
2489
|
meta.states = exp.states.map((s) => ({
|
|
2308
2490
|
name: s.name,
|
|
@@ -2716,7 +2898,8 @@ function resolve2(ctx) {
|
|
|
2716
2898
|
id: flowExport.id,
|
|
2717
2899
|
loc: base.loc,
|
|
2718
2900
|
touches: base.touches,
|
|
2719
|
-
steps: base.steps
|
|
2901
|
+
steps: base.steps,
|
|
2902
|
+
...base.covers ? { covers: base.covers } : {}
|
|
2720
2903
|
};
|
|
2721
2904
|
registry.add(flow);
|
|
2722
2905
|
} else {
|
|
@@ -2764,20 +2947,42 @@ function computeScope(displayPath) {
|
|
|
2764
2947
|
return null;
|
|
2765
2948
|
}
|
|
2766
2949
|
function flowsFromFacts(ff) {
|
|
2767
|
-
return (ff.flows ?? []).map((fact) =>
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2950
|
+
return (ff.flows ?? []).map((fact) => {
|
|
2951
|
+
const covers = dedupeBy(
|
|
2952
|
+
(fact.covers ?? []).map((c) => ({
|
|
2953
|
+
entity: c.entity,
|
|
2954
|
+
criterion: c.criterion,
|
|
2955
|
+
rev: c.rev
|
|
2956
|
+
})),
|
|
2957
|
+
(c) => `${c.entity} ${c.criterion} ${c.rev}`
|
|
2958
|
+
);
|
|
2959
|
+
return {
|
|
2960
|
+
kind: "flow",
|
|
2961
|
+
id: kebab(fact.title),
|
|
2962
|
+
loc: { file: ff.file.displayPath, line: fact.line },
|
|
2963
|
+
touches: dedupe(fact.calls.map((c) => c.id)),
|
|
2964
|
+
steps: fact.calls.filter((c) => c.action).map((c) => ({ entityId: c.id, action: c.action })),
|
|
2965
|
+
...covers.length > 0 ? { covers } : {}
|
|
2966
|
+
};
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
function dedupeBy(arr, key) {
|
|
2970
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2971
|
+
const out2 = [];
|
|
2972
|
+
for (const item of arr) {
|
|
2973
|
+
const k = key(item);
|
|
2974
|
+
if (seen.has(k)) continue;
|
|
2975
|
+
seen.add(k);
|
|
2976
|
+
out2.push(item);
|
|
2977
|
+
}
|
|
2978
|
+
return out2;
|
|
2774
2979
|
}
|
|
2775
2980
|
function dedupe(arr) {
|
|
2776
2981
|
return Array.from(new Set(arr));
|
|
2777
2982
|
}
|
|
2778
2983
|
|
|
2779
2984
|
// src/scanner/scan/audit.ts
|
|
2780
|
-
import * as
|
|
2985
|
+
import * as path7 from "path";
|
|
2781
2986
|
|
|
2782
2987
|
// src/scanner/scan/component-coverage.ts
|
|
2783
2988
|
var COMPOSED_KINDS = ["widget", "feature"];
|
|
@@ -2832,141 +3037,6 @@ function checkComponentCoverage(registry, manifest) {
|
|
|
2832
3037
|
return diagnostics;
|
|
2833
3038
|
}
|
|
2834
3039
|
|
|
2835
|
-
// src/scanner/scan/migrate-acknowledge.ts
|
|
2836
|
-
var CORE = CORE_STATE_KINDS;
|
|
2837
|
-
function orderScopes(scopes) {
|
|
2838
|
-
const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
|
|
2839
|
-
return [...scopes].sort((a, b) => rank(a) - rank(b));
|
|
2840
|
-
}
|
|
2841
|
-
function renderKey(scope) {
|
|
2842
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
|
|
2843
|
-
}
|
|
2844
|
-
function renderAck(a) {
|
|
2845
|
-
const parts = [`type: ${JSON.stringify(a.type)}`];
|
|
2846
|
-
if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
|
|
2847
|
-
return `{ ${parts.join(", ")} }`;
|
|
2848
|
-
}
|
|
2849
|
-
function renderAcknowledge(entries, indent) {
|
|
2850
|
-
const inner = indent + " ";
|
|
2851
|
-
const lines = orderScopes(Object.keys(entries)).map(
|
|
2852
|
-
(scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
|
|
2853
|
-
);
|
|
2854
|
-
return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
|
|
2855
|
-
}
|
|
2856
|
-
function indentAt(content, offset) {
|
|
2857
|
-
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2858
|
-
const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
|
|
2859
|
-
return m ? m[0] : "";
|
|
2860
|
-
}
|
|
2861
|
-
function plannedEntries(m) {
|
|
2862
|
-
const entries = {};
|
|
2863
|
-
if (m.legacyCapture === false) {
|
|
2864
|
-
entries[ACK_ALL] = {
|
|
2865
|
-
type: "impossible",
|
|
2866
|
-
reason: m.description || PLACEHOLDER_REASON
|
|
2867
|
-
};
|
|
2868
|
-
}
|
|
2869
|
-
for (const kind of CORE) {
|
|
2870
|
-
const reason = m.legacyWaivers?.[kind];
|
|
2871
|
-
if (reason) entries[kind] = { type: "impossible", reason };
|
|
2872
|
-
}
|
|
2873
|
-
return entries;
|
|
2874
|
-
}
|
|
2875
|
-
function migrateLegacyFields(ef) {
|
|
2876
|
-
const out2 = [];
|
|
2877
|
-
const { content, sourcePath, displayPath } = ef.file;
|
|
2878
|
-
for (const m of ef.metadata ?? []) {
|
|
2879
|
-
const legacy = [];
|
|
2880
|
-
if (m.legacyWaivers) legacy.push("waivers");
|
|
2881
|
-
if (m.legacyCapture !== void 0) legacy.push("capture");
|
|
2882
|
-
if (legacy.length === 0) continue;
|
|
2883
|
-
const fields = legacy.join(" and ");
|
|
2884
|
-
const base = {
|
|
2885
|
-
severity: "error",
|
|
2886
|
-
code: "legacy-acknowledge-field",
|
|
2887
|
-
file: displayPath,
|
|
2888
|
-
line: m.loc.line
|
|
2889
|
-
};
|
|
2890
|
-
if (m.acknowledge) {
|
|
2891
|
-
out2.push({
|
|
2892
|
-
...base,
|
|
2893
|
-
message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
|
|
2894
|
-
hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
|
|
2895
|
-
});
|
|
2896
|
-
continue;
|
|
2897
|
-
}
|
|
2898
|
-
const entries = plannedEntries(m);
|
|
2899
|
-
const edits = [];
|
|
2900
|
-
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);
|
|
2901
|
-
if (present.length !== legacy.length) {
|
|
2902
|
-
out2.push({
|
|
2903
|
-
...base,
|
|
2904
|
-
message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
|
|
2905
|
-
hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
|
|
2906
|
-
});
|
|
2907
|
-
continue;
|
|
2908
|
-
}
|
|
2909
|
-
const [anchor, ...rest] = present;
|
|
2910
|
-
if (Object.keys(entries).length === 0) {
|
|
2911
|
-
edits.push({
|
|
2912
|
-
path: sourcePath,
|
|
2913
|
-
start: anchor.del.start,
|
|
2914
|
-
end: anchor.del.end,
|
|
2915
|
-
replacement: ""
|
|
2916
|
-
});
|
|
2917
|
-
} else {
|
|
2918
|
-
edits.push({
|
|
2919
|
-
path: sourcePath,
|
|
2920
|
-
start: anchor.prop.start,
|
|
2921
|
-
end: anchor.prop.end,
|
|
2922
|
-
replacement: renderAcknowledge(
|
|
2923
|
-
entries,
|
|
2924
|
-
indentAt(content, anchor.prop.start)
|
|
2925
|
-
)
|
|
2926
|
-
});
|
|
2927
|
-
}
|
|
2928
|
-
for (const r of rest) {
|
|
2929
|
-
edits.push({
|
|
2930
|
-
path: sourcePath,
|
|
2931
|
-
start: r.del.start,
|
|
2932
|
-
end: r.del.end,
|
|
2933
|
-
replacement: ""
|
|
2934
|
-
});
|
|
2935
|
-
}
|
|
2936
|
-
out2.push({
|
|
2937
|
-
...base,
|
|
2938
|
-
message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
|
|
2939
|
-
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" : ""}.`,
|
|
2940
|
-
fix: {
|
|
2941
|
-
description: `Rewrite \`${fields}\` as \`acknowledge\``,
|
|
2942
|
-
edits
|
|
2943
|
-
}
|
|
2944
|
-
});
|
|
2945
|
-
}
|
|
2946
|
-
return out2;
|
|
2947
|
-
}
|
|
2948
|
-
function migrateLegacyBaseline(opts) {
|
|
2949
|
-
if (!isLegacyBaseline(opts.parsed)) return void 0;
|
|
2950
|
-
return {
|
|
2951
|
-
code: "legacy-coverage-baseline",
|
|
2952
|
-
severity: "error",
|
|
2953
|
-
message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
|
|
2954
|
-
file: opts.displayPath,
|
|
2955
|
-
hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
|
|
2956
|
-
fix: {
|
|
2957
|
-
description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
|
|
2958
|
-
edits: [
|
|
2959
|
-
{
|
|
2960
|
-
path: opts.path,
|
|
2961
|
-
start: 0,
|
|
2962
|
-
end: opts.raw.length,
|
|
2963
|
-
replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
|
|
2964
|
-
}
|
|
2965
|
-
]
|
|
2966
|
-
}
|
|
2967
|
-
};
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
3040
|
// src/scanner/scan/page-coverage.ts
|
|
2971
3041
|
function compileRoute(pattern) {
|
|
2972
3042
|
const segs = pattern.split("/").filter(Boolean);
|
|
@@ -2988,11 +3058,11 @@ function compileRoute(pattern) {
|
|
|
2988
3058
|
return { test: (url) => re.test(url), specificity };
|
|
2989
3059
|
}
|
|
2990
3060
|
function matchUrlToRoute(url, routePaths) {
|
|
2991
|
-
const
|
|
3061
|
+
const path14 = stripQuery(url);
|
|
2992
3062
|
let best = null;
|
|
2993
3063
|
for (const rp of routePaths) {
|
|
2994
3064
|
const { test, specificity } = compileRoute(rp);
|
|
2995
|
-
if (!test(
|
|
3065
|
+
if (!test(path14)) continue;
|
|
2996
3066
|
if (!best || specificity > best.specificity)
|
|
2997
3067
|
best = { path: rp, specificity };
|
|
2998
3068
|
}
|
|
@@ -3400,6 +3470,182 @@ function computeMatrixBacklog(registry, manifest) {
|
|
|
3400
3470
|
return out2;
|
|
3401
3471
|
}
|
|
3402
3472
|
|
|
3473
|
+
// src/scanner/scan/surface-check.ts
|
|
3474
|
+
import * as path6 from "path";
|
|
3475
|
+
var EXT_CANDIDATES = [
|
|
3476
|
+
"",
|
|
3477
|
+
".tsx",
|
|
3478
|
+
".ts",
|
|
3479
|
+
".jsx",
|
|
3480
|
+
".js",
|
|
3481
|
+
"/index.tsx",
|
|
3482
|
+
"/index.ts",
|
|
3483
|
+
"/index.jsx",
|
|
3484
|
+
"/index.js"
|
|
3485
|
+
];
|
|
3486
|
+
function checkFeatureSurfaces(registry, extracted, config) {
|
|
3487
|
+
const declarations = [];
|
|
3488
|
+
for (const ef of extracted) {
|
|
3489
|
+
for (const m of ef.metadata ?? []) {
|
|
3490
|
+
if (m.kind !== "feature") continue;
|
|
3491
|
+
if (typeof m.id !== "string") continue;
|
|
3492
|
+
if (!m.surfaces || m.surfaces.length === 0) continue;
|
|
3493
|
+
if (m.acknowledge?.["*"]) continue;
|
|
3494
|
+
declarations.push({
|
|
3495
|
+
ef,
|
|
3496
|
+
id: m.id,
|
|
3497
|
+
surfaces: m.surfaces,
|
|
3498
|
+
surfaceSpans: m.surfaceSpans,
|
|
3499
|
+
line: m.loc.line ?? 1
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
if (declarations.length === 0) return [];
|
|
3504
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
3505
|
+
const jsxFiles = /* @__PURE__ */ new Set();
|
|
3506
|
+
for (const ef of extracted) {
|
|
3507
|
+
fileSet.add(ef.file.displayPath);
|
|
3508
|
+
if (ef.hasJsx) jsxFiles.add(ef.file.displayPath);
|
|
3509
|
+
}
|
|
3510
|
+
const aliases = Object.entries(config.resolve?.aliases ?? {}).sort(
|
|
3511
|
+
(a, b) => b[0].length - a[0].length
|
|
3512
|
+
);
|
|
3513
|
+
const resolveSpecifier = (importer, spec) => {
|
|
3514
|
+
const bases = [];
|
|
3515
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
3516
|
+
bases.push(path6.posix.join(path6.posix.dirname(importer), spec));
|
|
3517
|
+
} else {
|
|
3518
|
+
const alias = aliases.find(([prefix]) => spec.startsWith(prefix));
|
|
3519
|
+
if (alias) {
|
|
3520
|
+
bases.push(alias[1] + spec.slice(alias[0].length));
|
|
3521
|
+
} else if (aliases.length === 0) {
|
|
3522
|
+
if (spec.startsWith("@/") || spec.startsWith("~/")) {
|
|
3523
|
+
bases.push("src/" + spec.slice(2));
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
bases.push(spec);
|
|
3527
|
+
}
|
|
3528
|
+
const hits = [];
|
|
3529
|
+
for (const base of bases) {
|
|
3530
|
+
const normalized = path6.posix.normalize(base);
|
|
3531
|
+
for (const ext of EXT_CANDIDATES) {
|
|
3532
|
+
const candidate = normalized + ext;
|
|
3533
|
+
if (fileSet.has(candidate)) hits.push(candidate);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
return hits;
|
|
3537
|
+
};
|
|
3538
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
3539
|
+
for (const ef of extracted) {
|
|
3540
|
+
const targets = [];
|
|
3541
|
+
for (const imp of ef.imports ?? []) {
|
|
3542
|
+
if (imp.isTypeOnly) continue;
|
|
3543
|
+
targets.push(...resolveSpecifier(ef.file.displayPath, imp.specifier));
|
|
3544
|
+
}
|
|
3545
|
+
if (targets.length > 0) adjacency.set(ef.file.displayPath, targets);
|
|
3546
|
+
}
|
|
3547
|
+
const inDir = (file, dir) => file === dir || file.startsWith(dir + "/");
|
|
3548
|
+
const reachesDir = (seeds, dir) => {
|
|
3549
|
+
const queue = [...seeds];
|
|
3550
|
+
const visited = new Set(queue);
|
|
3551
|
+
while (queue.length > 0) {
|
|
3552
|
+
const current = queue.pop();
|
|
3553
|
+
if (inDir(current, dir) && jsxFiles.has(current)) return true;
|
|
3554
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
3555
|
+
if (visited.has(next)) continue;
|
|
3556
|
+
visited.add(next);
|
|
3557
|
+
queue.push(next);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return false;
|
|
3561
|
+
};
|
|
3562
|
+
const featuresConvention = config.conventions?.features === void 0 ? DEFAULT_CONVENTIONS.features : config.conventions.features;
|
|
3563
|
+
const featureGlob = typeof featuresConvention === "string" ? featuresConvention : null;
|
|
3564
|
+
const diagnostics = [];
|
|
3565
|
+
for (const decl of declarations) {
|
|
3566
|
+
const declPath = decl.ef.file.displayPath;
|
|
3567
|
+
const featureDir = (featureGlob !== null ? extractFeatureDir(declPath, featureGlob) : null) ?? path6.posix.dirname(declPath);
|
|
3568
|
+
for (let i = 0; i < decl.surfaces.length; i++) {
|
|
3569
|
+
const pageId = decl.surfaces[i];
|
|
3570
|
+
const line = decl.surfaceSpans?.[i] ? lineOfOffset(decl.ef.file.content, decl.surfaceSpans[i].start) : decl.line;
|
|
3571
|
+
const page = registry.get("page", pageId) ?? registry.matchPattern("page", pageId);
|
|
3572
|
+
if (!page) {
|
|
3573
|
+
diagnostics.push({
|
|
3574
|
+
code: "unknown-reference",
|
|
3575
|
+
severity: "warning",
|
|
3576
|
+
message: `Feature "${decl.id}" declares a surface on unknown page "${pageId}"`,
|
|
3577
|
+
file: decl.ef.file.displayPath,
|
|
3578
|
+
line,
|
|
3579
|
+
entity: { kind: "feature", id: decl.id },
|
|
3580
|
+
hint: `No page with id "${pageId}" exists in the registry; fix the reference or add the page.`
|
|
3581
|
+
});
|
|
3582
|
+
continue;
|
|
3583
|
+
}
|
|
3584
|
+
const pageFile = page.loc?.file;
|
|
3585
|
+
if (!pageFile) continue;
|
|
3586
|
+
const seeds = [pageFile];
|
|
3587
|
+
if (path6.posix.basename(pageFile) === WELL_KNOWN_FILES.page) {
|
|
3588
|
+
const dir = path6.posix.dirname(pageFile);
|
|
3589
|
+
for (const f of fileSet) {
|
|
3590
|
+
if (f !== pageFile && path6.posix.dirname(f) === dir) seeds.push(f);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (reachesDir(seeds, featureDir)) continue;
|
|
3594
|
+
diagnostics.push({
|
|
3595
|
+
code: "feature-surface-unreferenced",
|
|
3596
|
+
severity: "warning",
|
|
3597
|
+
message: `Feature "${decl.id}" declares page "${pageId}" as a surface, but nothing reachable from ${pageFile} references a component file in ${featureDir}/`,
|
|
3598
|
+
file: decl.ef.file.displayPath,
|
|
3599
|
+
line,
|
|
3600
|
+
entity: { kind: "feature", id: decl.id },
|
|
3601
|
+
hint: `This is a static tripwire, not proof either way: verify page "${pageId}" really mounts a component from ${featureDir}/ (and that the import resolves within the scanned sources), or remove "${pageId}" from the feature's \`surfaces\`.`
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return diagnostics;
|
|
3606
|
+
}
|
|
3607
|
+
function checkFeatureSurfaceCaptures(registry, manifest) {
|
|
3608
|
+
const routes = registry.list("route");
|
|
3609
|
+
if (routes.length === 0) return [];
|
|
3610
|
+
const routePaths = routes.map((r) => r.path);
|
|
3611
|
+
const captured = manifest.captured ?? [];
|
|
3612
|
+
const diagnostics = [];
|
|
3613
|
+
for (const feature of registry.list("feature")) {
|
|
3614
|
+
const surfaces = feature.meta?.surfaces ?? [];
|
|
3615
|
+
if (surfaces.length === 0) continue;
|
|
3616
|
+
if (entityAcknowledge(feature.meta)) continue;
|
|
3617
|
+
const coveredRoutes = /* @__PURE__ */ new Set();
|
|
3618
|
+
for (const c of captured) {
|
|
3619
|
+
if (c.kind !== "feature" || c.entity !== feature.id) continue;
|
|
3620
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
3621
|
+
if (route) coveredRoutes.add(route);
|
|
3622
|
+
}
|
|
3623
|
+
for (const pageId of surfaces) {
|
|
3624
|
+
const pageRoutes = routes.filter((r) => r.page === pageId);
|
|
3625
|
+
if (pageRoutes.length === 0) continue;
|
|
3626
|
+
if (pageRoutes.some((r) => coveredRoutes.has(r.path))) continue;
|
|
3627
|
+
const routeList = pageRoutes.map((r) => `"${r.path}"`).join(", ");
|
|
3628
|
+
diagnostics.push({
|
|
3629
|
+
code: "feature-surface-uncaptured",
|
|
3630
|
+
severity: "warning",
|
|
3631
|
+
message: `Feature "${feature.id}" declares page "${pageId}" as a surface, but no capture rendered the feature on ${pageRoutes.length === 1 ? `route ${routeList}` : `any of its routes (${routeList})`}`,
|
|
3632
|
+
file: feature.loc?.file,
|
|
3633
|
+
line: feature.loc?.line,
|
|
3634
|
+
entity: { kind: "feature", id: feature.id },
|
|
3635
|
+
hint: `Author a states capture spec that renders feature "${feature.id}" on page "${pageId}" (a capture with kind "feature" driving ${routeList}), or acknowledge the feature with \`acknowledge: { "*": \u2026 }\`.`
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return diagnostics;
|
|
3640
|
+
}
|
|
3641
|
+
function lineOfOffset(content, offset) {
|
|
3642
|
+
let line = 1;
|
|
3643
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
3644
|
+
if (content[i] === "\n") line++;
|
|
3645
|
+
}
|
|
3646
|
+
return line;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3403
3649
|
// src/scanner/scan/audit.ts
|
|
3404
3650
|
function audit(opts) {
|
|
3405
3651
|
const diagnostics = [];
|
|
@@ -3407,6 +3653,8 @@ function audit(opts) {
|
|
|
3407
3653
|
const check = opts.check ?? false;
|
|
3408
3654
|
const lint = opts.lint ?? false;
|
|
3409
3655
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
3656
|
+
const surfacesEnabled = config.audit?.surfaces ?? true;
|
|
3657
|
+
const requireAcceptanceEnabled = config.audit?.requireAcceptance ?? true;
|
|
3410
3658
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
3411
3659
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
3412
3660
|
const statesEnabled = config.audit?.states ?? true;
|
|
@@ -3419,8 +3667,6 @@ function audit(opts) {
|
|
|
3419
3667
|
for (const ef of opts.flowExtracted ?? []) {
|
|
3420
3668
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
3421
3669
|
}
|
|
3422
|
-
for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
|
|
3423
|
-
if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
|
|
3424
3670
|
for (const ef of extracted) {
|
|
3425
3671
|
for (const m of ef.metadata ?? []) {
|
|
3426
3672
|
for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
|
|
@@ -3505,9 +3751,66 @@ function audit(opts) {
|
|
|
3505
3751
|
}
|
|
3506
3752
|
}
|
|
3507
3753
|
if (lint && acceptanceEnabled) {
|
|
3508
|
-
|
|
3754
|
+
const flowEntities = registry.list("flow");
|
|
3755
|
+
const acceptanceKinds = ["widget", "feature", "page"];
|
|
3756
|
+
const claims = /* @__PURE__ */ new Map();
|
|
3757
|
+
const addClaim = (kind, entityId, criterion, claim) => {
|
|
3758
|
+
const key = `${kind}:${entityId}`;
|
|
3759
|
+
let byCrit = claims.get(key);
|
|
3760
|
+
if (!byCrit) {
|
|
3761
|
+
byCrit = /* @__PURE__ */ new Map();
|
|
3762
|
+
claims.set(key, byCrit);
|
|
3763
|
+
}
|
|
3764
|
+
let list = byCrit.get(criterion);
|
|
3765
|
+
if (!list) {
|
|
3766
|
+
list = [];
|
|
3767
|
+
byCrit.set(criterion, list);
|
|
3768
|
+
}
|
|
3769
|
+
list.push(claim);
|
|
3770
|
+
};
|
|
3771
|
+
for (const flow of flowEntities) {
|
|
3772
|
+
for (const c of flow.covers ?? []) {
|
|
3773
|
+
const candidates = acceptanceKinds.flatMap((k) => {
|
|
3774
|
+
const e = registry.get(k, c.entity);
|
|
3775
|
+
return e ? [e] : [];
|
|
3776
|
+
});
|
|
3777
|
+
const matching = candidates.filter(
|
|
3778
|
+
(e) => entityCriteria(e).some((crit) => crit.id === c.criterion)
|
|
3779
|
+
);
|
|
3780
|
+
if (matching.length === 0) {
|
|
3781
|
+
diagnostics.push({
|
|
3782
|
+
code: "acceptance-orphaned",
|
|
3783
|
+
severity: "warning",
|
|
3784
|
+
message: `Flow "${flow.id}" covers criterion "${c.criterion}" on "${c.entity}", but ${candidates.length > 0 ? `no criterion with that id exists on ${candidates.map((e) => `${e.kind} "${c.entity}"`).join(" or ")}` : `no page/feature/widget "${c.entity}" exists`}`,
|
|
3785
|
+
file: flow.loc.file,
|
|
3786
|
+
line: flow.loc.line,
|
|
3787
|
+
entity: { kind: "flow", id: flow.id },
|
|
3788
|
+
hint: "Remove the stale uidexCovers claim, or fix the entity/criterion id it names."
|
|
3789
|
+
});
|
|
3790
|
+
continue;
|
|
3791
|
+
}
|
|
3792
|
+
if (matching.length > 1) {
|
|
3793
|
+
diagnostics.push({
|
|
3794
|
+
code: "acceptance-ambiguous",
|
|
3795
|
+
severity: "warning",
|
|
3796
|
+
message: `Flow "${flow.id}" covers criterion "${c.criterion}" on "${c.entity}", which matches ${matching.map((e) => `${e.kind} "${c.entity}"`).join(" and ")}; the claim credits all of them`,
|
|
3797
|
+
file: flow.loc.file,
|
|
3798
|
+
line: flow.loc.line,
|
|
3799
|
+
entity: { kind: "flow", id: flow.id },
|
|
3800
|
+
hint: "The claim syntax carries no kind, so a criterion id shared by same-id entities cannot be told apart \u2014 rename one entity or criterion id to disambiguate."
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
for (const target of matching) {
|
|
3804
|
+
addClaim(target.kind, target.id, c.criterion, {
|
|
3805
|
+
flowId: flow.id,
|
|
3806
|
+
rev: c.rev
|
|
3807
|
+
});
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
for (const kind of acceptanceKinds) {
|
|
3509
3812
|
for (const e of registry.list(kind)) {
|
|
3510
|
-
const criteria = e
|
|
3813
|
+
const criteria = entityCriteria(e);
|
|
3511
3814
|
if (criteria.length === 0) {
|
|
3512
3815
|
if (kind === "widget") {
|
|
3513
3816
|
diagnostics.push({
|
|
@@ -3522,14 +3825,27 @@ function audit(opts) {
|
|
|
3522
3825
|
}
|
|
3523
3826
|
continue;
|
|
3524
3827
|
}
|
|
3525
|
-
const
|
|
3526
|
-
|
|
3527
|
-
const
|
|
3528
|
-
|
|
3828
|
+
const entityClaims = claims.get(`${kind}:${e.id}`);
|
|
3829
|
+
for (const c of criteria) {
|
|
3830
|
+
const critClaims = entityClaims?.get(c.id) ?? [];
|
|
3831
|
+
const stale = critClaims.filter((cl) => cl.rev !== c.rev);
|
|
3832
|
+
for (const cl of stale) {
|
|
3833
|
+
diagnostics.push({
|
|
3834
|
+
code: "acceptance-outdated",
|
|
3835
|
+
severity: "warning",
|
|
3836
|
+
message: `Flow "${cl.flowId}" covers ${kind} "${e.id}" criterion "${c.id}" at rev ${cl.rev}, but the criterion is now rev ${c.rev}`,
|
|
3837
|
+
file: e.loc?.file,
|
|
3838
|
+
line: e.loc?.line,
|
|
3839
|
+
entity: { kind, id: e.id },
|
|
3840
|
+
hint: `Re-verify the flow against the current criterion text, then update its claim to \`uidexCovers("${e.id}", "${c.id}@${c.rev}")\`.`
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
if (critClaims.length === 0) {
|
|
3844
|
+
const hint = kind === "widget" ? `uidex scaffold widget ${e.id}, then claim it with \`uidexCovers("${e.id}", "${c.id}@${c.rev}")\`` : `Add a Playwright flow under e2e/** that touches ${kind} "${e.id}" (tag the describe with @uidex:flow) and claim the criterion with \`uidexCovers("${e.id}", "${c.id}@${c.rev}")\``;
|
|
3529
3845
|
diagnostics.push({
|
|
3530
3846
|
code: "acceptance-uncovered",
|
|
3531
3847
|
severity: "warning",
|
|
3532
|
-
message: `${kind} "${e.id}" has acceptance criterion not covered by any flow: "${c}"`,
|
|
3848
|
+
message: `${kind} "${e.id}" has acceptance criterion "${c.id}" not covered by any flow: "${c.text}"`,
|
|
3533
3849
|
file: e.loc?.file,
|
|
3534
3850
|
line: e.loc?.line,
|
|
3535
3851
|
entity: { kind, id: e.id },
|
|
@@ -3540,7 +3856,31 @@ function audit(opts) {
|
|
|
3540
3856
|
}
|
|
3541
3857
|
}
|
|
3542
3858
|
}
|
|
3859
|
+
if (lint && surfacesEnabled) {
|
|
3860
|
+
diagnostics.push(...checkFeatureSurfaces(registry, extracted, config));
|
|
3861
|
+
}
|
|
3862
|
+
if (lint && surfacesEnabled && opts.capturedStates) {
|
|
3863
|
+
diagnostics.push(
|
|
3864
|
+
...checkFeatureSurfaceCaptures(registry, opts.capturedStates)
|
|
3865
|
+
);
|
|
3866
|
+
}
|
|
3867
|
+
if (lint && requireAcceptanceEnabled) {
|
|
3868
|
+
for (const e of registry.list("feature")) {
|
|
3869
|
+
if (entityCriteria(e).length > 0) continue;
|
|
3870
|
+
diagnostics.push({
|
|
3871
|
+
code: "feature-missing-acceptance",
|
|
3872
|
+
severity: "warning",
|
|
3873
|
+
message: `Feature "${e.id}" declares no acceptance criteria`,
|
|
3874
|
+
file: e.loc?.file,
|
|
3875
|
+
line: e.loc?.line,
|
|
3876
|
+
entity: { kind: "feature", id: e.id },
|
|
3877
|
+
hint: `Add \`acceptance: [...]\` to the feature's \`export const uidex\` \u2014 observable outcomes, not implementation. They feed \`uidex scaffold feature ${e.id}\` and the acceptance-uncovered gate.`
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3543
3881
|
if (lint) {
|
|
3882
|
+
const featuresConvention = config.conventions?.features === void 0 ? DEFAULT_CONVENTIONS.features : config.conventions.features;
|
|
3883
|
+
const featureGlob = typeof featuresConvention === "string" ? featuresConvention : null;
|
|
3544
3884
|
const scannedPaths = new Set(files.map((f) => f.displayPath));
|
|
3545
3885
|
for (const ef of extracted) {
|
|
3546
3886
|
if (!ef.metadata) continue;
|
|
@@ -3549,10 +3889,24 @@ function audit(opts) {
|
|
|
3549
3889
|
if (typeof m.id !== "string") continue;
|
|
3550
3890
|
const filePath = ef.file.displayPath;
|
|
3551
3891
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
3552
|
-
if (
|
|
3553
|
-
const
|
|
3892
|
+
if (path7.posix.basename(filePath) === wellKnownName) continue;
|
|
3893
|
+
const featureDir = m.kind === "feature" && featureGlob !== null ? extractFeatureDir(filePath, featureGlob) : null;
|
|
3894
|
+
const dir = featureDir ?? path7.posix.dirname(filePath);
|
|
3554
3895
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
3555
3896
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
3897
|
+
const inConventionDir = featureDir !== null;
|
|
3898
|
+
if (inConventionDir) {
|
|
3899
|
+
diagnostics.push({
|
|
3900
|
+
code: "prefer-well-known-file",
|
|
3901
|
+
severity: "error",
|
|
3902
|
+
message: `Feature "${m.id}" metadata lives on ${filePath}; a feature in a convention-derived directory must declare its metadata in ${wellKnownPath}`,
|
|
3903
|
+
file: filePath,
|
|
3904
|
+
line: m.loc.line,
|
|
3905
|
+
entity: { kind: "feature", id: m.id },
|
|
3906
|
+
hint: `Move the \`export const uidex\` block to ${wellKnownPath} and remove it from ${filePath}.`
|
|
3907
|
+
});
|
|
3908
|
+
continue;
|
|
3909
|
+
}
|
|
3556
3910
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
3557
3911
|
diagnostics.push({
|
|
3558
3912
|
code: "prefer-well-known-file",
|
|
@@ -3636,6 +3990,7 @@ function audit(opts) {
|
|
|
3636
3990
|
const displayPath = ef.file.displayPath;
|
|
3637
3991
|
for (const imp of ef.imports ?? []) {
|
|
3638
3992
|
if (imp.isTypeOnly) continue;
|
|
3993
|
+
if (imp.dynamic) continue;
|
|
3639
3994
|
const baseName2 = imp.specifier.split("/").pop() ?? "";
|
|
3640
3995
|
const primitive = byName.get(
|
|
3641
3996
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
@@ -3753,7 +4108,7 @@ function audit(opts) {
|
|
|
3753
4108
|
severity: "warning",
|
|
3754
4109
|
message: `\`export const uidex\` in ${ef.file.displayPath} references unknown ${refKind} "${refId}"`,
|
|
3755
4110
|
file: ef.file.displayPath,
|
|
3756
|
-
line: spans?.[i] ?
|
|
4111
|
+
line: spans?.[i] ? lineOfOffset2(ef.file.content, spans[i].start) : m.loc.line,
|
|
3757
4112
|
hint: `No ${refKind} with id "${refId}" exists in the registry; fix the reference or add the ${refKind}.`
|
|
3758
4113
|
});
|
|
3759
4114
|
}
|
|
@@ -3800,13 +4155,21 @@ function audit(opts) {
|
|
|
3800
4155
|
};
|
|
3801
4156
|
return { diagnostics: governed, summary };
|
|
3802
4157
|
}
|
|
3803
|
-
function
|
|
4158
|
+
function lineOfOffset2(content, offset) {
|
|
3804
4159
|
let line = 1;
|
|
3805
4160
|
for (let i = 0; i < offset && i < content.length; i++) {
|
|
3806
4161
|
if (content[i] === "\n") line++;
|
|
3807
4162
|
}
|
|
3808
4163
|
return line;
|
|
3809
4164
|
}
|
|
4165
|
+
function entityCriteria(e) {
|
|
4166
|
+
const meta = e.meta;
|
|
4167
|
+
if (!meta) return [];
|
|
4168
|
+
return [
|
|
4169
|
+
...meta.acceptance ?? [],
|
|
4170
|
+
...(meta.states ?? []).flatMap((s) => s.acceptance ?? [])
|
|
4171
|
+
];
|
|
4172
|
+
}
|
|
3810
4173
|
var TAG_FALLBACK_ID = {
|
|
3811
4174
|
a: "link",
|
|
3812
4175
|
button: "button",
|
|
@@ -4251,10 +4614,16 @@ function emit(opts) {
|
|
|
4251
4614
|
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
4252
4615
|
);
|
|
4253
4616
|
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
4617
|
+
t.push(" export interface AcceptanceCriterion {");
|
|
4618
|
+
t.push(" id: string");
|
|
4619
|
+
t.push(" rev?: number");
|
|
4620
|
+
t.push(" text: string");
|
|
4621
|
+
t.push(" }");
|
|
4622
|
+
t.push(" export type Acceptance = readonly (string | AcceptanceCriterion)[]");
|
|
4254
4623
|
t.push(" export interface State {");
|
|
4255
4624
|
t.push(" name: string");
|
|
4256
4625
|
t.push(" kind?: StateKind");
|
|
4257
|
-
t.push(" acceptance?:
|
|
4626
|
+
t.push(" acceptance?: Acceptance");
|
|
4258
4627
|
t.push(" description?: string");
|
|
4259
4628
|
t.push(" }");
|
|
4260
4629
|
t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
|
|
@@ -4276,7 +4645,7 @@ function emit(opts) {
|
|
|
4276
4645
|
t.push(" name?: string");
|
|
4277
4646
|
t.push(" features?: readonly FeatureId[]");
|
|
4278
4647
|
t.push(" widgets?: readonly WidgetId[]");
|
|
4279
|
-
t.push(" acceptance?:
|
|
4648
|
+
t.push(" acceptance?: Acceptance");
|
|
4280
4649
|
t.push(" states?: readonly (string | State)[]");
|
|
4281
4650
|
t.push(" acknowledge?: Acknowledge");
|
|
4282
4651
|
t.push(" description?: string");
|
|
@@ -4285,7 +4654,8 @@ function emit(opts) {
|
|
|
4285
4654
|
t.push(" feature: FeatureId | false");
|
|
4286
4655
|
t.push(" name?: string");
|
|
4287
4656
|
t.push(" features?: readonly FeatureId[]");
|
|
4288
|
-
t.push("
|
|
4657
|
+
t.push(" surfaces?: readonly PageId[]");
|
|
4658
|
+
t.push(" acceptance?: Acceptance");
|
|
4289
4659
|
t.push(" states?: readonly (string | State)[]");
|
|
4290
4660
|
t.push(" acknowledge?: ComponentAcknowledge");
|
|
4291
4661
|
t.push(" description?: string");
|
|
@@ -4298,7 +4668,7 @@ function emit(opts) {
|
|
|
4298
4668
|
t.push(" export interface Widget {");
|
|
4299
4669
|
t.push(" widget: WidgetId");
|
|
4300
4670
|
t.push(" name?: string");
|
|
4301
|
-
t.push(" acceptance?:
|
|
4671
|
+
t.push(" acceptance?: Acceptance");
|
|
4302
4672
|
t.push(" states?: readonly (string | State)[]");
|
|
4303
4673
|
t.push(" acknowledge?: ComponentAcknowledge");
|
|
4304
4674
|
t.push(" description?: string");
|
|
@@ -4397,7 +4767,7 @@ function parseGitHubRef(ref) {
|
|
|
4397
4767
|
|
|
4398
4768
|
// src/scanner/scan/scaffold.ts
|
|
4399
4769
|
import * as fs4 from "fs";
|
|
4400
|
-
import * as
|
|
4770
|
+
import * as path8 from "path";
|
|
4401
4771
|
function scaffoldWidgetSpec(opts) {
|
|
4402
4772
|
return scaffoldSpec({
|
|
4403
4773
|
registry: opts.registry,
|
|
@@ -4421,9 +4791,9 @@ function scaffoldSpec(opts) {
|
|
|
4421
4791
|
if (!entity) {
|
|
4422
4792
|
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
4423
4793
|
}
|
|
4424
|
-
const criteria = entity
|
|
4794
|
+
const criteria = entityCriteria(entity);
|
|
4425
4795
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
4426
|
-
const outputPath =
|
|
4796
|
+
const outputPath = path8.resolve(outDir, filename);
|
|
4427
4797
|
if (fs4.existsSync(outputPath) && !force) {
|
|
4428
4798
|
return {
|
|
4429
4799
|
outputPath,
|
|
@@ -4432,8 +4802,8 @@ function scaffoldSpec(opts) {
|
|
|
4432
4802
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
4433
4803
|
};
|
|
4434
4804
|
}
|
|
4435
|
-
const content = renderSpec({ id, criteria, fixtureImport });
|
|
4436
|
-
fs4.mkdirSync(
|
|
4805
|
+
const content = renderSpec({ id, entityId: id, criteria, fixtureImport });
|
|
4806
|
+
fs4.mkdirSync(path8.dirname(outputPath), { recursive: true });
|
|
4437
4807
|
fs4.writeFileSync(outputPath, content, "utf8");
|
|
4438
4808
|
return { outputPath, written: true, skipped: false };
|
|
4439
4809
|
}
|
|
@@ -4460,8 +4830,9 @@ function renderSpec(args) {
|
|
|
4460
4830
|
]);
|
|
4461
4831
|
} else {
|
|
4462
4832
|
for (const criterion of args.criteria) {
|
|
4463
|
-
stub(criterion
|
|
4464
|
-
"// TODO: drive this criterion, then remove `.fixme` to arm the test
|
|
4833
|
+
stub(`${criterion.id}: ${criterion.text}`, [
|
|
4834
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test",
|
|
4835
|
+
`// and claim it: uidexCovers(${JSON.stringify(args.entityId)}, ${JSON.stringify(`${criterion.id}@${criterion.rev}`)})`
|
|
4465
4836
|
]);
|
|
4466
4837
|
}
|
|
4467
4838
|
}
|
|
@@ -4472,36 +4843,29 @@ function renderSpec(args) {
|
|
|
4472
4843
|
|
|
4473
4844
|
// src/scanner/scan/pipeline.ts
|
|
4474
4845
|
import * as fs5 from "fs";
|
|
4475
|
-
import * as
|
|
4846
|
+
import * as path9 from "path";
|
|
4476
4847
|
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
4477
4848
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
4478
4849
|
function loadCoverageBaseline(configDir, config) {
|
|
4479
4850
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4480
|
-
const abs =
|
|
4851
|
+
const abs = path9.resolve(configDir, rel);
|
|
4481
4852
|
let raw;
|
|
4482
4853
|
try {
|
|
4483
4854
|
raw = fs5.readFileSync(abs, "utf8");
|
|
4484
4855
|
} catch {
|
|
4485
|
-
return
|
|
4856
|
+
return void 0;
|
|
4486
4857
|
}
|
|
4487
4858
|
let parsed;
|
|
4488
4859
|
try {
|
|
4489
4860
|
parsed = JSON.parse(raw);
|
|
4490
4861
|
} catch {
|
|
4491
|
-
return
|
|
4862
|
+
return void 0;
|
|
4492
4863
|
}
|
|
4493
|
-
|
|
4494
|
-
path: abs,
|
|
4495
|
-
raw,
|
|
4496
|
-
parsed,
|
|
4497
|
-
displayPath: rel
|
|
4498
|
-
});
|
|
4499
|
-
if (legacy) return { legacy };
|
|
4500
|
-
return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
|
|
4864
|
+
return parseCoverageBaseline(parsed) ?? void 0;
|
|
4501
4865
|
}
|
|
4502
4866
|
function loadCapturedStates(configDir, config) {
|
|
4503
4867
|
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4504
|
-
const abs =
|
|
4868
|
+
const abs = path9.resolve(configDir, rel);
|
|
4505
4869
|
let raw;
|
|
4506
4870
|
try {
|
|
4507
4871
|
raw = fs5.readFileSync(abs, "utf8");
|
|
@@ -4559,11 +4923,11 @@ function runOne(dc, opts) {
|
|
|
4559
4923
|
gitContext
|
|
4560
4924
|
});
|
|
4561
4925
|
const typesRel = config.output;
|
|
4562
|
-
const typesPath =
|
|
4926
|
+
const typesPath = path9.resolve(configDir, typesRel);
|
|
4563
4927
|
const dataRel = dataOutputPath(config.output);
|
|
4564
|
-
const dataPath =
|
|
4928
|
+
const dataPath = path9.resolve(configDir, dataRel);
|
|
4565
4929
|
const locsRel = locsOutputPath(config.output);
|
|
4566
|
-
const locsPath =
|
|
4930
|
+
const locsPath = path9.resolve(configDir, locsRel);
|
|
4567
4931
|
let typesOnDisk = null;
|
|
4568
4932
|
let dataOnDisk = null;
|
|
4569
4933
|
let locsOnDisk = null;
|
|
@@ -4576,11 +4940,10 @@ function runOne(dc, opts) {
|
|
|
4576
4940
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
4577
4941
|
);
|
|
4578
4942
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
4579
|
-
const
|
|
4580
|
-
const coverageBaseline = loadedBaseline.baseline;
|
|
4943
|
+
const coverageBaseline = opts.coverageBaseline ?? loadCoverageBaseline(configDir, config);
|
|
4581
4944
|
const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
|
|
4582
4945
|
let auditResult;
|
|
4583
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics
|
|
4946
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4584
4947
|
auditResult = audit({
|
|
4585
4948
|
registry: resolved.registry,
|
|
4586
4949
|
extracted,
|
|
@@ -4604,7 +4967,6 @@ function runOne(dc, opts) {
|
|
|
4604
4967
|
locsOutputPath: locsRel,
|
|
4605
4968
|
capturedStates,
|
|
4606
4969
|
coverageBaseline,
|
|
4607
|
-
baselineDiagnostic: loadedBaseline.legacy,
|
|
4608
4970
|
workspace
|
|
4609
4971
|
});
|
|
4610
4972
|
}
|
|
@@ -4633,7 +4995,7 @@ function readOrNull(p2) {
|
|
|
4633
4995
|
}
|
|
4634
4996
|
function writeArtifact(artifact) {
|
|
4635
4997
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
4636
|
-
fs5.mkdirSync(
|
|
4998
|
+
fs5.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
|
|
4637
4999
|
fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
4638
5000
|
return true;
|
|
4639
5001
|
}
|
|
@@ -4645,7 +5007,7 @@ function writeScanResult(result) {
|
|
|
4645
5007
|
}
|
|
4646
5008
|
function coverageBaselinePath(result) {
|
|
4647
5009
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4648
|
-
return
|
|
5010
|
+
return path9.resolve(result.configDir, rel);
|
|
4649
5011
|
}
|
|
4650
5012
|
|
|
4651
5013
|
// src/scanner/scan/states-merge.ts
|
|
@@ -4681,7 +5043,7 @@ function mergeStates(committed, partial) {
|
|
|
4681
5043
|
|
|
4682
5044
|
// src/scanner/scan/fix.ts
|
|
4683
5045
|
import * as fs6 from "fs";
|
|
4684
|
-
import * as
|
|
5046
|
+
import * as path10 from "path";
|
|
4685
5047
|
function applyFixes(diagnostics) {
|
|
4686
5048
|
const entries = [];
|
|
4687
5049
|
for (const d of diagnostics) {
|
|
@@ -4740,10 +5102,10 @@ function applyFixes(diagnostics) {
|
|
|
4740
5102
|
if (entry.skippedReason) continue;
|
|
4741
5103
|
for (const create of entry.createFiles) {
|
|
4742
5104
|
if (fs6.existsSync(create.path)) {
|
|
4743
|
-
entry.skippedReason = `${
|
|
5105
|
+
entry.skippedReason = `${path10.basename(create.path)} already exists`;
|
|
4744
5106
|
continue;
|
|
4745
5107
|
}
|
|
4746
|
-
fs6.mkdirSync(
|
|
5108
|
+
fs6.mkdirSync(path10.dirname(create.path), { recursive: true });
|
|
4747
5109
|
fs6.writeFileSync(create.path, create.content, "utf8");
|
|
4748
5110
|
}
|
|
4749
5111
|
if (entry.skippedReason) continue;
|
|
@@ -4928,23 +5290,23 @@ function renameEntity(opts) {
|
|
|
4928
5290
|
|
|
4929
5291
|
// src/scanner/scan/cli.ts
|
|
4930
5292
|
import * as fs9 from "fs";
|
|
4931
|
-
import * as
|
|
5293
|
+
import * as path13 from "path";
|
|
4932
5294
|
|
|
4933
5295
|
// src/scanner/scan/ai/index.ts
|
|
4934
5296
|
import * as p from "@clack/prompts";
|
|
4935
5297
|
|
|
4936
5298
|
// src/scanner/scan/ai/providers/claude.ts
|
|
4937
5299
|
import * as fs8 from "fs";
|
|
4938
|
-
import * as
|
|
5300
|
+
import * as path12 from "path";
|
|
4939
5301
|
|
|
4940
5302
|
// src/scanner/scan/ai/templates.ts
|
|
4941
5303
|
import * as fs7 from "fs";
|
|
4942
|
-
import * as
|
|
5304
|
+
import * as path11 from "path";
|
|
4943
5305
|
function templatePath(rel) {
|
|
4944
5306
|
const candidates = [
|
|
4945
|
-
|
|
5307
|
+
path11.resolve(__dirname, "../../templates", rel),
|
|
4946
5308
|
// dist/cli/cli.cjs → ../../templates
|
|
4947
|
-
|
|
5309
|
+
path11.resolve(__dirname, "../../../../templates", rel)
|
|
4948
5310
|
// src/scanner/scan/ai → ../../../../templates
|
|
4949
5311
|
];
|
|
4950
5312
|
for (const c of candidates) {
|
|
@@ -4992,7 +5354,7 @@ var claudeProvider = {
|
|
|
4992
5354
|
async install({ cwd, force }) {
|
|
4993
5355
|
const changes = [];
|
|
4994
5356
|
for (const file of SKILL_FILES) {
|
|
4995
|
-
const dest =
|
|
5357
|
+
const dest = path12.join(cwd, file.dest);
|
|
4996
5358
|
const exists = fs8.existsSync(dest);
|
|
4997
5359
|
if (exists && !force) {
|
|
4998
5360
|
changes.push({
|
|
@@ -5002,7 +5364,7 @@ var claudeProvider = {
|
|
|
5002
5364
|
});
|
|
5003
5365
|
continue;
|
|
5004
5366
|
}
|
|
5005
|
-
fs8.mkdirSync(
|
|
5367
|
+
fs8.mkdirSync(path12.dirname(dest), { recursive: true });
|
|
5006
5368
|
fs8.writeFileSync(dest, readTemplate(file.template));
|
|
5007
5369
|
changes.push({
|
|
5008
5370
|
path: file.dest,
|
|
@@ -5010,21 +5372,21 @@ var claudeProvider = {
|
|
|
5010
5372
|
});
|
|
5011
5373
|
}
|
|
5012
5374
|
for (const rel of LEGACY_FILES) {
|
|
5013
|
-
const dest =
|
|
5375
|
+
const dest = path12.join(cwd, rel);
|
|
5014
5376
|
if (fs8.existsSync(dest)) {
|
|
5015
5377
|
fs8.unlinkSync(dest);
|
|
5016
5378
|
changes.push({ path: rel, action: "removed" });
|
|
5017
5379
|
}
|
|
5018
5380
|
}
|
|
5019
|
-
cleanupEmpty(
|
|
5020
|
-
cleanupEmpty(
|
|
5021
|
-
cleanupEmpty(
|
|
5381
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands/uidex"));
|
|
5382
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands"));
|
|
5383
|
+
cleanupEmpty(path12.join(cwd, ".claude/rules"));
|
|
5022
5384
|
return { changes };
|
|
5023
5385
|
},
|
|
5024
5386
|
async uninstall({ cwd }) {
|
|
5025
5387
|
const changes = [];
|
|
5026
5388
|
for (const file of SKILL_FILES) {
|
|
5027
|
-
const dest =
|
|
5389
|
+
const dest = path12.join(cwd, file.dest);
|
|
5028
5390
|
if (!fs8.existsSync(dest)) {
|
|
5029
5391
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
5030
5392
|
continue;
|
|
@@ -5032,19 +5394,19 @@ var claudeProvider = {
|
|
|
5032
5394
|
fs8.unlinkSync(dest);
|
|
5033
5395
|
changes.push({ path: file.dest, action: "removed" });
|
|
5034
5396
|
}
|
|
5035
|
-
cleanupEmpty(
|
|
5036
|
-
cleanupEmpty(
|
|
5037
|
-
cleanupEmpty(
|
|
5397
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills/uidex/references"));
|
|
5398
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills/uidex"));
|
|
5399
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills"));
|
|
5038
5400
|
for (const rel of LEGACY_FILES) {
|
|
5039
|
-
const dest =
|
|
5401
|
+
const dest = path12.join(cwd, rel);
|
|
5040
5402
|
if (fs8.existsSync(dest)) {
|
|
5041
5403
|
fs8.unlinkSync(dest);
|
|
5042
5404
|
changes.push({ path: rel, action: "removed" });
|
|
5043
5405
|
}
|
|
5044
5406
|
}
|
|
5045
|
-
cleanupEmpty(
|
|
5046
|
-
cleanupEmpty(
|
|
5047
|
-
cleanupEmpty(
|
|
5407
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands/uidex"));
|
|
5408
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands"));
|
|
5409
|
+
cleanupEmpty(path12.join(cwd, ".claude/rules"));
|
|
5048
5410
|
return { changes };
|
|
5049
5411
|
}
|
|
5050
5412
|
};
|
|
@@ -5257,7 +5619,7 @@ function helpText2() {
|
|
|
5257
5619
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
5258
5620
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
5259
5621
|
" --audit Equivalent to --check --lint (read-only)",
|
|
5260
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names
|
|
5622
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
5261
5623
|
" --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
|
|
5262
5624
|
" --json Emit JSON diagnostics on stdout",
|
|
5263
5625
|
" --force (scaffold) overwrite existing spec",
|
|
@@ -5265,7 +5627,7 @@ function helpText2() {
|
|
|
5265
5627
|
].join("\n");
|
|
5266
5628
|
}
|
|
5267
5629
|
function runInit(cwd, w) {
|
|
5268
|
-
const configPath =
|
|
5630
|
+
const configPath = path13.join(cwd, CONFIG_FILENAME);
|
|
5269
5631
|
if (fs9.existsSync(configPath)) {
|
|
5270
5632
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
5271
5633
|
return w.result(1);
|
|
@@ -5277,7 +5639,7 @@ function runInit(cwd, w) {
|
|
|
5277
5639
|
};
|
|
5278
5640
|
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
5279
5641
|
w.out(`Created ${configPath}`);
|
|
5280
|
-
const gitignorePath =
|
|
5642
|
+
const gitignorePath = path13.join(cwd, ".gitignore");
|
|
5281
5643
|
const entry = "*.gen.*";
|
|
5282
5644
|
if (fs9.existsSync(gitignorePath)) {
|
|
5283
5645
|
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
@@ -5335,7 +5697,7 @@ function runScanCommand(cwd, flags, w) {
|
|
|
5335
5697
|
const acknowledge = {};
|
|
5336
5698
|
const merge = (from) => {
|
|
5337
5699
|
for (const [route, scopes] of Object.entries(from)) {
|
|
5338
|
-
acknowledge[route] = { ...acknowledge[route]
|
|
5700
|
+
acknowledge[route] = { ...acknowledge[route], ...scopes };
|
|
5339
5701
|
}
|
|
5340
5702
|
};
|
|
5341
5703
|
merge(computePageBacklog(r.registry, manifest));
|
|
@@ -5423,7 +5785,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
5423
5785
|
for (const r of results) {
|
|
5424
5786
|
const entity = r.registry.get(scaffoldKind, id);
|
|
5425
5787
|
if (!entity) continue;
|
|
5426
|
-
const outDir =
|
|
5788
|
+
const outDir = path13.resolve(r.configDir, "e2e");
|
|
5427
5789
|
const result = scaffoldSpec({
|
|
5428
5790
|
registry: r.registry,
|
|
5429
5791
|
kind: scaffoldKind,
|
|
@@ -5458,8 +5820,8 @@ function runStates(cwd, args, w) {
|
|
|
5458
5820
|
let merged = 0;
|
|
5459
5821
|
for (const { configDir, config } of configs) {
|
|
5460
5822
|
const rel = config.statesManifest ?? "uidex-states.json";
|
|
5461
|
-
const target =
|
|
5462
|
-
const partial = explicitPartial ?
|
|
5823
|
+
const target = path13.resolve(configDir, rel);
|
|
5824
|
+
const partial = explicitPartial ? path13.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
|
|
5463
5825
|
const committedRaw = readJson(target);
|
|
5464
5826
|
const partialRaw = readJson(partial);
|
|
5465
5827
|
if (!partialRaw) continue;
|
|
@@ -5471,7 +5833,7 @@ function runStates(cwd, args, w) {
|
|
|
5471
5833
|
fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
|
|
5472
5834
|
merged++;
|
|
5473
5835
|
w.out(
|
|
5474
|
-
`Merged ${
|
|
5836
|
+
`Merged ${path13.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
|
|
5475
5837
|
);
|
|
5476
5838
|
for (const k of result.added) w.out(` + ${k}`);
|
|
5477
5839
|
for (const k of result.updated) w.out(` ~ ${k}`);
|
|
@@ -5563,13 +5925,9 @@ export {
|
|
|
5563
5925
|
extractUidexExports,
|
|
5564
5926
|
findWorkspaceRoot,
|
|
5565
5927
|
globToRegExp,
|
|
5566
|
-
isLegacyBaseline,
|
|
5567
5928
|
loadCoverageBaseline,
|
|
5568
5929
|
matchUrlToRoute,
|
|
5569
5930
|
mergeStates,
|
|
5570
|
-
migrateBaseline,
|
|
5571
|
-
migrateLegacyBaseline,
|
|
5572
|
-
migrateLegacyFields,
|
|
5573
5931
|
parseConfig,
|
|
5574
5932
|
parseCoverageBaseline,
|
|
5575
5933
|
pathToId,
|