@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.
- package/README.md +10 -5
- package/dist/bin.js +3 -3
- package/dist/{chunk-FKM4OQDF.js → chunk-ARR3EPS2.js} +17 -107
- package/dist/{chunk-6UDJDQPT.js → chunk-VMNUJOEH.js} +794 -328
- package/dist/{chunk-TMOCTDYY.js → chunk-XZFKK6V7.js} +311 -50
- package/dist/{health-Q7XF3I5Z.js → health-MB63O56B.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/{studio-EDQMI6JE.js → studio-6QGXJBVH.js} +2 -2
- package/dist/{workspace-JA2RZI6V.js → workspace-OGFYJA4N.js} +2 -2
- package/package.json +4 -4
|
@@ -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 =
|
|
122
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
33
123
|
function readProjectMetadata(projectRoot) {
|
|
34
|
-
const projectJsonPath =
|
|
35
|
-
if (!
|
|
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(
|
|
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 =
|
|
53
|
-
if (
|
|
54
|
-
const fromSrc =
|
|
55
|
-
if (
|
|
56
|
-
const fromCommandSrc =
|
|
57
|
-
if (
|
|
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 =
|
|
153
|
-
const alreadyExists =
|
|
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(
|
|
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 (!
|
|
345
|
+
if (!existsSync2(path)) return null;
|
|
253
346
|
try {
|
|
254
|
-
return createHash("sha256").update(
|
|
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 (!
|
|
353
|
+
if (!existsSync2(path)) return null;
|
|
261
354
|
try {
|
|
262
|
-
return JSON.parse(
|
|
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
|
|
620
|
+
return isAbsolute2(path) ? path : resolve2(projectRoot, path);
|
|
381
621
|
}
|
|
382
622
|
function hasProjectPlaywright(projectRoot) {
|
|
383
623
|
try {
|
|
384
|
-
const requireFromProject = createRequire(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
535
|
-
mkdirSync(
|
|
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 =
|
|
586
|
-
if (!
|
|
587
|
-
const css =
|
|
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 =
|
|
598
|
-
if (!
|
|
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(
|
|
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
|
|
891
|
+
return join2(projectRoot, ".decantr", "health-baseline.json");
|
|
652
892
|
}
|
|
653
893
|
function baselineDiffPath(projectRoot) {
|
|
654
|
-
return
|
|
894
|
+
return join2(projectRoot, ".decantr", "health-baseline-diff.json");
|
|
655
895
|
}
|
|
656
896
|
function screenshotHashes(projectRoot) {
|
|
657
897
|
const manifest = readJsonFile(
|
|
658
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
1094
|
-
mkdirSync(
|
|
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,
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createWorkspaceHealthReport
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ARR3EPS2.js";
|
|
4
4
|
import {
|
|
5
5
|
createProjectHealthReport
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-ARR3EPS2.js";
|
|
11
|
+
import "./chunk-XZFKK6V7.js";
|
|
12
12
|
import "./chunk-34TZXWIF.js";
|
|
13
13
|
export {
|
|
14
14
|
cmdWorkspace,
|