@decantr/cli 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,6 +61,7 @@ Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-pr
61
61
  - generates execution-pack context files for AI coding assistants
62
62
  - audits projects against Decantr contracts
63
63
  - produces local Project Health reports and a localhost Studio dashboard for end-user drift triage
64
+ - audits local registry content repositories with Content Health reports for schema, reference, and quality coverage
64
65
  - searches the registry and showcase benchmark corpus
65
66
  - validates, refreshes, and maintains `decantr.essence.json`
66
67
 
@@ -80,6 +81,7 @@ decantr audit
80
81
  decantr check
81
82
  decantr health --ci --fail-on error
82
83
  decantr studio --port 4319 --host 127.0.0.1
84
+ decantr content-health --ci --fail-on error
83
85
  decantr registry summary --namespace @official --json
84
86
  decantr showcase verification --json
85
87
  ```
@@ -108,6 +110,21 @@ decantr studio --port 4319 --host 127.0.0.1
108
110
 
109
111
  Studio is for local triage, not Decantr admin telemetry. The tabs cover Overview, Routes, Drift, Findings, Remediation, CI, and Packs without uploading source code, prompts, file paths, or project data.
110
112
 
113
+ ## Content Health
114
+
115
+ `decantr content-health` is the local supply-chain observability command for registry content repositories such as `decantr-content`. It is separate from Project Health: Project Health checks an end-user app against its Decantr contract, while Content Health checks published content inputs before they flow into the hosted registry.
116
+
117
+ ```bash
118
+ decantr content-health
119
+ decantr content-health --json
120
+ decantr content-health --markdown --output content-health.md
121
+ decantr content-health --ci --fail-on error
122
+ decantr content-health --ci --fail-on warn
123
+ decantr content-health --prompt <finding-id>
124
+ ```
125
+
126
+ The report validates local `patterns/`, `themes/`, `blueprints/`, `archetypes/`, and `shells/` against the published registry schemas, checks hard references such as blueprint themes and composed archetypes, summarizes softer generation-coverage gaps such as missing pattern coverage, and emits AI-ready remediation prompts. It does not call the hosted registry by default; use the existing registry drift audits when you need live publish parity.
127
+
111
128
  ## Greenfield Certification
112
129
 
113
130
  Use the built-in certification harness before releases when you want to prove that representative blueprints still scaffold into runnable starter projects:
package/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-Y45MCRGI.js";
2
+ import "./chunk-PKJSI6IH.js";
3
3
  import "./chunk-USOO77A5.js";
4
4
  import "./chunk-DI2PLOJ6.js";
@@ -6481,6 +6481,7 @@ ${YELLOW9}You're offline. Scaffolding Decantr default.${RESET13}`);
6481
6481
  console.log(" Commands:");
6482
6482
  console.log(` ${cyan3("decantr status")} Project health`);
6483
6483
  console.log(` ${cyan3("decantr health")} Contract health report`);
6484
+ console.log(` ${cyan3("decantr content-health")} Registry content health report`);
6484
6485
  console.log(` ${cyan3("decantr studio")} Local health dashboard`);
6485
6486
  console.log(` ${cyan3("decantr search")} Search registry`);
6486
6487
  console.log(` ${cyan3("decantr get")} Fetch content details`);
@@ -6926,6 +6927,7 @@ ${BOLD6}Usage:${RESET13}
6926
6927
  decantr registry get-pack <manifest|scaffold|review|section|page|mutation> [id] [--namespace <namespace>] [--json] [--essence <path>] [--write-context]
6927
6928
  decantr registry critique-file <file> [--namespace <namespace>] [--json] [--essence <path>] [--treatments <path>]
6928
6929
  decantr registry audit-project [--namespace <namespace>] [--json] [--essence <path>] [--dist <path>] [--sources <dir>]
6930
+ decantr content-health [--json] [--markdown] [--ci]
6929
6931
  decantr rules preview [--project=<path>]
6930
6932
  decantr rules apply [--project=<path>]
6931
6933
  decantr validate [path]
@@ -6964,6 +6966,7 @@ ${BOLD6}Commands:${RESET13}
6964
6966
  ${cyan3("init")} Attach Decantr contract/context files to an existing project or empty workspace
6965
6967
  ${cyan3("status")} Show project status, DNA axioms, and blueprint info
6966
6968
  ${cyan3("health")} Generate a local Project Health report [--json] [--markdown] [--ci]
6969
+ ${cyan3("content-health")} Generate a local registry content health report [--json] [--markdown] [--ci]
6967
6970
  ${cyan3("studio")} Open a local Project Health dashboard backed by the same report
6968
6971
  ${cyan3("sync")} Sync registry content from API
6969
6972
  ${cyan3("audit")} Audit the project or critique a specific file against compiled packs
@@ -7003,6 +7006,7 @@ ${BOLD6}Examples:${RESET13}
7003
7006
  decantr status
7004
7007
  decantr health
7005
7008
  decantr health --ci --fail-on error
7009
+ decantr content-health --ci --fail-on error
7006
7010
  decantr studio
7007
7011
  decantr audit
7008
7012
  decantr audit src/pages/HomePage.tsx
@@ -7192,6 +7196,16 @@ async function main() {
7192
7196
  }
7193
7197
  break;
7194
7198
  }
7199
+ case "content-health": {
7200
+ try {
7201
+ const { cmdContentHealth, parseContentHealthArgs } = await import("./content-health-QQHBR6XG.js");
7202
+ await cmdContentHealth(process.cwd(), parseContentHealthArgs(args));
7203
+ } catch (e) {
7204
+ console.error(error3(e.message));
7205
+ process.exitCode = 1;
7206
+ }
7207
+ break;
7208
+ }
7195
7209
  case "studio": {
7196
7210
  try {
7197
7211
  const { cmdStudio, parseStudioArgs } = await import("./studio-BCTWKXFH.js");
@@ -0,0 +1,1057 @@
1
+ // src/commands/content-health.ts
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { createRequire } from "module";
4
+ import { basename, join } from "path";
5
+ import Ajv2020 from "ajv/dist/2020.js";
6
+ var BOLD = "\x1B[1m";
7
+ var DIM = "\x1B[2m";
8
+ var RESET = "\x1B[0m";
9
+ var RED = "\x1B[31m";
10
+ var GREEN = "\x1B[32m";
11
+ var CYAN = "\x1B[36m";
12
+ var YELLOW = "\x1B[33m";
13
+ var CONTENT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/content-health-report.v1.json";
14
+ var DEFAULT_IGNORED_LOCAL_PREFIXES = ["recipefork"];
15
+ var CONTENT_DIRECTORIES = [
16
+ {
17
+ type: "pattern",
18
+ directory: "patterns",
19
+ schemaSpecifier: "@decantr/registry/schema/pattern.v2.json",
20
+ expectedSchema: "https://decantr.ai/schemas/pattern.v2.json"
21
+ },
22
+ {
23
+ type: "theme",
24
+ directory: "themes",
25
+ schemaSpecifier: "@decantr/registry/schema/theme.v1.json",
26
+ expectedSchema: "https://decantr.ai/schemas/theme.v1.json"
27
+ },
28
+ {
29
+ type: "blueprint",
30
+ directory: "blueprints",
31
+ schemaSpecifier: "@decantr/registry/schema/blueprint.v1.json",
32
+ expectedSchema: "https://decantr.ai/schemas/blueprint.v1.json"
33
+ },
34
+ {
35
+ type: "archetype",
36
+ directory: "archetypes",
37
+ schemaSpecifier: "@decantr/registry/schema/archetype.v2.json",
38
+ expectedSchema: "https://decantr.ai/schemas/archetype.v2.json"
39
+ },
40
+ {
41
+ type: "shell",
42
+ directory: "shells",
43
+ schemaSpecifier: "@decantr/registry/schema/shell.v1.json",
44
+ expectedSchema: "https://decantr.ai/schemas/shell.v1.json"
45
+ }
46
+ ];
47
+ var TYPE_DIRECTORY = Object.fromEntries(
48
+ CONTENT_DIRECTORIES.map((entry) => [entry.type, entry.directory])
49
+ );
50
+ var require2 = createRequire(import.meta.url);
51
+ function loadJsonSchema(specifier) {
52
+ return JSON.parse(readFileSync(require2.resolve(specifier), "utf-8"));
53
+ }
54
+ function createValidators() {
55
+ const ajv = new Ajv2020({
56
+ allErrors: true,
57
+ strict: false,
58
+ allowUnionTypes: true
59
+ });
60
+ ajv.addSchema(loadJsonSchema("@decantr/registry/schema/common.v1.json"));
61
+ return Object.fromEntries(
62
+ CONTENT_DIRECTORIES.map((entry) => [
63
+ entry.type,
64
+ ajv.compile(loadJsonSchema(entry.schemaSpecifier))
65
+ ])
66
+ );
67
+ }
68
+ function isRecord(value) {
69
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
70
+ }
71
+ function isNonEmptyString(value) {
72
+ return typeof value === "string" && value.trim().length > 0;
73
+ }
74
+ function toStringArray(value) {
75
+ return Array.isArray(value) ? value.filter((entry) => isNonEmptyString(entry)) : [];
76
+ }
77
+ function formatSchemaError(error) {
78
+ const instancePath = error.instancePath || "/";
79
+ return `${instancePath} ${error.message}`.trim();
80
+ }
81
+ function slugify(value) {
82
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
83
+ }
84
+ function isIgnoredLocalContentFile(fileName) {
85
+ return DEFAULT_IGNORED_LOCAL_PREFIXES.some((prefix) => fileName.startsWith(prefix));
86
+ }
87
+ function commandsForFinding(source) {
88
+ switch (source) {
89
+ case "schema":
90
+ return ["npm run validate", "decantr content-health"];
91
+ case "reference":
92
+ return ["decantr content-health", "npm run validate"];
93
+ case "quality":
94
+ case "coverage":
95
+ return ["decantr content-health --markdown --output content-health.md"];
96
+ default:
97
+ return ["decantr content-health"];
98
+ }
99
+ }
100
+ function buildRemediationPrompt(input) {
101
+ return [
102
+ "You are fixing one Decantr Content Health finding in a registry content repository.",
103
+ "",
104
+ "Read the referenced JSON content file and the matching Decantr schema before editing. Preserve the item id, published intent, and registry content type unless the finding explicitly says the id or type is wrong.",
105
+ "",
106
+ `Finding: ${input.id}`,
107
+ `Source: ${input.source}`,
108
+ `Severity: ${input.severity}`,
109
+ `Category: ${input.category}`,
110
+ input.type ? `Content type: ${input.type}` : null,
111
+ input.itemId ? `Item id: ${input.itemId}` : null,
112
+ input.file ? `File: ${input.file}` : null,
113
+ `Message: ${input.message}`,
114
+ input.evidence.length > 0 ? `Evidence:
115
+ ${input.evidence.map((entry) => `- ${entry}`).join("\n")}` : null,
116
+ input.suggestedFix ? `Suggested fix: ${input.suggestedFix}` : null,
117
+ "",
118
+ "Make the smallest coherent content change that resolves this finding. Do not add new source-code runtime dependencies for content-only fixes.",
119
+ "",
120
+ `After the fix, run:
121
+ ${input.commands.map((command) => `- ${command}`).join("\n")}`
122
+ ].filter((line) => Boolean(line)).join("\n");
123
+ }
124
+ function createContentFinding(input) {
125
+ const idBase = input.baseId || input.rule || `${input.category}-${input.file ?? ""}-${input.message}`;
126
+ const id = `${input.source}-${slugify(idBase)}`;
127
+ const commands = commandsForFinding(input.source);
128
+ const remediation = {
129
+ summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
130
+ commands,
131
+ prompt: buildRemediationPrompt({
132
+ id,
133
+ source: input.source,
134
+ category: input.category,
135
+ severity: input.severity,
136
+ message: input.message,
137
+ evidence: input.evidence ?? [],
138
+ file: input.file,
139
+ type: input.type,
140
+ itemId: input.itemId,
141
+ suggestedFix: input.suggestedFix,
142
+ commands
143
+ })
144
+ };
145
+ return {
146
+ id,
147
+ source: input.source,
148
+ category: input.category,
149
+ severity: input.severity,
150
+ message: input.message,
151
+ evidence: input.evidence ?? [],
152
+ file: input.file,
153
+ type: input.type,
154
+ itemId: input.itemId,
155
+ rule: input.rule,
156
+ suggestedFix: input.suggestedFix,
157
+ remediation
158
+ };
159
+ }
160
+ function countFindings(findings) {
161
+ return {
162
+ errorCount: findings.filter((finding) => finding.severity === "error").length,
163
+ warnCount: findings.filter((finding) => finding.severity === "warn").length,
164
+ infoCount: findings.filter((finding) => finding.severity === "info").length
165
+ };
166
+ }
167
+ function statusFromCounts(counts) {
168
+ if (counts.errorCount > 0) return "error";
169
+ if (counts.warnCount > 0) return "warning";
170
+ return "healthy";
171
+ }
172
+ function scoreFromCounts(counts) {
173
+ const warningPenalty = Math.min(counts.warnCount * 2, 75);
174
+ const infoPenalty = Math.min(counts.infoCount * 0.5, 10);
175
+ return Math.round(Math.max(0, Math.min(100, 100 - counts.errorCount * 15 - warningPenalty - infoPenalty)));
176
+ }
177
+ function percentage(count, total) {
178
+ if (total === 0) return 1;
179
+ return Math.round(count / total * 1e3) / 1e3;
180
+ }
181
+ function patternIdsFromReference(value) {
182
+ if (isNonEmptyString(value)) return [value];
183
+ if (!isRecord(value)) return [];
184
+ if (isNonEmptyString(value.pattern)) return [value.pattern];
185
+ if (Array.isArray(value.cols)) {
186
+ return value.cols.flatMap((item) => patternIdsFromReference(item));
187
+ }
188
+ return [];
189
+ }
190
+ function collectDependencyReferences(data) {
191
+ if (!isRecord(data.dependencies)) return [];
192
+ const map = {
193
+ patterns: "pattern",
194
+ themes: "theme",
195
+ blueprints: "blueprint",
196
+ archetypes: "archetype",
197
+ shells: "shell"
198
+ };
199
+ const refs = [];
200
+ for (const [group, values] of Object.entries(data.dependencies)) {
201
+ const referencedType = map[group];
202
+ if (!referencedType || !isRecord(values)) continue;
203
+ for (const id of Object.keys(values)) {
204
+ refs.push({
205
+ referencedType,
206
+ id,
207
+ rule: `dependency-${referencedType}`,
208
+ suggestedFix: `Add ${referencedType} "${id}" or remove the stale dependency reference.`
209
+ });
210
+ }
211
+ }
212
+ return refs;
213
+ }
214
+ function collectBlueprintReferences(item) {
215
+ const refs = [];
216
+ const data = item.data;
217
+ if (isRecord(data.theme) && isNonEmptyString(data.theme.id)) {
218
+ refs.push({
219
+ referencedType: "theme",
220
+ id: data.theme.id,
221
+ rule: "blueprint-theme",
222
+ severity: "error",
223
+ suggestedFix: `Add theme "${data.theme.id}" or choose an existing theme id.`
224
+ });
225
+ }
226
+ if (isNonEmptyString(data.archetype)) {
227
+ refs.push({
228
+ referencedType: "archetype",
229
+ id: data.archetype,
230
+ rule: "blueprint-archetype",
231
+ severity: "error",
232
+ suggestedFix: `Add archetype "${data.archetype}" or update the blueprint archetype field.`
233
+ });
234
+ }
235
+ for (const entry of Array.isArray(data.compose) ? data.compose : []) {
236
+ const archetype = isNonEmptyString(entry) ? entry : isRecord(entry) && isNonEmptyString(entry.archetype) ? entry.archetype : null;
237
+ if (archetype) {
238
+ refs.push({
239
+ referencedType: "archetype",
240
+ id: archetype,
241
+ rule: "blueprint-compose-archetype",
242
+ severity: "error",
243
+ suggestedFix: `Add archetype "${archetype}" or remove it from blueprint compose.`
244
+ });
245
+ }
246
+ }
247
+ for (const theme of toStringArray(data.suggested_themes)) {
248
+ refs.push({
249
+ referencedType: "theme",
250
+ id: theme,
251
+ rule: "blueprint-suggested-theme",
252
+ severity: "warn",
253
+ suggestedFix: `Add suggested theme "${theme}" or remove it from suggested_themes.`
254
+ });
255
+ }
256
+ if (isRecord(data.routes)) {
257
+ for (const [route, routeConfig] of Object.entries(data.routes)) {
258
+ if (!isRecord(routeConfig)) continue;
259
+ if (isNonEmptyString(routeConfig.archetype)) {
260
+ refs.push({
261
+ referencedType: "archetype",
262
+ id: routeConfig.archetype,
263
+ rule: "blueprint-route-archetype",
264
+ severity: "error",
265
+ suggestedFix: `Add archetype "${routeConfig.archetype}" or update route "${route}".`
266
+ });
267
+ }
268
+ if (isNonEmptyString(routeConfig.shell)) {
269
+ refs.push({
270
+ referencedType: "shell",
271
+ id: routeConfig.shell,
272
+ rule: "blueprint-route-shell",
273
+ severity: "error",
274
+ suggestedFix: `Add shell "${routeConfig.shell}" or update route "${route}".`
275
+ });
276
+ }
277
+ }
278
+ }
279
+ for (const dependency of collectDependencyReferences(data)) {
280
+ refs.push({
281
+ ...dependency,
282
+ severity: "error"
283
+ });
284
+ }
285
+ return refs;
286
+ }
287
+ function collectArchetypeReferences(item) {
288
+ const refs = [];
289
+ const data = item.data;
290
+ if (Array.isArray(data.pages)) {
291
+ for (const page of data.pages) {
292
+ if (!isRecord(page)) continue;
293
+ if (isNonEmptyString(page.shell) && page.shell !== "inherit") {
294
+ refs.push({
295
+ referencedType: "shell",
296
+ id: page.shell,
297
+ rule: "archetype-page-shell",
298
+ severity: "error",
299
+ suggestedFix: `Add shell "${page.shell}" or update the page shell reference.`
300
+ });
301
+ }
302
+ for (const patternId of Array.isArray(page.default_layout) ? page.default_layout.flatMap((entry) => patternIdsFromReference(entry)) : []) {
303
+ refs.push({
304
+ referencedType: "pattern",
305
+ id: patternId,
306
+ rule: "archetype-page-layout-pattern",
307
+ severity: "warn",
308
+ suggestedFix: `Add pattern "${patternId}" for stronger generation guidance or update the page default_layout reference to an existing pattern.`
309
+ });
310
+ }
311
+ for (const patternId of Array.isArray(page.patterns) ? page.patterns.flatMap((entry) => patternIdsFromReference(entry)) : []) {
312
+ refs.push({
313
+ referencedType: "pattern",
314
+ id: patternId,
315
+ rule: "archetype-page-pattern",
316
+ severity: "warn",
317
+ suggestedFix: `Add pattern "${patternId}" for stronger generation guidance or update the page patterns reference to an existing pattern.`
318
+ });
319
+ }
320
+ }
321
+ }
322
+ if (isRecord(data.suggested_theme)) {
323
+ for (const theme of toStringArray(data.suggested_theme.ids)) {
324
+ refs.push({
325
+ referencedType: "theme",
326
+ id: theme,
327
+ rule: "archetype-suggested-theme",
328
+ severity: "warn",
329
+ suggestedFix: `Add suggested theme "${theme}" or remove it from suggested_theme.ids.`
330
+ });
331
+ }
332
+ }
333
+ for (const dependency of collectDependencyReferences(data)) {
334
+ refs.push({
335
+ ...dependency,
336
+ severity: "error"
337
+ });
338
+ }
339
+ return refs;
340
+ }
341
+ function collectItemReferences(item) {
342
+ if (item.type === "blueprint") return collectBlueprintReferences(item);
343
+ if (item.type === "archetype") return collectArchetypeReferences(item);
344
+ return collectDependencyReferences(item.data).map((dependency) => ({
345
+ ...dependency,
346
+ severity: "error"
347
+ }));
348
+ }
349
+ function addQualityFindings(item, findings) {
350
+ const { data, file, type, id } = item;
351
+ if (type === "pattern") {
352
+ if (!data.visual_brief && !data.layout_hints) {
353
+ findings.push(
354
+ createContentFinding({
355
+ source: "quality",
356
+ category: "Pattern Guidance",
357
+ severity: "warn",
358
+ message: "Pattern is missing both visual_brief and layout_hints.",
359
+ evidence: ["AI scaffolds rely on visual guidance to avoid generic layouts."],
360
+ file,
361
+ type,
362
+ itemId: id,
363
+ rule: "pattern-guidance-missing",
364
+ suggestedFix: "Add a visual_brief or layout_hints that describes the intended composition.",
365
+ baseId: `${file}-pattern-guidance-missing`
366
+ })
367
+ );
368
+ }
369
+ if (!Array.isArray(data.components) || data.components.length === 0) {
370
+ findings.push(
371
+ createContentFinding({
372
+ source: "quality",
373
+ category: "Pattern Components",
374
+ severity: "warn",
375
+ message: "Pattern has no component inventory.",
376
+ evidence: ["components[] is empty or missing."],
377
+ file,
378
+ type,
379
+ itemId: id,
380
+ rule: "pattern-components-missing",
381
+ suggestedFix: "Add a compact components array naming the expected UI building blocks.",
382
+ baseId: `${file}-pattern-components-missing`
383
+ })
384
+ );
385
+ }
386
+ if (isRecord(data.presets)) {
387
+ for (const [presetName, preset] of Object.entries(data.presets)) {
388
+ if (isRecord(preset) && isNonEmptyString(preset.description) && preset.description.length < 30) {
389
+ findings.push(
390
+ createContentFinding({
391
+ source: "quality",
392
+ category: "Preset Guidance",
393
+ severity: "warn",
394
+ message: `Preset "${presetName}" description is too short to guide generation.`,
395
+ evidence: [`Description length: ${preset.description.length}`],
396
+ file,
397
+ type,
398
+ itemId: id,
399
+ rule: "preset-description-short",
400
+ suggestedFix: "Expand the preset description with layout, density, and usage intent.",
401
+ baseId: `${file}-${presetName}-preset-description-short`
402
+ })
403
+ );
404
+ }
405
+ }
406
+ }
407
+ }
408
+ if (type === "theme") {
409
+ const paletteSize = isRecord(data.palette) ? Object.keys(data.palette).length : 0;
410
+ if (paletteSize > 0 && paletteSize < 5) {
411
+ findings.push(
412
+ createContentFinding({
413
+ source: "quality",
414
+ category: "Theme Palette",
415
+ severity: "warn",
416
+ message: "Theme palette has fewer than five semantic colors.",
417
+ evidence: [`Palette entries: ${paletteSize}`],
418
+ file,
419
+ type,
420
+ itemId: id,
421
+ rule: "theme-palette-shallow",
422
+ suggestedFix: "Add semantic palette entries for background, surface, text, muted text, and accent roles.",
423
+ baseId: `${file}-theme-palette-shallow`
424
+ })
425
+ );
426
+ }
427
+ if (!isRecord(data.decorators) || Object.keys(data.decorators).length === 0) {
428
+ findings.push(
429
+ createContentFinding({
430
+ source: "quality",
431
+ category: "Theme Decorators",
432
+ severity: "warn",
433
+ message: "Theme has no decorator definitions.",
434
+ evidence: ["decorators is missing or empty."],
435
+ file,
436
+ type,
437
+ itemId: id,
438
+ rule: "theme-decorators-missing",
439
+ suggestedFix: "Add theme-specific decorator classes that can be rendered into DECANTR.md and section packs.",
440
+ baseId: `${file}-theme-decorators-missing`
441
+ })
442
+ );
443
+ } else {
444
+ for (const [decorator, description] of Object.entries(data.decorators)) {
445
+ if (typeof description === "string" && description.length < 20) {
446
+ findings.push(
447
+ createContentFinding({
448
+ source: "quality",
449
+ category: "Theme Decorators",
450
+ severity: "warn",
451
+ message: `Decorator "${decorator}" description is too short.`,
452
+ evidence: [`Description length: ${description.length}`],
453
+ file,
454
+ type,
455
+ itemId: id,
456
+ rule: "theme-decorator-description-short",
457
+ suggestedFix: "Describe where and how this decorator should be applied.",
458
+ baseId: `${file}-${decorator}-theme-decorator-description-short`
459
+ })
460
+ );
461
+ }
462
+ }
463
+ }
464
+ }
465
+ if (type === "blueprint") {
466
+ const personality = data.personality;
467
+ const personalityMissing = personality === void 0 || personality === null || Array.isArray(personality) && personality.length === 0 || typeof personality === "string" && personality.trim().length === 0;
468
+ if (personalityMissing) {
469
+ findings.push(
470
+ createContentFinding({
471
+ source: "quality",
472
+ category: "Blueprint Personality",
473
+ severity: "warn",
474
+ message: "Blueprint is missing personality guidance.",
475
+ evidence: ["personality is missing or empty."],
476
+ file,
477
+ type,
478
+ itemId: id,
479
+ rule: "blueprint-personality-missing",
480
+ suggestedFix: "Add a concise but specific personality string or trait array.",
481
+ baseId: `${file}-blueprint-personality-missing`
482
+ })
483
+ );
484
+ } else if (typeof personality === "string" && personality.length < 100) {
485
+ findings.push(
486
+ createContentFinding({
487
+ source: "quality",
488
+ category: "Blueprint Personality",
489
+ severity: "warn",
490
+ message: "Blueprint personality is shorter than 100 characters.",
491
+ evidence: [`Length: ${personality.length}`],
492
+ file,
493
+ type,
494
+ itemId: id,
495
+ rule: "blueprint-personality-short",
496
+ suggestedFix: "Expand personality with visual direction, tone, density, and interaction posture.",
497
+ baseId: `${file}-blueprint-personality-short`
498
+ })
499
+ );
500
+ }
501
+ if (!isRecord(data.voice)) {
502
+ findings.push(
503
+ createContentFinding({
504
+ source: "coverage",
505
+ category: "Blueprint Voice",
506
+ severity: "info",
507
+ message: "Blueprint has no voice guidance.",
508
+ evidence: ["voice is missing."],
509
+ file,
510
+ type,
511
+ itemId: id,
512
+ rule: "blueprint-voice-missing",
513
+ suggestedFix: "Add voice.tone, cta_verbs, avoid words, and state copy guidance when this blueprint needs product copy consistency.",
514
+ baseId: `${file}-blueprint-voice-missing`
515
+ })
516
+ );
517
+ }
518
+ }
519
+ if (type === "archetype" && !isRecord(data.page_briefs)) {
520
+ findings.push(
521
+ createContentFinding({
522
+ source: "coverage",
523
+ category: "Archetype Page Briefs",
524
+ severity: "info",
525
+ message: "Archetype has no page_briefs.",
526
+ evidence: ["page_briefs is missing."],
527
+ file,
528
+ type,
529
+ itemId: id,
530
+ rule: "archetype-page-briefs-missing",
531
+ suggestedFix: "Add page_briefs when route-level visual direction should be more specific than page names.",
532
+ baseId: `${file}-archetype-page-briefs-missing`
533
+ })
534
+ );
535
+ }
536
+ }
537
+ function typeSummary(config, items, findings, invalidFiles, ignoredCount) {
538
+ const typeFindings = findings.filter((finding) => finding.type === config.type);
539
+ return {
540
+ type: config.type,
541
+ directory: config.directory,
542
+ itemCount: items.length,
543
+ validCount: items.filter((item) => !invalidFiles.has(item.file)).length,
544
+ ...countFindings(typeFindings),
545
+ ignoredCount
546
+ };
547
+ }
548
+ function missingByTypeInitial() {
549
+ return {
550
+ pattern: 0,
551
+ theme: 0,
552
+ blueprint: 0,
553
+ archetype: 0,
554
+ shell: 0
555
+ };
556
+ }
557
+ async function createContentHealthReport(contentRoot = process.cwd(), options = {}) {
558
+ const validators = createValidators();
559
+ const findings = [];
560
+ const invalidFiles = /* @__PURE__ */ new Set();
561
+ const allItems = [];
562
+ const itemsByType = /* @__PURE__ */ new Map();
563
+ const ignoredCounts = /* @__PURE__ */ new Map();
564
+ let contentDirectoryCount = 0;
565
+ for (const config of CONTENT_DIRECTORIES) {
566
+ const directoryPath = join(contentRoot, config.directory);
567
+ const typeItems = /* @__PURE__ */ new Map();
568
+ itemsByType.set(config.type, typeItems);
569
+ ignoredCounts.set(config.type, 0);
570
+ if (!existsSync(directoryPath)) {
571
+ findings.push(
572
+ createContentFinding({
573
+ source: "content",
574
+ category: "Content Directory",
575
+ severity: "warn",
576
+ message: `Missing ${config.directory}/ directory.`,
577
+ evidence: [`Expected ${config.directory}/ under the content root.`],
578
+ type: config.type,
579
+ rule: "content-directory-missing",
580
+ suggestedFix: `Create ${config.directory}/ when this repository is expected to publish ${config.type} content.`,
581
+ baseId: `${config.directory}-content-directory-missing`
582
+ })
583
+ );
584
+ continue;
585
+ }
586
+ contentDirectoryCount += 1;
587
+ const files = readdirSync(directoryPath).filter((file) => file.endsWith(".json")).sort();
588
+ for (const fileName of files) {
589
+ if (!options.includeIgnored && isIgnoredLocalContentFile(fileName)) {
590
+ ignoredCounts.set(config.type, (ignoredCounts.get(config.type) ?? 0) + 1);
591
+ continue;
592
+ }
593
+ const relativeFile = `${config.directory}/${fileName}`;
594
+ const expectedId = basename(fileName, ".json");
595
+ let data;
596
+ try {
597
+ data = JSON.parse(readFileSync(join(contentRoot, relativeFile), "utf-8"));
598
+ } catch (e) {
599
+ invalidFiles.add(relativeFile);
600
+ findings.push(
601
+ createContentFinding({
602
+ source: "schema",
603
+ category: "JSON Parse",
604
+ severity: "error",
605
+ message: `Invalid JSON: ${e.message}`,
606
+ evidence: [`File: ${relativeFile}`],
607
+ file: relativeFile,
608
+ type: config.type,
609
+ itemId: expectedId,
610
+ rule: "json-invalid",
611
+ suggestedFix: "Repair the JSON syntax.",
612
+ baseId: `${relativeFile}-json-invalid`
613
+ })
614
+ );
615
+ continue;
616
+ }
617
+ if (!isRecord(data)) {
618
+ invalidFiles.add(relativeFile);
619
+ findings.push(
620
+ createContentFinding({
621
+ source: "schema",
622
+ category: "Content Shape",
623
+ severity: "error",
624
+ message: "Content item must be a JSON object.",
625
+ evidence: [`File: ${relativeFile}`],
626
+ file: relativeFile,
627
+ type: config.type,
628
+ itemId: expectedId,
629
+ rule: "content-object-required",
630
+ suggestedFix: "Replace the file with a JSON object matching the content schema.",
631
+ baseId: `${relativeFile}-content-object-required`
632
+ })
633
+ );
634
+ continue;
635
+ }
636
+ const id = isNonEmptyString(data.id) ? data.id : isNonEmptyString(data.slug) ? data.slug : expectedId;
637
+ const item = {
638
+ type: config.type,
639
+ directory: config.directory,
640
+ file: relativeFile,
641
+ id,
642
+ data
643
+ };
644
+ allItems.push(item);
645
+ if (typeItems.has(id)) {
646
+ invalidFiles.add(relativeFile);
647
+ findings.push(
648
+ createContentFinding({
649
+ source: "schema",
650
+ category: "Duplicate Content ID",
651
+ severity: "error",
652
+ message: `${config.type} id "${id}" is declared more than once.`,
653
+ evidence: [`Duplicate file: ${relativeFile}`],
654
+ file: relativeFile,
655
+ type: config.type,
656
+ itemId: id,
657
+ rule: "content-id-duplicate",
658
+ suggestedFix: "Make ids unique within each content type.",
659
+ baseId: `${relativeFile}-content-id-duplicate`
660
+ })
661
+ );
662
+ } else {
663
+ typeItems.set(id, item);
664
+ }
665
+ if (data.$schema !== config.expectedSchema) {
666
+ invalidFiles.add(relativeFile);
667
+ findings.push(
668
+ createContentFinding({
669
+ source: "schema",
670
+ category: "Schema URL",
671
+ severity: "error",
672
+ message: `$schema must be "${config.expectedSchema}".`,
673
+ evidence: [`Found: ${typeof data.$schema === "string" ? data.$schema : "missing"}`],
674
+ file: relativeFile,
675
+ type: config.type,
676
+ itemId: id,
677
+ rule: "schema-url-mismatch",
678
+ suggestedFix: `Set $schema to ${config.expectedSchema}.`,
679
+ baseId: `${relativeFile}-schema-url-mismatch`
680
+ })
681
+ );
682
+ }
683
+ if (id !== expectedId) {
684
+ invalidFiles.add(relativeFile);
685
+ findings.push(
686
+ createContentFinding({
687
+ source: "schema",
688
+ category: "Content ID",
689
+ severity: "error",
690
+ message: `id must match filename (${expectedId}).`,
691
+ evidence: [`Found id: ${id}`],
692
+ file: relativeFile,
693
+ type: config.type,
694
+ itemId: id,
695
+ rule: "content-id-filename-mismatch",
696
+ suggestedFix: `Rename the file or set id to "${expectedId}".`,
697
+ baseId: `${relativeFile}-content-id-filename-mismatch`
698
+ })
699
+ );
700
+ }
701
+ const validate = validators[config.type];
702
+ if (!validate(data)) {
703
+ invalidFiles.add(relativeFile);
704
+ for (const schemaError of (validate.errors || []).slice(0, 6)) {
705
+ findings.push(
706
+ createContentFinding({
707
+ source: "schema",
708
+ category: "Schema Validation",
709
+ severity: "error",
710
+ message: `Schema validation failed: ${formatSchemaError(schemaError)}`,
711
+ evidence: [`File: ${relativeFile}`],
712
+ file: relativeFile,
713
+ type: config.type,
714
+ itemId: id,
715
+ rule: "schema-validation-failed",
716
+ suggestedFix: "Update the content item to match the published schema.",
717
+ baseId: `${relativeFile}-${formatSchemaError(schemaError)}`
718
+ })
719
+ );
720
+ }
721
+ }
722
+ }
723
+ }
724
+ if (contentDirectoryCount === 0 || allItems.length === 0) {
725
+ findings.push(
726
+ createContentFinding({
727
+ source: "content",
728
+ category: "Content Root",
729
+ severity: "error",
730
+ message: "No Decantr registry content was found in this directory.",
731
+ evidence: ["Expected one or more of patterns/, themes/, blueprints/, archetypes/, shells/."],
732
+ rule: "content-root-empty",
733
+ suggestedFix: "Run this command from a decantr-content style repository.",
734
+ baseId: "content-root-empty"
735
+ })
736
+ );
737
+ }
738
+ let referencesChecked = 0;
739
+ const missingByType = missingByTypeInitial();
740
+ const missingReferenceGroups = /* @__PURE__ */ new Map();
741
+ for (const item of allItems) {
742
+ for (const reference of collectItemReferences(item)) {
743
+ referencesChecked += 1;
744
+ const referenced = itemsByType.get(reference.referencedType)?.has(reference.id);
745
+ if (referenced) continue;
746
+ missingByType[reference.referencedType] += 1;
747
+ const key = `${item.file}|${reference.rule}|${reference.severity}|${reference.referencedType}`;
748
+ const group = missingReferenceGroups.get(key);
749
+ if (group) {
750
+ group.ids.push(reference.id);
751
+ } else {
752
+ missingReferenceGroups.set(key, {
753
+ item,
754
+ referencedType: reference.referencedType,
755
+ rule: reference.rule,
756
+ severity: reference.severity,
757
+ ids: [reference.id],
758
+ suggestedFix: reference.suggestedFix
759
+ });
760
+ }
761
+ }
762
+ }
763
+ for (const group of missingReferenceGroups.values()) {
764
+ const missingList = [...new Set(group.ids)].sort();
765
+ const preview = missingList.slice(0, 12);
766
+ findings.push(
767
+ createContentFinding({
768
+ source: "reference",
769
+ category: "Missing Reference",
770
+ severity: group.severity,
771
+ message: missingList.length === 1 ? `${group.item.type} "${group.item.id}" references missing ${group.referencedType} "${missingList[0]}".` : `${group.item.type} "${group.item.id}" references ${missingList.length} missing ${group.referencedType} items.`,
772
+ evidence: [
773
+ `Reference directory: ${TYPE_DIRECTORY[group.referencedType]}/`,
774
+ `Rule: ${group.rule}`,
775
+ `Missing ${group.referencedType}: ${preview.join(", ")}${missingList.length > preview.length ? `, and ${missingList.length - preview.length} more` : ""}`
776
+ ],
777
+ file: group.item.file,
778
+ type: group.item.type,
779
+ itemId: group.item.id,
780
+ rule: group.rule,
781
+ suggestedFix: missingList.length === 1 ? group.suggestedFix : `Add the missing ${group.referencedType} items or update stale ${group.rule} references.`,
782
+ baseId: `${group.item.file}-${group.rule}-${group.referencedType}-missing`
783
+ })
784
+ );
785
+ }
786
+ for (const item of allItems) {
787
+ addQualityFindings(item, findings);
788
+ }
789
+ const byType = CONTENT_DIRECTORIES.map(
790
+ (config) => typeSummary(
791
+ config,
792
+ allItems.filter((item) => item.type === config.type),
793
+ findings,
794
+ invalidFiles,
795
+ ignoredCounts.get(config.type) ?? 0
796
+ )
797
+ );
798
+ const counts = countFindings(findings);
799
+ const validCount = allItems.filter((item) => !invalidFiles.has(item.file)).length;
800
+ const ignoredCount = [...ignoredCounts.values()].reduce((sum, count) => sum + count, 0);
801
+ const patterns = allItems.filter((item) => item.type === "pattern");
802
+ const themes = allItems.filter((item) => item.type === "theme");
803
+ const blueprints = allItems.filter((item) => item.type === "blueprint");
804
+ const archetypes = allItems.filter((item) => item.type === "archetype");
805
+ return {
806
+ $schema: CONTENT_HEALTH_SCHEMA_URL,
807
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
808
+ contentRoot,
809
+ status: statusFromCounts(counts),
810
+ score: scoreFromCounts(counts),
811
+ summary: {
812
+ itemCount: allItems.length,
813
+ validCount,
814
+ ...counts,
815
+ findingCount: findings.length,
816
+ ignoredCount,
817
+ contentDirectoryCount
818
+ },
819
+ content: byType,
820
+ references: {
821
+ checked: referencesChecked,
822
+ missing: Object.values(missingByType).reduce((sum, count) => sum + count, 0),
823
+ missingByType
824
+ },
825
+ quality: {
826
+ patternVisualBriefCoverage: percentage(
827
+ patterns.filter((item) => item.data.visual_brief || item.data.layout_hints).length,
828
+ patterns.length
829
+ ),
830
+ patternInteractionCoverage: percentage(
831
+ patterns.filter((item) => Array.isArray(item.data.interactions) && item.data.interactions.length > 0).length,
832
+ patterns.length
833
+ ),
834
+ themeDecoratorCoverage: percentage(
835
+ themes.filter((item) => isRecord(item.data.decorators) && Object.keys(item.data.decorators).length > 0).length,
836
+ themes.length
837
+ ),
838
+ blueprintPersonalityCoverage: percentage(
839
+ blueprints.filter((item) => {
840
+ const personality = item.data.personality;
841
+ return isNonEmptyString(personality) || Array.isArray(personality) && personality.some((entry) => isNonEmptyString(entry));
842
+ }).length,
843
+ blueprints.length
844
+ ),
845
+ blueprintVoiceCoverage: percentage(blueprints.filter((item) => isRecord(item.data.voice)).length, blueprints.length),
846
+ archetypePageBriefCoverage: percentage(
847
+ archetypes.filter((item) => isRecord(item.data.page_briefs)).length,
848
+ archetypes.length
849
+ )
850
+ },
851
+ ci: {
852
+ recommendedCommand: "decantr content-health --ci --fail-on error",
853
+ failOn: "error"
854
+ },
855
+ findings
856
+ };
857
+ }
858
+ function colorForStatus(status) {
859
+ if (status === "healthy") return GREEN;
860
+ if (status === "warning") return YELLOW;
861
+ return RED;
862
+ }
863
+ function percentLabel(value) {
864
+ return `${Math.round(value * 100)}%`;
865
+ }
866
+ function formatContentHealthText(report) {
867
+ const color = colorForStatus(report.status);
868
+ const lines = [
869
+ `${BOLD}Decantr Content Health${RESET}`,
870
+ "",
871
+ `${color}${report.status.toUpperCase()}${RESET} score ${report.score}/100`,
872
+ `${DIM}${report.contentRoot}${RESET}`,
873
+ "",
874
+ `${BOLD}Summary:${RESET}`,
875
+ ` Items: ${report.summary.itemCount} total, ${report.summary.validCount} valid, ${report.summary.ignoredCount} ignored`,
876
+ ` Findings: ${report.summary.errorCount} error(s), ${report.summary.warnCount} warn(s), ${report.summary.infoCount} info`,
877
+ ` References: ${report.references.checked} checked, ${report.references.missing} missing`,
878
+ ` Quality: pattern guidance ${percentLabel(report.quality.patternVisualBriefCoverage)} | theme decorators ${percentLabel(report.quality.themeDecoratorCoverage)} | blueprint voice ${percentLabel(report.quality.blueprintVoiceCoverage)}`,
879
+ "",
880
+ `${BOLD}Content:${RESET}`
881
+ ];
882
+ for (const entry of report.content) {
883
+ lines.push(
884
+ ` ${entry.directory.padEnd(10)} ${entry.itemCount} item(s), ${entry.validCount} valid, ${entry.errorCount} error(s), ${entry.warnCount} warn(s), ${entry.ignoredCount} ignored`
885
+ );
886
+ }
887
+ lines.push("");
888
+ lines.push(`${BOLD}Findings:${RESET}`);
889
+ if (report.findings.length === 0) {
890
+ lines.push(` ${GREEN}No findings. Content supply chain is healthy.${RESET}`);
891
+ } else {
892
+ for (const finding of report.findings.slice(0, 40)) {
893
+ const findingColor = finding.severity === "error" ? RED : finding.severity === "warn" ? YELLOW : CYAN;
894
+ lines.push(
895
+ ` ${findingColor}[${finding.severity.toUpperCase()}]${RESET} ${finding.id}: ${finding.message}`
896
+ );
897
+ if (finding.file) lines.push(` ${DIM}${finding.file}${RESET}`);
898
+ if (finding.suggestedFix) lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
899
+ lines.push(` ${DIM}Prompt: decantr content-health --prompt ${finding.id}${RESET}`);
900
+ }
901
+ if (report.findings.length > 40) {
902
+ lines.push(` ${DIM}Showing first 40 of ${report.findings.length} findings. Use --json for the full report.${RESET}`);
903
+ }
904
+ }
905
+ lines.push("");
906
+ lines.push(`${BOLD}CI:${RESET} ${report.ci.recommendedCommand}`);
907
+ return `${lines.join("\n")}
908
+ `;
909
+ }
910
+ function formatContentHealthMarkdown(report) {
911
+ const lines = [
912
+ "# Decantr Content Health",
913
+ "",
914
+ `- Status: **${report.status}**`,
915
+ `- Score: **${report.score}/100**`,
916
+ `- Content root: \`${report.contentRoot}\``,
917
+ `- Items: ${report.summary.itemCount} total, ${report.summary.validCount} valid, ${report.summary.ignoredCount} ignored`,
918
+ `- Findings: ${report.summary.errorCount} error(s), ${report.summary.warnCount} warn(s), ${report.summary.infoCount} info`,
919
+ `- References: ${report.references.checked} checked, ${report.references.missing} missing`,
920
+ "",
921
+ "## Content",
922
+ "",
923
+ "| Type | Items | Valid | Errors | Warnings | Info | Ignored |",
924
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: |"
925
+ ];
926
+ for (const entry of report.content) {
927
+ lines.push(
928
+ `| ${entry.type} | ${entry.itemCount} | ${entry.validCount} | ${entry.errorCount} | ${entry.warnCount} | ${entry.infoCount} | ${entry.ignoredCount} |`
929
+ );
930
+ }
931
+ lines.push("");
932
+ lines.push("## Quality Coverage");
933
+ lines.push("");
934
+ lines.push(`- Pattern visual guidance: ${percentLabel(report.quality.patternVisualBriefCoverage)}`);
935
+ lines.push(`- Pattern interactions: ${percentLabel(report.quality.patternInteractionCoverage)}`);
936
+ lines.push(`- Theme decorators: ${percentLabel(report.quality.themeDecoratorCoverage)}`);
937
+ lines.push(`- Blueprint personality: ${percentLabel(report.quality.blueprintPersonalityCoverage)}`);
938
+ lines.push(`- Blueprint voice: ${percentLabel(report.quality.blueprintVoiceCoverage)}`);
939
+ lines.push(`- Archetype page briefs: ${percentLabel(report.quality.archetypePageBriefCoverage)}`);
940
+ lines.push("");
941
+ lines.push("## Findings");
942
+ lines.push("");
943
+ if (report.findings.length === 0) {
944
+ lines.push("No findings. Content supply chain is healthy.");
945
+ } else {
946
+ for (const finding of report.findings) {
947
+ lines.push(`### ${finding.id}`);
948
+ lines.push("");
949
+ lines.push(`- Severity: ${finding.severity}`);
950
+ lines.push(`- Source: ${finding.source}`);
951
+ lines.push(`- Category: ${finding.category}`);
952
+ if (finding.file) lines.push(`- File: \`${finding.file}\``);
953
+ if (finding.type) lines.push(`- Type: ${finding.type}`);
954
+ if (finding.itemId) lines.push(`- Item: \`${finding.itemId}\``);
955
+ lines.push(`- Message: ${finding.message}`);
956
+ if (finding.suggestedFix) lines.push(`- Fix: ${finding.suggestedFix}`);
957
+ if (finding.evidence.length > 0) {
958
+ lines.push("- Evidence:");
959
+ for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
960
+ }
961
+ lines.push(`- Prompt: \`decantr content-health --prompt ${finding.id}\``);
962
+ lines.push("");
963
+ }
964
+ }
965
+ lines.push("## CI");
966
+ lines.push("");
967
+ lines.push(`\`${report.ci.recommendedCommand}\``);
968
+ return `${lines.join("\n")}
969
+ `;
970
+ }
971
+ function formatContentHealthJson(report) {
972
+ return `${JSON.stringify(report, null, 2)}
973
+ `;
974
+ }
975
+ function resolveFormat(options) {
976
+ if (options.json) return "json";
977
+ if (options.markdown) return "markdown";
978
+ return options.format ?? "text";
979
+ }
980
+ function shouldFailContentHealth(report, failOn) {
981
+ if (failOn === "none") return false;
982
+ if (failOn === "warn") return report.summary.errorCount > 0 || report.summary.warnCount > 0;
983
+ return report.summary.errorCount > 0;
984
+ }
985
+ async function cmdContentHealth(contentRoot = process.cwd(), options = {}) {
986
+ const report = await createContentHealthReport(contentRoot, options);
987
+ if (options.promptId) {
988
+ const finding = report.findings.find((entry) => entry.id === options.promptId);
989
+ if (!finding) {
990
+ console.error(`${RED}No content health finding found for id: ${options.promptId}${RESET}`);
991
+ process.exitCode = 1;
992
+ return;
993
+ }
994
+ console.log(finding.remediation.prompt);
995
+ return;
996
+ }
997
+ const format = resolveFormat(options);
998
+ const payload = format === "json" ? formatContentHealthJson(report) : format === "markdown" ? formatContentHealthMarkdown(report) : formatContentHealthText(report);
999
+ if (options.output) {
1000
+ writeFileSync(options.output, payload, "utf-8");
1001
+ if (!options.ci) {
1002
+ console.log(`${GREEN}Wrote Decantr content health report:${RESET} ${options.output}`);
1003
+ }
1004
+ } else {
1005
+ process.stdout.write(payload);
1006
+ }
1007
+ if (options.ci && shouldFailContentHealth(report, options.failOn ?? "error")) {
1008
+ process.exitCode = 1;
1009
+ }
1010
+ }
1011
+ function parseContentHealthArgs(args) {
1012
+ const options = {};
1013
+ for (let index = 1; index < args.length; index += 1) {
1014
+ const arg = args[index];
1015
+ if (arg === "--json") {
1016
+ options.json = true;
1017
+ } else if (arg === "--markdown") {
1018
+ options.markdown = true;
1019
+ } else if (arg === "--ci") {
1020
+ options.ci = true;
1021
+ } else if (arg === "--include-ignored") {
1022
+ options.includeIgnored = true;
1023
+ } else if (arg === "--format" && args[index + 1]) {
1024
+ options.format = args[++index];
1025
+ } else if (arg.startsWith("--format=")) {
1026
+ options.format = arg.split("=")[1];
1027
+ } else if (arg === "--output" && args[index + 1]) {
1028
+ options.output = args[++index];
1029
+ } else if (arg.startsWith("--output=")) {
1030
+ options.output = arg.split("=")[1];
1031
+ } else if (arg === "--fail-on" && args[index + 1]) {
1032
+ options.failOn = args[++index];
1033
+ } else if (arg.startsWith("--fail-on=")) {
1034
+ options.failOn = arg.split("=")[1];
1035
+ } else if (arg === "--prompt" && args[index + 1]) {
1036
+ options.promptId = args[++index];
1037
+ } else if (arg.startsWith("--prompt=")) {
1038
+ options.promptId = arg.split("=")[1];
1039
+ }
1040
+ }
1041
+ if (options.format && !["text", "json", "markdown"].includes(options.format)) {
1042
+ throw new Error("Invalid --format value. Use text, json, or markdown.");
1043
+ }
1044
+ if (options.failOn && !["error", "warn", "none"].includes(options.failOn)) {
1045
+ throw new Error("Invalid --fail-on value. Use error, warn, or none.");
1046
+ }
1047
+ return options;
1048
+ }
1049
+ export {
1050
+ cmdContentHealth,
1051
+ createContentHealthReport,
1052
+ formatContentHealthJson,
1053
+ formatContentHealthMarkdown,
1054
+ formatContentHealthText,
1055
+ parseContentHealthArgs,
1056
+ shouldFailContentHealth
1057
+ };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunk-Y45MCRGI.js";
1
+ import "./chunk-PKJSI6IH.js";
2
2
  import "./chunk-USOO77A5.js";
3
3
  import "./chunk-DI2PLOJ6.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
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",
@@ -30,10 +30,11 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
+ "ajv": "^8.18.0",
33
34
  "@decantr/core": "1.0.6",
34
35
  "@decantr/essence-spec": "1.0.7",
36
+ "@decantr/registry": "1.1.0",
35
37
  "@decantr/verifier": "1.1.0",
36
- "@decantr/registry": "1.0.4",
37
38
  "@decantr/telemetry": "0.1.2"
38
39
  },
39
40
  "scripts": {