@decantr/cli 1.7.29 → 1.8.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 +29 -0
- package/dist/bin.js +2 -2
- package/dist/chunk-DONMNPS7.js +466 -0
- package/dist/{heal-YHLXO5QL.js → chunk-RSDCWAHD.js} +26 -5
- package/dist/{chunk-HULA6E2D.js → chunk-USOO77A5.js} +10 -1
- package/dist/{chunk-US6RK5QT.js → chunk-Y45MCRGI.js} +30 -3
- 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 +5 -5
- package/src/templates/DECANTR.md.template +4 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ 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
|
|
63
64
|
- searches the registry and showcase benchmark corpus
|
|
64
65
|
- validates, refreshes, and maintains `decantr.essence.json`
|
|
65
66
|
|
|
@@ -77,10 +78,36 @@ decantr rules apply
|
|
|
77
78
|
decantr magic "AI-native analytics workspace"
|
|
78
79
|
decantr audit
|
|
79
80
|
decantr check
|
|
81
|
+
decantr health --ci --fail-on error
|
|
82
|
+
decantr studio --port 4319 --host 127.0.0.1
|
|
80
83
|
decantr registry summary --namespace @official --json
|
|
81
84
|
decantr showcase verification --json
|
|
82
85
|
```
|
|
83
86
|
|
|
87
|
+
## Project Health And Studio
|
|
88
|
+
|
|
89
|
+
`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.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
decantr health
|
|
93
|
+
decantr health --format json
|
|
94
|
+
decantr health --markdown --output health.md
|
|
95
|
+
decantr health --ci --fail-on error
|
|
96
|
+
decantr health --ci --fail-on warn
|
|
97
|
+
decantr health --prompt <finding-id>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
`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`.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
decantr studio
|
|
106
|
+
decantr studio --port 4319 --host 127.0.0.1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
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.
|
|
110
|
+
|
|
84
111
|
## Greenfield Certification
|
|
85
112
|
|
|
86
113
|
Use the built-in certification harness before releases when you want to prove that representative blueprints still scaffold into runnable starter projects:
|
|
@@ -155,6 +182,8 @@ Recommended read order for AI-assisted scaffolding:
|
|
|
155
182
|
|
|
156
183
|
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
184
|
|
|
185
|
+
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>`.
|
|
186
|
+
|
|
158
187
|
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
188
|
|
|
160
189
|
## 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
|
+
};
|
|
@@ -205,15 +205,22 @@ var YELLOW = "\x1B[33m";
|
|
|
205
205
|
var CYAN = "\x1B[36m";
|
|
206
206
|
var RESET = "\x1B[0m";
|
|
207
207
|
var DIM = "\x1B[2m";
|
|
208
|
-
|
|
208
|
+
function collectCheckIssues(projectRoot = process.cwd(), options = {}) {
|
|
209
209
|
const essencePath = join2(projectRoot, "decantr.essence.json");
|
|
210
210
|
if (!existsSync2(essencePath)) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
return {
|
|
212
|
+
essence: null,
|
|
213
|
+
issues: [
|
|
214
|
+
{
|
|
215
|
+
type: "error",
|
|
216
|
+
rule: "essence-missing",
|
|
217
|
+
message: "No decantr.essence.json found. Run `decantr init` first."
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
missingEssence: true
|
|
221
|
+
};
|
|
214
222
|
}
|
|
215
223
|
const essence = JSON.parse(readFileSync2(essencePath, "utf-8"));
|
|
216
|
-
console.log("Scanning for issues...\n");
|
|
217
224
|
const issues = [];
|
|
218
225
|
const validation = validateEssence(essence);
|
|
219
226
|
if (!validation.valid) {
|
|
@@ -266,6 +273,18 @@ async function cmdHeal(projectRoot = process.cwd(), options = {}) {
|
|
|
266
273
|
});
|
|
267
274
|
}
|
|
268
275
|
}
|
|
276
|
+
return { essence, issues, missingEssence: false };
|
|
277
|
+
}
|
|
278
|
+
async function cmdHeal(projectRoot = process.cwd(), options = {}) {
|
|
279
|
+
const result = collectCheckIssues(projectRoot, options);
|
|
280
|
+
console.log("Scanning for issues...\n");
|
|
281
|
+
if (result.missingEssence) {
|
|
282
|
+
console.error(result.issues[0]?.message ?? "No decantr.essence.json found.");
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const issues = result.issues;
|
|
287
|
+
const essence = result.essence ?? {};
|
|
269
288
|
if (issues.length === 0) {
|
|
270
289
|
console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
|
|
271
290
|
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
@@ -302,6 +321,8 @@ ${CYAN}Telemetry enabled.${RESET} Anonymous guard metrics will be sent on future
|
|
|
302
321
|
sendGuardMetrics(metrics);
|
|
303
322
|
}
|
|
304
323
|
}
|
|
324
|
+
|
|
305
325
|
export {
|
|
326
|
+
collectCheckIssues,
|
|
306
327
|
cmdHeal
|
|
307
328
|
};
|
|
@@ -3756,7 +3756,16 @@ Start implementation from the shell layouts and shared route structure before fi
|
|
|
3756
3756
|
}
|
|
3757
3757
|
briefLines.push(`- **Guard mode:** ${params.guardMode}`);
|
|
3758
3758
|
briefLines.push("");
|
|
3759
|
-
const escDecCell = (s) =>
|
|
3759
|
+
const escDecCell = (s) => {
|
|
3760
|
+
let escaped = "";
|
|
3761
|
+
for (const char of s) {
|
|
3762
|
+
if (char === "\\") escaped += "\\\\";
|
|
3763
|
+
else if (char === "|") escaped += "\\|";
|
|
3764
|
+
else if (char === "\n" || char === "\r") escaped += "<br>";
|
|
3765
|
+
else escaped += char;
|
|
3766
|
+
}
|
|
3767
|
+
return escaped;
|
|
3768
|
+
};
|
|
3760
3769
|
if (params.decoratorDefinitions && Object.keys(params.decoratorDefinitions).length > 0) {
|
|
3761
3770
|
briefLines.push("### Decorator Quick Reference");
|
|
3762
3771
|
briefLines.push(
|
|
@@ -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,8 @@ ${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 studio")} Local health dashboard`);
|
|
6483
6485
|
console.log(` ${cyan3("decantr search")} Search registry`);
|
|
6484
6486
|
console.log(` ${cyan3("decantr get")} Fetch content details`);
|
|
6485
6487
|
console.log(` ${cyan3("decantr validate")} Check essence file`);
|
|
@@ -6961,6 +6963,8 @@ ${BOLD6}Commands:${RESET13}
|
|
|
6961
6963
|
${cyan3("magic")} Greenfield-first intent flow; steers existing apps into analyze + init
|
|
6962
6964
|
${cyan3("init")} Attach Decantr contract/context files to an existing project or empty workspace
|
|
6963
6965
|
${cyan3("status")} Show project status, DNA axioms, and blueprint info
|
|
6966
|
+
${cyan3("health")} Generate a local Project Health report [--json] [--markdown] [--ci]
|
|
6967
|
+
${cyan3("studio")} Open a local Project Health dashboard backed by the same report
|
|
6964
6968
|
${cyan3("sync")} Sync registry content from API
|
|
6965
6969
|
${cyan3("audit")} Audit the project or critique a specific file against compiled packs
|
|
6966
6970
|
${cyan3("migrate")} Migrate v2 essence to v3 format (with .v2.backup.json backup)
|
|
@@ -6997,6 +7001,9 @@ ${BOLD6}Examples:${RESET13}
|
|
|
6997
7001
|
decantr rules preview
|
|
6998
7002
|
decantr rules apply
|
|
6999
7003
|
decantr status
|
|
7004
|
+
decantr health
|
|
7005
|
+
decantr health --ci --fail-on error
|
|
7006
|
+
decantr studio
|
|
7000
7007
|
decantr audit
|
|
7001
7008
|
decantr audit src/pages/HomePage.tsx
|
|
7002
7009
|
decantr migrate
|
|
@@ -7157,7 +7164,7 @@ async function main() {
|
|
|
7157
7164
|
break;
|
|
7158
7165
|
}
|
|
7159
7166
|
case "upgrade": {
|
|
7160
|
-
const { cmdUpgrade } = await import("./upgrade-
|
|
7167
|
+
const { cmdUpgrade } = await import("./upgrade-4NRDVD5N.js");
|
|
7161
7168
|
const applyFlag = args.includes("--apply");
|
|
7162
7169
|
await cmdUpgrade(process.cwd(), { apply: applyFlag });
|
|
7163
7170
|
break;
|
|
@@ -7169,12 +7176,32 @@ async function main() {
|
|
|
7169
7176
|
`${YELLOW9}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET13}`
|
|
7170
7177
|
);
|
|
7171
7178
|
}
|
|
7172
|
-
const { cmdHeal } = await import("./heal-
|
|
7179
|
+
const { cmdHeal } = await import("./heal-5JHGCLDX.js");
|
|
7173
7180
|
const telemetryFlag = args.includes("--telemetry");
|
|
7174
7181
|
const brownfieldFlag = args.includes("--brownfield");
|
|
7175
7182
|
await cmdHeal(process.cwd(), { telemetry: telemetryFlag, brownfield: brownfieldFlag });
|
|
7176
7183
|
break;
|
|
7177
7184
|
}
|
|
7185
|
+
case "health": {
|
|
7186
|
+
try {
|
|
7187
|
+
const { cmdHealth, parseHealthArgs } = await import("./health-VSL4MROO.js");
|
|
7188
|
+
await cmdHealth(process.cwd(), parseHealthArgs(args));
|
|
7189
|
+
} catch (e) {
|
|
7190
|
+
console.error(error3(e.message));
|
|
7191
|
+
process.exitCode = 1;
|
|
7192
|
+
}
|
|
7193
|
+
break;
|
|
7194
|
+
}
|
|
7195
|
+
case "studio": {
|
|
7196
|
+
try {
|
|
7197
|
+
const { cmdStudio, parseStudioArgs } = await import("./studio-BCTWKXFH.js");
|
|
7198
|
+
await cmdStudio(process.cwd(), parseStudioArgs(args));
|
|
7199
|
+
} catch (e) {
|
|
7200
|
+
console.error(error3(e.message));
|
|
7201
|
+
process.exitCode = 1;
|
|
7202
|
+
}
|
|
7203
|
+
break;
|
|
7204
|
+
}
|
|
7178
7205
|
case "migrate": {
|
|
7179
7206
|
await cmdMigrate(process.cwd());
|
|
7180
7207
|
break;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cmdHealth,
|
|
3
|
+
createProjectHealthReport,
|
|
4
|
+
formatProjectHealthJson,
|
|
5
|
+
formatProjectHealthMarkdown,
|
|
6
|
+
formatProjectHealthText,
|
|
7
|
+
parseHealthArgs,
|
|
8
|
+
shouldFailHealth
|
|
9
|
+
} from "./chunk-DONMNPS7.js";
|
|
10
|
+
import "./chunk-RSDCWAHD.js";
|
|
11
|
+
import "./chunk-DI2PLOJ6.js";
|
|
12
|
+
export {
|
|
13
|
+
cmdHealth,
|
|
14
|
+
createProjectHealthReport,
|
|
15
|
+
formatProjectHealthJson,
|
|
16
|
+
formatProjectHealthMarkdown,
|
|
17
|
+
formatProjectHealthText,
|
|
18
|
+
parseHealthArgs,
|
|
19
|
+
shouldFailHealth
|
|
20
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./chunk-
|
|
2
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-Y45MCRGI.js";
|
|
2
|
+
import "./chunk-USOO77A5.js";
|
|
3
3
|
import "./chunk-DI2PLOJ6.js";
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createProjectHealthReport
|
|
3
|
+
} from "./chunk-DONMNPS7.js";
|
|
4
|
+
import "./chunk-RSDCWAHD.js";
|
|
5
|
+
import "./chunk-DI2PLOJ6.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/studio.ts
|
|
8
|
+
import { createServer } from "http";
|
|
9
|
+
var GREEN = "\x1B[32m";
|
|
10
|
+
var CYAN = "\x1B[36m";
|
|
11
|
+
var RESET = "\x1B[0m";
|
|
12
|
+
function sendJson(res, status, value) {
|
|
13
|
+
const body = JSON.stringify(value, null, 2);
|
|
14
|
+
res.writeHead(status, {
|
|
15
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
16
|
+
"Cache-Control": "no-store"
|
|
17
|
+
});
|
|
18
|
+
res.end(body);
|
|
19
|
+
}
|
|
20
|
+
function sendHtml(res, body) {
|
|
21
|
+
res.writeHead(200, {
|
|
22
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
23
|
+
"Cache-Control": "no-store"
|
|
24
|
+
});
|
|
25
|
+
res.end(body);
|
|
26
|
+
}
|
|
27
|
+
function sendNotFound(res) {
|
|
28
|
+
sendJson(res, 404, { error: "not_found" });
|
|
29
|
+
}
|
|
30
|
+
function studioHtml() {
|
|
31
|
+
return `<!doctype html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8">
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
+
<title>Decantr Project Health</title>
|
|
37
|
+
<style>
|
|
38
|
+
:root {
|
|
39
|
+
color-scheme: dark;
|
|
40
|
+
--bg: #101014;
|
|
41
|
+
--panel: #181820;
|
|
42
|
+
--panel-2: #20202a;
|
|
43
|
+
--line: #343442;
|
|
44
|
+
--text: #f5f2eb;
|
|
45
|
+
--muted: #ada7bd;
|
|
46
|
+
--good: #5ee2a0;
|
|
47
|
+
--warn: #f2bd61;
|
|
48
|
+
--bad: #ff6f7d;
|
|
49
|
+
--accent: #8ed3ff;
|
|
50
|
+
--coral: #ff8b6a;
|
|
51
|
+
}
|
|
52
|
+
* { box-sizing: border-box; }
|
|
53
|
+
body {
|
|
54
|
+
margin: 0;
|
|
55
|
+
background: radial-gradient(circle at 20% 0%, rgba(255,139,106,0.16), transparent 26rem), var(--bg);
|
|
56
|
+
color: var(--text);
|
|
57
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
58
|
+
line-height: 1.4;
|
|
59
|
+
}
|
|
60
|
+
button, input { font: inherit; }
|
|
61
|
+
.shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
|
|
62
|
+
header {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
gap: 1rem;
|
|
67
|
+
padding: 1rem 1.25rem;
|
|
68
|
+
border-bottom: 1px solid var(--line);
|
|
69
|
+
background: rgba(16,16,20,0.84);
|
|
70
|
+
backdrop-filter: blur(18px);
|
|
71
|
+
position: sticky;
|
|
72
|
+
top: 0;
|
|
73
|
+
z-index: 2;
|
|
74
|
+
}
|
|
75
|
+
h1 { margin: 0; font-size: 1rem; letter-spacing: 0; }
|
|
76
|
+
.subtle { color: var(--muted); font-size: 0.875rem; }
|
|
77
|
+
.button {
|
|
78
|
+
border: 1px solid var(--line);
|
|
79
|
+
background: var(--panel-2);
|
|
80
|
+
color: var(--text);
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
padding: 0.55rem 0.8rem;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
.button:hover { border-color: var(--accent); }
|
|
86
|
+
main { display: grid; grid-template-columns: 15rem 1fr; min-height: 0; }
|
|
87
|
+
nav {
|
|
88
|
+
border-right: 1px solid var(--line);
|
|
89
|
+
padding: 1rem;
|
|
90
|
+
background: rgba(24,24,32,0.66);
|
|
91
|
+
}
|
|
92
|
+
.tab {
|
|
93
|
+
width: 100%;
|
|
94
|
+
text-align: left;
|
|
95
|
+
margin: 0 0 0.35rem;
|
|
96
|
+
border: 1px solid transparent;
|
|
97
|
+
border-radius: 8px;
|
|
98
|
+
padding: 0.65rem 0.7rem;
|
|
99
|
+
color: var(--muted);
|
|
100
|
+
background: transparent;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
.tab[aria-selected="true"] {
|
|
104
|
+
color: var(--text);
|
|
105
|
+
border-color: var(--line);
|
|
106
|
+
background: var(--panel-2);
|
|
107
|
+
}
|
|
108
|
+
.content { padding: 1rem; overflow: auto; }
|
|
109
|
+
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem; }
|
|
110
|
+
.card {
|
|
111
|
+
border: 1px solid var(--line);
|
|
112
|
+
background: linear-gradient(180deg, var(--panel), rgba(24,24,32,0.74));
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
padding: 1rem;
|
|
115
|
+
}
|
|
116
|
+
.metric { font-size: 1.85rem; font-weight: 720; }
|
|
117
|
+
.label { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; }
|
|
118
|
+
.status-healthy { color: var(--good); }
|
|
119
|
+
.status-warning { color: var(--warn); }
|
|
120
|
+
.status-error { color: var(--bad); }
|
|
121
|
+
table { width: 100%; border-collapse: collapse; }
|
|
122
|
+
th, td { border-bottom: 1px solid var(--line); padding: 0.7rem; text-align: left; vertical-align: top; }
|
|
123
|
+
th { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; }
|
|
124
|
+
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
125
|
+
pre {
|
|
126
|
+
white-space: pre-wrap;
|
|
127
|
+
border: 1px solid var(--line);
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
padding: 1rem;
|
|
130
|
+
background: #0c0c10;
|
|
131
|
+
overflow: auto;
|
|
132
|
+
}
|
|
133
|
+
.pill { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; padding: 0.2rem 0.55rem; }
|
|
134
|
+
.stack { display: grid; gap: 0.75rem; }
|
|
135
|
+
.hidden { display: none; }
|
|
136
|
+
@media (max-width: 760px) {
|
|
137
|
+
main { grid-template-columns: 1fr; }
|
|
138
|
+
nav { border-right: 0; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.35rem; }
|
|
139
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<div class="shell">
|
|
145
|
+
<header>
|
|
146
|
+
<div>
|
|
147
|
+
<h1>Decantr Project Health</h1>
|
|
148
|
+
<div id="project" class="subtle">Loading local contract state...</div>
|
|
149
|
+
</div>
|
|
150
|
+
<button id="refresh" class="button" type="button">Refresh</button>
|
|
151
|
+
</header>
|
|
152
|
+
<main>
|
|
153
|
+
<nav aria-label="Project Health Views">
|
|
154
|
+
<button class="tab" type="button" data-tab="overview" aria-selected="true">Overview</button>
|
|
155
|
+
<button class="tab" type="button" data-tab="routes">Routes</button>
|
|
156
|
+
<button class="tab" type="button" data-tab="drift">Drift</button>
|
|
157
|
+
<button class="tab" type="button" data-tab="findings">Findings</button>
|
|
158
|
+
<button class="tab" type="button" data-tab="remediation">Remediation</button>
|
|
159
|
+
<button class="tab" type="button" data-tab="ci">CI</button>
|
|
160
|
+
<button class="tab" type="button" data-tab="packs">Packs</button>
|
|
161
|
+
</nav>
|
|
162
|
+
<section class="content">
|
|
163
|
+
<div id="overview" class="view stack"></div>
|
|
164
|
+
<div id="routes" class="view stack hidden"></div>
|
|
165
|
+
<div id="drift" class="view stack hidden"></div>
|
|
166
|
+
<div id="findings" class="view stack hidden"></div>
|
|
167
|
+
<div id="remediation" class="view stack hidden"></div>
|
|
168
|
+
<div id="ci" class="view stack hidden"></div>
|
|
169
|
+
<div id="packs" class="view stack hidden"></div>
|
|
170
|
+
</section>
|
|
171
|
+
</main>
|
|
172
|
+
</div>
|
|
173
|
+
<script>
|
|
174
|
+
let report = null;
|
|
175
|
+
const tabs = [...document.querySelectorAll('.tab')];
|
|
176
|
+
const views = [...document.querySelectorAll('.view')];
|
|
177
|
+
function esc(value) {
|
|
178
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
179
|
+
}
|
|
180
|
+
function metric(label, value, cls = '') {
|
|
181
|
+
return '<div class="card"><div class="label">' + esc(label) + '</div><div class="metric ' + cls + '">' + esc(value) + '</div></div>';
|
|
182
|
+
}
|
|
183
|
+
function table(headers, rows) {
|
|
184
|
+
return '<table><thead><tr>' + headers.map((h) => '<th>' + esc(h) + '</th>').join('') + '</tr></thead><tbody>' +
|
|
185
|
+
rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table>';
|
|
186
|
+
}
|
|
187
|
+
function render() {
|
|
188
|
+
if (!report) return;
|
|
189
|
+
document.getElementById('project').textContent = report.projectRoot;
|
|
190
|
+
document.getElementById('overview').innerHTML =
|
|
191
|
+
'<div class="grid">' +
|
|
192
|
+
metric('Status', report.status, 'status-' + report.status) +
|
|
193
|
+
metric('Score', report.score + '/100') +
|
|
194
|
+
metric('Errors', report.summary.errorCount, 'status-error') +
|
|
195
|
+
metric('Warnings', report.summary.warnCount, 'status-warning') +
|
|
196
|
+
'</div><div class="card"><div class="label">Workflow</div><p>' + esc(report.summary.workflowMode || 'unknown') + ' / ' + esc(report.summary.adoptionMode || 'unknown') + '</p><p class="subtle">Generated ' + esc(report.generatedAt) + '</p></div>';
|
|
197
|
+
document.getElementById('routes').innerHTML =
|
|
198
|
+
'<div class="card"><div class="label">Route Coverage</div><p>Declared routes: ' + report.routes.declared.length + ' | runtime checked: ' + report.routes.runtimeChecked.length + ' | matched: ' + report.routes.runtimeMatched + '</p></div>' +
|
|
199
|
+
table(['Declared Route'], report.routes.declared.map((route) => ['<code>' + esc(route) + '</code>'])) +
|
|
200
|
+
(report.routes.issues.length ? '<div class="card"><div class="label">Route Issues</div><ul>' + report.routes.issues.map((issue) => '<li>' + esc(issue) + '</li>').join('') + '</ul></div>' : '');
|
|
201
|
+
const drift = report.findings.filter((finding) => finding.source === 'brownfield' || finding.id.includes('drift'));
|
|
202
|
+
document.getElementById('drift').innerHTML = drift.length
|
|
203
|
+
? table(['Severity', 'Source', 'Message'], drift.map((finding) => [esc(finding.severity), esc(finding.source), esc(finding.message)]))
|
|
204
|
+
: '<div class="card">No drift findings.</div>';
|
|
205
|
+
document.getElementById('findings').innerHTML = report.findings.length
|
|
206
|
+
? table(['Severity', 'Source', 'Finding', 'Prompt'], report.findings.map((finding) => [
|
|
207
|
+
'<span class="pill">' + esc(finding.severity) + '</span>',
|
|
208
|
+
esc(finding.source),
|
|
209
|
+
'<strong>' + esc(finding.id) + '</strong><br><span class="subtle">' + esc(finding.message) + '</span>',
|
|
210
|
+
'<code>decantr health --prompt ' + esc(finding.id) + '</code>'
|
|
211
|
+
]))
|
|
212
|
+
: '<div class="card">No findings. Project is healthy.</div>';
|
|
213
|
+
document.getElementById('remediation').innerHTML = report.findings.length
|
|
214
|
+
? report.findings.map((finding) => '<div class="card"><div class="label">' + esc(finding.id) + '</div><p>' + esc(finding.remediation.summary) + '</p><pre>' + esc(finding.remediation.prompt) + '</pre></div>').join('')
|
|
215
|
+
: '<div class="card">No remediation needed.</div>';
|
|
216
|
+
document.getElementById('ci').innerHTML = '<div class="card"><div class="label">Recommended CI Gate</div><pre>' + esc(report.ci.recommendedCommand) + '</pre></div>';
|
|
217
|
+
document.getElementById('packs').innerHTML =
|
|
218
|
+
'<div class="grid">' +
|
|
219
|
+
metric('Manifest', report.packs.manifestPresent ? 'present' : 'missing') +
|
|
220
|
+
metric('Review', report.packs.reviewPackPresent ? 'present' : 'missing') +
|
|
221
|
+
metric('Sections', report.packs.sectionPackCount) +
|
|
222
|
+
metric('Pages', report.packs.pagePackCount) +
|
|
223
|
+
'</div><div class="card"><div class="label">Generated</div><p>' + esc(report.packs.generatedAt || 'unknown') + '</p></div>';
|
|
224
|
+
}
|
|
225
|
+
async function load(refresh = false) {
|
|
226
|
+
const response = await fetch(refresh ? '/api/refresh' : '/api/health', { method: refresh ? 'POST' : 'GET' });
|
|
227
|
+
report = await response.json();
|
|
228
|
+
render();
|
|
229
|
+
}
|
|
230
|
+
tabs.forEach((tab) => tab.addEventListener('click', () => {
|
|
231
|
+
tabs.forEach((item) => item.setAttribute('aria-selected', String(item === tab)));
|
|
232
|
+
views.forEach((view) => view.classList.toggle('hidden', view.id !== tab.dataset.tab));
|
|
233
|
+
}));
|
|
234
|
+
document.getElementById('refresh').addEventListener('click', () => load(true));
|
|
235
|
+
load().catch((error) => {
|
|
236
|
+
document.getElementById('overview').innerHTML = '<div class="card status-error">Failed to load health report: ' + esc(error.message) + '</div>';
|
|
237
|
+
});
|
|
238
|
+
</script>
|
|
239
|
+
</body>
|
|
240
|
+
</html>`;
|
|
241
|
+
}
|
|
242
|
+
function createStudioRequestHandler(projectRoot) {
|
|
243
|
+
return async function handleStudioRequest(req, res) {
|
|
244
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
245
|
+
try {
|
|
246
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
247
|
+
sendHtml(res, studioHtml());
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
251
|
+
sendJson(res, 200, await createProjectHealthReport(projectRoot));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (req.method === "POST" && url.pathname === "/api/refresh") {
|
|
255
|
+
sendJson(res, 200, await createProjectHealthReport(projectRoot));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
sendNotFound(res);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
sendJson(res, 500, { error: "health_report_failed", message: e.message });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function startStudioServer(projectRoot = process.cwd(), options = {}) {
|
|
265
|
+
const host = options.host ?? "127.0.0.1";
|
|
266
|
+
const port = options.port ?? 4319;
|
|
267
|
+
const server = createServer(createStudioRequestHandler(projectRoot));
|
|
268
|
+
await new Promise((resolve, reject) => {
|
|
269
|
+
server.once("error", reject);
|
|
270
|
+
server.listen(port, host, () => {
|
|
271
|
+
server.off("error", reject);
|
|
272
|
+
resolve();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
const address = server.address();
|
|
276
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
277
|
+
return { server, url: `http://${host}:${actualPort}` };
|
|
278
|
+
}
|
|
279
|
+
async function cmdStudio(projectRoot = process.cwd(), options = {}) {
|
|
280
|
+
const handle = await startStudioServer(projectRoot, options);
|
|
281
|
+
console.log(`${GREEN}Decantr Studio is running.${RESET}`);
|
|
282
|
+
console.log(`${CYAN}${handle.url}${RESET}`);
|
|
283
|
+
console.log("Press Ctrl+C to stop.");
|
|
284
|
+
}
|
|
285
|
+
function parseStudioArgs(args) {
|
|
286
|
+
const options = {};
|
|
287
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
288
|
+
const arg = args[index];
|
|
289
|
+
if (arg === "--host" && args[index + 1]) {
|
|
290
|
+
options.host = args[++index];
|
|
291
|
+
} else if (arg.startsWith("--host=")) {
|
|
292
|
+
options.host = arg.split("=")[1];
|
|
293
|
+
} else if (arg === "--port" && args[index + 1]) {
|
|
294
|
+
options.port = Number.parseInt(args[++index], 10);
|
|
295
|
+
} else if (arg.startsWith("--port=")) {
|
|
296
|
+
options.port = Number.parseInt(arg.split("=")[1], 10);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (options.port !== void 0 && (!Number.isInteger(options.port) || options.port < 0)) {
|
|
300
|
+
throw new Error("Invalid --port value.");
|
|
301
|
+
}
|
|
302
|
+
return options;
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
cmdStudio,
|
|
306
|
+
createStudioRequestHandler,
|
|
307
|
+
parseStudioArgs,
|
|
308
|
+
startStudioServer
|
|
309
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decantr/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Decantr CLI
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
|
|
5
5
|
"author": "Decantr AI",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bugs": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@decantr/telemetry": "0.1.2",
|
|
34
33
|
"@decantr/core": "1.0.6",
|
|
35
34
|
"@decantr/essence-spec": "1.0.7",
|
|
36
|
-
"@decantr/verifier": "1.0
|
|
37
|
-
"@decantr/registry": "1.0.4"
|
|
35
|
+
"@decantr/verifier": "1.1.0",
|
|
36
|
+
"@decantr/registry": "1.0.4",
|
|
37
|
+
"@decantr/telemetry": "0.1.2"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsup",
|
|
@@ -115,12 +115,15 @@ Read `.decantr/context/page-{name}-pack.md` for the most local compiled route co
|
|
|
115
115
|
### Validation
|
|
116
116
|
|
|
117
117
|
Run `decantr check` to detect drift violations while editing and `decantr audit` to audit the whole project contract after implementation.
|
|
118
|
+
Run `decantr health` for the broader Project Health view before handoff, pull requests, or CI. Use `decantr health --prompt <finding-id>` to generate a scoped remediation prompt for a specific issue, and `decantr studio` to inspect local drift, routes, findings, remediation, CI, and pack state in a localhost dashboard.
|
|
118
119
|
Declared command palettes and hotkeys must be implemented, not merely acknowledged.
|
|
119
120
|
|
|
120
121
|
### Quick Commands
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
|
-
decantr status # Project
|
|
124
|
+
decantr status # Project status overview
|
|
125
|
+
decantr health # Local contract health report
|
|
126
|
+
decantr studio # Local health dashboard
|
|
124
127
|
decantr check # Detect drift violations
|
|
125
128
|
decantr get pattern X # Fetch a pattern spec from registry
|
|
126
129
|
decantr get theme X # Fetch theme details and decorators
|