uidex 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1273 -343
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +3 -0
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +19 -13
- package/dist/headless/index.d.ts +19 -13
- package/dist/headless/index.js +3 -0
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -13
- package/dist/index.d.ts +19 -13
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +36 -7
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.js +36 -7
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +36 -7
- package/dist/playwright/states-reporter.cjs.map +1 -1
- package/dist/playwright/states-reporter.d.cts +15 -0
- package/dist/playwright/states-reporter.d.ts +15 -0
- package/dist/playwright/states-reporter.js +36 -7
- package/dist/playwright/states-reporter.js.map +1 -1
- package/dist/playwright/states.cjs +8 -0
- package/dist/playwright/states.cjs.map +1 -1
- package/dist/playwright/states.d.cts +44 -1
- package/dist/playwright/states.d.ts +44 -1
- package/dist/playwright/states.js +7 -0
- package/dist/playwright/states.js.map +1 -1
- package/dist/react/index.cjs +3 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +19 -13
- package/dist/react/index.d.ts +19 -13
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1217 -257
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +410 -104
- package/dist/scan/index.d.ts +410 -104
- package/dist/scan/index.js +1197 -253
- package/dist/scan/index.js.map +1 -1
- package/package.json +17 -17
package/dist/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
|
}
|
|
@@ -202,7 +283,13 @@ function validateConfig(raw) {
|
|
|
202
283
|
fail(`conventions.features must be a string or false`);
|
|
203
284
|
}
|
|
204
285
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
205
|
-
|
|
286
|
+
const p2 = c.pages;
|
|
287
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
288
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
289
|
+
fail(
|
|
290
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
206
293
|
}
|
|
207
294
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
208
295
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -220,7 +307,8 @@ function validateConfig(raw) {
|
|
|
220
307
|
audit: raw.audit,
|
|
221
308
|
conventions: raw.conventions,
|
|
222
309
|
statesManifest: raw.statesManifest,
|
|
223
|
-
coverageBaseline: raw.coverageBaseline
|
|
310
|
+
coverageBaseline: raw.coverageBaseline,
|
|
311
|
+
workspace: raw.workspace
|
|
224
312
|
};
|
|
225
313
|
}
|
|
226
314
|
function parseConfig(json) {
|
|
@@ -270,9 +358,11 @@ function tryReadConfig(configPath) {
|
|
|
270
358
|
function discover(options = {}) {
|
|
271
359
|
const cwd = options.cwd ?? process.cwd();
|
|
272
360
|
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
361
|
+
const all = options.all ?? false;
|
|
273
362
|
const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
|
|
274
|
-
if (rootMatch) return [rootMatch];
|
|
363
|
+
if (rootMatch && !all) return [rootMatch];
|
|
275
364
|
const results = [];
|
|
365
|
+
if (rootMatch) results.push(rootMatch);
|
|
276
366
|
const queue = [[cwd, 0]];
|
|
277
367
|
while (queue.length > 0) {
|
|
278
368
|
const [dir, depth] = queue.shift();
|
|
@@ -376,6 +466,26 @@ function toPosix(p2) {
|
|
|
376
466
|
function matchesAny(rel, patterns) {
|
|
377
467
|
return patterns.some((g) => globToRegExp(g).test(rel));
|
|
378
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
|
+
}
|
|
379
489
|
function walk(sources, options) {
|
|
380
490
|
const { cwd, globalExcludes = [], includeTests = false } = options;
|
|
381
491
|
const out2 = [];
|
|
@@ -709,6 +819,132 @@ function createRegistry() {
|
|
|
709
819
|
};
|
|
710
820
|
}
|
|
711
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
|
+
|
|
712
948
|
// src/scanner/scan/extract-uidex-export.ts
|
|
713
949
|
var KIND_DISCRIMINATORS = [
|
|
714
950
|
"page",
|
|
@@ -727,26 +963,41 @@ var ALLOWED_FIELDS = {
|
|
|
727
963
|
"widgets",
|
|
728
964
|
"acceptance",
|
|
729
965
|
"states",
|
|
966
|
+
"acknowledge",
|
|
730
967
|
"capture",
|
|
731
968
|
"waivers",
|
|
732
969
|
"description"
|
|
733
970
|
]),
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
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).
|
|
737
977
|
feature: /* @__PURE__ */ new Set([
|
|
738
978
|
"feature",
|
|
739
979
|
"name",
|
|
740
980
|
"features",
|
|
741
981
|
"acceptance",
|
|
742
982
|
"states",
|
|
983
|
+
"acknowledge",
|
|
984
|
+
"capture",
|
|
743
985
|
"description"
|
|
744
986
|
]),
|
|
745
987
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
746
|
-
widget: /* @__PURE__ */ new Set([
|
|
988
|
+
widget: /* @__PURE__ */ new Set([
|
|
989
|
+
"widget",
|
|
990
|
+
"name",
|
|
991
|
+
"acceptance",
|
|
992
|
+
"states",
|
|
993
|
+
"acknowledge",
|
|
994
|
+
"capture",
|
|
995
|
+
"description"
|
|
996
|
+
]),
|
|
747
997
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
748
998
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
749
999
|
};
|
|
1000
|
+
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
750
1001
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
751
1002
|
"name",
|
|
752
1003
|
"kind",
|
|
@@ -997,7 +1248,8 @@ function objectLit(node, content, p2, pos, span) {
|
|
|
997
1248
|
key,
|
|
998
1249
|
value,
|
|
999
1250
|
keyPos,
|
|
1000
|
-
span: removalSpan(content, prop.start, prop.end)
|
|
1251
|
+
span: removalSpan(content, prop.start, prop.end),
|
|
1252
|
+
propSpan: { start: prop.start, end: prop.end }
|
|
1001
1253
|
});
|
|
1002
1254
|
}
|
|
1003
1255
|
return { kind: "object", entries, pos, span };
|
|
@@ -1145,8 +1397,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1145
1397
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1146
1398
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1147
1399
|
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1148
|
-
const
|
|
1149
|
-
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;
|
|
1150
1404
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1151
1405
|
const metadata = {
|
|
1152
1406
|
source: "ts-export",
|
|
@@ -1170,15 +1424,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1170
1424
|
metadata.widgetSpans = widgetsField.spans;
|
|
1171
1425
|
}
|
|
1172
1426
|
if (states?.length) metadata.states = states;
|
|
1173
|
-
if (
|
|
1174
|
-
|
|
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
|
+
}
|
|
1175
1434
|
if (notFlow) metadata.notFlow = true;
|
|
1176
1435
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1177
1436
|
metadata.idSpan = idValue.span;
|
|
1178
1437
|
}
|
|
1179
1438
|
const fieldSpans = {};
|
|
1180
|
-
|
|
1439
|
+
const fieldPropSpans = {};
|
|
1440
|
+
for (const entry of value.entries) {
|
|
1441
|
+
fieldSpans[entry.key] = entry.span;
|
|
1442
|
+
fieldPropSpans[entry.key] = entry.propSpan;
|
|
1443
|
+
}
|
|
1181
1444
|
metadata.fieldSpans = fieldSpans;
|
|
1445
|
+
metadata.fieldPropSpans = fieldPropSpans;
|
|
1182
1446
|
return metadata;
|
|
1183
1447
|
}
|
|
1184
1448
|
function readIdField(value, kind, fieldName) {
|
|
@@ -1208,6 +1472,80 @@ function readIdField(value, kind, fieldName) {
|
|
|
1208
1472
|
value.pos
|
|
1209
1473
|
);
|
|
1210
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
|
+
}
|
|
1211
1549
|
function readWaiversField(byKey) {
|
|
1212
1550
|
const entry = byKey.get("waivers");
|
|
1213
1551
|
if (!entry) return void 0;
|
|
@@ -1887,13 +2225,26 @@ var path4 = __toESM(require("path"), 1);
|
|
|
1887
2225
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1888
2226
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1889
2227
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1890
|
-
function detectRoutes(files) {
|
|
2228
|
+
function detectRoutes(files, options = {}) {
|
|
1891
2229
|
const out2 = [];
|
|
1892
2230
|
const seen = /* @__PURE__ */ new Set();
|
|
2231
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
2232
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
1893
2233
|
for (const f of files) {
|
|
1894
2234
|
const rel = f.displayPath;
|
|
1895
2235
|
const parts = rel.split("/");
|
|
1896
2236
|
const base = parts[parts.length - 1];
|
|
2237
|
+
if (routesDirParts) {
|
|
2238
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
2239
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
2240
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
2241
|
+
const routePath = routesStylePath(
|
|
2242
|
+
parts.slice(end, parts.length - 1),
|
|
2243
|
+
base
|
|
2244
|
+
);
|
|
2245
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
1897
2248
|
const appIdx = parts.indexOf("app");
|
|
1898
2249
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
1899
2250
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -1920,15 +2271,11 @@ function detectRoutes(files) {
|
|
|
1920
2271
|
}
|
|
1921
2272
|
const routesIdx = parts.indexOf("routes");
|
|
1922
2273
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
base.replace(/\.[^.]+$/, "")
|
|
1929
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
1930
|
-
const routePath = formatNextAppPath(normalized);
|
|
1931
|
-
push(out2, seen, routePath, f.displayPath);
|
|
2274
|
+
const routePath = routesStylePath(
|
|
2275
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
2276
|
+
base
|
|
2277
|
+
);
|
|
2278
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1932
2279
|
continue;
|
|
1933
2280
|
}
|
|
1934
2281
|
}
|
|
@@ -1944,6 +2291,63 @@ function formatNextAppPath(segments) {
|
|
|
1944
2291
|
if (kept.length === 0) return "/";
|
|
1945
2292
|
return "/" + kept.join("/");
|
|
1946
2293
|
}
|
|
2294
|
+
function dirEndIndex(parts, dirParts) {
|
|
2295
|
+
if (dirParts.length === 0) return 0;
|
|
2296
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
2297
|
+
let match = true;
|
|
2298
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
2299
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
2300
|
+
match = false;
|
|
2301
|
+
break;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (match) return i + dirParts.length;
|
|
2305
|
+
}
|
|
2306
|
+
return -1;
|
|
2307
|
+
}
|
|
2308
|
+
function routesStylePath(dirSegs, base) {
|
|
2309
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
2310
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
2311
|
+
if (leaf.startsWith("_")) return null;
|
|
2312
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
2313
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
2314
|
+
if (rawSegs[0] === "api") return null;
|
|
2315
|
+
return formatTanStackPath(rawSegs);
|
|
2316
|
+
}
|
|
2317
|
+
function splitFlatSegments(name) {
|
|
2318
|
+
const segs = [];
|
|
2319
|
+
let cur = "";
|
|
2320
|
+
let depth = 0;
|
|
2321
|
+
for (const ch of name) {
|
|
2322
|
+
if (ch === "[") depth++;
|
|
2323
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
2324
|
+
if (ch === "." && depth === 0) {
|
|
2325
|
+
segs.push(cur);
|
|
2326
|
+
cur = "";
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
cur += ch;
|
|
2330
|
+
}
|
|
2331
|
+
segs.push(cur);
|
|
2332
|
+
return segs;
|
|
2333
|
+
}
|
|
2334
|
+
function formatTanStackPath(segments) {
|
|
2335
|
+
const kept = [];
|
|
2336
|
+
for (const seg of segments) {
|
|
2337
|
+
if (!seg) continue;
|
|
2338
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
2339
|
+
if (seg.startsWith("_")) continue;
|
|
2340
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
2341
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
2342
|
+
}
|
|
2343
|
+
if (kept.length === 0) return "/";
|
|
2344
|
+
return "/" + kept.join("/");
|
|
2345
|
+
}
|
|
2346
|
+
function normalizeTanStackSegment(seg) {
|
|
2347
|
+
if (seg === "$") return "[...splat]";
|
|
2348
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
2349
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
2350
|
+
}
|
|
1947
2351
|
function pathToId(routePath) {
|
|
1948
2352
|
if (routePath === "/") return "root";
|
|
1949
2353
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1990,9 +2394,8 @@ function buildMetaFromExport(exp) {
|
|
|
1990
2394
|
...s.description ? { description: s.description } : {}
|
|
1991
2395
|
}));
|
|
1992
2396
|
}
|
|
1993
|
-
if (exp.
|
|
1994
|
-
|
|
1995
|
-
meta.waivers = exp.waivers;
|
|
2397
|
+
if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
|
|
2398
|
+
meta.acknowledge = exp.acknowledge;
|
|
1996
2399
|
}
|
|
1997
2400
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1998
2401
|
}
|
|
@@ -2039,9 +2442,12 @@ function resolve2(ctx) {
|
|
|
2039
2442
|
function metaWithComposes(kind, id, base) {
|
|
2040
2443
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
2041
2444
|
if (!composes || composes.length === 0) return base;
|
|
2042
|
-
return { ...base
|
|
2445
|
+
return { ...base, composes };
|
|
2043
2446
|
}
|
|
2044
|
-
const routes = conventions.pages ===
|
|
2447
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
2448
|
+
ctx.extracted.map((e) => e.file),
|
|
2449
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
2450
|
+
);
|
|
2045
2451
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
2046
2452
|
for (const route of routes) {
|
|
2047
2453
|
const routeDir = path4.posix.dirname(route.file);
|
|
@@ -2334,6 +2740,45 @@ function resolve2(ctx) {
|
|
|
2334
2740
|
};
|
|
2335
2741
|
registry.add(element);
|
|
2336
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
|
+
};
|
|
2337
2782
|
if (ctx.flowFiles && conventions.flows) {
|
|
2338
2783
|
for (const ff of ctx.flowFiles) {
|
|
2339
2784
|
const notFlowExport = (ff.metadata ?? []).find(
|
|
@@ -2343,7 +2788,10 @@ function resolve2(ctx) {
|
|
|
2343
2788
|
const flowExport = (ff.metadata ?? []).find(
|
|
2344
2789
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2345
2790
|
);
|
|
2346
|
-
const derived = flowsFromFacts(ff)
|
|
2791
|
+
const derived = flowsFromFacts(ff).map((f) => ({
|
|
2792
|
+
...f,
|
|
2793
|
+
touches: expandTouches(f.touches)
|
|
2794
|
+
}));
|
|
2347
2795
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2348
2796
|
const base = derived[0];
|
|
2349
2797
|
const flow = {
|
|
@@ -2412,7 +2860,195 @@ function dedupe(arr) {
|
|
|
2412
2860
|
}
|
|
2413
2861
|
|
|
2414
2862
|
// src/scanner/scan/audit.ts
|
|
2415
|
-
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
|
+
}
|
|
2416
3052
|
|
|
2417
3053
|
// src/scanner/scan/page-coverage.ts
|
|
2418
3054
|
function compileRoute(pattern) {
|
|
@@ -2435,11 +3071,11 @@ function compileRoute(pattern) {
|
|
|
2435
3071
|
return { test: (url) => re.test(url), specificity };
|
|
2436
3072
|
}
|
|
2437
3073
|
function matchUrlToRoute(url, routePaths) {
|
|
2438
|
-
const
|
|
3074
|
+
const path13 = stripQuery(url);
|
|
2439
3075
|
let best = null;
|
|
2440
3076
|
for (const rp of routePaths) {
|
|
2441
3077
|
const { test, specificity } = compileRoute(rp);
|
|
2442
|
-
if (!test(
|
|
3078
|
+
if (!test(path13)) continue;
|
|
2443
3079
|
if (!best || specificity > best.specificity)
|
|
2444
3080
|
best = { path: rp, specificity };
|
|
2445
3081
|
}
|
|
@@ -2470,29 +3106,49 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2470
3106
|
if (routes.length === 0) return [];
|
|
2471
3107
|
const routePaths = routes.map((r) => r.path);
|
|
2472
3108
|
const captured = manifest.captured ?? [];
|
|
2473
|
-
const optedOut = /* @__PURE__ */ new Set();
|
|
2474
|
-
for (const r of routes) {
|
|
2475
|
-
const page = registry.get("page", r.page);
|
|
2476
|
-
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2477
|
-
}
|
|
2478
3109
|
const covered = /* @__PURE__ */ new Set();
|
|
2479
3110
|
for (const c of captured) {
|
|
2480
3111
|
const route = routeForCapture(c, routes, routePaths);
|
|
2481
3112
|
if (route) covered.add(route);
|
|
2482
3113
|
}
|
|
2483
|
-
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2484
3114
|
const diagnostics = [];
|
|
2485
3115
|
for (const r of routes) {
|
|
2486
|
-
|
|
3116
|
+
const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
|
|
3117
|
+
ACK_ALL
|
|
3118
|
+
);
|
|
2487
3119
|
const loc = registry.get("page", r.page)?.loc;
|
|
2488
|
-
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") {
|
|
2489
3145
|
diagnostics.push({
|
|
2490
3146
|
code: "page-backlog",
|
|
2491
3147
|
severity: "info",
|
|
2492
3148
|
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2493
3149
|
file: loc?.file,
|
|
2494
3150
|
line: loc?.line,
|
|
2495
|
-
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}".`
|
|
2496
3152
|
});
|
|
2497
3153
|
continue;
|
|
2498
3154
|
}
|
|
@@ -2503,46 +3159,155 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2503
3159
|
file: loc?.file,
|
|
2504
3160
|
line: loc?.line,
|
|
2505
3161
|
entity: { kind: "route", id: r.path },
|
|
2506
|
-
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).`
|
|
2507
3163
|
});
|
|
2508
3164
|
}
|
|
2509
|
-
for (const b of
|
|
2510
|
-
if (
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2518
|
-
diagnostics.push({
|
|
2519
|
-
code: "page-captured",
|
|
2520
|
-
severity: "warning",
|
|
2521
|
-
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2522
|
-
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2523
|
-
});
|
|
2524
|
-
}
|
|
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
|
+
});
|
|
2525
3173
|
}
|
|
2526
3174
|
return diagnostics;
|
|
2527
3175
|
}
|
|
2528
|
-
function
|
|
2529
|
-
const
|
|
3176
|
+
function computePageBacklog(registry, manifest) {
|
|
3177
|
+
const out2 = {};
|
|
2530
3178
|
for (const d of checkPageCoverage(registry, manifest, {
|
|
2531
|
-
|
|
3179
|
+
version: 2,
|
|
3180
|
+
acknowledge: {}
|
|
2532
3181
|
})) {
|
|
2533
|
-
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
|
+
}
|
|
3185
|
+
}
|
|
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;
|
|
2534
3248
|
}
|
|
2535
|
-
|
|
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;
|
|
2536
3282
|
}
|
|
2537
3283
|
|
|
2538
3284
|
// src/scanner/scan/state-coverage.ts
|
|
2539
3285
|
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2540
|
-
function declaredStateEntities(registry) {
|
|
3286
|
+
function declaredStateEntities(registry, opts, ungated) {
|
|
2541
3287
|
const out2 = [];
|
|
2542
3288
|
for (const kind of STATE_KINDS2) {
|
|
2543
3289
|
for (const e of registry.list(kind)) {
|
|
2544
3290
|
const states = e.meta?.states;
|
|
2545
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
|
+
}
|
|
2546
3311
|
out2.push({
|
|
2547
3312
|
kind,
|
|
2548
3313
|
id: e.id,
|
|
@@ -2553,18 +3318,31 @@ function declaredStateEntities(registry) {
|
|
|
2553
3318
|
}
|
|
2554
3319
|
return out2;
|
|
2555
3320
|
}
|
|
2556
|
-
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) {
|
|
2557
3331
|
if (cap.entity !== ent.id) return false;
|
|
2558
|
-
|
|
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);
|
|
2559
3335
|
}
|
|
2560
|
-
|
|
3336
|
+
var EMPTY = /* @__PURE__ */ new Set();
|
|
3337
|
+
function checkStateCoverage(registry, manifest, opts = {}) {
|
|
2561
3338
|
const diagnostics = [];
|
|
2562
|
-
const declared = declaredStateEntities(registry);
|
|
2563
3339
|
const captured = manifest.captured ?? [];
|
|
3340
|
+
const unreliable = unreliablePageKinds(registry, captured);
|
|
3341
|
+
const declared = declaredStateEntities(registry, opts, diagnostics);
|
|
2564
3342
|
for (const ent of declared) {
|
|
2565
3343
|
for (const state of ent.states) {
|
|
2566
3344
|
const hit = captured.some(
|
|
2567
|
-
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
3345
|
+
(c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
|
|
2568
3346
|
);
|
|
2569
3347
|
if (hit) continue;
|
|
2570
3348
|
diagnostics.push({
|
|
@@ -2579,7 +3357,9 @@ function checkStateCoverage(registry, manifest) {
|
|
|
2579
3357
|
}
|
|
2580
3358
|
}
|
|
2581
3359
|
for (const cap of captured) {
|
|
2582
|
-
const candidates = declared.filter(
|
|
3360
|
+
const candidates = declared.filter(
|
|
3361
|
+
(ent2) => captureMatchesEntity(cap, ent2, unreliable)
|
|
3362
|
+
);
|
|
2583
3363
|
if (candidates.length === 0) continue;
|
|
2584
3364
|
const declaredSomewhere = candidates.some(
|
|
2585
3365
|
(ent2) => ent2.states.includes(cap.state)
|
|
@@ -2623,36 +3403,35 @@ function coreKindsByRoute(registry, manifest) {
|
|
|
2623
3403
|
}
|
|
2624
3404
|
return byRoute;
|
|
2625
3405
|
}
|
|
2626
|
-
function pageWaivers(registry, routePage) {
|
|
2627
|
-
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2628
|
-
}
|
|
2629
|
-
function isOptedOut(registry, page) {
|
|
2630
|
-
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2631
|
-
}
|
|
2632
3406
|
function checkStateMatrix(registry, manifest, baseline) {
|
|
2633
3407
|
const routes = registry.list("route");
|
|
2634
3408
|
if (routes.length === 0) return [];
|
|
2635
3409
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2636
3410
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2637
|
-
const baselined = baseline?.missingKinds ?? {};
|
|
2638
3411
|
const diagnostics = [];
|
|
2639
3412
|
for (const [route, kinds] of captured) {
|
|
2640
3413
|
const page = pageOf.get(route);
|
|
2641
|
-
|
|
2642
|
-
const waivers = page ? pageWaivers(registry, page) : {};
|
|
3414
|
+
const ack = routeAcknowledge(registry, baseline, route, page);
|
|
2643
3415
|
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2644
|
-
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
|
+
}
|
|
2645
3423
|
for (const kind of CORE_KINDS) {
|
|
2646
3424
|
if (kinds.has(kind)) continue;
|
|
2647
|
-
|
|
2648
|
-
if (
|
|
3425
|
+
const entry = ack.get(kind);
|
|
3426
|
+
if (entry?.type === "impossible") continue;
|
|
3427
|
+
if (entry?.type === "backlog") {
|
|
2649
3428
|
diagnostics.push({
|
|
2650
3429
|
code: "matrix-backlog",
|
|
2651
3430
|
severity: "info",
|
|
2652
|
-
message: `route "${route}" does not render core kind "${kind}" (acknowledged
|
|
3431
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
|
|
2653
3432
|
file: loc?.file,
|
|
2654
3433
|
line: loc?.line,
|
|
2655
|
-
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" }\`.`
|
|
2656
3435
|
});
|
|
2657
3436
|
continue;
|
|
2658
3437
|
}
|
|
@@ -2663,42 +3442,43 @@ function checkStateMatrix(registry, manifest, baseline) {
|
|
|
2663
3442
|
file: loc?.file,
|
|
2664
3443
|
line: loc?.line,
|
|
2665
3444
|
entity: { kind: "route", id: route },
|
|
2666
|
-
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).`
|
|
2667
3446
|
});
|
|
2668
3447
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if (!page || isOptedOut(registry, page)) continue;
|
|
2673
|
-
const waivers = pageWaivers(registry, page);
|
|
2674
|
-
const loc = registry.get("page", page)?.loc;
|
|
2675
|
-
for (const kind of Object.keys(waivers)) {
|
|
2676
|
-
if (kinds.has(kind)) {
|
|
2677
|
-
diagnostics.push({
|
|
2678
|
-
code: "stale-waiver",
|
|
2679
|
-
severity: "warning",
|
|
2680
|
-
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2681
|
-
file: loc?.file,
|
|
2682
|
-
line: loc?.line,
|
|
2683
|
-
entity: { kind: "page", id: page },
|
|
2684
|
-
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
3448
|
+
for (const [scope, entry] of ack) {
|
|
3449
|
+
if (scope === ACK_ALL || !kinds.has(scope)) continue;
|
|
3450
|
+
diagnostics.push(staleDiagnostic(entry, route, page, loc));
|
|
2687
3451
|
}
|
|
2688
3452
|
}
|
|
2689
3453
|
return diagnostics;
|
|
2690
3454
|
}
|
|
2691
|
-
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) {
|
|
2692
3468
|
const routes = registry.list("route");
|
|
2693
3469
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2694
3470
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2695
3471
|
const out2 = {};
|
|
2696
3472
|
for (const [route, kinds] of captured) {
|
|
2697
3473
|
const page = pageOf.get(route);
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
const
|
|
2701
|
-
|
|
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;
|
|
2702
3482
|
}
|
|
2703
3483
|
return out2;
|
|
2704
3484
|
}
|
|
@@ -2722,6 +3502,23 @@ function audit(opts) {
|
|
|
2722
3502
|
for (const ef of opts.flowExtracted ?? []) {
|
|
2723
3503
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2724
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
|
+
}
|
|
2725
3522
|
if (check && opts.generated !== void 0) {
|
|
2726
3523
|
const outRel = opts.outputPath ?? config.output;
|
|
2727
3524
|
const fresh = normalizeForCheck(opts.generated);
|
|
@@ -2835,8 +3632,8 @@ function audit(opts) {
|
|
|
2835
3632
|
if (typeof m.id !== "string") continue;
|
|
2836
3633
|
const filePath = ef.file.displayPath;
|
|
2837
3634
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2838
|
-
if (
|
|
2839
|
-
const dir =
|
|
3635
|
+
if (path6.posix.basename(filePath) === wellKnownName) continue;
|
|
3636
|
+
const dir = path6.posix.dirname(filePath);
|
|
2840
3637
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2841
3638
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2842
3639
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -3050,25 +3847,41 @@ function audit(opts) {
|
|
|
3050
3847
|
}
|
|
3051
3848
|
}
|
|
3052
3849
|
if (lint && statesEnabled && opts.capturedStates) {
|
|
3053
|
-
|
|
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
|
+
);
|
|
3054
3859
|
}
|
|
3055
3860
|
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
3056
3861
|
diagnostics.push(
|
|
3057
|
-
...checkPageCoverage(registry, opts.capturedStates, opts.
|
|
3862
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
|
|
3058
3863
|
);
|
|
3059
3864
|
}
|
|
3865
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3866
|
+
diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
|
|
3867
|
+
}
|
|
3060
3868
|
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
3061
3869
|
diagnostics.push(
|
|
3062
|
-
...checkStateMatrix(registry, opts.capturedStates,
|
|
3063
|
-
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
3064
|
-
})
|
|
3870
|
+
...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
|
|
3065
3871
|
);
|
|
3066
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
|
+
});
|
|
3067
3880
|
const summary = {
|
|
3068
|
-
errors:
|
|
3069
|
-
warnings:
|
|
3881
|
+
errors: governed.filter((d) => d.severity === "error").length,
|
|
3882
|
+
warnings: governed.filter((d) => d.severity === "warning").length
|
|
3070
3883
|
};
|
|
3071
|
-
return { diagnostics, summary };
|
|
3884
|
+
return { diagnostics: governed, summary };
|
|
3072
3885
|
}
|
|
3073
3886
|
function lineOfOffset(content, offset) {
|
|
3074
3887
|
let line = 1;
|
|
@@ -3396,6 +4209,30 @@ ${body}
|
|
|
3396
4209
|
`;
|
|
3397
4210
|
}
|
|
3398
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
|
+
}
|
|
3399
4236
|
function emitId(name, ids) {
|
|
3400
4237
|
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3401
4238
|
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
@@ -3483,6 +4320,14 @@ function emit(opts) {
|
|
|
3483
4320
|
);
|
|
3484
4321
|
t.push(emitId("StateId", [...stateNames]));
|
|
3485
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
|
+
);
|
|
3486
4331
|
t.push("// ---- authoring-surface shape types ----");
|
|
3487
4332
|
t.push("export namespace Uidex {");
|
|
3488
4333
|
t.push(
|
|
@@ -3495,7 +4340,20 @@ function emit(opts) {
|
|
|
3495
4340
|
t.push(" acceptance?: readonly string[]");
|
|
3496
4341
|
t.push(" description?: string");
|
|
3497
4342
|
t.push(" }");
|
|
3498
|
-
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 }');
|
|
3499
4357
|
t.push(" export interface Page {");
|
|
3500
4358
|
t.push(" page: PageId | false");
|
|
3501
4359
|
t.push(" name?: string");
|
|
@@ -3503,8 +4361,7 @@ function emit(opts) {
|
|
|
3503
4361
|
t.push(" widgets?: readonly WidgetId[]");
|
|
3504
4362
|
t.push(" acceptance?: readonly string[]");
|
|
3505
4363
|
t.push(" states?: readonly (string | State)[]");
|
|
3506
|
-
t.push("
|
|
3507
|
-
t.push(" waivers?: Waivers");
|
|
4364
|
+
t.push(" acknowledge?: Acknowledge");
|
|
3508
4365
|
t.push(" description?: string");
|
|
3509
4366
|
t.push(" }");
|
|
3510
4367
|
t.push(" export interface Feature {");
|
|
@@ -3513,6 +4370,7 @@ function emit(opts) {
|
|
|
3513
4370
|
t.push(" features?: readonly FeatureId[]");
|
|
3514
4371
|
t.push(" acceptance?: readonly string[]");
|
|
3515
4372
|
t.push(" states?: readonly (string | State)[]");
|
|
4373
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3516
4374
|
t.push(" description?: string");
|
|
3517
4375
|
t.push(" }");
|
|
3518
4376
|
t.push(" export interface Primitive {");
|
|
@@ -3525,6 +4383,7 @@ function emit(opts) {
|
|
|
3525
4383
|
t.push(" name?: string");
|
|
3526
4384
|
t.push(" acceptance?: readonly string[]");
|
|
3527
4385
|
t.push(" states?: readonly (string | State)[]");
|
|
4386
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3528
4387
|
t.push(" description?: string");
|
|
3529
4388
|
t.push(" }");
|
|
3530
4389
|
t.push(" export interface Region {");
|
|
@@ -3620,8 +4479,8 @@ function parseGitHubRef(ref) {
|
|
|
3620
4479
|
}
|
|
3621
4480
|
|
|
3622
4481
|
// src/scanner/scan/scaffold.ts
|
|
3623
|
-
var
|
|
3624
|
-
var
|
|
4482
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
4483
|
+
var path7 = __toESM(require("path"), 1);
|
|
3625
4484
|
function scaffoldWidgetSpec(opts) {
|
|
3626
4485
|
return scaffoldSpec({
|
|
3627
4486
|
registry: opts.registry,
|
|
@@ -3647,8 +4506,8 @@ function scaffoldSpec(opts) {
|
|
|
3647
4506
|
}
|
|
3648
4507
|
const criteria = entity.meta?.acceptance ?? [];
|
|
3649
4508
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
3650
|
-
const outputPath =
|
|
3651
|
-
if (
|
|
4509
|
+
const outputPath = path7.resolve(outDir, filename);
|
|
4510
|
+
if (fs4.existsSync(outputPath) && !force) {
|
|
3652
4511
|
return {
|
|
3653
4512
|
outputPath,
|
|
3654
4513
|
written: false,
|
|
@@ -3657,8 +4516,8 @@ function scaffoldSpec(opts) {
|
|
|
3657
4516
|
};
|
|
3658
4517
|
}
|
|
3659
4518
|
const content = renderSpec({ id, criteria, fixtureImport });
|
|
3660
|
-
|
|
3661
|
-
|
|
4519
|
+
fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
|
|
4520
|
+
fs4.writeFileSync(outputPath, content, "utf8");
|
|
3662
4521
|
return { outputPath, written: true, skipped: false };
|
|
3663
4522
|
}
|
|
3664
4523
|
function capitalize(s) {
|
|
@@ -3666,25 +4525,27 @@ function capitalize(s) {
|
|
|
3666
4525
|
}
|
|
3667
4526
|
function renderSpec(args) {
|
|
3668
4527
|
const lines = [];
|
|
3669
|
-
lines.push(
|
|
3670
|
-
`import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
|
|
3671
|
-
);
|
|
4528
|
+
lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
|
|
3672
4529
|
lines.push("");
|
|
3673
4530
|
lines.push(
|
|
3674
4531
|
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3675
4532
|
);
|
|
3676
|
-
|
|
3677
|
-
lines.push(` test(
|
|
3678
|
-
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`);
|
|
3679
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
|
+
]);
|
|
3680
4544
|
} else {
|
|
3681
4545
|
for (const criterion of args.criteria) {
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
lines.push(` expect(true).toBe(true)`);
|
|
3686
|
-
lines.push(` })`);
|
|
3687
|
-
lines.push("");
|
|
4546
|
+
stub(criterion, [
|
|
4547
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test."
|
|
4548
|
+
]);
|
|
3688
4549
|
}
|
|
3689
4550
|
}
|
|
3690
4551
|
lines.push("})");
|
|
@@ -3693,47 +4554,40 @@ function renderSpec(args) {
|
|
|
3693
4554
|
}
|
|
3694
4555
|
|
|
3695
4556
|
// src/scanner/scan/pipeline.ts
|
|
3696
|
-
var
|
|
3697
|
-
var
|
|
3698
|
-
var
|
|
4557
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
4558
|
+
var path8 = __toESM(require("path"), 1);
|
|
4559
|
+
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
3699
4560
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3700
|
-
function
|
|
4561
|
+
function loadCoverageBaseline(configDir, config) {
|
|
3701
4562
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3702
|
-
const abs =
|
|
4563
|
+
const abs = path8.resolve(configDir, rel);
|
|
3703
4564
|
let raw;
|
|
3704
4565
|
try {
|
|
3705
|
-
raw =
|
|
4566
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3706
4567
|
} catch {
|
|
3707
|
-
return
|
|
4568
|
+
return {};
|
|
3708
4569
|
}
|
|
4570
|
+
let parsed;
|
|
3709
4571
|
try {
|
|
3710
|
-
|
|
3711
|
-
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3712
|
-
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3713
|
-
(p2) => typeof p2 === "string"
|
|
3714
|
-
);
|
|
3715
|
-
let missingKinds;
|
|
3716
|
-
const mkRaw = parsed.missingKinds;
|
|
3717
|
-
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3718
|
-
const mk = {};
|
|
3719
|
-
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3720
|
-
if (Array.isArray(kinds)) {
|
|
3721
|
-
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3722
|
-
}
|
|
3723
|
-
}
|
|
3724
|
-
missingKinds = mk;
|
|
3725
|
-
}
|
|
3726
|
-
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
4572
|
+
parsed = JSON.parse(raw);
|
|
3727
4573
|
} catch {
|
|
3728
|
-
return
|
|
4574
|
+
return {};
|
|
3729
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 };
|
|
3730
4584
|
}
|
|
3731
4585
|
function loadCapturedStates(configDir, config) {
|
|
3732
|
-
const rel = config.statesManifest ??
|
|
3733
|
-
const abs =
|
|
4586
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4587
|
+
const abs = path8.resolve(configDir, rel);
|
|
3734
4588
|
let raw;
|
|
3735
4589
|
try {
|
|
3736
|
-
raw =
|
|
4590
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3737
4591
|
} catch {
|
|
3738
4592
|
return void 0;
|
|
3739
4593
|
}
|
|
@@ -3752,7 +4606,7 @@ function loadCapturedStates(configDir, config) {
|
|
|
3752
4606
|
if (typeof c.route === "string") rec.route = c.route;
|
|
3753
4607
|
captured.push(rec);
|
|
3754
4608
|
}
|
|
3755
|
-
return { captured };
|
|
4609
|
+
return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
|
|
3756
4610
|
} catch {
|
|
3757
4611
|
return void 0;
|
|
3758
4612
|
}
|
|
@@ -3788,11 +4642,11 @@ function runOne(dc, opts) {
|
|
|
3788
4642
|
gitContext
|
|
3789
4643
|
});
|
|
3790
4644
|
const typesRel = config.output;
|
|
3791
|
-
const typesPath =
|
|
4645
|
+
const typesPath = path8.resolve(configDir, typesRel);
|
|
3792
4646
|
const dataRel = dataOutputPath(config.output);
|
|
3793
|
-
const dataPath =
|
|
4647
|
+
const dataPath = path8.resolve(configDir, dataRel);
|
|
3794
4648
|
const locsRel = locsOutputPath(config.output);
|
|
3795
|
-
const locsPath =
|
|
4649
|
+
const locsPath = path8.resolve(configDir, locsRel);
|
|
3796
4650
|
let typesOnDisk = null;
|
|
3797
4651
|
let dataOnDisk = null;
|
|
3798
4652
|
let locsOnDisk = null;
|
|
@@ -3805,9 +4659,11 @@ function runOne(dc, opts) {
|
|
|
3805
4659
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3806
4660
|
);
|
|
3807
4661
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3808
|
-
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 });
|
|
3809
4665
|
let auditResult;
|
|
3810
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4666
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
|
|
3811
4667
|
auditResult = audit({
|
|
3812
4668
|
registry: resolved.registry,
|
|
3813
4669
|
extracted,
|
|
@@ -3830,7 +4686,9 @@ function runOne(dc, opts) {
|
|
|
3830
4686
|
locsOnDisk,
|
|
3831
4687
|
locsOutputPath: locsRel,
|
|
3832
4688
|
capturedStates,
|
|
3833
|
-
|
|
4689
|
+
coverageBaseline,
|
|
4690
|
+
baselineDiagnostic: loadedBaseline.legacy,
|
|
4691
|
+
workspace
|
|
3834
4692
|
});
|
|
3835
4693
|
}
|
|
3836
4694
|
return {
|
|
@@ -3851,15 +4709,15 @@ function runOne(dc, opts) {
|
|
|
3851
4709
|
}
|
|
3852
4710
|
function readOrNull(p2) {
|
|
3853
4711
|
try {
|
|
3854
|
-
return
|
|
4712
|
+
return fs5.readFileSync(p2, "utf8");
|
|
3855
4713
|
} catch {
|
|
3856
4714
|
return null;
|
|
3857
4715
|
}
|
|
3858
4716
|
}
|
|
3859
4717
|
function writeArtifact(artifact) {
|
|
3860
4718
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3861
|
-
|
|
3862
|
-
|
|
4719
|
+
fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
|
|
4720
|
+
fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3863
4721
|
return true;
|
|
3864
4722
|
}
|
|
3865
4723
|
function writeScanResult(result) {
|
|
@@ -3868,14 +4726,45 @@ function writeScanResult(result) {
|
|
|
3868
4726
|
const wroteLocs = writeArtifact(result.locs);
|
|
3869
4727
|
return wroteTypes || wroteData || wroteLocs;
|
|
3870
4728
|
}
|
|
3871
|
-
function
|
|
4729
|
+
function coverageBaselinePath(result) {
|
|
3872
4730
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3873
|
-
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
|
+
};
|
|
3874
4763
|
}
|
|
3875
4764
|
|
|
3876
4765
|
// src/scanner/scan/fix.ts
|
|
3877
|
-
var
|
|
3878
|
-
var
|
|
4766
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
4767
|
+
var path9 = __toESM(require("path"), 1);
|
|
3879
4768
|
function applyFixes(diagnostics) {
|
|
3880
4769
|
const entries = [];
|
|
3881
4770
|
for (const d of diagnostics) {
|
|
@@ -3908,7 +4797,7 @@ function applyFixes(diagnostics) {
|
|
|
3908
4797
|
for (const [filePath, edits] of editsByFile) {
|
|
3909
4798
|
let content;
|
|
3910
4799
|
try {
|
|
3911
|
-
content =
|
|
4800
|
+
content = fs6.readFileSync(filePath, "utf8");
|
|
3912
4801
|
} catch {
|
|
3913
4802
|
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
3914
4803
|
continue;
|
|
@@ -3928,22 +4817,22 @@ function applyFixes(diagnostics) {
|
|
|
3928
4817
|
const edit = kept[i];
|
|
3929
4818
|
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
3930
4819
|
}
|
|
3931
|
-
if (kept.length > 0)
|
|
4820
|
+
if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
|
|
3932
4821
|
}
|
|
3933
4822
|
for (const entry of entries) {
|
|
3934
4823
|
if (entry.skippedReason) continue;
|
|
3935
4824
|
for (const create of entry.createFiles) {
|
|
3936
|
-
if (
|
|
3937
|
-
entry.skippedReason = `${
|
|
4825
|
+
if (fs6.existsSync(create.path)) {
|
|
4826
|
+
entry.skippedReason = `${path9.basename(create.path)} already exists`;
|
|
3938
4827
|
continue;
|
|
3939
4828
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
4829
|
+
fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
|
|
4830
|
+
fs6.writeFileSync(create.path, create.content, "utf8");
|
|
3942
4831
|
}
|
|
3943
4832
|
if (entry.skippedReason) continue;
|
|
3944
4833
|
for (const del of entry.deleteFiles) {
|
|
3945
4834
|
try {
|
|
3946
|
-
|
|
4835
|
+
fs6.unlinkSync(del);
|
|
3947
4836
|
} catch {
|
|
3948
4837
|
}
|
|
3949
4838
|
}
|
|
@@ -4121,29 +5010,29 @@ function renameEntity(opts) {
|
|
|
4121
5010
|
}
|
|
4122
5011
|
|
|
4123
5012
|
// src/scanner/scan/cli.ts
|
|
4124
|
-
var
|
|
4125
|
-
var
|
|
5013
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
5014
|
+
var path12 = __toESM(require("path"), 1);
|
|
4126
5015
|
|
|
4127
5016
|
// src/scanner/scan/ai/index.ts
|
|
4128
5017
|
var p = __toESM(require("@clack/prompts"), 1);
|
|
4129
5018
|
|
|
4130
5019
|
// src/scanner/scan/ai/providers/claude.ts
|
|
4131
|
-
var
|
|
4132
|
-
var
|
|
5020
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
5021
|
+
var path11 = __toESM(require("path"), 1);
|
|
4133
5022
|
|
|
4134
5023
|
// src/scanner/scan/ai/templates.ts
|
|
4135
|
-
var
|
|
4136
|
-
var
|
|
5024
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
5025
|
+
var path10 = __toESM(require("path"), 1);
|
|
4137
5026
|
function templatePath(rel) {
|
|
4138
5027
|
const candidates = [
|
|
4139
|
-
|
|
5028
|
+
path10.resolve(__dirname, "../../templates", rel),
|
|
4140
5029
|
// dist/cli/cli.cjs → ../../templates
|
|
4141
|
-
|
|
5030
|
+
path10.resolve(__dirname, "../../../../templates", rel)
|
|
4142
5031
|
// src/scanner/scan/ai → ../../../../templates
|
|
4143
5032
|
];
|
|
4144
5033
|
for (const c of candidates) {
|
|
4145
5034
|
try {
|
|
4146
|
-
|
|
5035
|
+
fs7.accessSync(c, fs7.constants.R_OK);
|
|
4147
5036
|
return c;
|
|
4148
5037
|
} catch {
|
|
4149
5038
|
continue;
|
|
@@ -4155,7 +5044,7 @@ function templatePath(rel) {
|
|
|
4155
5044
|
);
|
|
4156
5045
|
}
|
|
4157
5046
|
function readTemplate(rel) {
|
|
4158
|
-
return
|
|
5047
|
+
return fs7.readFileSync(templatePath(rel), "utf8");
|
|
4159
5048
|
}
|
|
4160
5049
|
|
|
4161
5050
|
// src/scanner/scan/ai/providers/claude.ts
|
|
@@ -4186,8 +5075,8 @@ var claudeProvider = {
|
|
|
4186
5075
|
async install({ cwd, force }) {
|
|
4187
5076
|
const changes = [];
|
|
4188
5077
|
for (const file of SKILL_FILES) {
|
|
4189
|
-
const dest =
|
|
4190
|
-
const exists =
|
|
5078
|
+
const dest = path11.join(cwd, file.dest);
|
|
5079
|
+
const exists = fs8.existsSync(dest);
|
|
4191
5080
|
if (exists && !force) {
|
|
4192
5081
|
changes.push({
|
|
4193
5082
|
path: file.dest,
|
|
@@ -4196,56 +5085,56 @@ var claudeProvider = {
|
|
|
4196
5085
|
});
|
|
4197
5086
|
continue;
|
|
4198
5087
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
5088
|
+
fs8.mkdirSync(path11.dirname(dest), { recursive: true });
|
|
5089
|
+
fs8.writeFileSync(dest, readTemplate(file.template));
|
|
4201
5090
|
changes.push({
|
|
4202
5091
|
path: file.dest,
|
|
4203
5092
|
action: exists ? "overwritten" : "created"
|
|
4204
5093
|
});
|
|
4205
5094
|
}
|
|
4206
5095
|
for (const rel of LEGACY_FILES) {
|
|
4207
|
-
const dest =
|
|
4208
|
-
if (
|
|
4209
|
-
|
|
5096
|
+
const dest = path11.join(cwd, rel);
|
|
5097
|
+
if (fs8.existsSync(dest)) {
|
|
5098
|
+
fs8.unlinkSync(dest);
|
|
4210
5099
|
changes.push({ path: rel, action: "removed" });
|
|
4211
5100
|
}
|
|
4212
5101
|
}
|
|
4213
|
-
cleanupEmpty(
|
|
4214
|
-
cleanupEmpty(
|
|
4215
|
-
cleanupEmpty(
|
|
5102
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5103
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5104
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4216
5105
|
return { changes };
|
|
4217
5106
|
},
|
|
4218
5107
|
async uninstall({ cwd }) {
|
|
4219
5108
|
const changes = [];
|
|
4220
5109
|
for (const file of SKILL_FILES) {
|
|
4221
|
-
const dest =
|
|
4222
|
-
if (!
|
|
5110
|
+
const dest = path11.join(cwd, file.dest);
|
|
5111
|
+
if (!fs8.existsSync(dest)) {
|
|
4223
5112
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
4224
5113
|
continue;
|
|
4225
5114
|
}
|
|
4226
|
-
|
|
5115
|
+
fs8.unlinkSync(dest);
|
|
4227
5116
|
changes.push({ path: file.dest, action: "removed" });
|
|
4228
5117
|
}
|
|
4229
|
-
cleanupEmpty(
|
|
4230
|
-
cleanupEmpty(
|
|
4231
|
-
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"));
|
|
4232
5121
|
for (const rel of LEGACY_FILES) {
|
|
4233
|
-
const dest =
|
|
4234
|
-
if (
|
|
4235
|
-
|
|
5122
|
+
const dest = path11.join(cwd, rel);
|
|
5123
|
+
if (fs8.existsSync(dest)) {
|
|
5124
|
+
fs8.unlinkSync(dest);
|
|
4236
5125
|
changes.push({ path: rel, action: "removed" });
|
|
4237
5126
|
}
|
|
4238
5127
|
}
|
|
4239
|
-
cleanupEmpty(
|
|
4240
|
-
cleanupEmpty(
|
|
4241
|
-
cleanupEmpty(
|
|
5128
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5129
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5130
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4242
5131
|
return { changes };
|
|
4243
5132
|
}
|
|
4244
5133
|
};
|
|
4245
5134
|
function cleanupEmpty(dir) {
|
|
4246
5135
|
try {
|
|
4247
|
-
const entries =
|
|
4248
|
-
if (entries.length === 0)
|
|
5136
|
+
const entries = fs8.readdirSync(dir);
|
|
5137
|
+
if (entries.length === 0) fs8.rmdirSync(dir);
|
|
4249
5138
|
} catch {
|
|
4250
5139
|
}
|
|
4251
5140
|
}
|
|
@@ -4414,6 +5303,8 @@ async function run(opts) {
|
|
|
4414
5303
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
4415
5304
|
case "rename":
|
|
4416
5305
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
5306
|
+
case "states":
|
|
5307
|
+
return runStates(cwd, positional.slice(1), writer);
|
|
4417
5308
|
case "ai": {
|
|
4418
5309
|
const result = await runAiCommand({
|
|
4419
5310
|
cwd,
|
|
@@ -4442,22 +5333,23 @@ function helpText2() {
|
|
|
4442
5333
|
" scan [flags] Run the scanner pipeline",
|
|
4443
5334
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
4444
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",
|
|
4445
5337
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
4446
5338
|
"",
|
|
4447
5339
|
"Flags:",
|
|
4448
5340
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
4449
5341
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
4450
5342
|
" --audit Equivalent to --check --lint (read-only)",
|
|
4451
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4452
|
-
" --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",
|
|
4453
5345
|
" --json Emit JSON diagnostics on stdout",
|
|
4454
5346
|
" --force (scaffold) overwrite existing spec",
|
|
4455
5347
|
""
|
|
4456
5348
|
].join("\n");
|
|
4457
5349
|
}
|
|
4458
5350
|
function runInit(cwd, w) {
|
|
4459
|
-
const configPath =
|
|
4460
|
-
if (
|
|
5351
|
+
const configPath = path12.join(cwd, CONFIG_FILENAME);
|
|
5352
|
+
if (fs9.existsSync(configPath)) {
|
|
4461
5353
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
4462
5354
|
return w.result(1);
|
|
4463
5355
|
}
|
|
@@ -4466,16 +5358,16 @@ function runInit(cwd, w) {
|
|
|
4466
5358
|
sources: [{ rootDir: "src" }],
|
|
4467
5359
|
output: "src/uidex.gen.ts"
|
|
4468
5360
|
};
|
|
4469
|
-
|
|
5361
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
4470
5362
|
w.out(`Created ${configPath}`);
|
|
4471
|
-
const gitignorePath =
|
|
5363
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
4472
5364
|
const entry = "*.gen.*";
|
|
4473
|
-
if (
|
|
4474
|
-
const existing =
|
|
5365
|
+
if (fs9.existsSync(gitignorePath)) {
|
|
5366
|
+
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
4475
5367
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
4476
5368
|
if (!hasEntry) {
|
|
4477
5369
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4478
|
-
|
|
5370
|
+
fs9.appendFileSync(
|
|
4479
5371
|
gitignorePath,
|
|
4480
5372
|
`${needsNewline ? "\n" : ""}${entry}
|
|
4481
5373
|
`,
|
|
@@ -4484,7 +5376,7 @@ function runInit(cwd, w) {
|
|
|
4484
5376
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
4485
5377
|
}
|
|
4486
5378
|
} else {
|
|
4487
|
-
|
|
5379
|
+
fs9.writeFileSync(gitignorePath, `${entry}
|
|
4488
5380
|
`, "utf8");
|
|
4489
5381
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
4490
5382
|
}
|
|
@@ -4523,20 +5415,25 @@ function runScanCommand(cwd, flags, w) {
|
|
|
4523
5415
|
if (updateBaseline) {
|
|
4524
5416
|
for (const r of results) {
|
|
4525
5417
|
const manifest = r.capturedStates ?? { captured: [] };
|
|
4526
|
-
const
|
|
4527
|
-
const
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
5418
|
+
const acknowledge = {};
|
|
5419
|
+
const merge = (from) => {
|
|
5420
|
+
for (const [route, scopes] of Object.entries(from)) {
|
|
5421
|
+
acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
|
|
5422
|
+
}
|
|
4531
5423
|
};
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
const
|
|
4535
|
-
|
|
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,
|
|
4536
5433
|
0
|
|
4537
5434
|
);
|
|
4538
5435
|
w.out(
|
|
4539
|
-
`Wrote ${p2} \u2014 ${
|
|
5436
|
+
`Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
|
|
4540
5437
|
);
|
|
4541
5438
|
}
|
|
4542
5439
|
return w.result(0);
|
|
@@ -4609,7 +5506,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4609
5506
|
for (const r of results) {
|
|
4610
5507
|
const entity = r.registry.get(scaffoldKind, id);
|
|
4611
5508
|
if (!entity) continue;
|
|
4612
|
-
const outDir =
|
|
5509
|
+
const outDir = path12.resolve(r.configDir, "e2e");
|
|
4613
5510
|
const result = scaffoldSpec({
|
|
4614
5511
|
registry: r.registry,
|
|
4615
5512
|
kind: scaffoldKind,
|
|
@@ -4630,6 +5527,53 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4630
5527
|
return w.result(1);
|
|
4631
5528
|
}
|
|
4632
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
|
+
}
|
|
4633
5577
|
function runRename(cwd, args, flags, w) {
|
|
4634
5578
|
const [kind, oldId, newId] = args;
|
|
4635
5579
|
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
@@ -4678,35 +5622,51 @@ function createWriter() {
|
|
|
4678
5622
|
}
|
|
4679
5623
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4680
5624
|
0 && (module.exports = {
|
|
5625
|
+
ACK_ALL,
|
|
5626
|
+
ACK_SCOPES,
|
|
4681
5627
|
CONFIG_FILENAME,
|
|
4682
5628
|
ConfigError,
|
|
4683
5629
|
DEFAULT_CONVENTIONS,
|
|
5630
|
+
DIAGNOSTIC_CODES,
|
|
4684
5631
|
applyFixes,
|
|
4685
5632
|
audit,
|
|
5633
|
+
capturedElsewhere,
|
|
5634
|
+
checkComponentCoverage,
|
|
4686
5635
|
checkPageCoverage,
|
|
4687
5636
|
checkStateCoverage,
|
|
4688
5637
|
checkStateMatrix,
|
|
4689
5638
|
compileRoute,
|
|
4690
|
-
|
|
4691
|
-
|
|
5639
|
+
computeMatrixBacklog,
|
|
5640
|
+
computePageBacklog,
|
|
5641
|
+
coverageBaselinePath,
|
|
4692
5642
|
detectRoutes,
|
|
4693
5643
|
discover,
|
|
4694
5644
|
emit,
|
|
5645
|
+
entityAcknowledge,
|
|
4695
5646
|
extract,
|
|
4696
5647
|
extractUidexExports,
|
|
5648
|
+
findWorkspaceRoot,
|
|
4697
5649
|
globToRegExp,
|
|
4698
|
-
|
|
5650
|
+
isLegacyBaseline,
|
|
5651
|
+
loadCoverageBaseline,
|
|
4699
5652
|
matchUrlToRoute,
|
|
4700
|
-
|
|
5653
|
+
mergeStates,
|
|
5654
|
+
migrateBaseline,
|
|
5655
|
+
migrateLegacyBaseline,
|
|
5656
|
+
migrateLegacyFields,
|
|
4701
5657
|
parseConfig,
|
|
5658
|
+
parseCoverageBaseline,
|
|
4702
5659
|
pathToId,
|
|
4703
5660
|
renameEntity,
|
|
4704
5661
|
resolve,
|
|
4705
5662
|
resolveGitContext,
|
|
5663
|
+
resolveWorkspace,
|
|
5664
|
+
routeAcknowledge,
|
|
4706
5665
|
runCli,
|
|
4707
5666
|
runScan,
|
|
4708
5667
|
scaffoldSpec,
|
|
4709
5668
|
scaffoldWidgetSpec,
|
|
5669
|
+
serializeCoverageBaseline,
|
|
4710
5670
|
validateConfig,
|
|
4711
5671
|
walk,
|
|
4712
5672
|
writeScanResult
|