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.js
CHANGED
|
@@ -30,7 +30,8 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
30
30
|
"audit",
|
|
31
31
|
"conventions",
|
|
32
32
|
"statesManifest",
|
|
33
|
-
"coverageBaseline"
|
|
33
|
+
"coverageBaseline",
|
|
34
|
+
"workspace"
|
|
34
35
|
]);
|
|
35
36
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
36
37
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -46,19 +47,64 @@ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
|
46
47
|
"acceptance",
|
|
47
48
|
"states",
|
|
48
49
|
"pageCoverage",
|
|
49
|
-
"stateMatrix"
|
|
50
|
+
"stateMatrix",
|
|
51
|
+
"severity"
|
|
50
52
|
]);
|
|
53
|
+
var DIAGNOSTIC_CODES = [
|
|
54
|
+
"acceptance-uncovered",
|
|
55
|
+
"acknowledged-elsewhere",
|
|
56
|
+
"competing-uidex-export",
|
|
57
|
+
"component-state-uncaptured",
|
|
58
|
+
"core-kind",
|
|
59
|
+
"duplicate-id",
|
|
60
|
+
"dynamic-attr",
|
|
61
|
+
"dynamic-flow-reference",
|
|
62
|
+
"gen-missing",
|
|
63
|
+
"gen-stale",
|
|
64
|
+
"legacy-acknowledge-field",
|
|
65
|
+
"legacy-coverage-baseline",
|
|
66
|
+
"matrix-backlog",
|
|
67
|
+
"missing-element-annotation",
|
|
68
|
+
"missing-state-capture",
|
|
69
|
+
"orphan-state-capture",
|
|
70
|
+
"page-backlog",
|
|
71
|
+
"page-captured",
|
|
72
|
+
"page-no-capture",
|
|
73
|
+
"parse-error",
|
|
74
|
+
"placeholder-reason",
|
|
75
|
+
"prefer-well-known-file",
|
|
76
|
+
"rename",
|
|
77
|
+
"scope-leak",
|
|
78
|
+
"spread-attr",
|
|
79
|
+
"stale-acknowledgment",
|
|
80
|
+
"stale-page-baseline",
|
|
81
|
+
"uidex-export-ambiguous-kind",
|
|
82
|
+
"uidex-export-duplicate-field",
|
|
83
|
+
"uidex-export-duplicate-state",
|
|
84
|
+
"uidex-export-empty-name",
|
|
85
|
+
"uidex-export-invalid-field",
|
|
86
|
+
"uidex-export-invalid-literal",
|
|
87
|
+
"uidex-export-missing-kind",
|
|
88
|
+
"uidex-export-satisfies-mismatch",
|
|
89
|
+
"uidex-export-unknown-field",
|
|
90
|
+
"unknown-reference",
|
|
91
|
+
"widget-id-mismatch",
|
|
92
|
+
"widget-missing-acceptance",
|
|
93
|
+
"widget-missing-dom"
|
|
94
|
+
];
|
|
95
|
+
var DIAGNOSTIC_CODE_SET = new Set(DIAGNOSTIC_CODES);
|
|
96
|
+
var SEVERITY_LEVELS = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
|
|
51
97
|
function fail(msg) {
|
|
52
98
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
53
99
|
}
|
|
54
|
-
function assertObject(value,
|
|
100
|
+
function assertObject(value, path13) {
|
|
55
101
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
56
|
-
fail(`${
|
|
102
|
+
fail(`${path13} must be an object`);
|
|
57
103
|
}
|
|
58
104
|
}
|
|
59
|
-
function assertStringArray(value,
|
|
105
|
+
function assertStringArray(value, path13) {
|
|
60
106
|
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
|
|
61
|
-
fail(`${
|
|
107
|
+
fail(`${path13} must be a string[]`);
|
|
62
108
|
}
|
|
63
109
|
}
|
|
64
110
|
function validateConfig(raw) {
|
|
@@ -112,11 +158,30 @@ function validateConfig(raw) {
|
|
|
112
158
|
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
113
159
|
fail(`"coverageBaseline" must be a string`);
|
|
114
160
|
}
|
|
161
|
+
if (raw.workspace !== void 0 && raw.workspace !== false && typeof raw.workspace !== "string") {
|
|
162
|
+
fail(`"workspace" must be a path string or false`);
|
|
163
|
+
}
|
|
115
164
|
if (raw.audit !== void 0) {
|
|
116
165
|
assertObject(raw.audit, "audit");
|
|
117
166
|
for (const key of Object.keys(raw.audit)) {
|
|
118
167
|
if (!ALLOWED_AUDIT_KEYS.has(key)) fail(`unknown key "${key}" in audit`);
|
|
119
168
|
const v = raw.audit[key];
|
|
169
|
+
if (key === "severity") {
|
|
170
|
+
assertObject(v, "audit.severity");
|
|
171
|
+
for (const [code, level] of Object.entries(v)) {
|
|
172
|
+
if (!DIAGNOSTIC_CODE_SET.has(code)) {
|
|
173
|
+
fail(
|
|
174
|
+
`unknown diagnostic code "${code}" in audit.severity (a typo here would silently leave that gate at its default severity)`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (typeof level !== "string" || !SEVERITY_LEVELS.has(level)) {
|
|
178
|
+
fail(
|
|
179
|
+
`audit.severity["${code}"] must be one of "error", "warning", "info", "off"`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
120
185
|
if (typeof v !== "boolean") fail(`audit.${key} must be a boolean`);
|
|
121
186
|
}
|
|
122
187
|
}
|
|
@@ -135,7 +200,13 @@ function validateConfig(raw) {
|
|
|
135
200
|
fail(`conventions.features must be a string or false`);
|
|
136
201
|
}
|
|
137
202
|
if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
|
|
138
|
-
|
|
203
|
+
const p2 = c.pages;
|
|
204
|
+
const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
|
|
205
|
+
if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
|
|
206
|
+
fail(
|
|
207
|
+
`conventions.pages must be "auto", false, or { routesDir: string }`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
139
210
|
}
|
|
140
211
|
if (c.flows !== void 0 && c.flows !== false) {
|
|
141
212
|
assertStringArray(c.flows, "conventions.flows");
|
|
@@ -153,7 +224,8 @@ function validateConfig(raw) {
|
|
|
153
224
|
audit: raw.audit,
|
|
154
225
|
conventions: raw.conventions,
|
|
155
226
|
statesManifest: raw.statesManifest,
|
|
156
|
-
coverageBaseline: raw.coverageBaseline
|
|
227
|
+
coverageBaseline: raw.coverageBaseline,
|
|
228
|
+
workspace: raw.workspace
|
|
157
229
|
};
|
|
158
230
|
}
|
|
159
231
|
function parseConfig(json) {
|
|
@@ -203,9 +275,11 @@ function tryReadConfig(configPath) {
|
|
|
203
275
|
function discover(options = {}) {
|
|
204
276
|
const cwd = options.cwd ?? process.cwd();
|
|
205
277
|
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
278
|
+
const all = options.all ?? false;
|
|
206
279
|
const rootMatch = tryReadConfig(path.join(cwd, CONFIG_FILENAME));
|
|
207
|
-
if (rootMatch) return [rootMatch];
|
|
280
|
+
if (rootMatch && !all) return [rootMatch];
|
|
208
281
|
const results = [];
|
|
282
|
+
if (rootMatch) results.push(rootMatch);
|
|
209
283
|
const queue = [[cwd, 0]];
|
|
210
284
|
while (queue.length > 0) {
|
|
211
285
|
const [dir, depth] = queue.shift();
|
|
@@ -309,6 +383,26 @@ function toPosix(p2) {
|
|
|
309
383
|
function matchesAny(rel, patterns) {
|
|
310
384
|
return patterns.some((g) => globToRegExp(g).test(rel));
|
|
311
385
|
}
|
|
386
|
+
function createSourceMatcher(sources, options) {
|
|
387
|
+
const { cwd, globalExcludes = [], includeTests = false } = options;
|
|
388
|
+
const baseDefaults = includeTests ? BASE_EXCLUDES : DEFAULT_EXCLUDES;
|
|
389
|
+
const compiled = sources.map((source) => ({
|
|
390
|
+
absRoot: path2.resolve(cwd, source.rootDir),
|
|
391
|
+
includes: source.include?.length ? source.include : DEFAULT_INCLUDES,
|
|
392
|
+
excludes: [...baseDefaults, ...globalExcludes, ...source.exclude ?? []]
|
|
393
|
+
}));
|
|
394
|
+
return (absFile) => {
|
|
395
|
+
const abs = path2.resolve(absFile);
|
|
396
|
+
return compiled.some(({ absRoot, includes, excludes }) => {
|
|
397
|
+
const rel = toPosix(path2.relative(absRoot, abs));
|
|
398
|
+
if (rel.length === 0 || rel.startsWith("../") || path2.isAbsolute(rel)) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
if (matchesAny(rel, excludes)) return false;
|
|
402
|
+
return matchesAny(rel, includes);
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
}
|
|
312
406
|
function walk(sources, options) {
|
|
313
407
|
const { cwd, globalExcludes = [], includeTests = false } = options;
|
|
314
408
|
const out2 = [];
|
|
@@ -642,6 +736,132 @@ function createRegistry() {
|
|
|
642
736
|
};
|
|
643
737
|
}
|
|
644
738
|
|
|
739
|
+
// src/scanner/scan/acknowledge.ts
|
|
740
|
+
var ACK_ALL = "*";
|
|
741
|
+
var ACK_SCOPES = [ACK_ALL, ...CORE_STATE_KINDS];
|
|
742
|
+
var COMPONENT_ACK_SCOPES = [ACK_ALL];
|
|
743
|
+
var AUTHORABLE_ACK_TYPES = [
|
|
744
|
+
"impossible",
|
|
745
|
+
"backlog"
|
|
746
|
+
];
|
|
747
|
+
var CORE_SET = new Set(CORE_STATE_KINDS);
|
|
748
|
+
function isCoreKind(scope) {
|
|
749
|
+
return CORE_SET.has(scope);
|
|
750
|
+
}
|
|
751
|
+
var PLACEHOLDER_REASON = "TODO: why can this never be captured?";
|
|
752
|
+
function isPlaceholderReason(reason) {
|
|
753
|
+
return typeof reason === "string" && /^\s*TODO\b/i.test(reason);
|
|
754
|
+
}
|
|
755
|
+
function describeAck(a) {
|
|
756
|
+
if (a.type === "captured-elsewhere") {
|
|
757
|
+
return a.by ? `captured-elsewhere (${a.by})` : "captured-elsewhere";
|
|
758
|
+
}
|
|
759
|
+
return a.reason ? `${a.type} ("${a.reason}")` : a.type;
|
|
760
|
+
}
|
|
761
|
+
function originHint(a, page) {
|
|
762
|
+
if (a.origin === "baseline") {
|
|
763
|
+
return "Prune it with `uidex scan --update-baseline` so the backlog reflects reality.";
|
|
764
|
+
}
|
|
765
|
+
if (a.origin === "workspace") {
|
|
766
|
+
return "This is computed from the workspace; it resolves itself once the sibling capture changes.";
|
|
767
|
+
}
|
|
768
|
+
const where = page ? ` on page "${page}"` : "";
|
|
769
|
+
return `Remove the \`acknowledge.${a.scope}\` entry${where} \u2014 a capture contradicts it.`;
|
|
770
|
+
}
|
|
771
|
+
function toResolved(map, origin, into) {
|
|
772
|
+
for (const [scope, ack] of Object.entries(map ?? {})) {
|
|
773
|
+
into.set(scope, { ...ack, scope, origin });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function routeAcknowledge(registry, baseline, route, page) {
|
|
777
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
778
|
+
toResolved(baseline?.acknowledge?.[route], "baseline", out2);
|
|
779
|
+
if (page) {
|
|
780
|
+
toResolved(registry.get("page", page)?.meta?.acknowledge, "source", out2);
|
|
781
|
+
}
|
|
782
|
+
return out2;
|
|
783
|
+
}
|
|
784
|
+
function entityAcknowledge(meta, capturedElsewhereBy) {
|
|
785
|
+
const source = meta?.acknowledge?.[ACK_ALL];
|
|
786
|
+
if (source) return { ...source, scope: ACK_ALL, origin: "source" };
|
|
787
|
+
if (capturedElsewhereBy) {
|
|
788
|
+
return {
|
|
789
|
+
type: "captured-elsewhere",
|
|
790
|
+
by: capturedElsewhereBy,
|
|
791
|
+
scope: ACK_ALL,
|
|
792
|
+
origin: "workspace"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return void 0;
|
|
796
|
+
}
|
|
797
|
+
function readAck(raw) {
|
|
798
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
799
|
+
const r = raw;
|
|
800
|
+
if (r.type !== "impossible" && r.type !== "backlog") return void 0;
|
|
801
|
+
const out2 = { type: r.type };
|
|
802
|
+
if (typeof r.reason === "string" && r.reason.length > 0) out2.reason = r.reason;
|
|
803
|
+
return out2;
|
|
804
|
+
}
|
|
805
|
+
function parseCoverageBaseline(raw) {
|
|
806
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
807
|
+
const r = raw;
|
|
808
|
+
const ackRaw = r.acknowledge;
|
|
809
|
+
if (!ackRaw || typeof ackRaw !== "object" || Array.isArray(ackRaw))
|
|
810
|
+
return null;
|
|
811
|
+
const acknowledge = {};
|
|
812
|
+
for (const [route, scopesRaw] of Object.entries(
|
|
813
|
+
ackRaw
|
|
814
|
+
)) {
|
|
815
|
+
if (!scopesRaw || typeof scopesRaw !== "object" || Array.isArray(scopesRaw))
|
|
816
|
+
continue;
|
|
817
|
+
const scopes = {};
|
|
818
|
+
for (const [scope, ackVal] of Object.entries(
|
|
819
|
+
scopesRaw
|
|
820
|
+
)) {
|
|
821
|
+
if (scope !== ACK_ALL && !isCoreKind(scope)) continue;
|
|
822
|
+
const ack = readAck(ackVal);
|
|
823
|
+
if (ack) scopes[scope] = ack;
|
|
824
|
+
}
|
|
825
|
+
if (Object.keys(scopes).length > 0) acknowledge[route] = scopes;
|
|
826
|
+
}
|
|
827
|
+
return { version: 2, acknowledge };
|
|
828
|
+
}
|
|
829
|
+
function isLegacyBaseline(raw) {
|
|
830
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
831
|
+
const r = raw;
|
|
832
|
+
if ("acknowledge" in r) return false;
|
|
833
|
+
return "uncapturedPages" in r || "missingKinds" in r;
|
|
834
|
+
}
|
|
835
|
+
function migrateBaseline(legacy) {
|
|
836
|
+
const acknowledge = {};
|
|
837
|
+
const at = (route) => acknowledge[route] ??= {};
|
|
838
|
+
for (const route of legacy.uncapturedPages ?? []) {
|
|
839
|
+
if (typeof route === "string") at(route)[ACK_ALL] = { type: "backlog" };
|
|
840
|
+
}
|
|
841
|
+
for (const [route, kinds] of Object.entries(legacy.missingKinds ?? {})) {
|
|
842
|
+
if (!Array.isArray(kinds)) continue;
|
|
843
|
+
for (const kind of kinds) {
|
|
844
|
+
if (typeof kind === "string" && isCoreKind(kind)) {
|
|
845
|
+
at(route)[kind] = { type: "backlog" };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return { version: 2, acknowledge };
|
|
850
|
+
}
|
|
851
|
+
function serializeCoverageBaseline(baseline) {
|
|
852
|
+
const routes = Object.keys(baseline.acknowledge).sort();
|
|
853
|
+
const ordered = {};
|
|
854
|
+
for (const route of routes) {
|
|
855
|
+
const scopes = baseline.acknowledge[route];
|
|
856
|
+
const sorted = {};
|
|
857
|
+
for (const scope of ACK_SCOPES) {
|
|
858
|
+
if (scopes[scope]) sorted[scope] = scopes[scope];
|
|
859
|
+
}
|
|
860
|
+
ordered[route] = sorted;
|
|
861
|
+
}
|
|
862
|
+
return JSON.stringify({ version: 2, acknowledge: ordered }, null, 2) + "\n";
|
|
863
|
+
}
|
|
864
|
+
|
|
645
865
|
// src/scanner/scan/extract-uidex-export.ts
|
|
646
866
|
var KIND_DISCRIMINATORS = [
|
|
647
867
|
"page",
|
|
@@ -660,26 +880,41 @@ var ALLOWED_FIELDS = {
|
|
|
660
880
|
"widgets",
|
|
661
881
|
"acceptance",
|
|
662
882
|
"states",
|
|
883
|
+
"acknowledge",
|
|
663
884
|
"capture",
|
|
664
885
|
"waivers",
|
|
665
886
|
"description"
|
|
666
887
|
]),
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
888
|
+
// A page's `acknowledge` accepts both scope shapes: `"*"` (the whole route)
|
|
889
|
+
// and a core kind (one cell of its matrix). A feature/widget has no route, so
|
|
890
|
+
// the core-kind matrix is meaningless there and only `"*"` is accepted —
|
|
891
|
+
// giving a component a scoped opt-out instead of forcing you to delete its
|
|
892
|
+
// whole `states` block. `capture` / `waivers` are REMOVED; they stay parseable
|
|
893
|
+
// only so `--fix` can rewrite them into `acknowledge` (see MIGRATION below).
|
|
670
894
|
feature: /* @__PURE__ */ new Set([
|
|
671
895
|
"feature",
|
|
672
896
|
"name",
|
|
673
897
|
"features",
|
|
674
898
|
"acceptance",
|
|
675
899
|
"states",
|
|
900
|
+
"acknowledge",
|
|
901
|
+
"capture",
|
|
676
902
|
"description"
|
|
677
903
|
]),
|
|
678
904
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
679
|
-
widget: /* @__PURE__ */ new Set([
|
|
905
|
+
widget: /* @__PURE__ */ new Set([
|
|
906
|
+
"widget",
|
|
907
|
+
"name",
|
|
908
|
+
"acceptance",
|
|
909
|
+
"states",
|
|
910
|
+
"acknowledge",
|
|
911
|
+
"capture",
|
|
912
|
+
"description"
|
|
913
|
+
]),
|
|
680
914
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
681
915
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
682
916
|
};
|
|
917
|
+
var ACK_FIELDS = /* @__PURE__ */ new Set(["type", "reason"]);
|
|
683
918
|
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
684
919
|
"name",
|
|
685
920
|
"kind",
|
|
@@ -930,7 +1165,8 @@ function objectLit(node, content, p2, pos, span) {
|
|
|
930
1165
|
key,
|
|
931
1166
|
value,
|
|
932
1167
|
keyPos,
|
|
933
|
-
span: removalSpan(content, prop.start, prop.end)
|
|
1168
|
+
span: removalSpan(content, prop.start, prop.end),
|
|
1169
|
+
propSpan: { start: prop.start, end: prop.end }
|
|
934
1170
|
});
|
|
935
1171
|
}
|
|
936
1172
|
return { kind: "object", entries, pos, span };
|
|
@@ -1078,8 +1314,10 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1078
1314
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1079
1315
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
1080
1316
|
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1317
|
+
const stateBearing = kind === "page" || kind === "feature" || kind === "widget";
|
|
1318
|
+
const acknowledge = stateBearing ? readAcknowledgeField(byKey, kind === "page") : void 0;
|
|
1319
|
+
const legacyCapture = stateBearing ? readBooleanField(byKey, "capture") : void 0;
|
|
1320
|
+
const legacyWaivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
1083
1321
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1084
1322
|
const metadata = {
|
|
1085
1323
|
source: "ts-export",
|
|
@@ -1103,15 +1341,25 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1103
1341
|
metadata.widgetSpans = widgetsField.spans;
|
|
1104
1342
|
}
|
|
1105
1343
|
if (states?.length) metadata.states = states;
|
|
1106
|
-
if (
|
|
1107
|
-
|
|
1344
|
+
if (acknowledge && Object.keys(acknowledge).length > 0) {
|
|
1345
|
+
metadata.acknowledge = acknowledge;
|
|
1346
|
+
}
|
|
1347
|
+
if (legacyCapture !== void 0) metadata.legacyCapture = legacyCapture;
|
|
1348
|
+
if (legacyWaivers && Object.keys(legacyWaivers).length > 0) {
|
|
1349
|
+
metadata.legacyWaivers = legacyWaivers;
|
|
1350
|
+
}
|
|
1108
1351
|
if (notFlow) metadata.notFlow = true;
|
|
1109
1352
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1110
1353
|
metadata.idSpan = idValue.span;
|
|
1111
1354
|
}
|
|
1112
1355
|
const fieldSpans = {};
|
|
1113
|
-
|
|
1356
|
+
const fieldPropSpans = {};
|
|
1357
|
+
for (const entry of value.entries) {
|
|
1358
|
+
fieldSpans[entry.key] = entry.span;
|
|
1359
|
+
fieldPropSpans[entry.key] = entry.propSpan;
|
|
1360
|
+
}
|
|
1114
1361
|
metadata.fieldSpans = fieldSpans;
|
|
1362
|
+
metadata.fieldPropSpans = fieldPropSpans;
|
|
1115
1363
|
return metadata;
|
|
1116
1364
|
}
|
|
1117
1365
|
function readIdField(value, kind, fieldName) {
|
|
@@ -1141,6 +1389,80 @@ function readIdField(value, kind, fieldName) {
|
|
|
1141
1389
|
value.pos
|
|
1142
1390
|
);
|
|
1143
1391
|
}
|
|
1392
|
+
function readAcknowledgeField(byKey, isPage) {
|
|
1393
|
+
const entry = byKey.get("acknowledge");
|
|
1394
|
+
if (!entry) return void 0;
|
|
1395
|
+
if (entry.value.kind !== "object") {
|
|
1396
|
+
throw new ExtractError(
|
|
1397
|
+
"uidex-export-invalid-field",
|
|
1398
|
+
'`acknowledge` must be an object mapping a scope (`"*"` or a core kind) to `{ type, reason? }`.',
|
|
1399
|
+
entry.value.pos
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const out2 = {};
|
|
1403
|
+
for (const e of entry.value.entries) {
|
|
1404
|
+
const scopes = isPage ? ACK_SCOPES : COMPONENT_ACK_SCOPES;
|
|
1405
|
+
if (!scopes.includes(e.key)) {
|
|
1406
|
+
throw new ExtractError(
|
|
1407
|
+
"uidex-export-invalid-field",
|
|
1408
|
+
`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)"}.`,
|
|
1409
|
+
e.keyPos
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (e.value.kind !== "object") {
|
|
1413
|
+
throw new ExtractError(
|
|
1414
|
+
"uidex-export-invalid-field",
|
|
1415
|
+
`acknowledge "${e.key}" must be an object \`{ type, reason? }\`.`,
|
|
1416
|
+
e.value.pos
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
1420
|
+
for (const f of e.value.entries) {
|
|
1421
|
+
if (!ACK_FIELDS.has(f.key)) {
|
|
1422
|
+
throw new ExtractError(
|
|
1423
|
+
"uidex-export-invalid-field",
|
|
1424
|
+
`Unknown field "${f.key}" in an \`acknowledge\` entry. Allowed: ${[...ACK_FIELDS].sort().join(", ")}.`,
|
|
1425
|
+
f.value.pos
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
fieldByKey.set(f.key, f);
|
|
1429
|
+
}
|
|
1430
|
+
const type = readStringField(fieldByKey, "type");
|
|
1431
|
+
if (type === void 0) {
|
|
1432
|
+
throw new ExtractError(
|
|
1433
|
+
"uidex-export-invalid-field",
|
|
1434
|
+
`acknowledge "${e.key}" needs a \`type\`: ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")}.`,
|
|
1435
|
+
e.value.pos
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (type === "captured-elsewhere") {
|
|
1439
|
+
throw new ExtractError(
|
|
1440
|
+
"uidex-export-invalid-field",
|
|
1441
|
+
'`type: "captured-elsewhere"` cannot be authored; the workspace computes it from the sibling app that captured the declaration.',
|
|
1442
|
+
fieldByKey.get("type").value.pos,
|
|
1443
|
+
"Remove the entry \u2014 if a sibling app really captures this, the workspace reports it as `acknowledged-elsewhere` on its own."
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
if (type !== "impossible" && type !== "backlog") {
|
|
1447
|
+
throw new ExtractError(
|
|
1448
|
+
"uidex-export-invalid-field",
|
|
1449
|
+
`acknowledge \`type\` must be ${AUTHORABLE_ACK_TYPES.map((t) => `"${t}"`).join(" or ")} (got "${type}").`,
|
|
1450
|
+
fieldByKey.get("type").value.pos
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
const reason = readStringField(fieldByKey, "reason");
|
|
1454
|
+
if (type === "impossible" && (reason === void 0 || reason.length === 0)) {
|
|
1455
|
+
throw new ExtractError(
|
|
1456
|
+
"uidex-export-invalid-field",
|
|
1457
|
+
`acknowledge "${e.key}" is \`type: "impossible"\` and needs a non-empty \`reason\` (why this can NEVER be rendered/captured).`,
|
|
1458
|
+
e.value.pos,
|
|
1459
|
+
'If it merely has not been captured yet, that is `type: "backlog"` \u2014 which needs no reason.'
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
out2[e.key] = reason ? { type, reason } : { type };
|
|
1463
|
+
}
|
|
1464
|
+
return out2;
|
|
1465
|
+
}
|
|
1144
1466
|
function readWaiversField(byKey) {
|
|
1145
1467
|
const entry = byKey.get("waivers");
|
|
1146
1468
|
if (!entry) return void 0;
|
|
@@ -1820,13 +2142,26 @@ import * as path4 from "path";
|
|
|
1820
2142
|
var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1821
2143
|
var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1822
2144
|
var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
|
|
1823
|
-
function detectRoutes(files) {
|
|
2145
|
+
function detectRoutes(files, options = {}) {
|
|
1824
2146
|
const out2 = [];
|
|
1825
2147
|
const seen = /* @__PURE__ */ new Set();
|
|
2148
|
+
const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
|
|
2149
|
+
const routesDirParts = routesDir ? routesDir.split("/") : null;
|
|
1826
2150
|
for (const f of files) {
|
|
1827
2151
|
const rel = f.displayPath;
|
|
1828
2152
|
const parts = rel.split("/");
|
|
1829
2153
|
const base = parts[parts.length - 1];
|
|
2154
|
+
if (routesDirParts) {
|
|
2155
|
+
const end = dirEndIndex(parts, routesDirParts);
|
|
2156
|
+
if (end === -1 || end > parts.length - 1) continue;
|
|
2157
|
+
if (!PAGES_ROUTER_BASENAME.test(base)) continue;
|
|
2158
|
+
const routePath = routesStylePath(
|
|
2159
|
+
parts.slice(end, parts.length - 1),
|
|
2160
|
+
base
|
|
2161
|
+
);
|
|
2162
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
1830
2165
|
const appIdx = parts.indexOf("app");
|
|
1831
2166
|
if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
|
|
1832
2167
|
const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
|
|
@@ -1853,15 +2188,11 @@ function detectRoutes(files) {
|
|
|
1853
2188
|
}
|
|
1854
2189
|
const routesIdx = parts.indexOf("routes");
|
|
1855
2190
|
if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
base.replace(/\.[^.]+$/, "")
|
|
1862
|
-
].filter((s) => s !== "index" && s !== "__root");
|
|
1863
|
-
const routePath = formatNextAppPath(normalized);
|
|
1864
|
-
push(out2, seen, routePath, f.displayPath);
|
|
2191
|
+
const routePath = routesStylePath(
|
|
2192
|
+
parts.slice(routesIdx + 1, parts.length - 1),
|
|
2193
|
+
base
|
|
2194
|
+
);
|
|
2195
|
+
if (routePath !== null) push(out2, seen, routePath, f.displayPath);
|
|
1865
2196
|
continue;
|
|
1866
2197
|
}
|
|
1867
2198
|
}
|
|
@@ -1877,6 +2208,63 @@ function formatNextAppPath(segments) {
|
|
|
1877
2208
|
if (kept.length === 0) return "/";
|
|
1878
2209
|
return "/" + kept.join("/");
|
|
1879
2210
|
}
|
|
2211
|
+
function dirEndIndex(parts, dirParts) {
|
|
2212
|
+
if (dirParts.length === 0) return 0;
|
|
2213
|
+
for (let i = 0; i + dirParts.length <= parts.length; i++) {
|
|
2214
|
+
let match = true;
|
|
2215
|
+
for (let j = 0; j < dirParts.length; j++) {
|
|
2216
|
+
if (parts[i + j] !== dirParts[j]) {
|
|
2217
|
+
match = false;
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (match) return i + dirParts.length;
|
|
2222
|
+
}
|
|
2223
|
+
return -1;
|
|
2224
|
+
}
|
|
2225
|
+
function routesStylePath(dirSegs, base) {
|
|
2226
|
+
const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
|
|
2227
|
+
const leaf = fileSegs[fileSegs.length - 1];
|
|
2228
|
+
if (leaf.startsWith("_")) return null;
|
|
2229
|
+
const rawSegs = [...dirSegs, ...fileSegs];
|
|
2230
|
+
if (rawSegs.some((s) => s.startsWith("-"))) return null;
|
|
2231
|
+
if (rawSegs[0] === "api") return null;
|
|
2232
|
+
return formatTanStackPath(rawSegs);
|
|
2233
|
+
}
|
|
2234
|
+
function splitFlatSegments(name) {
|
|
2235
|
+
const segs = [];
|
|
2236
|
+
let cur = "";
|
|
2237
|
+
let depth = 0;
|
|
2238
|
+
for (const ch of name) {
|
|
2239
|
+
if (ch === "[") depth++;
|
|
2240
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
2241
|
+
if (ch === "." && depth === 0) {
|
|
2242
|
+
segs.push(cur);
|
|
2243
|
+
cur = "";
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
cur += ch;
|
|
2247
|
+
}
|
|
2248
|
+
segs.push(cur);
|
|
2249
|
+
return segs;
|
|
2250
|
+
}
|
|
2251
|
+
function formatTanStackPath(segments) {
|
|
2252
|
+
const kept = [];
|
|
2253
|
+
for (const seg of segments) {
|
|
2254
|
+
if (!seg) continue;
|
|
2255
|
+
if (seg === "index" || seg === "route" || seg === "__root") continue;
|
|
2256
|
+
if (seg.startsWith("_")) continue;
|
|
2257
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
2258
|
+
kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
|
|
2259
|
+
}
|
|
2260
|
+
if (kept.length === 0) return "/";
|
|
2261
|
+
return "/" + kept.join("/");
|
|
2262
|
+
}
|
|
2263
|
+
function normalizeTanStackSegment(seg) {
|
|
2264
|
+
if (seg === "$") return "[...splat]";
|
|
2265
|
+
if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
|
|
2266
|
+
return seg.replace(/\[([^\]]*)\]/g, "$1");
|
|
2267
|
+
}
|
|
1880
2268
|
function pathToId(routePath) {
|
|
1881
2269
|
if (routePath === "/") return "root";
|
|
1882
2270
|
return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1923,9 +2311,8 @@ function buildMetaFromExport(exp) {
|
|
|
1923
2311
|
...s.description ? { description: s.description } : {}
|
|
1924
2312
|
}));
|
|
1925
2313
|
}
|
|
1926
|
-
if (exp.
|
|
1927
|
-
|
|
1928
|
-
meta.waivers = exp.waivers;
|
|
2314
|
+
if (exp.acknowledge && Object.keys(exp.acknowledge).length > 0) {
|
|
2315
|
+
meta.acknowledge = exp.acknowledge;
|
|
1929
2316
|
}
|
|
1930
2317
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
1931
2318
|
}
|
|
@@ -1972,9 +2359,12 @@ function resolve2(ctx) {
|
|
|
1972
2359
|
function metaWithComposes(kind, id, base) {
|
|
1973
2360
|
const composes = directChildren.get(`${kind}:${id}`);
|
|
1974
2361
|
if (!composes || composes.length === 0) return base;
|
|
1975
|
-
return { ...base
|
|
2362
|
+
return { ...base, composes };
|
|
1976
2363
|
}
|
|
1977
|
-
const routes = conventions.pages ===
|
|
2364
|
+
const routes = conventions.pages === false ? [] : detectRoutes(
|
|
2365
|
+
ctx.extracted.map((e) => e.file),
|
|
2366
|
+
conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
|
|
2367
|
+
);
|
|
1978
2368
|
const handledPageFiles = /* @__PURE__ */ new Set();
|
|
1979
2369
|
for (const route of routes) {
|
|
1980
2370
|
const routeDir = path4.posix.dirname(route.file);
|
|
@@ -2267,6 +2657,45 @@ function resolve2(ctx) {
|
|
|
2267
2657
|
};
|
|
2268
2658
|
registry.add(element);
|
|
2269
2659
|
}
|
|
2660
|
+
const ancestorsById = /* @__PURE__ */ new Map();
|
|
2661
|
+
const fileById = /* @__PURE__ */ new Map();
|
|
2662
|
+
for (const a of allAnnotations) {
|
|
2663
|
+
if (!fileById.has(a.id)) fileById.set(a.id, a.file);
|
|
2664
|
+
if (a.ancestors?.length) {
|
|
2665
|
+
const prev = ancestorsById.get(a.id) ?? [];
|
|
2666
|
+
ancestorsById.set(a.id, [...prev, ...a.ancestors.map((x) => x.id)]);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
const ownersByFile = /* @__PURE__ */ new Map();
|
|
2670
|
+
const addOwner = (file, id) => {
|
|
2671
|
+
ownersByFile.set(file, [...ownersByFile.get(file) ?? [], id]);
|
|
2672
|
+
};
|
|
2673
|
+
const featureDirs = [];
|
|
2674
|
+
for (const f of registry.list("feature")) {
|
|
2675
|
+
const loc = f.loc?.file;
|
|
2676
|
+
if (!loc) continue;
|
|
2677
|
+
const dir = loc.endsWith("/") ? loc.slice(0, -1) : path4.posix.dirname(loc);
|
|
2678
|
+
featureDirs.push({ dir, id: f.id });
|
|
2679
|
+
}
|
|
2680
|
+
for (const p2 of registry.list("page")) {
|
|
2681
|
+
if (p2.loc?.file) addOwner(p2.loc.file, p2.id);
|
|
2682
|
+
}
|
|
2683
|
+
const ownersFor = (file) => {
|
|
2684
|
+
const out2 = [...ownersByFile.get(file) ?? []];
|
|
2685
|
+
for (const { dir, id } of featureDirs) {
|
|
2686
|
+
if (file === dir || file.startsWith(`${dir}/`)) out2.push(id);
|
|
2687
|
+
}
|
|
2688
|
+
return out2;
|
|
2689
|
+
};
|
|
2690
|
+
const expandTouches = (ids) => {
|
|
2691
|
+
const out2 = new Set(ids);
|
|
2692
|
+
for (const id of ids) {
|
|
2693
|
+
for (const anc of ancestorsById.get(id) ?? []) out2.add(anc);
|
|
2694
|
+
const file = fileById.get(id);
|
|
2695
|
+
if (file) for (const owner of ownersFor(file)) out2.add(owner);
|
|
2696
|
+
}
|
|
2697
|
+
return [...out2];
|
|
2698
|
+
};
|
|
2270
2699
|
if (ctx.flowFiles && conventions.flows) {
|
|
2271
2700
|
for (const ff of ctx.flowFiles) {
|
|
2272
2701
|
const notFlowExport = (ff.metadata ?? []).find(
|
|
@@ -2276,7 +2705,10 @@ function resolve2(ctx) {
|
|
|
2276
2705
|
const flowExport = (ff.metadata ?? []).find(
|
|
2277
2706
|
(m) => m.kind === "flow" && typeof m.id === "string"
|
|
2278
2707
|
);
|
|
2279
|
-
const derived = flowsFromFacts(ff)
|
|
2708
|
+
const derived = flowsFromFacts(ff).map((f) => ({
|
|
2709
|
+
...f,
|
|
2710
|
+
touches: expandTouches(f.touches)
|
|
2711
|
+
}));
|
|
2280
2712
|
if (flowExport && typeof flowExport.id === "string" && derived.length === 1) {
|
|
2281
2713
|
const base = derived[0];
|
|
2282
2714
|
const flow = {
|
|
@@ -2345,7 +2777,195 @@ function dedupe(arr) {
|
|
|
2345
2777
|
}
|
|
2346
2778
|
|
|
2347
2779
|
// src/scanner/scan/audit.ts
|
|
2348
|
-
import * as
|
|
2780
|
+
import * as path6 from "path";
|
|
2781
|
+
|
|
2782
|
+
// src/scanner/scan/component-coverage.ts
|
|
2783
|
+
var COMPOSED_KINDS = ["widget", "feature"];
|
|
2784
|
+
function composedStateEntities(registry, page) {
|
|
2785
|
+
const meta = registry.get("page", page)?.meta;
|
|
2786
|
+
if (!meta) return [];
|
|
2787
|
+
const out2 = [];
|
|
2788
|
+
for (const kind of COMPOSED_KINDS) {
|
|
2789
|
+
const ids = (kind === "widget" ? meta.widgets : meta.features) ?? [];
|
|
2790
|
+
for (const id of ids) {
|
|
2791
|
+
const e = registry.get(kind, id);
|
|
2792
|
+
if (!e?.meta?.states?.length) continue;
|
|
2793
|
+
if (e.meta.acknowledge?.[ACK_ALL]) continue;
|
|
2794
|
+
out2.push({ kind, id });
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
return out2;
|
|
2798
|
+
}
|
|
2799
|
+
function capturedStatesFor(captured, kind, id) {
|
|
2800
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
2801
|
+
for (const c of captured) {
|
|
2802
|
+
if (c.entity !== id) continue;
|
|
2803
|
+
if (c.kind !== void 0 && c.kind !== kind && c.kind !== "page") continue;
|
|
2804
|
+
out2.add(c.state);
|
|
2805
|
+
}
|
|
2806
|
+
return out2;
|
|
2807
|
+
}
|
|
2808
|
+
function checkComponentCoverage(registry, manifest) {
|
|
2809
|
+
const routes = registry.list("route");
|
|
2810
|
+
if (routes.length === 0) return [];
|
|
2811
|
+
const captured = manifest.captured ?? [];
|
|
2812
|
+
const diagnostics = [];
|
|
2813
|
+
for (const route of routes) {
|
|
2814
|
+
if (registry.get("page", route.page)?.meta?.acknowledge?.[ACK_ALL]) continue;
|
|
2815
|
+
for (const { kind, id } of composedStateEntities(registry, route.page)) {
|
|
2816
|
+
const entity = registry.get(kind, id);
|
|
2817
|
+
const declared = entity.meta.states.map((s) => s.name);
|
|
2818
|
+
const shot = capturedStatesFor(captured, kind, id);
|
|
2819
|
+
const missing = declared.filter((s) => !shot.has(s));
|
|
2820
|
+
if (missing.length === 0) continue;
|
|
2821
|
+
diagnostics.push({
|
|
2822
|
+
code: "component-state-uncaptured",
|
|
2823
|
+
severity: "info",
|
|
2824
|
+
message: `route "${route.path}" composes ${kind} "${id}", whose state(s) [${missing.join(", ")}] no capture produced`,
|
|
2825
|
+
file: entity.loc?.file,
|
|
2826
|
+
line: entity.loc?.line,
|
|
2827
|
+
entity: { kind, id },
|
|
2828
|
+
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}".`
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return diagnostics;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/scanner/scan/migrate-acknowledge.ts
|
|
2836
|
+
var CORE = CORE_STATE_KINDS;
|
|
2837
|
+
function orderScopes(scopes) {
|
|
2838
|
+
const rank = (s) => s === ACK_ALL ? -1 : CORE.indexOf(s);
|
|
2839
|
+
return [...scopes].sort((a, b) => rank(a) - rank(b));
|
|
2840
|
+
}
|
|
2841
|
+
function renderKey(scope) {
|
|
2842
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(scope) ? scope : JSON.stringify(scope);
|
|
2843
|
+
}
|
|
2844
|
+
function renderAck(a) {
|
|
2845
|
+
const parts = [`type: ${JSON.stringify(a.type)}`];
|
|
2846
|
+
if (a.reason) parts.push(`reason: ${JSON.stringify(a.reason)}`);
|
|
2847
|
+
return `{ ${parts.join(", ")} }`;
|
|
2848
|
+
}
|
|
2849
|
+
function renderAcknowledge(entries, indent) {
|
|
2850
|
+
const inner = indent + " ";
|
|
2851
|
+
const lines = orderScopes(Object.keys(entries)).map(
|
|
2852
|
+
(scope) => `${inner}${renderKey(scope)}: ${renderAck(entries[scope])},`
|
|
2853
|
+
);
|
|
2854
|
+
return ["acknowledge: {", ...lines, `${indent}}`].join("\n");
|
|
2855
|
+
}
|
|
2856
|
+
function indentAt(content, offset) {
|
|
2857
|
+
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2858
|
+
const m = /^[ \t]*/.exec(content.slice(lineStart, offset));
|
|
2859
|
+
return m ? m[0] : "";
|
|
2860
|
+
}
|
|
2861
|
+
function plannedEntries(m) {
|
|
2862
|
+
const entries = {};
|
|
2863
|
+
if (m.legacyCapture === false) {
|
|
2864
|
+
entries[ACK_ALL] = {
|
|
2865
|
+
type: "impossible",
|
|
2866
|
+
reason: m.description || PLACEHOLDER_REASON
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
for (const kind of CORE) {
|
|
2870
|
+
const reason = m.legacyWaivers?.[kind];
|
|
2871
|
+
if (reason) entries[kind] = { type: "impossible", reason };
|
|
2872
|
+
}
|
|
2873
|
+
return entries;
|
|
2874
|
+
}
|
|
2875
|
+
function migrateLegacyFields(ef) {
|
|
2876
|
+
const out2 = [];
|
|
2877
|
+
const { content, sourcePath, displayPath } = ef.file;
|
|
2878
|
+
for (const m of ef.metadata ?? []) {
|
|
2879
|
+
const legacy = [];
|
|
2880
|
+
if (m.legacyWaivers) legacy.push("waivers");
|
|
2881
|
+
if (m.legacyCapture !== void 0) legacy.push("capture");
|
|
2882
|
+
if (legacy.length === 0) continue;
|
|
2883
|
+
const fields = legacy.join(" and ");
|
|
2884
|
+
const base = {
|
|
2885
|
+
severity: "error",
|
|
2886
|
+
code: "legacy-acknowledge-field",
|
|
2887
|
+
file: displayPath,
|
|
2888
|
+
line: m.loc.line
|
|
2889
|
+
};
|
|
2890
|
+
if (m.acknowledge) {
|
|
2891
|
+
out2.push({
|
|
2892
|
+
...base,
|
|
2893
|
+
message: `\`${fields}\` was replaced by \`acknowledge\`, and this declaration already has an \`acknowledge\` field`,
|
|
2894
|
+
hint: `Merge the remaining \`${fields}\` entries into \`acknowledge\` by hand and delete them; --fix will not merge two maps for you.`
|
|
2895
|
+
});
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
const entries = plannedEntries(m);
|
|
2899
|
+
const edits = [];
|
|
2900
|
+
const present = legacy.map((f) => ({ f, prop: m.fieldPropSpans?.[f], del: m.fieldSpans?.[f] })).filter((x) => x.prop && x.del).sort((a, b) => a.prop.start - b.prop.start);
|
|
2901
|
+
if (present.length !== legacy.length) {
|
|
2902
|
+
out2.push({
|
|
2903
|
+
...base,
|
|
2904
|
+
message: `\`${fields}\` was replaced by \`acknowledge\`, but the field spans needed to rewrite it are unavailable`,
|
|
2905
|
+
hint: "Rewrite the declaration by hand: `acknowledge: { <scope>: { type, reason? } }`."
|
|
2906
|
+
});
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
const [anchor, ...rest] = present;
|
|
2910
|
+
if (Object.keys(entries).length === 0) {
|
|
2911
|
+
edits.push({
|
|
2912
|
+
path: sourcePath,
|
|
2913
|
+
start: anchor.del.start,
|
|
2914
|
+
end: anchor.del.end,
|
|
2915
|
+
replacement: ""
|
|
2916
|
+
});
|
|
2917
|
+
} else {
|
|
2918
|
+
edits.push({
|
|
2919
|
+
path: sourcePath,
|
|
2920
|
+
start: anchor.prop.start,
|
|
2921
|
+
end: anchor.prop.end,
|
|
2922
|
+
replacement: renderAcknowledge(
|
|
2923
|
+
entries,
|
|
2924
|
+
indentAt(content, anchor.prop.start)
|
|
2925
|
+
)
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
for (const r of rest) {
|
|
2929
|
+
edits.push({
|
|
2930
|
+
path: sourcePath,
|
|
2931
|
+
start: r.del.start,
|
|
2932
|
+
end: r.del.end,
|
|
2933
|
+
replacement: ""
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
out2.push({
|
|
2937
|
+
...base,
|
|
2938
|
+
message: `\`${fields}\` was replaced by the unified \`acknowledge\` field`,
|
|
2939
|
+
hint: `Run \`uidex scan --fix\` to rewrite it${m.legacyCapture === false && !m.description ? ", then replace the TODO reason it plants \u2014 `capture: false` carried no explanation and `impossible` requires one" : ""}.`,
|
|
2940
|
+
fix: {
|
|
2941
|
+
description: `Rewrite \`${fields}\` as \`acknowledge\``,
|
|
2942
|
+
edits
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
return out2;
|
|
2947
|
+
}
|
|
2948
|
+
function migrateLegacyBaseline(opts) {
|
|
2949
|
+
if (!isLegacyBaseline(opts.parsed)) return void 0;
|
|
2950
|
+
return {
|
|
2951
|
+
code: "legacy-coverage-baseline",
|
|
2952
|
+
severity: "error",
|
|
2953
|
+
message: `coverage baseline "${opts.displayPath}" uses the removed \`uncapturedPages\` / \`missingKinds\` shape`,
|
|
2954
|
+
file: opts.displayPath,
|
|
2955
|
+
hint: "Run `uidex scan --fix` to rewrite it as `acknowledge` entries of type `backlog` (identical content, new shape \u2014 no capture run needed).",
|
|
2956
|
+
fix: {
|
|
2957
|
+
description: `Rewrite ${opts.displayPath} as an \`acknowledge\` baseline`,
|
|
2958
|
+
edits: [
|
|
2959
|
+
{
|
|
2960
|
+
path: opts.path,
|
|
2961
|
+
start: 0,
|
|
2962
|
+
end: opts.raw.length,
|
|
2963
|
+
replacement: serializeCoverageBaseline(migrateBaseline(opts.parsed))
|
|
2964
|
+
}
|
|
2965
|
+
]
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2349
2969
|
|
|
2350
2970
|
// src/scanner/scan/page-coverage.ts
|
|
2351
2971
|
function compileRoute(pattern) {
|
|
@@ -2368,11 +2988,11 @@ function compileRoute(pattern) {
|
|
|
2368
2988
|
return { test: (url) => re.test(url), specificity };
|
|
2369
2989
|
}
|
|
2370
2990
|
function matchUrlToRoute(url, routePaths) {
|
|
2371
|
-
const
|
|
2991
|
+
const path13 = stripQuery(url);
|
|
2372
2992
|
let best = null;
|
|
2373
2993
|
for (const rp of routePaths) {
|
|
2374
2994
|
const { test, specificity } = compileRoute(rp);
|
|
2375
|
-
if (!test(
|
|
2995
|
+
if (!test(path13)) continue;
|
|
2376
2996
|
if (!best || specificity > best.specificity)
|
|
2377
2997
|
best = { path: rp, specificity };
|
|
2378
2998
|
}
|
|
@@ -2403,29 +3023,49 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2403
3023
|
if (routes.length === 0) return [];
|
|
2404
3024
|
const routePaths = routes.map((r) => r.path);
|
|
2405
3025
|
const captured = manifest.captured ?? [];
|
|
2406
|
-
const optedOut = /* @__PURE__ */ new Set();
|
|
2407
|
-
for (const r of routes) {
|
|
2408
|
-
const page = registry.get("page", r.page);
|
|
2409
|
-
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
2410
|
-
}
|
|
2411
3026
|
const covered = /* @__PURE__ */ new Set();
|
|
2412
3027
|
for (const c of captured) {
|
|
2413
3028
|
const route = routeForCapture(c, routes, routePaths);
|
|
2414
3029
|
if (route) covered.add(route);
|
|
2415
3030
|
}
|
|
2416
|
-
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
2417
3031
|
const diagnostics = [];
|
|
2418
3032
|
for (const r of routes) {
|
|
2419
|
-
|
|
3033
|
+
const ack = routeAcknowledge(registry, baseline, r.path, r.page).get(
|
|
3034
|
+
ACK_ALL
|
|
3035
|
+
);
|
|
2420
3036
|
const loc = registry.get("page", r.page)?.loc;
|
|
2421
|
-
if (
|
|
3037
|
+
if (covered.has(r.path)) {
|
|
3038
|
+
if (ack?.type === "impossible") {
|
|
3039
|
+
diagnostics.push({
|
|
3040
|
+
code: "stale-acknowledgment",
|
|
3041
|
+
severity: "warning",
|
|
3042
|
+
message: `page "${r.page}" acknowledges "*" as ${describeAck(ack)}, but route "${r.path}" is captured`,
|
|
3043
|
+
file: loc?.file,
|
|
3044
|
+
line: loc?.line,
|
|
3045
|
+
entity: { kind: "page", id: r.page },
|
|
3046
|
+
hint: originHint(ack, r.page)
|
|
3047
|
+
});
|
|
3048
|
+
} else if (ack?.type === "backlog") {
|
|
3049
|
+
diagnostics.push({
|
|
3050
|
+
code: "page-captured",
|
|
3051
|
+
severity: "warning",
|
|
3052
|
+
message: `route "${r.path}" is now captured but is still acknowledged as backlog`,
|
|
3053
|
+
file: loc?.file,
|
|
3054
|
+
line: loc?.line,
|
|
3055
|
+
hint: originHint(ack, r.page)
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
if (ack?.type === "impossible") continue;
|
|
3061
|
+
if (ack?.type === "backlog") {
|
|
2422
3062
|
diagnostics.push({
|
|
2423
3063
|
code: "page-backlog",
|
|
2424
3064
|
severity: "info",
|
|
2425
3065
|
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
2426
3066
|
file: loc?.file,
|
|
2427
3067
|
line: loc?.line,
|
|
2428
|
-
hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or
|
|
3068
|
+
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}".`
|
|
2429
3069
|
});
|
|
2430
3070
|
continue;
|
|
2431
3071
|
}
|
|
@@ -2436,46 +3076,155 @@ function checkPageCoverage(registry, manifest, baseline) {
|
|
|
2436
3076
|
file: loc?.file,
|
|
2437
3077
|
line: loc?.line,
|
|
2438
3078
|
entity: { kind: "route", id: r.path },
|
|
2439
|
-
hint: `Capture the route in a states spec
|
|
3079
|
+
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).`
|
|
2440
3080
|
});
|
|
2441
3081
|
}
|
|
2442
|
-
for (const b of
|
|
2443
|
-
if (
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
} else if (covered.has(b) || optedOut.has(b)) {
|
|
2451
|
-
diagnostics.push({
|
|
2452
|
-
code: "page-captured",
|
|
2453
|
-
severity: "warning",
|
|
2454
|
-
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
2455
|
-
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
3082
|
+
for (const b of Object.keys(baseline?.acknowledge ?? {})) {
|
|
3083
|
+
if (routePaths.includes(b)) continue;
|
|
3084
|
+
diagnostics.push({
|
|
3085
|
+
code: "stale-page-baseline",
|
|
3086
|
+
severity: "warning",
|
|
3087
|
+
message: `coverage baseline acknowledges "${b}", which is not a route in the registry`,
|
|
3088
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
3089
|
+
});
|
|
2458
3090
|
}
|
|
2459
3091
|
return diagnostics;
|
|
2460
3092
|
}
|
|
2461
|
-
function
|
|
2462
|
-
const
|
|
3093
|
+
function computePageBacklog(registry, manifest) {
|
|
3094
|
+
const out2 = {};
|
|
2463
3095
|
for (const d of checkPageCoverage(registry, manifest, {
|
|
2464
|
-
|
|
3096
|
+
version: 2,
|
|
3097
|
+
acknowledge: {}
|
|
2465
3098
|
})) {
|
|
2466
|
-
if (d.code === "page-no-capture" && d.entity)
|
|
3099
|
+
if (d.code === "page-no-capture" && d.entity) {
|
|
3100
|
+
out2[d.entity.id] = { [ACK_ALL]: { type: "backlog" } };
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
return out2;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/scanner/scan/workspace.ts
|
|
3107
|
+
import * as fs3 from "fs";
|
|
3108
|
+
import * as path5 from "path";
|
|
3109
|
+
var WORKSPACE_MARKERS = [
|
|
3110
|
+
"pnpm-workspace.yaml",
|
|
3111
|
+
"pnpm-workspace.yml",
|
|
3112
|
+
"turbo.json",
|
|
3113
|
+
"lerna.json",
|
|
3114
|
+
"nx.json",
|
|
3115
|
+
".git"
|
|
3116
|
+
];
|
|
3117
|
+
var MAX_ASCENT = 8;
|
|
3118
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3119
|
+
function captureKey(kind, entity) {
|
|
3120
|
+
return `${kind ?? ""}|${entity}`;
|
|
3121
|
+
}
|
|
3122
|
+
function hasMarker(dir) {
|
|
3123
|
+
return WORKSPACE_MARKERS.some((m) => fs3.existsSync(path5.join(dir, m)));
|
|
3124
|
+
}
|
|
3125
|
+
function findWorkspaceRoot(configDir) {
|
|
3126
|
+
let dir = path5.resolve(configDir);
|
|
3127
|
+
for (let i = 0; i < MAX_ASCENT; i++) {
|
|
3128
|
+
const parent = path5.dirname(dir);
|
|
3129
|
+
if (parent === dir) return void 0;
|
|
3130
|
+
dir = parent;
|
|
3131
|
+
if (hasMarker(dir)) return dir;
|
|
3132
|
+
}
|
|
3133
|
+
return void 0;
|
|
3134
|
+
}
|
|
3135
|
+
function captureKeys(manifest) {
|
|
3136
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
3137
|
+
for (const c of manifest?.captured ?? []) {
|
|
3138
|
+
out2.add(captureKey(c.kind, c.entity));
|
|
3139
|
+
}
|
|
3140
|
+
return out2;
|
|
3141
|
+
}
|
|
3142
|
+
function readManifest(configDir, config) {
|
|
3143
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3144
|
+
let raw;
|
|
3145
|
+
try {
|
|
3146
|
+
raw = fs3.readFileSync(path5.resolve(configDir, rel), "utf8");
|
|
3147
|
+
} catch {
|
|
3148
|
+
return void 0;
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
const parsed = JSON.parse(raw);
|
|
3152
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3153
|
+
const captured = [];
|
|
3154
|
+
for (const entry of parsed.captured) {
|
|
3155
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3156
|
+
const c = entry;
|
|
3157
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3158
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3159
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3160
|
+
captured.push(rec);
|
|
3161
|
+
}
|
|
3162
|
+
return { captured };
|
|
3163
|
+
} catch {
|
|
3164
|
+
return void 0;
|
|
2467
3165
|
}
|
|
2468
|
-
|
|
3166
|
+
}
|
|
3167
|
+
function toApp(dc) {
|
|
3168
|
+
return {
|
|
3169
|
+
name: path5.basename(dc.configDir),
|
|
3170
|
+
configDir: dc.configDir,
|
|
3171
|
+
configPath: dc.configPath,
|
|
3172
|
+
registers: createSourceMatcher(dc.config.sources, {
|
|
3173
|
+
cwd: dc.configDir,
|
|
3174
|
+
globalExcludes: dc.config.exclude
|
|
3175
|
+
}),
|
|
3176
|
+
captured: captureKeys(readManifest(dc.configDir, dc.config))
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
function resolveWorkspace(opts) {
|
|
3180
|
+
const { configDir, config } = opts;
|
|
3181
|
+
if (config.workspace === false) return void 0;
|
|
3182
|
+
const root = typeof config.workspace === "string" ? path5.resolve(configDir, config.workspace) : findWorkspaceRoot(configDir);
|
|
3183
|
+
if (!root) return void 0;
|
|
3184
|
+
const configs = opts.configs ?? discover({ cwd: root, all: true });
|
|
3185
|
+
const self = path5.resolve(configDir, CONFIG_FILENAME);
|
|
3186
|
+
const siblings = configs.filter((dc) => path5.resolve(dc.configPath) !== self).map(toApp);
|
|
3187
|
+
if (siblings.length === 0) return void 0;
|
|
3188
|
+
return { root, siblings };
|
|
3189
|
+
}
|
|
3190
|
+
function capturedElsewhere(workspace, absFile, kind, id) {
|
|
3191
|
+
if (!workspace || !absFile) return void 0;
|
|
3192
|
+
for (const app of workspace.siblings) {
|
|
3193
|
+
if (!app.registers(absFile)) continue;
|
|
3194
|
+
if (app.captured.has(captureKey(kind, id)) || app.captured.has(captureKey(void 0, id))) {
|
|
3195
|
+
return app.name;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return void 0;
|
|
2469
3199
|
}
|
|
2470
3200
|
|
|
2471
3201
|
// src/scanner/scan/state-coverage.ts
|
|
2472
3202
|
var STATE_KINDS2 = ["page", "feature", "widget"];
|
|
2473
|
-
function declaredStateEntities(registry) {
|
|
3203
|
+
function declaredStateEntities(registry, opts, ungated) {
|
|
2474
3204
|
const out2 = [];
|
|
2475
3205
|
for (const kind of STATE_KINDS2) {
|
|
2476
3206
|
for (const e of registry.list(kind)) {
|
|
2477
3207
|
const states = e.meta?.states;
|
|
2478
3208
|
if (!states || states.length === 0) continue;
|
|
3209
|
+
const absFile = e.loc?.file ? opts.resolveSourcePath?.(e.loc.file) : void 0;
|
|
3210
|
+
const ack = entityAcknowledge(
|
|
3211
|
+
e.meta,
|
|
3212
|
+
capturedElsewhere(opts.workspace, absFile, kind, e.id)
|
|
3213
|
+
);
|
|
3214
|
+
if (ack) {
|
|
3215
|
+
if (ack.origin === "workspace") {
|
|
3216
|
+
ungated.push({
|
|
3217
|
+
code: "acknowledged-elsewhere",
|
|
3218
|
+
severity: "info",
|
|
3219
|
+
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)})`,
|
|
3220
|
+
file: e.loc?.file,
|
|
3221
|
+
line: e.loc?.line,
|
|
3222
|
+
entity: { kind, id: e.id },
|
|
3223
|
+
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.`
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
2479
3228
|
out2.push({
|
|
2480
3229
|
kind,
|
|
2481
3230
|
id: e.id,
|
|
@@ -2486,18 +3235,31 @@ function declaredStateEntities(registry) {
|
|
|
2486
3235
|
}
|
|
2487
3236
|
return out2;
|
|
2488
3237
|
}
|
|
2489
|
-
function
|
|
3238
|
+
function unreliablePageKinds(registry, captured) {
|
|
3239
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
3240
|
+
for (const c of captured) {
|
|
3241
|
+
if (c.kind !== "page") continue;
|
|
3242
|
+
if (registry.get("page", c.entity)) continue;
|
|
3243
|
+
out2.add(c.entity);
|
|
3244
|
+
}
|
|
3245
|
+
return out2;
|
|
3246
|
+
}
|
|
3247
|
+
function captureMatchesEntity(cap, ent, unreliable = EMPTY) {
|
|
2490
3248
|
if (cap.entity !== ent.id) return false;
|
|
2491
|
-
|
|
3249
|
+
if (cap.kind === void 0) return true;
|
|
3250
|
+
if (cap.kind === ent.kind) return true;
|
|
3251
|
+
return cap.kind === "page" && unreliable.has(cap.entity);
|
|
2492
3252
|
}
|
|
2493
|
-
|
|
3253
|
+
var EMPTY = /* @__PURE__ */ new Set();
|
|
3254
|
+
function checkStateCoverage(registry, manifest, opts = {}) {
|
|
2494
3255
|
const diagnostics = [];
|
|
2495
|
-
const declared = declaredStateEntities(registry);
|
|
2496
3256
|
const captured = manifest.captured ?? [];
|
|
3257
|
+
const unreliable = unreliablePageKinds(registry, captured);
|
|
3258
|
+
const declared = declaredStateEntities(registry, opts, diagnostics);
|
|
2497
3259
|
for (const ent of declared) {
|
|
2498
3260
|
for (const state of ent.states) {
|
|
2499
3261
|
const hit = captured.some(
|
|
2500
|
-
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
3262
|
+
(c) => captureMatchesEntity(c, ent, unreliable) && c.state === state
|
|
2501
3263
|
);
|
|
2502
3264
|
if (hit) continue;
|
|
2503
3265
|
diagnostics.push({
|
|
@@ -2512,7 +3274,9 @@ function checkStateCoverage(registry, manifest) {
|
|
|
2512
3274
|
}
|
|
2513
3275
|
}
|
|
2514
3276
|
for (const cap of captured) {
|
|
2515
|
-
const candidates = declared.filter(
|
|
3277
|
+
const candidates = declared.filter(
|
|
3278
|
+
(ent2) => captureMatchesEntity(cap, ent2, unreliable)
|
|
3279
|
+
);
|
|
2516
3280
|
if (candidates.length === 0) continue;
|
|
2517
3281
|
const declaredSomewhere = candidates.some(
|
|
2518
3282
|
(ent2) => ent2.states.includes(cap.state)
|
|
@@ -2556,36 +3320,35 @@ function coreKindsByRoute(registry, manifest) {
|
|
|
2556
3320
|
}
|
|
2557
3321
|
return byRoute;
|
|
2558
3322
|
}
|
|
2559
|
-
function pageWaivers(registry, routePage) {
|
|
2560
|
-
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
2561
|
-
}
|
|
2562
|
-
function isOptedOut(registry, page) {
|
|
2563
|
-
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
2564
|
-
}
|
|
2565
3323
|
function checkStateMatrix(registry, manifest, baseline) {
|
|
2566
3324
|
const routes = registry.list("route");
|
|
2567
3325
|
if (routes.length === 0) return [];
|
|
2568
3326
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2569
3327
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2570
|
-
const baselined = baseline?.missingKinds ?? {};
|
|
2571
3328
|
const diagnostics = [];
|
|
2572
3329
|
for (const [route, kinds] of captured) {
|
|
2573
3330
|
const page = pageOf.get(route);
|
|
2574
|
-
|
|
2575
|
-
const waivers = page ? pageWaivers(registry, page) : {};
|
|
3331
|
+
const ack = routeAcknowledge(registry, baseline, route, page);
|
|
2576
3332
|
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
2577
|
-
const
|
|
3333
|
+
const all = ack.get(ACK_ALL);
|
|
3334
|
+
if (all) {
|
|
3335
|
+
if (all.type === "impossible") {
|
|
3336
|
+
diagnostics.push(staleDiagnostic(all, route, page, loc));
|
|
3337
|
+
}
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
2578
3340
|
for (const kind of CORE_KINDS) {
|
|
2579
3341
|
if (kinds.has(kind)) continue;
|
|
2580
|
-
|
|
2581
|
-
if (
|
|
3342
|
+
const entry = ack.get(kind);
|
|
3343
|
+
if (entry?.type === "impossible") continue;
|
|
3344
|
+
if (entry?.type === "backlog") {
|
|
2582
3345
|
diagnostics.push({
|
|
2583
3346
|
code: "matrix-backlog",
|
|
2584
3347
|
severity: "info",
|
|
2585
|
-
message: `route "${route}" does not render core kind "${kind}" (acknowledged
|
|
3348
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged backlog)`,
|
|
2586
3349
|
file: loc?.file,
|
|
2587
3350
|
line: loc?.line,
|
|
2588
|
-
hint: `Capture the "${kind}" state for "${route}",
|
|
3351
|
+
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" }\`.`
|
|
2589
3352
|
});
|
|
2590
3353
|
continue;
|
|
2591
3354
|
}
|
|
@@ -2596,42 +3359,43 @@ function checkStateMatrix(registry, manifest, baseline) {
|
|
|
2596
3359
|
file: loc?.file,
|
|
2597
3360
|
line: loc?.line,
|
|
2598
3361
|
entity: { kind: "route", id: route },
|
|
2599
|
-
hint: `Capture the "${kind}" state
|
|
3362
|
+
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).`
|
|
2600
3363
|
});
|
|
2601
3364
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
if (!page || isOptedOut(registry, page)) continue;
|
|
2606
|
-
const waivers = pageWaivers(registry, page);
|
|
2607
|
-
const loc = registry.get("page", page)?.loc;
|
|
2608
|
-
for (const kind of Object.keys(waivers)) {
|
|
2609
|
-
if (kinds.has(kind)) {
|
|
2610
|
-
diagnostics.push({
|
|
2611
|
-
code: "stale-waiver",
|
|
2612
|
-
severity: "warning",
|
|
2613
|
-
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
2614
|
-
file: loc?.file,
|
|
2615
|
-
line: loc?.line,
|
|
2616
|
-
entity: { kind: "page", id: page },
|
|
2617
|
-
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
3365
|
+
for (const [scope, entry] of ack) {
|
|
3366
|
+
if (scope === ACK_ALL || !kinds.has(scope)) continue;
|
|
3367
|
+
diagnostics.push(staleDiagnostic(entry, route, page, loc));
|
|
2620
3368
|
}
|
|
2621
3369
|
}
|
|
2622
3370
|
return diagnostics;
|
|
2623
3371
|
}
|
|
2624
|
-
function
|
|
3372
|
+
function staleDiagnostic(entry, route, page, loc) {
|
|
3373
|
+
const what = entry.scope === ACK_ALL ? `route "${route}" is captured` : `route "${route}" does render core kind "${entry.scope}"`;
|
|
3374
|
+
return {
|
|
3375
|
+
code: "stale-acknowledgment",
|
|
3376
|
+
severity: "warning",
|
|
3377
|
+
message: `${page ? `page "${page}"` : `route "${route}"`} acknowledges "${entry.scope}" as ${describeAck(entry)}, but ${what}`,
|
|
3378
|
+
file: loc?.file,
|
|
3379
|
+
line: loc?.line,
|
|
3380
|
+
...page ? { entity: { kind: "page", id: page } } : {},
|
|
3381
|
+
hint: originHint(entry, page)
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
function computeMatrixBacklog(registry, manifest) {
|
|
2625
3385
|
const routes = registry.list("route");
|
|
2626
3386
|
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
2627
3387
|
const captured = coreKindsByRoute(registry, manifest);
|
|
2628
3388
|
const out2 = {};
|
|
2629
3389
|
for (const [route, kinds] of captured) {
|
|
2630
3390
|
const page = pageOf.get(route);
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
const
|
|
2634
|
-
|
|
3391
|
+
const ack = routeAcknowledge(registry, void 0, route, page);
|
|
3392
|
+
if (ack.has(ACK_ALL)) continue;
|
|
3393
|
+
const scopes = {};
|
|
3394
|
+
for (const kind of CORE_KINDS) {
|
|
3395
|
+
if (kinds.has(kind) || ack.has(kind)) continue;
|
|
3396
|
+
scopes[kind] = { type: "backlog" };
|
|
3397
|
+
}
|
|
3398
|
+
if (Object.keys(scopes).length > 0) out2[route] = scopes;
|
|
2635
3399
|
}
|
|
2636
3400
|
return out2;
|
|
2637
3401
|
}
|
|
@@ -2655,6 +3419,23 @@ function audit(opts) {
|
|
|
2655
3419
|
for (const ef of opts.flowExtracted ?? []) {
|
|
2656
3420
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
2657
3421
|
}
|
|
3422
|
+
for (const ef of extracted) diagnostics.push(...migrateLegacyFields(ef));
|
|
3423
|
+
if (opts.baselineDiagnostic) diagnostics.push(opts.baselineDiagnostic);
|
|
3424
|
+
for (const ef of extracted) {
|
|
3425
|
+
for (const m of ef.metadata ?? []) {
|
|
3426
|
+
for (const [scope, ack] of Object.entries(m.acknowledge ?? {})) {
|
|
3427
|
+
if (!isPlaceholderReason(ack.reason)) continue;
|
|
3428
|
+
diagnostics.push({
|
|
3429
|
+
code: "placeholder-reason",
|
|
3430
|
+
severity: "warning",
|
|
3431
|
+
message: `acknowledge "${scope}" still has the migration's placeholder reason ("${ack.reason}")`,
|
|
3432
|
+
file: ef.file.displayPath,
|
|
3433
|
+
line: m.loc.line,
|
|
3434
|
+
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).`
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
2658
3439
|
if (check && opts.generated !== void 0) {
|
|
2659
3440
|
const outRel = opts.outputPath ?? config.output;
|
|
2660
3441
|
const fresh = normalizeForCheck(opts.generated);
|
|
@@ -2768,8 +3549,8 @@ function audit(opts) {
|
|
|
2768
3549
|
if (typeof m.id !== "string") continue;
|
|
2769
3550
|
const filePath = ef.file.displayPath;
|
|
2770
3551
|
const wellKnownName = WELL_KNOWN_FILES[m.kind];
|
|
2771
|
-
if (
|
|
2772
|
-
const dir =
|
|
3552
|
+
if (path6.posix.basename(filePath) === wellKnownName) continue;
|
|
3553
|
+
const dir = path6.posix.dirname(filePath);
|
|
2773
3554
|
const wellKnownPath = dir === "." ? wellKnownName : `${dir}/${wellKnownName}`;
|
|
2774
3555
|
if (scannedPaths.has(wellKnownPath)) continue;
|
|
2775
3556
|
const kindLabel = m.kind === "page" ? "Page" : "Feature";
|
|
@@ -2983,25 +3764,41 @@ function audit(opts) {
|
|
|
2983
3764
|
}
|
|
2984
3765
|
}
|
|
2985
3766
|
if (lint && statesEnabled && opts.capturedStates) {
|
|
2986
|
-
|
|
3767
|
+
const sourceByDisplay = new Map(
|
|
3768
|
+
files.map((f) => [f.displayPath, f.sourcePath])
|
|
3769
|
+
);
|
|
3770
|
+
diagnostics.push(
|
|
3771
|
+
...checkStateCoverage(registry, opts.capturedStates, {
|
|
3772
|
+
workspace: opts.workspace,
|
|
3773
|
+
resolveSourcePath: (displayPath) => sourceByDisplay.get(displayPath)
|
|
3774
|
+
})
|
|
3775
|
+
);
|
|
2987
3776
|
}
|
|
2988
3777
|
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
2989
3778
|
diagnostics.push(
|
|
2990
|
-
...checkPageCoverage(registry, opts.capturedStates, opts.
|
|
3779
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.coverageBaseline)
|
|
2991
3780
|
);
|
|
2992
3781
|
}
|
|
3782
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
3783
|
+
diagnostics.push(...checkComponentCoverage(registry, opts.capturedStates));
|
|
3784
|
+
}
|
|
2993
3785
|
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
2994
3786
|
diagnostics.push(
|
|
2995
|
-
...checkStateMatrix(registry, opts.capturedStates,
|
|
2996
|
-
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
2997
|
-
})
|
|
3787
|
+
...checkStateMatrix(registry, opts.capturedStates, opts.coverageBaseline)
|
|
2998
3788
|
);
|
|
2999
3789
|
}
|
|
3790
|
+
const overrides = config.audit?.severity ?? {};
|
|
3791
|
+
const governed = diagnostics.flatMap((d) => {
|
|
3792
|
+
const level = overrides[d.code];
|
|
3793
|
+
if (level === void 0) return [d];
|
|
3794
|
+
if (level === "off") return [];
|
|
3795
|
+
return [{ ...d, severity: level }];
|
|
3796
|
+
});
|
|
3000
3797
|
const summary = {
|
|
3001
|
-
errors:
|
|
3002
|
-
warnings:
|
|
3798
|
+
errors: governed.filter((d) => d.severity === "error").length,
|
|
3799
|
+
warnings: governed.filter((d) => d.severity === "warning").length
|
|
3003
3800
|
};
|
|
3004
|
-
return { diagnostics, summary };
|
|
3801
|
+
return { diagnostics: governed, summary };
|
|
3005
3802
|
}
|
|
3006
3803
|
function lineOfOffset(content, offset) {
|
|
3007
3804
|
let line = 1;
|
|
@@ -3329,6 +4126,30 @@ ${body}
|
|
|
3329
4126
|
`;
|
|
3330
4127
|
}
|
|
3331
4128
|
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
4129
|
+
function stateEntities(list) {
|
|
4130
|
+
return list.map((e) => ({
|
|
4131
|
+
id: e.id,
|
|
4132
|
+
states: (e.meta?.states ?? []).map((s) => s.name)
|
|
4133
|
+
}));
|
|
4134
|
+
}
|
|
4135
|
+
function emitStateMap(groups) {
|
|
4136
|
+
const lines = ["export interface UidexStates {"];
|
|
4137
|
+
for (const g of groups) {
|
|
4138
|
+
const withStates = g.entities.filter((e) => e.states.length > 0);
|
|
4139
|
+
if (withStates.length === 0) {
|
|
4140
|
+
lines.push(` ${g.kind}: never`);
|
|
4141
|
+
continue;
|
|
4142
|
+
}
|
|
4143
|
+
lines.push(` ${g.kind}: {`);
|
|
4144
|
+
for (const e of [...withStates].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4145
|
+
const states = [...new Set(e.states)].sort().map((s) => JSON.stringify(s)).join(" | ");
|
|
4146
|
+
lines.push(` ${JSON.stringify(e.id)}: ${states}`);
|
|
4147
|
+
}
|
|
4148
|
+
lines.push(" }");
|
|
4149
|
+
}
|
|
4150
|
+
lines.push("}");
|
|
4151
|
+
return lines.join("\n") + "\n";
|
|
4152
|
+
}
|
|
3332
4153
|
function emitId(name, ids) {
|
|
3333
4154
|
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
3334
4155
|
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
@@ -3416,6 +4237,14 @@ function emit(opts) {
|
|
|
3416
4237
|
);
|
|
3417
4238
|
t.push(emitId("StateId", [...stateNames]));
|
|
3418
4239
|
t.push("");
|
|
4240
|
+
t.push("// ---- declared state space (kind \u2192 entity \u2192 its own states) ----");
|
|
4241
|
+
t.push(
|
|
4242
|
+
emitStateMap([
|
|
4243
|
+
{ kind: "page", entities: stateEntities(pages) },
|
|
4244
|
+
{ kind: "feature", entities: stateEntities(features) },
|
|
4245
|
+
{ kind: "widget", entities: stateEntities(widgets) }
|
|
4246
|
+
])
|
|
4247
|
+
);
|
|
3419
4248
|
t.push("// ---- authoring-surface shape types ----");
|
|
3420
4249
|
t.push("export namespace Uidex {");
|
|
3421
4250
|
t.push(
|
|
@@ -3428,7 +4257,20 @@ function emit(opts) {
|
|
|
3428
4257
|
t.push(" acceptance?: readonly string[]");
|
|
3429
4258
|
t.push(" description?: string");
|
|
3430
4259
|
t.push(" }");
|
|
3431
|
-
t.push(
|
|
4260
|
+
t.push(' export type AcknowledgeScope = CoreStateKind | "*"');
|
|
4261
|
+
t.push(" export interface Impossible {");
|
|
4262
|
+
t.push(' type: "impossible"');
|
|
4263
|
+
t.push(" reason: string");
|
|
4264
|
+
t.push(" }");
|
|
4265
|
+
t.push(" export interface Backlog {");
|
|
4266
|
+
t.push(' type: "backlog"');
|
|
4267
|
+
t.push(" reason?: string");
|
|
4268
|
+
t.push(" }");
|
|
4269
|
+
t.push(" export type Acknowledgment = Impossible | Backlog");
|
|
4270
|
+
t.push(
|
|
4271
|
+
" export type Acknowledge = Partial<Record<AcknowledgeScope, Acknowledgment>>"
|
|
4272
|
+
);
|
|
4273
|
+
t.push(' export type ComponentAcknowledge = { "*"?: Acknowledgment }');
|
|
3432
4274
|
t.push(" export interface Page {");
|
|
3433
4275
|
t.push(" page: PageId | false");
|
|
3434
4276
|
t.push(" name?: string");
|
|
@@ -3436,8 +4278,7 @@ function emit(opts) {
|
|
|
3436
4278
|
t.push(" widgets?: readonly WidgetId[]");
|
|
3437
4279
|
t.push(" acceptance?: readonly string[]");
|
|
3438
4280
|
t.push(" states?: readonly (string | State)[]");
|
|
3439
|
-
t.push("
|
|
3440
|
-
t.push(" waivers?: Waivers");
|
|
4281
|
+
t.push(" acknowledge?: Acknowledge");
|
|
3441
4282
|
t.push(" description?: string");
|
|
3442
4283
|
t.push(" }");
|
|
3443
4284
|
t.push(" export interface Feature {");
|
|
@@ -3446,6 +4287,7 @@ function emit(opts) {
|
|
|
3446
4287
|
t.push(" features?: readonly FeatureId[]");
|
|
3447
4288
|
t.push(" acceptance?: readonly string[]");
|
|
3448
4289
|
t.push(" states?: readonly (string | State)[]");
|
|
4290
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3449
4291
|
t.push(" description?: string");
|
|
3450
4292
|
t.push(" }");
|
|
3451
4293
|
t.push(" export interface Primitive {");
|
|
@@ -3458,6 +4300,7 @@ function emit(opts) {
|
|
|
3458
4300
|
t.push(" name?: string");
|
|
3459
4301
|
t.push(" acceptance?: readonly string[]");
|
|
3460
4302
|
t.push(" states?: readonly (string | State)[]");
|
|
4303
|
+
t.push(" acknowledge?: ComponentAcknowledge");
|
|
3461
4304
|
t.push(" description?: string");
|
|
3462
4305
|
t.push(" }");
|
|
3463
4306
|
t.push(" export interface Region {");
|
|
@@ -3553,8 +4396,8 @@ function parseGitHubRef(ref) {
|
|
|
3553
4396
|
}
|
|
3554
4397
|
|
|
3555
4398
|
// src/scanner/scan/scaffold.ts
|
|
3556
|
-
import * as
|
|
3557
|
-
import * as
|
|
4399
|
+
import * as fs4 from "fs";
|
|
4400
|
+
import * as path7 from "path";
|
|
3558
4401
|
function scaffoldWidgetSpec(opts) {
|
|
3559
4402
|
return scaffoldSpec({
|
|
3560
4403
|
registry: opts.registry,
|
|
@@ -3580,8 +4423,8 @@ function scaffoldSpec(opts) {
|
|
|
3580
4423
|
}
|
|
3581
4424
|
const criteria = entity.meta?.acceptance ?? [];
|
|
3582
4425
|
const filename = kind === "widget" ? `widget-${id}.spec.ts` : `flow-${id}.spec.ts`;
|
|
3583
|
-
const outputPath =
|
|
3584
|
-
if (
|
|
4426
|
+
const outputPath = path7.resolve(outDir, filename);
|
|
4427
|
+
if (fs4.existsSync(outputPath) && !force) {
|
|
3585
4428
|
return {
|
|
3586
4429
|
outputPath,
|
|
3587
4430
|
written: false,
|
|
@@ -3590,8 +4433,8 @@ function scaffoldSpec(opts) {
|
|
|
3590
4433
|
};
|
|
3591
4434
|
}
|
|
3592
4435
|
const content = renderSpec({ id, criteria, fixtureImport });
|
|
3593
|
-
|
|
3594
|
-
|
|
4436
|
+
fs4.mkdirSync(path7.dirname(outputPath), { recursive: true });
|
|
4437
|
+
fs4.writeFileSync(outputPath, content, "utf8");
|
|
3595
4438
|
return { outputPath, written: true, skipped: false };
|
|
3596
4439
|
}
|
|
3597
4440
|
function capitalize(s) {
|
|
@@ -3599,25 +4442,27 @@ function capitalize(s) {
|
|
|
3599
4442
|
}
|
|
3600
4443
|
function renderSpec(args) {
|
|
3601
4444
|
const lines = [];
|
|
3602
|
-
lines.push(
|
|
3603
|
-
`import { test, expect } from ${JSON.stringify(args.fixtureImport)}`
|
|
3604
|
-
);
|
|
4445
|
+
lines.push(`import { test } from ${JSON.stringify(args.fixtureImport)}`);
|
|
3605
4446
|
lines.push("");
|
|
3606
4447
|
lines.push(
|
|
3607
4448
|
`test.describe(${JSON.stringify(args.id)}, { tag: "@uidex:flow" }, () => {`
|
|
3608
4449
|
);
|
|
3609
|
-
|
|
3610
|
-
lines.push(` test(
|
|
3611
|
-
lines.push(`
|
|
4450
|
+
const stub = (title, body) => {
|
|
4451
|
+
lines.push(` test.fixme(${JSON.stringify(title)}, async ({ uidex }) => {`);
|
|
4452
|
+
for (const l of body) lines.push(` ${l}`);
|
|
4453
|
+
lines.push(` void uidex`);
|
|
3612
4454
|
lines.push(` })`);
|
|
4455
|
+
lines.push("");
|
|
4456
|
+
};
|
|
4457
|
+
if (args.criteria.length === 0) {
|
|
4458
|
+
stub("TODO: add acceptance criteria", [
|
|
4459
|
+
"// Declare `acceptance: [...]` on the entity, then re-run scaffold."
|
|
4460
|
+
]);
|
|
3613
4461
|
} else {
|
|
3614
4462
|
for (const criterion of args.criteria) {
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
lines.push(` expect(true).toBe(true)`);
|
|
3619
|
-
lines.push(` })`);
|
|
3620
|
-
lines.push("");
|
|
4463
|
+
stub(criterion, [
|
|
4464
|
+
"// TODO: drive this criterion, then remove `.fixme` to arm the test."
|
|
4465
|
+
]);
|
|
3621
4466
|
}
|
|
3622
4467
|
}
|
|
3623
4468
|
lines.push("})");
|
|
@@ -3626,47 +4471,40 @@ function renderSpec(args) {
|
|
|
3626
4471
|
}
|
|
3627
4472
|
|
|
3628
4473
|
// src/scanner/scan/pipeline.ts
|
|
3629
|
-
import * as
|
|
3630
|
-
import * as
|
|
3631
|
-
var
|
|
4474
|
+
import * as fs5 from "fs";
|
|
4475
|
+
import * as path8 from "path";
|
|
4476
|
+
var DEFAULT_STATES_MANIFEST2 = "uidex-states.json";
|
|
3632
4477
|
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3633
|
-
function
|
|
4478
|
+
function loadCoverageBaseline(configDir, config) {
|
|
3634
4479
|
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3635
|
-
const abs =
|
|
4480
|
+
const abs = path8.resolve(configDir, rel);
|
|
3636
4481
|
let raw;
|
|
3637
4482
|
try {
|
|
3638
|
-
raw =
|
|
4483
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3639
4484
|
} catch {
|
|
3640
|
-
return
|
|
4485
|
+
return {};
|
|
3641
4486
|
}
|
|
4487
|
+
let parsed;
|
|
3642
4488
|
try {
|
|
3643
|
-
|
|
3644
|
-
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3645
|
-
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3646
|
-
(p2) => typeof p2 === "string"
|
|
3647
|
-
);
|
|
3648
|
-
let missingKinds;
|
|
3649
|
-
const mkRaw = parsed.missingKinds;
|
|
3650
|
-
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3651
|
-
const mk = {};
|
|
3652
|
-
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3653
|
-
if (Array.isArray(kinds)) {
|
|
3654
|
-
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
missingKinds = mk;
|
|
3658
|
-
}
|
|
3659
|
-
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
4489
|
+
parsed = JSON.parse(raw);
|
|
3660
4490
|
} catch {
|
|
3661
|
-
return
|
|
4491
|
+
return {};
|
|
3662
4492
|
}
|
|
4493
|
+
const legacy = migrateLegacyBaseline({
|
|
4494
|
+
path: abs,
|
|
4495
|
+
raw,
|
|
4496
|
+
parsed,
|
|
4497
|
+
displayPath: rel
|
|
4498
|
+
});
|
|
4499
|
+
if (legacy) return { legacy };
|
|
4500
|
+
return { baseline: parseCoverageBaseline(parsed) ?? void 0 };
|
|
3663
4501
|
}
|
|
3664
4502
|
function loadCapturedStates(configDir, config) {
|
|
3665
|
-
const rel = config.statesManifest ??
|
|
3666
|
-
const abs =
|
|
4503
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST2;
|
|
4504
|
+
const abs = path8.resolve(configDir, rel);
|
|
3667
4505
|
let raw;
|
|
3668
4506
|
try {
|
|
3669
|
-
raw =
|
|
4507
|
+
raw = fs5.readFileSync(abs, "utf8");
|
|
3670
4508
|
} catch {
|
|
3671
4509
|
return void 0;
|
|
3672
4510
|
}
|
|
@@ -3685,7 +4523,7 @@ function loadCapturedStates(configDir, config) {
|
|
|
3685
4523
|
if (typeof c.route === "string") rec.route = c.route;
|
|
3686
4524
|
captured.push(rec);
|
|
3687
4525
|
}
|
|
3688
|
-
return { captured };
|
|
4526
|
+
return typeof parsed.generatedAt === "string" ? { captured, generatedAt: parsed.generatedAt } : { captured };
|
|
3689
4527
|
} catch {
|
|
3690
4528
|
return void 0;
|
|
3691
4529
|
}
|
|
@@ -3721,11 +4559,11 @@ function runOne(dc, opts) {
|
|
|
3721
4559
|
gitContext
|
|
3722
4560
|
});
|
|
3723
4561
|
const typesRel = config.output;
|
|
3724
|
-
const typesPath =
|
|
4562
|
+
const typesPath = path8.resolve(configDir, typesRel);
|
|
3725
4563
|
const dataRel = dataOutputPath(config.output);
|
|
3726
|
-
const dataPath =
|
|
4564
|
+
const dataPath = path8.resolve(configDir, dataRel);
|
|
3727
4565
|
const locsRel = locsOutputPath(config.output);
|
|
3728
|
-
const locsPath =
|
|
4566
|
+
const locsPath = path8.resolve(configDir, locsRel);
|
|
3729
4567
|
let typesOnDisk = null;
|
|
3730
4568
|
let dataOnDisk = null;
|
|
3731
4569
|
let locsOnDisk = null;
|
|
@@ -3738,9 +4576,11 @@ function runOne(dc, opts) {
|
|
|
3738
4576
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3739
4577
|
);
|
|
3740
4578
|
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
3741
|
-
const
|
|
4579
|
+
const loadedBaseline = opts.coverageBaseline ? { baseline: opts.coverageBaseline } : loadCoverageBaseline(configDir, config);
|
|
4580
|
+
const coverageBaseline = loadedBaseline.baseline;
|
|
4581
|
+
const workspace = opts.workspace ?? resolveWorkspace({ configDir, config });
|
|
3742
4582
|
let auditResult;
|
|
3743
|
-
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
4583
|
+
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics || loadedBaseline.legacy) {
|
|
3744
4584
|
auditResult = audit({
|
|
3745
4585
|
registry: resolved.registry,
|
|
3746
4586
|
extracted,
|
|
@@ -3763,7 +4603,9 @@ function runOne(dc, opts) {
|
|
|
3763
4603
|
locsOnDisk,
|
|
3764
4604
|
locsOutputPath: locsRel,
|
|
3765
4605
|
capturedStates,
|
|
3766
|
-
|
|
4606
|
+
coverageBaseline,
|
|
4607
|
+
baselineDiagnostic: loadedBaseline.legacy,
|
|
4608
|
+
workspace
|
|
3767
4609
|
});
|
|
3768
4610
|
}
|
|
3769
4611
|
return {
|
|
@@ -3784,15 +4626,15 @@ function runOne(dc, opts) {
|
|
|
3784
4626
|
}
|
|
3785
4627
|
function readOrNull(p2) {
|
|
3786
4628
|
try {
|
|
3787
|
-
return
|
|
4629
|
+
return fs5.readFileSync(p2, "utf8");
|
|
3788
4630
|
} catch {
|
|
3789
4631
|
return null;
|
|
3790
4632
|
}
|
|
3791
4633
|
}
|
|
3792
4634
|
function writeArtifact(artifact) {
|
|
3793
4635
|
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
3794
|
-
|
|
3795
|
-
|
|
4636
|
+
fs5.mkdirSync(path8.dirname(artifact.outputPath), { recursive: true });
|
|
4637
|
+
fs5.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
3796
4638
|
return true;
|
|
3797
4639
|
}
|
|
3798
4640
|
function writeScanResult(result) {
|
|
@@ -3801,14 +4643,45 @@ function writeScanResult(result) {
|
|
|
3801
4643
|
const wroteLocs = writeArtifact(result.locs);
|
|
3802
4644
|
return wroteTypes || wroteData || wroteLocs;
|
|
3803
4645
|
}
|
|
3804
|
-
function
|
|
4646
|
+
function coverageBaselinePath(result) {
|
|
3805
4647
|
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3806
|
-
return
|
|
4648
|
+
return path8.resolve(result.configDir, rel);
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// src/scanner/scan/states-merge.ts
|
|
4652
|
+
function keyOf(c) {
|
|
4653
|
+
return `${c.kind ?? "page"} ${c.entity} ${c.state}`;
|
|
4654
|
+
}
|
|
4655
|
+
function mergeStates(committed, partial) {
|
|
4656
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4657
|
+
for (const c of committed.captured ?? []) byKey.set(keyOf(c), c);
|
|
4658
|
+
const updated = [];
|
|
4659
|
+
const added = [];
|
|
4660
|
+
for (const c of partial.captured ?? []) {
|
|
4661
|
+
const k = keyOf(c);
|
|
4662
|
+
(byKey.has(k) ? updated : added).push(k);
|
|
4663
|
+
byKey.set(k, c);
|
|
4664
|
+
}
|
|
4665
|
+
const captured = [...byKey.values()].sort(
|
|
4666
|
+
(a, b) => a.entity.localeCompare(b.entity) || a.state.localeCompare(b.state)
|
|
4667
|
+
);
|
|
4668
|
+
return {
|
|
4669
|
+
manifest: {
|
|
4670
|
+
...committed,
|
|
4671
|
+
captured,
|
|
4672
|
+
// The merged file is only as fresh as its OLDEST evidence: rows carried
|
|
4673
|
+
// over from the committed manifest were not re-verified by this run, so
|
|
4674
|
+
// claiming the partial's newer stamp would defeat the staleness check.
|
|
4675
|
+
...committed.generatedAt ? { generatedAt: committed.generatedAt } : {}
|
|
4676
|
+
},
|
|
4677
|
+
updated: updated.sort(),
|
|
4678
|
+
added: added.sort()
|
|
4679
|
+
};
|
|
3807
4680
|
}
|
|
3808
4681
|
|
|
3809
4682
|
// src/scanner/scan/fix.ts
|
|
3810
|
-
import * as
|
|
3811
|
-
import * as
|
|
4683
|
+
import * as fs6 from "fs";
|
|
4684
|
+
import * as path9 from "path";
|
|
3812
4685
|
function applyFixes(diagnostics) {
|
|
3813
4686
|
const entries = [];
|
|
3814
4687
|
for (const d of diagnostics) {
|
|
@@ -3841,7 +4714,7 @@ function applyFixes(diagnostics) {
|
|
|
3841
4714
|
for (const [filePath, edits] of editsByFile) {
|
|
3842
4715
|
let content;
|
|
3843
4716
|
try {
|
|
3844
|
-
content =
|
|
4717
|
+
content = fs6.readFileSync(filePath, "utf8");
|
|
3845
4718
|
} catch {
|
|
3846
4719
|
for (const e of edits) e.entry.skippedReason ??= "file is unreadable";
|
|
3847
4720
|
continue;
|
|
@@ -3861,22 +4734,22 @@ function applyFixes(diagnostics) {
|
|
|
3861
4734
|
const edit = kept[i];
|
|
3862
4735
|
content = content.slice(0, edit.start) + edit.replacement + content.slice(edit.end);
|
|
3863
4736
|
}
|
|
3864
|
-
if (kept.length > 0)
|
|
4737
|
+
if (kept.length > 0) fs6.writeFileSync(filePath, content, "utf8");
|
|
3865
4738
|
}
|
|
3866
4739
|
for (const entry of entries) {
|
|
3867
4740
|
if (entry.skippedReason) continue;
|
|
3868
4741
|
for (const create of entry.createFiles) {
|
|
3869
|
-
if (
|
|
3870
|
-
entry.skippedReason = `${
|
|
4742
|
+
if (fs6.existsSync(create.path)) {
|
|
4743
|
+
entry.skippedReason = `${path9.basename(create.path)} already exists`;
|
|
3871
4744
|
continue;
|
|
3872
4745
|
}
|
|
3873
|
-
|
|
3874
|
-
|
|
4746
|
+
fs6.mkdirSync(path9.dirname(create.path), { recursive: true });
|
|
4747
|
+
fs6.writeFileSync(create.path, create.content, "utf8");
|
|
3875
4748
|
}
|
|
3876
4749
|
if (entry.skippedReason) continue;
|
|
3877
4750
|
for (const del of entry.deleteFiles) {
|
|
3878
4751
|
try {
|
|
3879
|
-
|
|
4752
|
+
fs6.unlinkSync(del);
|
|
3880
4753
|
} catch {
|
|
3881
4754
|
}
|
|
3882
4755
|
}
|
|
@@ -4054,29 +4927,29 @@ function renameEntity(opts) {
|
|
|
4054
4927
|
}
|
|
4055
4928
|
|
|
4056
4929
|
// src/scanner/scan/cli.ts
|
|
4057
|
-
import * as
|
|
4058
|
-
import * as
|
|
4930
|
+
import * as fs9 from "fs";
|
|
4931
|
+
import * as path12 from "path";
|
|
4059
4932
|
|
|
4060
4933
|
// src/scanner/scan/ai/index.ts
|
|
4061
4934
|
import * as p from "@clack/prompts";
|
|
4062
4935
|
|
|
4063
4936
|
// src/scanner/scan/ai/providers/claude.ts
|
|
4064
|
-
import * as
|
|
4065
|
-
import * as
|
|
4937
|
+
import * as fs8 from "fs";
|
|
4938
|
+
import * as path11 from "path";
|
|
4066
4939
|
|
|
4067
4940
|
// src/scanner/scan/ai/templates.ts
|
|
4068
|
-
import * as
|
|
4069
|
-
import * as
|
|
4941
|
+
import * as fs7 from "fs";
|
|
4942
|
+
import * as path10 from "path";
|
|
4070
4943
|
function templatePath(rel) {
|
|
4071
4944
|
const candidates = [
|
|
4072
|
-
|
|
4945
|
+
path10.resolve(__dirname, "../../templates", rel),
|
|
4073
4946
|
// dist/cli/cli.cjs → ../../templates
|
|
4074
|
-
|
|
4947
|
+
path10.resolve(__dirname, "../../../../templates", rel)
|
|
4075
4948
|
// src/scanner/scan/ai → ../../../../templates
|
|
4076
4949
|
];
|
|
4077
4950
|
for (const c of candidates) {
|
|
4078
4951
|
try {
|
|
4079
|
-
|
|
4952
|
+
fs7.accessSync(c, fs7.constants.R_OK);
|
|
4080
4953
|
return c;
|
|
4081
4954
|
} catch {
|
|
4082
4955
|
continue;
|
|
@@ -4088,7 +4961,7 @@ function templatePath(rel) {
|
|
|
4088
4961
|
);
|
|
4089
4962
|
}
|
|
4090
4963
|
function readTemplate(rel) {
|
|
4091
|
-
return
|
|
4964
|
+
return fs7.readFileSync(templatePath(rel), "utf8");
|
|
4092
4965
|
}
|
|
4093
4966
|
|
|
4094
4967
|
// src/scanner/scan/ai/providers/claude.ts
|
|
@@ -4119,8 +4992,8 @@ var claudeProvider = {
|
|
|
4119
4992
|
async install({ cwd, force }) {
|
|
4120
4993
|
const changes = [];
|
|
4121
4994
|
for (const file of SKILL_FILES) {
|
|
4122
|
-
const dest =
|
|
4123
|
-
const exists =
|
|
4995
|
+
const dest = path11.join(cwd, file.dest);
|
|
4996
|
+
const exists = fs8.existsSync(dest);
|
|
4124
4997
|
if (exists && !force) {
|
|
4125
4998
|
changes.push({
|
|
4126
4999
|
path: file.dest,
|
|
@@ -4129,56 +5002,56 @@ var claudeProvider = {
|
|
|
4129
5002
|
});
|
|
4130
5003
|
continue;
|
|
4131
5004
|
}
|
|
4132
|
-
|
|
4133
|
-
|
|
5005
|
+
fs8.mkdirSync(path11.dirname(dest), { recursive: true });
|
|
5006
|
+
fs8.writeFileSync(dest, readTemplate(file.template));
|
|
4134
5007
|
changes.push({
|
|
4135
5008
|
path: file.dest,
|
|
4136
5009
|
action: exists ? "overwritten" : "created"
|
|
4137
5010
|
});
|
|
4138
5011
|
}
|
|
4139
5012
|
for (const rel of LEGACY_FILES) {
|
|
4140
|
-
const dest =
|
|
4141
|
-
if (
|
|
4142
|
-
|
|
5013
|
+
const dest = path11.join(cwd, rel);
|
|
5014
|
+
if (fs8.existsSync(dest)) {
|
|
5015
|
+
fs8.unlinkSync(dest);
|
|
4143
5016
|
changes.push({ path: rel, action: "removed" });
|
|
4144
5017
|
}
|
|
4145
5018
|
}
|
|
4146
|
-
cleanupEmpty(
|
|
4147
|
-
cleanupEmpty(
|
|
4148
|
-
cleanupEmpty(
|
|
5019
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5020
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5021
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4149
5022
|
return { changes };
|
|
4150
5023
|
},
|
|
4151
5024
|
async uninstall({ cwd }) {
|
|
4152
5025
|
const changes = [];
|
|
4153
5026
|
for (const file of SKILL_FILES) {
|
|
4154
|
-
const dest =
|
|
4155
|
-
if (!
|
|
5027
|
+
const dest = path11.join(cwd, file.dest);
|
|
5028
|
+
if (!fs8.existsSync(dest)) {
|
|
4156
5029
|
changes.push({ path: file.dest, action: "skipped", reason: "absent" });
|
|
4157
5030
|
continue;
|
|
4158
5031
|
}
|
|
4159
|
-
|
|
5032
|
+
fs8.unlinkSync(dest);
|
|
4160
5033
|
changes.push({ path: file.dest, action: "removed" });
|
|
4161
5034
|
}
|
|
4162
|
-
cleanupEmpty(
|
|
4163
|
-
cleanupEmpty(
|
|
4164
|
-
cleanupEmpty(
|
|
5035
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills/uidex/references"));
|
|
5036
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills/uidex"));
|
|
5037
|
+
cleanupEmpty(path11.join(cwd, ".claude/skills"));
|
|
4165
5038
|
for (const rel of LEGACY_FILES) {
|
|
4166
|
-
const dest =
|
|
4167
|
-
if (
|
|
4168
|
-
|
|
5039
|
+
const dest = path11.join(cwd, rel);
|
|
5040
|
+
if (fs8.existsSync(dest)) {
|
|
5041
|
+
fs8.unlinkSync(dest);
|
|
4169
5042
|
changes.push({ path: rel, action: "removed" });
|
|
4170
5043
|
}
|
|
4171
5044
|
}
|
|
4172
|
-
cleanupEmpty(
|
|
4173
|
-
cleanupEmpty(
|
|
4174
|
-
cleanupEmpty(
|
|
5045
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands/uidex"));
|
|
5046
|
+
cleanupEmpty(path11.join(cwd, ".claude/commands"));
|
|
5047
|
+
cleanupEmpty(path11.join(cwd, ".claude/rules"));
|
|
4175
5048
|
return { changes };
|
|
4176
5049
|
}
|
|
4177
5050
|
};
|
|
4178
5051
|
function cleanupEmpty(dir) {
|
|
4179
5052
|
try {
|
|
4180
|
-
const entries =
|
|
4181
|
-
if (entries.length === 0)
|
|
5053
|
+
const entries = fs8.readdirSync(dir);
|
|
5054
|
+
if (entries.length === 0) fs8.rmdirSync(dir);
|
|
4182
5055
|
} catch {
|
|
4183
5056
|
}
|
|
4184
5057
|
}
|
|
@@ -4347,6 +5220,8 @@ async function run(opts) {
|
|
|
4347
5220
|
return runScaffold(cwd, positional.slice(1), flags, writer);
|
|
4348
5221
|
case "rename":
|
|
4349
5222
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
5223
|
+
case "states":
|
|
5224
|
+
return runStates(cwd, positional.slice(1), writer);
|
|
4350
5225
|
case "ai": {
|
|
4351
5226
|
const result = await runAiCommand({
|
|
4352
5227
|
cwd,
|
|
@@ -4375,22 +5250,23 @@ function helpText2() {
|
|
|
4375
5250
|
" scan [flags] Run the scanner pipeline",
|
|
4376
5251
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
4377
5252
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
5253
|
+
" states merge [partial] Fold a filtered run's *.partial.json into the committed manifest",
|
|
4378
5254
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
4379
5255
|
"",
|
|
4380
5256
|
"Flags:",
|
|
4381
5257
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
4382
5258
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
4383
5259
|
" --audit Equivalent to --check --lint (read-only)",
|
|
4384
|
-
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4385
|
-
" --update-baseline Regenerate the
|
|
5260
|
+
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names, migrate waivers/capture to acknowledge), then rescan and write",
|
|
5261
|
+
" --update-baseline Regenerate the coverage baseline (uidex-coverage-baseline.json) as `acknowledge` backlog entries",
|
|
4386
5262
|
" --json Emit JSON diagnostics on stdout",
|
|
4387
5263
|
" --force (scaffold) overwrite existing spec",
|
|
4388
5264
|
""
|
|
4389
5265
|
].join("\n");
|
|
4390
5266
|
}
|
|
4391
5267
|
function runInit(cwd, w) {
|
|
4392
|
-
const configPath =
|
|
4393
|
-
if (
|
|
5268
|
+
const configPath = path12.join(cwd, CONFIG_FILENAME);
|
|
5269
|
+
if (fs9.existsSync(configPath)) {
|
|
4394
5270
|
w.err(`.uidex.json already exists at ${configPath}`);
|
|
4395
5271
|
return w.result(1);
|
|
4396
5272
|
}
|
|
@@ -4399,16 +5275,16 @@ function runInit(cwd, w) {
|
|
|
4399
5275
|
sources: [{ rootDir: "src" }],
|
|
4400
5276
|
output: "src/uidex.gen.ts"
|
|
4401
5277
|
};
|
|
4402
|
-
|
|
5278
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
4403
5279
|
w.out(`Created ${configPath}`);
|
|
4404
|
-
const gitignorePath =
|
|
5280
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
4405
5281
|
const entry = "*.gen.*";
|
|
4406
|
-
if (
|
|
4407
|
-
const existing =
|
|
5282
|
+
if (fs9.existsSync(gitignorePath)) {
|
|
5283
|
+
const existing = fs9.readFileSync(gitignorePath, "utf8");
|
|
4408
5284
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
4409
5285
|
if (!hasEntry) {
|
|
4410
5286
|
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
4411
|
-
|
|
5287
|
+
fs9.appendFileSync(
|
|
4412
5288
|
gitignorePath,
|
|
4413
5289
|
`${needsNewline ? "\n" : ""}${entry}
|
|
4414
5290
|
`,
|
|
@@ -4417,7 +5293,7 @@ function runInit(cwd, w) {
|
|
|
4417
5293
|
w.out(`Appended ${entry} to ${gitignorePath}`);
|
|
4418
5294
|
}
|
|
4419
5295
|
} else {
|
|
4420
|
-
|
|
5296
|
+
fs9.writeFileSync(gitignorePath, `${entry}
|
|
4421
5297
|
`, "utf8");
|
|
4422
5298
|
w.out(`Created ${gitignorePath} with ${entry}`);
|
|
4423
5299
|
}
|
|
@@ -4456,20 +5332,25 @@ function runScanCommand(cwd, flags, w) {
|
|
|
4456
5332
|
if (updateBaseline) {
|
|
4457
5333
|
for (const r of results) {
|
|
4458
5334
|
const manifest = r.capturedStates ?? { captured: [] };
|
|
4459
|
-
const
|
|
4460
|
-
const
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
5335
|
+
const acknowledge = {};
|
|
5336
|
+
const merge = (from) => {
|
|
5337
|
+
for (const [route, scopes] of Object.entries(from)) {
|
|
5338
|
+
acknowledge[route] = { ...acknowledge[route] ?? {}, ...scopes };
|
|
5339
|
+
}
|
|
4464
5340
|
};
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
const
|
|
4468
|
-
|
|
5341
|
+
merge(computePageBacklog(r.registry, manifest));
|
|
5342
|
+
merge(computeMatrixBacklog(r.registry, manifest));
|
|
5343
|
+
const baseline = { version: 2, acknowledge };
|
|
5344
|
+
const p2 = coverageBaselinePath(r);
|
|
5345
|
+
fs9.writeFileSync(p2, serializeCoverageBaseline(baseline));
|
|
5346
|
+
const entries = Object.values(acknowledge);
|
|
5347
|
+
const pages = entries.filter((s) => s["*"]).length;
|
|
5348
|
+
const kindGaps = entries.reduce(
|
|
5349
|
+
(n, s) => n + Object.keys(s).filter((k) => k !== "*").length,
|
|
4469
5350
|
0
|
|
4470
5351
|
);
|
|
4471
5352
|
w.out(
|
|
4472
|
-
`Wrote ${p2} \u2014 ${
|
|
5353
|
+
`Wrote ${p2} \u2014 ${pages} uncaptured page(s), ${kindGaps} missing core-kind(s) acknowledged as backlog`
|
|
4473
5354
|
);
|
|
4474
5355
|
}
|
|
4475
5356
|
return w.result(0);
|
|
@@ -4542,7 +5423,7 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4542
5423
|
for (const r of results) {
|
|
4543
5424
|
const entity = r.registry.get(scaffoldKind, id);
|
|
4544
5425
|
if (!entity) continue;
|
|
4545
|
-
const outDir =
|
|
5426
|
+
const outDir = path12.resolve(r.configDir, "e2e");
|
|
4546
5427
|
const result = scaffoldSpec({
|
|
4547
5428
|
registry: r.registry,
|
|
4548
5429
|
kind: scaffoldKind,
|
|
@@ -4563,6 +5444,53 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
4563
5444
|
return w.result(1);
|
|
4564
5445
|
}
|
|
4565
5446
|
var RENAME_KINDS = /* @__PURE__ */ new Set(["element", "widget", "region"]);
|
|
5447
|
+
function runStates(cwd, args, w) {
|
|
5448
|
+
const [sub, explicitPartial] = args;
|
|
5449
|
+
if (sub !== "merge") {
|
|
5450
|
+
w.err("Usage: uidex states merge [partial-manifest]");
|
|
5451
|
+
return w.result(1);
|
|
5452
|
+
}
|
|
5453
|
+
const configs = discover({ cwd });
|
|
5454
|
+
if (configs.length === 0) {
|
|
5455
|
+
w.err(`No ${CONFIG_FILENAME} found under ${cwd}`);
|
|
5456
|
+
return w.result(1);
|
|
5457
|
+
}
|
|
5458
|
+
let merged = 0;
|
|
5459
|
+
for (const { configDir, config } of configs) {
|
|
5460
|
+
const rel = config.statesManifest ?? "uidex-states.json";
|
|
5461
|
+
const target = path12.resolve(configDir, rel);
|
|
5462
|
+
const partial = explicitPartial ? path12.resolve(cwd, explicitPartial) : target.replace(/\.json$/i, "") + ".partial.json";
|
|
5463
|
+
const committedRaw = readJson(target);
|
|
5464
|
+
const partialRaw = readJson(partial);
|
|
5465
|
+
if (!partialRaw) continue;
|
|
5466
|
+
if (!committedRaw) {
|
|
5467
|
+
w.err(`No committed manifest at ${rel}; nothing to merge into`);
|
|
5468
|
+
return w.result(1);
|
|
5469
|
+
}
|
|
5470
|
+
const result = mergeStates(committedRaw, partialRaw);
|
|
5471
|
+
fs9.writeFileSync(target, JSON.stringify(result.manifest, null, 2) + "\n");
|
|
5472
|
+
merged++;
|
|
5473
|
+
w.out(
|
|
5474
|
+
`Merged ${path12.basename(partial)} \u2192 ${rel}: ${result.added.length} added, ${result.updated.length} updated, ${result.manifest.captured.length} total`
|
|
5475
|
+
);
|
|
5476
|
+
for (const k of result.added) w.out(` + ${k}`);
|
|
5477
|
+
for (const k of result.updated) w.out(` ~ ${k}`);
|
|
5478
|
+
}
|
|
5479
|
+
if (merged === 0) {
|
|
5480
|
+
w.out("No partial manifest found; nothing to merge.");
|
|
5481
|
+
}
|
|
5482
|
+
return w.result(0);
|
|
5483
|
+
}
|
|
5484
|
+
function readJson(p2) {
|
|
5485
|
+
try {
|
|
5486
|
+
const parsed = JSON.parse(fs9.readFileSync(p2, "utf8"));
|
|
5487
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
5488
|
+
const m = parsed;
|
|
5489
|
+
return Array.isArray(m.captured) ? m : null;
|
|
5490
|
+
} catch {
|
|
5491
|
+
return null;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
4566
5494
|
function runRename(cwd, args, flags, w) {
|
|
4567
5495
|
const [kind, oldId, newId] = args;
|
|
4568
5496
|
if (!kind || !RENAME_KINDS.has(kind) || !oldId || !newId) {
|
|
@@ -4610,35 +5538,51 @@ function createWriter() {
|
|
|
4610
5538
|
};
|
|
4611
5539
|
}
|
|
4612
5540
|
export {
|
|
5541
|
+
ACK_ALL,
|
|
5542
|
+
ACK_SCOPES,
|
|
4613
5543
|
CONFIG_FILENAME,
|
|
4614
5544
|
ConfigError,
|
|
4615
5545
|
DEFAULT_CONVENTIONS,
|
|
5546
|
+
DIAGNOSTIC_CODES,
|
|
4616
5547
|
applyFixes,
|
|
4617
5548
|
audit,
|
|
5549
|
+
capturedElsewhere,
|
|
5550
|
+
checkComponentCoverage,
|
|
4618
5551
|
checkPageCoverage,
|
|
4619
5552
|
checkStateCoverage,
|
|
4620
5553
|
checkStateMatrix,
|
|
4621
5554
|
compileRoute,
|
|
4622
|
-
|
|
4623
|
-
|
|
5555
|
+
computeMatrixBacklog,
|
|
5556
|
+
computePageBacklog,
|
|
5557
|
+
coverageBaselinePath,
|
|
4624
5558
|
detectRoutes,
|
|
4625
5559
|
discover,
|
|
4626
5560
|
emit,
|
|
5561
|
+
entityAcknowledge,
|
|
4627
5562
|
extract,
|
|
4628
5563
|
extractUidexExports,
|
|
5564
|
+
findWorkspaceRoot,
|
|
4629
5565
|
globToRegExp,
|
|
4630
|
-
|
|
5566
|
+
isLegacyBaseline,
|
|
5567
|
+
loadCoverageBaseline,
|
|
4631
5568
|
matchUrlToRoute,
|
|
4632
|
-
|
|
5569
|
+
mergeStates,
|
|
5570
|
+
migrateBaseline,
|
|
5571
|
+
migrateLegacyBaseline,
|
|
5572
|
+
migrateLegacyFields,
|
|
4633
5573
|
parseConfig,
|
|
5574
|
+
parseCoverageBaseline,
|
|
4634
5575
|
pathToId,
|
|
4635
5576
|
renameEntity,
|
|
4636
5577
|
resolve2 as resolve,
|
|
4637
5578
|
resolveGitContext,
|
|
5579
|
+
resolveWorkspace,
|
|
5580
|
+
routeAcknowledge,
|
|
4638
5581
|
run as runCli,
|
|
4639
5582
|
runScan,
|
|
4640
5583
|
scaffoldSpec,
|
|
4641
5584
|
scaffoldWidgetSpec,
|
|
5585
|
+
serializeCoverageBaseline,
|
|
4642
5586
|
validateConfig,
|
|
4643
5587
|
walk,
|
|
4644
5588
|
writeScanResult
|