@decantr/cli 2.9.2 → 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.
@@ -10,7 +10,7 @@ import { execFileSync } from "child_process";
10
10
  import { createHash } from "crypto";
11
11
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
12
  import { createRequire } from "module";
13
- import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } 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,
@@ -20,7 +20,7 @@ import {
20
20
 
21
21
  // src/workspace.ts
22
22
  import { existsSync, readdirSync, readFileSync } from "fs";
23
- import { dirname, join, resolve } from "path";
23
+ import { dirname, isAbsolute, join, resolve } from "path";
24
24
  function readPackageJson(dir) {
25
25
  const path = join(dir, "package.json");
26
26
  if (!existsSync(path)) return null;
@@ -92,7 +92,7 @@ function listWorkspaceAppCandidates(workspaceRoot) {
92
92
  function resolveWorkspaceInfo(cwd, projectArg) {
93
93
  const absoluteCwd = resolve(cwd);
94
94
  const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
95
- const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
95
+ const appRoot = projectArg ? resolve(isAbsolute(projectArg) ? "/" : workspaceRoot, projectArg) : absoluteCwd;
96
96
  const appCandidates = listWorkspaceApps(workspaceRoot);
97
97
  const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
98
98
  const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
@@ -333,6 +333,9 @@ function contractAssertionApplies(assertion, metadata) {
333
333
  if (assertion.rule === "tokens-file-present" && metadata.adoptionMode === "contract-only") {
334
334
  return false;
335
335
  }
336
+ if (metadata.adoptionMode === "contract-only" && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
337
+ return false;
338
+ }
336
339
  return true;
337
340
  }
338
341
  function slugify(value) {
@@ -382,15 +385,19 @@ function commandsForFinding(source) {
382
385
  }
383
386
  function commandContextForProject(projectRoot) {
384
387
  const workspaceInfo = resolveWorkspaceInfo(projectRoot);
385
- const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(/\\/g, "/");
386
- const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute(relativeProjectPath) ? relativeProjectPath : null;
388
+ const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(
389
+ /\\/g,
390
+ "/"
391
+ );
392
+ const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute2(relativeProjectPath) ? relativeProjectPath : null;
387
393
  const projectFlag = projectPath ? ` --project ${projectPath}` : "";
388
394
  const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
389
395
  return {
390
396
  projectPath,
391
397
  compilePacksCommand: `decantr registry compile-packs ${essencePath} --write-context`,
392
398
  verifyCommand: `decantr verify${projectFlag}`,
393
- ciCommand: `decantr ci${projectFlag} --fail-on error`
399
+ ciCommand: `decantr ci${projectFlag} --fail-on error`,
400
+ promptCommand: (id) => `decantr health${projectFlag} --prompt ${id}`
394
401
  };
395
402
  }
396
403
  function rewriteHealthCommand(command, context) {
@@ -403,8 +410,14 @@ function rewriteHealthCommand(command, context) {
403
410
  /^decantr init --existing\b/,
404
411
  `decantr init --project ${context.projectPath} --existing`
405
412
  );
406
- rewritten = rewritten.replace(/^decantr analyze\b/, `decantr analyze --project ${context.projectPath}`);
407
- rewritten = rewritten.replace(/^decantr check\b/, `decantr check --project ${context.projectPath}`);
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
+ );
408
421
  rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
409
422
  rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
410
423
  return rewritten;
@@ -509,6 +522,69 @@ function createHealthFinding(input) {
509
522
  remediation
510
523
  };
511
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
+ }
512
588
  function countFindings(findings) {
513
589
  return {
514
590
  errorCount: findings.filter((finding) => finding.severity === "error").length,
@@ -541,7 +617,7 @@ function isDuplicateFinding(existing, finding) {
541
617
  }
542
618
  function resolveOptionalPath(projectRoot, path) {
543
619
  if (!path) return void 0;
544
- return isAbsolute(path) ? path : resolve2(projectRoot, path);
620
+ return isAbsolute2(path) ? path : resolve2(projectRoot, path);
545
621
  }
546
622
  function hasProjectPlaywright(projectRoot) {
547
623
  try {
@@ -758,7 +834,7 @@ function parseDecantrCssTokenNames(projectRoot) {
758
834
  function collectDesignTokenEvidence(projectRoot, designTokensPath) {
759
835
  const resolved = resolveOptionalPath(projectRoot, designTokensPath);
760
836
  if (!resolved) return void 0;
761
- const sourceLabel = isAbsolute(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
837
+ const sourceLabel = isAbsolute2(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
762
838
  if (!existsSync2(resolved)) {
763
839
  return {
764
840
  source: sourceLabel,
@@ -1033,6 +1109,13 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
1033
1109
  }
1034
1110
  const declaredRoutes = collectDeclaredRoutes(audit.essence);
1035
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
+ }
1036
1119
  const browserVerification = await collectBrowserVerification(
1037
1120
  projectRoot,
1038
1121
  options,
@@ -1092,6 +1175,7 @@ function colorForStatus(status) {
1092
1175
  }
1093
1176
  function formatProjectHealthText(report) {
1094
1177
  const color = colorForStatus(report.status);
1178
+ const commandContext = commandContextForProject(report.projectRoot);
1095
1179
  const lines = [
1096
1180
  `${BOLD}Decantr Project Health${RESET}`,
1097
1181
  "",
@@ -1121,7 +1205,7 @@ function formatProjectHealthText(report) {
1121
1205
  if (finding.suggestedFix) {
1122
1206
  lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
1123
1207
  }
1124
- lines.push(` ${DIM}Prompt: decantr health --prompt ${finding.id}${RESET}`);
1208
+ lines.push(` ${DIM}Prompt: ${commandContext.promptCommand(finding.id)}${RESET}`);
1125
1209
  }
1126
1210
  }
1127
1211
  lines.push("");
@@ -1130,6 +1214,7 @@ function formatProjectHealthText(report) {
1130
1214
  `;
1131
1215
  }
1132
1216
  function formatProjectHealthMarkdown(report) {
1217
+ const commandContext = commandContextForProject(report.projectRoot);
1133
1218
  const lines = [
1134
1219
  "# Decantr Project Health",
1135
1220
  "",
@@ -1158,7 +1243,7 @@ function formatProjectHealthMarkdown(report) {
1158
1243
  lines.push("- Evidence:");
1159
1244
  for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
1160
1245
  }
1161
- lines.push(`- Prompt: \`decantr health --prompt ${finding.id}\``);
1246
+ lines.push(`- Prompt: \`${commandContext.promptCommand(finding.id)}\``);
1162
1247
  lines.push("");
1163
1248
  }
1164
1249
  }
@@ -1252,17 +1337,25 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
1252
1337
  }
1253
1338
  const format = resolveFormat(options);
1254
1339
  const failOn = options.failOn ?? "error";
1255
- 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)}
1256
1342
  ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
1257
1343
  const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
1258
1344
  if (options.output) {
1259
- const outputPath = isAbsolute(options.output) ? options.output : join2(projectRoot, options.output);
1345
+ const outputPath = isAbsolute2(options.output) ? options.output : join2(projectRoot, options.output);
1260
1346
  mkdirSync(dirname2(outputPath), { recursive: true });
1261
1347
  writeFileSync(outputPath, payload, "utf-8");
1262
1348
  if (!options.ci) {
1263
1349
  console.log(
1264
1350
  `${GREEN}Wrote Decantr ${options.evidence ? "evidence bundle" : "health report"}:${RESET} ${options.output}`
1265
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
+ }
1266
1359
  }
1267
1360
  } else {
1268
1361
  process.stdout.write(payload);
@@ -10,7 +10,7 @@ import {
10
10
  renderProjectHealthCiWorkflow,
11
11
  shouldFailHealth,
12
12
  writeProjectHealthCiWorkflow
13
- } from "./chunk-DX2UDORT.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-AXMGQ5IB.js";
1
+ import "./chunk-VMNUJOEH.js";
2
2
  import "./chunk-RXF7ZYGK.js";
3
- import "./chunk-R57DMFLF.js";
4
- import "./chunk-DX2UDORT.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-R57DMFLF.js";
3
+ } from "./chunk-ARR3EPS2.js";
4
4
  import {
5
5
  createProjectHealthReport
6
- } from "./chunk-DX2UDORT.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-R57DMFLF.js";
11
- import "./chunk-DX2UDORT.js";
10
+ } from "./chunk-ARR3EPS2.js";
11
+ import "./chunk-XZFKK6V7.js";
12
12
  import "./chunk-34TZXWIF.js";
13
13
  export {
14
14
  cmdWorkspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
4
4
  "description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
5
5
  "keywords": [
6
6
  "decantr",
@@ -48,11 +48,11 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "ajv": "^8.20.0",
51
- "@decantr/core": "2.1.0",
52
51
  "@decantr/essence-spec": "2.0.1",
53
52
  "@decantr/registry": "2.2.0",
53
+ "@decantr/verifier": "2.3.3",
54
54
  "@decantr/telemetry": "2.2.1",
55
- "@decantr/verifier": "2.3.2"
55
+ "@decantr/core": "2.1.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",