@decantr/cli 2.9.2 → 2.9.4
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/README.md +8 -3
- package/dist/bin.js +4 -4
- package/dist/{chunk-DX2UDORT.js → chunk-FACL3NXU.js} +161 -17
- package/dist/{chunk-AXMGQ5IB.js → chunk-GDGZFGMK.js} +728 -319
- package/dist/{chunk-R57DMFLF.js → chunk-VCUFZB45.js} +2 -2
- package/dist/{chunk-34TZXWIF.js → chunk-WONPNSSI.js} +19 -1
- package/dist/{heal-2BDT7TR5.js → heal-ZQHEHBUJ.js} +1 -1
- package/dist/{health-LTDSTNOV.js → health-R7AV5G4V.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{studio-7E2LJS3A.js → studio-2734P63C.js} +3 -3
- package/dist/{workspace-53EIHUXB.js → workspace-DRHTQ2NG.js} +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,9 +21,11 @@ Use `decantr setup` when you are unsure which path applies. It detects whether t
|
|
|
21
21
|
Use `decantr new` for a greenfield workspace in a fresh directory. With a blueprint/archetype it uses the runnable adapter and Decantr CSS; without registry content it creates a contract-only workspace unless you explicitly pass `--adoption=decantr-css`.
|
|
22
22
|
Use `decantr adopt` when you already have an app and want Decantr governance without adopting a blueprint. Brownfield attach is proposal-driven: Decantr inventories the app, writes an observed essence proposal, hydrates hosted execution packs when online, and only applies the contract when you explicitly accept or merge it.
|
|
23
23
|
Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr codify --from-audit` when you want project-owned UI patterns and local rules such as button/card/shell/theme standards to appear in future task context and verification.
|
|
24
|
-
In monorepos, app-scoped commands accept `--project <app-path>`. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`.
|
|
24
|
+
In monorepos, app-scoped commands accept `--project <app-path>`. `setup` shows attach guidance before adoption and the day-two loop after adoption. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`. In contract-only/offline Brownfield, deferred hosted packs are optional context unless a present manifest references missing files.
|
|
25
25
|
Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
|
|
26
26
|
|
|
27
|
+
App-scoped primitives now share the same `--project` posture as the primary workflow commands. From a workspace root, `health`, `status`, `upgrade`, `add`, `remove`, `theme`, `export`, `suggest`, `magic`, `rules`, and `telemetry` target the selected app instead of the root. Task/read paths, local-law summaries, and refresh change summaries are printed as openable workspace paths. Nonexistent project paths fail immediately, and Brownfield adoption refuses component packages unless you intentionally pass `--force-package`.
|
|
28
|
+
|
|
27
29
|
Current starter adapter availability:
|
|
28
30
|
|
|
29
31
|
- `react-vite` is the React + Vite runnable bootstrap adapter
|
|
@@ -128,6 +130,8 @@ decantr list blueprints --blueprint-set featured
|
|
|
128
130
|
decantr list blueprints --blueprint-set certified
|
|
129
131
|
decantr search dashboard --type blueprint --blueprint-set labs
|
|
130
132
|
decantr suggest "recipe feed with infinite scroll" --route /feed --from-code
|
|
133
|
+
decantr suggest --from-code --file app/page.tsx --project apps/web
|
|
134
|
+
decantr suggest "standardize buttons" --project apps/web
|
|
131
135
|
decantr list patterns
|
|
132
136
|
decantr showcase verification --json
|
|
133
137
|
```
|
|
@@ -156,6 +160,7 @@ decantr health
|
|
|
156
160
|
decantr health --format json
|
|
157
161
|
decantr health --markdown --output health.md
|
|
158
162
|
decantr health --prompt <finding-id>
|
|
163
|
+
decantr health --project apps/registry --prompt <finding-id>
|
|
159
164
|
decantr health --evidence --output .decantr/evidence/latest.json
|
|
160
165
|
decantr health --browser --base-url http://localhost:3000 --evidence
|
|
161
166
|
decantr health --save-baseline
|
|
@@ -171,11 +176,11 @@ decantr verify --workspace --changed --since origin/main
|
|
|
171
176
|
decantr export --to figma-tokens
|
|
172
177
|
```
|
|
173
178
|
|
|
174
|
-
Use `--json` for machines and schema validation, `--markdown` for summaries, `--evidence` for the privacy-redacted Evidence Bundle, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. The prompt command prints instructions only; it does not modify source files. `--browser` uses a project-local Playwright install and a supplied base URL to capture local route screenshots under `.decantr/evidence/screenshots/` and write `.decantr/evidence/visual-manifest.json`; missing Playwright becomes a setup finding, not a crash. `--save-baseline` writes `.decantr/health-baseline.json`; `--since-baseline` writes `.decantr/health-baseline-diff.json` with changed files, route impact, finding deltas, screenshot hash drift, and contract drift. `--design-tokens <path>` compares a Tokens Studio/Figma token JSON export against Decantr CSS token names. `decantr ci --fail-on error` fails only when blocking errors exist; `decantr ci --fail-on warn` also fails on warnings.
|
|
179
|
+
Use `--json` for machines and schema validation, `--markdown` for summaries, `--evidence` for the privacy-redacted Evidence Bundle, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. The prompt command prints instructions only; it does not modify source files. In monorepos, prompt commands preserve `--project <path>`, include app-prefixed read targets such as `apps/web/DECANTR.md`, and use root-safe runtime commands such as `pnpm --dir apps/web build` so the finding resolves from the same app that produced it. `--browser` uses a project-local Playwright install and a supplied base URL to capture local route screenshots under `.decantr/evidence/screenshots/` and write `.decantr/evidence/visual-manifest.json`; missing Playwright becomes a visible setup finding/message, not a crash or silent skip. `--save-baseline` writes `.decantr/health-baseline.json`; `--since-baseline` writes `.decantr/health-baseline-diff.json` with changed files, route impact, finding deltas, screenshot hash drift, and contract drift. `--design-tokens <path>` compares a Tokens Studio/Figma token JSON export against Decantr CSS token names. `decantr ci --fail-on error` fails only when blocking errors exist; `decantr ci --fail-on warn` also fails on warnings.
|
|
175
180
|
|
|
176
181
|
`decantr ci init` installs `.github/workflows/decantr-ci.yml` for GitHub Actions. The generated workflow installs dependencies at the workspace root, writes JSON/markdown CI artifacts, gates with `decantr ci`, appends the markdown report to the GitHub step summary, and uploads both files as artifacts. Use `--force` to replace an existing workflow or `--fail-on warn` for stricter repositories. In monorepos, add `--project <path>` from the repository root; dependency install stays at the root while CI evaluates the app contract and uploads app-scoped artifacts. Use `--workspace` to generate an aggregate gate. Use `--provider generic` for Jenkins, Please, Buildkite, GitLab, Azure DevOps, or internal deployment tools. Generated CI uses the pinned local package-manager command and does not depend on `@latest`. Project Health remediation prompts are also monorepo-aware, so missing-pack fixes use `apps/web/decantr.essence.json` and CI recommendations include `--project apps/web`.
|
|
177
182
|
|
|
178
|
-
`decantr workspace` is the monorepo reliability namespace. Before attach, `workspace list` shows app candidates. After attach, it also discovers Decantr projects from `.decantr/workspace.json` or by finding `decantr.essence.json` files. Workspace health runs projects with deterministic ordering, concurrency, per-project timeout, failure isolation, and aggregate JSON, and can limit a run to changed projects:
|
|
183
|
+
`decantr workspace` is the monorepo reliability namespace. Before attach, `workspace list` shows app candidates. After attach, it also discovers Decantr projects from `.decantr/workspace.json` or by finding `decantr.essence.json` files, and it distinguishes "attach another app" from the empty-workspace first attach. Workspace health runs projects with deterministic ordering, concurrency, per-project timeout, failure isolation, and aggregate JSON, and can limit a run to changed projects:
|
|
179
184
|
|
|
180
185
|
```bash
|
|
181
186
|
decantr workspace list
|
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-GDGZFGMK.js";
|
|
3
3
|
import "./chunk-RXF7ZYGK.js";
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
import "./chunk-VCUFZB45.js";
|
|
5
|
+
import "./chunk-FACL3NXU.js";
|
|
6
|
+
import "./chunk-WONPNSSI.js";
|
|
@@ -3,14 +3,14 @@ import {
|
|
|
3
3
|
sendProjectHealthCiFailedTelemetry,
|
|
4
4
|
sendProjectHealthPromptTelemetry,
|
|
5
5
|
sendProjectHealthReportTelemetry
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-WONPNSSI.js";
|
|
7
7
|
|
|
8
8
|
// src/commands/health.ts
|
|
9
9
|
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(
|
|
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) {
|
|
@@ -380,20 +383,67 @@ function commandsForFinding(source) {
|
|
|
380
383
|
return ["decantr audit", "decantr health"];
|
|
381
384
|
}
|
|
382
385
|
}
|
|
386
|
+
function detectPackageManager(root) {
|
|
387
|
+
const pkg = readJsonFile(join2(root, "package.json"));
|
|
388
|
+
const declared = pkg?.packageManager?.split("@")[0];
|
|
389
|
+
if (declared === "pnpm" || declared === "npm" || declared === "yarn" || declared === "bun") {
|
|
390
|
+
return declared;
|
|
391
|
+
}
|
|
392
|
+
if (existsSync2(join2(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
393
|
+
if (existsSync2(join2(root, "package-lock.json"))) return "npm";
|
|
394
|
+
if (existsSync2(join2(root, "yarn.lock"))) return "yarn";
|
|
395
|
+
if (existsSync2(join2(root, "bun.lock")) || existsSync2(join2(root, "bun.lockb"))) return "bun";
|
|
396
|
+
return "unknown";
|
|
397
|
+
}
|
|
398
|
+
function buildCommandForProject(workspaceRoot, projectPath) {
|
|
399
|
+
const packageManager = detectPackageManager(workspaceRoot);
|
|
400
|
+
if (projectPath) {
|
|
401
|
+
switch (packageManager) {
|
|
402
|
+
case "pnpm":
|
|
403
|
+
return `pnpm --dir ${projectPath} build`;
|
|
404
|
+
case "yarn":
|
|
405
|
+
return `yarn --cwd ${projectPath} build`;
|
|
406
|
+
case "bun":
|
|
407
|
+
return `bun --cwd ${projectPath} run build`;
|
|
408
|
+
case "npm":
|
|
409
|
+
return `npm --prefix ${projectPath} run build`;
|
|
410
|
+
default:
|
|
411
|
+
return `cd ${projectPath} && npm run build`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
switch (packageManager) {
|
|
415
|
+
case "pnpm":
|
|
416
|
+
return "pnpm build";
|
|
417
|
+
case "yarn":
|
|
418
|
+
return "yarn build";
|
|
419
|
+
case "bun":
|
|
420
|
+
return "bun run build";
|
|
421
|
+
case "npm":
|
|
422
|
+
return "npm run build";
|
|
423
|
+
default:
|
|
424
|
+
return "npm run build";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
383
427
|
function commandContextForProject(projectRoot) {
|
|
384
428
|
const workspaceInfo = resolveWorkspaceInfo(projectRoot);
|
|
385
|
-
const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(
|
|
386
|
-
|
|
429
|
+
const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(
|
|
430
|
+
/\\/g,
|
|
431
|
+
"/"
|
|
432
|
+
);
|
|
433
|
+
const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute2(relativeProjectPath) ? relativeProjectPath : null;
|
|
387
434
|
const projectFlag = projectPath ? ` --project ${projectPath}` : "";
|
|
388
435
|
const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
|
|
389
436
|
return {
|
|
390
437
|
projectPath,
|
|
438
|
+
buildCommand: buildCommandForProject(workspaceInfo.workspaceRoot, projectPath),
|
|
391
439
|
compilePacksCommand: `decantr registry compile-packs ${essencePath} --write-context`,
|
|
392
440
|
verifyCommand: `decantr verify${projectFlag}`,
|
|
393
|
-
ciCommand: `decantr ci${projectFlag} --fail-on error
|
|
441
|
+
ciCommand: `decantr ci${projectFlag} --fail-on error`,
|
|
442
|
+
promptCommand: (id) => `decantr health${projectFlag} --prompt ${id}`
|
|
394
443
|
};
|
|
395
444
|
}
|
|
396
445
|
function rewriteHealthCommand(command, context) {
|
|
446
|
+
if (command === "npm run build") return context.buildCommand;
|
|
397
447
|
let rewritten = command.replace(
|
|
398
448
|
/decantr registry compile-packs decantr\.essence\.json --write-context/g,
|
|
399
449
|
context.compilePacksCommand
|
|
@@ -403,8 +453,14 @@ function rewriteHealthCommand(command, context) {
|
|
|
403
453
|
/^decantr init --existing\b/,
|
|
404
454
|
`decantr init --project ${context.projectPath} --existing`
|
|
405
455
|
);
|
|
406
|
-
rewritten = rewritten.replace(
|
|
407
|
-
|
|
456
|
+
rewritten = rewritten.replace(
|
|
457
|
+
/^decantr analyze\b/,
|
|
458
|
+
`decantr analyze --project ${context.projectPath}`
|
|
459
|
+
);
|
|
460
|
+
rewritten = rewritten.replace(
|
|
461
|
+
/^decantr check\b/,
|
|
462
|
+
`decantr check --project ${context.projectPath}`
|
|
463
|
+
);
|
|
408
464
|
rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
|
|
409
465
|
rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
|
|
410
466
|
return rewritten;
|
|
@@ -448,17 +504,25 @@ function scopeHealthFindingsToProject(projectRoot, findings) {
|
|
|
448
504
|
message: finding.message,
|
|
449
505
|
evidence: finding.evidence,
|
|
450
506
|
suggestedFix,
|
|
451
|
-
commands
|
|
507
|
+
commands,
|
|
508
|
+
projectPath: context.projectPath
|
|
452
509
|
})
|
|
453
510
|
}
|
|
454
511
|
};
|
|
455
512
|
});
|
|
456
513
|
}
|
|
457
514
|
function buildRemediationPrompt(input) {
|
|
515
|
+
const prefix = input.projectPath ? `${input.projectPath}/` : "";
|
|
516
|
+
const readTargets = [
|
|
517
|
+
`${prefix}DECANTR.md`,
|
|
518
|
+
`${prefix}decantr.essence.json`,
|
|
519
|
+
`${prefix}.decantr/context/scaffold-pack.md`,
|
|
520
|
+
`${prefix}.decantr/context/scaffold.md`
|
|
521
|
+
];
|
|
458
522
|
return [
|
|
459
523
|
"You are fixing one Decantr Project Health finding in this local workspace.",
|
|
460
524
|
"",
|
|
461
|
-
|
|
525
|
+
`Read project-scoped Decantr files if they exist: ${readTargets.map((target) => `\`${target}\``).join(", ")}. For route or page work, read the matching page/section packs before editing.`,
|
|
462
526
|
"",
|
|
463
527
|
`Finding: ${input.id}`,
|
|
464
528
|
`Source: ${input.source}`,
|
|
@@ -509,6 +573,69 @@ function createHealthFinding(input) {
|
|
|
509
573
|
remediation
|
|
510
574
|
};
|
|
511
575
|
}
|
|
576
|
+
function collectContractPackConsistencyFindings(projectRoot, essence, manifest) {
|
|
577
|
+
if (!essence || typeof essence !== "object") return [];
|
|
578
|
+
const record = essence;
|
|
579
|
+
const blueprint = record.blueprint;
|
|
580
|
+
if (!blueprint || typeof blueprint !== "object") return [];
|
|
581
|
+
const bp = blueprint;
|
|
582
|
+
const routes = bp.routes && typeof bp.routes === "object" && !Array.isArray(bp.routes) ? bp.routes : {};
|
|
583
|
+
const routeTargets = new Set(
|
|
584
|
+
Object.values(routes).filter(
|
|
585
|
+
(entry) => Boolean(entry) && typeof entry === "object"
|
|
586
|
+
).map((entry) => `${String(entry.section ?? "")}/${String(entry.page ?? "")}`)
|
|
587
|
+
);
|
|
588
|
+
const pages = [];
|
|
589
|
+
for (const section of Array.isArray(bp.sections) ? bp.sections : []) {
|
|
590
|
+
if (!section || typeof section !== "object") continue;
|
|
591
|
+
const sectionRecord = section;
|
|
592
|
+
const sectionId = typeof sectionRecord.id === "string" ? sectionRecord.id : "unknown";
|
|
593
|
+
for (const page of Array.isArray(sectionRecord.pages) ? sectionRecord.pages : []) {
|
|
594
|
+
if (!page || typeof page !== "object") continue;
|
|
595
|
+
const pageRecord = page;
|
|
596
|
+
const pageId = typeof pageRecord.id === "string" ? pageRecord.id : "unknown";
|
|
597
|
+
const route = typeof pageRecord.route === "string" ? pageRecord.route : null;
|
|
598
|
+
pages.push({ section: sectionId, page: pageId, route });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const findings = [];
|
|
602
|
+
const routeLess = pages.filter(
|
|
603
|
+
(page) => !page.route && !routeTargets.has(`${page.section}/${page.page}`)
|
|
604
|
+
);
|
|
605
|
+
if (routeLess.length > 0) {
|
|
606
|
+
findings.push(
|
|
607
|
+
createHealthFinding({
|
|
608
|
+
source: "assertion",
|
|
609
|
+
category: "Contract Route Topology",
|
|
610
|
+
severity: "error",
|
|
611
|
+
message: "One or more blueprint pages have no route and cannot be addressed by task-time context.",
|
|
612
|
+
evidence: routeLess.slice(0, 8).map(
|
|
613
|
+
(page) => `${page.section}/${page.page} has no page.route or blueprint.routes entry`
|
|
614
|
+
),
|
|
615
|
+
rule: "page-route-required",
|
|
616
|
+
suggestedFix: "Add a route for each page or rerun the add-page flow with a route-aware Decantr CLI.",
|
|
617
|
+
baseId: "page-route-required"
|
|
618
|
+
})
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const pagePackCount = manifest && "pages" in manifest && Array.isArray(manifest.pages) ? manifest.pages.length : 0;
|
|
622
|
+
if (manifest && pages.length !== pagePackCount) {
|
|
623
|
+
const context = commandContextForProject(projectRoot);
|
|
624
|
+
findings.push(
|
|
625
|
+
createHealthFinding({
|
|
626
|
+
source: "pack",
|
|
627
|
+
category: "Generated Artifacts",
|
|
628
|
+
severity: "warn",
|
|
629
|
+
message: `Compiled page pack count (${pagePackCount}) does not match the contract page count (${pages.length}).`,
|
|
630
|
+
evidence: ["Page packs should be regenerated after adding, removing, or re-routing pages."],
|
|
631
|
+
rule: "page-pack-count-mismatch",
|
|
632
|
+
suggestedFix: context.compilePacksCommand,
|
|
633
|
+
baseId: "page-pack-count-mismatch"
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
return findings;
|
|
638
|
+
}
|
|
512
639
|
function countFindings(findings) {
|
|
513
640
|
return {
|
|
514
641
|
errorCount: findings.filter((finding) => finding.severity === "error").length,
|
|
@@ -541,7 +668,7 @@ function isDuplicateFinding(existing, finding) {
|
|
|
541
668
|
}
|
|
542
669
|
function resolveOptionalPath(projectRoot, path) {
|
|
543
670
|
if (!path) return void 0;
|
|
544
|
-
return
|
|
671
|
+
return isAbsolute2(path) ? path : resolve2(projectRoot, path);
|
|
545
672
|
}
|
|
546
673
|
function hasProjectPlaywright(projectRoot) {
|
|
547
674
|
try {
|
|
@@ -758,7 +885,7 @@ function parseDecantrCssTokenNames(projectRoot) {
|
|
|
758
885
|
function collectDesignTokenEvidence(projectRoot, designTokensPath) {
|
|
759
886
|
const resolved = resolveOptionalPath(projectRoot, designTokensPath);
|
|
760
887
|
if (!resolved) return void 0;
|
|
761
|
-
const sourceLabel =
|
|
888
|
+
const sourceLabel = isAbsolute2(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
|
|
762
889
|
if (!existsSync2(resolved)) {
|
|
763
890
|
return {
|
|
764
891
|
source: sourceLabel,
|
|
@@ -1033,6 +1160,13 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
|
|
|
1033
1160
|
}
|
|
1034
1161
|
const declaredRoutes = collectDeclaredRoutes(audit.essence);
|
|
1035
1162
|
const manifest = audit.packManifest;
|
|
1163
|
+
for (const consistencyFinding of collectContractPackConsistencyFindings(
|
|
1164
|
+
projectRoot,
|
|
1165
|
+
audit.essence,
|
|
1166
|
+
manifest
|
|
1167
|
+
)) {
|
|
1168
|
+
if (!isDuplicateFinding(seen, consistencyFinding)) findings.push(consistencyFinding);
|
|
1169
|
+
}
|
|
1036
1170
|
const browserVerification = await collectBrowserVerification(
|
|
1037
1171
|
projectRoot,
|
|
1038
1172
|
options,
|
|
@@ -1092,6 +1226,7 @@ function colorForStatus(status) {
|
|
|
1092
1226
|
}
|
|
1093
1227
|
function formatProjectHealthText(report) {
|
|
1094
1228
|
const color = colorForStatus(report.status);
|
|
1229
|
+
const commandContext = commandContextForProject(report.projectRoot);
|
|
1095
1230
|
const lines = [
|
|
1096
1231
|
`${BOLD}Decantr Project Health${RESET}`,
|
|
1097
1232
|
"",
|
|
@@ -1121,7 +1256,7 @@ function formatProjectHealthText(report) {
|
|
|
1121
1256
|
if (finding.suggestedFix) {
|
|
1122
1257
|
lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
|
|
1123
1258
|
}
|
|
1124
|
-
lines.push(` ${DIM}Prompt:
|
|
1259
|
+
lines.push(` ${DIM}Prompt: ${commandContext.promptCommand(finding.id)}${RESET}`);
|
|
1125
1260
|
}
|
|
1126
1261
|
}
|
|
1127
1262
|
lines.push("");
|
|
@@ -1130,6 +1265,7 @@ function formatProjectHealthText(report) {
|
|
|
1130
1265
|
`;
|
|
1131
1266
|
}
|
|
1132
1267
|
function formatProjectHealthMarkdown(report) {
|
|
1268
|
+
const commandContext = commandContextForProject(report.projectRoot);
|
|
1133
1269
|
const lines = [
|
|
1134
1270
|
"# Decantr Project Health",
|
|
1135
1271
|
"",
|
|
@@ -1158,7 +1294,7 @@ function formatProjectHealthMarkdown(report) {
|
|
|
1158
1294
|
lines.push("- Evidence:");
|
|
1159
1295
|
for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
|
|
1160
1296
|
}
|
|
1161
|
-
lines.push(`- Prompt:
|
|
1297
|
+
lines.push(`- Prompt: \`${commandContext.promptCommand(finding.id)}\``);
|
|
1162
1298
|
lines.push("");
|
|
1163
1299
|
}
|
|
1164
1300
|
}
|
|
@@ -1252,17 +1388,25 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
|
|
|
1252
1388
|
}
|
|
1253
1389
|
const format = resolveFormat(options);
|
|
1254
1390
|
const failOn = options.failOn ?? "error";
|
|
1255
|
-
const
|
|
1391
|
+
const evidenceBundle = options.evidence ? await createProjectEvidenceBundle(projectRoot, report, reportOptions) : null;
|
|
1392
|
+
const basePayload = options.evidence ? `${JSON.stringify(evidenceBundle, null, 2)}
|
|
1256
1393
|
` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
|
|
1257
1394
|
const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
|
|
1258
1395
|
if (options.output) {
|
|
1259
|
-
const outputPath =
|
|
1396
|
+
const outputPath = isAbsolute2(options.output) ? options.output : join2(projectRoot, options.output);
|
|
1260
1397
|
mkdirSync(dirname2(outputPath), { recursive: true });
|
|
1261
1398
|
writeFileSync(outputPath, payload, "utf-8");
|
|
1262
1399
|
if (!options.ci) {
|
|
1263
1400
|
console.log(
|
|
1264
1401
|
`${GREEN}Wrote Decantr ${options.evidence ? "evidence bundle" : "health report"}:${RESET} ${options.output}`
|
|
1265
1402
|
);
|
|
1403
|
+
if (options.browser && evidenceBundle?.browser?.status === "unavailable") {
|
|
1404
|
+
const reason = evidenceBundle.browser.findings[0] ?? "Playwright is not available to Decantr in this project.";
|
|
1405
|
+
console.log(`${YELLOW}Browser evidence unavailable:${RESET} ${reason}`);
|
|
1406
|
+
console.log(
|
|
1407
|
+
`${DIM}Static evidence was still written. Install Playwright or rerun without --browser if screenshots are not needed.${RESET}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1266
1410
|
}
|
|
1267
1411
|
} else {
|
|
1268
1412
|
process.stdout.write(payload);
|