@decantr/cli 3.0.2 → 3.4.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/bin.js +4 -4
- package/dist/{chunk-B2PJDAMS.js → chunk-EL4KSW7E.js} +1 -1
- package/dist/{chunk-7Z74ETDR.js → chunk-XINHWP4T.js} +141 -15
- package/dist/{chunk-4NDOHCYY.js → chunk-XUHPMETF.js} +547 -55
- package/dist/{chunk-GN63PIKK.js → chunk-ZBGL7RID.js} +44 -13
- package/dist/{heal-HHVVTVIH.js → heal-ZY4BX5HU.js} +1 -1
- package/dist/{health-DDT4RABT.js → health-H2256NIM.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{studio-WDJ3BOCU.js → studio-UKSCSRX7.js} +3 -3
- package/dist/{workspace-3XNCMZ5L.js → workspace-SR6ZGG2E.js} +3 -3
- package/package.json +6 -6
|
@@ -3,14 +3,14 @@ import {
|
|
|
3
3
|
sendProjectHealthCiFailedTelemetry,
|
|
4
4
|
sendProjectHealthPromptTelemetry,
|
|
5
5
|
sendProjectHealthReportTelemetry
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-XINHWP4T.js";
|
|
7
7
|
|
|
8
8
|
// src/commands/health.ts
|
|
9
9
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
10
10
|
import { createHash as createHash2 } from "crypto";
|
|
11
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
11
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync, writeFileSync as writeFileSync4 } from "fs";
|
|
12
12
|
import { createRequire } from "module";
|
|
13
|
-
import { dirname as dirname3, isAbsolute as
|
|
13
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, relative as relative3, resolve as resolve3 } from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
import {
|
|
16
16
|
anchorFindingsToGraph,
|
|
@@ -114,8 +114,8 @@ function resolveWorkspaceInfo(cwd, projectArg) {
|
|
|
114
114
|
|
|
115
115
|
// src/commands/graph.ts
|
|
116
116
|
import { createHash } from "crypto";
|
|
117
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
118
|
-
import { dirname as dirname2, isAbsolute as
|
|
117
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
118
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2 } from "path";
|
|
119
119
|
import {
|
|
120
120
|
buildContractCapsuleFromSnapshot,
|
|
121
121
|
buildGraphImpactContext,
|
|
@@ -138,7 +138,7 @@ import {
|
|
|
138
138
|
// src/local-law.ts
|
|
139
139
|
import { execFileSync } from "child_process";
|
|
140
140
|
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
|
|
141
|
-
import { basename, extname, join as join2, relative, sep } from "path";
|
|
141
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2, sep } from "path";
|
|
142
142
|
import { isV4 } from "@decantr/essence-spec";
|
|
143
143
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
144
144
|
".astro",
|
|
@@ -827,12 +827,13 @@ function createLocalLawTaskSummary(projectRoot) {
|
|
|
827
827
|
function changedFiles(projectRoot, since) {
|
|
828
828
|
const changed = /* @__PURE__ */ new Set();
|
|
829
829
|
try {
|
|
830
|
+
const gitRoot = gitTopLevel(projectRoot) ?? projectRoot;
|
|
830
831
|
const commands = since ? [
|
|
831
|
-
["diff", "--name-only", since, "--"],
|
|
832
|
-
["diff", "--name-only", "--cached"]
|
|
832
|
+
["diff", "--name-only", "--relative", since, "--"],
|
|
833
|
+
["diff", "--name-only", "--relative", "--cached"]
|
|
833
834
|
] : [
|
|
834
|
-
["diff", "--name-only"],
|
|
835
|
-
["diff", "--name-only", "--cached"]
|
|
835
|
+
["diff", "--name-only", "--relative"],
|
|
836
|
+
["diff", "--name-only", "--relative", "--cached"]
|
|
836
837
|
];
|
|
837
838
|
for (const args of commands) {
|
|
838
839
|
const output = execFileSync("git", args, {
|
|
@@ -842,13 +843,38 @@ function changedFiles(projectRoot, since) {
|
|
|
842
843
|
});
|
|
843
844
|
for (const line of output.split(/\r?\n/)) {
|
|
844
845
|
const file = line.trim();
|
|
845
|
-
|
|
846
|
+
const projectFile = changedFileForProject(projectRoot, gitRoot, file);
|
|
847
|
+
if (projectFile) changed.add(projectFile);
|
|
846
848
|
}
|
|
847
849
|
}
|
|
848
850
|
} catch {
|
|
849
851
|
}
|
|
850
852
|
return [...changed].sort();
|
|
851
853
|
}
|
|
854
|
+
function gitTopLevel(projectRoot) {
|
|
855
|
+
try {
|
|
856
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
857
|
+
cwd: projectRoot,
|
|
858
|
+
encoding: "utf-8",
|
|
859
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
860
|
+
}).trim();
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
function changedFileForProject(projectRoot, gitRoot, file) {
|
|
866
|
+
if (!file) return null;
|
|
867
|
+
const absoluteProjectRoot = resolve2(projectRoot);
|
|
868
|
+
const candidateAbsoluteFiles = isAbsolute2(file) ? [file] : [join2(absoluteProjectRoot, file), join2(gitRoot, file)];
|
|
869
|
+
for (const absoluteFile of candidateAbsoluteFiles) {
|
|
870
|
+
const projectRelative = normalizePath(relative(absoluteProjectRoot, absoluteFile));
|
|
871
|
+
if (!projectRelative || projectRelative.startsWith("../") || projectRelative === "..") {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
return projectRelative;
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
852
878
|
function routeImpacts(projectRoot, files) {
|
|
853
879
|
const analysis = readJsonFile(
|
|
854
880
|
join2(projectRoot, ".decantr", "analysis.json")
|
|
@@ -1235,6 +1261,19 @@ function colorTokenNames(styling) {
|
|
|
1235
1261
|
}
|
|
1236
1262
|
return [...names].slice(0, 40);
|
|
1237
1263
|
}
|
|
1264
|
+
function firstCssVariable(styling, terms) {
|
|
1265
|
+
return styling.cssVariables.find((name) => terms.test(name)) ?? null;
|
|
1266
|
+
}
|
|
1267
|
+
function firstClassHint(projectRoot, ids) {
|
|
1268
|
+
return classHintsForPattern(projectRoot, ids)[0] ?? null;
|
|
1269
|
+
}
|
|
1270
|
+
function nativeRefForMapping(input) {
|
|
1271
|
+
const cssVariable = firstCssVariable(input.styling, input.tokenTerms);
|
|
1272
|
+
if (cssVariable) return { kind: "css-var", ref: cssVariable };
|
|
1273
|
+
const classHint = firstClassHint(input.projectRoot, input.patternIds);
|
|
1274
|
+
if (classHint) return { kind: "class", ref: classHint };
|
|
1275
|
+
return { kind: "class", ref: input.fallbackClass };
|
|
1276
|
+
}
|
|
1238
1277
|
function createStyleBridgeProposal(input) {
|
|
1239
1278
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1240
1279
|
const theme = readThemeInventory(input.projectRoot);
|
|
@@ -1246,7 +1285,7 @@ function createStyleBridgeProposal(input) {
|
|
|
1246
1285
|
const themeModes = theme.modes.length > 0 ? theme.modes : darkModeDetected ? ["base", "dark"] : ["base"];
|
|
1247
1286
|
const themeVariantIds = theme.variantIds.length > 0 ? theme.variantIds : themeModes.filter((mode) => mode !== "base");
|
|
1248
1287
|
return {
|
|
1249
|
-
version:
|
|
1288
|
+
version: 2,
|
|
1250
1289
|
status: "proposal",
|
|
1251
1290
|
generatedAt,
|
|
1252
1291
|
source: "decantr codify --style-bridge",
|
|
@@ -1286,6 +1325,17 @@ function createStyleBridgeProposal(input) {
|
|
|
1286
1325
|
label: "Surfaces and cards",
|
|
1287
1326
|
decantrIntent: "surface background, card treatment, border, radius, depth, and hover state",
|
|
1288
1327
|
projectAuthority: "Use the app card/surface primitives, tokens, and accepted local law.",
|
|
1328
|
+
native: nativeRefForMapping({
|
|
1329
|
+
projectRoot: input.projectRoot,
|
|
1330
|
+
styling: input.styling,
|
|
1331
|
+
patternIds: ["surface-card"],
|
|
1332
|
+
tokenTerms: /surface|card|panel|bg|background|border|shadow|radius/i,
|
|
1333
|
+
fallbackClass: "surface-card"
|
|
1334
|
+
}),
|
|
1335
|
+
essence: { kind: "treatment", ref: "surface" },
|
|
1336
|
+
confidence: 0.7,
|
|
1337
|
+
source: "inferred",
|
|
1338
|
+
property: "surface",
|
|
1289
1339
|
tokenHints: tokenHints(
|
|
1290
1340
|
input.styling,
|
|
1291
1341
|
/surface|card|panel|bg|background|border|shadow|radius/i
|
|
@@ -1302,6 +1352,17 @@ function createStyleBridgeProposal(input) {
|
|
|
1302
1352
|
label: "Actions and buttons",
|
|
1303
1353
|
decantrIntent: "primary, secondary, tertiary, destructive, icon-only, loading, and disabled action states",
|
|
1304
1354
|
projectAuthority: "Use the app button/action primitives and local variant names.",
|
|
1355
|
+
native: nativeRefForMapping({
|
|
1356
|
+
projectRoot: input.projectRoot,
|
|
1357
|
+
styling: input.styling,
|
|
1358
|
+
patternIds: ["button"],
|
|
1359
|
+
tokenTerms: /primary|secondary|accent|danger|error|destructive|focus/i,
|
|
1360
|
+
fallbackClass: "button"
|
|
1361
|
+
}),
|
|
1362
|
+
essence: { kind: "treatment", ref: "action" },
|
|
1363
|
+
confidence: 0.72,
|
|
1364
|
+
source: "inferred",
|
|
1365
|
+
property: "color",
|
|
1305
1366
|
tokenHints: tokenHints(
|
|
1306
1367
|
input.styling,
|
|
1307
1368
|
/primary|secondary|accent|danger|error|destructive|focus/i
|
|
@@ -1318,6 +1379,17 @@ function createStyleBridgeProposal(input) {
|
|
|
1318
1379
|
label: "Focus and accessibility",
|
|
1319
1380
|
decantrIntent: "visible focus, keyboard clarity, contrast, and reduced-motion safety",
|
|
1320
1381
|
projectAuthority: "Use the app accessibility classes, focus tokens, and framework conventions.",
|
|
1382
|
+
native: nativeRefForMapping({
|
|
1383
|
+
projectRoot: input.projectRoot,
|
|
1384
|
+
styling: input.styling,
|
|
1385
|
+
patternIds: [],
|
|
1386
|
+
tokenTerms: /focus|ring|outline|contrast|motion|duration/i,
|
|
1387
|
+
fallbackClass: "focus-visible"
|
|
1388
|
+
}),
|
|
1389
|
+
essence: { kind: "behavior", ref: "focus-accessibility" },
|
|
1390
|
+
confidence: 0.66,
|
|
1391
|
+
source: "inferred",
|
|
1392
|
+
property: "focus",
|
|
1321
1393
|
tokenHints: tokenHints(input.styling, /focus|ring|outline|contrast|motion|duration/i),
|
|
1322
1394
|
classHints: [],
|
|
1323
1395
|
sourceEvidence: [],
|
|
@@ -1331,6 +1403,17 @@ function createStyleBridgeProposal(input) {
|
|
|
1331
1403
|
label: "Layout density and spacing",
|
|
1332
1404
|
decantrIntent: "route gutters, section gaps, component padding, density, and responsive rhythm",
|
|
1333
1405
|
projectAuthority: "Use existing layout wrappers, shell primitives, and spacing tokens/classes.",
|
|
1406
|
+
native: nativeRefForMapping({
|
|
1407
|
+
projectRoot: input.projectRoot,
|
|
1408
|
+
styling: input.styling,
|
|
1409
|
+
patternIds: ["page-shell"],
|
|
1410
|
+
tokenTerms: /space|spacing|gap|gutter|container|radius/i,
|
|
1411
|
+
fallbackClass: "page-shell"
|
|
1412
|
+
}),
|
|
1413
|
+
essence: { kind: "layout", ref: "density-spacing" },
|
|
1414
|
+
confidence: 0.68,
|
|
1415
|
+
source: "inferred",
|
|
1416
|
+
property: "spacing",
|
|
1334
1417
|
tokenHints: tokenHints(input.styling, /space|spacing|gap|gutter|container|radius/i),
|
|
1335
1418
|
classHints: classHintsForPattern(input.projectRoot, ["page-shell"]),
|
|
1336
1419
|
sourceEvidence: sourceEvidence(input.projectRoot, ["page-shell"]),
|
|
@@ -1344,6 +1427,17 @@ function createStyleBridgeProposal(input) {
|
|
|
1344
1427
|
label: "Theme variants",
|
|
1345
1428
|
decantrIntent: "light, dark, brand, tenant, seasonal, and density variants",
|
|
1346
1429
|
projectAuthority: "Use the app theme provider, data attributes, CSS variables, or Tailwind mode strategy.",
|
|
1430
|
+
native: nativeRefForMapping({
|
|
1431
|
+
projectRoot: input.projectRoot,
|
|
1432
|
+
styling: input.styling,
|
|
1433
|
+
patternIds: ["theme-variant"],
|
|
1434
|
+
tokenTerms: /theme|dark|light|brand|tenant|mode|color/i,
|
|
1435
|
+
fallbackClass: "theme-variant"
|
|
1436
|
+
}),
|
|
1437
|
+
essence: { kind: "token", ref: "theme-variant" },
|
|
1438
|
+
confidence: 0.64,
|
|
1439
|
+
source: "inferred",
|
|
1440
|
+
property: "theme",
|
|
1347
1441
|
tokenHints: tokenHints(input.styling, /theme|dark|light|brand|tenant|mode|color/i),
|
|
1348
1442
|
classHints: classHintsForPattern(input.projectRoot, ["theme-variant"]),
|
|
1349
1443
|
sourceEvidence: sourceEvidence(input.projectRoot, ["theme-variant"]),
|
|
@@ -1427,6 +1521,11 @@ function styleBridgeMatches(projectRoot, query) {
|
|
|
1427
1521
|
mapping.label,
|
|
1428
1522
|
mapping.decantrIntent,
|
|
1429
1523
|
mapping.projectAuthority,
|
|
1524
|
+
mapping.native?.ref,
|
|
1525
|
+
mapping.native?.kind,
|
|
1526
|
+
mapping.essence?.ref,
|
|
1527
|
+
mapping.essence?.kind,
|
|
1528
|
+
mapping.property,
|
|
1430
1529
|
...mapping.tokenHints,
|
|
1431
1530
|
...mapping.classHints,
|
|
1432
1531
|
...mapping.guardrails
|
|
@@ -1647,17 +1746,24 @@ function readGraphSnapshotSelection(artifacts, snapshotId) {
|
|
|
1647
1746
|
}
|
|
1648
1747
|
function projectRelativePath(projectRoot, path) {
|
|
1649
1748
|
if (!path) return null;
|
|
1650
|
-
const absolutePath =
|
|
1749
|
+
const absolutePath = isAbsolute3(path) ? path : join4(projectRoot, path);
|
|
1651
1750
|
const relativePath = relative2(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
1652
|
-
if (!relativePath || relativePath.startsWith("..") ||
|
|
1751
|
+
if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
|
|
1653
1752
|
return null;
|
|
1654
1753
|
}
|
|
1655
1754
|
return relativePath;
|
|
1656
1755
|
}
|
|
1756
|
+
function pathIsFile(path) {
|
|
1757
|
+
try {
|
|
1758
|
+
return statSync2(path).isFile();
|
|
1759
|
+
} catch {
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1657
1763
|
function existingProjectRelativePath(projectRoot, path) {
|
|
1658
1764
|
const relativePath = projectRelativePath(projectRoot, path);
|
|
1659
1765
|
if (!relativePath) return null;
|
|
1660
|
-
return
|
|
1766
|
+
return pathIsFile(join4(projectRoot, relativePath)) ? relativePath : null;
|
|
1661
1767
|
}
|
|
1662
1768
|
function stripJsonComments(value) {
|
|
1663
1769
|
return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
@@ -1708,7 +1814,7 @@ function existingImportCandidate(projectRoot, candidate) {
|
|
|
1708
1814
|
join4(relativeCandidate, "index.cts").replace(/\\/g, "/")
|
|
1709
1815
|
];
|
|
1710
1816
|
for (const possible of candidates) {
|
|
1711
|
-
if (
|
|
1817
|
+
if (pathIsFile(join4(projectRoot, possible))) return possible;
|
|
1712
1818
|
}
|
|
1713
1819
|
return null;
|
|
1714
1820
|
}
|
|
@@ -1841,7 +1947,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
|
|
|
1841
1947
|
const routeFile = projectRelativePath(projectRoot, route.file);
|
|
1842
1948
|
if (!routeFile) continue;
|
|
1843
1949
|
const routeFilePath = join4(projectRoot, routeFile);
|
|
1844
|
-
if (!
|
|
1950
|
+
if (!pathIsFile(routeFilePath)) continue;
|
|
1845
1951
|
sources.push({
|
|
1846
1952
|
id: `src:${routeFile}`,
|
|
1847
1953
|
kind: "route-source",
|
|
@@ -1860,7 +1966,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
|
|
|
1860
1966
|
const componentFile = projectRelativePath(projectRoot, declaration.file);
|
|
1861
1967
|
if (!componentFile) continue;
|
|
1862
1968
|
const componentPath = join4(projectRoot, componentFile);
|
|
1863
|
-
if (!
|
|
1969
|
+
if (!pathIsFile(componentPath)) continue;
|
|
1864
1970
|
if (sources.some((source) => source.id === `src:${componentFile}`)) continue;
|
|
1865
1971
|
sources.push({
|
|
1866
1972
|
id: `src:${componentFile}`,
|
|
@@ -1879,7 +1985,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
|
|
|
1879
1985
|
const importingFile = projectRelativePath(projectRoot, importReference.file);
|
|
1880
1986
|
if (!importingFile) continue;
|
|
1881
1987
|
const importingPath = join4(projectRoot, importingFile);
|
|
1882
|
-
if (
|
|
1988
|
+
if (pathIsFile(importingPath) && !sources.some((source) => source.id === `src:${importingFile}`)) {
|
|
1883
1989
|
sources.push({
|
|
1884
1990
|
id: `src:${importingFile}`,
|
|
1885
1991
|
kind: "code-source",
|
|
@@ -1899,7 +2005,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
|
|
|
1899
2005
|
);
|
|
1900
2006
|
if (!importedFile) continue;
|
|
1901
2007
|
const importedPath = join4(projectRoot, importedFile);
|
|
1902
|
-
if (!
|
|
2008
|
+
if (!pathIsFile(importedPath)) continue;
|
|
1903
2009
|
if (sources.some((source) => source.id === `src:${importedFile}`)) continue;
|
|
1904
2010
|
sources.push({
|
|
1905
2011
|
id: `src:${importedFile}`,
|
|
@@ -1928,7 +2034,7 @@ function sourceArtifacts(projectRoot, componentReuseAudit = null) {
|
|
|
1928
2034
|
for (const route of visualManifest.routes ?? []) {
|
|
1929
2035
|
if (!route.screenshot) continue;
|
|
1930
2036
|
const screenshotPath = join4(projectRoot, route.screenshot);
|
|
1931
|
-
if (!
|
|
2037
|
+
if (!pathIsFile(screenshotPath)) continue;
|
|
1932
2038
|
sources.push({
|
|
1933
2039
|
id: `src:${route.screenshot}`,
|
|
1934
2040
|
kind: "visual-screenshot",
|
|
@@ -2596,7 +2702,7 @@ function augmentProjectGraph(snapshot, projectRoot, sources, componentReuseAudit
|
|
|
2596
2702
|
function pathForDisplay(projectRoot, path, displayRoot) {
|
|
2597
2703
|
if (!displayRoot) return path.replace(`${projectRoot}/`, "");
|
|
2598
2704
|
const relativePath = relative2(displayRoot, path).replace(/\\/g, "/");
|
|
2599
|
-
if (relativePath && !relativePath.startsWith("..") && !
|
|
2705
|
+
if (relativePath && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
|
|
2600
2706
|
return relativePath;
|
|
2601
2707
|
}
|
|
2602
2708
|
return path;
|
|
@@ -2836,7 +2942,7 @@ function graphSourceNodeIdForFile(projectRoot, snapshot, file) {
|
|
|
2836
2942
|
})?.id ?? null;
|
|
2837
2943
|
}
|
|
2838
2944
|
function resolvePathFromCwd(path) {
|
|
2839
|
-
return
|
|
2945
|
+
return isAbsolute3(path) ? path : join4(process.cwd(), path);
|
|
2840
2946
|
}
|
|
2841
2947
|
function impactContextPayload(impactContext, selection) {
|
|
2842
2948
|
if (!selection) return void 0;
|
|
@@ -3110,6 +3216,20 @@ var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
|
|
|
3110
3216
|
var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
|
|
3111
3217
|
var DEFAULT_HEALTH_CI_CLI_VERSION = "latest";
|
|
3112
3218
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
3219
|
+
var HEALTH_BROWSER_RUNTIME_DIAGNOSTICS = [
|
|
3220
|
+
{
|
|
3221
|
+
rule: "browser-runtime-probes-failed",
|
|
3222
|
+
code: "RUNTIME010",
|
|
3223
|
+
repairId: "repair-browser-runtime-probes",
|
|
3224
|
+
family: "RUNTIME"
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
rule: "browser-axe-violations",
|
|
3228
|
+
code: "A11Y020",
|
|
3229
|
+
repairId: "fix-rendered-accessibility",
|
|
3230
|
+
family: "A11Y"
|
|
3231
|
+
}
|
|
3232
|
+
];
|
|
3113
3233
|
function readProjectMetadata(projectRoot) {
|
|
3114
3234
|
const projectJsonPath = join5(projectRoot, ".decantr", "project.json");
|
|
3115
3235
|
if (!existsSync5(projectJsonPath)) {
|
|
@@ -3467,7 +3587,7 @@ function commandsForFinding(source) {
|
|
|
3467
3587
|
case "runtime":
|
|
3468
3588
|
return ["npm run build", "decantr health"];
|
|
3469
3589
|
case "interaction":
|
|
3470
|
-
return ["decantr
|
|
3590
|
+
return ["decantr verify --brownfield --local-patterns", "decantr verify --evidence"];
|
|
3471
3591
|
case "assertion":
|
|
3472
3592
|
return ["decantr refresh", "decantr health --evidence"];
|
|
3473
3593
|
case "browser":
|
|
@@ -3477,7 +3597,7 @@ function commandsForFinding(source) {
|
|
|
3477
3597
|
case "style-bridge":
|
|
3478
3598
|
return ["decantr codify --style-bridge", "decantr verify --evidence"];
|
|
3479
3599
|
case "graph":
|
|
3480
|
-
return ["decantr graph", "decantr
|
|
3600
|
+
return ["decantr graph", "decantr verify --evidence"];
|
|
3481
3601
|
case "check":
|
|
3482
3602
|
return ["decantr check", "decantr health"];
|
|
3483
3603
|
default:
|
|
@@ -3531,7 +3651,7 @@ function commandContextForProject(projectRoot) {
|
|
|
3531
3651
|
/\\/g,
|
|
3532
3652
|
"/"
|
|
3533
3653
|
);
|
|
3534
|
-
const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !
|
|
3654
|
+
const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute4(relativeProjectPath) ? relativeProjectPath : null;
|
|
3535
3655
|
const projectFlag = projectPath ? ` --project ${projectPath}` : "";
|
|
3536
3656
|
const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
|
|
3537
3657
|
return {
|
|
@@ -3566,6 +3686,10 @@ function rewriteHealthCommand(command, context) {
|
|
|
3566
3686
|
/^decantr graph\b/,
|
|
3567
3687
|
`decantr graph --project ${context.projectPath}`
|
|
3568
3688
|
);
|
|
3689
|
+
rewritten = rewritten.replace(
|
|
3690
|
+
/^decantr verify\b/,
|
|
3691
|
+
`decantr verify --project ${context.projectPath}`
|
|
3692
|
+
);
|
|
3569
3693
|
rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
|
|
3570
3694
|
rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
|
|
3571
3695
|
return rewritten;
|
|
@@ -3796,7 +3920,7 @@ function isDuplicateFinding(existing, finding) {
|
|
|
3796
3920
|
}
|
|
3797
3921
|
function resolveOptionalPath(projectRoot, path) {
|
|
3798
3922
|
if (!path) return void 0;
|
|
3799
|
-
return
|
|
3923
|
+
return isAbsolute4(path) ? path : resolve3(projectRoot, path);
|
|
3800
3924
|
}
|
|
3801
3925
|
function hasProjectPlaywright(projectRoot) {
|
|
3802
3926
|
try {
|
|
@@ -3824,6 +3948,304 @@ function loadProjectPlaywright(projectRoot) {
|
|
|
3824
3948
|
}
|
|
3825
3949
|
return null;
|
|
3826
3950
|
}
|
|
3951
|
+
var KNOWN_INTERACTION_STYLE_CLASSES = [
|
|
3952
|
+
"d-enter-fade",
|
|
3953
|
+
"d-enter-slide-up",
|
|
3954
|
+
"d-enter-scale",
|
|
3955
|
+
"d-stagger-children",
|
|
3956
|
+
"d-pulse",
|
|
3957
|
+
"d-pulse-ring",
|
|
3958
|
+
"d-shimmer",
|
|
3959
|
+
"d-float",
|
|
3960
|
+
"d-glow-hover",
|
|
3961
|
+
"d-lift-hover",
|
|
3962
|
+
"d-scale-hover",
|
|
3963
|
+
"d-ripple"
|
|
3964
|
+
];
|
|
3965
|
+
function compactBrowserEvidence(value) {
|
|
3966
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
3967
|
+
}
|
|
3968
|
+
function browserErrorMessage(error) {
|
|
3969
|
+
return compactBrowserEvidence(error instanceof Error ? error.message : String(error));
|
|
3970
|
+
}
|
|
3971
|
+
function isPathInside(parentPath, childPath) {
|
|
3972
|
+
const parentRealPath = realpathSync(parentPath);
|
|
3973
|
+
const childRealPath = realpathSync(childPath);
|
|
3974
|
+
const relativePath = relative3(parentRealPath, childRealPath);
|
|
3975
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute4(relativePath);
|
|
3976
|
+
}
|
|
3977
|
+
function loadProjectAxeCore(projectRoot) {
|
|
3978
|
+
try {
|
|
3979
|
+
const requireFromProject = createRequire(join5(projectRoot, "package.json"));
|
|
3980
|
+
const resolved = requireFromProject.resolve("axe-core");
|
|
3981
|
+
const workspaceRoot = resolveWorkspaceInfo(projectRoot).workspaceRoot;
|
|
3982
|
+
const resolvedInProject = [projectRoot, workspaceRoot].some(
|
|
3983
|
+
(root) => isPathInside(root, resolved)
|
|
3984
|
+
);
|
|
3985
|
+
if (!resolvedInProject) return null;
|
|
3986
|
+
const loaded = requireFromProject("axe-core");
|
|
3987
|
+
return typeof loaded.source === "string" && loaded.source.length > 0 ? { source: loaded.source } : null;
|
|
3988
|
+
} catch {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function consoleErrorMessage(message) {
|
|
3993
|
+
try {
|
|
3994
|
+
const type = typeof message.type === "function" ? message.type() : "error";
|
|
3995
|
+
if (type !== "error") return null;
|
|
3996
|
+
const text = typeof message.text === "function" ? message.text() : "Console error";
|
|
3997
|
+
return compactBrowserEvidence(text || "Console error");
|
|
3998
|
+
} catch {
|
|
3999
|
+
return "Console error event could not be read.";
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
function consoleProbe(messages) {
|
|
4003
|
+
return {
|
|
4004
|
+
status: messages.length > 0 ? "failed" : "passed",
|
|
4005
|
+
count: messages.length,
|
|
4006
|
+
messages: messages.slice(0, 5)
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function fallbackRuntimeDomSnapshot(reason) {
|
|
4010
|
+
return {
|
|
4011
|
+
routeRendered: {
|
|
4012
|
+
status: "failed",
|
|
4013
|
+
readyState: null,
|
|
4014
|
+
url: null,
|
|
4015
|
+
title: null,
|
|
4016
|
+
bodyPresent: false,
|
|
4017
|
+
appRootPresent: false,
|
|
4018
|
+
bodyChildCount: 0,
|
|
4019
|
+
reason
|
|
4020
|
+
},
|
|
4021
|
+
nonblankDom: {
|
|
4022
|
+
status: "failed",
|
|
4023
|
+
textLength: 0,
|
|
4024
|
+
meaningfulElementCount: 0,
|
|
4025
|
+
mediaElementCount: 0,
|
|
4026
|
+
controlElementCount: 0,
|
|
4027
|
+
reason
|
|
4028
|
+
},
|
|
4029
|
+
interactionStyles: {
|
|
4030
|
+
status: "skipped",
|
|
4031
|
+
checked: 0,
|
|
4032
|
+
matchedClasses: [],
|
|
4033
|
+
animatedOrTransitioned: 0,
|
|
4034
|
+
missing: [],
|
|
4035
|
+
reason
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
async function collectRuntimeDomSnapshot(page) {
|
|
4040
|
+
try {
|
|
4041
|
+
return await page.evaluate((knownInteractionClasses) => {
|
|
4042
|
+
const global = globalThis;
|
|
4043
|
+
const document = global.document;
|
|
4044
|
+
const body = document?.body;
|
|
4045
|
+
const readyState = typeof document?.readyState === "string" ? document.readyState : null;
|
|
4046
|
+
const title = typeof document?.title === "string" ? document.title : null;
|
|
4047
|
+
const url = typeof global.location?.href === "string" ? global.location.href : null;
|
|
4048
|
+
const querySelector = typeof document?.querySelector === "function" ? document.querySelector : null;
|
|
4049
|
+
const querySelectorAll = typeof document?.querySelectorAll === "function" ? document.querySelectorAll : null;
|
|
4050
|
+
const bodyChildCount = typeof body?.childElementCount === "number" ? body.childElementCount : 0;
|
|
4051
|
+
const appRootPresent = Boolean(
|
|
4052
|
+
querySelector?.('[data-decantr-root], #root, #app, main, [role="main"], body > *')
|
|
4053
|
+
);
|
|
4054
|
+
const routeRendered = Boolean(body) && readyState !== "loading" && (appRootPresent || bodyChildCount > 0);
|
|
4055
|
+
const rawElements = querySelectorAll ? querySelectorAll("body *") : [];
|
|
4056
|
+
const elements = Array.from(rawElements);
|
|
4057
|
+
const text = String(body?.innerText ?? body?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
4058
|
+
const mediaTags = /* @__PURE__ */ new Set(["IMG", "SVG", "CANVAS", "VIDEO", "PICTURE"]);
|
|
4059
|
+
const controlTags = /* @__PURE__ */ new Set(["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY"]);
|
|
4060
|
+
const elementTag = (element) => typeof element.tagName === "string" ? element.tagName.toUpperCase() : "";
|
|
4061
|
+
const elementText = (element) => String(element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
4062
|
+
const elementAttribute = (element, name) => typeof element.getAttribute === "function" ? String(element.getAttribute(name) ?? "").trim() : "";
|
|
4063
|
+
const mediaElementCount = elements.filter(
|
|
4064
|
+
(element) => mediaTags.has(elementTag(element))
|
|
4065
|
+
).length;
|
|
4066
|
+
const controlElementCount = elements.filter(
|
|
4067
|
+
(element) => controlTags.has(elementTag(element))
|
|
4068
|
+
).length;
|
|
4069
|
+
const meaningfulElementCount = elements.filter((element) => {
|
|
4070
|
+
const tag = elementTag(element);
|
|
4071
|
+
return elementText(element).length > 0 || mediaTags.has(tag) || controlTags.has(tag) || elementAttribute(element, "aria-label").length > 0 || elementAttribute(element, "alt").length > 0 || elementAttribute(element, "title").length > 0;
|
|
4072
|
+
}).length;
|
|
4073
|
+
const nonblank = text.length > 0 || meaningfulElementCount > 0 || mediaElementCount > 0 || controlElementCount > 0;
|
|
4074
|
+
const durationHasTime = (value) => String(value ?? "").split(",").some((part) => {
|
|
4075
|
+
const trimmed = part.trim();
|
|
4076
|
+
if (!trimmed) return false;
|
|
4077
|
+
if (trimmed.endsWith("ms")) return Number.parseFloat(trimmed) > 0;
|
|
4078
|
+
if (trimmed.endsWith("s")) return Number.parseFloat(trimmed) > 0;
|
|
4079
|
+
return Number.parseFloat(trimmed) > 0;
|
|
4080
|
+
});
|
|
4081
|
+
const classListContains = (element, className) => {
|
|
4082
|
+
const classList = element.classList;
|
|
4083
|
+
if (typeof classList?.contains === "function") return classList.contains(className);
|
|
4084
|
+
return String(element.className ?? "").split(/\s+/).includes(className);
|
|
4085
|
+
};
|
|
4086
|
+
const styleTargets = elements.map((element) => ({
|
|
4087
|
+
element,
|
|
4088
|
+
classes: knownInteractionClasses.filter(
|
|
4089
|
+
(className) => classListContains(element, className)
|
|
4090
|
+
)
|
|
4091
|
+
})).filter((entry) => entry.classes.length > 0);
|
|
4092
|
+
const matchedClasses = [...new Set(styleTargets.flatMap((entry) => entry.classes))].sort();
|
|
4093
|
+
const missing = /* @__PURE__ */ new Set();
|
|
4094
|
+
let animatedOrTransitioned = 0;
|
|
4095
|
+
for (const entry of styleTargets) {
|
|
4096
|
+
const computed = typeof global.getComputedStyle === "function" ? global.getComputedStyle(entry.element) : {};
|
|
4097
|
+
const animationName = String(computed.animationName ?? "none");
|
|
4098
|
+
const transitionProperty = String(computed.transitionProperty ?? "none");
|
|
4099
|
+
const hasAnimation = animationName !== "none" && durationHasTime(computed.animationDuration);
|
|
4100
|
+
const hasTransition = transitionProperty !== "none" && durationHasTime(computed.transitionDuration);
|
|
4101
|
+
if (hasAnimation || hasTransition) {
|
|
4102
|
+
animatedOrTransitioned += 1;
|
|
4103
|
+
} else {
|
|
4104
|
+
for (const className of entry.classes) missing.add(className);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
const interactionStatus = styleTargets.length === 0 ? "skipped" : missing.size === 0 ? "passed" : "failed";
|
|
4108
|
+
return {
|
|
4109
|
+
routeRendered: {
|
|
4110
|
+
status: routeRendered ? "passed" : "failed",
|
|
4111
|
+
readyState,
|
|
4112
|
+
url,
|
|
4113
|
+
title,
|
|
4114
|
+
bodyPresent: Boolean(body),
|
|
4115
|
+
appRootPresent,
|
|
4116
|
+
bodyChildCount,
|
|
4117
|
+
...routeRendered ? {} : { reason: "No rendered app root or body content was detected after navigation." }
|
|
4118
|
+
},
|
|
4119
|
+
nonblankDom: {
|
|
4120
|
+
status: nonblank ? "passed" : "failed",
|
|
4121
|
+
textLength: text.length,
|
|
4122
|
+
meaningfulElementCount,
|
|
4123
|
+
mediaElementCount,
|
|
4124
|
+
controlElementCount,
|
|
4125
|
+
...nonblank ? {} : {
|
|
4126
|
+
reason: "DOM rendered, but no meaningful text, media, controls, or labels were detected."
|
|
4127
|
+
}
|
|
4128
|
+
},
|
|
4129
|
+
interactionStyles: {
|
|
4130
|
+
status: interactionStatus,
|
|
4131
|
+
checked: styleTargets.length,
|
|
4132
|
+
matchedClasses,
|
|
4133
|
+
animatedOrTransitioned,
|
|
4134
|
+
missing: [...missing].slice(0, 8),
|
|
4135
|
+
...styleTargets.length === 0 ? { reason: "No known Decantr interaction classes were present on this route." } : {}
|
|
4136
|
+
}
|
|
4137
|
+
};
|
|
4138
|
+
}, KNOWN_INTERACTION_STYLE_CLASSES);
|
|
4139
|
+
} catch (error) {
|
|
4140
|
+
return fallbackRuntimeDomSnapshot(`Runtime DOM probe failed: ${browserErrorMessage(error)}`);
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
function skippedAccessibilityProbe(reason) {
|
|
4144
|
+
return {
|
|
4145
|
+
status: "skipped",
|
|
4146
|
+
engine: "axe-core",
|
|
4147
|
+
violations: 0,
|
|
4148
|
+
incomplete: 0,
|
|
4149
|
+
messages: [],
|
|
4150
|
+
reason
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
async function collectAccessibilityProbe(page, axeCore) {
|
|
4154
|
+
if (!axeCore) return skippedAccessibilityProbe("axe-core is not installed in this project.");
|
|
4155
|
+
if (!page.addScriptTag) {
|
|
4156
|
+
return skippedAccessibilityProbe("The local Playwright adapter does not expose addScriptTag.");
|
|
4157
|
+
}
|
|
4158
|
+
try {
|
|
4159
|
+
await page.addScriptTag({ content: axeCore.source });
|
|
4160
|
+
const result = await page.evaluate(() => {
|
|
4161
|
+
const global = globalThis;
|
|
4162
|
+
if (!global.axe?.run) return { error: "axe-core did not expose window.axe.run." };
|
|
4163
|
+
return global.axe.run(global.document, { resultTypes: ["violations"] }).then((axeResult) => ({
|
|
4164
|
+
violations: (axeResult.violations ?? []).map((violation) => ({
|
|
4165
|
+
id: violation.id,
|
|
4166
|
+
impact: violation.impact,
|
|
4167
|
+
help: violation.help,
|
|
4168
|
+
description: violation.description,
|
|
4169
|
+
targets: (violation.nodes ?? []).flatMap((node) => node.target ?? []).slice(0, 3)
|
|
4170
|
+
})),
|
|
4171
|
+
incomplete: axeResult.incomplete?.length ?? 0
|
|
4172
|
+
}));
|
|
4173
|
+
});
|
|
4174
|
+
if (result.error) {
|
|
4175
|
+
return {
|
|
4176
|
+
status: "failed",
|
|
4177
|
+
engine: "axe-core",
|
|
4178
|
+
violations: 0,
|
|
4179
|
+
incomplete: 0,
|
|
4180
|
+
messages: [result.error],
|
|
4181
|
+
reason: result.error
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
const violations = result.violations ?? [];
|
|
4185
|
+
const messages = violations.slice(0, 5).map((violation) => {
|
|
4186
|
+
const label = violation.id ?? "axe-violation";
|
|
4187
|
+
const detail = violation.help ?? violation.description ?? "Accessibility violation";
|
|
4188
|
+
const impact = violation.impact ? ` (${violation.impact})` : "";
|
|
4189
|
+
const targets = violation.targets && violation.targets.length > 0 ? ` [${violation.targets.join(", ")}]` : "";
|
|
4190
|
+
return compactBrowserEvidence(`${label}${impact}: ${detail}${targets}`);
|
|
4191
|
+
});
|
|
4192
|
+
return {
|
|
4193
|
+
status: violations.length > 0 ? "failed" : "passed",
|
|
4194
|
+
engine: "axe-core",
|
|
4195
|
+
violations: violations.length,
|
|
4196
|
+
incomplete: result.incomplete ?? 0,
|
|
4197
|
+
messages
|
|
4198
|
+
};
|
|
4199
|
+
} catch (error) {
|
|
4200
|
+
const message = `Axe probe failed: ${browserErrorMessage(error)}`;
|
|
4201
|
+
return {
|
|
4202
|
+
status: "failed",
|
|
4203
|
+
engine: "axe-core",
|
|
4204
|
+
violations: 0,
|
|
4205
|
+
incomplete: 0,
|
|
4206
|
+
messages: [message],
|
|
4207
|
+
reason: message
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
function browserRuntimeFailureMessages(route, runtime) {
|
|
4212
|
+
const messages = [];
|
|
4213
|
+
if (runtime.routeRendered.status === "failed") {
|
|
4214
|
+
messages.push(
|
|
4215
|
+
`${route}: route-rendered probe failed (${runtime.routeRendered.reason ?? "no rendered app root detected"})`
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
if (runtime.nonblankDom.status === "failed") {
|
|
4219
|
+
messages.push(
|
|
4220
|
+
`${route}: nonblank DOM probe failed (${runtime.nonblankDom.reason ?? "blank DOM detected"})`
|
|
4221
|
+
);
|
|
4222
|
+
}
|
|
4223
|
+
if (runtime.consoleErrors.count > 0) {
|
|
4224
|
+
messages.push(
|
|
4225
|
+
`${route}: console errors (${runtime.consoleErrors.count}) ${runtime.consoleErrors.messages.join(" | ")}`
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
if (runtime.pageErrors.count > 0) {
|
|
4229
|
+
messages.push(
|
|
4230
|
+
`${route}: page errors (${runtime.pageErrors.count}) ${runtime.pageErrors.messages.join(" | ")}`
|
|
4231
|
+
);
|
|
4232
|
+
}
|
|
4233
|
+
if (runtime.interactionStyles.status === "failed") {
|
|
4234
|
+
messages.push(
|
|
4235
|
+
`${route}: interaction style probe found known classes without computed animation/transition (${runtime.interactionStyles.missing.join(", ")})`
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
4238
|
+
return messages.map(compactBrowserEvidence);
|
|
4239
|
+
}
|
|
4240
|
+
function browserAccessibilityFailureMessages(route, runtime) {
|
|
4241
|
+
if (runtime.accessibility.status !== "failed") return [];
|
|
4242
|
+
const detail = runtime.accessibility.messages.length > 0 ? runtime.accessibility.messages.join(" | ") : runtime.accessibility.reason ?? "axe-core reported accessibility failures";
|
|
4243
|
+
return [
|
|
4244
|
+
compactBrowserEvidence(
|
|
4245
|
+
`${route}: axe-core violations (${runtime.accessibility.violations}) ${detail}`
|
|
4246
|
+
)
|
|
4247
|
+
];
|
|
4248
|
+
}
|
|
3827
4249
|
function browserRouteUrl(baseUrl, route) {
|
|
3828
4250
|
return new URL(route || "/", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
3829
4251
|
}
|
|
@@ -3845,7 +4267,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3845
4267
|
baseId: "playwright-missing"
|
|
3846
4268
|
});
|
|
3847
4269
|
return {
|
|
3848
|
-
finding,
|
|
4270
|
+
findings: [finding],
|
|
3849
4271
|
evidence: {
|
|
3850
4272
|
enabled: true,
|
|
3851
4273
|
status: "unavailable",
|
|
@@ -3867,7 +4289,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3867
4289
|
baseId: "base-url-missing"
|
|
3868
4290
|
});
|
|
3869
4291
|
return {
|
|
3870
|
-
finding,
|
|
4292
|
+
findings: [finding],
|
|
3871
4293
|
evidence: {
|
|
3872
4294
|
enabled: true,
|
|
3873
4295
|
status: "unavailable",
|
|
@@ -3890,7 +4312,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3890
4312
|
baseId: "adapter-missing"
|
|
3891
4313
|
});
|
|
3892
4314
|
return {
|
|
3893
|
-
finding,
|
|
4315
|
+
findings: [finding],
|
|
3894
4316
|
evidence: {
|
|
3895
4317
|
enabled: true,
|
|
3896
4318
|
status: "unavailable",
|
|
@@ -3902,19 +4324,41 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3902
4324
|
}
|
|
3903
4325
|
const routes = (declaredRoutes.length > 0 ? declaredRoutes : ["/"]).slice(0, 12);
|
|
3904
4326
|
const screenshots = [];
|
|
3905
|
-
const
|
|
4327
|
+
const routeFailures = [];
|
|
4328
|
+
const runtimeFailures = [];
|
|
4329
|
+
const accessibilityFailures = [];
|
|
3906
4330
|
const visualRoutes = [];
|
|
3907
4331
|
const screenshotDir = join5(projectRoot, ".decantr", "evidence", "screenshots");
|
|
3908
4332
|
mkdirSync4(screenshotDir, { recursive: true });
|
|
4333
|
+
const axeCore = loadProjectAxeCore(projectRoot);
|
|
3909
4334
|
let browser = null;
|
|
3910
4335
|
try {
|
|
3911
4336
|
browser = await playwright.chromium.launch({ headless: true });
|
|
3912
|
-
const page = await browser.newPage();
|
|
3913
4337
|
for (const route of routes) {
|
|
3914
4338
|
const url = browserRouteUrl(options.browserBaseUrl, route);
|
|
3915
4339
|
const relativePath = browserScreenshotRelativePath(route);
|
|
4340
|
+
let page = null;
|
|
4341
|
+
const consoleErrors = [];
|
|
4342
|
+
const pageErrors = [];
|
|
3916
4343
|
try {
|
|
4344
|
+
page = await browser.newPage();
|
|
4345
|
+
page.on?.("console", (message) => {
|
|
4346
|
+
const errorMessage = consoleErrorMessage(message);
|
|
4347
|
+
if (errorMessage) consoleErrors.push(errorMessage);
|
|
4348
|
+
});
|
|
4349
|
+
page.on?.("pageerror", (error) => {
|
|
4350
|
+
pageErrors.push(browserErrorMessage(error));
|
|
4351
|
+
});
|
|
3917
4352
|
await page.goto(url, { waitUntil: "networkidle", timeout: 15e3 });
|
|
4353
|
+
const domSnapshot = await collectRuntimeDomSnapshot(page);
|
|
4354
|
+
const runtime = {
|
|
4355
|
+
...domSnapshot,
|
|
4356
|
+
consoleErrors: consoleProbe(consoleErrors),
|
|
4357
|
+
pageErrors: consoleProbe(pageErrors),
|
|
4358
|
+
accessibility: await collectAccessibilityProbe(page, axeCore)
|
|
4359
|
+
};
|
|
4360
|
+
runtimeFailures.push(...browserRuntimeFailureMessages(route, runtime));
|
|
4361
|
+
accessibilityFailures.push(...browserAccessibilityFailureMessages(route, runtime));
|
|
3918
4362
|
const absoluteScreenshotPath = join5(projectRoot, relativePath);
|
|
3919
4363
|
await page.screenshot({ path: absoluteScreenshotPath, fullPage: true });
|
|
3920
4364
|
screenshots.push(relativePath);
|
|
@@ -3923,11 +4367,12 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3923
4367
|
url,
|
|
3924
4368
|
screenshot: relativePath,
|
|
3925
4369
|
screenshotHash: hashFile2(absoluteScreenshotPath),
|
|
3926
|
-
status: "captured"
|
|
4370
|
+
status: "captured",
|
|
4371
|
+
runtime
|
|
3927
4372
|
});
|
|
3928
4373
|
} catch (error) {
|
|
3929
4374
|
const message = error.message;
|
|
3930
|
-
|
|
4375
|
+
routeFailures.push(`${route}: ${message}`);
|
|
3931
4376
|
visualRoutes.push({
|
|
3932
4377
|
route,
|
|
3933
4378
|
url,
|
|
@@ -3936,10 +4381,15 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3936
4381
|
status: "failed",
|
|
3937
4382
|
error: message
|
|
3938
4383
|
});
|
|
4384
|
+
} finally {
|
|
4385
|
+
try {
|
|
4386
|
+
await page?.close?.();
|
|
4387
|
+
} catch {
|
|
4388
|
+
}
|
|
3939
4389
|
}
|
|
3940
4390
|
}
|
|
3941
4391
|
} catch (error) {
|
|
3942
|
-
|
|
4392
|
+
routeFailures.push(error.message);
|
|
3943
4393
|
} finally {
|
|
3944
4394
|
if (browser) await browser.close();
|
|
3945
4395
|
}
|
|
@@ -3953,30 +4403,68 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
3953
4403
|
const visualManifestPath = join5(projectRoot, ".decantr", "evidence", "visual-manifest.json");
|
|
3954
4404
|
mkdirSync4(dirname3(visualManifestPath), { recursive: true });
|
|
3955
4405
|
writeFileSync4(visualManifestPath, JSON.stringify(visualManifest, null, 2) + "\n", "utf-8");
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
4406
|
+
const findings = [];
|
|
4407
|
+
if (routeFailures.length > 0) {
|
|
4408
|
+
findings.push(
|
|
4409
|
+
createHealthFinding({
|
|
4410
|
+
source: "browser",
|
|
4411
|
+
category: "Browser Verification",
|
|
4412
|
+
severity: options.requireBrowser ? "error" : "warn",
|
|
4413
|
+
message: "Browser verification could not render every declared route.",
|
|
4414
|
+
evidence: routeFailures.slice(0, 5),
|
|
4415
|
+
rule: "browser-route-verification-failed",
|
|
4416
|
+
suggestedFix: "Start the app at the provided base URL, fix route render errors, and rerun `decantr health --browser --evidence`.",
|
|
4417
|
+
baseId: "route-verification-failed"
|
|
4418
|
+
})
|
|
4419
|
+
);
|
|
4420
|
+
}
|
|
4421
|
+
if (runtimeFailures.length > 0) {
|
|
4422
|
+
findings.push(
|
|
4423
|
+
createHealthFinding({
|
|
4424
|
+
source: "browser",
|
|
4425
|
+
category: "Browser Runtime",
|
|
4426
|
+
severity: options.requireBrowser ? "error" : "warn",
|
|
4427
|
+
message: "Browser runtime probes failed for one or more rendered routes.",
|
|
4428
|
+
evidence: runtimeFailures.slice(0, 5),
|
|
4429
|
+
rule: "browser-runtime-probes-failed",
|
|
4430
|
+
suggestedFix: "Inspect console/page errors and rendered DOM state, repair the route runtime behavior, and rerun `decantr health --browser --evidence`.",
|
|
4431
|
+
code: "RUNTIME010",
|
|
4432
|
+
repair: { id: "repair-browser-runtime-probes" },
|
|
4433
|
+
baseId: "runtime-probes-failed"
|
|
4434
|
+
})
|
|
4435
|
+
);
|
|
4436
|
+
}
|
|
4437
|
+
if (accessibilityFailures.length > 0) {
|
|
4438
|
+
findings.push(
|
|
4439
|
+
createHealthFinding({
|
|
4440
|
+
source: "browser",
|
|
4441
|
+
category: "Browser Accessibility",
|
|
4442
|
+
severity: options.requireBrowser ? "error" : "warn",
|
|
4443
|
+
message: "Axe reported accessibility violations in rendered browser evidence.",
|
|
4444
|
+
evidence: accessibilityFailures.slice(0, 5),
|
|
4445
|
+
rule: "browser-axe-violations",
|
|
4446
|
+
suggestedFix: "Repair the rendered accessibility violations and rerun `decantr health --browser --evidence`.",
|
|
4447
|
+
code: "A11Y020",
|
|
4448
|
+
repair: { id: "fix-rendered-accessibility" },
|
|
4449
|
+
baseId: "axe-violations"
|
|
4450
|
+
})
|
|
4451
|
+
);
|
|
4452
|
+
}
|
|
4453
|
+
if (findings.length > 0) {
|
|
4454
|
+
const evidenceFindings = [...routeFailures, ...runtimeFailures, ...accessibilityFailures];
|
|
3967
4455
|
return {
|
|
3968
|
-
|
|
4456
|
+
findings,
|
|
3969
4457
|
evidence: {
|
|
3970
4458
|
enabled: true,
|
|
3971
4459
|
status: "failed",
|
|
3972
4460
|
baseUrl: options.browserBaseUrl,
|
|
3973
4461
|
screenshots,
|
|
3974
|
-
findings:
|
|
4462
|
+
findings: evidenceFindings
|
|
3975
4463
|
}
|
|
3976
4464
|
};
|
|
3977
4465
|
}
|
|
3978
4466
|
return {
|
|
3979
|
-
|
|
4467
|
+
findings: [],
|
|
3980
4468
|
evidence: {
|
|
3981
4469
|
enabled: true,
|
|
3982
4470
|
status: "passed",
|
|
@@ -4013,7 +4501,7 @@ function parseDecantrCssTokenNames(projectRoot) {
|
|
|
4013
4501
|
function collectDesignTokenEvidence(projectRoot, designTokensPath) {
|
|
4014
4502
|
const resolved = resolveOptionalPath(projectRoot, designTokensPath);
|
|
4015
4503
|
if (!resolved) return void 0;
|
|
4016
|
-
const sourceLabel =
|
|
4504
|
+
const sourceLabel = isAbsolute4(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
|
|
4017
4505
|
if (!existsSync5(resolved)) {
|
|
4018
4506
|
return {
|
|
4019
4507
|
source: sourceLabel,
|
|
@@ -4361,8 +4849,8 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
|
|
|
4361
4849
|
options,
|
|
4362
4850
|
declaredRoutes
|
|
4363
4851
|
);
|
|
4364
|
-
|
|
4365
|
-
findings.push(
|
|
4852
|
+
for (const browserFinding of browserVerification?.findings ?? []) {
|
|
4853
|
+
if (!isDuplicateFinding(seen, browserFinding)) findings.push(browserFinding);
|
|
4366
4854
|
}
|
|
4367
4855
|
const anchoredFindings = anchorProjectHealthFindings(projectRoot, findings);
|
|
4368
4856
|
const scopedFindings = scopeHealthFindingsToProject(projectRoot, anchoredFindings);
|
|
@@ -4524,8 +5012,12 @@ function formatProjectHealthJson(report) {
|
|
|
4524
5012
|
`;
|
|
4525
5013
|
}
|
|
4526
5014
|
function diagnosticCatalogPayload() {
|
|
5015
|
+
const diagnosticsByRule = /* @__PURE__ */ new Map();
|
|
5016
|
+
for (const entry of [...KNOWN_VERIFICATION_DIAGNOSTICS, ...HEALTH_BROWSER_RUNTIME_DIAGNOSTICS]) {
|
|
5017
|
+
diagnosticsByRule.set(entry.rule, entry);
|
|
5018
|
+
}
|
|
4527
5019
|
return {
|
|
4528
|
-
diagnostics:
|
|
5020
|
+
diagnostics: [...diagnosticsByRule.values()].map((entry) => ({
|
|
4529
5021
|
code: entry.code,
|
|
4530
5022
|
family: entry.family,
|
|
4531
5023
|
rule: entry.rule,
|
|
@@ -4609,7 +5101,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
|
|
|
4609
5101
|
const format2 = resolveFormat(options);
|
|
4610
5102
|
const payload2 = format2 === "json" ? formatDiagnosticCatalogJson() : format2 === "markdown" ? formatDiagnosticCatalogMarkdown() : formatDiagnosticCatalogText();
|
|
4611
5103
|
if (options.output) {
|
|
4612
|
-
const outputPath =
|
|
5104
|
+
const outputPath = isAbsolute4(options.output) ? options.output : join5(projectRoot, options.output);
|
|
4613
5105
|
mkdirSync4(dirname3(outputPath), { recursive: true });
|
|
4614
5106
|
writeFileSync4(outputPath, payload2, "utf-8");
|
|
4615
5107
|
if (!options.ci) {
|
|
@@ -4664,7 +5156,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
|
|
|
4664
5156
|
` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
|
|
4665
5157
|
const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
|
|
4666
5158
|
if (options.output) {
|
|
4667
|
-
const outputPath =
|
|
5159
|
+
const outputPath = isAbsolute4(options.output) ? options.output : join5(projectRoot, options.output);
|
|
4668
5160
|
mkdirSync4(dirname3(outputPath), { recursive: true });
|
|
4669
5161
|
writeFileSync4(outputPath, payload, "utf-8");
|
|
4670
5162
|
if (!options.ci) {
|