@decantr/cli 3.1.0 → 3.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-WSL6ZSTZ.js";
2
+ import "./chunk-33IZQXZH.js";
3
3
  import "./chunk-SIDKK73N.js";
4
- import "./chunk-TXNLMAN5.js";
5
- import "./chunk-BY6S6IWG.js";
6
- import "./chunk-GPN2THFD.js";
4
+ import "./chunk-UGRAHWCG.js";
5
+ import "./chunk-BDVI3SSP.js";
6
+ import "./chunk-2GCVVEQC.js";
@@ -737,8 +737,8 @@ function extractCSSVariables(content) {
737
737
  const colors = {};
738
738
  const variables = [];
739
739
  const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
740
- let match;
741
- while ((match = varRegex.exec(content)) !== null) {
740
+ let match = varRegex.exec(content);
741
+ while (match !== null) {
742
742
  const name = match[1];
743
743
  const value = match[2].trim();
744
744
  variables.push(`--${name}`);
@@ -758,6 +758,24 @@ function extractCSSVariables(content) {
758
758
  if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || colorPatterns.some((p) => name.includes(p))) {
759
759
  colors[name] = value;
760
760
  }
761
+ match = varRegex.exec(content);
762
+ }
763
+ return { colors, variables };
764
+ }
765
+ function extractTailwindThemeTokens(content) {
766
+ const colors = {};
767
+ const variables = [];
768
+ for (const match of content.matchAll(
769
+ /\b([a-zA-Z][\w-]*)\s*:\s*['"`](#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)|var\(--[^)]+\))['"`]/g
770
+ )) {
771
+ const name = match[1];
772
+ const value = match[2];
773
+ if (/color|bg|background|surface|text|foreground|border|primary|secondary|accent|muted|danger|error|success|warning/i.test(
774
+ name
775
+ ) || /^#|^(?:rgba?|hsla?|oklch)\(/i.test(value) || /^var\(--/i.test(value)) {
776
+ colors[name] = value;
777
+ variables.push(`theme.colors.${name}`);
778
+ }
761
779
  }
762
780
  return { colors, variables };
763
781
  }
@@ -919,6 +937,16 @@ function scanStyling(projectRoot) {
919
937
  colors = { ...colors, ...extracted.colors };
920
938
  cssVariables.push(...extracted.variables);
921
939
  }
940
+ for (const cfg of TAILWIND_CONFIGS) {
941
+ const configPath = join3(projectRoot, cfg);
942
+ if (!existsSync3(configPath)) continue;
943
+ try {
944
+ const extracted = extractTailwindThemeTokens(readFileSync3(configPath, "utf-8"));
945
+ colors = { ...colors, ...extracted.colors };
946
+ cssVariables.push(...extracted.variables);
947
+ } catch {
948
+ }
949
+ }
922
950
  cssVariables = [...new Set(cssVariables)];
923
951
  const darkMode = detectDarkMode(projectRoot, cssContents);
924
952
  if (approach === "unknown" && cssContents.length > 0) {
@@ -1159,7 +1187,7 @@ function routeLooksPublicFullBleed(path, file, code) {
1159
1187
  );
1160
1188
  }
1161
1189
  function routeLooksAppShell(file, code) {
1162
- return /\b(?:sidebar|side-nav|sidenav|app-frame|dashboard-shell|app-shell)\b|<\s*(?:Sidebar|AppShell|DashboardShell)\b/i.test(
1190
+ return /\b(?:sidebar|side-nav|sidenav|app-frame|dashboard-shell|protected-shell)\b|<\s*(?:Sidebar|DashboardShell)\b/i.test(
1163
1191
  `${file} ${code}`
1164
1192
  );
1165
1193
  }
@@ -22,7 +22,7 @@ import {
22
22
  listWorkspaceCandidates,
23
23
  listWorkspaceProjects,
24
24
  shouldFailWorkspaceHealth
25
- } from "./chunk-TXNLMAN5.js";
25
+ } from "./chunk-UGRAHWCG.js";
26
26
  import {
27
27
  acceptBrownfieldLocalLaw,
28
28
  acceptStyleBridge,
@@ -52,7 +52,7 @@ import {
52
52
  writeBrownfieldCodifyProposal,
53
53
  writeHostedPatternMappingProposal,
54
54
  writeStyleBridgeProposal
55
- } from "./chunk-BY6S6IWG.js";
55
+ } from "./chunk-BDVI3SSP.js";
56
56
  import {
57
57
  buildGuardRegistryContext,
58
58
  createDoctrineMap,
@@ -69,7 +69,7 @@ import {
69
69
  sendCliCommandTelemetry,
70
70
  sendNewProjectCompletedTelemetry,
71
71
  writeDoctrineMap
72
- } from "./chunk-GPN2THFD.js";
72
+ } from "./chunk-2GCVVEQC.js";
73
73
 
74
74
  // src/index.ts
75
75
  import { existsSync as existsSync28, mkdirSync as mkdirSync15, readdirSync as readdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync18 } from "fs";
@@ -10859,7 +10859,7 @@ async function cmdAdoptWorkflow(args) {
10859
10859
  await cmdGraph(projectRoot, { displayRoot: process.cwd() });
10860
10860
  if (process.exitCode && process.exitCode !== 0) return;
10861
10861
  if (runVerify) {
10862
- const { cmdHealth } = await import("./health-IOTIZT7Z.js");
10862
+ const { cmdHealth } = await import("./health-G7HQCDXX.js");
10863
10863
  await cmdHealth(projectRoot, {
10864
10864
  browser: runBrowser,
10865
10865
  browserBaseUrl: baseUrl,
@@ -10930,7 +10930,7 @@ async function cmdVerifyWorkflow(args) {
10930
10930
  return;
10931
10931
  }
10932
10932
  if (workspaceMode) {
10933
- const { cmdWorkspace } = await import("./workspace-72SJI3MW.js");
10933
+ const { cmdWorkspace } = await import("./workspace-3AJEXXUL.js");
10934
10934
  await cmdWorkspace(process.cwd(), ["workspace", "health", ...withoutWorkflowOnlyFlags(args)]);
10935
10935
  return;
10936
10936
  }
@@ -10963,7 +10963,7 @@ async function cmdVerifyWorkflow(args) {
10963
10963
  }
10964
10964
  let guardExitCode;
10965
10965
  if (brownfield) {
10966
- const { cmdHeal, collectCheckIssues } = await import("./heal-2DKBFWB6.js");
10966
+ const { cmdHeal, collectCheckIssues } = await import("./heal-OKZ5QZN7.js");
10967
10967
  if (quietOutput) {
10968
10968
  const result = collectCheckIssues(workspaceInfo.appRoot, { brownfield: true });
10969
10969
  guardExitCode = result.issues.some((issue) => issue.type === "error") ? 1 : void 0;
@@ -10973,7 +10973,7 @@ async function cmdVerifyWorkflow(args) {
10973
10973
  process.exitCode = void 0;
10974
10974
  }
10975
10975
  }
10976
- const { cmdHealth, parseHealthArgs } = await import("./health-IOTIZT7Z.js");
10976
+ const { cmdHealth, parseHealthArgs } = await import("./health-G7HQCDXX.js");
10977
10977
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
10978
10978
  if (localPatterns) {
10979
10979
  const validation = validateLocalLaw(workspaceInfo.appRoot);
@@ -12476,7 +12476,7 @@ async function main() {
12476
12476
  `${YELLOW12}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET16}`
12477
12477
  );
12478
12478
  }
12479
- const { cmdHeal } = await import("./heal-2DKBFWB6.js");
12479
+ const { cmdHeal } = await import("./heal-OKZ5QZN7.js");
12480
12480
  const { flags } = parseLooseArgs(args);
12481
12481
  const workspaceInfo = resolveWorkflowProject(flags, "check");
12482
12482
  if (!workspaceInfo) break;
@@ -12501,7 +12501,7 @@ async function main() {
12501
12501
  const { flags } = parseLooseArgs(args);
12502
12502
  const workspaceInfo = resolveWorkflowProject(flags, "health");
12503
12503
  if (!workspaceInfo) break;
12504
- const { cmdHealth, parseHealthArgs } = await import("./health-IOTIZT7Z.js");
12504
+ const { cmdHealth, parseHealthArgs } = await import("./health-G7HQCDXX.js");
12505
12505
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(stripProjectArgs(args)));
12506
12506
  } catch (e) {
12507
12507
  console.error(error2(e.message));
@@ -12529,7 +12529,7 @@ async function main() {
12529
12529
  cmdStudioHelp();
12530
12530
  break;
12531
12531
  }
12532
- const { cmdStudio, parseStudioArgs } = await import("./studio-DLWUMCC3.js");
12532
+ const { cmdStudio, parseStudioArgs } = await import("./studio-T73T4Q2V.js");
12533
12533
  await cmdStudio(process.cwd(), parseStudioArgs(args));
12534
12534
  } catch (e) {
12535
12535
  console.error(error2(e.message));
@@ -12543,7 +12543,7 @@ async function main() {
12543
12543
  cmdWorkspaceHelp();
12544
12544
  break;
12545
12545
  }
12546
- const { cmdWorkspace } = await import("./workspace-72SJI3MW.js");
12546
+ const { cmdWorkspace } = await import("./workspace-3AJEXXUL.js");
12547
12547
  await cmdWorkspace(process.cwd(), args);
12548
12548
  } catch (e) {
12549
12549
  console.error(error2(e.message));
@@ -3,12 +3,12 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-GPN2THFD.js";
6
+ } from "./chunk-2GCVVEQC.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
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";
@@ -1261,6 +1261,19 @@ function colorTokenNames(styling) {
1261
1261
  }
1262
1262
  return [...names].slice(0, 40);
1263
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
+ }
1264
1277
  function createStyleBridgeProposal(input) {
1265
1278
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1266
1279
  const theme = readThemeInventory(input.projectRoot);
@@ -1272,7 +1285,7 @@ function createStyleBridgeProposal(input) {
1272
1285
  const themeModes = theme.modes.length > 0 ? theme.modes : darkModeDetected ? ["base", "dark"] : ["base"];
1273
1286
  const themeVariantIds = theme.variantIds.length > 0 ? theme.variantIds : themeModes.filter((mode) => mode !== "base");
1274
1287
  return {
1275
- version: 1,
1288
+ version: 2,
1276
1289
  status: "proposal",
1277
1290
  generatedAt,
1278
1291
  source: "decantr codify --style-bridge",
@@ -1312,6 +1325,17 @@ function createStyleBridgeProposal(input) {
1312
1325
  label: "Surfaces and cards",
1313
1326
  decantrIntent: "surface background, card treatment, border, radius, depth, and hover state",
1314
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",
1315
1339
  tokenHints: tokenHints(
1316
1340
  input.styling,
1317
1341
  /surface|card|panel|bg|background|border|shadow|radius/i
@@ -1328,6 +1352,17 @@ function createStyleBridgeProposal(input) {
1328
1352
  label: "Actions and buttons",
1329
1353
  decantrIntent: "primary, secondary, tertiary, destructive, icon-only, loading, and disabled action states",
1330
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",
1331
1366
  tokenHints: tokenHints(
1332
1367
  input.styling,
1333
1368
  /primary|secondary|accent|danger|error|destructive|focus/i
@@ -1344,6 +1379,17 @@ function createStyleBridgeProposal(input) {
1344
1379
  label: "Focus and accessibility",
1345
1380
  decantrIntent: "visible focus, keyboard clarity, contrast, and reduced-motion safety",
1346
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",
1347
1393
  tokenHints: tokenHints(input.styling, /focus|ring|outline|contrast|motion|duration/i),
1348
1394
  classHints: [],
1349
1395
  sourceEvidence: [],
@@ -1357,6 +1403,17 @@ function createStyleBridgeProposal(input) {
1357
1403
  label: "Layout density and spacing",
1358
1404
  decantrIntent: "route gutters, section gaps, component padding, density, and responsive rhythm",
1359
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",
1360
1417
  tokenHints: tokenHints(input.styling, /space|spacing|gap|gutter|container|radius/i),
1361
1418
  classHints: classHintsForPattern(input.projectRoot, ["page-shell"]),
1362
1419
  sourceEvidence: sourceEvidence(input.projectRoot, ["page-shell"]),
@@ -1370,6 +1427,17 @@ function createStyleBridgeProposal(input) {
1370
1427
  label: "Theme variants",
1371
1428
  decantrIntent: "light, dark, brand, tenant, seasonal, and density variants",
1372
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",
1373
1441
  tokenHints: tokenHints(input.styling, /theme|dark|light|brand|tenant|mode|color/i),
1374
1442
  classHints: classHintsForPattern(input.projectRoot, ["theme-variant"]),
1375
1443
  sourceEvidence: sourceEvidence(input.projectRoot, ["theme-variant"]),
@@ -1453,6 +1521,11 @@ function styleBridgeMatches(projectRoot, query) {
1453
1521
  mapping.label,
1454
1522
  mapping.decantrIntent,
1455
1523
  mapping.projectAuthority,
1524
+ mapping.native?.ref,
1525
+ mapping.native?.kind,
1526
+ mapping.essence?.ref,
1527
+ mapping.essence?.kind,
1528
+ mapping.property,
1456
1529
  ...mapping.tokenHints,
1457
1530
  ...mapping.classHints,
1458
1531
  ...mapping.guardrails
@@ -3143,6 +3216,20 @@ var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
3143
3216
  var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
3144
3217
  var DEFAULT_HEALTH_CI_CLI_VERSION = "latest";
3145
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
+ ];
3146
3233
  function readProjectMetadata(projectRoot) {
3147
3234
  const projectJsonPath = join5(projectRoot, ".decantr", "project.json");
3148
3235
  if (!existsSync5(projectJsonPath)) {
@@ -3861,6 +3948,304 @@ function loadProjectPlaywright(projectRoot) {
3861
3948
  }
3862
3949
  return null;
3863
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
+ }
3864
4249
  function browserRouteUrl(baseUrl, route) {
3865
4250
  return new URL(route || "/", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
3866
4251
  }
@@ -3882,7 +4267,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3882
4267
  baseId: "playwright-missing"
3883
4268
  });
3884
4269
  return {
3885
- finding,
4270
+ findings: [finding],
3886
4271
  evidence: {
3887
4272
  enabled: true,
3888
4273
  status: "unavailable",
@@ -3904,7 +4289,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3904
4289
  baseId: "base-url-missing"
3905
4290
  });
3906
4291
  return {
3907
- finding,
4292
+ findings: [finding],
3908
4293
  evidence: {
3909
4294
  enabled: true,
3910
4295
  status: "unavailable",
@@ -3927,7 +4312,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3927
4312
  baseId: "adapter-missing"
3928
4313
  });
3929
4314
  return {
3930
- finding,
4315
+ findings: [finding],
3931
4316
  evidence: {
3932
4317
  enabled: true,
3933
4318
  status: "unavailable",
@@ -3939,19 +4324,41 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3939
4324
  }
3940
4325
  const routes = (declaredRoutes.length > 0 ? declaredRoutes : ["/"]).slice(0, 12);
3941
4326
  const screenshots = [];
3942
- const browserFindings = [];
4327
+ const routeFailures = [];
4328
+ const runtimeFailures = [];
4329
+ const accessibilityFailures = [];
3943
4330
  const visualRoutes = [];
3944
4331
  const screenshotDir = join5(projectRoot, ".decantr", "evidence", "screenshots");
3945
4332
  mkdirSync4(screenshotDir, { recursive: true });
4333
+ const axeCore = loadProjectAxeCore(projectRoot);
3946
4334
  let browser = null;
3947
4335
  try {
3948
4336
  browser = await playwright.chromium.launch({ headless: true });
3949
- const page = await browser.newPage();
3950
4337
  for (const route of routes) {
3951
4338
  const url = browserRouteUrl(options.browserBaseUrl, route);
3952
4339
  const relativePath = browserScreenshotRelativePath(route);
4340
+ let page = null;
4341
+ const consoleErrors = [];
4342
+ const pageErrors = [];
3953
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
+ });
3954
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));
3955
4362
  const absoluteScreenshotPath = join5(projectRoot, relativePath);
3956
4363
  await page.screenshot({ path: absoluteScreenshotPath, fullPage: true });
3957
4364
  screenshots.push(relativePath);
@@ -3960,11 +4367,12 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3960
4367
  url,
3961
4368
  screenshot: relativePath,
3962
4369
  screenshotHash: hashFile2(absoluteScreenshotPath),
3963
- status: "captured"
4370
+ status: "captured",
4371
+ runtime
3964
4372
  });
3965
4373
  } catch (error) {
3966
4374
  const message = error.message;
3967
- browserFindings.push(`${route}: ${message}`);
4375
+ routeFailures.push(`${route}: ${message}`);
3968
4376
  visualRoutes.push({
3969
4377
  route,
3970
4378
  url,
@@ -3973,10 +4381,15 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3973
4381
  status: "failed",
3974
4382
  error: message
3975
4383
  });
4384
+ } finally {
4385
+ try {
4386
+ await page?.close?.();
4387
+ } catch {
4388
+ }
3976
4389
  }
3977
4390
  }
3978
4391
  } catch (error) {
3979
- browserFindings.push(error.message);
4392
+ routeFailures.push(error.message);
3980
4393
  } finally {
3981
4394
  if (browser) await browser.close();
3982
4395
  }
@@ -3990,30 +4403,68 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
3990
4403
  const visualManifestPath = join5(projectRoot, ".decantr", "evidence", "visual-manifest.json");
3991
4404
  mkdirSync4(dirname3(visualManifestPath), { recursive: true });
3992
4405
  writeFileSync4(visualManifestPath, JSON.stringify(visualManifest, null, 2) + "\n", "utf-8");
3993
- if (browserFindings.length > 0) {
3994
- const finding = createHealthFinding({
3995
- source: "browser",
3996
- category: "Browser Verification",
3997
- severity: options.requireBrowser ? "error" : "warn",
3998
- message: "Browser verification could not render every declared route.",
3999
- evidence: browserFindings.slice(0, 5),
4000
- rule: "browser-route-verification-failed",
4001
- suggestedFix: "Start the app at the provided base URL, fix route render errors, and rerun `decantr health --browser --evidence`.",
4002
- baseId: "route-verification-failed"
4003
- });
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];
4004
4455
  return {
4005
- finding,
4456
+ findings,
4006
4457
  evidence: {
4007
4458
  enabled: true,
4008
4459
  status: "failed",
4009
4460
  baseUrl: options.browserBaseUrl,
4010
4461
  screenshots,
4011
- findings: browserFindings
4462
+ findings: evidenceFindings
4012
4463
  }
4013
4464
  };
4014
4465
  }
4015
4466
  return {
4016
- finding: null,
4467
+ findings: [],
4017
4468
  evidence: {
4018
4469
  enabled: true,
4019
4470
  status: "passed",
@@ -4398,8 +4849,8 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
4398
4849
  options,
4399
4850
  declaredRoutes
4400
4851
  );
4401
- if (browserVerification?.finding && !isDuplicateFinding(seen, browserVerification.finding)) {
4402
- findings.push(browserVerification.finding);
4852
+ for (const browserFinding of browserVerification?.findings ?? []) {
4853
+ if (!isDuplicateFinding(seen, browserFinding)) findings.push(browserFinding);
4403
4854
  }
4404
4855
  const anchoredFindings = anchorProjectHealthFindings(projectRoot, findings);
4405
4856
  const scopedFindings = scopeHealthFindingsToProject(projectRoot, anchoredFindings);
@@ -4561,8 +5012,12 @@ function formatProjectHealthJson(report) {
4561
5012
  `;
4562
5013
  }
4563
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
+ }
4564
5019
  return {
4565
- diagnostics: KNOWN_VERIFICATION_DIAGNOSTICS.map((entry) => ({
5020
+ diagnostics: [...diagnosticsByRule.values()].map((entry) => ({
4566
5021
  code: entry.code,
4567
5022
  family: entry.family,
4568
5023
  rule: entry.rule,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-BY6S6IWG.js";
4
+ } from "./chunk-BDVI3SSP.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cmdHeal,
3
3
  collectCheckIssues
4
- } from "./chunk-GPN2THFD.js";
4
+ } from "./chunk-2GCVVEQC.js";
5
5
  export {
6
6
  cmdHeal,
7
7
  collectCheckIssues
@@ -13,8 +13,8 @@ import {
13
13
  renderProjectHealthCiWorkflow,
14
14
  shouldFailHealth,
15
15
  writeProjectHealthCiWorkflow
16
- } from "./chunk-BY6S6IWG.js";
17
- import "./chunk-GPN2THFD.js";
16
+ } from "./chunk-BDVI3SSP.js";
17
+ import "./chunk-2GCVVEQC.js";
18
18
  export {
19
19
  cmdHealth,
20
20
  collectDesignTokenEvidence,
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./chunk-WSL6ZSTZ.js";
1
+ import "./chunk-33IZQXZH.js";
2
2
  import "./chunk-SIDKK73N.js";
3
- import "./chunk-TXNLMAN5.js";
4
- import "./chunk-BY6S6IWG.js";
5
- import "./chunk-GPN2THFD.js";
3
+ import "./chunk-UGRAHWCG.js";
4
+ import "./chunk-BDVI3SSP.js";
5
+ import "./chunk-2GCVVEQC.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  createWorkspaceHealthReport
3
- } from "./chunk-TXNLMAN5.js";
3
+ } from "./chunk-UGRAHWCG.js";
4
4
  import {
5
5
  createProjectHealthReport
6
- } from "./chunk-BY6S6IWG.js";
6
+ } from "./chunk-BDVI3SSP.js";
7
7
  import {
8
8
  sendStudioHealthRefreshedTelemetry,
9
9
  sendStudioStartedTelemetry
10
- } from "./chunk-GPN2THFD.js";
10
+ } from "./chunk-2GCVVEQC.js";
11
11
 
12
12
  // src/commands/studio.ts
13
13
  import { readFileSync } from "fs";
@@ -7,9 +7,9 @@ import {
7
7
  listWorkspaceProjects,
8
8
  parseWorkspaceArgs,
9
9
  shouldFailWorkspaceHealth
10
- } from "./chunk-TXNLMAN5.js";
11
- import "./chunk-BY6S6IWG.js";
12
- import "./chunk-GPN2THFD.js";
10
+ } from "./chunk-UGRAHWCG.js";
11
+ import "./chunk-BDVI3SSP.js";
12
+ import "./chunk-2GCVVEQC.js";
13
13
  export {
14
14
  cmdWorkspace,
15
15
  createWorkspaceHealthReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "3.1.0",
3
+ "version": "3.4.1",
4
4
  "description": "Decantr CLI - adopt, verify, graph, and govern frontend codebases touched by AI agents",
5
5
  "keywords": [
6
6
  "decantr",
@@ -49,11 +49,11 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "ajv": "^8.20.0",
52
- "@decantr/core": "3.1.0",
53
- "@decantr/registry": "3.1.0",
54
- "@decantr/essence-spec": "3.1.0",
55
- "@decantr/telemetry": "3.1.0",
56
- "@decantr/verifier": "3.1.0"
52
+ "@decantr/core": "3.4.0",
53
+ "@decantr/registry": "3.4.0",
54
+ "@decantr/essence-spec": "3.4.0",
55
+ "@decantr/telemetry": "3.4.0",
56
+ "@decantr/verifier": "3.4.1"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup",