@decantr/cli 1.7.28 → 1.7.29

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,307 @@
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
+ async function cmdHeal(projectRoot = process.cwd(), options = {}) {
209
+ const essencePath = join2(projectRoot, "decantr.essence.json");
210
+ if (!existsSync2(essencePath)) {
211
+ console.error("No decantr.essence.json found. Run `decantr init` first.");
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ const essence = JSON.parse(readFileSync2(essencePath, "utf-8"));
216
+ console.log("Scanning for issues...\n");
217
+ const issues = [];
218
+ const validation = validateEssence(essence);
219
+ if (!validation.valid) {
220
+ for (const err of validation.errors) {
221
+ issues.push({
222
+ type: "error",
223
+ rule: "schema",
224
+ message: err
225
+ });
226
+ }
227
+ }
228
+ let interactionIssues = [];
229
+ try {
230
+ interactionIssues = scanProjectInteractions(projectRoot);
231
+ } catch {
232
+ }
233
+ try {
234
+ const guardContext = buildGuardRegistryContext(projectRoot);
235
+ const violations = evaluateGuard(essence, {
236
+ ...guardContext,
237
+ interaction_issues: interactionIssues
238
+ });
239
+ for (const v of violations) {
240
+ issues.push({
241
+ type: v.severity === "error" ? "error" : "warning",
242
+ rule: v.rule,
243
+ message: v.message,
244
+ suggestion: v.suggestion
245
+ });
246
+ }
247
+ } catch {
248
+ }
249
+ if (options.brownfield) {
250
+ try {
251
+ if (isV3(essence)) {
252
+ const brownfieldIssues = scanBrownfieldIssues(projectRoot, essence);
253
+ issues.push(...brownfieldIssues);
254
+ } else {
255
+ issues.push({
256
+ type: "warning",
257
+ rule: "brownfield-check",
258
+ message: "Brownfield checks require a v3 Decantr essence."
259
+ });
260
+ }
261
+ } catch (e) {
262
+ issues.push({
263
+ type: "warning",
264
+ rule: "brownfield-check",
265
+ message: `Brownfield check could not complete: ${e.message}`
266
+ });
267
+ }
268
+ }
269
+ if (issues.length === 0) {
270
+ console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
271
+ await maybeSendTelemetry(projectRoot, essence, issues, options);
272
+ return;
273
+ }
274
+ console.log(`Found ${issues.length} issue(s):
275
+ `);
276
+ for (const issue of issues) {
277
+ const icon = issue.type === "error" ? `${RED}x${RESET}` : `${YELLOW}!${RESET}`;
278
+ console.log(`${icon} [${issue.rule}] ${issue.message}`);
279
+ if (issue.suggestion) {
280
+ console.log(` ${DIM}Suggestion: ${issue.suggestion}${RESET}`);
281
+ }
282
+ }
283
+ console.log(`
284
+ ${YELLOW}Manual fixes required. Review the issues above.${RESET}`);
285
+ const hasError = issues.some((i) => i.type === "error");
286
+ if (hasError) {
287
+ process.exitCode = 1;
288
+ }
289
+ await maybeSendTelemetry(projectRoot, essence, issues, options);
290
+ }
291
+ async function maybeSendTelemetry(projectRoot, essence, issues, options) {
292
+ if (options.telemetry && !isOptedIn(projectRoot)) {
293
+ optIn(projectRoot);
294
+ console.log(
295
+ `
296
+ ${CYAN}Telemetry enabled.${RESET} Anonymous guard metrics will be sent on future checks.`
297
+ );
298
+ console.log(`${DIM}Set "telemetry": false in .decantr/project.json to opt out.${RESET}`);
299
+ }
300
+ if (isOptedIn(projectRoot)) {
301
+ const metrics = collectMetrics(essence, issues);
302
+ sendGuardMetrics(metrics);
303
+ }
304
+ }
305
+ export {
306
+ cmdHeal
307
+ };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunk-56VBV4MT.js";
2
- import "./chunk-GCDFX7UE.js";
3
- import "./chunk-RRRHQ45P.js";
1
+ import "./chunk-US6RK5QT.js";
2
+ import "./chunk-HULA6E2D.js";
3
+ import "./chunk-DI2PLOJ6.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RegistryClient,
3
3
  refreshDerivedFiles
4
- } from "./chunk-GCDFX7UE.js";
4
+ } from "./chunk-HULA6E2D.js";
5
5
 
6
6
  // src/commands/upgrade.ts
7
7
  import { existsSync, readFileSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.7.28",
3
+ "version": "1.7.29",
4
4
  "description": "Decantr CLI — scaffold, audit, and maintain Decantr projects from the terminal",
5
5
  "author": "Decantr AI",
6
6
  "license": "MIT",
@@ -30,11 +30,11 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
+ "@decantr/telemetry": "0.1.2",
33
34
  "@decantr/core": "1.0.6",
34
- "@decantr/registry": "1.0.4",
35
+ "@decantr/essence-spec": "1.0.7",
35
36
  "@decantr/verifier": "1.0.6",
36
- "@decantr/telemetry": "0.1.2",
37
- "@decantr/essence-spec": "1.0.6"
37
+ "@decantr/registry": "1.0.4"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsup",