@decantr/cli 1.7.29 → 1.9.0
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 +46 -0
- package/dist/bin.js +2 -2
- package/dist/chunk-DONMNPS7.js +466 -0
- package/dist/{chunk-US6RK5QT.js → chunk-PKJSI6IH.js} +44 -3
- package/dist/{heal-YHLXO5QL.js → chunk-RSDCWAHD.js} +26 -5
- package/dist/{chunk-HULA6E2D.js → chunk-USOO77A5.js} +10 -1
- package/dist/content-health-QQHBR6XG.js +1057 -0
- package/dist/heal-5JHGCLDX.js +9 -0
- package/dist/health-VSL4MROO.js +20 -0
- package/dist/index.js +2 -2
- package/dist/studio-BCTWKXFH.js +309 -0
- package/dist/{upgrade-EV23CKA3.js → upgrade-4NRDVD5N.js} +1 -1
- package/package.json +6 -5
- package/src/templates/DECANTR.md.template +4 -1
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-pr
|
|
|
60
60
|
- supports explicit workflow lanes: greenfield blueprint, greenfield contract-only, brownfield adoption, and hybrid composition
|
|
61
61
|
- generates execution-pack context files for AI coding assistants
|
|
62
62
|
- audits projects against Decantr contracts
|
|
63
|
+
- produces local Project Health reports and a localhost Studio dashboard for end-user drift triage
|
|
64
|
+
- audits local registry content repositories with Content Health reports for schema, reference, and quality coverage
|
|
63
65
|
- searches the registry and showcase benchmark corpus
|
|
64
66
|
- validates, refreshes, and maintains `decantr.essence.json`
|
|
65
67
|
|
|
@@ -77,10 +79,52 @@ decantr rules apply
|
|
|
77
79
|
decantr magic "AI-native analytics workspace"
|
|
78
80
|
decantr audit
|
|
79
81
|
decantr check
|
|
82
|
+
decantr health --ci --fail-on error
|
|
83
|
+
decantr studio --port 4319 --host 127.0.0.1
|
|
84
|
+
decantr content-health --ci --fail-on error
|
|
80
85
|
decantr registry summary --namespace @official --json
|
|
81
86
|
decantr showcase verification --json
|
|
82
87
|
```
|
|
83
88
|
|
|
89
|
+
## Project Health And Studio
|
|
90
|
+
|
|
91
|
+
`decantr health` is the local project observability command. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, and execution-pack files into a `ProjectHealthReport` with a status, score, route summary, pack summary, findings, and AI-ready remediation prompts.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
decantr health
|
|
95
|
+
decantr health --format json
|
|
96
|
+
decantr health --markdown --output health.md
|
|
97
|
+
decantr health --ci --fail-on error
|
|
98
|
+
decantr health --ci --fail-on warn
|
|
99
|
+
decantr health --prompt <finding-id>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use `--json` for machines and schema validation, `--markdown` for CI summaries, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. `--ci --fail-on error` fails only when blocking errors exist; `--ci --fail-on warn` also fails on warnings.
|
|
103
|
+
|
|
104
|
+
`decantr studio` starts a local-only dashboard powered by the same report. It uses Node built-ins only and serves `GET /`, `GET /api/health`, and `POST /api/refresh`.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
decantr studio
|
|
108
|
+
decantr studio --port 4319 --host 127.0.0.1
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Studio is for local triage, not Decantr admin telemetry. The tabs cover Overview, Routes, Drift, Findings, Remediation, CI, and Packs without uploading source code, prompts, file paths, or project data.
|
|
112
|
+
|
|
113
|
+
## Content Health
|
|
114
|
+
|
|
115
|
+
`decantr content-health` is the local supply-chain observability command for registry content repositories such as `decantr-content`. It is separate from Project Health: Project Health checks an end-user app against its Decantr contract, while Content Health checks published content inputs before they flow into the hosted registry.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
decantr content-health
|
|
119
|
+
decantr content-health --json
|
|
120
|
+
decantr content-health --markdown --output content-health.md
|
|
121
|
+
decantr content-health --ci --fail-on error
|
|
122
|
+
decantr content-health --ci --fail-on warn
|
|
123
|
+
decantr content-health --prompt <finding-id>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The report validates local `patterns/`, `themes/`, `blueprints/`, `archetypes/`, and `shells/` against the published registry schemas, checks hard references such as blueprint themes and composed archetypes, summarizes softer generation-coverage gaps such as missing pattern coverage, and emits AI-ready remediation prompts. It does not call the hosted registry by default; use the existing registry drift audits when you need live publish parity.
|
|
127
|
+
|
|
84
128
|
## Greenfield Certification
|
|
85
129
|
|
|
86
130
|
Use the built-in certification harness before releases when you want to prove that representative blueprints still scaffold into runnable starter projects:
|
|
@@ -155,6 +199,8 @@ Recommended read order for AI-assisted scaffolding:
|
|
|
155
199
|
|
|
156
200
|
Treat the compiled execution packs as the source of truth. Use the narrative docs as secondary explanation, start with the shell and route structure first, and run `decantr check` plus `decantr audit` after implementation.
|
|
157
201
|
|
|
202
|
+
For a broader health pass, run `decantr health` after `refresh`, before opening a pull request, or inside CI. Findings include remediation commands and can be turned into focused AI prompts with `decantr health --prompt <finding-id>`.
|
|
203
|
+
|
|
158
204
|
For cold-start harness or certification runs, use only the scaffolded workspace files as the contract. If local scaffold files disagree, stop and report the mismatch rather than relying on repo-global Decantr assumptions.
|
|
159
205
|
|
|
160
206
|
## Related Packages
|
package/dist/bin.js
CHANGED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectCheckIssues
|
|
3
|
+
} from "./chunk-RSDCWAHD.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/health.ts
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
auditProject
|
|
10
|
+
} from "@decantr/verifier";
|
|
11
|
+
var BOLD = "\x1B[1m";
|
|
12
|
+
var DIM = "\x1B[2m";
|
|
13
|
+
var RESET = "\x1B[0m";
|
|
14
|
+
var RED = "\x1B[31m";
|
|
15
|
+
var GREEN = "\x1B[32m";
|
|
16
|
+
var CYAN = "\x1B[36m";
|
|
17
|
+
var YELLOW = "\x1B[33m";
|
|
18
|
+
var PROJECT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v1.json";
|
|
19
|
+
function readProjectMetadata(projectRoot) {
|
|
20
|
+
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
21
|
+
if (!existsSync(projectJsonPath)) {
|
|
22
|
+
return { workflowMode: null, adoptionMode: null, autoBrownfield: false };
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
26
|
+
const workflowMode = typeof data.initialized?.workflowMode === "string" ? data.initialized.workflowMode : null;
|
|
27
|
+
const adoptionMode = typeof data.initialized?.adoptionMode === "string" ? data.initialized.adoptionMode : null;
|
|
28
|
+
return {
|
|
29
|
+
workflowMode,
|
|
30
|
+
adoptionMode,
|
|
31
|
+
autoBrownfield: workflowMode === "brownfield-attach"
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return { workflowMode: null, adoptionMode: null, autoBrownfield: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function collectDeclaredRoutes(essence) {
|
|
38
|
+
if (!essence || typeof essence !== "object") return [];
|
|
39
|
+
const record = essence;
|
|
40
|
+
const blueprint = record.blueprint;
|
|
41
|
+
if (!blueprint || typeof blueprint !== "object") return [];
|
|
42
|
+
const bp = blueprint;
|
|
43
|
+
const routes = /* @__PURE__ */ new Set();
|
|
44
|
+
if (bp.routes && typeof bp.routes === "object" && !Array.isArray(bp.routes)) {
|
|
45
|
+
for (const route of Object.keys(bp.routes)) {
|
|
46
|
+
routes.add(route);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const flatPages = Array.isArray(bp.pages) ? bp.pages : [];
|
|
50
|
+
for (const page of flatPages) {
|
|
51
|
+
if (page && typeof page === "object") {
|
|
52
|
+
const route = page.route;
|
|
53
|
+
if (typeof route === "string") routes.add(route);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const sections = Array.isArray(bp.sections) ? bp.sections : [];
|
|
57
|
+
for (const section of sections) {
|
|
58
|
+
if (!section || typeof section !== "object") continue;
|
|
59
|
+
const pages = section.pages;
|
|
60
|
+
if (!Array.isArray(pages)) continue;
|
|
61
|
+
for (const page of pages) {
|
|
62
|
+
if (page && typeof page === "object") {
|
|
63
|
+
const route = page.route;
|
|
64
|
+
if (typeof route === "string") routes.add(route);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...routes].sort();
|
|
69
|
+
}
|
|
70
|
+
function severityFromCheckIssue(issue) {
|
|
71
|
+
return issue.type === "error" ? "error" : "warn";
|
|
72
|
+
}
|
|
73
|
+
function sourceFromAuditFinding(finding) {
|
|
74
|
+
const category = finding.category.toLowerCase();
|
|
75
|
+
const id = finding.id.toLowerCase();
|
|
76
|
+
const rule = finding.rule?.toLowerCase() ?? "";
|
|
77
|
+
if (category.includes("runtime") || category.includes("document") || category.includes("performance")) {
|
|
78
|
+
return "runtime";
|
|
79
|
+
}
|
|
80
|
+
if (category.includes("pack") || category.includes("review contract")) {
|
|
81
|
+
return "pack";
|
|
82
|
+
}
|
|
83
|
+
if (category.includes("interaction") || id.includes("interaction") || rule.includes("interaction")) {
|
|
84
|
+
return "interaction";
|
|
85
|
+
}
|
|
86
|
+
return "audit";
|
|
87
|
+
}
|
|
88
|
+
function sourceFromCheckIssue(issue) {
|
|
89
|
+
if (issue.rule.startsWith("brownfield-")) return "brownfield";
|
|
90
|
+
if (issue.rule.includes("interaction")) return "interaction";
|
|
91
|
+
return "check";
|
|
92
|
+
}
|
|
93
|
+
function slugify(value) {
|
|
94
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
95
|
+
}
|
|
96
|
+
function commandsForFinding(source) {
|
|
97
|
+
switch (source) {
|
|
98
|
+
case "brownfield":
|
|
99
|
+
return ["decantr analyze", "decantr init --existing --merge-proposal", "decantr health"];
|
|
100
|
+
case "pack":
|
|
101
|
+
return ["decantr refresh", "decantr registry get-pack review --write-context", "decantr health"];
|
|
102
|
+
case "runtime":
|
|
103
|
+
return ["npm run build", "decantr health"];
|
|
104
|
+
case "interaction":
|
|
105
|
+
return ["decantr check --strict", "decantr health"];
|
|
106
|
+
case "check":
|
|
107
|
+
return ["decantr check", "decantr health"];
|
|
108
|
+
default:
|
|
109
|
+
return ["decantr audit", "decantr health"];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function buildRemediationPrompt(input) {
|
|
113
|
+
return [
|
|
114
|
+
"You are fixing one Decantr Project Health finding in this local workspace.",
|
|
115
|
+
"",
|
|
116
|
+
"Read `DECANTR.md`, `decantr.essence.json`, and `.decantr/context/scaffold-pack.md` if they exist. For route or page work, read the matching page/section packs before editing.",
|
|
117
|
+
"",
|
|
118
|
+
`Finding: ${input.id}`,
|
|
119
|
+
`Source: ${input.source}`,
|
|
120
|
+
`Severity: ${input.severity}`,
|
|
121
|
+
`Category: ${input.category}`,
|
|
122
|
+
`Message: ${input.message}`,
|
|
123
|
+
input.evidence.length > 0 ? `Evidence:
|
|
124
|
+
${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
|
|
125
|
+
input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
|
|
126
|
+
"",
|
|
127
|
+
"Make the smallest coherent code or contract change that resolves this finding. Preserve the existing framework, routing, styling system, and Decantr workflow mode unless the finding explicitly requires a contract update.",
|
|
128
|
+
"",
|
|
129
|
+
`After the fix, run:
|
|
130
|
+
${input.commands.map((command) => `- ${command}`).join("\n")}`
|
|
131
|
+
].filter((line) => Boolean(line)).join("\n");
|
|
132
|
+
}
|
|
133
|
+
function createHealthFinding(input) {
|
|
134
|
+
const idBase = input.baseId || input.rule || `${input.category}-${input.message}`;
|
|
135
|
+
const id = `${input.source}-${slugify(idBase)}`;
|
|
136
|
+
const commands = commandsForFinding(input.source);
|
|
137
|
+
const remediation = {
|
|
138
|
+
summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
|
|
139
|
+
commands,
|
|
140
|
+
prompt: buildRemediationPrompt({
|
|
141
|
+
id,
|
|
142
|
+
source: input.source,
|
|
143
|
+
category: input.category,
|
|
144
|
+
severity: input.severity,
|
|
145
|
+
message: input.message,
|
|
146
|
+
evidence: input.evidence ?? [],
|
|
147
|
+
suggestedFix: input.suggestedFix,
|
|
148
|
+
commands
|
|
149
|
+
})
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
source: input.source,
|
|
154
|
+
category: input.category,
|
|
155
|
+
severity: input.severity,
|
|
156
|
+
message: input.message,
|
|
157
|
+
evidence: input.evidence ?? [],
|
|
158
|
+
target: input.target,
|
|
159
|
+
file: input.file,
|
|
160
|
+
rule: input.rule,
|
|
161
|
+
suggestedFix: input.suggestedFix,
|
|
162
|
+
remediation
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function countFindings(findings) {
|
|
166
|
+
return {
|
|
167
|
+
errorCount: findings.filter((finding) => finding.severity === "error").length,
|
|
168
|
+
warnCount: findings.filter((finding) => finding.severity === "warn").length,
|
|
169
|
+
infoCount: findings.filter((finding) => finding.severity === "info").length
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function statusFromCounts(counts) {
|
|
173
|
+
if (counts.errorCount > 0) return "error";
|
|
174
|
+
if (counts.warnCount > 0) return "warning";
|
|
175
|
+
return "healthy";
|
|
176
|
+
}
|
|
177
|
+
function scoreFromCounts(counts) {
|
|
178
|
+
return Math.max(0, Math.min(100, 100 - counts.errorCount * 15 - counts.warnCount * 5 - counts.infoCount));
|
|
179
|
+
}
|
|
180
|
+
function routeIssuesFromFindings(findings) {
|
|
181
|
+
const issues = findings.filter(
|
|
182
|
+
(finding) => finding.category.toLowerCase().includes("route") || finding.rule?.toLowerCase().includes("route") || finding.id.toLowerCase().includes("route")
|
|
183
|
+
).map((finding) => finding.message);
|
|
184
|
+
return [...new Set(issues)];
|
|
185
|
+
}
|
|
186
|
+
function isDuplicateFinding(existing, finding) {
|
|
187
|
+
const key = `${finding.rule ?? finding.id}|${finding.message}`;
|
|
188
|
+
if (existing.has(key)) return true;
|
|
189
|
+
existing.add(key);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
async function createProjectHealthReport(projectRoot = process.cwd()) {
|
|
193
|
+
const metadata = readProjectMetadata(projectRoot);
|
|
194
|
+
const audit = await auditProject(projectRoot);
|
|
195
|
+
const findings = [];
|
|
196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const finding of audit.findings) {
|
|
198
|
+
const healthFinding = createHealthFinding({
|
|
199
|
+
source: sourceFromAuditFinding(finding),
|
|
200
|
+
category: finding.category,
|
|
201
|
+
severity: finding.severity,
|
|
202
|
+
message: finding.message,
|
|
203
|
+
evidence: finding.evidence,
|
|
204
|
+
target: finding.target,
|
|
205
|
+
file: finding.file,
|
|
206
|
+
rule: finding.rule,
|
|
207
|
+
suggestedFix: finding.suggestedFix,
|
|
208
|
+
baseId: finding.id
|
|
209
|
+
});
|
|
210
|
+
if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const check = collectCheckIssues(projectRoot, { brownfield: metadata.autoBrownfield });
|
|
214
|
+
for (const issue of check.issues) {
|
|
215
|
+
const source = sourceFromCheckIssue(issue);
|
|
216
|
+
const healthFinding = createHealthFinding({
|
|
217
|
+
source,
|
|
218
|
+
category: source === "brownfield" ? "Brownfield Drift" : "Contract Check",
|
|
219
|
+
severity: severityFromCheckIssue(issue),
|
|
220
|
+
message: issue.message,
|
|
221
|
+
evidence: [`Rule: ${issue.rule}`],
|
|
222
|
+
rule: issue.rule,
|
|
223
|
+
suggestedFix: issue.suggestion,
|
|
224
|
+
baseId: issue.rule
|
|
225
|
+
});
|
|
226
|
+
if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
const healthFinding = createHealthFinding({
|
|
230
|
+
source: "check",
|
|
231
|
+
category: "Contract Check",
|
|
232
|
+
severity: "error",
|
|
233
|
+
message: `Decantr check could not complete: ${e.message}`,
|
|
234
|
+
evidence: ["The health command could not run the local check pass."],
|
|
235
|
+
rule: "check-failed",
|
|
236
|
+
suggestedFix: "Repair the local Decantr contract and rerun `decantr health`.",
|
|
237
|
+
baseId: "check-failed"
|
|
238
|
+
});
|
|
239
|
+
if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
|
|
240
|
+
}
|
|
241
|
+
if (!audit.valid && findings.every((finding) => finding.severity !== "error")) {
|
|
242
|
+
findings.push(
|
|
243
|
+
createHealthFinding({
|
|
244
|
+
source: "audit",
|
|
245
|
+
category: "Project Contract",
|
|
246
|
+
severity: "error",
|
|
247
|
+
message: "Project audit is not valid.",
|
|
248
|
+
evidence: ["The verifier returned valid=false."],
|
|
249
|
+
rule: "project-audit-invalid",
|
|
250
|
+
suggestedFix: "Resolve blocking audit findings and rerun `decantr health`."
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const counts = countFindings(findings);
|
|
255
|
+
const declaredRoutes = collectDeclaredRoutes(audit.essence);
|
|
256
|
+
const manifest = audit.packManifest;
|
|
257
|
+
return {
|
|
258
|
+
$schema: PROJECT_HEALTH_SCHEMA_URL,
|
|
259
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
+
projectRoot,
|
|
261
|
+
status: statusFromCounts(counts),
|
|
262
|
+
score: scoreFromCounts(counts),
|
|
263
|
+
summary: {
|
|
264
|
+
...counts,
|
|
265
|
+
findingCount: findings.length,
|
|
266
|
+
workflowMode: metadata.workflowMode,
|
|
267
|
+
adoptionMode: metadata.adoptionMode,
|
|
268
|
+
essenceVersion: audit.summary.essenceVersion,
|
|
269
|
+
pageCount: audit.summary.pageCount,
|
|
270
|
+
runtimeAuditChecked: audit.summary.runtimeAuditChecked,
|
|
271
|
+
runtimePassed: audit.summary.runtimePassed,
|
|
272
|
+
packManifestPresent: audit.summary.packManifestPresent,
|
|
273
|
+
reviewPackPresent: audit.summary.reviewPackPresent
|
|
274
|
+
},
|
|
275
|
+
routes: {
|
|
276
|
+
declared: declaredRoutes,
|
|
277
|
+
runtimeChecked: audit.runtimeAudit.routeHintsChecked,
|
|
278
|
+
runtimeMatched: audit.runtimeAudit.routeHintsMatched,
|
|
279
|
+
runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
|
|
280
|
+
issues: routeIssuesFromFindings(findings)
|
|
281
|
+
},
|
|
282
|
+
packs: {
|
|
283
|
+
manifestPresent: Boolean(manifest),
|
|
284
|
+
reviewPackPresent: Boolean(manifest?.review ?? audit.reviewPack),
|
|
285
|
+
scaffoldPackPresent: Boolean(manifest?.scaffold),
|
|
286
|
+
sectionPackCount: manifest?.sections.length ?? 0,
|
|
287
|
+
pagePackCount: manifest?.pages.length ?? 0,
|
|
288
|
+
mutationPackCount: manifest?.mutations?.length ?? 0,
|
|
289
|
+
generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
|
|
290
|
+
},
|
|
291
|
+
ci: {
|
|
292
|
+
recommendedCommand: "decantr health --ci --fail-on error",
|
|
293
|
+
failOn: "error"
|
|
294
|
+
},
|
|
295
|
+
findings
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function colorForStatus(status) {
|
|
299
|
+
if (status === "healthy") return GREEN;
|
|
300
|
+
if (status === "warning") return YELLOW;
|
|
301
|
+
return RED;
|
|
302
|
+
}
|
|
303
|
+
function formatProjectHealthText(report) {
|
|
304
|
+
const color = colorForStatus(report.status);
|
|
305
|
+
const lines = [
|
|
306
|
+
`${BOLD}Decantr Project Health${RESET}`,
|
|
307
|
+
"",
|
|
308
|
+
`${color}${report.status.toUpperCase()}${RESET} score ${report.score}/100`,
|
|
309
|
+
`${DIM}${report.projectRoot}${RESET}`,
|
|
310
|
+
"",
|
|
311
|
+
`${BOLD}Summary:${RESET}`,
|
|
312
|
+
` Findings: ${report.summary.errorCount} error(s), ${report.summary.warnCount} warn(s), ${report.summary.infoCount} info`,
|
|
313
|
+
` Essence: ${report.summary.essenceVersion ?? "missing"} | pages ${report.summary.pageCount}`,
|
|
314
|
+
` Workflow: ${report.summary.workflowMode ?? "unknown"} | adoption ${report.summary.adoptionMode ?? "unknown"}`,
|
|
315
|
+
` Runtime audit: ${report.summary.runtimeAuditChecked ? report.summary.runtimePassed ? "passed" : "failed" : "not checked"}`,
|
|
316
|
+
` Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"} | review ${report.packs.reviewPackPresent ? "present" : "missing"} | pages ${report.packs.pagePackCount}`,
|
|
317
|
+
"",
|
|
318
|
+
`${BOLD}Findings:${RESET}`
|
|
319
|
+
];
|
|
320
|
+
if (report.findings.length === 0) {
|
|
321
|
+
lines.push(` ${GREEN}No findings. Project is healthy.${RESET}`);
|
|
322
|
+
} else {
|
|
323
|
+
for (const finding of report.findings) {
|
|
324
|
+
const findingColor = finding.severity === "error" ? RED : finding.severity === "warn" ? YELLOW : CYAN;
|
|
325
|
+
lines.push(
|
|
326
|
+
` ${findingColor}[${finding.severity.toUpperCase()}]${RESET} ${finding.id}: ${finding.message}`
|
|
327
|
+
);
|
|
328
|
+
if (finding.evidence.length > 0) {
|
|
329
|
+
lines.push(` ${DIM}${finding.evidence[0]}${RESET}`);
|
|
330
|
+
}
|
|
331
|
+
if (finding.suggestedFix) {
|
|
332
|
+
lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
|
|
333
|
+
}
|
|
334
|
+
lines.push(` ${DIM}Prompt: decantr health --prompt ${finding.id}${RESET}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push(`${BOLD}CI:${RESET} ${report.ci.recommendedCommand}`);
|
|
339
|
+
return `${lines.join("\n")}
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
function formatProjectHealthMarkdown(report) {
|
|
343
|
+
const lines = [
|
|
344
|
+
"# Decantr Project Health",
|
|
345
|
+
"",
|
|
346
|
+
`- Status: **${report.status}**`,
|
|
347
|
+
`- Score: **${report.score}/100**`,
|
|
348
|
+
`- Project: \`${report.projectRoot}\``,
|
|
349
|
+
`- Findings: ${report.summary.errorCount} error(s), ${report.summary.warnCount} warn(s), ${report.summary.infoCount} info`,
|
|
350
|
+
`- Runtime audit: ${report.summary.runtimeAuditChecked ? report.summary.runtimePassed ? "passed" : "failed" : "not checked"}`,
|
|
351
|
+
`- Packs: manifest ${report.packs.manifestPresent ? "present" : "missing"}, review ${report.packs.reviewPackPresent ? "present" : "missing"}`,
|
|
352
|
+
"",
|
|
353
|
+
"## Findings",
|
|
354
|
+
""
|
|
355
|
+
];
|
|
356
|
+
if (report.findings.length === 0) {
|
|
357
|
+
lines.push("No findings. Project is healthy.");
|
|
358
|
+
} else {
|
|
359
|
+
for (const finding of report.findings) {
|
|
360
|
+
lines.push(`### ${finding.id}`);
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push(`- Severity: ${finding.severity}`);
|
|
363
|
+
lines.push(`- Source: ${finding.source}`);
|
|
364
|
+
lines.push(`- Category: ${finding.category}`);
|
|
365
|
+
lines.push(`- Message: ${finding.message}`);
|
|
366
|
+
if (finding.suggestedFix) lines.push(`- Fix: ${finding.suggestedFix}`);
|
|
367
|
+
if (finding.evidence.length > 0) {
|
|
368
|
+
lines.push("- Evidence:");
|
|
369
|
+
for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
|
|
370
|
+
}
|
|
371
|
+
lines.push(`- Prompt: \`decantr health --prompt ${finding.id}\``);
|
|
372
|
+
lines.push("");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
lines.push("## CI");
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push(`\`${report.ci.recommendedCommand}\``);
|
|
378
|
+
return `${lines.join("\n")}
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
function formatProjectHealthJson(report) {
|
|
382
|
+
return `${JSON.stringify(report, null, 2)}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
function resolveFormat(options) {
|
|
386
|
+
if (options.json) return "json";
|
|
387
|
+
if (options.markdown) return "markdown";
|
|
388
|
+
return options.format ?? "text";
|
|
389
|
+
}
|
|
390
|
+
function shouldFailHealth(report, failOn) {
|
|
391
|
+
if (failOn === "none") return false;
|
|
392
|
+
if (failOn === "warn") return report.summary.errorCount > 0 || report.summary.warnCount > 0;
|
|
393
|
+
return report.summary.errorCount > 0;
|
|
394
|
+
}
|
|
395
|
+
async function cmdHealth(projectRoot = process.cwd(), options = {}) {
|
|
396
|
+
const report = await createProjectHealthReport(projectRoot);
|
|
397
|
+
if (options.promptId) {
|
|
398
|
+
const finding = report.findings.find((entry) => entry.id === options.promptId);
|
|
399
|
+
if (!finding) {
|
|
400
|
+
console.error(`${RED}No health finding found for id: ${options.promptId}${RESET}`);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
console.log(finding.remediation.prompt);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const format = resolveFormat(options);
|
|
408
|
+
const payload = format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
|
|
409
|
+
if (options.output) {
|
|
410
|
+
writeFileSync(options.output, payload, "utf-8");
|
|
411
|
+
if (!options.ci) {
|
|
412
|
+
console.log(`${GREEN}Wrote Decantr health report:${RESET} ${options.output}`);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
process.stdout.write(payload);
|
|
416
|
+
}
|
|
417
|
+
if (options.ci && shouldFailHealth(report, options.failOn ?? "error")) {
|
|
418
|
+
process.exitCode = 1;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function parseHealthArgs(args) {
|
|
422
|
+
const options = {};
|
|
423
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
424
|
+
const arg = args[index];
|
|
425
|
+
if (arg === "--json") {
|
|
426
|
+
options.json = true;
|
|
427
|
+
} else if (arg === "--markdown") {
|
|
428
|
+
options.markdown = true;
|
|
429
|
+
} else if (arg === "--ci") {
|
|
430
|
+
options.ci = true;
|
|
431
|
+
} else if (arg === "--format" && args[index + 1]) {
|
|
432
|
+
options.format = args[++index];
|
|
433
|
+
} else if (arg.startsWith("--format=")) {
|
|
434
|
+
options.format = arg.split("=")[1];
|
|
435
|
+
} else if (arg === "--output" && args[index + 1]) {
|
|
436
|
+
options.output = args[++index];
|
|
437
|
+
} else if (arg.startsWith("--output=")) {
|
|
438
|
+
options.output = arg.split("=")[1];
|
|
439
|
+
} else if (arg === "--fail-on" && args[index + 1]) {
|
|
440
|
+
options.failOn = args[++index];
|
|
441
|
+
} else if (arg.startsWith("--fail-on=")) {
|
|
442
|
+
options.failOn = arg.split("=")[1];
|
|
443
|
+
} else if (arg === "--prompt" && args[index + 1]) {
|
|
444
|
+
options.promptId = args[++index];
|
|
445
|
+
} else if (arg.startsWith("--prompt=")) {
|
|
446
|
+
options.promptId = arg.split("=")[1];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (options.format && !["text", "json", "markdown"].includes(options.format)) {
|
|
450
|
+
throw new Error("Invalid --format value. Use text, json, or markdown.");
|
|
451
|
+
}
|
|
452
|
+
if (options.failOn && !["error", "warn", "none"].includes(options.failOn)) {
|
|
453
|
+
throw new Error("Invalid --fail-on value. Use error, warn, or none.");
|
|
454
|
+
}
|
|
455
|
+
return options;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export {
|
|
459
|
+
createProjectHealthReport,
|
|
460
|
+
formatProjectHealthText,
|
|
461
|
+
formatProjectHealthMarkdown,
|
|
462
|
+
formatProjectHealthJson,
|
|
463
|
+
shouldFailHealth,
|
|
464
|
+
cmdHealth,
|
|
465
|
+
parseHealthArgs
|
|
466
|
+
};
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
scaffoldProject,
|
|
15
15
|
syncRegistry,
|
|
16
16
|
writeExecutionPackBundleArtifacts
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-USOO77A5.js";
|
|
18
18
|
import {
|
|
19
19
|
buildGuardRegistryContext,
|
|
20
20
|
createDoctrineMap,
|
|
@@ -6480,6 +6480,9 @@ ${YELLOW9}You're offline. Scaffolding Decantr default.${RESET13}`);
|
|
|
6480
6480
|
console.log("");
|
|
6481
6481
|
console.log(" Commands:");
|
|
6482
6482
|
console.log(` ${cyan3("decantr status")} Project health`);
|
|
6483
|
+
console.log(` ${cyan3("decantr health")} Contract health report`);
|
|
6484
|
+
console.log(` ${cyan3("decantr content-health")} Registry content health report`);
|
|
6485
|
+
console.log(` ${cyan3("decantr studio")} Local health dashboard`);
|
|
6483
6486
|
console.log(` ${cyan3("decantr search")} Search registry`);
|
|
6484
6487
|
console.log(` ${cyan3("decantr get")} Fetch content details`);
|
|
6485
6488
|
console.log(` ${cyan3("decantr validate")} Check essence file`);
|
|
@@ -6924,6 +6927,7 @@ ${BOLD6}Usage:${RESET13}
|
|
|
6924
6927
|
decantr registry get-pack <manifest|scaffold|review|section|page|mutation> [id] [--namespace <namespace>] [--json] [--essence <path>] [--write-context]
|
|
6925
6928
|
decantr registry critique-file <file> [--namespace <namespace>] [--json] [--essence <path>] [--treatments <path>]
|
|
6926
6929
|
decantr registry audit-project [--namespace <namespace>] [--json] [--essence <path>] [--dist <path>] [--sources <dir>]
|
|
6930
|
+
decantr content-health [--json] [--markdown] [--ci]
|
|
6927
6931
|
decantr rules preview [--project=<path>]
|
|
6928
6932
|
decantr rules apply [--project=<path>]
|
|
6929
6933
|
decantr validate [path]
|
|
@@ -6961,6 +6965,9 @@ ${BOLD6}Commands:${RESET13}
|
|
|
6961
6965
|
${cyan3("magic")} Greenfield-first intent flow; steers existing apps into analyze + init
|
|
6962
6966
|
${cyan3("init")} Attach Decantr contract/context files to an existing project or empty workspace
|
|
6963
6967
|
${cyan3("status")} Show project status, DNA axioms, and blueprint info
|
|
6968
|
+
${cyan3("health")} Generate a local Project Health report [--json] [--markdown] [--ci]
|
|
6969
|
+
${cyan3("content-health")} Generate a local registry content health report [--json] [--markdown] [--ci]
|
|
6970
|
+
${cyan3("studio")} Open a local Project Health dashboard backed by the same report
|
|
6964
6971
|
${cyan3("sync")} Sync registry content from API
|
|
6965
6972
|
${cyan3("audit")} Audit the project or critique a specific file against compiled packs
|
|
6966
6973
|
${cyan3("migrate")} Migrate v2 essence to v3 format (with .v2.backup.json backup)
|
|
@@ -6997,6 +7004,10 @@ ${BOLD6}Examples:${RESET13}
|
|
|
6997
7004
|
decantr rules preview
|
|
6998
7005
|
decantr rules apply
|
|
6999
7006
|
decantr status
|
|
7007
|
+
decantr health
|
|
7008
|
+
decantr health --ci --fail-on error
|
|
7009
|
+
decantr content-health --ci --fail-on error
|
|
7010
|
+
decantr studio
|
|
7000
7011
|
decantr audit
|
|
7001
7012
|
decantr audit src/pages/HomePage.tsx
|
|
7002
7013
|
decantr migrate
|
|
@@ -7157,7 +7168,7 @@ async function main() {
|
|
|
7157
7168
|
break;
|
|
7158
7169
|
}
|
|
7159
7170
|
case "upgrade": {
|
|
7160
|
-
const { cmdUpgrade } = await import("./upgrade-
|
|
7171
|
+
const { cmdUpgrade } = await import("./upgrade-4NRDVD5N.js");
|
|
7161
7172
|
const applyFlag = args.includes("--apply");
|
|
7162
7173
|
await cmdUpgrade(process.cwd(), { apply: applyFlag });
|
|
7163
7174
|
break;
|
|
@@ -7169,12 +7180,42 @@ async function main() {
|
|
|
7169
7180
|
`${YELLOW9}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET13}`
|
|
7170
7181
|
);
|
|
7171
7182
|
}
|
|
7172
|
-
const { cmdHeal } = await import("./heal-
|
|
7183
|
+
const { cmdHeal } = await import("./heal-5JHGCLDX.js");
|
|
7173
7184
|
const telemetryFlag = args.includes("--telemetry");
|
|
7174
7185
|
const brownfieldFlag = args.includes("--brownfield");
|
|
7175
7186
|
await cmdHeal(process.cwd(), { telemetry: telemetryFlag, brownfield: brownfieldFlag });
|
|
7176
7187
|
break;
|
|
7177
7188
|
}
|
|
7189
|
+
case "health": {
|
|
7190
|
+
try {
|
|
7191
|
+
const { cmdHealth, parseHealthArgs } = await import("./health-VSL4MROO.js");
|
|
7192
|
+
await cmdHealth(process.cwd(), parseHealthArgs(args));
|
|
7193
|
+
} catch (e) {
|
|
7194
|
+
console.error(error3(e.message));
|
|
7195
|
+
process.exitCode = 1;
|
|
7196
|
+
}
|
|
7197
|
+
break;
|
|
7198
|
+
}
|
|
7199
|
+
case "content-health": {
|
|
7200
|
+
try {
|
|
7201
|
+
const { cmdContentHealth, parseContentHealthArgs } = await import("./content-health-QQHBR6XG.js");
|
|
7202
|
+
await cmdContentHealth(process.cwd(), parseContentHealthArgs(args));
|
|
7203
|
+
} catch (e) {
|
|
7204
|
+
console.error(error3(e.message));
|
|
7205
|
+
process.exitCode = 1;
|
|
7206
|
+
}
|
|
7207
|
+
break;
|
|
7208
|
+
}
|
|
7209
|
+
case "studio": {
|
|
7210
|
+
try {
|
|
7211
|
+
const { cmdStudio, parseStudioArgs } = await import("./studio-BCTWKXFH.js");
|
|
7212
|
+
await cmdStudio(process.cwd(), parseStudioArgs(args));
|
|
7213
|
+
} catch (e) {
|
|
7214
|
+
console.error(error3(e.message));
|
|
7215
|
+
process.exitCode = 1;
|
|
7216
|
+
}
|
|
7217
|
+
break;
|
|
7218
|
+
}
|
|
7178
7219
|
case "migrate": {
|
|
7179
7220
|
await cmdMigrate(process.cwd());
|
|
7180
7221
|
break;
|