uidex 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1190 -335
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +3 -0
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +19 -13
- package/dist/headless/index.d.ts +19 -13
- package/dist/headless/index.js +3 -0
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -13
- package/dist/index.d.ts +19 -13
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +36 -7
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.js +36 -7
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +36 -7
- package/dist/playwright/states-reporter.cjs.map +1 -1
- package/dist/playwright/states-reporter.d.cts +15 -0
- package/dist/playwright/states-reporter.d.ts +15 -0
- package/dist/playwright/states-reporter.js +36 -7
- package/dist/playwright/states-reporter.js.map +1 -1
- package/dist/playwright/states.cjs +8 -0
- package/dist/playwright/states.cjs.map +1 -1
- package/dist/playwright/states.d.cts +44 -1
- package/dist/playwright/states.d.ts +44 -1
- package/dist/playwright/states.js +7 -0
- package/dist/playwright/states.js.map +1 -1
- package/dist/react/index.cjs +3 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +19 -13
- package/dist/react/index.d.ts +19 -13
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1129 -244
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +379 -100
- package/dist/scan/index.d.ts +379 -100
- package/dist/scan/index.js +1109 -240
- package/dist/scan/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scan/index.cjs
CHANGED
|
@@ -30,35 +30,51 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/scanner/scan/index.ts
|
|
31
31
|
var scan_exports = {};
|
|
32
32
|
__export(scan_exports, {
|
|
33
|
+
ACK_ALL: () => ACK_ALL,
|
|
34
|
+
ACK_SCOPES: () => ACK_SCOPES,
|
|
33
35
|
CONFIG_FILENAME: () => CONFIG_FILENAME,
|
|
34
36
|
ConfigError: () => ConfigError,
|
|
35
37
|
DEFAULT_CONVENTIONS: () => DEFAULT_CONVENTIONS,
|
|
38
|
+
DIAGNOSTIC_CODES: () => DIAGNOSTIC_CODES,
|
|
36
39
|
applyFixes: () => applyFixes,
|
|
37
40
|
audit: () => audit,
|
|
41
|
+
capturedElsewhere: () => capturedElsewhere,
|
|
42
|
+
checkComponentCoverage: () => checkComponentCoverage,
|
|
38
43
|
checkPageCoverage: () => checkPageCoverage,
|
|
39
44
|
checkStateCoverage: () => checkStateCoverage,
|
|
40
45
|
checkStateMatrix: () => checkStateMatrix,
|
|
41
46
|
compileRoute: () => compileRoute,
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
computeMatrixBacklog: () => computeMatrixBacklog,
|
|
48
|
+
computePageBacklog: () => computePageBacklog,
|
|
49
|
+
coverageBaselinePath: () => coverageBaselinePath,
|
|
44
50
|
detectRoutes: () => detectRoutes,
|
|
45
51
|
discover: () => discover,
|
|
46
52
|
emit: () => emit,
|
|
53
|
+
entityAcknowledge: () => entityAcknowledge,
|
|
47
54
|
extract: () => extract,
|
|
48
55
|
extractUidexExports: () => extractUidexExports,
|
|
56
|
+
findWorkspaceRoot: () => findWorkspaceRoot,
|
|
49
57
|
globToRegExp: () => globToRegExp,
|
|
50
|
-
|
|
58
|
+
isLegacyBaseline: () => isLegacyBaseline,
|
|
59
|
+
loadCoverageBaseline: () => loadCoverageBaseline,
|
|
51
60
|
matchUrlToRoute: () => matchUrlToRoute,
|
|
52
|
-
|
|
61
|
+
mergeStates: () => mergeStates,
|
|
62
|
+
migrateBaseline: () => migrateBaseline,
|
|
63
|
+
migrateLegacyBaseline: () => migrateLegacyBaseline,
|
|
64
|
+
migrateLegacyFields: () => migrateLegacyFields,
|
|
53
65
|
parseConfig: () => parseConfig,
|
|
66
|
+
parseCoverageBaseline: () => parseCoverageBaseline,
|
|
54
67
|
pathToId: () => pathToId,
|
|
55
68
|
renameEntity: () => renameEntity,
|
|
56
69
|
resolve: () => resolve2,
|
|
57
70
|
resolveGitContext: () => resolveGitContext,
|
|
71
|
+
resolveWorkspace: () => resolveWorkspace,
|
|
72
|
+
routeAcknowledge: () => routeAcknowledge,
|
|
58
73
|
runCli: () => run,
|
|
59
74
|
runScan: () => runScan,
|
|
60
75
|
scaffoldSpec: () => scaffoldSpec,
|
|
61
76
|
scaffoldWidgetSpec: () => scaffoldWidgetSpec,
|
|
77
|
+
serializeCoverageBaseline: () => serializeCoverageBaseline,
|
|
62
78
|
validateConfig: () => validateConfig,
|
|
63
79
|
walk: () => walk,
|
|
64
80
|
writeScanResult: () => writeScanResult
|
|
@@ -97,7 +113,8 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
97
113
|
"audit",
|
|
98
114
|
"conventions",
|
|
99
115
|
"statesManifest",
|
|
100
|
-
"coverageBaseline"
|
|
116
|
+
"coverageBaseline",
|
|
117
|
+
"workspace"
|
|
101
118
|
]);
|
|
102
119
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
103
120
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -113,19 +130,64 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
|
113
130
|
"acceptance",
|
|
114
131
|
"states",
|
|
115
132
|
"pageCoverage",
|
|
116
|
-
"stateMatrix"
|
|
133
|
+
"stateMatrix",
|
|
134
|
+
"severity"
|
|
117
135
|
]);
|
|
136
|
+
var DIAGNOSTIC_CODES = [
|
|
137
|
+
"acceptance-uncovered",
|
|
138
|
+
"acknowledged-elsewhere",
|
|
139
|
+
"competing-uidex-export",
|
|
140
|
+
"component-state-uncaptured",
|
|
141
|
+
"core-kind",
|
|
142
|
+
"duplicate-id",
|
|
143
|
+
"dynamic-attr",
|
|
144
|
+
"dynamic-flow-reference",
|
|
145
|
+
"gen-missing",
|
|
146
|
+
"gen-stale",
|
|
147
|
+
"legacy-acknowledge-field",
|
|
148
|
+
"legacy-coverage-baseline",
|
|
149
|
+
"matrix-backlog",
|
|
150
|
+
"missing-element-annotation",
|
|
151
|
+
"missing-state-capture",
|
|
152
|
+
"orphan-state-capture",
|
|
153
|
+
"page-backlog",
|
|
154
|
+
"page-captured",
|
|
155
|
+
"page-no-capture",
|
|
156
|
+
"parse-error",
|
|
157
|
+
"placeholder-reason",
|
|
158
|
+
"prefer-well-known-file",
|
|
159
|
+
"rename",
|
|
160
|
+
"scope-leak",
|
|
161
|
+
"spread-attr",
|
|
162
|
+
"stale-acknowledgment",
|
|
163
|
+
"stale-page-baseline",
|
|
164
|
+
"uidex-export-ambiguous-kind",
|
|
165
|
+
"uidex-export-duplicate-field",
|
|
166
|
+
"uidex-export-duplicate-state",
|
|
167
|
+
"uidex-export-empty-name",
|
|
168
|
+
"uidex-export-invalid-field",
|
|
169
|
+
"uidex-export-invalid-literal",
|
|
170
|
+
"uidex-export-missing-kind",
|
|
171
|
+
"uidex-export-satisfies-mismatch",
|
|
172
|
+
"uidex-export-unknown-field",
|
|
173
|
+
"unknown-reference",
|
|
174
|
+
"widget-id-mismatch",
|
|
175
|
+
"widget-missing-acceptance",
|
|
176
|
+
"widget-missing-dom"
|
|
177
|
+
];
|
|
178
|
+
var DIAGNOSTIC_CODE_SET = new Set(DIAGNOSTIC_CODES);
|
|
179
|
+
var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
|
|
118
180
|
function fail(msg) {
|
|
119
181
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
120
182
|
}
|
|
121
|
-
function assertObject(value,
|
|
183
|
+
function assertObject(value, path13) {
|
|
122
184
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
123
|
-
fail(`${
|
|
185
|
+
fail(`${path13} must be an object`);
|
|
124
186
|
}
|
|
125
187
|
}
|
|
126
|
-
function assertStringArray(value,
|
|
188
|
+
function assertStringArray(value, path13) {
|
|
127
189
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
128
|
-
fail(`${
|
|
190
|
+
fail(`${path13} must be a string[]`);
|
|
129
191
|
}
|
|
130
192
|
}
|
|
131
193
|
function validateConfig(raw) {
|
|
@@ -179,11 +241,30 @@ function validateConfig(raw) {
|
|
|
179
241
|
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
180
242
|
fail(`"coverageBaseline" must be a string`);
|
|
181
243
|
}
|
|
244
|
+
if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
|
|
245
|
+
fail(`"workspace" must be a path string or false`);
|
|
246
|
+
}
|
|
182
247
|
if (raw.audit !== void 0) {
|
|
183
248
|
assertObject(raw.audit, "audit");
|
|
184
249
|
for (const key of Object.keys(raw.audit)) {
|
|
185
250
|
if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
|
|
186
251
|
const v = raw.audit[key];
|
|
252
|
+
if (key === "severity") {
|
|
253
|
+
assertObject(v, "audit.severity");
|
|
254
|
+
for (const [code, level] of Object.entries(v)) {
|
|
255
|
+
if (!DIAGNOSTIC_CODE_SET.has(code)) {
|
|
256
|
+
fail(
|
|
257
|
+
`unknown diagnostic code "${code}" in audit.severity (a typo here would silently leave that gate at its default severity)`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (typeof level !== "string" || !SEVERITY_LEVELS.has(level)) {
|
|
261
|
+
fail(
|
|
262
|
+
`audit.severity["${code}"] must be one of "error", "warning", "info", "off"`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
187
268
|
if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
|
|
188
269
|
}
|
|
189
270
|
}
|
|
@@ -226,7 +307,8 @@ function validateConfig(raw) {
|
|
|
226
307
|
audit: raw.audit,
|
|
227
308
|
conventions: raw.conventions,
|
|
228
309
|
statesManifest: raw.statesManifest,
|
|
229
|
-
coverageBaseline: raw.coverageBaseline
|
|
310
|
+
coverageBaseline: raw.coverageBaseline,
|
|
311
|
+
workspace: raw.workspace
|
|
230
312
|
};
|
|
231
313
|
}
|
|
232
314
|
function parseConfig(json) {
|
|
@@ -276,9 +358,11 @@ function tryReadConfig(configPath) {
|
|
|
276
358
|
function discover(options = {}) {
|
|
277
359
|
const cwd = options.cwd ?? process.cwd();
|
|
278
360
|
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
361
|
+
const all = options.all ?? false;
|
|
279
362
|
const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
|
|
280
|
-
if (rootMatch) return [rootMatch];
|
|
363
|
+
if (rootMatch && !all) return [rootMatch];
|
|
281
364
|
const results = [];
|
|
365
|
+
if (rootMatch) results.push(rootMatch);
|
|
282
366
|
const queue = [[cwd, 0]];
|
|
283
367
|
while (queue.length > 0) {
|
|
284
368
|
const [dir, depth] = queue.shift();
|
|
@@ -382,6 +466,26 @@ function toPosix(p2) {
|
|
|
382
466
|
function matchesAny(rel, patterns) {
|
|
383
467
|
return patterns.some((g) => globToRegExp(g).test(rel));
|
|
384
468
|
}
|
|
469
|
+
function createSourceMatcher(sources, options) {
|
|
470
|
+
const { cwd, globalExcludes = [], includeTests = false } = options;
|
|
471
|
+
const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
|
|
472
|
+
const compiled = sources.map((source) => ({
|
|
473
|
+
absRoot: path2.resolve(cwd, source.rootDir),
|
|
474
|
+
includes: source.include?.length ? source.include : DEFAULT_INCLUDES,
|
|
475
|
+
excludes: [...baseDefaults, ...globalExcludes, ...source.exclude ?? []]
|
|
476
|
+
}));
|
|
477
|
+
return (absFile) => {
|
|
478
|
+
const abs = path2.resolve(absFile);
|
|
479
|
+
return compiled.some(({ absRoot, includes, excludes }) => {
|
|
480
|
+
const rel = toPosix(path2.relative(absRoot, abs));
|
|
481
|
+
if (rel.length === 0 || rel.startsWith("../") || path2.isAbsolute(rel)) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
if (matchesAny(rel, excludes)) return false;
|
|
485
|
+
return matchesAny(rel, includes);
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
}
|
|
385
489
|
function walk(sources, options) {
|
|
386
490
|
const { cwd, globalExcludes = [], includeTests = false } = options;
|
|
387
491
|
const out2 = [];
|
|
@@ -715,6 +819,132 @@ function createRegistry() {
|
|
|
715
819
|
};
|
|
716
820
|
}
|
|
717
821
|
|
|
822
|
+
// src/scanner/scan/acknowledge.ts
|
|
823
|
+
var ACK_ALL = "*";
|
|
824
|
+
var ACK_SCOPES = [ACK_ALL, ...CORE_STATE_KINDS];
|
|
825
|
+
var COMPONENT_ACK_SCOPES = [ACK_ALL];
|
|
826
|
+
var AUTHORABLE_ACK_TYPES = [
|
|
827
|
+
"impossible",
|
|
828
|
+
"backlog"
|
|
829
|
+
];
|
|
830
|
+
var CORE_SET = new Set(CORE_STATE_KINDS);
|
|
831
|
+
function isCoreKind(scope) {
|
|
832
|
+
return CORE_SET.has(scope);
|
|
833
|
+
}
|
|
834
|
+
var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
|
|
835
|
+
function isPlaceholderReason(reason) {
|
|
836
|
+
return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
|
|
837
|
+
}
|
|
838
|
+
function describeAck(a) {
|
|
839
|
+
if (a.type === "captured-elsewhere") {
|
|
840
|
+
return a.by ? `captured-elsewhere (${a.by})` : "captured-elsewhere";
|
|
841
|
+
}
|
|
842
|
+
return a.reason ? `${a.type} ("${a.reason}")` : a.type;
|
|
843
|
+
}
|
|
844
|
+
function originHint(a, page) {
|
|
845
|
+
if (a.origin === "baseline") {
|
|
846
|
+
return "Prune it with `uidex scan --update-baseline` so the backlog reflects reality.";
|
|
847
|
+
}
|
|
848
|
+
if (a.origin === "workspace") {
|
|
849
|
+
return "This is computed from the workspace; it resolves itself once the sibling capture changes.";
|
|
850
|
+
}
|
|
851
|
+
const where = page ? ` on page "${page}"` : "";
|
|
852
|
+
return `Remove the \`acknowledge.${a.scope}\` entry${where} \u2014 a capture contradicts it.`;
|
|
853
|
+
}
|
|
854
|
+
function toResolved(map, origin, into) {
|
|
855
|
+
for (const [scope, ack] of Object.entries(map ?? {})) {
|
|
856
|
+
into.set(scope, { ...ack, scope, origin });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function routeAcknowledge(registry, baseline, route, page) {
|
|
860
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
861
|
+
toResolved(baseline?.acknowledge?.[route], "baseline", out2);
|
|
862
|
+
if (page) {
|
|
863
|
+
toResolved(registry.get("page", page)?.meta?.acknowledge, "source", out2);
|
|
864
|
+
}
|
|
865
|
+
return out2;
|
|
866
|
+
}
|
|
867
|
+
function entityAcknowledge(meta, capturedElsewhereBy) {
|
|
868
|
+
const source = meta?.acknowledge?.[ACK_ALL];
|
|
869
|
+
if (source) return { ...source, scope: ACK_ALL, origin: "source" };
|
|
870
|
+
if (capturedElsewhereBy) {
|
|
871
|
+
return {
|
|
872
|
+
type: "captured-elsewhere",
|
|
873
|
+
by: capturedElsewhereBy,
|
|
874
|
+
scope: ACK_ALL,
|
|
875
|
+
origin: "workspace"
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
return void 0;
|
|
879
|
+
}
|
|
880
|
+
function readAck(raw) {
|
|
881
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
882
|
+
const r = raw;
|
|
883
|
+
if (r.type !== "impossible" && r.type !== "backlog") return void 0;
|
|
884
|
+
const out2 = { type: r.type };
|
|
885
|
+
if (typeof r.reason === "string" && r.reason.length > 0) out2.reason = r.reason;
|
|
886
|
+
return out2;
|
|
887
|
+
}
|
|
888
|
+
function parseCoverageBaseline(raw) {
|
|
889
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
890
|
+
const r = raw;
|
|
891
|
+
const ackRaw = r.acknowledge;
|
|
892
|
+
if (!ackRaw || typeof ackRaw !== "object" || Array.isArray(ackRaw))
|
|
893
|
+
return null;
|
|
894
|
+
const acknowledge = {};
|
|
895
|
+
for (const [route, scopesRaw] of Object.entries(
|
|
896
|
+
ackRaw
|
|
897
|
+
)) {
|
|
898
|
+
if (!scopesRaw || typeof scopesRaw !== "object" || Array.isArray(scopesRaw))
|
|
899
|
+
continue;
|
|
900
|
+
const scopes = {};
|
|
901
|
+
for (const [scope, ackVal] of Object.entries(
|
|
902
|
+
scopesRaw
|
|
903
|
+
)) {
|
|
904
|
+
if (scope !== ACK_ALL && !isCoreKind(scope)) continue;
|
|
905
|
+
const ack = readAck(ackVal);
|
|
906
|
+
if (ack) scopes[scope] = ack;
|
|
907
|
+
}
|
|
908
|
+
if (Object.keys(scopes).length > 0) acknowledge[route] = scopes;
|
|
909
|
+
}
|
|
910
|
+
return { version: 2, acknowledge };
|
|
911
|
+
}
|
|
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
|
+
function serializeCoverageBaseline(baseline) {
|
|
935
|
+
const routes = Object.keys(baseline.acknowledge).sort();
|
|
936
|
+
const ordered = {};
|
|
937
|
+
for (const route of routes) {
|
|
938
|
+
const scopes = baseline.acknowledge[route];
|
|
939
|
+
const sorted = {};
|
|
940
|
+
for (const scope of ACK_SCOPES) {
|
|
941
|
+
if (scopes[scope]) sorted[scope] = scopes[scope];
|
|
942
|
+
}
|
|
943
|
+
ordered[route] = sorted;
|
|
944
|
+
}
|
|
945
|
+
return JSON.stringify({ version: 2, acknowledge: ordered }, null, 2) + "\n";
|
|
946
|
+
}
|
|
947
|
+
|
|
718
948
|
// src/scanner/scan/extract-uidex-export.ts
|
|
719
949
|
var KIND_DISCRIMINATORS = [
|
|
720
950
|
"page",
|
|
@@ -733,26 +963,41 @@ var ALLOWED_FIELDS = {
|
|
|
733
963
|
"widgets",
|
|
734
964
|
"acceptance",
|
|
735
965
|
"states",
|
|
966
|
+
"acknowledge",
|
|
736
967
|
"capture",
|
|
737
968
|
"waivers",
|
|
738
969
|
"description"
|
|
739
970
|
]),
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
971
|
+
// A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
|
|
972
|
+
// and a core kind (one cell of its matrix). A feature/widget has no route, so
|
|
973
|
+
// the core-kind matrix is meaningless there and only `"*"` is accepted —
|
|
974
|
+
// giving a component a scoped opt-out instead of forcing you to delete its
|
|
975
|
+
// whole `states` block. `capture` / `waivers` are REMOVED; they stay parseable
|
|
976
|
+
// only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
|
|
743
977
|
feature: /* @__PURE__ */ new Set([
|
|
744
978
|
"feature",
|
|
745
979
|
"name",
|
|
746
980
|
"features",
|
|
747
981
|
"acceptance",
|
|
748
982
|
"states",
|
|
983
|
+
"acknowledge",
|
|
984
|
+
"capture",
|
|
749
985
|
"description"
|
|
750
986
|
]),
|
|
751
987
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
752
|
-
widget: /* @__PURE__ */ new Set([
|
|
988
|
+
widget: /* @__PURE__ */ new Set([
|
|
989
|
+
"widget",
|
|
990
|
+
"name",
|
|
991
|
+
"acceptance",
|
|
992
|
+
"states",
|
|
993
|
+
"acknowledge",
|
|
994
|
+
"capture",
|
|
995
|
+
"description"
|
|
996
|
+
]),
|
|
753
997
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
754
998
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
755
999
|
};
|
|
1000
|
+
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
756
1001
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
757
1002
|
"name",
|
|
758
1003
|
"kind",
|
|
@@ -1003,7 +1248,8 @@ function objectLit(node, content, p2, pos, span) {
|
|
|
1003
1248
|
key,
|
|
1004
1249
|
value,
|
|
1005
1250
|
keyPos,
|
|
1006
|
-
span: removalSpan(content, prop.start, prop.end)
|
|
1251
|
+
span: removalSpan(content, prop.start, prop.end),
|
|
1252
|
+
propSpan: { start: prop.start, end: prop.end }
|
|
1007
1253
|
});
|
|
1008
1254
|
}
|
|
1009
1255
|
return { kind: "object", entries, pos, span };
|
|
@@ -1151,8 +1397,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1151
1397
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1152
1398
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1153
1399
|
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1154
|
-
const
|
|
1155
|
-
const
|
|
1400
|
+
const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
|
|
1401
|
+
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;
|
|
1156
1404
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1157
1405
|
const metadata = {
|
|
1158
1406
|
source: "ts-export",
|
|
@@ -1176,15 +1424,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1176
1424
|
metadata.widgetSpans = widgetsField.spans;
|
|
1177
1425
|
}
|
|
1178
1426
|
if (states?.length) metadata.states = states;
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1427
|
+
if (acknowledge && Object.keys(acknowledge).length > 0) {
|
|
1428
|
+
metadata.acknowledge = acknowledge;
|
|
1429
|
+
}
|
|
1430
|
+
if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
|
|
1431
|
+
if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
|
|
1432
|
+
metadata.legacyWaivers = legacyWaivers;
|
|
1433
|
+
}
|
|
1181
1434
|
if (notFlow) metadata.notFlow = true;
|
|
1182
1435
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1183
1436
|
metadata.idSpan = idValue.span;
|
|
1184
1437
|
}
|
|
1185
1438
|
const fieldSpans = {};
|
|
1186
|
-
|
|
1439
|
+
const fieldPropSpans = {};
|
|
1440
|
+
for (const entry of value.entries) {
|
|
1441
|
+
fieldSpans[entry.key] = entry.span;
|
|
1442
|
+
fieldPropSpans[entry.key] = entry.propSpan;
|
|
1443
|
+
}
|
|
1187
1444
|
metadata.fieldSpans = fieldSpans;
|
|
1445
|
+
metadata.fieldPropSpans = fieldPropSpans;
|
|
1188
1446
|
return metadata;
|
|
1189
1447
|
}
|
|
1190
1448
|
function readIdField(value, kind, fieldName) {
|
|
@@ -1214,6 +1472,80 @@ function readIdField(value, kind, fieldName) {
|
|
|
1214
1472
|
value.pos
|
|
1215
1473
|
);
|
|
1216
1474
|
}
|
|
1475
|
+
function readAcknowledgeField(byKey, isPage) {
|
|
1476
|
+
const entry = byKey.get("acknowledge");
|
|
1477
|
+
if (!entry) return void 0;
|
|
1478
|
+
if (entry.value.kind !== "object") {
|
|
1479
|
+
throw new ExtractError(
|
|
1480
|
+
"uidex-export-invalid-field",
|
|
1481
|
+
'`acknowledge` must be an object mapping a scope (`"*"` or a core kind) to `{ type, reason? }`.',
|
|
1482
|
+
entry.value.pos
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
const out2 = {};
|
|
1486
|
+
for (const e of entry.value.entries) {
|
|
1487
|
+
const scopes = isPage ? ACK_SCOPES : COMPONENT_ACK_SCOPES;
|
|
1488
|
+
if (!scopes.includes(e.key)) {
|
|
1489
|
+
throw new ExtractError(
|
|
1490
|
+
"uidex-export-invalid-field",
|
|
1491
|
+
`acknowledge scope "${e.key}" is not valid here; expected ${scopes.map((s) => `"${s}"`).join(", ")}${isPage ? "" : " (core-kind scopes are page-only \u2014 the state matrix is route-scoped)"}.`,
|
|
1492
|
+
e.keyPos
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (e.value.kind !== "object") {
|
|
1496
|
+
throw new ExtractError(
|
|
1497
|
+
"uidex-export-invalid-field",
|
|
1498
|
+
`acknowledge "${e.key}" must be an object \`{ type, reason? }\`.`,
|
|
1499
|
+
e.value.pos
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1503
|
+
for (const f of e.value.entries) {
|
|
1504
|
+
if (!ACK_FIELDS.has(f.key)) {
|
|
1505
|
+
throw new ExtractError(
|
|
1506
|
+
"uidex-export-invalid-field",
|
|
1507
|
+
`Unknown field "${f.key}" in an \`acknowledge\` entry. Allowed: ${[...ACK_FIELDS].sort().join(", ")}.`,
|
|
1508
|
+
f.value.pos
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
fieldByKey.set(f.key, f);
|
|
1512
|
+
}
|
|
1513
|
+
const type = readStringField(fieldByKey, "type");
|
|
1514
|
+
if (type === void 0) {
|
|
1515
|
+
throw new ExtractError(
|
|
1516
|
+
"uidex-export-invalid-field",
|
|
1517
|
+
`acknowledge "${e.key}" needs a \`type\`: ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")}.`,
|
|
1518
|
+
e.value.pos
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
if (type === "captured-elsewhere") {
|
|
1522
|
+
throw new ExtractError(
|
|
1523
|
+
"uidex-export-invalid-field",
|
|
1524
|
+
'`type: "captured-elsewhere"` cannot be authored; the workspace computes it from the sibling app that captured the declaration.',
|
|
1525
|
+
fieldByKey.get("type").value.pos,
|
|
1526
|
+
"Remove the entry \u2014 if a sibling app really captures this, the workspace reports it as `acknowledged-elsewhere` on its own."
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (type !== "impossible" && type !== "backlog") {
|
|
1530
|
+
throw new ExtractError(
|
|
1531
|
+
"uidex-export-invalid-field",
|
|
1532
|
+
`acknowledge \`type\` must be ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")} (got "${type}").`,
|
|
1533
|
+
fieldByKey.get("type").value.pos
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
const reason = readStringField(fieldByKey, "reason");
|
|
1537
|
+
if (type === "impossible" && (reason === void 0 || reason.length === 0)) {
|
|
1538
|
+
throw new ExtractError(
|
|
1539
|
+
"uidex-export-invalid-field",
|
|
1540
|
+
`acknowledge "${e.key}" is \`type: "impossible"\` and needs a non-empty \`reason\` (why this can NEVER be rendered/captured).`,
|
|
1541
|
+
e.value.pos,
|
|
1542
|
+
'If it merely has not been captured yet, that is `type: "backlog"` \u2014 which needs no reason.'
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
out2[e.key] = reason ? { type, reason } : { type };
|
|
1546
|
+
}
|
|
1547
|
+
return out2;
|
|
1548
|
+
}
|
|
1217
1549
|
function readWaiversField(byKey) {
|
|
1218
1550
|
const entry = byKey.get("waivers");
|
|
1219
1551
|
if (!entry) return void 0;
|
|
@@ -2062,9 +2394,8 @@ function buildMetaFromExport(exp) {
|
|
|
2062
2394
|
...s.description ? { description: s.description } : {}
|
|
2063
2395
|
}));
|
|
2064
2396
|
}
|
|
2065
|
-
if (exp.
|
|
2066
|
-
|
|
2067
|
-
meta.waivers = exp.waivers;
|
|
2397
|
+
if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
|
|
2398
|
+
meta.acknowledge = exp.acknowledge;
|
|
2068
2399
|
}
|
|
2069
2400
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
2070
2401
|
}
|
|
@@ -2409,6 +2740,45 @@ function resolve2(ctx) {
|
|
|
2409
2740
|
};
|
|
2410
2741
|
registry.add(element);
|
|
2411
2742
|
}
|
|
2743
|
+
const ancestorsById = /* @__PURE__ */ new Map();
|
|
2744
|
+
const fileById = /* @__PURE__ */ new Map();
|
|
2745
|
+
for (const a of allAnnotations) {
|
|
2746
|
+
if (!fileById.has(a.id)) fileById.set(a.id, a.file);
|
|
2747
|
+
if (a.ancestors?.length) {
|
|
2748
|
+
const prev = ancestorsById.get(a.id) ?? [];
|
|
2749
|
+
ancestorsById.set(a.id, [...prev, ...a.ancestors.map((x) => x.id)]);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
const ownersByFile = /* @__PURE__ */ new Map();
|
|
2753
|
+
const addOwner = (file, id) => {
|
|
2754
|
+
ownersByFile.set(file, [...ownersByFile.get(file) ?? [], id]);
|
|
2755
|
+
};
|
|
2756
|
+
const featureDirs = [];
|
|
2757
|
+
for (const f of registry.list("feature")) {
|
|
2758
|
+
const loc = f.loc?.file;
|
|
2759
|
+
if (!loc) continue;
|
|
2760
|
+
const dir = loc.endsWith("/") ? loc.slice(0, -1) : path4.posix.dirname(loc);
|
|
2761
|
+
featureDirs.push({ dir, id: f.id });
|
|
2762
|
+
}
|
|
2763
|
+
for (const p2 of registry.list("page")) {
|
|
2764
|
+
if (p2.loc?.file) addOwner(p2.loc.file, p2.id);
|
|
2765
|
+
}
|
|
2766
|
+
const ownersFor = (file) => {
|
|
2767
|
+
const out2 = [...ownersByFile.get(file) ?? []];
|
|
2768
|
+
for (const { dir, id } of featureDirs) {
|
|
2769
|
+
if (file === dir || file.startsWith(`${dir}/`)) out2.push(id);
|
|
2770
|
+
}
|
|
2771
|
+
return out2;
|
|
2772
|
+
};
|
|
2773
|
+
const expandTouches = (ids) => {
|
|
2774
|
+
const out2 = new Set(ids);
|
|
2775
|
+
for (const id of ids) {
|
|
2776
|
+
for (const anc of ancestorsById.get(id) ?? []) out2.add(anc);
|
|
2777
|
+
const file = fileById.get(id);
|
|
2778
|
+
if (file) for (const owner of ownersFor(file)) out2.add(owner);
|
|
2779
|
+
}
|
|
2780
|
+
return [...out2];
|
|
2781
|
+
};
|
|
2412
2782
|
if (ctx.flowFiles && conventions.flows) {
|
|
2413
2783
|
for (const ff of ctx.flowFiles) {
|
|
2414
2784
|
const notFlowExport = (ff.metadata ?? []).find(
|
|
@@ -2418,7 +2788,10 @@ function resolve2(ctx) {
|
|
|
2418
2788
|
const flowExport = (ff.metadata ?? []).find(
|
|
2419
2789
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2420
2790
|
);
|
|
2421
|
-
const derived = flowsFromFacts(ff)
|
|
2791
|
+
const derived = flowsFromFacts(ff).map((f) => ({
|
|
2792
|
+
...f,
|
|
2793
|
+
touches: expandTouches(f.touches)
|
|
2794
|
+
}));
|
|
2422
2795
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2423
2796
|
const base = derived[0];
|
|
2424
2797
|
const flow = {
|
|
@@ -2487,7 +2860,195 @@ function dedupe(arr) {
|
|
|
2487
2860
|
}
|
|
2488
2861
|
|
|
2489
2862
|
// src/scanner/scan/audit.ts
|
|
2490
|
-
var
|
|
2863
|
+
var path6 = __toESM(require("path"), 1);
|
|
2864
|
+
|
|
2865
|
+
// src/scanner/scan/component-coverage.ts
|
|
2866
|
+
var COMPOSED_KINDS = ["widget", "feature"];
|
|
2867
|
+
function composedStateEntities(registry, page) {
|
|
2868
|
+
const meta = registry.get("page", page)?.meta;
|
|
2869
|
+
if (!meta) return [];
|
|
2870
|
+
const out2 = [];
|
|
2871
|
+
for (const kind of COMPOSED_KINDS) {
|
|
2872
|
+
const ids = (kind === "widget" ? meta.widgets : meta.features) ?? [];
|
|
2873
|
+
for (const id of ids) {
|
|
2874
|
+
const e = registry.get(kind, id);
|
|
2875
|
+
if (!e?.meta?.states?.length) continue;
|
|
2876
|
+
if (e.meta.acknowledge?.[ACK_ALL]) continue;
|
|
2877
|
+
out2.push({ kind, id });
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
return out2;
|
|
2881
|
+
}
|
|
2882
|
+
function capturedStatesFor(captured, kind, id) {
|
|
2883
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
2884
|
+
for (const c of captured) {
|
|
2885
|
+
if (c.entity !== id) continue;
|
|
2886
|
+
if (c.kind !== void 0 && c.kind !== kind && c.kind !== "page") continue;
|
|
2887
|
+
out2.add(c.state);
|
|
2888
|
+
}
|
|
2889
|
+
return out2;
|
|
2890
|
+
}
|
|
2891
|
+
function checkComponentCoverage(registry, manifest) {
|
|
2892
|
+
const routes = registry.list("route");
|
|
2893
|
+
if (routes.length === 0) return [];
|
|
2894
|
+
const captured = manifest.captured ?? [];
|
|
2895
|
+
const diagnostics = [];
|
|
2896
|
+
for (const route of routes) {
|
|
2897
|
+
if (registry.get("page", route.page)?.meta?.acknowledge?.[ACK_ALL]) continue;
|
|
2898
|
+
for (const { kind, id } of composedStateEntities(registry, route.page)) {
|
|
2899
|
+
const entity = registry.get(kind, id);
|
|
2900
|
+
const declared = entity.meta.states.map((s) => s.name);
|
|
2901
|
+
const shot = capturedStatesFor(captured, kind, id);
|
|
2902
|
+
const missing = declared.filter((s) => !shot.has(s));
|
|
2903
|
+
if (missing.length === 0) continue;
|
|
2904
|
+
diagnostics.push({
|
|
2905
|
+
code: "component-state-uncaptured",
|
|
2906
|
+
severity: "info",
|
|
2907
|
+
message: `route "${route.path}" composes ${kind} "${id}", whose state(s) [${missing.join(", ")}] no capture produced`,
|
|
2908
|
+
file: entity.loc?.file,
|
|
2909
|
+
line: entity.loc?.line,
|
|
2910
|
+
entity: { kind, id },
|
|
2911
|
+
hint: `Capture the missing state(s) for "${id}", or drop them from its \`states\` if they no longer exist. This is the integration view: an isolated component capture never proves the state renders correctly INSIDE "${route.path}".`
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return diagnostics;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
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
|
+
}
|
|
2491
3052
|
|
|
2492
3053
|
// src/scanner/scan/page-coverage.ts
|
|
2493
3054
|
function compileRoute(pattern) {
|
|
@@ -2510,11 +3071,11 @@ function compileRoute(pattern) {
|
|
|
2510
3071
|
return { test: (url) => re.test(url), specificity };
|
|
2511
3072
|
}
|
|
2512
3073
|
function matchUrlToRoute(url, routePaths) {
|
|
2513
|
-
const
|
|
3074
|
+
const path13 = stripQuery(url);
|
|
2514
3075
|
let best = null;
|
|
2515
3076
|
for (const rp of routePaths) {
|
|
2516
3077
|
const { test, specificity } = compileRoute(rp);
|
|
2517
|
-
if (!test(
|
|
3078
|
+
if (!test(path13)) continue;
|
|
2518
3079
|
if (!best || specificity > best.specificity)
|
|
2519
3080
|
best = { path: rp, specificity };
|
|
2520
3081
|
}
|
|
@@ -2545,29 +3106,49 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2545
3106
|
if (routes.length === 0) return [];
|
|
2546
3107
|
const routePaths = routes.map((r) => r.path);
|
|
2547
3108
|
const captured = manifest.captured ?? [];
|
|
2548
|
-
const optedOut = /* @__PURE__ */ new Set();
|
|
2549
|
-
for (const r of routes) {
|
|
2550
|
-
const page = registry.get("page", r.page);
|
|
2551
|
-
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2552
|
-
}
|
|
2553
3109
|
const covered = /* @__PURE__ */ new Set();
|
|
2554
3110
|
for (const c of captured) {
|
|
2555
3111
|
const route = routeForCapture(c, routes, routePaths);
|
|
2556
3112
|
if (route) covered.add(route);
|
|
2557
3113
|
}
|
|
2558
|
-
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2559
3114
|
const diagnostics = [];
|
|
2560
3115
|
for (const r of routes) {
|
|
2561
|
-
|
|
3116
|
+
const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
|
|
3117
|
+
ACK_ALL
|
|
3118
|
+
);
|
|
2562
3119
|
const loc = registry.get("page", r.page)?.loc;
|
|
2563
|
-
if (
|
|
3120
|
+
if (covered.has(r.path)) {
|
|
3121
|
+
if (ack?.type === "impossible") {
|
|
3122
|
+
diagnostics.push({
|
|
3123
|
+
code: "stale-acknowledgment",
|
|
3124
|
+
severity: "warning",
|
|
3125
|
+
message: `page "${r.page}" acknowledges "*" as ${describeAck(ack)}, but route "${r.path}" is captured`,
|
|
3126
|
+
file: loc?.file,
|
|
3127
|
+
line: loc?.line,
|
|
3128
|
+
entity: { kind: "page", id: r.page },
|
|
3129
|
+
hint: originHint(ack, r.page)
|
|
3130
|
+
});
|
|
3131
|
+
} else if (ack?.type === "backlog") {
|
|
3132
|
+
diagnostics.push({
|
|
3133
|
+
code: "page-captured",
|
|
3134
|
+
severity: "warning",
|
|
3135
|
+
message: `route "${r.path}" is now captured but is still acknowledged as backlog`,
|
|
3136
|
+
file: loc?.file,
|
|
3137
|
+
line: loc?.line,
|
|
3138
|
+
hint: originHint(ack, r.page)
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
if (ack?.type === "impossible") continue;
|
|
3144
|
+
if (ack?.type === "backlog") {
|
|
2564
3145
|
diagnostics.push({
|
|
2565
3146
|
code: "page-backlog",
|
|
2566
3147
|
severity: "info",
|
|
2567
3148
|
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2568
3149
|
file: loc?.file,
|
|
2569
3150
|
line: loc?.line,
|
|
2570
|
-
hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or
|
|
3151
|
+
hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or \u2014 if it can NEVER be captured \u2014 say so with \`acknowledge: { "*": { type: "impossible", reason: "\u2026" } }\` on page "${r.page}".`
|
|
2571
3152
|
});
|
|
2572
3153
|
continue;
|
|
2573
3154
|
}
|
|
@@ -2578,46 +3159,155 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2578
3159
|
file: loc?.file,
|
|
2579
3160
|
line: loc?.line,
|
|
2580
3161
|
entity: { kind: "route", id: r.path },
|
|
2581
|
-
hint: `Capture the route in a states spec
|
|
3162
|
+
hint: `Capture the route in a states spec; or if it can NEVER be captured add \`acknowledge: { "*": { type: "impossible", reason: "why" } }\` to page "${r.page}"; or record it as debt with \`uidex scan --update-baseline\` (which writes \`{ type: "backlog" }\`, visible in the diff for review).`
|
|
2582
3163
|
});
|
|
2583
3164
|
}
|
|
2584
|
-
for (const b of
|
|
2585
|
-
if (
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2593
|
-
diagnostics.push({
|
|
2594
|
-
code: "page-captured",
|
|
2595
|
-
severity: "warning",
|
|
2596
|
-
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2597
|
-
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2598
|
-
});
|
|
2599
|
-
}
|
|
3165
|
+
for (const b of Object.keys(baseline?.acknowledge ?? {})) {
|
|
3166
|
+
if (routePaths.includes(b)) continue;
|
|
3167
|
+
diagnostics.push({
|
|
3168
|
+
code: "stale-page-baseline",
|
|
3169
|
+
severity: "warning",
|
|
3170
|
+
message: `coverage baseline acknowledges "${b}", which is not a route in the registry`,
|
|
3171
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
3172
|
+
});
|
|
2600
3173
|
}
|
|
2601
3174
|
return diagnostics;
|
|
2602
3175
|
}
|
|
2603
|
-
function
|
|
2604
|
-
const
|
|
3176
|
+
function computePageBacklog(registry, manifest) {
|
|
3177
|
+
const out2 = {};
|
|
2605
3178
|
for (const d of checkPageCoverage(registry, manifest, {
|
|
2606
|
-
|
|
3179
|
+
version: 2,
|
|
3180
|
+
acknowledge: {}
|
|
2607
3181
|
})) {
|
|
2608
|
-
if (d.code === "page-no-capture" && d.entity)
|
|
3182
|
+
if (d.code === "page-no-capture" && d.entity) {
|
|
3183
|
+
out2[d.entity.id] = { [ACK_ALL]: { type: "backlog" } };
|
|
3184
|
+
}
|
|
2609
3185
|
}
|
|
2610
|
-
return
|
|
3186
|
+
return out2;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// src/scanner/scan/workspace.ts
|
|
3190
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
3191
|
+
var path5 = __toESM(require("path"), 1);
|
|
3192
|
+
var WORKSPACE_MARKERS = [
|
|
3193
|
+
"pnpm-workspace.yaml",
|
|
3194
|
+
"pnpm-workspace.yml",
|
|
3195
|
+
"turbo.json",
|
|
3196
|
+
"lerna.json",
|
|
3197
|
+
"nx.json",
|
|
3198
|
+
".git"
|
|
3199
|
+
];
|
|
3200
|
+
var MAX_ASCENT = 8;
|
|
3201
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3202
|
+
function captureKey(kind, entity) {
|
|
3203
|
+
return `${kind ?? ""}|${entity}`;
|
|
3204
|
+
}
|
|
3205
|
+
function hasMarker(dir) {
|
|
3206
|
+
return WORKSPACE_MARKERS.some((m) => fs3.existsSync(path5.join(dir, m)));
|
|
3207
|
+
}
|
|
3208
|
+
function findWorkspaceRoot(configDir) {
|
|
3209
|
+
let dir = path5.resolve(configDir);
|
|
3210
|
+
for (let i = 0; i < MAX_ASCENT; i++) {
|
|
3211
|
+
const parent = path5.dirname(dir);
|
|
3212
|
+
if (parent === dir) return void 0;
|
|
3213
|
+
dir = parent;
|
|
3214
|
+
if (hasMarker(dir)) return dir;
|
|
3215
|
+
}
|
|
3216
|
+
return void 0;
|
|
3217
|
+
}
|
|
3218
|
+
function captureKeys(manifest) {
|
|
3219
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
3220
|
+
for (const c of manifest?.captured ?? []) {
|
|
3221
|
+
out2.add(captureKey(c.kind, c.entity));
|
|
3222
|
+
}
|
|
3223
|
+
return out2;
|
|
3224
|
+
}
|
|
3225
|
+
function readManifest(configDir, config) {
|
|
3226
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3227
|
+
let raw;
|
|
3228
|
+
try {
|
|
3229
|
+
raw = fs3.readFileSync(path5.resolve(configDir, rel), "utf8");
|
|
3230
|
+
} catch {
|
|
3231
|
+
return void 0;
|
|
3232
|
+
}
|
|
3233
|
+
try {
|
|
3234
|
+
const parsed = JSON.parse(raw);
|
|
3235
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3236
|
+
const captured = [];
|
|
3237
|
+
for (const entry of parsed.captured) {
|
|
3238
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3239
|
+
const c = entry;
|
|
3240
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3241
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3242
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3243
|
+
captured.push(rec);
|
|
3244
|
+
}
|
|
3245
|
+
return { captured };
|
|
3246
|
+
} catch {
|
|
3247
|
+
return void 0;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function toApp(dc) {
|
|
3251
|
+
return {
|
|
3252
|
+
name: path5.basename(dc.configDir),
|
|
3253
|
+
configDir: dc.configDir,
|
|
3254
|
+
configPath: dc.configPath,
|
|
3255
|
+
registers: createSourceMatcher(dc.config.sources, {
|
|
3256
|
+
cwd: dc.configDir,
|
|
3257
|
+
globalExcludes: dc.config.exclude
|
|
3258
|
+
}),
|
|
3259
|
+
captured: captureKeys(readManifest(dc.configDir, dc.config))
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
function resolveWorkspace(opts) {
|
|
3263
|
+
const { configDir, config } = opts;
|
|
3264
|
+
if (config.workspace === false) return void 0;
|
|
3265
|
+
const root = typeof config.workspace === "string" ? path5.resolve(configDir, config.workspace) : findWorkspaceRoot(configDir);
|
|
3266
|
+
if (!root) return void 0;
|
|
3267
|
+
const configs = opts.configs ?? discover({ cwd: root, all: true });
|
|
3268
|
+
const self = path5.resolve(configDir, CONFIG_FILENAME);
|
|
3269
|
+
const siblings = configs.filter((dc) => path5.resolve(dc.configPath) !== self).map(toApp);
|
|
3270
|
+
if (siblings.length === 0) return void 0;
|
|
3271
|
+
return { root, siblings };
|
|
3272
|
+
}
|
|
3273
|
+
function capturedElsewhere(workspace, absFile, kind, id) {
|
|
3274
|
+
if (!workspace || !absFile) return void 0;
|
|
3275
|
+
for (const app of workspace.siblings) {
|
|
3276
|
+
if (!app.registers(absFile)) continue;
|
|
3277
|
+
if (app.captured.has(captureKey(kind, id)) || app.captured.has(captureKey(void 0, id))) {
|
|
3278
|
+
return app.name;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
return void 0;
|
|
2611
3282
|
}
|
|
2612
3283
|
|
|
2613
3284
|
// src/scanner/scan/state-coverage.ts
|
|
2614
3285
|
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2615
|
-
function declaredStateEntities(registry) {
|
|
3286
|
+
function declaredStateEntities(registry, opts, ungated) {
|
|
2616
3287
|
const out2 = [];
|
|
2617
3288
|
for (const kind of STATE_KINDS2) {
|
|
2618
3289
|
for (const e of registry.list(kind)) {
|
|
2619
3290
|
const states = e.meta?.states;
|
|
2620
3291
|
if (!states || states.length === 0) continue;
|
|
3292
|
+
const absFile = e.loc?.file ? opts.resolveSourcePath?.(e.loc.file) : void 0;
|
|
3293
|
+
const ack = entityAcknowledge(
|
|
3294
|
+
e.meta,
|
|
3295
|
+
capturedElsewhere(opts.workspace, absFile, kind, e.id)
|
|
3296
|
+
);
|
|
3297
|
+
if (ack) {
|
|
3298
|
+
if (ack.origin === "workspace") {
|
|
3299
|
+
ungated.push({
|
|
3300
|
+
code: "acknowledged-elsewhere",
|
|
3301
|
+
severity: "info",
|
|
3302
|
+
message: `${kind} "${e.id}" declares states that no capture here produced, but sibling app "${ack.by}" registers the same declaration and captured it (${describeAck(ack)})`,
|
|
3303
|
+
file: e.loc?.file,
|
|
3304
|
+
line: e.loc?.line,
|
|
3305
|
+
entity: { kind, id: e.id },
|
|
3306
|
+
hint: `This is the workspace answering, not a guess: "${ack.by}" sources the declaring file and its manifest has a capture for "${e.id}". Set \`workspace: false\` in .uidex.json to gate it here anyway.`
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
continue;
|
|
3310
|
+
}
|
|
2621
3311
|
out2.push({
|
|
2622
3312
|
kind,
|
|
2623
3313
|
id: e.id,
|
|
@@ -2628,18 +3318,31 @@ function declaredStateEntities(registry) {
|
|
|
2628
3318
|
}
|
|
2629
3319
|
return out2;
|
|
2630
3320
|
}
|
|
2631
|
-
function
|
|
3321
|
+
function unreliablePageKinds(registry, captured) {
|
|
3322
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
3323
|
+
for (const c of captured) {
|
|
3324
|
+
if (c.kind !== "page") continue;
|
|
3325
|
+
if (registry.get("page", c.entity)) continue;
|
|
3326
|
+
out2.add(c.entity);
|
|
3327
|
+
}
|
|
3328
|
+
return out2;
|
|
3329
|
+
}
|
|
3330
|
+
function captureMatchesEntity(cap, ent, unreliable = EMPTY) {
|
|
2632
3331
|
if (cap.entity !== ent.id) return false;
|
|
2633
|
-
|
|
3332
|
+
if (cap.kind === void 0) return true;
|
|
3333
|
+
if (cap.kind === ent.kind) return true;
|
|
3334
|
+
return cap.kind === "page" && unreliable.has(cap.entity);
|
|
2634
3335
|
}
|
|
2635
|
-
|
|
3336
|
+
var EMPTY = /* @__PURE__ */ new Set();
|
|
3337
|
+
function checkStateCoverage(registry, manifest, opts = {}) {
|
|
2636
3338
|
const diagnostics = [];
|
|
2637
|
-
const declared = declaredStateEntities(registry);
|
|
2638
3339
|
const captured = manifest.captured ?? [];
|
|
3340
|
+
const unreliable = unreliablePageKinds(registry, captured);
|
|
3341
|
+
const declared = declaredStateEntities(registry, opts, diagnostics);
|
|
2639
3342
|
for (const ent of declared) {
|
|
2640
3343
|
for (const state of ent.states) {
|
|
2641
3344
|
const hit = captured.some(
|
|
2642
|
-
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
3345
|
+
(c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
|
|
2643
3346
|
);
|
|
2644
3347
|
if (hit) continue;
|
|
2645
3348
|
diagnostics.push({
|
|
@@ -2654,7 +3357,9 @@ function checkStateCoverage(registry, manifest) {
|
|
|
2654
3357
|
}
|
|
2655
3358
|
}
|
|
2656
3359
|
for (const cap of captured) {
|
|
2657
|
-
const candidates = declared.filter(
|
|
3360
|
+
const candidates = declared.filter(
|
|
3361
|
+
(ent2) => captureMatchesEntity(cap, ent2, unreliable)
|
|
3362
|
+
);
|
|
2658
3363
|
if (candidates.length === 0) continue;
|
|
2659
3364
|
const declaredSomewhere = candidates.some(
|
|
2660
3365
|
(ent2) => ent2.states.includes(cap.state)
|
|
@@ -2698,36 +3403,35 @@ function coreKindsByRoute(registry, manifest) {
|
|
|
2698
3403
|
}
|
|
2699
3404
|
return byRoute;
|
|
2700
3405
|
}
|
|
2701
|
-
function pageWaivers(registry, routePage) {
|
|
2702
|
-
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2703
|
-
}
|
|
2704
|
-
function isOptedOut(registry, page) {
|
|
2705
|
-
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2706
|
-
}
|
|
2707
3406
|
function checkStateMatrix(registry, manifest, baseline) {
|
|
2708
3407
|
const routes = registry.list("route");
|
|
2709
3408
|
if (routes.length === 0) return [];
|
|
2710
3409
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2711
3410
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2712
|
-
const baselined = baseline?.missingKinds ?? {};
|
|
2713
3411
|
const diagnostics = [];
|
|
2714
3412
|
for (const [route, kinds] of captured) {
|
|
2715
3413
|
const page = pageOf.get(route);
|
|
2716
|
-
|
|
2717
|
-
const waivers = page ? pageWaivers(registry, page) : {};
|
|
3414
|
+
const ack = routeAcknowledge(registry, baseline, route, page);
|
|
2718
3415
|
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2719
|
-
const
|
|
3416
|
+
const all = ack.get(ACK_ALL);
|
|
3417
|
+
if (all) {
|
|
3418
|
+
if (all.type === "impossible") {
|
|
3419
|
+
diagnostics.push(staleDiagnostic(all, route, page, loc));
|
|
3420
|
+
}
|
|
3421
|
+
continue;
|
|
3422
|
+
}
|
|
2720
3423
|
for (const kind of CORE_KINDS) {
|
|
2721
3424
|
if (kinds.has(kind)) continue;
|
|
2722
|
-
|
|
2723
|
-
if (
|
|
3425
|
+
const entry = ack.get(kind);
|
|
3426
|
+
if (entry?.type === "impossible") continue;
|
|
3427
|
+
if (entry?.type === "backlog") {
|
|
2724
3428
|
diagnostics.push({
|
|
2725
3429
|
code: "matrix-backlog",
|
|
2726
3430
|
severity: "info",
|
|
2727
|
-
message: `route "${route}" does not render core kind "${kind}" (acknowledged
|
|
3431
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
|
|
2728
3432
|
file: loc?.file,
|
|
2729
3433
|
line: loc?.line,
|
|
2730
|
-
hint: `Capture the "${kind}" state for "${route}",
|
|
3434
|
+
hint: `Capture the "${kind}" state for "${route}", or if the route CANNOT render it change the acknowledgment to \`{ type: "impossible", reason: "why it can't" }\`.`
|
|
2731
3435
|
});
|
|
2732
3436
|
continue;
|
|
2733
3437
|
}
|
|
@@ -2738,42 +3442,43 @@ function checkStateMatrix(registry, manifest, baseline) {
|
|
|
2738
3442
|
file: loc?.file,
|
|
2739
3443
|
line: loc?.line,
|
|
2740
3444
|
entity: { kind: "route", id: route },
|
|
2741
|
-
hint: `Capture the "${kind}" state
|
|
3445
|
+
hint: `Capture the "${kind}" state; or if the route CANNOT render it, add \`acknowledge: { ${kind}: { type: "impossible", reason: "why it can't" } }\` to page "${page}"; or record it as debt with \`uidex scan --update-baseline\` (which writes \`{ type: "backlog" }\`, visible in the diff for review).`
|
|
2742
3446
|
});
|
|
2743
3447
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
if (!page || isOptedOut(registry, page)) continue;
|
|
2748
|
-
const waivers = pageWaivers(registry, page);
|
|
2749
|
-
const loc = registry.get("page", page)?.loc;
|
|
2750
|
-
for (const kind of Object.keys(waivers)) {
|
|
2751
|
-
if (kinds.has(kind)) {
|
|
2752
|
-
diagnostics.push({
|
|
2753
|
-
code: "stale-waiver",
|
|
2754
|
-
severity: "warning",
|
|
2755
|
-
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2756
|
-
file: loc?.file,
|
|
2757
|
-
line: loc?.line,
|
|
2758
|
-
entity: { kind: "page", id: page },
|
|
2759
|
-
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2760
|
-
});
|
|
2761
|
-
}
|
|
3448
|
+
for (const [scope, entry] of ack) {
|
|
3449
|
+
if (scope === ACK_ALL || !kinds.has(scope)) continue;
|
|
3450
|
+
diagnostics.push(staleDiagnostic(entry, route, page, loc));
|
|
2762
3451
|
}
|
|
2763
3452
|
}
|
|
2764
3453
|
return diagnostics;
|
|
2765
3454
|
}
|
|
2766
|
-
function
|
|
3455
|
+
function staleDiagnostic(entry, route, page, loc) {
|
|
3456
|
+
const what = entry.scope === ACK_ALL ? `route "${route}" is captured` : `route "${route}" does render core kind "${entry.scope}"`;
|
|
3457
|
+
return {
|
|
3458
|
+
code: "stale-acknowledgment",
|
|
3459
|
+
severity: "warning",
|
|
3460
|
+
message: `${page ? `page "${page}"` : `route "${route}"`} acknowledges "${entry.scope}" as ${describeAck(entry)}, but ${what}`,
|
|
3461
|
+
file: loc?.file,
|
|
3462
|
+
line: loc?.line,
|
|
3463
|
+
...page ? { entity: { kind: "page", id: page } } : {},
|
|
3464
|
+
hint: originHint(entry, page)
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
function computeMatrixBacklog(registry, manifest) {
|
|
2767
3468
|
const routes = registry.list("route");
|
|
2768
3469
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2769
3470
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2770
3471
|
const out2 = {};
|
|
2771
3472
|
for (const [route, kinds] of captured) {
|
|
2772
3473
|
const page = pageOf.get(route);
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
const
|
|
2776
|
-
|
|
3474
|
+
const ack = routeAcknowledge(registry, void 0, route, page);
|
|
3475
|
+
if (ack.has(ACK_ALL)) continue;
|
|
3476
|
+
const scopes = {};
|
|
3477
|
+
for (const kind of CORE_KINDS) {
|
|
3478
|
+
if (kinds.has(kind) || ack.has(kind)) continue;
|
|
3479
|
+
scopes[kind] = { type: "backlog" };
|
|
3480
|
+
}
|
|
3481
|
+
if (Object.keys(scopes).length > 0) out2[route] = scopes;
|
|
2777
3482
|
}
|
|
2778
3483
|
return out2;
|
|
2779
3484
|
}
|
|
@@ -2797,6 +3502,23 @@ function audit(opts) {
|
|
|
2797
3502
|
for (const ef of opts.flowExtracted ?? []) {
|
|
2798
3503
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2799
3504
|
}
|
|
3505
|
+
for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
|
|
3506
|
+
if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
|
|
3507
|
+
for (const ef of extracted) {
|
|
3508
|
+
for (const m of ef.metadata ?? []) {
|
|
3509
|
+
for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
|
|
3510
|
+
if (!isPlaceholderReason(ack.reason)) continue;
|
|
3511
|
+
diagnostics.push({
|
|
3512
|
+
code: "placeholder-reason",
|
|
3513
|
+
severity: "warning",
|
|
3514
|
+
message: `acknowledge "${scope}" still has the migration's placeholder reason ("${ack.reason}")`,
|
|
3515
|
+
file: ef.file.displayPath,
|
|
3516
|
+
line: m.loc.line,
|
|
3517
|
+
hint: `Replace it with why this can NEVER be captured \u2014 or, if it merely has not been captured yet, change the type to "backlog" (which needs no reason).`
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
2800
3522
|
if (check && opts.generated !== void 0) {
|
|
2801
3523
|
const outRel = opts.outputPath ?? config.output;
|
|
2802
3524
|
const fresh = normalizeForCheck(opts.generated);
|
|
@@ -2910,8 +3632,8 @@ function audit(opts) {
|
|
|
2910
3632
|
if (typeof m.id !== "string") continue;
|
|
2911
3633
|
const filePath = ef.file.displayPath;
|
|
2912
3634
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2913
|
-
if (
|
|
2914
|
-
const dir =
|
|
3635
|
+
if (path6.posix.basename(filePath) === wellKnownName) continue;
|
|
3636
|
+
const dir = path6.posix.dirname(filePath);
|
|
2915
3637
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2916
3638
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2917
3639
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -3125,25 +3847,41 @@ function audit(opts) {
|
|
|
3125
3847
|
}
|
|
3126
3848
|
}
|
|
3127
3849
|
if (lint && statesEnabled && opts.capturedStates) {
|
|
3128
|
-
|
|
3850
|
+
const sourceByDisplay = new Map(
|
|
3851
|
+
files.map((f) => [f.displayPath, f.sourcePath])
|
|
3852
|
+
);
|
|
3853
|
+
diagnostics.push(
|
|
3854
|
+
...checkStateCoverage(registry, opts.capturedStates, {
|
|
3855
|
+
workspace: opts.workspace,
|
|
3856
|
+
resolveSourcePath: (displayPath) => sourceByDisplay.get(displayPath)
|
|
3857
|
+
})
|
|
3858
|
+
);
|
|
3129
3859
|
}
|
|
3130
3860
|
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
3131
3861
|
diagnostics.push(
|
|
3132
|
-
...checkPageCoverage(registry, opts.capturedStates, opts.
|
|
3862
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
|
|
3133
3863
|
);
|
|
3134
3864
|
}
|
|
3865
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3866
|
+
diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
|
|
3867
|
+
}
|
|
3135
3868
|
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
3136
3869
|
diagnostics.push(
|
|
3137
|
-
...checkStateMatrix(registry, opts.capturedStates,
|
|
3138
|
-
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
3139
|
-
})
|
|
3870
|
+
...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
|
|
3140
3871
|
);
|
|
3141
3872
|
}
|
|
3873
|
+
const overrides = config.audit?.severity ?? {};
|
|
3874
|
+
const governed = diagnostics.flatMap((d) => {
|
|
3875
|
+
const level = overrides[d.code];
|
|
3876
|
+
if (level === void 0) return [d];
|
|
3877
|
+
if (level === "off") return [];
|
|
3878
|
+
return [{ ...d, severity: level }];
|
|
3879
|
+
});
|
|
3142
3880
|
const summary = {
|
|
3143
|
-
errors:
|
|
3144
|
-
warnings:
|
|
3881
|
+
errors: governed.filter((d) => d.severity === "error").length,
|
|
3882
|
+
warnings: governed.filter((d) => d.severity === "warning").length
|
|
3145
3883
|
};
|
|
3146
|
-
return { diagnostics, summary };
|
|
3884
|
+
return { diagnostics: governed, summary };
|
|
3147
3885
|
}
|
|
3148
3886
|
function lineOfOffset(content, offset) {
|
|
3149
3887
|
let line = 1;
|
|
@@ -3471,6 +4209,30 @@ ${body}
|
|
|
3471
4209
|
`;
|
|
3472
4210
|
}
|
|
3473
4211
|
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
4212
|
+
function stateEntities(list) {
|
|
4213
|
+
return list.map((e) => ({
|
|
4214
|
+
id: e.id,
|
|
4215
|
+
states: (e.meta?.states ?? []).map((s) => s.name)
|
|
4216
|
+
}));
|
|
4217
|
+
}
|
|
4218
|
+
function emitStateMap(groups) {
|
|
4219
|
+
const lines = ["export interface UidexStates {"];
|
|
4220
|
+
for (const g of groups) {
|
|
4221
|
+
const withStates = g.entities.filter((e) => e.states.length > 0);
|
|
4222
|
+
if (withStates.length === 0) {
|
|
4223
|
+
lines.push(` ${g.kind}: never`);
|
|
4224
|
+
continue;
|
|
4225
|
+
}
|
|
4226
|
+
lines.push(` ${g.kind}: {`);
|
|
4227
|
+
for (const e of [...withStates].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4228
|
+
const states = [...new Set(e.states)].sort().map((s) => JSON.stringify(s)).join(" | ");
|
|
4229
|
+
lines.push(` ${JSON.stringify(e.id)}: ${states}`);
|
|
4230
|
+
}
|
|
4231
|
+
lines.push(" }");
|
|
4232
|
+
}
|
|
4233
|
+
lines.push("}");
|
|
4234
|
+
return lines.join("\n") + "\n";
|
|
4235
|
+
}
|
|
3474
4236
|
function emitId(name, ids) {
|
|
3475
4237
|
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3476
4238
|
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
@@ -3558,6 +4320,14 @@ function emit(opts) {
|
|
|
3558
4320
|
);
|
|
3559
4321
|
t.push(emitId("StateId", [...stateNames]));
|
|
3560
4322
|
t.push("");
|
|
4323
|
+
t.push("// ---- declared state space (kind \u2192 entity \u2192 its own states) ----");
|
|
4324
|
+
t.push(
|
|
4325
|
+
emitStateMap([
|
|
4326
|
+
{ kind: "page", entities: stateEntities(pages) },
|
|
4327
|
+
{ kind: "feature", entities: stateEntities(features) },
|
|
4328
|
+
{ kind: "widget", entities: stateEntities(widgets) }
|
|
4329
|
+
])
|
|
4330
|
+
);
|
|
3561
4331
|
t.push("// ---- authoring-surface shape types ----");
|
|
3562
4332
|
t.push("export namespace Uidex {");
|
|
3563
4333
|
t.push(
|
|
@@ -3570,7 +4340,20 @@ function emit(opts) {
|
|
|
3570
4340
|
t.push(" acceptance?: readonly string[]");
|
|
3571
4341
|
t.push(" description?: string");
|
|
3572
4342
|
t.push(" }");
|
|
3573
|
-
t.push(
|
|
4343
|
+
t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
|
|
4344
|
+
t.push(" export interface Impossible {");
|
|
4345
|
+
t.push(' type: "impossible"');
|
|
4346
|
+
t.push(" reason: string");
|
|
4347
|
+
t.push(" }");
|
|
4348
|
+
t.push(" export interface Backlog {");
|
|
4349
|
+
t.push(' type: "backlog"');
|
|
4350
|
+
t.push(" reason?: string");
|
|
4351
|
+
t.push(" }");
|
|
4352
|
+
t.push(" export type Acknowledgment = Impossible | Backlog");
|
|
4353
|
+
t.push(
|
|
4354
|
+
" export type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>"
|
|
4355
|
+
);
|
|
4356
|
+
t.push(' export type ComponentAcknowledge = { "*"?: Acknowledgment }');
|
|
3574
4357
|
t.push(" export interface Page {");
|
|
3575
4358
|
t.push(" page: PageId | false");
|
|
3576
4359
|
t.push(" name?: string");
|
|
@@ -3578,8 +4361,7 @@ function emit(opts) {
|
|
|
3578
4361
|
t.push(" widgets?: readonly WidgetId[]");
|
|
3579
4362
|
t.push(" acceptance?: readonly string[]");
|
|
3580
4363
|
t.push(" states?: readonly (string | State)[]");
|
|
3581
|
-
t.push("
|
|
3582
|
-
t.push(" waivers?: Waivers");
|
|
4364
|
+
t.push(" acknowledge?: Acknowledge");
|
|
3583
4365
|
t.push(" description?: string");
|
|
3584
4366
|
t.push(" }");
|
|
3585
4367
|
t.push(" export interface Feature {");
|
|
@@ -3588,6 +4370,7 @@ function emit(opts) {
|
|
|
3588
4370
|
t.push(" features?: readonly FeatureId[]");
|
|
3589
4371
|
t.push(" acceptance?: readonly string[]");
|
|
3590
4372
|
t.push(" states?: readonly (string | State)[]");
|
|
4373
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3591
4374
|
t.push(" description?: string");
|
|
3592
4375
|
t.push(" }");
|
|
3593
4376
|
t.push(" export interface Primitive {");
|
|
@@ -3600,6 +4383,7 @@ function emit(opts) {
|
|
|
3600
4383
|
t.push(" name?: string");
|
|
3601
4384
|
t.push(" acceptance?: readonly string[]");
|
|
3602
4385
|
t.push(" states?: readonly (string | State)[]");
|
|
4386
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3603
4387
|
t.push(" description?: string");
|
|
3604
4388
|
t.push(" }");
|
|
3605
4389
|
t.push(" export interface Region {");
|
|
@@ -3695,8 +4479,8 @@ function parseGitHubRef(ref) {
|
|
|
3695
4479
|
}
|
|
3696
4480
|
|
|
3697
4481
|
// src/scanner/scan/scaffold.ts
|
|
3698
|
-
var
|
|
3699
|
-
var
|
|
4482
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
4483
|
+
var path7 = __toESM(require("path"), 1);
|
|
3700
4484
|
function scaffoldWidgetSpec(opts) {
|
|
3701
4485
|
return scaffoldSpec({
|
|
3702
4486
|
registry: opts.registry,
|
|
@@ -3722,8 +4506,8 @@ function scaffoldSpec(opts) {
|
|
|
3722
4506
|
}
|
|
3723
4507
|
const criteria = entity.meta?.acceptance ?? [];
|
|
3724
4508
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
3725
|
-
const outputPath =
|
|
3726
|
-
if (
|
|
4509
|
+
const outputPath = path7.resolve(outDir, filename);
|
|
4510
|
+
if (fs4.existsSync(outputPath) && !force) {
|
|
3727
4511
|
return {
|
|
3728
4512
|
outputPath,
|
|
3729
4513
|
written: false,
|
|
@@ -3732,8 +4516,8 @@ function scaffoldSpec(opts) {
|
|
|
3732
4516
|
};
|
|
3733
4517
|
}
|
|
3734
4518
|
const content = renderSpec({ id, criteria, fixtureImport });
|
|
3735
|
-
|
|
3736
|
-
|
|
4519
|
+
fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
|
|
4520
|
+
fs4.writeFileSync(outputPath, content, "utf8");
|
|
3737
4521
|
return { outputPath, written: true, skipped: false };
|
|
3738
4522
|
}
|
|
3739
4523
|
function capitalize(s) {
|
|
@@ -3741,25 +4525,27 @@ function capitalize(s) {
|
|
|
3741
4525
|
}
|
|
3742
4526
|
function renderSpec(args) {
|
|
3743
4527
|
const lines = [];
|
|
3744
|
-
lines.push(
|
|
3745
|
-
`import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
|
|
3746
|
-
);
|
|
4528
|
+
lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
|
|
3747
4529
|
lines.push("");
|
|
3748
4530
|
lines.push(
|
|
3749
4531
|
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3750
4532
|
);
|
|
3751
|
-
|
|
3752
|
-
lines.push(` test(
|
|
3753
|
-
lines.push(`
|
|
4533
|
+
const stub = (title, body) => {
|
|
4534
|
+
lines.push(` test.fixme(${JSON.stringify(title)}, async ({ uidex }) => {`);
|
|
4535
|
+
for (const l of body) lines.push(` ${l}`);
|
|
4536
|
+
lines.push(` void uidex`);
|
|
3754
4537
|
lines.push(` })`);
|
|
4538
|
+
lines.push("");
|
|
4539
|
+
};
|
|
4540
|
+
if (args.criteria.length === 0) {
|
|
4541
|
+
stub("TODO: add acceptance criteria", [
|
|
4542
|
+
"// Declare `acceptance: [...]` on the entity, then re-run scaffold."
|
|
4543
|
+
]);
|
|
3755
4544
|
} else {
|
|
3756
4545
|
for (const criterion of args.criteria) {
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
lines.push(` expect(true).toBe(true)`);
|
|
3761
|
-
lines.push(` })`);
|
|
3762
|
-
lines.push("");
|
|
4546
|
+
stub(criterion, [
|
|
4547
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test."
|
|
4548
|
+
]);
|
|
3763
4549
|
}
|
|
3764
4550
|
}
|
|
3765
4551
|
lines.push("})");
|
|
@@ -3768,47 +4554,40 @@ function renderSpec(args) {
|
|
|
3768
4554
|
}
|
|
3769
4555
|
|
|
3770
4556
|
// src/scanner/scan/pipeline.ts
|
|
3771
|
-
var
|
|
3772
|
-
var
|
|
3773
|
-
var
|
|
4557
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
4558
|
+
var path8 = __toESM(require("path"), 1);
|
|
4559
|
+
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
3774
4560
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3775
|
-
function
|
|
4561
|
+
function loadCoverageBaseline(configDir, config) {
|
|
3776
4562
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3777
|
-
const abs =
|
|
4563
|
+
const abs = path8.resolve(configDir, rel);
|
|
3778
4564
|
let raw;
|
|
3779
4565
|
try {
|
|
3780
|
-
raw =
|
|
4566
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3781
4567
|
} catch {
|
|
3782
|
-
return
|
|
4568
|
+
return {};
|
|
3783
4569
|
}
|
|
4570
|
+
let parsed;
|
|
3784
4571
|
try {
|
|
3785
|
-
|
|
3786
|
-
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3787
|
-
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3788
|
-
(p2) => typeof p2 === "string"
|
|
3789
|
-
);
|
|
3790
|
-
let missingKinds;
|
|
3791
|
-
const mkRaw = parsed.missingKinds;
|
|
3792
|
-
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3793
|
-
const mk = {};
|
|
3794
|
-
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3795
|
-
if (Array.isArray(kinds)) {
|
|
3796
|
-
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
missingKinds = mk;
|
|
3800
|
-
}
|
|
3801
|
-
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
4572
|
+
parsed = JSON.parse(raw);
|
|
3802
4573
|
} catch {
|
|
3803
|
-
return
|
|
4574
|
+
return {};
|
|
3804
4575
|
}
|
|
4576
|
+
const legacy = migrateLegacyBaseline({
|
|
4577
|
+
path: abs,
|
|
4578
|
+
raw,
|
|
4579
|
+
parsed,
|
|
4580
|
+
displayPath: rel
|
|
4581
|
+
});
|
|
4582
|
+
if (legacy) return { legacy };
|
|
4583
|
+
return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
|
|
3805
4584
|
}
|
|
3806
4585
|
function loadCapturedStates(configDir, config) {
|
|
3807
|
-
const rel = config.statesManifest ??
|
|
3808
|
-
const abs =
|
|
4586
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4587
|
+
const abs = path8.resolve(configDir, rel);
|
|
3809
4588
|
let raw;
|
|
3810
4589
|
try {
|
|
3811
|
-
raw =
|
|
4590
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3812
4591
|
} catch {
|
|
3813
4592
|
return void 0;
|
|
3814
4593
|
}
|
|
@@ -3827,7 +4606,7 @@ function loadCapturedStates(configDir, config) {
|
|
|
3827
4606
|
if (typeof c.route === "string") rec.route = c.route;
|
|
3828
4607
|
captured.push(rec);
|
|
3829
4608
|
}
|
|
3830
|
-
return { captured };
|
|
4609
|
+
return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
|
|
3831
4610
|
} catch {
|
|
3832
4611
|
return void 0;
|
|
3833
4612
|
}
|
|
@@ -3863,11 +4642,11 @@ function runOne(dc, opts) {
|
|
|
3863
4642
|
gitContext
|
|
3864
4643
|
});
|
|
3865
4644
|
const typesRel = config.output;
|
|
3866
|
-
const typesPath =
|
|
4645
|
+
const typesPath = path8.resolve(configDir, typesRel);
|
|
3867
4646
|
const dataRel = dataOutputPath(config.output);
|
|
3868
|
-
const dataPath =
|
|
4647
|
+
const dataPath = path8.resolve(configDir, dataRel);
|
|
3869
4648
|
const locsRel = locsOutputPath(config.output);
|
|
3870
|
-
const locsPath =
|
|
4649
|
+
const locsPath = path8.resolve(configDir, locsRel);
|
|
3871
4650
|
let typesOnDisk = null;
|
|
3872
4651
|
let dataOnDisk = null;
|
|
3873
4652
|
let locsOnDisk = null;
|
|
@@ -3880,9 +4659,11 @@ function runOne(dc, opts) {
|
|
|
3880
4659
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3881
4660
|
);
|
|
3882
4661
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3883
|
-
const
|
|
4662
|
+
const loadedBaseline = opts.coverageBaseline ? { baseline: opts.coverageBaseline } : loadCoverageBaseline(configDir, config);
|
|
4663
|
+
const coverageBaseline = loadedBaseline.baseline;
|
|
4664
|
+
const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
|
|
3884
4665
|
let auditResult;
|
|
3885
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4666
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
|
|
3886
4667
|
auditResult = audit({
|
|
3887
4668
|
registry: resolved.registry,
|
|
3888
4669
|
extracted,
|
|
@@ -3905,7 +4686,9 @@ function runOne(dc, opts) {
|
|
|
3905
4686
|
locsOnDisk,
|
|
3906
4687
|
locsOutputPath: locsRel,
|
|
3907
4688
|
capturedStates,
|
|
3908
|
-
|
|
4689
|
+
coverageBaseline,
|
|
4690
|
+
baselineDiagnostic: loadedBaseline.legacy,
|
|
4691
|
+
workspace
|
|
3909
4692
|
});
|
|
3910
4693
|
}
|
|
3911
4694
|
return {
|
|
@@ -3926,15 +4709,15 @@ function runOne(dc, opts) {
|
|
|
3926
4709
|
}
|
|
3927
4710
|
function readOrNull(p2) {
|
|
3928
4711
|
try {
|
|
3929
|
-
return
|
|
4712
|
+
return fs5.readFileSync(p2, "utf8");
|
|
3930
4713
|
} catch {
|
|
3931
4714
|
return null;
|
|
3932
4715
|
}
|
|
3933
4716
|
}
|
|
3934
4717
|
function writeArtifact(artifact) {
|
|
3935
4718
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3936
|
-
|
|
3937
|
-
|
|
4719
|
+
fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
|
|
4720
|
+
fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3938
4721
|
return true;
|
|
3939
4722
|
}
|
|
3940
4723
|
function writeScanResult(result) {
|
|
@@ -3943,14 +4726,45 @@ function writeScanResult(result) {
|
|
|
3943
4726
|
const wroteLocs = writeArtifact(result.locs);
|
|
3944
4727
|
return wroteTypes || wroteData || wroteLocs;
|
|
3945
4728
|
}
|
|
3946
|
-
function
|
|
4729
|
+
function coverageBaselinePath(result) {
|
|
3947
4730
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3948
|
-
return
|
|
4731
|
+
return path8.resolve(result.configDir, rel);
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
// src/scanner/scan/states-merge.ts
|
|
4735
|
+
function keyOf(c) {
|
|
4736
|
+
return `${c.kind ?? "page"} ${c.entity} ${c.state}`;
|
|
4737
|
+
}
|
|
4738
|
+
function mergeStates(committed, partial) {
|
|
4739
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4740
|
+
for (const c of committed.captured ?? []) byKey.set(keyOf(c), c);
|
|
4741
|
+
const updated = [];
|
|
4742
|
+
const added = [];
|
|
4743
|
+
for (const c of partial.captured ?? []) {
|
|
4744
|
+
const k = keyOf(c);
|
|
4745
|
+
(byKey.has(k) ? updated : added).push(k);
|
|
4746
|
+
byKey.set(k, c);
|
|
4747
|
+
}
|
|
4748
|
+
const captured = [...byKey.values()].sort(
|
|
4749
|
+
(a, b) => a.entity.localeCompare(b.entity) || a.state.localeCompare(b.state)
|
|
4750
|
+
);
|
|
4751
|
+
return {
|
|
4752
|
+
manifest: {
|
|
4753
|
+
...committed,
|
|
4754
|
+
captured,
|
|
4755
|
+
// The merged file is only as fresh as its OLDEST evidence: rows carried
|
|
4756
|
+
// over from the committed manifest were not re-verified by this run, so
|
|
4757
|
+
// claiming the partial's newer stamp would defeat the staleness check.
|
|
4758
|
+
...committed.generatedAt ? { generatedAt: committed.generatedAt } : {}
|
|
4759
|
+
},
|
|
4760
|
+
updated: updated.sort(),
|
|
4761
|
+
added: added.sort()
|
|
4762
|
+
};
|
|
3949
4763
|
}
|
|
3950
4764
|
|
|
3951
4765
|
// src/scanner/scan/fix.ts
|
|
3952
|
-
var
|
|
3953
|
-
var
|
|
4766
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
4767
|
+
var path9 = __toESM(require("path"), 1);
|
|
3954
4768
|
function applyFixes(diagnostics) {
|
|
3955
4769
|
const entries = [];
|
|
3956
4770
|
for (const d of diagnostics) {
|
|
@@ -3983,7 +4797,7 @@ function applyFixes(diagnostics) {
|
|
|
3983
4797
|
for (const [filePath, edits] of editsByFile) {
|
|
3984
4798
|
let content;
|
|
3985
4799
|
try {
|
|
3986
|
-
content =
|
|
4800
|
+
content = fs6.readFileSync(filePath, "utf8");
|
|
3987
4801
|
} catch {
|
|
3988
4802
|
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
3989
4803
|
continue;
|
|
@@ -4003,22 +4817,22 @@ function applyFixes(diagnostics) {
|
|
|
4003
4817
|
const edit = kept[i];
|
|
4004
4818
|
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
4005
4819
|
}
|
|
4006
|
-
if (kept.length > 0)
|
|
4820
|
+
if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
|
|
4007
4821
|
}
|
|
4008
4822
|
for (const entry of entries) {
|
|
4009
4823
|
if (entry.skippedReason) continue;
|
|
4010
4824
|
for (const create of entry.createFiles) {
|
|
4011
|
-
if (
|
|
4012
|
-
entry.skippedReason = `${
|
|
4825
|
+
if (fs6.existsSync(create.path)) {
|
|
4826
|
+
entry.skippedReason = `${path9.basename(create.path)} already exists`;
|
|
4013
4827
|
continue;
|
|
4014
4828
|
}
|
|
4015
|
-
|
|
4016
|
-
|
|
4829
|
+
fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
|
|
4830
|
+
fs6.writeFileSync(create.path, create.content, "utf8");
|
|
4017
4831
|
}
|
|
4018
4832
|
if (entry.skippedReason) continue;
|
|
4019
4833
|
for (const del of entry.deleteFiles) {
|
|
4020
4834
|
try {
|
|
4021
|
-
|
|
4835
|
+
fs6.unlinkSync(del);
|
|
4022
4836
|
} catch {
|
|
4023
4837
|
}
|
|
4024
4838
|
}
|
|
@@ -4196,29 +5010,29 @@ function renameEntity(opts) {
|
|
|
4196
5010
|
}
|
|
4197
5011
|
|
|
4198
5012
|
// src/scanner/scan/cli.ts
|
|
4199
|
-
var
|
|
4200
|
-
var
|
|
5013
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
5014
|
+
var path12 = __toESM(require("path"), 1);
|
|
4201
5015
|
|
|
4202
5016
|
// src/scanner/scan/ai/index.ts
|
|
4203
5017
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
4204
5018
|
|
|
4205
5019
|
// src/scanner/scan/ai/providers/claude.ts
|
|
4206
|
-
var
|
|
4207
|
-
var
|
|
5020
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
5021
|
+
var path11 = __toESM(require("path"), 1);
|
|
4208
5022
|
|
|
4209
5023
|
// src/scanner/scan/ai/templates.ts
|
|
4210
|
-
var
|
|
4211
|
-
var
|
|
5024
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
5025
|
+
var path10 = __toESM(require("path"), 1);
|
|
4212
5026
|
function templatePath(rel) {
|
|
4213
5027
|
const candidates = [
|
|
4214
|
-
|
|
5028
|
+
path10.resolve(__dirname, "../../templates", rel),
|
|
4215
5029
|
// dist/cli/cli.cjs → ../../templates
|
|
4216
|
-
|
|
5030
|
+
path10.resolve(__dirname, "../../../../templates", rel)
|
|
4217
5031
|
// src/scanner/scan/ai → ../../../../templates
|
|
4218
5032
|
];
|
|
4219
5033
|
for (const c of candidates) {
|
|
4220
5034
|
try {
|
|
4221
|
-
|
|
5035
|
+
fs7.accessSync(c, fs7.constants.R_OK);
|
|
4222
5036
|
return c;
|
|
4223
5037
|
} catch {
|
|
4224
5038
|
continue;
|
|
@@ -4230,7 +5044,7 @@ function templatePath(rel) {
|
|
|
4230
5044
|
);
|
|
4231
5045
|
}
|
|
4232
5046
|
function readTemplate(rel) {
|
|
4233
|
-
return
|
|
5047
|
+
return fs7.readFileSync(templatePath(rel), "utf8");
|
|
4234
5048
|
}
|
|
4235
5049
|
|
|
4236
5050
|
// src/scanner/scan/ai/providers/claude.ts
|
|
@@ -4261,8 +5075,8 @@ var claudeProvider = {
|
|
|
4261
5075
|
async install({ cwd, force }) {
|
|
4262
5076
|
const changes = [];
|
|
4263
5077
|
for (const file of SKILL_FILES) {
|
|
4264
|
-
const dest =
|
|
4265
|
-
const exists =
|
|
5078
|
+
const dest = path11.join(cwd, file.dest);
|
|
5079
|
+
const exists = fs8.existsSync(dest);
|
|
4266
5080
|
if (exists && !force) {
|
|
4267
5081
|
changes.push({
|
|
4268
5082
|
path: file.dest,
|
|
@@ -4271,56 +5085,56 @@ var claudeProvider = {
|
|
|
4271
5085
|
});
|
|
4272
5086
|
continue;
|
|
4273
5087
|
}
|
|
4274
|
-
|
|
4275
|
-
|
|
5088
|
+
fs8.mkdirSync(path11.dirname(dest), { recursive: true });
|
|
5089
|
+
fs8.writeFileSync(dest, readTemplate(file.template));
|
|
4276
5090
|
changes.push({
|
|
4277
5091
|
path: file.dest,
|
|
4278
5092
|
action: exists ? "overwritten" : "created"
|
|
4279
5093
|
});
|
|
4280
5094
|
}
|
|
4281
5095
|
for (const rel of LEGACY_FILES) {
|
|
4282
|
-
const dest =
|
|
4283
|
-
if (
|
|
4284
|
-
|
|
5096
|
+
const dest = path11.join(cwd, rel);
|
|
5097
|
+
if (fs8.existsSync(dest)) {
|
|
5098
|
+
fs8.unlinkSync(dest);
|
|
4285
5099
|
changes.push({ path: rel, action: "removed" });
|
|
4286
5100
|
}
|
|
4287
5101
|
}
|
|
4288
|
-
cleanupEmpty(
|
|
4289
|
-
cleanupEmpty(
|
|
4290
|
-
cleanupEmpty(
|
|
5102
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5103
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5104
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4291
5105
|
return { changes };
|
|
4292
5106
|
},
|
|
4293
5107
|
async uninstall({ cwd }) {
|
|
4294
5108
|
const changes = [];
|
|
4295
5109
|
for (const file of SKILL_FILES) {
|
|
4296
|
-
const dest =
|
|
4297
|
-
if (!
|
|
5110
|
+
const dest = path11.join(cwd, file.dest);
|
|
5111
|
+
if (!fs8.existsSync(dest)) {
|
|
4298
5112
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
4299
5113
|
continue;
|
|
4300
5114
|
}
|
|
4301
|
-
|
|
5115
|
+
fs8.unlinkSync(dest);
|
|
4302
5116
|
changes.push({ path: file.dest, action: "removed" });
|
|
4303
5117
|
}
|
|
4304
|
-
cleanupEmpty(
|
|
4305
|
-
cleanupEmpty(
|
|
4306
|
-
cleanupEmpty(
|
|
5118
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills/uidex/references"));
|
|
5119
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills/uidex"));
|
|
5120
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills"));
|
|
4307
5121
|
for (const rel of LEGACY_FILES) {
|
|
4308
|
-
const dest =
|
|
4309
|
-
if (
|
|
4310
|
-
|
|
5122
|
+
const dest = path11.join(cwd, rel);
|
|
5123
|
+
if (fs8.existsSync(dest)) {
|
|
5124
|
+
fs8.unlinkSync(dest);
|
|
4311
5125
|
changes.push({ path: rel, action: "removed" });
|
|
4312
5126
|
}
|
|
4313
5127
|
}
|
|
4314
|
-
cleanupEmpty(
|
|
4315
|
-
cleanupEmpty(
|
|
4316
|
-
cleanupEmpty(
|
|
5128
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5129
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5130
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4317
5131
|
return { changes };
|
|
4318
5132
|
}
|
|
4319
5133
|
};
|
|
4320
5134
|
function cleanupEmpty(dir) {
|
|
4321
5135
|
try {
|
|
4322
|
-
const entries =
|
|
4323
|
-
if (entries.length === 0)
|
|
5136
|
+
const entries = fs8.readdirSync(dir);
|
|
5137
|
+
if (entries.length === 0) fs8.rmdirSync(dir);
|
|
4324
5138
|
} catch {
|
|
4325
5139
|
}
|
|
4326
5140
|
}
|
|
@@ -4489,6 +5303,8 @@ async function run(opts) {
|
|
|
4489
5303
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
4490
5304
|
case "rename":
|
|
4491
5305
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
5306
|
+
case "states":
|
|
5307
|
+
return runStates(cwd, positional.slice(1), writer);
|
|
4492
5308
|
case "ai": {
|
|
4493
5309
|
const result = await runAiCommand({
|
|
4494
5310
|
cwd,
|
|
@@ -4517,22 +5333,23 @@ function helpText2() {
|
|
|
4517
5333
|
" scan [flags] Run the scanner pipeline",
|
|
4518
5334
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
4519
5335
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
5336
|
+
" states merge [partial] Fold a filtered run's *.partial.json into the committed manifest",
|
|
4520
5337
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
4521
5338
|
"",
|
|
4522
5339
|
"Flags:",
|
|
4523
5340
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
4524
5341
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
4525
5342
|
" --audit Equivalent to --check --lint (read-only)",
|
|
4526
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4527
|
-
" --update-baseline Regenerate the
|
|
5343
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names, migrate waivers/capture to acknowledge), then rescan and write",
|
|
5344
|
+
" --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
|
|
4528
5345
|
" --json Emit JSON diagnostics on stdout",
|
|
4529
5346
|
" --force (scaffold) overwrite existing spec",
|
|
4530
5347
|
""
|
|
4531
5348
|
].join("\n");
|
|
4532
5349
|
}
|
|
4533
5350
|
function runInit(cwd, w) {
|
|
4534
|
-
const configPath =
|
|
4535
|
-
if (
|
|
5351
|
+
const configPath = path12.join(cwd, CONFIG_FILENAME);
|
|
5352
|
+
if (fs9.existsSync(configPath)) {
|
|
4536
5353
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
4537
5354
|
return w.result(1);
|
|
4538
5355
|
}
|
|
@@ -4541,16 +5358,16 @@ function runInit(cwd, w) {
|
|
|
4541
5358
|
sources: [{ rootDir: "src" }],
|
|
4542
5359
|
output: "src/uidex.gen.ts"
|
|
4543
5360
|
};
|
|
4544
|
-
|
|
5361
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
4545
5362
|
w.out(`Created ${configPath}`);
|
|
4546
|
-
const gitignorePath =
|
|
5363
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
4547
5364
|
const entry = "*.gen.*";
|
|
4548
|
-
if (
|
|
4549
|
-
const existing =
|
|
5365
|
+
if (fs9.existsSync(gitignorePath)) {
|
|
5366
|
+
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
4550
5367
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
4551
5368
|
if (!hasEntry) {
|
|
4552
5369
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4553
|
-
|
|
5370
|
+
fs9.appendFileSync(
|
|
4554
5371
|
gitignorePath,
|
|
4555
5372
|
`${needsNewline ? "\n" : ""}${entry}
|
|
4556
5373
|
`,
|
|
@@ -4559,7 +5376,7 @@ function runInit(cwd, w) {
|
|
|
4559
5376
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
4560
5377
|
}
|
|
4561
5378
|
} else {
|
|
4562
|
-
|
|
5379
|
+
fs9.writeFileSync(gitignorePath, `${entry}
|
|
4563
5380
|
`, "utf8");
|
|
4564
5381
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
4565
5382
|
}
|
|
@@ -4598,20 +5415,25 @@ function runScanCommand(cwd, flags, w) {
|
|
|
4598
5415
|
if (updateBaseline) {
|
|
4599
5416
|
for (const r of results) {
|
|
4600
5417
|
const manifest = r.capturedStates ?? { captured: [] };
|
|
4601
|
-
const
|
|
4602
|
-
const
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
5418
|
+
const acknowledge = {};
|
|
5419
|
+
const merge = (from) => {
|
|
5420
|
+
for (const [route, scopes] of Object.entries(from)) {
|
|
5421
|
+
acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
|
|
5422
|
+
}
|
|
4606
5423
|
};
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
const
|
|
4610
|
-
|
|
5424
|
+
merge(computePageBacklog(r.registry, manifest));
|
|
5425
|
+
merge(computeMatrixBacklog(r.registry, manifest));
|
|
5426
|
+
const baseline = { version: 2, acknowledge };
|
|
5427
|
+
const p2 = coverageBaselinePath(r);
|
|
5428
|
+
fs9.writeFileSync(p2, serializeCoverageBaseline(baseline));
|
|
5429
|
+
const entries = Object.values(acknowledge);
|
|
5430
|
+
const pages = entries.filter((s) => s["*"]).length;
|
|
5431
|
+
const kindGaps = entries.reduce(
|
|
5432
|
+
(n, s) => n + Object.keys(s).filter((k) => k !== "*").length,
|
|
4611
5433
|
0
|
|
4612
5434
|
);
|
|
4613
5435
|
w.out(
|
|
4614
|
-
`Wrote ${p2} \u2014 ${
|
|
5436
|
+
`Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
|
|
4615
5437
|
);
|
|
4616
5438
|
}
|
|
4617
5439
|
return w.result(0);
|
|
@@ -4684,7 +5506,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4684
5506
|
for (const r of results) {
|
|
4685
5507
|
const entity = r.registry.get(scaffoldKind, id);
|
|
4686
5508
|
if (!entity) continue;
|
|
4687
|
-
const outDir =
|
|
5509
|
+
const outDir = path12.resolve(r.configDir, "e2e");
|
|
4688
5510
|
const result = scaffoldSpec({
|
|
4689
5511
|
registry: r.registry,
|
|
4690
5512
|
kind: scaffoldKind,
|
|
@@ -4705,6 +5527,53 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4705
5527
|
return w.result(1);
|
|
4706
5528
|
}
|
|
4707
5529
|
var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
|
|
5530
|
+
function runStates(cwd, args, w) {
|
|
5531
|
+
const [sub, explicitPartial] = args;
|
|
5532
|
+
if (sub !== "merge") {
|
|
5533
|
+
w.err("Usage: uidex states merge [partial-manifest]");
|
|
5534
|
+
return w.result(1);
|
|
5535
|
+
}
|
|
5536
|
+
const configs = discover({ cwd });
|
|
5537
|
+
if (configs.length === 0) {
|
|
5538
|
+
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
5539
|
+
return w.result(1);
|
|
5540
|
+
}
|
|
5541
|
+
let merged = 0;
|
|
5542
|
+
for (const { configDir, config } of configs) {
|
|
5543
|
+
const rel = config.statesManifest ?? "uidex-states.json";
|
|
5544
|
+
const target = path12.resolve(configDir, rel);
|
|
5545
|
+
const partial = explicitPartial ? path12.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
|
|
5546
|
+
const committedRaw = readJson(target);
|
|
5547
|
+
const partialRaw = readJson(partial);
|
|
5548
|
+
if (!partialRaw) continue;
|
|
5549
|
+
if (!committedRaw) {
|
|
5550
|
+
w.err(`No committed manifest at ${rel}; nothing to merge into`);
|
|
5551
|
+
return w.result(1);
|
|
5552
|
+
}
|
|
5553
|
+
const result = mergeStates(committedRaw, partialRaw);
|
|
5554
|
+
fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
|
|
5555
|
+
merged++;
|
|
5556
|
+
w.out(
|
|
5557
|
+
`Merged ${path12.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
|
|
5558
|
+
);
|
|
5559
|
+
for (const k of result.added) w.out(` + ${k}`);
|
|
5560
|
+
for (const k of result.updated) w.out(` ~ ${k}`);
|
|
5561
|
+
}
|
|
5562
|
+
if (merged === 0) {
|
|
5563
|
+
w.out("No partial manifest found; nothing to merge.");
|
|
5564
|
+
}
|
|
5565
|
+
return w.result(0);
|
|
5566
|
+
}
|
|
5567
|
+
function readJson(p2) {
|
|
5568
|
+
try {
|
|
5569
|
+
const parsed = JSON.parse(fs9.readFileSync(p2, "utf8"));
|
|
5570
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
5571
|
+
const m = parsed;
|
|
5572
|
+
return Array.isArray(m.captured) ? m : null;
|
|
5573
|
+
} catch {
|
|
5574
|
+
return null;
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
4708
5577
|
function runRename(cwd, args, flags, w) {
|
|
4709
5578
|
const [kind, oldId, newId] = args;
|
|
4710
5579
|
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
@@ -4753,35 +5622,51 @@ function createWriter() {
|
|
|
4753
5622
|
}
|
|
4754
5623
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4755
5624
|
0 && (module.exports = {
|
|
5625
|
+
ACK_ALL,
|
|
5626
|
+
ACK_SCOPES,
|
|
4756
5627
|
CONFIG_FILENAME,
|
|
4757
5628
|
ConfigError,
|
|
4758
5629
|
DEFAULT_CONVENTIONS,
|
|
5630
|
+
DIAGNOSTIC_CODES,
|
|
4759
5631
|
applyFixes,
|
|
4760
5632
|
audit,
|
|
5633
|
+
capturedElsewhere,
|
|
5634
|
+
checkComponentCoverage,
|
|
4761
5635
|
checkPageCoverage,
|
|
4762
5636
|
checkStateCoverage,
|
|
4763
5637
|
checkStateMatrix,
|
|
4764
5638
|
compileRoute,
|
|
4765
|
-
|
|
4766
|
-
|
|
5639
|
+
computeMatrixBacklog,
|
|
5640
|
+
computePageBacklog,
|
|
5641
|
+
coverageBaselinePath,
|
|
4767
5642
|
detectRoutes,
|
|
4768
5643
|
discover,
|
|
4769
5644
|
emit,
|
|
5645
|
+
entityAcknowledge,
|
|
4770
5646
|
extract,
|
|
4771
5647
|
extractUidexExports,
|
|
5648
|
+
findWorkspaceRoot,
|
|
4772
5649
|
globToRegExp,
|
|
4773
|
-
|
|
5650
|
+
isLegacyBaseline,
|
|
5651
|
+
loadCoverageBaseline,
|
|
4774
5652
|
matchUrlToRoute,
|
|
4775
|
-
|
|
5653
|
+
mergeStates,
|
|
5654
|
+
migrateBaseline,
|
|
5655
|
+
migrateLegacyBaseline,
|
|
5656
|
+
migrateLegacyFields,
|
|
4776
5657
|
parseConfig,
|
|
5658
|
+
parseCoverageBaseline,
|
|
4777
5659
|
pathToId,
|
|
4778
5660
|
renameEntity,
|
|
4779
5661
|
resolve,
|
|
4780
5662
|
resolveGitContext,
|
|
5663
|
+
resolveWorkspace,
|
|
5664
|
+
routeAcknowledge,
|
|
4781
5665
|
runCli,
|
|
4782
5666
|
runScan,
|
|
4783
5667
|
scaffoldSpec,
|
|
4784
5668
|
scaffoldWidgetSpec,
|
|
5669
|
+
serializeCoverageBaseline,
|
|
4785
5670
|
validateConfig,
|
|
4786
5671
|
walk,
|
|
4787
5672
|
writeScanResult
|