uidex 0.10.0 → 0.11.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 +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 +4 -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 +3 -0
- package/dist/playwright/index.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 +1 -1
package/dist/scan/index.cjs
CHANGED
|
@@ -55,13 +55,9 @@ __export(scan_exports, {
|
|
|
55
55
|
extractUidexExports: () => extractUidexExports,
|
|
56
56
|
findWorkspaceRoot: () => findWorkspaceRoot,
|
|
57
57
|
globToRegExp: () => globToRegExp,
|
|
58
|
-
isLegacyBaseline: () => isLegacyBaseline,
|
|
59
58
|
loadCoverageBaseline: () => loadCoverageBaseline,
|
|
60
59
|
matchUrlToRoute: () => matchUrlToRoute,
|
|
61
60
|
mergeStates: () => mergeStates,
|
|
62
|
-
migrateBaseline: () => migrateBaseline,
|
|
63
|
-
migrateLegacyBaseline: () => migrateLegacyBaseline,
|
|
64
|
-
migrateLegacyFields: () => migrateLegacyFields,
|
|
65
61
|
parseConfig: () => parseConfig,
|
|
66
62
|
parseCoverageBaseline: () => parseCoverageBaseline,
|
|
67
63
|
pathToId: () => pathToId,
|
|
@@ -110,6 +106,7 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
110
106
|
"exclude",
|
|
111
107
|
"output",
|
|
112
108
|
"flows",
|
|
109
|
+
"resolve",
|
|
113
110
|
"audit",
|
|
114
111
|
"conventions",
|
|
115
112
|
"statesManifest",
|
|
@@ -128,24 +125,32 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
|
128
125
|
"scopeLeak",
|
|
129
126
|
"coverage",
|
|
130
127
|
"acceptance",
|
|
128
|
+
"surfaces",
|
|
129
|
+
"requireAcceptance",
|
|
131
130
|
"states",
|
|
132
131
|
"pageCoverage",
|
|
133
132
|
"stateMatrix",
|
|
134
133
|
"severity"
|
|
135
134
|
]);
|
|
136
135
|
var DIAGNOSTIC_CODES = [
|
|
136
|
+
"acceptance-ambiguous",
|
|
137
|
+
"acceptance-duplicate-id",
|
|
138
|
+
"acceptance-orphaned",
|
|
139
|
+
"acceptance-outdated",
|
|
137
140
|
"acceptance-uncovered",
|
|
138
141
|
"acknowledged-elsewhere",
|
|
139
142
|
"competing-uidex-export",
|
|
140
143
|
"component-state-uncaptured",
|
|
141
144
|
"core-kind",
|
|
145
|
+
"covers-missing-rev",
|
|
142
146
|
"duplicate-id",
|
|
143
147
|
"dynamic-attr",
|
|
144
148
|
"dynamic-flow-reference",
|
|
149
|
+
"feature-missing-acceptance",
|
|
150
|
+
"feature-surface-uncaptured",
|
|
151
|
+
"feature-surface-unreferenced",
|
|
145
152
|
"gen-missing",
|
|
146
153
|
"gen-stale",
|
|
147
|
-
"legacy-acknowledge-field",
|
|
148
|
-
"legacy-coverage-baseline",
|
|
149
154
|
"matrix-backlog",
|
|
150
155
|
"missing-element-annotation",
|
|
151
156
|
"missing-state-capture",
|
|
@@ -180,14 +185,14 @@ var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"
|
|
|
180
185
|
function fail(msg) {
|
|
181
186
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
182
187
|
}
|
|
183
|
-
function assertObject(value,
|
|
188
|
+
function assertObject(value, path14) {
|
|
184
189
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
185
|
-
fail(`${
|
|
190
|
+
fail(`${path14} must be an object`);
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
|
-
function assertStringArray(value,
|
|
193
|
+
function assertStringArray(value, path14) {
|
|
189
194
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
190
|
-
fail(`${
|
|
195
|
+
fail(`${path14} must be a string[]`);
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
function validateConfig(raw) {
|
|
@@ -244,6 +249,23 @@ function validateConfig(raw) {
|
|
|
244
249
|
if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
|
|
245
250
|
fail(`"workspace" must be a path string or false`);
|
|
246
251
|
}
|
|
252
|
+
if (raw.resolve !== void 0) {
|
|
253
|
+
assertObject(raw.resolve, "resolve");
|
|
254
|
+
for (const key of Object.keys(raw.resolve)) {
|
|
255
|
+
if (key !== "aliases") fail(`unknown key "${key}" in resolve`);
|
|
256
|
+
}
|
|
257
|
+
const aliases = raw.resolve.aliases;
|
|
258
|
+
if (aliases === void 0) {
|
|
259
|
+
fail(`resolve.aliases is required when "resolve" is present`);
|
|
260
|
+
}
|
|
261
|
+
assertObject(aliases, "resolve.aliases");
|
|
262
|
+
for (const [alias, target] of Object.entries(aliases)) {
|
|
263
|
+
if (alias.length === 0) fail(`resolve.aliases keys must be non-empty`);
|
|
264
|
+
if (typeof target !== "string") {
|
|
265
|
+
fail(`resolve.aliases["${alias}"] must be a string`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
247
269
|
if (raw.audit !== void 0) {
|
|
248
270
|
assertObject(raw.audit, "audit");
|
|
249
271
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -304,6 +326,7 @@ function validateConfig(raw) {
|
|
|
304
326
|
exclude: raw.exclude,
|
|
305
327
|
output: raw.output,
|
|
306
328
|
flows: raw.flows,
|
|
329
|
+
resolve: raw.resolve,
|
|
307
330
|
audit: raw.audit,
|
|
308
331
|
conventions: raw.conventions,
|
|
309
332
|
statesManifest: raw.statesManifest,
|
|
@@ -831,7 +854,6 @@ var CORE_SET = new Set(CORE_STATE_KINDS);
|
|
|
831
854
|
function isCoreKind(scope) {
|
|
832
855
|
return CORE_SET.has(scope);
|
|
833
856
|
}
|
|
834
|
-
var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
|
|
835
857
|
function isPlaceholderReason(reason) {
|
|
836
858
|
return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
|
|
837
859
|
}
|
|
@@ -909,28 +931,6 @@ function parseCoverageBaseline(raw) {
|
|
|
909
931
|
}
|
|
910
932
|
return { version: 2, acknowledge };
|
|
911
933
|
}
|
|
912
|
-
function isLegacyBaseline(raw) {
|
|
913
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
914
|
-
const r = raw;
|
|
915
|
-
if ("acknowledge" in r) return false;
|
|
916
|
-
return "uncapturedPages" in r || "missingKinds" in r;
|
|
917
|
-
}
|
|
918
|
-
function migrateBaseline(legacy) {
|
|
919
|
-
const acknowledge = {};
|
|
920
|
-
const at = (route) => acknowledge[route] ??= {};
|
|
921
|
-
for (const route of legacy.uncapturedPages ?? []) {
|
|
922
|
-
if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
|
|
923
|
-
}
|
|
924
|
-
for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
|
|
925
|
-
if (!Array.isArray(kinds)) continue;
|
|
926
|
-
for (const kind of kinds) {
|
|
927
|
-
if (typeof kind === "string" && isCoreKind(kind)) {
|
|
928
|
-
at(route)[kind] = { type: "backlog" };
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return { version: 2, acknowledge };
|
|
933
|
-
}
|
|
934
934
|
function serializeCoverageBaseline(baseline) {
|
|
935
935
|
const routes = Object.keys(baseline.acknowledge).sort();
|
|
936
936
|
const ordered = {};
|
|
@@ -964,24 +964,21 @@ var ALLOWED_FIELDS = {
|
|
|
964
964
|
"acceptance",
|
|
965
965
|
"states",
|
|
966
966
|
"acknowledge",
|
|
967
|
-
"capture",
|
|
968
|
-
"waivers",
|
|
969
967
|
"description"
|
|
970
968
|
]),
|
|
971
969
|
// A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
|
|
972
970
|
// and a core kind (one cell of its matrix). A feature/widget has no route, so
|
|
973
971
|
// the core-kind matrix is meaningless there and only `"*"` is accepted —
|
|
974
972
|
// giving a component a scoped opt-out instead of forcing you to delete its
|
|
975
|
-
// whole `states` block.
|
|
976
|
-
// only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
|
|
973
|
+
// whole `states` block.
|
|
977
974
|
feature: /* @__PURE__ */ new Set([
|
|
978
975
|
"feature",
|
|
979
976
|
"name",
|
|
980
977
|
"features",
|
|
978
|
+
"surfaces",
|
|
981
979
|
"acceptance",
|
|
982
980
|
"states",
|
|
983
981
|
"acknowledge",
|
|
984
|
-
"capture",
|
|
985
982
|
"description"
|
|
986
983
|
]),
|
|
987
984
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
@@ -991,20 +988,19 @@ var ALLOWED_FIELDS = {
|
|
|
991
988
|
"acceptance",
|
|
992
989
|
"states",
|
|
993
990
|
"acknowledge",
|
|
994
|
-
"capture",
|
|
995
991
|
"description"
|
|
996
992
|
]),
|
|
997
993
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
998
994
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
999
995
|
};
|
|
1000
996
|
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
997
|
+
var ACCEPTANCE_FIELDS = /* @__PURE__ */ new Set(["id", "rev", "text"]);
|
|
1001
998
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
1002
999
|
"name",
|
|
1003
1000
|
"kind",
|
|
1004
1001
|
"acceptance",
|
|
1005
1002
|
"description"
|
|
1006
1003
|
]);
|
|
1007
|
-
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
1008
1004
|
var STATE_KINDS = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
1009
1005
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
1010
1006
|
"page",
|
|
@@ -1370,7 +1366,8 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1370
1366
|
} else {
|
|
1371
1367
|
id = readIdField(idValue, kind, idField);
|
|
1372
1368
|
}
|
|
1373
|
-
const
|
|
1369
|
+
const criterionIds = /* @__PURE__ */ new Set();
|
|
1370
|
+
const acceptance = readAcceptanceField(byKey, criterionIds);
|
|
1374
1371
|
const description = readStringField(byKey, "description");
|
|
1375
1372
|
const name = readStringField(byKey, "name");
|
|
1376
1373
|
if (name === "") {
|
|
@@ -1396,11 +1393,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1396
1393
|
}
|
|
1397
1394
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1398
1395
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1399
|
-
const
|
|
1396
|
+
const surfacesField = kind === "feature" ? readStringArrayField(byKey, "surfaces") : void 0;
|
|
1397
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey, criterionIds) : void 0;
|
|
1400
1398
|
const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
|
|
1401
1399
|
const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
|
|
1402
|
-
const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
|
|
1403
|
-
const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
1404
1400
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1405
1401
|
const metadata = {
|
|
1406
1402
|
source: "ts-export",
|
|
@@ -1423,14 +1419,14 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1423
1419
|
metadata.widgets = widgetsField.values;
|
|
1424
1420
|
metadata.widgetSpans = widgetsField.spans;
|
|
1425
1421
|
}
|
|
1422
|
+
if (surfacesField) {
|
|
1423
|
+
metadata.surfaces = surfacesField.values;
|
|
1424
|
+
metadata.surfaceSpans = surfacesField.spans;
|
|
1425
|
+
}
|
|
1426
1426
|
if (states?.length) metadata.states = states;
|
|
1427
1427
|
if (acknowledge && Object.keys(acknowledge).length > 0) {
|
|
1428
1428
|
metadata.acknowledge = acknowledge;
|
|
1429
1429
|
}
|
|
1430
|
-
if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
|
|
1431
|
-
if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
|
|
1432
|
-
metadata.legacyWaivers = legacyWaivers;
|
|
1433
|
-
}
|
|
1434
1430
|
if (notFlow) metadata.notFlow = true;
|
|
1435
1431
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1436
1432
|
metadata.idSpan = idValue.span;
|
|
@@ -1546,47 +1542,111 @@ function readAcknowledgeField(byKey, isPage) {
|
|
|
1546
1542
|
}
|
|
1547
1543
|
return out2;
|
|
1548
1544
|
}
|
|
1549
|
-
function
|
|
1550
|
-
|
|
1545
|
+
function acceptanceSlug(text) {
|
|
1546
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).slice(0, 8).join("-");
|
|
1547
|
+
}
|
|
1548
|
+
function readAcceptanceField(byKey, seen) {
|
|
1549
|
+
const entry = byKey.get("acceptance");
|
|
1551
1550
|
if (!entry) return void 0;
|
|
1552
|
-
|
|
1551
|
+
return readAcceptanceItems(entry, seen);
|
|
1552
|
+
}
|
|
1553
|
+
function readAcceptanceItems(entry, seen) {
|
|
1554
|
+
if (entry.value.kind !== "array") {
|
|
1553
1555
|
throw new ExtractError(
|
|
1554
1556
|
"uidex-export-invalid-field",
|
|
1555
|
-
"`
|
|
1557
|
+
"`acceptance` must be an array of strings or `{ id, rev?, text }` objects.",
|
|
1556
1558
|
entry.value.pos
|
|
1557
1559
|
);
|
|
1558
1560
|
}
|
|
1559
|
-
const
|
|
1560
|
-
for (const
|
|
1561
|
-
|
|
1561
|
+
const criteria = [];
|
|
1562
|
+
for (const item of entry.value.items) {
|
|
1563
|
+
let criterion;
|
|
1564
|
+
if (item.kind === "string") {
|
|
1565
|
+
if (item.value.length === 0) {
|
|
1566
|
+
throw new ExtractError(
|
|
1567
|
+
"uidex-export-invalid-field",
|
|
1568
|
+
"An `acceptance` entry is an empty string.",
|
|
1569
|
+
item.pos
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const id = acceptanceSlug(item.value);
|
|
1573
|
+
if (!id) {
|
|
1574
|
+
throw new ExtractError(
|
|
1575
|
+
"uidex-export-invalid-field",
|
|
1576
|
+
`Acceptance text "${item.value}" contains no alphanumeric characters to derive an id from; use the object form with an explicit \`id\`.`,
|
|
1577
|
+
item.pos
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
criterion = { id, rev: 1, text: item.value };
|
|
1581
|
+
} else if (item.kind === "object") {
|
|
1582
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1583
|
+
for (const e of item.entries) {
|
|
1584
|
+
if (!ACCEPTANCE_FIELDS.has(e.key)) {
|
|
1585
|
+
throw new ExtractError(
|
|
1586
|
+
"uidex-export-invalid-field",
|
|
1587
|
+
`Unknown field "${e.key}" in an \`acceptance\` entry. Allowed: ${[
|
|
1588
|
+
...ACCEPTANCE_FIELDS
|
|
1589
|
+
].sort().join(", ")}.`,
|
|
1590
|
+
e.value.pos
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
fieldByKey.set(e.key, e);
|
|
1594
|
+
}
|
|
1595
|
+
const id = readStringField(fieldByKey, "id");
|
|
1596
|
+
if (id === void 0 || id.length === 0) {
|
|
1597
|
+
throw new ExtractError(
|
|
1598
|
+
"uidex-export-invalid-field",
|
|
1599
|
+
"An object `acceptance` entry needs a non-empty string `id`.",
|
|
1600
|
+
item.pos
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (id.includes("@")) {
|
|
1604
|
+
throw new ExtractError(
|
|
1605
|
+
"uidex-export-invalid-field",
|
|
1606
|
+
`acceptance id "${id}" must not contain "@" \u2014 it is reserved for the \`@<rev>\` pin in \`uidexCovers\` claims.`,
|
|
1607
|
+
item.pos
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const text = readStringField(fieldByKey, "text");
|
|
1611
|
+
if (text === void 0 || text.length === 0) {
|
|
1612
|
+
throw new ExtractError(
|
|
1613
|
+
"uidex-export-invalid-field",
|
|
1614
|
+
`acceptance "${id}" needs a non-empty string \`text\`.`,
|
|
1615
|
+
item.pos
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
let rev = 1;
|
|
1619
|
+
const revEntry = fieldByKey.get("rev");
|
|
1620
|
+
if (revEntry) {
|
|
1621
|
+
if (revEntry.value.kind !== "number" || !Number.isInteger(revEntry.value.value) || revEntry.value.value < 1) {
|
|
1622
|
+
throw new ExtractError(
|
|
1623
|
+
"uidex-export-invalid-field",
|
|
1624
|
+
`acceptance "${id}" \`rev\` must be a positive integer.`,
|
|
1625
|
+
revEntry.value.pos
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
rev = revEntry.value.value;
|
|
1629
|
+
}
|
|
1630
|
+
criterion = { id, rev, text };
|
|
1631
|
+
} else {
|
|
1562
1632
|
throw new ExtractError(
|
|
1563
1633
|
"uidex-export-invalid-field",
|
|
1564
|
-
`
|
|
1565
|
-
|
|
1634
|
+
"`acceptance` items must be strings or `{ id, rev?, text }` objects.",
|
|
1635
|
+
item.pos
|
|
1566
1636
|
);
|
|
1567
1637
|
}
|
|
1568
|
-
if (
|
|
1638
|
+
if (seen.has(criterion.id)) {
|
|
1569
1639
|
throw new ExtractError(
|
|
1570
|
-
"
|
|
1571
|
-
`
|
|
1572
|
-
|
|
1640
|
+
"acceptance-duplicate-id",
|
|
1641
|
+
`Duplicate acceptance criterion id "${criterion.id}"; each criterion id must be unique within the entity.`,
|
|
1642
|
+
item.pos,
|
|
1643
|
+
"Give the criterion an explicit distinct `id` (object form) \u2014 two string criteria can slug to the same id."
|
|
1573
1644
|
);
|
|
1574
1645
|
}
|
|
1575
|
-
|
|
1646
|
+
seen.add(criterion.id);
|
|
1647
|
+
criteria.push(criterion);
|
|
1576
1648
|
}
|
|
1577
|
-
return
|
|
1578
|
-
}
|
|
1579
|
-
function readBooleanField(byKey, name) {
|
|
1580
|
-
const entry = byKey.get(name);
|
|
1581
|
-
if (!entry) return void 0;
|
|
1582
|
-
if (entry.value.kind !== "boolean") {
|
|
1583
|
-
throw new ExtractError(
|
|
1584
|
-
"uidex-export-invalid-field",
|
|
1585
|
-
`\`${name}\` must be a boolean.`,
|
|
1586
|
-
entry.value.pos
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
return entry.value.value;
|
|
1649
|
+
return criteria;
|
|
1590
1650
|
}
|
|
1591
1651
|
function readStringField(byKey, name) {
|
|
1592
1652
|
const entry = byKey.get(name);
|
|
@@ -1625,7 +1685,7 @@ function readStringArrayField(byKey, name) {
|
|
|
1625
1685
|
}
|
|
1626
1686
|
return { values, spans };
|
|
1627
1687
|
}
|
|
1628
|
-
function readStatesField(byKey) {
|
|
1688
|
+
function readStatesField(byKey, criterionIds) {
|
|
1629
1689
|
const entry = byKey.get("states");
|
|
1630
1690
|
if (!entry) return void 0;
|
|
1631
1691
|
if (entry.value.kind !== "array") {
|
|
@@ -1675,8 +1735,11 @@ function readStatesField(byKey) {
|
|
|
1675
1735
|
}
|
|
1676
1736
|
state.kind = kind;
|
|
1677
1737
|
}
|
|
1678
|
-
const
|
|
1679
|
-
if (
|
|
1738
|
+
const accEntry = fieldByKey.get("acceptance");
|
|
1739
|
+
if (accEntry) {
|
|
1740
|
+
const acceptance = readAcceptanceItems(accEntry, criterionIds);
|
|
1741
|
+
if (acceptance.length > 0) state.acceptance = acceptance;
|
|
1742
|
+
}
|
|
1680
1743
|
const description = readStringField(fieldByKey, "description");
|
|
1681
1744
|
if (description) state.description = description;
|
|
1682
1745
|
} else {
|
|
@@ -1732,20 +1795,21 @@ function isNode2(value) {
|
|
|
1732
1795
|
}
|
|
1733
1796
|
|
|
1734
1797
|
// src/scanner/scan/flow-facts.ts
|
|
1735
|
-
function collectFlowFacts(parsed, content) {
|
|
1798
|
+
function collectFlowFacts(parsed, content, displayPath) {
|
|
1736
1799
|
if (parsed.program === null || !content.includes("test.describe")) {
|
|
1737
|
-
return [];
|
|
1800
|
+
return { facts: [], diagnostics: [] };
|
|
1738
1801
|
}
|
|
1739
1802
|
const facts = [];
|
|
1803
|
+
const diagnostics = [];
|
|
1740
1804
|
walkAst(parsed.program, (node) => {
|
|
1741
1805
|
if (node.type !== "CallExpression") return void 0;
|
|
1742
|
-
const fact = readTaggedDescribe(node, parsed);
|
|
1806
|
+
const fact = readTaggedDescribe(node, parsed, diagnostics, displayPath);
|
|
1743
1807
|
if (fact) facts.push(fact);
|
|
1744
1808
|
return void 0;
|
|
1745
1809
|
});
|
|
1746
|
-
return facts;
|
|
1810
|
+
return { facts, diagnostics };
|
|
1747
1811
|
}
|
|
1748
|
-
function readTaggedDescribe(call, parsed) {
|
|
1812
|
+
function readTaggedDescribe(call, parsed, diagnostics, displayPath) {
|
|
1749
1813
|
const callee = call.callee;
|
|
1750
1814
|
if (!callee || callee.type !== "MemberExpression" || !isIdentifier(callee.object, "test") || !isIdentifier(callee.property, "describe")) {
|
|
1751
1815
|
return null;
|
|
@@ -1756,13 +1820,83 @@ function readTaggedDescribe(call, parsed) {
|
|
|
1756
1820
|
if (!hasFlowTag(args[1])) return null;
|
|
1757
1821
|
const body = args[2];
|
|
1758
1822
|
const { calls, dynamicCalls } = body ? collectUidexCalls(body, parsed) : { calls: [], dynamicCalls: [] };
|
|
1823
|
+
const covers = body ? collectCoversClaims(body, parsed, title, diagnostics, displayPath) : [];
|
|
1759
1824
|
return {
|
|
1760
1825
|
title,
|
|
1761
1826
|
line: parsed.lineAt(call.start),
|
|
1762
1827
|
calls,
|
|
1763
|
-
...dynamicCalls.length > 0 ? { dynamicCalls } : {}
|
|
1828
|
+
...dynamicCalls.length > 0 ? { dynamicCalls } : {},
|
|
1829
|
+
...covers.length > 0 ? { covers } : {}
|
|
1764
1830
|
};
|
|
1765
1831
|
}
|
|
1832
|
+
function collectCoversClaims(body, parsed, flowTitle, diagnostics, displayPath) {
|
|
1833
|
+
const covers = [];
|
|
1834
|
+
walkAst(body, (node) => {
|
|
1835
|
+
if (node.type !== "CallExpression") return void 0;
|
|
1836
|
+
if (!isIdentifier(node.callee, "uidexCovers")) return void 0;
|
|
1837
|
+
const line = parsed.lineAt(node.start);
|
|
1838
|
+
const args = node.arguments ?? [];
|
|
1839
|
+
const entity = stringLiteralValue(args[0]);
|
|
1840
|
+
if (entity === null) {
|
|
1841
|
+
diagnostics.push({
|
|
1842
|
+
code: "covers-missing-rev",
|
|
1843
|
+
severity: "error",
|
|
1844
|
+
message: `\`uidexCovers(\u2026)\` in flow "${flowTitle}" needs a static string entity id as its first argument.`,
|
|
1845
|
+
file: displayPath,
|
|
1846
|
+
line,
|
|
1847
|
+
hint: 'Write `uidexCovers("<entityId>", "<criterionId>@<rev>")` with string literals.'
|
|
1848
|
+
});
|
|
1849
|
+
return void 0;
|
|
1850
|
+
}
|
|
1851
|
+
if (args.length < 2) {
|
|
1852
|
+
diagnostics.push({
|
|
1853
|
+
code: "covers-missing-rev",
|
|
1854
|
+
severity: "error",
|
|
1855
|
+
message: `\`uidexCovers("${entity}")\` in flow "${flowTitle}" names no criterion; a claim covers nothing without at least one \`"<criterionId>@<rev>"\` argument.`,
|
|
1856
|
+
file: displayPath,
|
|
1857
|
+
line,
|
|
1858
|
+
hint: 'List the criteria this flow verifies: `uidexCovers("<entityId>", "<criterionId>@<rev>", ...)`.'
|
|
1859
|
+
});
|
|
1860
|
+
return void 0;
|
|
1861
|
+
}
|
|
1862
|
+
for (const arg of args.slice(1)) {
|
|
1863
|
+
const claimLine = parsed.lineAt(arg.start);
|
|
1864
|
+
const raw = stringLiteralValue(arg);
|
|
1865
|
+
const at = raw === null ? -1 : raw.lastIndexOf("@");
|
|
1866
|
+
if (raw === null || at <= 0) {
|
|
1867
|
+
diagnostics.push({
|
|
1868
|
+
code: "covers-missing-rev",
|
|
1869
|
+
severity: "error",
|
|
1870
|
+
message: `\`uidexCovers(\u2026)\` claim ${raw === null ? "" : `"${raw}" `}in flow "${flowTitle}" is missing its \`@<rev>\` pin.`,
|
|
1871
|
+
file: displayPath,
|
|
1872
|
+
line: claimLine,
|
|
1873
|
+
hint: 'Pin the criterion rev \u2014 `uidexCovers("<entityId>", "<criterionId>@<rev>")` \u2014 so a later rev bump surfaces this claim as outdated.'
|
|
1874
|
+
});
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const revStr = raw.slice(at + 1);
|
|
1878
|
+
if (!/^[1-9]\d*$/.test(revStr)) {
|
|
1879
|
+
diagnostics.push({
|
|
1880
|
+
code: "covers-missing-rev",
|
|
1881
|
+
severity: "error",
|
|
1882
|
+
message: `\`uidexCovers(\u2026)\` claim "${raw}" in flow "${flowTitle}" has an invalid rev "@${revStr}"; the rev must be a positive integer (no leading zeros).`,
|
|
1883
|
+
file: displayPath,
|
|
1884
|
+
line: claimLine,
|
|
1885
|
+
hint: 'Write the pin as `"<criterionId>@<rev>"` with a positive integer rev, e.g. `"gated@1"`.'
|
|
1886
|
+
});
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
covers.push({
|
|
1890
|
+
entity,
|
|
1891
|
+
criterion: raw.slice(0, at),
|
|
1892
|
+
rev: Number(revStr),
|
|
1893
|
+
line: claimLine
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
return void 0;
|
|
1897
|
+
});
|
|
1898
|
+
return covers;
|
|
1899
|
+
}
|
|
1766
1900
|
function hasFlowTag(node) {
|
|
1767
1901
|
if (!node || node.type !== "ObjectExpression") return false;
|
|
1768
1902
|
for (const prop of node.properties ?? []) {
|
|
@@ -2154,11 +2288,13 @@ function extract(files) {
|
|
|
2154
2288
|
const out2 = { file, annotations: [] };
|
|
2155
2289
|
out2.annotations = extractOne(file, parsed, out2);
|
|
2156
2290
|
if (exports2.length > 0) out2.metadata = exports2;
|
|
2291
|
+
const flowFacts = collectFlowFacts(parsed, file.content, file.displayPath);
|
|
2292
|
+
diagnostics.push(...flowFacts.diagnostics);
|
|
2157
2293
|
if (diagnostics.length > 0) out2.diagnostics = diagnostics;
|
|
2158
|
-
|
|
2159
|
-
if (flows.length > 0) out2.flows = flows;
|
|
2294
|
+
if (flowFacts.facts.length > 0) out2.flows = flowFacts.facts;
|
|
2160
2295
|
const imports = collectImportFacts(parsed);
|
|
2161
2296
|
if (imports.length > 0) out2.imports = imports;
|
|
2297
|
+
if (containsJsx(parsed)) out2.hasJsx = true;
|
|
2162
2298
|
return out2;
|
|
2163
2299
|
});
|
|
2164
2300
|
}
|
|
@@ -2183,6 +2319,32 @@ function extractOne(file, parsed, out2) {
|
|
|
2183
2319
|
}
|
|
2184
2320
|
return annotations;
|
|
2185
2321
|
}
|
|
2322
|
+
function containsJsx(parsed) {
|
|
2323
|
+
if (parsed.program === null) return false;
|
|
2324
|
+
let found = false;
|
|
2325
|
+
walkAst(parsed.program, (node) => {
|
|
2326
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
2327
|
+
found = true;
|
|
2328
|
+
return false;
|
|
2329
|
+
}
|
|
2330
|
+
if (node.type === "CallExpression" && isCreateElementCallee(node.callee)) {
|
|
2331
|
+
found = true;
|
|
2332
|
+
return false;
|
|
2333
|
+
}
|
|
2334
|
+
return void 0;
|
|
2335
|
+
});
|
|
2336
|
+
return found;
|
|
2337
|
+
}
|
|
2338
|
+
function isCreateElementCallee(callee) {
|
|
2339
|
+
const node = callee;
|
|
2340
|
+
if (!node) return false;
|
|
2341
|
+
if (node.type === "Identifier") return String(node.name) === "createElement";
|
|
2342
|
+
if (node.type === "MemberExpression") {
|
|
2343
|
+
const prop = node.property;
|
|
2344
|
+
return prop?.type === "Identifier" && String(prop.name) === "createElement";
|
|
2345
|
+
}
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2186
2348
|
function collectImportFacts(parsed) {
|
|
2187
2349
|
if (parsed.program === null) return [];
|
|
2188
2350
|
const out2 = [];
|
|
@@ -2215,6 +2377,21 @@ function collectImportFacts(parsed) {
|
|
|
2215
2377
|
names
|
|
2216
2378
|
});
|
|
2217
2379
|
}
|
|
2380
|
+
walkAst(parsed.program, (node) => {
|
|
2381
|
+
if (node.type !== "ImportExpression") return void 0;
|
|
2382
|
+
const source = node.source;
|
|
2383
|
+
if (!source || source.type !== "Literal") return void 0;
|
|
2384
|
+
if (typeof source.value !== "string") return void 0;
|
|
2385
|
+
out2.push({
|
|
2386
|
+
specifier: source.value,
|
|
2387
|
+
line: parsed.lineAt(node.start),
|
|
2388
|
+
span: { start: node.start, end: node.end },
|
|
2389
|
+
isTypeOnly: false,
|
|
2390
|
+
names: [],
|
|
2391
|
+
dynamic: true
|
|
2392
|
+
});
|
|
2393
|
+
return void 0;
|
|
2394
|
+
});
|
|
2218
2395
|
return out2;
|
|
2219
2396
|
}
|
|
2220
2397
|
|
|
@@ -2386,6 +2563,7 @@ function buildMetaFromExport(exp) {
|
|
|
2386
2563
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
2387
2564
|
if (exp.features?.length) meta.features = exp.features;
|
|
2388
2565
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
2566
|
+
if (exp.surfaces?.length) meta.surfaces = exp.surfaces;
|
|
2389
2567
|
if (exp.states?.length) {
|
|
2390
2568
|
meta.states = exp.states.map((s) => ({
|
|
2391
2569
|
name: s.name,
|
|
@@ -2799,7 +2977,8 @@ function resolve2(ctx) {
|
|
|
2799
2977
|
id: flowExport.id,
|
|
2800
2978
|
loc: base.loc,
|
|
2801
2979
|
touches: base.touches,
|
|
2802
|
-
steps: base.steps
|
|
2980
|
+
steps: base.steps,
|
|
2981
|
+
...base.covers ? { covers: base.covers } : {}
|
|
2803
2982
|
};
|
|
2804
2983
|
registry.add(flow);
|
|
2805
2984
|
} else {
|
|
@@ -2847,20 +3026,42 @@ function computeScope(displayPath) {
|
|
|
2847
3026
|
return null;
|
|
2848
3027
|
}
|
|
2849
3028
|
function flowsFromFacts(ff) {
|
|
2850
|
-
return (ff.flows ?? []).map((fact) =>
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
3029
|
+
return (ff.flows ?? []).map((fact) => {
|
|
3030
|
+
const covers = dedupeBy(
|
|
3031
|
+
(fact.covers ?? []).map((c) => ({
|
|
3032
|
+
entity: c.entity,
|
|
3033
|
+
criterion: c.criterion,
|
|
3034
|
+
rev: c.rev
|
|
3035
|
+
})),
|
|
3036
|
+
(c) => `${c.entity} ${c.criterion} ${c.rev}`
|
|
3037
|
+
);
|
|
3038
|
+
return {
|
|
3039
|
+
kind: "flow",
|
|
3040
|
+
id: kebab(fact.title),
|
|
3041
|
+
loc: { file: ff.file.displayPath, line: fact.line },
|
|
3042
|
+
touches: dedupe(fact.calls.map((c) => c.id)),
|
|
3043
|
+
steps: fact.calls.filter((c) => c.action).map((c) => ({ entityId: c.id, action: c.action })),
|
|
3044
|
+
...covers.length > 0 ? { covers } : {}
|
|
3045
|
+
};
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
function dedupeBy(arr, key) {
|
|
3049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3050
|
+
const out2 = [];
|
|
3051
|
+
for (const item of arr) {
|
|
3052
|
+
const k = key(item);
|
|
3053
|
+
if (seen.has(k)) continue;
|
|
3054
|
+
seen.add(k);
|
|
3055
|
+
out2.push(item);
|
|
3056
|
+
}
|
|
3057
|
+
return out2;
|
|
2857
3058
|
}
|
|
2858
3059
|
function dedupe(arr) {
|
|
2859
3060
|
return Array.from(new Set(arr));
|
|
2860
3061
|
}
|
|
2861
3062
|
|
|
2862
3063
|
// src/scanner/scan/audit.ts
|
|
2863
|
-
var
|
|
3064
|
+
var path7 = __toESM(require("path"), 1);
|
|
2864
3065
|
|
|
2865
3066
|
// src/scanner/scan/component-coverage.ts
|
|
2866
3067
|
var COMPOSED_KINDS = ["widget", "feature"];
|
|
@@ -2915,141 +3116,6 @@ function checkComponentCoverage(registry, manifest) {
|
|
|
2915
3116
|
return diagnostics;
|
|
2916
3117
|
}
|
|
2917
3118
|
|
|
2918
|
-
// src/scanner/scan/migrate-acknowledge.ts
|
|
2919
|
-
var CORE = CORE_STATE_KINDS;
|
|
2920
|
-
function orderScopes(scopes) {
|
|
2921
|
-
const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
|
|
2922
|
-
return [...scopes].sort((a, b) => rank(a) - rank(b));
|
|
2923
|
-
}
|
|
2924
|
-
function renderKey(scope) {
|
|
2925
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
|
|
2926
|
-
}
|
|
2927
|
-
function renderAck(a) {
|
|
2928
|
-
const parts = [`type: ${JSON.stringify(a.type)}`];
|
|
2929
|
-
if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
|
|
2930
|
-
return `{ ${parts.join(", ")} }`;
|
|
2931
|
-
}
|
|
2932
|
-
function renderAcknowledge(entries, indent) {
|
|
2933
|
-
const inner = indent + " ";
|
|
2934
|
-
const lines = orderScopes(Object.keys(entries)).map(
|
|
2935
|
-
(scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
|
|
2936
|
-
);
|
|
2937
|
-
return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
|
|
2938
|
-
}
|
|
2939
|
-
function indentAt(content, offset) {
|
|
2940
|
-
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2941
|
-
const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
|
|
2942
|
-
return m ? m[0] : "";
|
|
2943
|
-
}
|
|
2944
|
-
function plannedEntries(m) {
|
|
2945
|
-
const entries = {};
|
|
2946
|
-
if (m.legacyCapture === false) {
|
|
2947
|
-
entries[ACK_ALL] = {
|
|
2948
|
-
type: "impossible",
|
|
2949
|
-
reason: m.description || PLACEHOLDER_REASON
|
|
2950
|
-
};
|
|
2951
|
-
}
|
|
2952
|
-
for (const kind of CORE) {
|
|
2953
|
-
const reason = m.legacyWaivers?.[kind];
|
|
2954
|
-
if (reason) entries[kind] = { type: "impossible", reason };
|
|
2955
|
-
}
|
|
2956
|
-
return entries;
|
|
2957
|
-
}
|
|
2958
|
-
function migrateLegacyFields(ef) {
|
|
2959
|
-
const out2 = [];
|
|
2960
|
-
const { content, sourcePath, displayPath } = ef.file;
|
|
2961
|
-
for (const m of ef.metadata ?? []) {
|
|
2962
|
-
const legacy = [];
|
|
2963
|
-
if (m.legacyWaivers) legacy.push("waivers");
|
|
2964
|
-
if (m.legacyCapture !== void 0) legacy.push("capture");
|
|
2965
|
-
if (legacy.length === 0) continue;
|
|
2966
|
-
const fields = legacy.join(" and ");
|
|
2967
|
-
const base = {
|
|
2968
|
-
severity: "error",
|
|
2969
|
-
code: "legacy-acknowledge-field",
|
|
2970
|
-
file: displayPath,
|
|
2971
|
-
line: m.loc.line
|
|
2972
|
-
};
|
|
2973
|
-
if (m.acknowledge) {
|
|
2974
|
-
out2.push({
|
|
2975
|
-
...base,
|
|
2976
|
-
message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
|
|
2977
|
-
hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
|
|
2978
|
-
});
|
|
2979
|
-
continue;
|
|
2980
|
-
}
|
|
2981
|
-
const entries = plannedEntries(m);
|
|
2982
|
-
const edits = [];
|
|
2983
|
-
const present = legacy.map((f) => ({ f, prop: m.fieldPropSpans?.[f], del: m.fieldSpans?.[f] })).filter((x) => x.prop && x.del).sort((a, b) => a.prop.start - b.prop.start);
|
|
2984
|
-
if (present.length !== legacy.length) {
|
|
2985
|
-
out2.push({
|
|
2986
|
-
...base,
|
|
2987
|
-
message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
|
|
2988
|
-
hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
|
|
2989
|
-
});
|
|
2990
|
-
continue;
|
|
2991
|
-
}
|
|
2992
|
-
const [anchor, ...rest] = present;
|
|
2993
|
-
if (Object.keys(entries).length === 0) {
|
|
2994
|
-
edits.push({
|
|
2995
|
-
path: sourcePath,
|
|
2996
|
-
start: anchor.del.start,
|
|
2997
|
-
end: anchor.del.end,
|
|
2998
|
-
replacement: ""
|
|
2999
|
-
});
|
|
3000
|
-
} else {
|
|
3001
|
-
edits.push({
|
|
3002
|
-
path: sourcePath,
|
|
3003
|
-
start: anchor.prop.start,
|
|
3004
|
-
end: anchor.prop.end,
|
|
3005
|
-
replacement: renderAcknowledge(
|
|
3006
|
-
entries,
|
|
3007
|
-
indentAt(content, anchor.prop.start)
|
|
3008
|
-
)
|
|
3009
|
-
});
|
|
3010
|
-
}
|
|
3011
|
-
for (const r of rest) {
|
|
3012
|
-
edits.push({
|
|
3013
|
-
path: sourcePath,
|
|
3014
|
-
start: r.del.start,
|
|
3015
|
-
end: r.del.end,
|
|
3016
|
-
replacement: ""
|
|
3017
|
-
});
|
|
3018
|
-
}
|
|
3019
|
-
out2.push({
|
|
3020
|
-
...base,
|
|
3021
|
-
message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
|
|
3022
|
-
hint: `Run \`uidex scan --fix\` to rewrite it${m.legacyCapture === false && !m.description ? ", then replace the TODO reason it plants \u2014 `capture: false` carried no explanation and `impossible` requires one" : ""}.`,
|
|
3023
|
-
fix: {
|
|
3024
|
-
description: `Rewrite \`${fields}\` as \`acknowledge\``,
|
|
3025
|
-
edits
|
|
3026
|
-
}
|
|
3027
|
-
});
|
|
3028
|
-
}
|
|
3029
|
-
return out2;
|
|
3030
|
-
}
|
|
3031
|
-
function migrateLegacyBaseline(opts) {
|
|
3032
|
-
if (!isLegacyBaseline(opts.parsed)) return void 0;
|
|
3033
|
-
return {
|
|
3034
|
-
code: "legacy-coverage-baseline",
|
|
3035
|
-
severity: "error",
|
|
3036
|
-
message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
|
|
3037
|
-
file: opts.displayPath,
|
|
3038
|
-
hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
|
|
3039
|
-
fix: {
|
|
3040
|
-
description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
|
|
3041
|
-
edits: [
|
|
3042
|
-
{
|
|
3043
|
-
path: opts.path,
|
|
3044
|
-
start: 0,
|
|
3045
|
-
end: opts.raw.length,
|
|
3046
|
-
replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
|
|
3047
|
-
}
|
|
3048
|
-
]
|
|
3049
|
-
}
|
|
3050
|
-
};
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
3119
|
// src/scanner/scan/page-coverage.ts
|
|
3054
3120
|
function compileRoute(pattern) {
|
|
3055
3121
|
const segs = pattern.split("/").filter(Boolean);
|
|
@@ -3071,11 +3137,11 @@ function compileRoute(pattern) {
|
|
|
3071
3137
|
return { test: (url) => re.test(url), specificity };
|
|
3072
3138
|
}
|
|
3073
3139
|
function matchUrlToRoute(url, routePaths) {
|
|
3074
|
-
const
|
|
3140
|
+
const path14 = stripQuery(url);
|
|
3075
3141
|
let best = null;
|
|
3076
3142
|
for (const rp of routePaths) {
|
|
3077
3143
|
const { test, specificity } = compileRoute(rp);
|
|
3078
|
-
if (!test(
|
|
3144
|
+
if (!test(path14)) continue;
|
|
3079
3145
|
if (!best || specificity > best.specificity)
|
|
3080
3146
|
best = { path: rp, specificity };
|
|
3081
3147
|
}
|
|
@@ -3483,6 +3549,182 @@ function computeMatrixBacklog(registry, manifest) {
|
|
|
3483
3549
|
return out2;
|
|
3484
3550
|
}
|
|
3485
3551
|
|
|
3552
|
+
// src/scanner/scan/surface-check.ts
|
|
3553
|
+
var path6 = __toESM(require("path"), 1);
|
|
3554
|
+
var EXT_CANDIDATES = [
|
|
3555
|
+
"",
|
|
3556
|
+
".tsx",
|
|
3557
|
+
".ts",
|
|
3558
|
+
".jsx",
|
|
3559
|
+
".js",
|
|
3560
|
+
"/index.tsx",
|
|
3561
|
+
"/index.ts",
|
|
3562
|
+
"/index.jsx",
|
|
3563
|
+
"/index.js"
|
|
3564
|
+
];
|
|
3565
|
+
function checkFeatureSurfaces(registry, extracted, config) {
|
|
3566
|
+
const declarations = [];
|
|
3567
|
+
for (const ef of extracted) {
|
|
3568
|
+
for (const m of ef.metadata ?? []) {
|
|
3569
|
+
if (m.kind !== "feature") continue;
|
|
3570
|
+
if (typeof m.id !== "string") continue;
|
|
3571
|
+
if (!m.surfaces || m.surfaces.length === 0) continue;
|
|
3572
|
+
if (m.acknowledge?.["*"]) continue;
|
|
3573
|
+
declarations.push({
|
|
3574
|
+
ef,
|
|
3575
|
+
id: m.id,
|
|
3576
|
+
surfaces: m.surfaces,
|
|
3577
|
+
surfaceSpans: m.surfaceSpans,
|
|
3578
|
+
line: m.loc.line ?? 1
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
if (declarations.length === 0) return [];
|
|
3583
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
3584
|
+
const jsxFiles = /* @__PURE__ */ new Set();
|
|
3585
|
+
for (const ef of extracted) {
|
|
3586
|
+
fileSet.add(ef.file.displayPath);
|
|
3587
|
+
if (ef.hasJsx) jsxFiles.add(ef.file.displayPath);
|
|
3588
|
+
}
|
|
3589
|
+
const aliases = Object.entries(config.resolve?.aliases ?? {}).sort(
|
|
3590
|
+
(a, b) => b[0].length - a[0].length
|
|
3591
|
+
);
|
|
3592
|
+
const resolveSpecifier = (importer, spec) => {
|
|
3593
|
+
const bases = [];
|
|
3594
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
3595
|
+
bases.push(path6.posix.join(path6.posix.dirname(importer), spec));
|
|
3596
|
+
} else {
|
|
3597
|
+
const alias = aliases.find(([prefix]) => spec.startsWith(prefix));
|
|
3598
|
+
if (alias) {
|
|
3599
|
+
bases.push(alias[1] + spec.slice(alias[0].length));
|
|
3600
|
+
} else if (aliases.length === 0) {
|
|
3601
|
+
if (spec.startsWith("@/") || spec.startsWith("~/")) {
|
|
3602
|
+
bases.push("src/" + spec.slice(2));
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
bases.push(spec);
|
|
3606
|
+
}
|
|
3607
|
+
const hits = [];
|
|
3608
|
+
for (const base of bases) {
|
|
3609
|
+
const normalized = path6.posix.normalize(base);
|
|
3610
|
+
for (const ext of EXT_CANDIDATES) {
|
|
3611
|
+
const candidate = normalized + ext;
|
|
3612
|
+
if (fileSet.has(candidate)) hits.push(candidate);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
return hits;
|
|
3616
|
+
};
|
|
3617
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
3618
|
+
for (const ef of extracted) {
|
|
3619
|
+
const targets = [];
|
|
3620
|
+
for (const imp of ef.imports ?? []) {
|
|
3621
|
+
if (imp.isTypeOnly) continue;
|
|
3622
|
+
targets.push(...resolveSpecifier(ef.file.displayPath, imp.specifier));
|
|
3623
|
+
}
|
|
3624
|
+
if (targets.length > 0) adjacency.set(ef.file.displayPath, targets);
|
|
3625
|
+
}
|
|
3626
|
+
const inDir = (file, dir) => file === dir || file.startsWith(dir + "/");
|
|
3627
|
+
const reachesDir = (seeds, dir) => {
|
|
3628
|
+
const queue = [...seeds];
|
|
3629
|
+
const visited = new Set(queue);
|
|
3630
|
+
while (queue.length > 0) {
|
|
3631
|
+
const current = queue.pop();
|
|
3632
|
+
if (inDir(current, dir) && jsxFiles.has(current)) return true;
|
|
3633
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
3634
|
+
if (visited.has(next)) continue;
|
|
3635
|
+
visited.add(next);
|
|
3636
|
+
queue.push(next);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
return false;
|
|
3640
|
+
};
|
|
3641
|
+
const featuresConvention = config.conventions?.features === void 0 ? DEFAULT_CONVENTIONS.features : config.conventions.features;
|
|
3642
|
+
const featureGlob = typeof featuresConvention === "string" ? featuresConvention : null;
|
|
3643
|
+
const diagnostics = [];
|
|
3644
|
+
for (const decl of declarations) {
|
|
3645
|
+
const declPath = decl.ef.file.displayPath;
|
|
3646
|
+
const featureDir = (featureGlob !== null ? extractFeatureDir(declPath, featureGlob) : null) ?? path6.posix.dirname(declPath);
|
|
3647
|
+
for (let i = 0; i < decl.surfaces.length; i++) {
|
|
3648
|
+
const pageId = decl.surfaces[i];
|
|
3649
|
+
const line = decl.surfaceSpans?.[i] ? lineOfOffset(decl.ef.file.content, decl.surfaceSpans[i].start) : decl.line;
|
|
3650
|
+
const page = registry.get("page", pageId) ?? registry.matchPattern("page", pageId);
|
|
3651
|
+
if (!page) {
|
|
3652
|
+
diagnostics.push({
|
|
3653
|
+
code: "unknown-reference",
|
|
3654
|
+
severity: "warning",
|
|
3655
|
+
message: `Feature "${decl.id}" declares a surface on unknown page "${pageId}"`,
|
|
3656
|
+
file: decl.ef.file.displayPath,
|
|
3657
|
+
line,
|
|
3658
|
+
entity: { kind: "feature", id: decl.id },
|
|
3659
|
+
hint: `No page with id "${pageId}" exists in the registry; fix the reference or add the page.`
|
|
3660
|
+
});
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
const pageFile = page.loc?.file;
|
|
3664
|
+
if (!pageFile) continue;
|
|
3665
|
+
const seeds = [pageFile];
|
|
3666
|
+
if (path6.posix.basename(pageFile) === WELL_KNOWN_FILES.page) {
|
|
3667
|
+
const dir = path6.posix.dirname(pageFile);
|
|
3668
|
+
for (const f of fileSet) {
|
|
3669
|
+
if (f !== pageFile && path6.posix.dirname(f) === dir) seeds.push(f);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
if (reachesDir(seeds, featureDir)) continue;
|
|
3673
|
+
diagnostics.push({
|
|
3674
|
+
code: "feature-surface-unreferenced",
|
|
3675
|
+
severity: "warning",
|
|
3676
|
+
message: `Feature "${decl.id}" declares page "${pageId}" as a surface, but nothing reachable from ${pageFile} references a component file in ${featureDir}/`,
|
|
3677
|
+
file: decl.ef.file.displayPath,
|
|
3678
|
+
line,
|
|
3679
|
+
entity: { kind: "feature", id: decl.id },
|
|
3680
|
+
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\`.`
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
return diagnostics;
|
|
3685
|
+
}
|
|
3686
|
+
function checkFeatureSurfaceCaptures(registry, manifest) {
|
|
3687
|
+
const routes = registry.list("route");
|
|
3688
|
+
if (routes.length === 0) return [];
|
|
3689
|
+
const routePaths = routes.map((r) => r.path);
|
|
3690
|
+
const captured = manifest.captured ?? [];
|
|
3691
|
+
const diagnostics = [];
|
|
3692
|
+
for (const feature of registry.list("feature")) {
|
|
3693
|
+
const surfaces = feature.meta?.surfaces ?? [];
|
|
3694
|
+
if (surfaces.length === 0) continue;
|
|
3695
|
+
if (entityAcknowledge(feature.meta)) continue;
|
|
3696
|
+
const coveredRoutes = /* @__PURE__ */ new Set();
|
|
3697
|
+
for (const c of captured) {
|
|
3698
|
+
if (c.kind !== "feature" || c.entity !== feature.id) continue;
|
|
3699
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
3700
|
+
if (route) coveredRoutes.add(route);
|
|
3701
|
+
}
|
|
3702
|
+
for (const pageId of surfaces) {
|
|
3703
|
+
const pageRoutes = routes.filter((r) => r.page === pageId);
|
|
3704
|
+
if (pageRoutes.length === 0) continue;
|
|
3705
|
+
if (pageRoutes.some((r) => coveredRoutes.has(r.path))) continue;
|
|
3706
|
+
const routeList = pageRoutes.map((r) => `"${r.path}"`).join(", ");
|
|
3707
|
+
diagnostics.push({
|
|
3708
|
+
code: "feature-surface-uncaptured",
|
|
3709
|
+
severity: "warning",
|
|
3710
|
+
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})`}`,
|
|
3711
|
+
file: feature.loc?.file,
|
|
3712
|
+
line: feature.loc?.line,
|
|
3713
|
+
entity: { kind: "feature", id: feature.id },
|
|
3714
|
+
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 }\`.`
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
return diagnostics;
|
|
3719
|
+
}
|
|
3720
|
+
function lineOfOffset(content, offset) {
|
|
3721
|
+
let line = 1;
|
|
3722
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
3723
|
+
if (content[i] === "\n") line++;
|
|
3724
|
+
}
|
|
3725
|
+
return line;
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3486
3728
|
// src/scanner/scan/audit.ts
|
|
3487
3729
|
function audit(opts) {
|
|
3488
3730
|
const diagnostics = [];
|
|
@@ -3490,6 +3732,8 @@ function audit(opts) {
|
|
|
3490
3732
|
const check = opts.check ?? false;
|
|
3491
3733
|
const lint = opts.lint ?? false;
|
|
3492
3734
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
3735
|
+
const surfacesEnabled = config.audit?.surfaces ?? true;
|
|
3736
|
+
const requireAcceptanceEnabled = config.audit?.requireAcceptance ?? true;
|
|
3493
3737
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
3494
3738
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
3495
3739
|
const statesEnabled = config.audit?.states ?? true;
|
|
@@ -3502,8 +3746,6 @@ function audit(opts) {
|
|
|
3502
3746
|
for (const ef of opts.flowExtracted ?? []) {
|
|
3503
3747
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
3504
3748
|
}
|
|
3505
|
-
for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
|
|
3506
|
-
if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
|
|
3507
3749
|
for (const ef of extracted) {
|
|
3508
3750
|
for (const m of ef.metadata ?? []) {
|
|
3509
3751
|
for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
|
|
@@ -3588,9 +3830,66 @@ function audit(opts) {
|
|
|
3588
3830
|
}
|
|
3589
3831
|
}
|
|
3590
3832
|
if (lint && acceptanceEnabled) {
|
|
3591
|
-
|
|
3833
|
+
const flowEntities = registry.list("flow");
|
|
3834
|
+
const acceptanceKinds = ["widget", "feature", "page"];
|
|
3835
|
+
const claims = /* @__PURE__ */ new Map();
|
|
3836
|
+
const addClaim = (kind, entityId, criterion, claim) => {
|
|
3837
|
+
const key = `${kind}:${entityId}`;
|
|
3838
|
+
let byCrit = claims.get(key);
|
|
3839
|
+
if (!byCrit) {
|
|
3840
|
+
byCrit = /* @__PURE__ */ new Map();
|
|
3841
|
+
claims.set(key, byCrit);
|
|
3842
|
+
}
|
|
3843
|
+
let list = byCrit.get(criterion);
|
|
3844
|
+
if (!list) {
|
|
3845
|
+
list = [];
|
|
3846
|
+
byCrit.set(criterion, list);
|
|
3847
|
+
}
|
|
3848
|
+
list.push(claim);
|
|
3849
|
+
};
|
|
3850
|
+
for (const flow of flowEntities) {
|
|
3851
|
+
for (const c of flow.covers ?? []) {
|
|
3852
|
+
const candidates = acceptanceKinds.flatMap((k) => {
|
|
3853
|
+
const e = registry.get(k, c.entity);
|
|
3854
|
+
return e ? [e] : [];
|
|
3855
|
+
});
|
|
3856
|
+
const matching = candidates.filter(
|
|
3857
|
+
(e) => entityCriteria(e).some((crit) => crit.id === c.criterion)
|
|
3858
|
+
);
|
|
3859
|
+
if (matching.length === 0) {
|
|
3860
|
+
diagnostics.push({
|
|
3861
|
+
code: "acceptance-orphaned",
|
|
3862
|
+
severity: "warning",
|
|
3863
|
+
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`}`,
|
|
3864
|
+
file: flow.loc.file,
|
|
3865
|
+
line: flow.loc.line,
|
|
3866
|
+
entity: { kind: "flow", id: flow.id },
|
|
3867
|
+
hint: "Remove the stale uidexCovers claim, or fix the entity/criterion id it names."
|
|
3868
|
+
});
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
if (matching.length > 1) {
|
|
3872
|
+
diagnostics.push({
|
|
3873
|
+
code: "acceptance-ambiguous",
|
|
3874
|
+
severity: "warning",
|
|
3875
|
+
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`,
|
|
3876
|
+
file: flow.loc.file,
|
|
3877
|
+
line: flow.loc.line,
|
|
3878
|
+
entity: { kind: "flow", id: flow.id },
|
|
3879
|
+
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."
|
|
3880
|
+
});
|
|
3881
|
+
}
|
|
3882
|
+
for (const target of matching) {
|
|
3883
|
+
addClaim(target.kind, target.id, c.criterion, {
|
|
3884
|
+
flowId: flow.id,
|
|
3885
|
+
rev: c.rev
|
|
3886
|
+
});
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
for (const kind of acceptanceKinds) {
|
|
3592
3891
|
for (const e of registry.list(kind)) {
|
|
3593
|
-
const criteria = e
|
|
3892
|
+
const criteria = entityCriteria(e);
|
|
3594
3893
|
if (criteria.length === 0) {
|
|
3595
3894
|
if (kind === "widget") {
|
|
3596
3895
|
diagnostics.push({
|
|
@@ -3605,14 +3904,27 @@ function audit(opts) {
|
|
|
3605
3904
|
}
|
|
3606
3905
|
continue;
|
|
3607
3906
|
}
|
|
3608
|
-
const
|
|
3609
|
-
|
|
3610
|
-
const
|
|
3611
|
-
|
|
3907
|
+
const entityClaims = claims.get(`${kind}:${e.id}`);
|
|
3908
|
+
for (const c of criteria) {
|
|
3909
|
+
const critClaims = entityClaims?.get(c.id) ?? [];
|
|
3910
|
+
const stale = critClaims.filter((cl) => cl.rev !== c.rev);
|
|
3911
|
+
for (const cl of stale) {
|
|
3912
|
+
diagnostics.push({
|
|
3913
|
+
code: "acceptance-outdated",
|
|
3914
|
+
severity: "warning",
|
|
3915
|
+
message: `Flow "${cl.flowId}" covers ${kind} "${e.id}" criterion "${c.id}" at rev ${cl.rev}, but the criterion is now rev ${c.rev}`,
|
|
3916
|
+
file: e.loc?.file,
|
|
3917
|
+
line: e.loc?.line,
|
|
3918
|
+
entity: { kind, id: e.id },
|
|
3919
|
+
hint: `Re-verify the flow against the current criterion text, then update its claim to \`uidexCovers("${e.id}", "${c.id}@${c.rev}")\`.`
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
if (critClaims.length === 0) {
|
|
3923
|
+
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}")\``;
|
|
3612
3924
|
diagnostics.push({
|
|
3613
3925
|
code: "acceptance-uncovered",
|
|
3614
3926
|
severity: "warning",
|
|
3615
|
-
message: `${kind} "${e.id}" has acceptance criterion not covered by any flow: "${c}"`,
|
|
3927
|
+
message: `${kind} "${e.id}" has acceptance criterion "${c.id}" not covered by any flow: "${c.text}"`,
|
|
3616
3928
|
file: e.loc?.file,
|
|
3617
3929
|
line: e.loc?.line,
|
|
3618
3930
|
entity: { kind, id: e.id },
|
|
@@ -3623,7 +3935,31 @@ function audit(opts) {
|
|
|
3623
3935
|
}
|
|
3624
3936
|
}
|
|
3625
3937
|
}
|
|
3938
|
+
if (lint && surfacesEnabled) {
|
|
3939
|
+
diagnostics.push(...checkFeatureSurfaces(registry, extracted, config));
|
|
3940
|
+
}
|
|
3941
|
+
if (lint && surfacesEnabled && opts.capturedStates) {
|
|
3942
|
+
diagnostics.push(
|
|
3943
|
+
...checkFeatureSurfaceCaptures(registry, opts.capturedStates)
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
if (lint && requireAcceptanceEnabled) {
|
|
3947
|
+
for (const e of registry.list("feature")) {
|
|
3948
|
+
if (entityCriteria(e).length > 0) continue;
|
|
3949
|
+
diagnostics.push({
|
|
3950
|
+
code: "feature-missing-acceptance",
|
|
3951
|
+
severity: "warning",
|
|
3952
|
+
message: `Feature "${e.id}" declares no acceptance criteria`,
|
|
3953
|
+
file: e.loc?.file,
|
|
3954
|
+
line: e.loc?.line,
|
|
3955
|
+
entity: { kind: "feature", id: e.id },
|
|
3956
|
+
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.`
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3626
3960
|
if (lint) {
|
|
3961
|
+
const featuresConvention = config.conventions?.features === void 0 ? DEFAULT_CONVENTIONS.features : config.conventions.features;
|
|
3962
|
+
const featureGlob = typeof featuresConvention === "string" ? featuresConvention : null;
|
|
3627
3963
|
const scannedPaths = new Set(files.map((f) => f.displayPath));
|
|
3628
3964
|
for (const ef of extracted) {
|
|
3629
3965
|
if (!ef.metadata) continue;
|
|
@@ -3632,10 +3968,24 @@ function audit(opts) {
|
|
|
3632
3968
|
if (typeof m.id !== "string") continue;
|
|
3633
3969
|
const filePath = ef.file.displayPath;
|
|
3634
3970
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
3635
|
-
if (
|
|
3636
|
-
const
|
|
3971
|
+
if (path7.posix.basename(filePath) === wellKnownName) continue;
|
|
3972
|
+
const featureDir = m.kind === "feature" && featureGlob !== null ? extractFeatureDir(filePath, featureGlob) : null;
|
|
3973
|
+
const dir = featureDir ?? path7.posix.dirname(filePath);
|
|
3637
3974
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
3638
3975
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
3976
|
+
const inConventionDir = featureDir !== null;
|
|
3977
|
+
if (inConventionDir) {
|
|
3978
|
+
diagnostics.push({
|
|
3979
|
+
code: "prefer-well-known-file",
|
|
3980
|
+
severity: "error",
|
|
3981
|
+
message: `Feature "${m.id}" metadata lives on ${filePath}; a feature in a convention-derived directory must declare its metadata in ${wellKnownPath}`,
|
|
3982
|
+
file: filePath,
|
|
3983
|
+
line: m.loc.line,
|
|
3984
|
+
entity: { kind: "feature", id: m.id },
|
|
3985
|
+
hint: `Move the \`export const uidex\` block to ${wellKnownPath} and remove it from ${filePath}.`
|
|
3986
|
+
});
|
|
3987
|
+
continue;
|
|
3988
|
+
}
|
|
3639
3989
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
3640
3990
|
diagnostics.push({
|
|
3641
3991
|
code: "prefer-well-known-file",
|
|
@@ -3719,6 +4069,7 @@ function audit(opts) {
|
|
|
3719
4069
|
const displayPath = ef.file.displayPath;
|
|
3720
4070
|
for (const imp of ef.imports ?? []) {
|
|
3721
4071
|
if (imp.isTypeOnly) continue;
|
|
4072
|
+
if (imp.dynamic) continue;
|
|
3722
4073
|
const baseName2 = imp.specifier.split("/").pop() ?? "";
|
|
3723
4074
|
const primitive = byName.get(
|
|
3724
4075
|
baseName2.replace(/\.(tsx|ts|jsx|js|mjs|cjs)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
|
|
@@ -3836,7 +4187,7 @@ function audit(opts) {
|
|
|
3836
4187
|
severity: "warning",
|
|
3837
4188
|
message: `\`export const uidex\` in ${ef.file.displayPath} references unknown ${refKind} "${refId}"`,
|
|
3838
4189
|
file: ef.file.displayPath,
|
|
3839
|
-
line: spans?.[i] ?
|
|
4190
|
+
line: spans?.[i] ? lineOfOffset2(ef.file.content, spans[i].start) : m.loc.line,
|
|
3840
4191
|
hint: `No ${refKind} with id "${refId}" exists in the registry; fix the reference or add the ${refKind}.`
|
|
3841
4192
|
});
|
|
3842
4193
|
}
|
|
@@ -3883,13 +4234,21 @@ function audit(opts) {
|
|
|
3883
4234
|
};
|
|
3884
4235
|
return { diagnostics: governed, summary };
|
|
3885
4236
|
}
|
|
3886
|
-
function
|
|
4237
|
+
function lineOfOffset2(content, offset) {
|
|
3887
4238
|
let line = 1;
|
|
3888
4239
|
for (let i = 0; i < offset && i < content.length; i++) {
|
|
3889
4240
|
if (content[i] === "\n") line++;
|
|
3890
4241
|
}
|
|
3891
4242
|
return line;
|
|
3892
4243
|
}
|
|
4244
|
+
function entityCriteria(e) {
|
|
4245
|
+
const meta = e.meta;
|
|
4246
|
+
if (!meta) return [];
|
|
4247
|
+
return [
|
|
4248
|
+
...meta.acceptance ?? [],
|
|
4249
|
+
...(meta.states ?? []).flatMap((s) => s.acceptance ?? [])
|
|
4250
|
+
];
|
|
4251
|
+
}
|
|
3893
4252
|
var TAG_FALLBACK_ID = {
|
|
3894
4253
|
a: "link",
|
|
3895
4254
|
button: "button",
|
|
@@ -4334,10 +4693,16 @@ function emit(opts) {
|
|
|
4334
4693
|
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
4335
4694
|
);
|
|
4336
4695
|
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
4696
|
+
t.push(" export interface AcceptanceCriterion {");
|
|
4697
|
+
t.push(" id: string");
|
|
4698
|
+
t.push(" rev?: number");
|
|
4699
|
+
t.push(" text: string");
|
|
4700
|
+
t.push(" }");
|
|
4701
|
+
t.push(" export type Acceptance = readonly (string | AcceptanceCriterion)[]");
|
|
4337
4702
|
t.push(" export interface State {");
|
|
4338
4703
|
t.push(" name: string");
|
|
4339
4704
|
t.push(" kind?: StateKind");
|
|
4340
|
-
t.push(" acceptance?:
|
|
4705
|
+
t.push(" acceptance?: Acceptance");
|
|
4341
4706
|
t.push(" description?: string");
|
|
4342
4707
|
t.push(" }");
|
|
4343
4708
|
t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
|
|
@@ -4359,7 +4724,7 @@ function emit(opts) {
|
|
|
4359
4724
|
t.push(" name?: string");
|
|
4360
4725
|
t.push(" features?: readonly FeatureId[]");
|
|
4361
4726
|
t.push(" widgets?: readonly WidgetId[]");
|
|
4362
|
-
t.push(" acceptance?:
|
|
4727
|
+
t.push(" acceptance?: Acceptance");
|
|
4363
4728
|
t.push(" states?: readonly (string | State)[]");
|
|
4364
4729
|
t.push(" acknowledge?: Acknowledge");
|
|
4365
4730
|
t.push(" description?: string");
|
|
@@ -4368,7 +4733,8 @@ function emit(opts) {
|
|
|
4368
4733
|
t.push(" feature: FeatureId | false");
|
|
4369
4734
|
t.push(" name?: string");
|
|
4370
4735
|
t.push(" features?: readonly FeatureId[]");
|
|
4371
|
-
t.push("
|
|
4736
|
+
t.push(" surfaces?: readonly PageId[]");
|
|
4737
|
+
t.push(" acceptance?: Acceptance");
|
|
4372
4738
|
t.push(" states?: readonly (string | State)[]");
|
|
4373
4739
|
t.push(" acknowledge?: ComponentAcknowledge");
|
|
4374
4740
|
t.push(" description?: string");
|
|
@@ -4381,7 +4747,7 @@ function emit(opts) {
|
|
|
4381
4747
|
t.push(" export interface Widget {");
|
|
4382
4748
|
t.push(" widget: WidgetId");
|
|
4383
4749
|
t.push(" name?: string");
|
|
4384
|
-
t.push(" acceptance?:
|
|
4750
|
+
t.push(" acceptance?: Acceptance");
|
|
4385
4751
|
t.push(" states?: readonly (string | State)[]");
|
|
4386
4752
|
t.push(" acknowledge?: ComponentAcknowledge");
|
|
4387
4753
|
t.push(" description?: string");
|
|
@@ -4480,7 +4846,7 @@ function parseGitHubRef(ref) {
|
|
|
4480
4846
|
|
|
4481
4847
|
// src/scanner/scan/scaffold.ts
|
|
4482
4848
|
var fs4 = __toESM(require("fs"), 1);
|
|
4483
|
-
var
|
|
4849
|
+
var path8 = __toESM(require("path"), 1);
|
|
4484
4850
|
function scaffoldWidgetSpec(opts) {
|
|
4485
4851
|
return scaffoldSpec({
|
|
4486
4852
|
registry: opts.registry,
|
|
@@ -4504,9 +4870,9 @@ function scaffoldSpec(opts) {
|
|
|
4504
4870
|
if (!entity) {
|
|
4505
4871
|
throw new Error(`${capitalize(kind)} "${id}" not found in registry`);
|
|
4506
4872
|
}
|
|
4507
|
-
const criteria = entity
|
|
4873
|
+
const criteria = entityCriteria(entity);
|
|
4508
4874
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
4509
|
-
const outputPath =
|
|
4875
|
+
const outputPath = path8.resolve(outDir, filename);
|
|
4510
4876
|
if (fs4.existsSync(outputPath) && !force) {
|
|
4511
4877
|
return {
|
|
4512
4878
|
outputPath,
|
|
@@ -4515,8 +4881,8 @@ function scaffoldSpec(opts) {
|
|
|
4515
4881
|
reason: `spec already exists at ${outputPath}; pass --force to overwrite`
|
|
4516
4882
|
};
|
|
4517
4883
|
}
|
|
4518
|
-
const content = renderSpec({ id, criteria, fixtureImport });
|
|
4519
|
-
fs4.mkdirSync(
|
|
4884
|
+
const content = renderSpec({ id, entityId: id, criteria, fixtureImport });
|
|
4885
|
+
fs4.mkdirSync(path8.dirname(outputPath), { recursive: true });
|
|
4520
4886
|
fs4.writeFileSync(outputPath, content, "utf8");
|
|
4521
4887
|
return { outputPath, written: true, skipped: false };
|
|
4522
4888
|
}
|
|
@@ -4543,8 +4909,9 @@ function renderSpec(args) {
|
|
|
4543
4909
|
]);
|
|
4544
4910
|
} else {
|
|
4545
4911
|
for (const criterion of args.criteria) {
|
|
4546
|
-
stub(criterion
|
|
4547
|
-
"// TODO: drive this criterion, then remove `.fixme` to arm the test
|
|
4912
|
+
stub(`${criterion.id}: ${criterion.text}`, [
|
|
4913
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test",
|
|
4914
|
+
`// and claim it: uidexCovers(${JSON.stringify(args.entityId)}, ${JSON.stringify(`${criterion.id}@${criterion.rev}`)})`
|
|
4548
4915
|
]);
|
|
4549
4916
|
}
|
|
4550
4917
|
}
|
|
@@ -4555,36 +4922,29 @@ function renderSpec(args) {
|
|
|
4555
4922
|
|
|
4556
4923
|
// src/scanner/scan/pipeline.ts
|
|
4557
4924
|
var fs5 = __toESM(require("fs"), 1);
|
|
4558
|
-
var
|
|
4925
|
+
var path9 = __toESM(require("path"), 1);
|
|
4559
4926
|
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
4560
4927
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
4561
4928
|
function loadCoverageBaseline(configDir, config) {
|
|
4562
4929
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4563
|
-
const abs =
|
|
4930
|
+
const abs = path9.resolve(configDir, rel);
|
|
4564
4931
|
let raw;
|
|
4565
4932
|
try {
|
|
4566
4933
|
raw = fs5.readFileSync(abs, "utf8");
|
|
4567
4934
|
} catch {
|
|
4568
|
-
return
|
|
4935
|
+
return void 0;
|
|
4569
4936
|
}
|
|
4570
4937
|
let parsed;
|
|
4571
4938
|
try {
|
|
4572
4939
|
parsed = JSON.parse(raw);
|
|
4573
4940
|
} catch {
|
|
4574
|
-
return
|
|
4941
|
+
return void 0;
|
|
4575
4942
|
}
|
|
4576
|
-
|
|
4577
|
-
path: abs,
|
|
4578
|
-
raw,
|
|
4579
|
-
parsed,
|
|
4580
|
-
displayPath: rel
|
|
4581
|
-
});
|
|
4582
|
-
if (legacy) return { legacy };
|
|
4583
|
-
return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
|
|
4943
|
+
return parseCoverageBaseline(parsed) ?? void 0;
|
|
4584
4944
|
}
|
|
4585
4945
|
function loadCapturedStates(configDir, config) {
|
|
4586
4946
|
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4587
|
-
const abs =
|
|
4947
|
+
const abs = path9.resolve(configDir, rel);
|
|
4588
4948
|
let raw;
|
|
4589
4949
|
try {
|
|
4590
4950
|
raw = fs5.readFileSync(abs, "utf8");
|
|
@@ -4642,11 +5002,11 @@ function runOne(dc, opts) {
|
|
|
4642
5002
|
gitContext
|
|
4643
5003
|
});
|
|
4644
5004
|
const typesRel = config.output;
|
|
4645
|
-
const typesPath =
|
|
5005
|
+
const typesPath = path9.resolve(configDir, typesRel);
|
|
4646
5006
|
const dataRel = dataOutputPath(config.output);
|
|
4647
|
-
const dataPath =
|
|
5007
|
+
const dataPath = path9.resolve(configDir, dataRel);
|
|
4648
5008
|
const locsRel = locsOutputPath(config.output);
|
|
4649
|
-
const locsPath =
|
|
5009
|
+
const locsPath = path9.resolve(configDir, locsRel);
|
|
4650
5010
|
let typesOnDisk = null;
|
|
4651
5011
|
let dataOnDisk = null;
|
|
4652
5012
|
let locsOnDisk = null;
|
|
@@ -4659,11 +5019,10 @@ function runOne(dc, opts) {
|
|
|
4659
5019
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
4660
5020
|
);
|
|
4661
5021
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
4662
|
-
const
|
|
4663
|
-
const coverageBaseline = loadedBaseline.baseline;
|
|
5022
|
+
const coverageBaseline = opts.coverageBaseline ?? loadCoverageBaseline(configDir, config);
|
|
4664
5023
|
const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
|
|
4665
5024
|
let auditResult;
|
|
4666
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics
|
|
5025
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4667
5026
|
auditResult = audit({
|
|
4668
5027
|
registry: resolved.registry,
|
|
4669
5028
|
extracted,
|
|
@@ -4687,7 +5046,6 @@ function runOne(dc, opts) {
|
|
|
4687
5046
|
locsOutputPath: locsRel,
|
|
4688
5047
|
capturedStates,
|
|
4689
5048
|
coverageBaseline,
|
|
4690
|
-
baselineDiagnostic: loadedBaseline.legacy,
|
|
4691
5049
|
workspace
|
|
4692
5050
|
});
|
|
4693
5051
|
}
|
|
@@ -4716,7 +5074,7 @@ function readOrNull(p2) {
|
|
|
4716
5074
|
}
|
|
4717
5075
|
function writeArtifact(artifact) {
|
|
4718
5076
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
4719
|
-
fs5.mkdirSync(
|
|
5077
|
+
fs5.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
|
|
4720
5078
|
fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
4721
5079
|
return true;
|
|
4722
5080
|
}
|
|
@@ -4728,7 +5086,7 @@ function writeScanResult(result) {
|
|
|
4728
5086
|
}
|
|
4729
5087
|
function coverageBaselinePath(result) {
|
|
4730
5088
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4731
|
-
return
|
|
5089
|
+
return path9.resolve(result.configDir, rel);
|
|
4732
5090
|
}
|
|
4733
5091
|
|
|
4734
5092
|
// src/scanner/scan/states-merge.ts
|
|
@@ -4764,7 +5122,7 @@ function mergeStates(committed, partial) {
|
|
|
4764
5122
|
|
|
4765
5123
|
// src/scanner/scan/fix.ts
|
|
4766
5124
|
var fs6 = __toESM(require("fs"), 1);
|
|
4767
|
-
var
|
|
5125
|
+
var path10 = __toESM(require("path"), 1);
|
|
4768
5126
|
function applyFixes(diagnostics) {
|
|
4769
5127
|
const entries = [];
|
|
4770
5128
|
for (const d of diagnostics) {
|
|
@@ -4823,10 +5181,10 @@ function applyFixes(diagnostics) {
|
|
|
4823
5181
|
if (entry.skippedReason) continue;
|
|
4824
5182
|
for (const create of entry.createFiles) {
|
|
4825
5183
|
if (fs6.existsSync(create.path)) {
|
|
4826
|
-
entry.skippedReason = `${
|
|
5184
|
+
entry.skippedReason = `${path10.basename(create.path)} already exists`;
|
|
4827
5185
|
continue;
|
|
4828
5186
|
}
|
|
4829
|
-
fs6.mkdirSync(
|
|
5187
|
+
fs6.mkdirSync(path10.dirname(create.path), { recursive: true });
|
|
4830
5188
|
fs6.writeFileSync(create.path, create.content, "utf8");
|
|
4831
5189
|
}
|
|
4832
5190
|
if (entry.skippedReason) continue;
|
|
@@ -5011,23 +5369,23 @@ function renameEntity(opts) {
|
|
|
5011
5369
|
|
|
5012
5370
|
// src/scanner/scan/cli.ts
|
|
5013
5371
|
var fs9 = __toESM(require("fs"), 1);
|
|
5014
|
-
var
|
|
5372
|
+
var path13 = __toESM(require("path"), 1);
|
|
5015
5373
|
|
|
5016
5374
|
// src/scanner/scan/ai/index.ts
|
|
5017
5375
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
5018
5376
|
|
|
5019
5377
|
// src/scanner/scan/ai/providers/claude.ts
|
|
5020
5378
|
var fs8 = __toESM(require("fs"), 1);
|
|
5021
|
-
var
|
|
5379
|
+
var path12 = __toESM(require("path"), 1);
|
|
5022
5380
|
|
|
5023
5381
|
// src/scanner/scan/ai/templates.ts
|
|
5024
5382
|
var fs7 = __toESM(require("fs"), 1);
|
|
5025
|
-
var
|
|
5383
|
+
var path11 = __toESM(require("path"), 1);
|
|
5026
5384
|
function templatePath(rel) {
|
|
5027
5385
|
const candidates = [
|
|
5028
|
-
|
|
5386
|
+
path11.resolve(__dirname, "../../templates", rel),
|
|
5029
5387
|
// dist/cli/cli.cjs → ../../templates
|
|
5030
|
-
|
|
5388
|
+
path11.resolve(__dirname, "../../../../templates", rel)
|
|
5031
5389
|
// src/scanner/scan/ai → ../../../../templates
|
|
5032
5390
|
];
|
|
5033
5391
|
for (const c of candidates) {
|
|
@@ -5075,7 +5433,7 @@ var claudeProvider = {
|
|
|
5075
5433
|
async install({ cwd, force }) {
|
|
5076
5434
|
const changes = [];
|
|
5077
5435
|
for (const file of SKILL_FILES) {
|
|
5078
|
-
const dest =
|
|
5436
|
+
const dest = path12.join(cwd, file.dest);
|
|
5079
5437
|
const exists = fs8.existsSync(dest);
|
|
5080
5438
|
if (exists && !force) {
|
|
5081
5439
|
changes.push({
|
|
@@ -5085,7 +5443,7 @@ var claudeProvider = {
|
|
|
5085
5443
|
});
|
|
5086
5444
|
continue;
|
|
5087
5445
|
}
|
|
5088
|
-
fs8.mkdirSync(
|
|
5446
|
+
fs8.mkdirSync(path12.dirname(dest), { recursive: true });
|
|
5089
5447
|
fs8.writeFileSync(dest, readTemplate(file.template));
|
|
5090
5448
|
changes.push({
|
|
5091
5449
|
path: file.dest,
|
|
@@ -5093,21 +5451,21 @@ var claudeProvider = {
|
|
|
5093
5451
|
});
|
|
5094
5452
|
}
|
|
5095
5453
|
for (const rel of LEGACY_FILES) {
|
|
5096
|
-
const dest =
|
|
5454
|
+
const dest = path12.join(cwd, rel);
|
|
5097
5455
|
if (fs8.existsSync(dest)) {
|
|
5098
5456
|
fs8.unlinkSync(dest);
|
|
5099
5457
|
changes.push({ path: rel, action: "removed" });
|
|
5100
5458
|
}
|
|
5101
5459
|
}
|
|
5102
|
-
cleanupEmpty(
|
|
5103
|
-
cleanupEmpty(
|
|
5104
|
-
cleanupEmpty(
|
|
5460
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands/uidex"));
|
|
5461
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands"));
|
|
5462
|
+
cleanupEmpty(path12.join(cwd, ".claude/rules"));
|
|
5105
5463
|
return { changes };
|
|
5106
5464
|
},
|
|
5107
5465
|
async uninstall({ cwd }) {
|
|
5108
5466
|
const changes = [];
|
|
5109
5467
|
for (const file of SKILL_FILES) {
|
|
5110
|
-
const dest =
|
|
5468
|
+
const dest = path12.join(cwd, file.dest);
|
|
5111
5469
|
if (!fs8.existsSync(dest)) {
|
|
5112
5470
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
5113
5471
|
continue;
|
|
@@ -5115,19 +5473,19 @@ var claudeProvider = {
|
|
|
5115
5473
|
fs8.unlinkSync(dest);
|
|
5116
5474
|
changes.push({ path: file.dest, action: "removed" });
|
|
5117
5475
|
}
|
|
5118
|
-
cleanupEmpty(
|
|
5119
|
-
cleanupEmpty(
|
|
5120
|
-
cleanupEmpty(
|
|
5476
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills/uidex/references"));
|
|
5477
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills/uidex"));
|
|
5478
|
+
cleanupEmpty(path12.join(cwd, ".claude/skills"));
|
|
5121
5479
|
for (const rel of LEGACY_FILES) {
|
|
5122
|
-
const dest =
|
|
5480
|
+
const dest = path12.join(cwd, rel);
|
|
5123
5481
|
if (fs8.existsSync(dest)) {
|
|
5124
5482
|
fs8.unlinkSync(dest);
|
|
5125
5483
|
changes.push({ path: rel, action: "removed" });
|
|
5126
5484
|
}
|
|
5127
5485
|
}
|
|
5128
|
-
cleanupEmpty(
|
|
5129
|
-
cleanupEmpty(
|
|
5130
|
-
cleanupEmpty(
|
|
5486
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands/uidex"));
|
|
5487
|
+
cleanupEmpty(path12.join(cwd, ".claude/commands"));
|
|
5488
|
+
cleanupEmpty(path12.join(cwd, ".claude/rules"));
|
|
5131
5489
|
return { changes };
|
|
5132
5490
|
}
|
|
5133
5491
|
};
|
|
@@ -5340,7 +5698,7 @@ function helpText2() {
|
|
|
5340
5698
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
5341
5699
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
5342
5700
|
" --audit Equivalent to --check --lint (read-only)",
|
|
5343
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names
|
|
5701
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
5344
5702
|
" --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
|
|
5345
5703
|
" --json Emit JSON diagnostics on stdout",
|
|
5346
5704
|
" --force (scaffold) overwrite existing spec",
|
|
@@ -5348,7 +5706,7 @@ function helpText2() {
|
|
|
5348
5706
|
].join("\n");
|
|
5349
5707
|
}
|
|
5350
5708
|
function runInit(cwd, w) {
|
|
5351
|
-
const configPath =
|
|
5709
|
+
const configPath = path13.join(cwd, CONFIG_FILENAME);
|
|
5352
5710
|
if (fs9.existsSync(configPath)) {
|
|
5353
5711
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
5354
5712
|
return w.result(1);
|
|
@@ -5360,7 +5718,7 @@ function runInit(cwd, w) {
|
|
|
5360
5718
|
};
|
|
5361
5719
|
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
5362
5720
|
w.out(`Created ${configPath}`);
|
|
5363
|
-
const gitignorePath =
|
|
5721
|
+
const gitignorePath = path13.join(cwd, ".gitignore");
|
|
5364
5722
|
const entry = "*.gen.*";
|
|
5365
5723
|
if (fs9.existsSync(gitignorePath)) {
|
|
5366
5724
|
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
@@ -5418,7 +5776,7 @@ function runScanCommand(cwd, flags, w) {
|
|
|
5418
5776
|
const acknowledge = {};
|
|
5419
5777
|
const merge = (from) => {
|
|
5420
5778
|
for (const [route, scopes] of Object.entries(from)) {
|
|
5421
|
-
acknowledge[route] = { ...acknowledge[route]
|
|
5779
|
+
acknowledge[route] = { ...acknowledge[route], ...scopes };
|
|
5422
5780
|
}
|
|
5423
5781
|
};
|
|
5424
5782
|
merge(computePageBacklog(r.registry, manifest));
|
|
@@ -5506,7 +5864,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
5506
5864
|
for (const r of results) {
|
|
5507
5865
|
const entity = r.registry.get(scaffoldKind, id);
|
|
5508
5866
|
if (!entity) continue;
|
|
5509
|
-
const outDir =
|
|
5867
|
+
const outDir = path13.resolve(r.configDir, "e2e");
|
|
5510
5868
|
const result = scaffoldSpec({
|
|
5511
5869
|
registry: r.registry,
|
|
5512
5870
|
kind: scaffoldKind,
|
|
@@ -5541,8 +5899,8 @@ function runStates(cwd, args, w) {
|
|
|
5541
5899
|
let merged = 0;
|
|
5542
5900
|
for (const { configDir, config } of configs) {
|
|
5543
5901
|
const rel = config.statesManifest ?? "uidex-states.json";
|
|
5544
|
-
const target =
|
|
5545
|
-
const partial = explicitPartial ?
|
|
5902
|
+
const target = path13.resolve(configDir, rel);
|
|
5903
|
+
const partial = explicitPartial ? path13.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
|
|
5546
5904
|
const committedRaw = readJson(target);
|
|
5547
5905
|
const partialRaw = readJson(partial);
|
|
5548
5906
|
if (!partialRaw) continue;
|
|
@@ -5554,7 +5912,7 @@ function runStates(cwd, args, w) {
|
|
|
5554
5912
|
fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
|
|
5555
5913
|
merged++;
|
|
5556
5914
|
w.out(
|
|
5557
|
-
`Merged ${
|
|
5915
|
+
`Merged ${path13.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
|
|
5558
5916
|
);
|
|
5559
5917
|
for (const k of result.added) w.out(` + ${k}`);
|
|
5560
5918
|
for (const k of result.updated) w.out(` ~ ${k}`);
|
|
@@ -5647,13 +6005,9 @@ function createWriter() {
|
|
|
5647
6005
|
extractUidexExports,
|
|
5648
6006
|
findWorkspaceRoot,
|
|
5649
6007
|
globToRegExp,
|
|
5650
|
-
isLegacyBaseline,
|
|
5651
6008
|
loadCoverageBaseline,
|
|
5652
6009
|
matchUrlToRoute,
|
|
5653
6010
|
mergeStates,
|
|
5654
|
-
migrateBaseline,
|
|
5655
|
-
migrateLegacyBaseline,
|
|
5656
|
-
migrateLegacyFields,
|
|
5657
6011
|
parseConfig,
|
|
5658
6012
|
parseCoverageBaseline,
|
|
5659
6013
|
pathToId,
|