@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 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
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-US6RK5QT.js";
3
- import "./chunk-HULA6E2D.js";
2
+ import "./chunk-Y45MCRGI.js";
3
+ import "./chunk-USOO77A5.js";
4
4
  import "./chunk-DI2PLOJ6.js";
@@ -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
- async function cmdHeal(projectRoot = process.cwd(), options = {}) {
208
+ function collectCheckIssues(projectRoot = process.cwd(), options = {}) {
209
209
  const essencePath = join2(projectRoot, "decantr.essence.json");
210
210
  if (!existsSync2(essencePath)) {
211
- console.error("No decantr.essence.json found. Run `decantr init` first.");
212
- process.exitCode = 1;
213
- return;
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) => s.replace(/\|/g, "\\|");
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-HULA6E2D.js";
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-EV23CKA3.js");
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-YHLXO5QL.js");
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,9 @@
1
+ import {
2
+ cmdHeal,
3
+ collectCheckIssues
4
+ } from "./chunk-RSDCWAHD.js";
5
+ import "./chunk-DI2PLOJ6.js";
6
+ export {
7
+ cmdHeal,
8
+ collectCheckIssues
9
+ };
@@ -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-US6RK5QT.js";
2
- import "./chunk-HULA6E2D.js";
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RegistryClient,
3
3
  refreshDerivedFiles
4
- } from "./chunk-HULA6E2D.js";
4
+ } from "./chunk-USOO77A5.js";
5
5
 
6
6
  // src/commands/upgrade.ts
7
7
  import { existsSync, readFileSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.7.29",
4
- "description": "Decantr CLI scaffold, audit, and maintain Decantr projects from the terminal",
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.6",
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 health
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