@decantr/cli 2.9.1 → 2.9.3

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.
@@ -8,15 +8,105 @@ import {
8
8
  // src/commands/health.ts
9
9
  import { execFileSync } from "child_process";
10
10
  import { createHash } from "crypto";
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
12
  import { createRequire } from "module";
13
- import { dirname, isAbsolute, join, resolve } from "path";
13
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import {
16
16
  auditProject,
17
17
  createContractAssertions,
18
18
  createEvidenceBundle
19
19
  } from "@decantr/verifier";
20
+
21
+ // src/workspace.ts
22
+ import { existsSync, readdirSync, readFileSync } from "fs";
23
+ import { dirname, isAbsolute, join, resolve } from "path";
24
+ function readPackageJson(dir) {
25
+ const path = join(dir, "package.json");
26
+ if (!existsSync(path)) return null;
27
+ try {
28
+ return JSON.parse(readFileSync(path, "utf-8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function hasWorkspaceMarker(dir) {
34
+ if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "turbo.json")) || existsSync(join(dir, "nx.json"))) {
35
+ return true;
36
+ }
37
+ const pkg = readPackageJson(dir);
38
+ return Boolean(pkg?.workspaces);
39
+ }
40
+ function findWorkspaceRoot(startDir) {
41
+ let current = resolve(startDir);
42
+ while (true) {
43
+ if (hasWorkspaceMarker(current)) return current;
44
+ const parent = dirname(current);
45
+ if (parent === current) return null;
46
+ current = parent;
47
+ }
48
+ }
49
+ function looksLikeApp(dir, options = {}) {
50
+ const allowSourceDirs = options.allowSourceDirs ?? true;
51
+ const allowPackageDeps = options.allowPackageDeps ?? true;
52
+ const pkg = readPackageJson(dir);
53
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
54
+ const hasFrontendDependency = Boolean(
55
+ deps.react || deps["react-dom"] || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
56
+ );
57
+ const hasServerOnlyDependency = Boolean(
58
+ deps.hono || deps.express || deps.fastify || deps.koa || deps["@hapi/hapi"]
59
+ );
60
+ if (existsSync(join(dir, "next.config.js")) || existsSync(join(dir, "next.config.ts")) || existsSync(join(dir, "next.config.mjs")) || existsSync(join(dir, "vite.config.ts")) || existsSync(join(dir, "vite.config.js")) || existsSync(join(dir, "angular.json")) || existsSync(join(dir, "svelte.config.js")) || existsSync(join(dir, "svelte.config.ts")) || existsSync(join(dir, "astro.config.mjs"))) {
61
+ return true;
62
+ }
63
+ if (allowSourceDirs && (existsSync(join(dir, "src")) || existsSync(join(dir, "app")) || existsSync(join(dir, "pages")))) {
64
+ if (hasFrontendDependency) return true;
65
+ if (hasServerOnlyDependency) return false;
66
+ return true;
67
+ }
68
+ if (!allowPackageDeps) return false;
69
+ return hasFrontendDependency;
70
+ }
71
+ function listWorkspaceApps(workspaceRoot) {
72
+ const candidates = [];
73
+ for (const base of ["apps", "packages"]) {
74
+ const baseDir = join(workspaceRoot, base);
75
+ if (!existsSync(baseDir)) continue;
76
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
77
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
78
+ const candidate = join(baseDir, entry.name);
79
+ if (looksLikeApp(candidate, {
80
+ allowSourceDirs: base === "apps",
81
+ allowPackageDeps: base === "apps"
82
+ })) {
83
+ candidates.push(`${base}/${entry.name}`);
84
+ }
85
+ }
86
+ }
87
+ return candidates.sort();
88
+ }
89
+ function listWorkspaceAppCandidates(workspaceRoot) {
90
+ return listWorkspaceApps(resolve(workspaceRoot));
91
+ }
92
+ function resolveWorkspaceInfo(cwd, projectArg) {
93
+ const absoluteCwd = resolve(cwd);
94
+ const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
95
+ const appRoot = projectArg ? resolve(isAbsolute(projectArg) ? "/" : workspaceRoot, projectArg) : absoluteCwd;
96
+ const appCandidates = listWorkspaceApps(workspaceRoot);
97
+ const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
98
+ const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
99
+ return {
100
+ cwd: absoluteCwd,
101
+ workspaceRoot,
102
+ appRoot,
103
+ projectScope,
104
+ appCandidates,
105
+ requiresProjectSelection
106
+ };
107
+ }
108
+
109
+ // src/commands/health.ts
20
110
  var BOLD = "\x1B[1m";
21
111
  var DIM = "\x1B[2m";
22
112
  var RESET = "\x1B[0m";
@@ -29,14 +119,14 @@ var DEFAULT_HEALTH_CI_WORKFLOW_PATH = ".github/workflows/decantr-health.yml";
29
119
  var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
30
120
  var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
31
121
  var DEFAULT_HEALTH_CI_CLI_VERSION = "latest";
32
- var __dirname = dirname(fileURLToPath(import.meta.url));
122
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
33
123
  function readProjectMetadata(projectRoot) {
34
- const projectJsonPath = join(projectRoot, ".decantr", "project.json");
35
- if (!existsSync(projectJsonPath)) {
124
+ const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
125
+ if (!existsSync2(projectJsonPath)) {
36
126
  return { workflowMode: null, adoptionMode: null, autoBrownfield: false };
37
127
  }
38
128
  try {
39
- const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
129
+ const data = JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
40
130
  const workflowMode = typeof data.initialized?.workflowMode === "string" ? data.initialized.workflowMode : null;
41
131
  const adoptionMode = typeof data.initialized?.adoptionMode === "string" ? data.initialized.adoptionMode : null;
42
132
  return {
@@ -49,12 +139,12 @@ function readProjectMetadata(projectRoot) {
49
139
  }
50
140
  }
51
141
  function loadHealthTemplate(name) {
52
- const fromDist = join(__dirname, "..", "src", "templates", name);
53
- if (existsSync(fromDist)) return readFileSync(fromDist, "utf-8");
54
- const fromSrc = join(__dirname, "..", "templates", name);
55
- if (existsSync(fromSrc)) return readFileSync(fromSrc, "utf-8");
56
- const fromCommandSrc = join(__dirname, "..", "..", "templates", name);
57
- if (existsSync(fromCommandSrc)) return readFileSync(fromCommandSrc, "utf-8");
142
+ const fromDist = join2(__dirname, "..", "src", "templates", name);
143
+ if (existsSync2(fromDist)) return readFileSync2(fromDist, "utf-8");
144
+ const fromSrc = join2(__dirname, "..", "templates", name);
145
+ if (existsSync2(fromSrc)) return readFileSync2(fromSrc, "utf-8");
146
+ const fromCommandSrc = join2(__dirname, "..", "..", "templates", name);
147
+ if (existsSync2(fromCommandSrc)) return readFileSync2(fromCommandSrc, "utf-8");
58
148
  throw new Error(`Template not found: ${name}`);
59
149
  }
60
150
  function renderTemplate(template, vars) {
@@ -149,14 +239,14 @@ function writeProjectHealthCiWorkflow(projectRoot, options = {}) {
149
239
  const workflowRelativePath = validateWorkflowPath(
150
240
  options.workflowPath || DEFAULT_HEALTH_CI_WORKFLOW_PATH
151
241
  );
152
- const workflowPath = join(projectRoot, workflowRelativePath);
153
- const alreadyExists = existsSync(workflowPath);
242
+ const workflowPath = join2(projectRoot, workflowRelativePath);
243
+ const alreadyExists = existsSync2(workflowPath);
154
244
  if (alreadyExists && !options.force) {
155
245
  throw new Error(
156
246
  `${workflowRelativePath} already exists. Re-run with --force to replace it, or use --workflow-path <file>.`
157
247
  );
158
248
  }
159
- mkdirSync(dirname(workflowPath), { recursive: true });
249
+ mkdirSync(dirname2(workflowPath), { recursive: true });
160
250
  writeFileSync(workflowPath, renderProjectHealthCiWorkflow(options), "utf-8");
161
251
  const projectPath = options.workspace ? void 0 : validateProjectPath(options.projectPath);
162
252
  const result = {
@@ -243,23 +333,26 @@ function contractAssertionApplies(assertion, metadata) {
243
333
  if (assertion.rule === "tokens-file-present" && metadata.adoptionMode === "contract-only") {
244
334
  return false;
245
335
  }
336
+ if (metadata.adoptionMode === "contract-only" && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
337
+ return false;
338
+ }
246
339
  return true;
247
340
  }
248
341
  function slugify(value) {
249
342
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
250
343
  }
251
344
  function hashFile(path) {
252
- if (!existsSync(path)) return null;
345
+ if (!existsSync2(path)) return null;
253
346
  try {
254
- return createHash("sha256").update(readFileSync(path)).digest("hex");
347
+ return createHash("sha256").update(readFileSync2(path)).digest("hex");
255
348
  } catch {
256
349
  return null;
257
350
  }
258
351
  }
259
352
  function readJsonFile(path) {
260
- if (!existsSync(path)) return null;
353
+ if (!existsSync2(path)) return null;
261
354
  try {
262
- return JSON.parse(readFileSync(path, "utf-8"));
355
+ return JSON.parse(readFileSync2(path, "utf-8"));
263
356
  } catch {
264
357
  return null;
265
358
  }
@@ -290,6 +383,90 @@ function commandsForFinding(source) {
290
383
  return ["decantr audit", "decantr health"];
291
384
  }
292
385
  }
386
+ function commandContextForProject(projectRoot) {
387
+ const workspaceInfo = resolveWorkspaceInfo(projectRoot);
388
+ const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(
389
+ /\\/g,
390
+ "/"
391
+ );
392
+ const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute2(relativeProjectPath) ? relativeProjectPath : null;
393
+ const projectFlag = projectPath ? ` --project ${projectPath}` : "";
394
+ const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
395
+ return {
396
+ projectPath,
397
+ compilePacksCommand: `decantr registry compile-packs ${essencePath} --write-context`,
398
+ verifyCommand: `decantr verify${projectFlag}`,
399
+ ciCommand: `decantr ci${projectFlag} --fail-on error`,
400
+ promptCommand: (id) => `decantr health${projectFlag} --prompt ${id}`
401
+ };
402
+ }
403
+ function rewriteHealthCommand(command, context) {
404
+ let rewritten = command.replace(
405
+ /decantr registry compile-packs decantr\.essence\.json --write-context/g,
406
+ context.compilePacksCommand
407
+ );
408
+ if (!context.projectPath) return rewritten;
409
+ rewritten = rewritten.replace(
410
+ /^decantr init --existing\b/,
411
+ `decantr init --project ${context.projectPath} --existing`
412
+ );
413
+ rewritten = rewritten.replace(
414
+ /^decantr analyze\b/,
415
+ `decantr analyze --project ${context.projectPath}`
416
+ );
417
+ rewritten = rewritten.replace(
418
+ /^decantr check\b/,
419
+ `decantr check --project ${context.projectPath}`
420
+ );
421
+ rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
422
+ rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
423
+ return rewritten;
424
+ }
425
+ function rewriteSuggestedFixForProject(suggestedFix, context) {
426
+ if (!suggestedFix) return suggestedFix;
427
+ return suggestedFix.replace(
428
+ /decantr registry compile-packs decantr\.essence\.json --write-context/g,
429
+ context.compilePacksCommand
430
+ );
431
+ }
432
+ function commandsForProjectFinding(finding, context) {
433
+ const isPackHydrationFinding = finding.source === "pack" || /pack-manifest|review-pack|compile-packs/i.test(
434
+ `${finding.id} ${finding.rule ?? ""} ${finding.suggestedFix ?? ""}`
435
+ );
436
+ if (isPackHydrationFinding) {
437
+ return [context.compilePacksCommand, context.verifyCommand];
438
+ }
439
+ return [
440
+ ...new Set(
441
+ finding.remediation.commands.map((command) => rewriteHealthCommand(command, context))
442
+ )
443
+ ];
444
+ }
445
+ function scopeHealthFindingsToProject(projectRoot, findings) {
446
+ const context = commandContextForProject(projectRoot);
447
+ return findings.map((finding) => {
448
+ const suggestedFix = rewriteSuggestedFixForProject(finding.suggestedFix, context);
449
+ const commands = commandsForProjectFinding(finding, context);
450
+ return {
451
+ ...finding,
452
+ suggestedFix,
453
+ remediation: {
454
+ summary: suggestedFix || finding.remediation.summary,
455
+ commands,
456
+ prompt: buildRemediationPrompt({
457
+ id: finding.id,
458
+ source: finding.source,
459
+ category: finding.category,
460
+ severity: finding.severity,
461
+ message: finding.message,
462
+ evidence: finding.evidence,
463
+ suggestedFix,
464
+ commands
465
+ })
466
+ }
467
+ };
468
+ });
469
+ }
293
470
  function buildRemediationPrompt(input) {
294
471
  return [
295
472
  "You are fixing one Decantr Project Health finding in this local workspace.",
@@ -345,6 +522,69 @@ function createHealthFinding(input) {
345
522
  remediation
346
523
  };
347
524
  }
525
+ function collectContractPackConsistencyFindings(projectRoot, essence, manifest) {
526
+ if (!essence || typeof essence !== "object") return [];
527
+ const record = essence;
528
+ const blueprint = record.blueprint;
529
+ if (!blueprint || typeof blueprint !== "object") return [];
530
+ const bp = blueprint;
531
+ const routes = bp.routes && typeof bp.routes === "object" && !Array.isArray(bp.routes) ? bp.routes : {};
532
+ const routeTargets = new Set(
533
+ Object.values(routes).filter(
534
+ (entry) => Boolean(entry) && typeof entry === "object"
535
+ ).map((entry) => `${String(entry.section ?? "")}/${String(entry.page ?? "")}`)
536
+ );
537
+ const pages = [];
538
+ for (const section of Array.isArray(bp.sections) ? bp.sections : []) {
539
+ if (!section || typeof section !== "object") continue;
540
+ const sectionRecord = section;
541
+ const sectionId = typeof sectionRecord.id === "string" ? sectionRecord.id : "unknown";
542
+ for (const page of Array.isArray(sectionRecord.pages) ? sectionRecord.pages : []) {
543
+ if (!page || typeof page !== "object") continue;
544
+ const pageRecord = page;
545
+ const pageId = typeof pageRecord.id === "string" ? pageRecord.id : "unknown";
546
+ const route = typeof pageRecord.route === "string" ? pageRecord.route : null;
547
+ pages.push({ section: sectionId, page: pageId, route });
548
+ }
549
+ }
550
+ const findings = [];
551
+ const routeLess = pages.filter(
552
+ (page) => !page.route && !routeTargets.has(`${page.section}/${page.page}`)
553
+ );
554
+ if (routeLess.length > 0) {
555
+ findings.push(
556
+ createHealthFinding({
557
+ source: "assertion",
558
+ category: "Contract Route Topology",
559
+ severity: "error",
560
+ message: "One or more blueprint pages have no route and cannot be addressed by task-time context.",
561
+ evidence: routeLess.slice(0, 8).map(
562
+ (page) => `${page.section}/${page.page} has no page.route or blueprint.routes entry`
563
+ ),
564
+ rule: "page-route-required",
565
+ suggestedFix: "Add a route for each page or rerun the add-page flow with a route-aware Decantr CLI.",
566
+ baseId: "page-route-required"
567
+ })
568
+ );
569
+ }
570
+ const pagePackCount = manifest && "pages" in manifest && Array.isArray(manifest.pages) ? manifest.pages.length : 0;
571
+ if (manifest && pages.length !== pagePackCount) {
572
+ const context = commandContextForProject(projectRoot);
573
+ findings.push(
574
+ createHealthFinding({
575
+ source: "pack",
576
+ category: "Generated Artifacts",
577
+ severity: "warn",
578
+ message: `Compiled page pack count (${pagePackCount}) does not match the contract page count (${pages.length}).`,
579
+ evidence: ["Page packs should be regenerated after adding, removing, or re-routing pages."],
580
+ rule: "page-pack-count-mismatch",
581
+ suggestedFix: context.compilePacksCommand,
582
+ baseId: "page-pack-count-mismatch"
583
+ })
584
+ );
585
+ }
586
+ return findings;
587
+ }
348
588
  function countFindings(findings) {
349
589
  return {
350
590
  errorCount: findings.filter((finding) => finding.severity === "error").length,
@@ -377,16 +617,16 @@ function isDuplicateFinding(existing, finding) {
377
617
  }
378
618
  function resolveOptionalPath(projectRoot, path) {
379
619
  if (!path) return void 0;
380
- return isAbsolute(path) ? path : resolve(projectRoot, path);
620
+ return isAbsolute2(path) ? path : resolve2(projectRoot, path);
381
621
  }
382
622
  function hasProjectPlaywright(projectRoot) {
383
623
  try {
384
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
624
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
385
625
  requireFromProject.resolve("playwright");
386
626
  return true;
387
627
  } catch {
388
628
  try {
389
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
629
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
390
630
  requireFromProject.resolve("@playwright/test");
391
631
  return true;
392
632
  } catch {
@@ -395,7 +635,7 @@ function hasProjectPlaywright(projectRoot) {
395
635
  }
396
636
  }
397
637
  function loadProjectPlaywright(projectRoot) {
398
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
638
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
399
639
  for (const packageName of ["playwright", "@playwright/test"]) {
400
640
  try {
401
641
  const loaded = requireFromProject(packageName);
@@ -485,7 +725,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
485
725
  const screenshots = [];
486
726
  const browserFindings = [];
487
727
  const visualRoutes = [];
488
- const screenshotDir = join(projectRoot, ".decantr", "evidence", "screenshots");
728
+ const screenshotDir = join2(projectRoot, ".decantr", "evidence", "screenshots");
489
729
  mkdirSync(screenshotDir, { recursive: true });
490
730
  let browser = null;
491
731
  try {
@@ -496,7 +736,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
496
736
  const relativePath = browserScreenshotRelativePath(route);
497
737
  try {
498
738
  await page.goto(url, { waitUntil: "networkidle", timeout: 15e3 });
499
- const absoluteScreenshotPath = join(projectRoot, relativePath);
739
+ const absoluteScreenshotPath = join2(projectRoot, relativePath);
500
740
  await page.screenshot({ path: absoluteScreenshotPath, fullPage: true });
501
741
  screenshots.push(relativePath);
502
742
  visualRoutes.push({
@@ -531,8 +771,8 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
531
771
  baseUrl: options.browserBaseUrl,
532
772
  routes: visualRoutes
533
773
  };
534
- const visualManifestPath = join(projectRoot, ".decantr", "evidence", "visual-manifest.json");
535
- mkdirSync(dirname(visualManifestPath), { recursive: true });
774
+ const visualManifestPath = join2(projectRoot, ".decantr", "evidence", "visual-manifest.json");
775
+ mkdirSync(dirname2(visualManifestPath), { recursive: true });
536
776
  writeFileSync(visualManifestPath, JSON.stringify(visualManifest, null, 2) + "\n", "utf-8");
537
777
  if (browserFindings.length > 0) {
538
778
  const finding = createHealthFinding({
@@ -582,9 +822,9 @@ function flattenDesignTokenKeys(value, prefix = "") {
582
822
  return keys;
583
823
  }
584
824
  function parseDecantrCssTokenNames(projectRoot) {
585
- const tokensPath = join(projectRoot, "src", "styles", "tokens.css");
586
- if (!existsSync(tokensPath)) return [];
587
- const css = readFileSync(tokensPath, "utf-8");
825
+ const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
826
+ if (!existsSync2(tokensPath)) return [];
827
+ const css = readFileSync2(tokensPath, "utf-8");
588
828
  const names = /* @__PURE__ */ new Set();
589
829
  for (const match of css.matchAll(/(--d-[\w-]+)\s*:/g)) {
590
830
  names.add(match[1]);
@@ -594,8 +834,8 @@ function parseDecantrCssTokenNames(projectRoot) {
594
834
  function collectDesignTokenEvidence(projectRoot, designTokensPath) {
595
835
  const resolved = resolveOptionalPath(projectRoot, designTokensPath);
596
836
  if (!resolved) return void 0;
597
- const sourceLabel = isAbsolute(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
598
- if (!existsSync(resolved)) {
837
+ const sourceLabel = isAbsolute2(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
838
+ if (!existsSync2(resolved)) {
599
839
  return {
600
840
  source: sourceLabel,
601
841
  status: "error",
@@ -605,7 +845,7 @@ function collectDesignTokenEvidence(projectRoot, designTokensPath) {
605
845
  };
606
846
  }
607
847
  const decantrTokens = parseDecantrCssTokenNames(projectRoot);
608
- const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
848
+ const parsed = JSON.parse(readFileSync2(resolved, "utf-8"));
609
849
  const designKeys = flattenDesignTokenKeys(parsed);
610
850
  const missing = decantrTokens.filter((token) => {
611
851
  const bare = token.replace(/^--/, "");
@@ -648,19 +888,19 @@ function collectDesignTokenFinding(projectRoot, designTokensPath) {
648
888
  });
649
889
  }
650
890
  function baselinePath(projectRoot) {
651
- return join(projectRoot, ".decantr", "health-baseline.json");
891
+ return join2(projectRoot, ".decantr", "health-baseline.json");
652
892
  }
653
893
  function baselineDiffPath(projectRoot) {
654
- return join(projectRoot, ".decantr", "health-baseline-diff.json");
894
+ return join2(projectRoot, ".decantr", "health-baseline-diff.json");
655
895
  }
656
896
  function screenshotHashes(projectRoot) {
657
897
  const manifest = readJsonFile(
658
- join(projectRoot, ".decantr", "evidence", "visual-manifest.json")
898
+ join2(projectRoot, ".decantr", "evidence", "visual-manifest.json")
659
899
  );
660
900
  if (manifest?.routes) {
661
901
  return manifest.routes.filter((route) => typeof route.screenshot === "string").map((route) => ({
662
902
  path: route.screenshot,
663
- hash: route.screenshotHash ?? hashFile(join(projectRoot, route.screenshot))
903
+ hash: route.screenshotHash ?? hashFile(join2(projectRoot, route.screenshot))
664
904
  }));
665
905
  }
666
906
  return [];
@@ -688,7 +928,7 @@ function changedFilesSinceBaseline(projectRoot) {
688
928
  }
689
929
  function routeImpactsFromChangedFiles(report, changedFiles) {
690
930
  const analysis = readJsonFile(
691
- join(report.projectRoot, ".decantr", "analysis.json")
931
+ join2(report.projectRoot, ".decantr", "analysis.json")
692
932
  );
693
933
  const routeEntries = analysis?.routes?.routes ?? [];
694
934
  const impacted = /* @__PURE__ */ new Set();
@@ -721,7 +961,7 @@ function createHealthBaseline(projectRoot, report) {
721
961
  }
722
962
  function saveHealthBaseline(projectRoot, report) {
723
963
  const path = baselinePath(projectRoot);
724
- mkdirSync(dirname(path), { recursive: true });
964
+ mkdirSync(dirname2(path), { recursive: true });
725
965
  writeFileSync(
726
966
  path,
727
967
  JSON.stringify(createHealthBaseline(projectRoot, report), null, 2) + "\n",
@@ -760,7 +1000,7 @@ function compareHealthBaseline(projectRoot, report) {
760
1000
  }
761
1001
  function saveHealthBaselineComparison(projectRoot, comparison) {
762
1002
  const path = baselineDiffPath(projectRoot);
763
- mkdirSync(dirname(path), { recursive: true });
1003
+ mkdirSync(dirname2(path), { recursive: true });
764
1004
  writeFileSync(path, JSON.stringify(comparison, null, 2) + "\n", "utf-8");
765
1005
  return path;
766
1006
  }
@@ -869,6 +1109,13 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
869
1109
  }
870
1110
  const declaredRoutes = collectDeclaredRoutes(audit.essence);
871
1111
  const manifest = audit.packManifest;
1112
+ for (const consistencyFinding of collectContractPackConsistencyFindings(
1113
+ projectRoot,
1114
+ audit.essence,
1115
+ manifest
1116
+ )) {
1117
+ if (!isDuplicateFinding(seen, consistencyFinding)) findings.push(consistencyFinding);
1118
+ }
872
1119
  const browserVerification = await collectBrowserVerification(
873
1120
  projectRoot,
874
1121
  options,
@@ -877,7 +1124,9 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
877
1124
  if (browserVerification?.finding && !isDuplicateFinding(seen, browserVerification.finding)) {
878
1125
  findings.push(browserVerification.finding);
879
1126
  }
880
- const finalCounts = countFindings(findings);
1127
+ const scopedFindings = scopeHealthFindingsToProject(projectRoot, findings);
1128
+ const finalCounts = countFindings(scopedFindings);
1129
+ const commandContext = commandContextForProject(projectRoot);
881
1130
  return {
882
1131
  $schema: PROJECT_HEALTH_SCHEMA_URL,
883
1132
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -901,7 +1150,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
901
1150
  runtimeChecked: audit.runtimeAudit.routeHintsChecked,
902
1151
  runtimeMatched: audit.runtimeAudit.routeHintsMatched,
903
1152
  runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
904
- issues: routeIssuesFromFindings(findings)
1153
+ issues: routeIssuesFromFindings(scopedFindings)
905
1154
  },
906
1155
  packs: {
907
1156
  manifestPresent: Boolean(manifest),
@@ -913,10 +1162,10 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
913
1162
  generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
914
1163
  },
915
1164
  ci: {
916
- recommendedCommand: "decantr ci --fail-on error",
1165
+ recommendedCommand: commandContext.ciCommand,
917
1166
  failOn: "error"
918
1167
  },
919
- findings
1168
+ findings: scopedFindings
920
1169
  };
921
1170
  }
922
1171
  function colorForStatus(status) {
@@ -926,6 +1175,7 @@ function colorForStatus(status) {
926
1175
  }
927
1176
  function formatProjectHealthText(report) {
928
1177
  const color = colorForStatus(report.status);
1178
+ const commandContext = commandContextForProject(report.projectRoot);
929
1179
  const lines = [
930
1180
  `${BOLD}Decantr Project Health${RESET}`,
931
1181
  "",
@@ -955,7 +1205,7 @@ function formatProjectHealthText(report) {
955
1205
  if (finding.suggestedFix) {
956
1206
  lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
957
1207
  }
958
- lines.push(` ${DIM}Prompt: decantr health --prompt ${finding.id}${RESET}`);
1208
+ lines.push(` ${DIM}Prompt: ${commandContext.promptCommand(finding.id)}${RESET}`);
959
1209
  }
960
1210
  }
961
1211
  lines.push("");
@@ -964,6 +1214,7 @@ function formatProjectHealthText(report) {
964
1214
  `;
965
1215
  }
966
1216
  function formatProjectHealthMarkdown(report) {
1217
+ const commandContext = commandContextForProject(report.projectRoot);
967
1218
  const lines = [
968
1219
  "# Decantr Project Health",
969
1220
  "",
@@ -992,7 +1243,7 @@ function formatProjectHealthMarkdown(report) {
992
1243
  lines.push("- Evidence:");
993
1244
  for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
994
1245
  }
995
- lines.push(`- Prompt: \`decantr health --prompt ${finding.id}\``);
1246
+ lines.push(`- Prompt: \`${commandContext.promptCommand(finding.id)}\``);
996
1247
  lines.push("");
997
1248
  }
998
1249
  }
@@ -1014,7 +1265,7 @@ async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
1014
1265
  report,
1015
1266
  audit,
1016
1267
  assertions,
1017
- workspaceConfigPath: existsSync(join(projectRoot, ".decantr", "workspace.json")) ? join(projectRoot, ".decantr", "workspace.json") : null,
1268
+ workspaceConfigPath: existsSync2(join2(projectRoot, ".decantr", "workspace.json")) ? join2(projectRoot, ".decantr", "workspace.json") : null,
1018
1269
  designTokensPath: resolveOptionalPath(projectRoot, options.designTokensPath) ?? null,
1019
1270
  browser: await browserEvidenceFromOptions(projectRoot, options, report.routes.declared),
1020
1271
  designTokens: collectDesignTokenEvidence(projectRoot, options.designTokensPath)
@@ -1086,17 +1337,25 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
1086
1337
  }
1087
1338
  const format = resolveFormat(options);
1088
1339
  const failOn = options.failOn ?? "error";
1089
- const basePayload = options.evidence ? `${JSON.stringify(await createProjectEvidenceBundle(projectRoot, report, reportOptions), null, 2)}
1340
+ const evidenceBundle = options.evidence ? await createProjectEvidenceBundle(projectRoot, report, reportOptions) : null;
1341
+ const basePayload = options.evidence ? `${JSON.stringify(evidenceBundle, null, 2)}
1090
1342
  ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
1091
1343
  const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
1092
1344
  if (options.output) {
1093
- const outputPath = isAbsolute(options.output) ? options.output : join(projectRoot, options.output);
1094
- mkdirSync(dirname(outputPath), { recursive: true });
1345
+ const outputPath = isAbsolute2(options.output) ? options.output : join2(projectRoot, options.output);
1346
+ mkdirSync(dirname2(outputPath), { recursive: true });
1095
1347
  writeFileSync(outputPath, payload, "utf-8");
1096
1348
  if (!options.ci) {
1097
1349
  console.log(
1098
1350
  `${GREEN}Wrote Decantr ${options.evidence ? "evidence bundle" : "health report"}:${RESET} ${options.output}`
1099
1351
  );
1352
+ if (options.browser && evidenceBundle?.browser?.status === "unavailable") {
1353
+ const reason = evidenceBundle.browser.findings[0] ?? "Playwright is not available to Decantr in this project.";
1354
+ console.log(`${YELLOW}Browser evidence unavailable:${RESET} ${reason}`);
1355
+ console.log(
1356
+ `${DIM}Static evidence was still written. Install Playwright or rerun without --browser if screenshots are not needed.${RESET}`
1357
+ );
1358
+ }
1100
1359
  }
1101
1360
  } else {
1102
1361
  process.stdout.write(payload);
@@ -1236,6 +1495,8 @@ function parseHealthArgs(args) {
1236
1495
  }
1237
1496
 
1238
1497
  export {
1498
+ listWorkspaceAppCandidates,
1499
+ resolveWorkspaceInfo,
1239
1500
  renderProjectHealthCiWorkflow,
1240
1501
  writeProjectHealthCiWorkflow,
1241
1502
  collectDesignTokenEvidence,
@@ -10,7 +10,7 @@ import {
10
10
  renderProjectHealthCiWorkflow,
11
11
  shouldFailHealth,
12
12
  writeProjectHealthCiWorkflow
13
- } from "./chunk-TMOCTDYY.js";
13
+ } from "./chunk-XZFKK6V7.js";
14
14
  import "./chunk-34TZXWIF.js";
15
15
  export {
16
16
  cmdHealth,
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./chunk-6UDJDQPT.js";
1
+ import "./chunk-VMNUJOEH.js";
2
2
  import "./chunk-RXF7ZYGK.js";
3
- import "./chunk-FKM4OQDF.js";
4
- import "./chunk-TMOCTDYY.js";
3
+ import "./chunk-ARR3EPS2.js";
4
+ import "./chunk-XZFKK6V7.js";
5
5
  import "./chunk-34TZXWIF.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createWorkspaceHealthReport
3
- } from "./chunk-FKM4OQDF.js";
3
+ } from "./chunk-ARR3EPS2.js";
4
4
  import {
5
5
  createProjectHealthReport
6
- } from "./chunk-TMOCTDYY.js";
6
+ } from "./chunk-XZFKK6V7.js";
7
7
  import {
8
8
  sendStudioHealthRefreshedTelemetry,
9
9
  sendStudioStartedTelemetry
@@ -7,8 +7,8 @@ import {
7
7
  listWorkspaceProjects,
8
8
  parseWorkspaceArgs,
9
9
  shouldFailWorkspaceHealth
10
- } from "./chunk-FKM4OQDF.js";
11
- import "./chunk-TMOCTDYY.js";
10
+ } from "./chunk-ARR3EPS2.js";
11
+ import "./chunk-XZFKK6V7.js";
12
12
  import "./chunk-34TZXWIF.js";
13
13
  export {
14
14
  cmdWorkspace,