@decantr/cli 1.7.28 → 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.
@@ -0,0 +1,328 @@
1
+ import {
2
+ buildGuardRegistryContext,
3
+ collectMetrics,
4
+ createDoctrineMap,
5
+ isOptedIn,
6
+ optIn,
7
+ readDoctrineMap,
8
+ scanAmbientContext,
9
+ scanProjectInteractions,
10
+ scanRoutes,
11
+ scanStyling,
12
+ sendGuardMetrics
13
+ } from "./chunk-DI2PLOJ6.js";
14
+
15
+ // src/commands/heal.ts
16
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
17
+ import { join as join2 } from "path";
18
+ import { evaluateGuard, isV3, validateEssence } from "@decantr/essence-spec";
19
+
20
+ // src/brownfield-check.ts
21
+ import { existsSync, readFileSync } from "fs";
22
+ import { join } from "path";
23
+ function readProjectJson(projectRoot) {
24
+ const path = join(projectRoot, ".decantr", "project.json");
25
+ if (!existsSync(path)) return {};
26
+ try {
27
+ return JSON.parse(readFileSync(path, "utf-8"));
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+ function essenceRoutes(essence) {
33
+ const fromRouteMap = Object.keys(essence.blueprint.routes ?? {});
34
+ const fromPages = essence.blueprint.sections?.flatMap(
35
+ (section) => section.pages.map((page) => page.route).filter((route) => Boolean(route))
36
+ ) ?? essence.blueprint.pages?.map((page) => page.route).filter((route) => Boolean(route)) ?? [];
37
+ return /* @__PURE__ */ new Set([...fromRouteMap, ...fromPages]);
38
+ }
39
+ function routeLabel(routes) {
40
+ if (routes.length <= 6) return routes.join(", ");
41
+ return `${routes.slice(0, 6).join(", ")} (+${routes.length - 6} more)`;
42
+ }
43
+ function hasDoctrineEffect(essence, key) {
44
+ const effects = essence.dna.constraints?.effects;
45
+ return Boolean(effects && effects[key]);
46
+ }
47
+ function hasActionableDoctrineSource(doctrine, area) {
48
+ return doctrine.sources.some(
49
+ (source) => source.area === area && source.currency === "current" && source.safeToCite && source.confidence >= 0.72 && source.precedence >= 75
50
+ );
51
+ }
52
+ function hasAssistantBridge(projectRoot) {
53
+ const previewPath = join(projectRoot, ".decantr", "context", "assistant-bridge.md");
54
+ if (existsSync(previewPath)) return true;
55
+ const candidateFiles = [
56
+ "CLAUDE.md",
57
+ "AGENTS.md",
58
+ "GEMINI.md",
59
+ "copilot-instructions.md",
60
+ ".github/copilot-instructions.md",
61
+ ".cursorrules",
62
+ ".windsurfrules",
63
+ ".claude/rules/decantr.md",
64
+ ".cursor/rules/decantr.mdc"
65
+ ];
66
+ return candidateFiles.some((rel) => {
67
+ const path = join(projectRoot, rel);
68
+ if (!existsSync(path)) return false;
69
+ try {
70
+ return readFileSync(path, "utf-8").includes("decantr:assistant-bridge:start");
71
+ } catch {
72
+ return false;
73
+ }
74
+ });
75
+ }
76
+ function scanBrownfieldIssues(projectRoot, essence) {
77
+ const projectJson = readProjectJson(projectRoot);
78
+ const routes = scanRoutes(projectRoot);
79
+ const styling = scanStyling(projectRoot);
80
+ const ambient = scanAmbientContext(projectRoot);
81
+ const doctrine = readDoctrineMap(projectRoot) ?? createDoctrineMap(ambient);
82
+ const issues = [];
83
+ const declaredRoutes = essenceRoutes(essence);
84
+ const observedRoutes = new Set(routes.routes.map((route) => route.path));
85
+ const missingFromEssence = [...observedRoutes].filter((route) => !declaredRoutes.has(route));
86
+ const missingFromSource = [...declaredRoutes].filter((route) => !observedRoutes.has(route));
87
+ if (routes.routes.length > 0 && declaredRoutes.size === 0) {
88
+ issues.push({
89
+ type: "error",
90
+ rule: "brownfield-route-coverage",
91
+ message: `The app has ${routes.routes.length} observed route(s), but the Decantr essence declares no routes.`,
92
+ suggestion: "Run `decantr analyze`, review the proposal, then `decantr init --existing --accept-proposal` or `--merge-proposal`."
93
+ });
94
+ } else if (missingFromEssence.length > 0) {
95
+ issues.push({
96
+ type: "error",
97
+ rule: "brownfield-route-drift",
98
+ message: `Observed routes are missing from the Decantr contract: ${routeLabel(missingFromEssence)}.`,
99
+ suggestion: "Regenerate a brownfield proposal and merge the missing routes into the essence."
100
+ });
101
+ }
102
+ if (routes.routes.length > 0 && declaredRoutes.size === 1 && declaredRoutes.has("/") && routes.routes.length > 1) {
103
+ issues.push({
104
+ type: "error",
105
+ rule: "brownfield-generic-contract",
106
+ message: "The essence only declares `/` while the app has multiple observed routes.",
107
+ suggestion: "Accept or merge an observed brownfield proposal instead of using a generic scaffold contract."
108
+ });
109
+ }
110
+ if (missingFromSource.length > 0 && routes.routes.length > 0) {
111
+ issues.push({
112
+ type: "warning",
113
+ rule: "brownfield-stale-route",
114
+ message: `Essence routes were not observed in source: ${routeLabel(missingFromSource)}.`,
115
+ suggestion: "Confirm whether these are generated/dynamic routes or stale contract entries."
116
+ });
117
+ }
118
+ const adoptionMode = projectJson.initialized?.adoptionMode;
119
+ const themeId = essence.dna.theme.id;
120
+ if (adoptionMode === "contract-only" && themeId === "luminarum" && (styling.approach !== "unknown" || styling.cssVariables.length > 0)) {
121
+ issues.push({
122
+ type: "warning",
123
+ rule: "brownfield-theme-default",
124
+ message: "Contract-only brownfield essence still uses Decantr theme `luminarum` while the app has an existing styling system.",
125
+ suggestion: 'Use an observed proposal with `theme.id = "existing"` unless the user explicitly opts into a Decantr theme.'
126
+ });
127
+ }
128
+ for (const conflict of ambient.conflicts) {
129
+ issues.push({
130
+ type: "warning",
131
+ rule: "brownfield-doctrine-conflict",
132
+ message: conflict,
133
+ suggestion: "Resolve or document precedence before treating these rules as enforceable contract."
134
+ });
135
+ }
136
+ if (ambient.items.length === 0) {
137
+ issues.push({
138
+ type: "warning",
139
+ rule: "brownfield-context-missing",
140
+ message: "No ambient project context was detected for this brownfield check.",
141
+ suggestion: "Run `decantr analyze` to create `.decantr/ambient-context.json` and a proposal-backed report."
142
+ });
143
+ }
144
+ const hasBrownfieldArtifacts = Boolean(projectJson.initialized?.workflowMode === "brownfield-attach");
145
+ if (hasBrownfieldArtifacts && !existsSync(join(projectRoot, ".decantr", "doctrine-map.json"))) {
146
+ issues.push({
147
+ type: "warning",
148
+ rule: "brownfield-doctrine-map-missing",
149
+ message: "Brownfield attach metadata exists, but `.decantr/doctrine-map.json` is missing.",
150
+ suggestion: "Run `decantr analyze` to regenerate ranked doctrine evidence."
151
+ });
152
+ }
153
+ if (hasActionableDoctrineSource(doctrine, "security-data") && !hasDoctrineEffect(essence, "doctrine-security-data")) {
154
+ issues.push({
155
+ type: "warning",
156
+ rule: "brownfield-doctrine-coverage",
157
+ message: "Security/data doctrine was detected, but the essence does not record a security/data preservation constraint.",
158
+ suggestion: "Regenerate and merge a brownfield proposal so security/data doctrine is represented in `dna.constraints.effects`."
159
+ });
160
+ }
161
+ if (hasActionableDoctrineSource(doctrine, "design-system") && !hasDoctrineEffect(essence, "doctrine-design-system")) {
162
+ issues.push({
163
+ type: "warning",
164
+ rule: "brownfield-doctrine-coverage",
165
+ message: "Design-system doctrine was detected, but the essence does not record a design-system preservation constraint.",
166
+ suggestion: "Regenerate and merge a brownfield proposal so design-system doctrine is represented in `dna.constraints.effects`."
167
+ });
168
+ }
169
+ if (styling.approach !== "unknown") {
170
+ const palette = String(essence.dna.color.palette ?? "");
171
+ const observedPalette = palette === "observed" || palette === styling.approach;
172
+ if (themeId === "existing" && !observedPalette) {
173
+ issues.push({
174
+ type: "warning",
175
+ rule: "brownfield-style-drift",
176
+ message: `Observed styling approach is ${styling.approach}, but the essence color palette is ${palette || "unset"}.`,
177
+ suggestion: "Regenerate and merge a brownfield proposal so the contract reflects the existing styling system."
178
+ });
179
+ }
180
+ }
181
+ if (ambient.items.some((item) => item.role === "assistant-specific") && hasBrownfieldArtifacts && !hasAssistantBridge(projectRoot)) {
182
+ issues.push({
183
+ type: "warning",
184
+ rule: "brownfield-assistant-bridge-missing",
185
+ message: "Assistant-specific rule files were detected, but no Decantr assistant bridge preview or applied bridge block was found.",
186
+ suggestion: "Run `decantr rules preview` first, then `decantr rules apply` if the user explicitly approves rule-file mutation."
187
+ });
188
+ }
189
+ const unsafeSources = doctrine.sources.filter((source) => !source.safeToCite);
190
+ if (unsafeSources.length > 0) {
191
+ issues.push({
192
+ type: "warning",
193
+ rule: "brownfield-unsafe-context",
194
+ message: `Some ambient context should not be cited directly: ${unsafeSources.slice(0, 4).map((source) => source.path).join(", ")}${unsafeSources.length > 4 ? ` (+${unsafeSources.length - 4} more)` : ""}.`,
195
+ suggestion: "Keep unsafe source paths in the inventory, but do not paste their contents into assistant context."
196
+ });
197
+ }
198
+ return issues;
199
+ }
200
+
201
+ // src/commands/heal.ts
202
+ var GREEN = "\x1B[32m";
203
+ var RED = "\x1B[31m";
204
+ var YELLOW = "\x1B[33m";
205
+ var CYAN = "\x1B[36m";
206
+ var RESET = "\x1B[0m";
207
+ var DIM = "\x1B[2m";
208
+ function collectCheckIssues(projectRoot = process.cwd(), options = {}) {
209
+ const essencePath = join2(projectRoot, "decantr.essence.json");
210
+ if (!existsSync2(essencePath)) {
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
+ };
222
+ }
223
+ const essence = JSON.parse(readFileSync2(essencePath, "utf-8"));
224
+ const issues = [];
225
+ const validation = validateEssence(essence);
226
+ if (!validation.valid) {
227
+ for (const err of validation.errors) {
228
+ issues.push({
229
+ type: "error",
230
+ rule: "schema",
231
+ message: err
232
+ });
233
+ }
234
+ }
235
+ let interactionIssues = [];
236
+ try {
237
+ interactionIssues = scanProjectInteractions(projectRoot);
238
+ } catch {
239
+ }
240
+ try {
241
+ const guardContext = buildGuardRegistryContext(projectRoot);
242
+ const violations = evaluateGuard(essence, {
243
+ ...guardContext,
244
+ interaction_issues: interactionIssues
245
+ });
246
+ for (const v of violations) {
247
+ issues.push({
248
+ type: v.severity === "error" ? "error" : "warning",
249
+ rule: v.rule,
250
+ message: v.message,
251
+ suggestion: v.suggestion
252
+ });
253
+ }
254
+ } catch {
255
+ }
256
+ if (options.brownfield) {
257
+ try {
258
+ if (isV3(essence)) {
259
+ const brownfieldIssues = scanBrownfieldIssues(projectRoot, essence);
260
+ issues.push(...brownfieldIssues);
261
+ } else {
262
+ issues.push({
263
+ type: "warning",
264
+ rule: "brownfield-check",
265
+ message: "Brownfield checks require a v3 Decantr essence."
266
+ });
267
+ }
268
+ } catch (e) {
269
+ issues.push({
270
+ type: "warning",
271
+ rule: "brownfield-check",
272
+ message: `Brownfield check could not complete: ${e.message}`
273
+ });
274
+ }
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 ?? {};
288
+ if (issues.length === 0) {
289
+ console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
290
+ await maybeSendTelemetry(projectRoot, essence, issues, options);
291
+ return;
292
+ }
293
+ console.log(`Found ${issues.length} issue(s):
294
+ `);
295
+ for (const issue of issues) {
296
+ const icon = issue.type === "error" ? `${RED}x${RESET}` : `${YELLOW}!${RESET}`;
297
+ console.log(`${icon} [${issue.rule}] ${issue.message}`);
298
+ if (issue.suggestion) {
299
+ console.log(` ${DIM}Suggestion: ${issue.suggestion}${RESET}`);
300
+ }
301
+ }
302
+ console.log(`
303
+ ${YELLOW}Manual fixes required. Review the issues above.${RESET}`);
304
+ const hasError = issues.some((i) => i.type === "error");
305
+ if (hasError) {
306
+ process.exitCode = 1;
307
+ }
308
+ await maybeSendTelemetry(projectRoot, essence, issues, options);
309
+ }
310
+ async function maybeSendTelemetry(projectRoot, essence, issues, options) {
311
+ if (options.telemetry && !isOptedIn(projectRoot)) {
312
+ optIn(projectRoot);
313
+ console.log(
314
+ `
315
+ ${CYAN}Telemetry enabled.${RESET} Anonymous guard metrics will be sent on future checks.`
316
+ );
317
+ console.log(`${DIM}Set "telemetry": false in .decantr/project.json to opt out.${RESET}`);
318
+ }
319
+ if (isOptedIn(projectRoot)) {
320
+ const metrics = collectMetrics(essence, issues);
321
+ sendGuardMetrics(metrics);
322
+ }
323
+ }
324
+
325
+ export {
326
+ collectCheckIssues,
327
+ cmdHeal
328
+ };
@@ -2865,7 +2865,8 @@ function resolvePatternAlias(item, patterns) {
2865
2865
  return item;
2866
2866
  }
2867
2867
  function buildEssenceV3(options, archetypeData, themeHints) {
2868
- let pages = [{ id: "home", layout: ["hero"] }];
2868
+ const isBrownfieldAttach = options.workflowMode === "brownfield-attach";
2869
+ let pages = isBrownfieldAttach ? [{ id: "observed-app", layout: ["existing-surface"] }] : [{ id: "home", layout: ["hero"] }];
2869
2870
  let features = options.features;
2870
2871
  let defaultShell = options.shell || "sidebar-main";
2871
2872
  if (archetypeData?.pages) {
@@ -3710,10 +3711,10 @@ function generateDecantrMdV31(params) {
3710
3711
  WORKFLOW_GUIDANCE: params.workflowMode === "brownfield-attach" ? params.analysisArtifacts ? `This project is using Decantr in **brownfield attach** mode with **${params.adoptionMode || "contract-only"}** adoption.
3711
3712
 
3712
3713
  Read \`.decantr/analysis.json\` first for the detected framework, routes, styling, layout, and dependency facts.
3713
- Then read \`.decantr/init-seed.json\` for the recommended attach defaults.
3714
- Then read \`.decantr/context/scaffold-pack.md\` and \`.decantr/context/scaffold.md\` to understand the Decantr contract you are layering onto the existing app.
3714
+ Then read \`.decantr/doctrine-map.json\`, \`.decantr/ambient-context.json\`, and \`.decantr/brownfield-report.md\` for ranked source precedence, existing assistant rules, docs, design-system evidence, and unresolved doctrine risks.
3715
+ Then read \`.decantr/context/scaffold-pack.md\` and \`.decantr/context/scaffold.md\` to understand the accepted Decantr contract.
3715
3716
 
3716
- Preserve the current framework, package manager, router, and working runtime structure unless the contract gives you a reviewed reason to change them. Map existing routes and components onto the declared Decantr sections/pages before creating new files. Registry content is optional in this workflow unless the task explicitly asks for it.` : `This project is using Decantr in **brownfield attach** mode with **${params.adoptionMode || "contract-only"}** adoption.
3717
+ Treat Decantr as the reconciled contract layer and the original docs/rules as cited evidence. Preserve the current framework, package manager, router, styling system, data boundaries, and working runtime structure unless the contract gives you a reviewed reason to change them. Registry content is optional in this workflow unless the task explicitly asks for it.` : `This project is using Decantr in **brownfield attach** mode with **${params.adoptionMode || "contract-only"}** adoption.
3717
3718
 
3718
3719
  No \`.decantr/analysis.json\` or \`.decantr/init-seed.json\` was present when this context was generated. Inventory the current framework, routes, styling, layout, package manager, and rule files before changing runtime code. Then read \`.decantr/context/scaffold-pack.md\` and \`.decantr/context/scaffold.md\` to understand the Decantr contract you are layering onto the existing app.
3719
3720
 
@@ -3755,7 +3756,16 @@ Start implementation from the shell layouts and shared route structure before fi
3755
3756
  }
3756
3757
  briefLines.push(`- **Guard mode:** ${params.guardMode}`);
3757
3758
  briefLines.push("");
3758
- 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
+ };
3759
3769
  if (params.decoratorDefinitions && Object.keys(params.decoratorDefinitions).length > 0) {
3760
3770
  briefLines.push("### Decorator Quick Reference");
3761
3771
  briefLines.push(