@decantr/cli 2.8.1 → 2.9.1
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 +30 -15
- package/dist/bin.js +5 -4
- package/dist/{chunk-KT2ROK2D.js → chunk-34TZXWIF.js} +1858 -1542
- package/dist/{chunk-RKZMHS2K.js → chunk-6UDJDQPT.js} +3021 -2005
- package/dist/{chunk-FV6DGYD7.js → chunk-FKM4OQDF.js} +106 -18
- package/dist/{chunk-V3XAQWKD.js → chunk-RXF7ZYGK.js} +22 -8
- package/dist/{chunk-PAF4PBD3.js → chunk-TMOCTDYY.js} +28 -8
- package/dist/{content-health-QQHBR6XG.js → content-health-4KP2EGTI.js} +27 -10
- package/dist/{heal-ZYD6NVGE.js → heal-2BDT7TR5.js} +1 -2
- package/dist/{health-ETZXWGTW.js → health-Q7XF3I5Z.js} +2 -3
- package/dist/index.js +5 -4
- package/dist/{studio-G3YOU5YF.js → studio-EDQMI6JE.js} +3 -5
- package/dist/{upgrade-U2BTWJJJ.js → upgrade-VON7Y3LG.js} +1 -1
- package/dist/{workspace-U7J3CJY3.js → workspace-JA2RZI6V.js} +3 -5
- package/package.json +4 -4
- package/dist/chunk-3TH5PLFO.js +0 -331
- package/dist/chunk-VE6N3XWG.js +0 -78
|
@@ -1,1756 +1,2073 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { existsSync
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// src/commands/heal.ts
|
|
2
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3
|
+
import { join as join10 } from "path";
|
|
4
|
+
import { evaluateGuard, isV4, validateEssence } from "@decantr/essence-spec";
|
|
5
|
+
|
|
6
|
+
// src/brownfield-check.ts
|
|
7
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
8
|
+
import { join as join5 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/ambient-context.ts
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
12
|
+
import { basename, extname, join, relative, sep } from "path";
|
|
13
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
14
|
+
".decantr",
|
|
15
|
+
".git",
|
|
16
|
+
".next",
|
|
17
|
+
".nuxt",
|
|
18
|
+
".svelte-kit",
|
|
19
|
+
".turbo",
|
|
20
|
+
".vercel",
|
|
21
|
+
"build",
|
|
22
|
+
"coverage",
|
|
23
|
+
"dist",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"playwright-report"
|
|
26
|
+
]);
|
|
27
|
+
var ROOT_CONTEXT_FILES = /* @__PURE__ */ new Set([
|
|
28
|
+
"AGENTS.md",
|
|
29
|
+
"CLAUDE.md",
|
|
30
|
+
"GEMINI.md",
|
|
31
|
+
"README.md",
|
|
32
|
+
"copilot-instructions.md",
|
|
33
|
+
".cursorrules",
|
|
34
|
+
".windsurfrules",
|
|
35
|
+
".cursorignore",
|
|
36
|
+
".claudeignore",
|
|
37
|
+
"components.json",
|
|
38
|
+
"tailwind.config.js",
|
|
39
|
+
"tailwind.config.ts",
|
|
40
|
+
"tailwind.config.mjs",
|
|
41
|
+
"tailwind.config.cjs",
|
|
42
|
+
"next.config.js",
|
|
43
|
+
"next.config.ts",
|
|
44
|
+
"next.config.mjs",
|
|
45
|
+
"nuxt.config.js",
|
|
46
|
+
"nuxt.config.ts",
|
|
47
|
+
"astro.config.mjs",
|
|
48
|
+
"astro.config.ts",
|
|
49
|
+
"svelte.config.js",
|
|
50
|
+
"svelte.config.ts",
|
|
51
|
+
"angular.json",
|
|
52
|
+
"vite.config.js",
|
|
53
|
+
"vite.config.ts",
|
|
54
|
+
"vitest.config.ts",
|
|
55
|
+
"vitest.config.js",
|
|
56
|
+
"playwright.config.ts",
|
|
57
|
+
"playwright.config.js",
|
|
58
|
+
"tsconfig.json",
|
|
59
|
+
"package.json",
|
|
60
|
+
"decantr.essence.json"
|
|
61
|
+
]);
|
|
62
|
+
var CONTEXT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
63
|
+
".agents",
|
|
64
|
+
".claude",
|
|
65
|
+
".claude/initiatives",
|
|
66
|
+
".claude/rules",
|
|
67
|
+
".codex",
|
|
68
|
+
".cursor",
|
|
69
|
+
".cursor/rules",
|
|
70
|
+
".github/workflows",
|
|
71
|
+
"docs",
|
|
72
|
+
"docs/initiatives",
|
|
73
|
+
"initiatives",
|
|
74
|
+
"memory",
|
|
75
|
+
"memories",
|
|
76
|
+
"project-memory",
|
|
77
|
+
"supabase"
|
|
78
|
+
]);
|
|
79
|
+
function shouldSkipDir(name) {
|
|
80
|
+
return SKIP_DIRS.has(name);
|
|
12
81
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
const candidate = join(dir, `${id}.json`);
|
|
16
|
-
if (existsSync(candidate)) return candidate;
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
82
|
+
function normalizedPath(relPath) {
|
|
83
|
+
return relPath.split(sep).join("/");
|
|
19
84
|
}
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
85
|
+
function isPotentialContextFile(relPath, name) {
|
|
86
|
+
const normalized2 = normalizedPath(relPath);
|
|
87
|
+
if (ROOT_CONTEXT_FILES.has(name)) return true;
|
|
88
|
+
if (name.startsWith(".env")) return true;
|
|
89
|
+
if (normalized2.startsWith(".claude/")) return true;
|
|
90
|
+
if (normalized2.startsWith(".agents/")) return true;
|
|
91
|
+
if (normalized2.startsWith(".codex/")) return true;
|
|
92
|
+
if (normalized2.startsWith(".cursor/")) return true;
|
|
93
|
+
if (normalized2.startsWith(".github/workflows/")) return true;
|
|
94
|
+
if (normalized2.startsWith("docs/")) return true;
|
|
95
|
+
if (normalized2.startsWith("initiatives/")) return true;
|
|
96
|
+
if (normalized2.startsWith("memory/")) return true;
|
|
97
|
+
if (normalized2.startsWith("memories/")) return true;
|
|
98
|
+
if (normalized2.startsWith("project-memory/")) return true;
|
|
99
|
+
if (normalized2.startsWith("supabase/")) return true;
|
|
100
|
+
if (normalized2.startsWith("migrations/")) return true;
|
|
101
|
+
if (normalized2.startsWith("db/")) return true;
|
|
102
|
+
if (normalized2.startsWith("ROLEMIGRATIONS/")) return true;
|
|
103
|
+
if (normalized2 === "src/middleware.ts" || normalized2 === "middleware.ts") return true;
|
|
104
|
+
if (normalized2.includes("/middleware.")) return true;
|
|
105
|
+
const ext = extname(name).toLowerCase();
|
|
106
|
+
return ext === ".md" || ext === ".mdx" || ext === ".sql" || ext === ".yml" || ext === ".yaml";
|
|
29
107
|
}
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
41
|
-
entries.push({ id, data, path });
|
|
42
|
-
seen.add(id);
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
108
|
+
function classifyContext(relPath) {
|
|
109
|
+
const normalized2 = normalizedPath(relPath);
|
|
110
|
+
const lower = normalized2.toLowerCase();
|
|
111
|
+
const name = basename(normalized2);
|
|
112
|
+
if (lower === "decantr.essence.json") {
|
|
113
|
+
return {
|
|
114
|
+
role: "architecture",
|
|
115
|
+
confidence: 0.82,
|
|
116
|
+
reason: "existing Decantr contract evidence"
|
|
117
|
+
};
|
|
46
118
|
}
|
|
47
|
-
|
|
119
|
+
if (lower === ".claude/initiatives" || lower === "docs/initiatives" || lower === "initiatives" || lower === "memory" || lower === "memories" || lower === "project-memory" || lower.startsWith(".claude/initiatives/") || lower.startsWith("docs/initiatives/") || lower.startsWith("initiatives/") || lower.startsWith("memory/") || lower.startsWith("memories/") || lower.startsWith("project-memory/") || lower.includes("/feature/") || lower.includes("feature") || lower.includes("rbac") || lower.includes("billing") || lower.includes("admin") || lower.includes("dashboard")) {
|
|
120
|
+
return {
|
|
121
|
+
role: "feature-business",
|
|
122
|
+
confidence: 0.78,
|
|
123
|
+
reason: "feature, initiative, memory, or business-domain evidence"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (lower === ".agents" || lower === ".claude" || lower === ".codex" || lower === ".cursor" || lower === "claude.md" || lower === "agents.md" || lower === "gemini.md" || lower === "copilot-instructions.md" || lower === ".cursorrules" || lower === ".windsurfrules" || lower.startsWith(".claude/") || lower.startsWith(".agents/") || lower.startsWith(".codex/") || lower.startsWith(".cursor/rules/")) {
|
|
127
|
+
return {
|
|
128
|
+
role: "assistant-specific",
|
|
129
|
+
confidence: 0.98,
|
|
130
|
+
reason: "assistant or AI-agent instruction surface"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (lower.includes("security") || lower.includes("auth") || lower.includes("rls") || lower.includes("schema") || lower.includes("migration") || lower.startsWith("supabase/") || lower.startsWith("migrations/") || lower.startsWith("db/") || lower.startsWith("rolemigrations/") || lower.includes("middleware.")) {
|
|
134
|
+
return {
|
|
135
|
+
role: "security-data",
|
|
136
|
+
confidence: 0.9,
|
|
137
|
+
reason: "security, auth, schema, middleware, or data-governance evidence"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (lower.includes("design-system") || lower === "components.json" || lower.startsWith("tailwind.config") || lower.includes("ui-components") || lower.includes("colors") || lower.includes("typography") || lower.includes("spacing")) {
|
|
141
|
+
return {
|
|
142
|
+
role: "design-system",
|
|
143
|
+
confidence: 0.88,
|
|
144
|
+
reason: "design system or styling convention evidence"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (lower.startsWith(".github/workflows/") || lower.includes("workflow") || lower.includes("testing") || lower.includes("deployment") || lower.includes("vitest.config") || lower.includes("playwright.config") || lower === "package.json") {
|
|
148
|
+
return {
|
|
149
|
+
role: "workflow-ci",
|
|
150
|
+
confidence: 0.84,
|
|
151
|
+
reason: "workflow, CI, deployment, or validation command evidence"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (lower === "docs" || lower.includes("architecture") || lower === "readme.md" || lower.includes("setup") || lower.includes("contributing") || name === "tsconfig.json" || lower.endsWith("config.ts") || lower.endsWith("config.js") || lower.endsWith("config.mjs")) {
|
|
155
|
+
return {
|
|
156
|
+
role: "architecture",
|
|
157
|
+
confidence: 0.72,
|
|
158
|
+
reason: "architecture, setup, or framework configuration evidence"
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (lower.includes("complete") || lower.includes("summary") || lower.includes("deprecated") || lower.includes("legacy") || lower.includes("migration")) {
|
|
162
|
+
return {
|
|
163
|
+
role: "stale-or-historical",
|
|
164
|
+
confidence: 0.64,
|
|
165
|
+
reason: "historical or possibly stale project documentation"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { role: "unknown", confidence: 0.35, reason: "unclassified context candidate" };
|
|
48
169
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
55
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
56
|
-
import {
|
|
57
|
-
createFetchTelemetrySink,
|
|
58
|
-
createTelemetryClient,
|
|
59
|
-
isTelemetryActorType
|
|
60
|
-
} from "@decantr/telemetry";
|
|
61
|
-
var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
|
|
62
|
-
var DEFAULT_TELEMETRY_EVENTS_ENDPOINT = "https://api.decantr.ai/v1/telemetry/events";
|
|
63
|
-
var TELEMETRY_TIMEOUT_MS = 3e3;
|
|
64
|
-
var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
|
|
65
|
-
async function sendGuardMetrics(metrics) {
|
|
66
|
-
try {
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
69
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: { "Content-Type": "application/json" },
|
|
72
|
-
body: JSON.stringify(metrics),
|
|
73
|
-
signal: controller.signal
|
|
74
|
-
});
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
} catch {
|
|
170
|
+
function isSafeToCite(relPath) {
|
|
171
|
+
const lower = normalizedPath(relPath).toLowerCase();
|
|
172
|
+
if (lower.startsWith(".env") && lower !== ".env.example" && lower !== ".env.sample") return false;
|
|
173
|
+
if (lower.includes("secret") || lower.includes("private-key") || lower.includes("credentials")) {
|
|
174
|
+
return false;
|
|
77
175
|
}
|
|
176
|
+
return true;
|
|
78
177
|
}
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
178
|
+
function addDirectoryContext(items, projectRoot, relPath) {
|
|
179
|
+
const fullPath = join(projectRoot, relPath);
|
|
180
|
+
if (!existsSync(fullPath)) return;
|
|
181
|
+
const stats = statSync(fullPath);
|
|
182
|
+
const classified = classifyContext(relPath);
|
|
183
|
+
items.push({
|
|
184
|
+
path: normalizedPath(relPath),
|
|
185
|
+
type: "directory",
|
|
186
|
+
role: classified.role,
|
|
187
|
+
confidence: classified.confidence,
|
|
188
|
+
sizeBytes: stats.size,
|
|
189
|
+
safeToCite: isSafeToCite(relPath),
|
|
190
|
+
reason: classified.reason
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function walk(projectRoot, dir, items, depth) {
|
|
194
|
+
if (depth > 6) return;
|
|
195
|
+
let entries;
|
|
82
196
|
try {
|
|
83
|
-
|
|
84
|
-
return data.telemetry === true;
|
|
197
|
+
entries = readdirSync(dir);
|
|
85
198
|
} catch {
|
|
86
|
-
return
|
|
199
|
+
return;
|
|
87
200
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (shouldSkipDir(entry)) continue;
|
|
203
|
+
const fullPath = join(dir, entry);
|
|
204
|
+
const relPath = normalizedPath(relative(projectRoot, fullPath));
|
|
205
|
+
let stats;
|
|
93
206
|
try {
|
|
94
|
-
|
|
207
|
+
stats = statSync(fullPath);
|
|
95
208
|
} catch {
|
|
209
|
+
continue;
|
|
96
210
|
}
|
|
211
|
+
if (stats.isDirectory()) {
|
|
212
|
+
if (CONTEXT_DIRECTORIES.has(relPath)) {
|
|
213
|
+
addDirectoryContext(items, projectRoot, relPath);
|
|
214
|
+
}
|
|
215
|
+
walk(projectRoot, fullPath, items, depth + 1);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!stats.isFile() || !isPotentialContextFile(relPath, entry)) continue;
|
|
219
|
+
const classified = classifyContext(relPath);
|
|
220
|
+
items.push({
|
|
221
|
+
path: relPath,
|
|
222
|
+
type: "file",
|
|
223
|
+
role: classified.role,
|
|
224
|
+
confidence: classified.confidence,
|
|
225
|
+
sizeBytes: stats.size,
|
|
226
|
+
safeToCite: isSafeToCite(relPath),
|
|
227
|
+
reason: classified.reason
|
|
228
|
+
});
|
|
97
229
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
const identities = ensureTelemetryIdentities(projectRoot);
|
|
111
|
-
if (!identities) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const registrySource = input.registrySource ?? getRegistrySourceProperty(input.properties) ?? inferRegistrySource(input.args ?? []);
|
|
115
|
-
const client = createTelemetryClient({
|
|
116
|
-
sink: createFetchTelemetrySink({
|
|
117
|
-
endpoint: getTelemetryEventsEndpoint(),
|
|
118
|
-
timeoutMs: TELEMETRY_TIMEOUT_MS
|
|
119
|
-
})
|
|
120
|
-
});
|
|
121
|
-
const event = {
|
|
122
|
-
name: input.name,
|
|
123
|
-
context: {
|
|
124
|
-
source: "cli",
|
|
125
|
-
actorType: getTelemetryActorType(),
|
|
126
|
-
environment: "production",
|
|
127
|
-
decantrVersion: getCliVersion(),
|
|
128
|
-
installId: identities.installId,
|
|
129
|
-
projectId: identities.projectId,
|
|
130
|
-
registrySource
|
|
131
|
-
},
|
|
132
|
-
properties: input.properties
|
|
230
|
+
}
|
|
231
|
+
function summarize(items) {
|
|
232
|
+
const summary = {
|
|
233
|
+
"assistant-specific": 0,
|
|
234
|
+
"security-data": 0,
|
|
235
|
+
architecture: 0,
|
|
236
|
+
"design-system": 0,
|
|
237
|
+
"workflow-ci": 0,
|
|
238
|
+
"feature-business": 0,
|
|
239
|
+
"stale-or-historical": 0,
|
|
240
|
+
unknown: 0
|
|
133
241
|
};
|
|
242
|
+
for (const item of items) summary[item.role] += 1;
|
|
243
|
+
return summary;
|
|
244
|
+
}
|
|
245
|
+
function readSmallText(projectRoot, relPath) {
|
|
246
|
+
const fullPath = join(projectRoot, relPath);
|
|
134
247
|
try {
|
|
135
|
-
|
|
248
|
+
const stat = statSync(fullPath);
|
|
249
|
+
if (stat.size > 64e3) return "";
|
|
250
|
+
return readFileSync(fullPath, "utf-8");
|
|
136
251
|
} catch {
|
|
252
|
+
return "";
|
|
137
253
|
}
|
|
138
254
|
}
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
255
|
+
function detectConflicts(projectRoot, items) {
|
|
256
|
+
const text = items.filter(
|
|
257
|
+
(item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
|
|
258
|
+
).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
|
|
259
|
+
const conflicts = [];
|
|
260
|
+
const frameworkSignals = [
|
|
261
|
+
["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
|
|
262
|
+
["angular", /\bangular\b/],
|
|
263
|
+
["svelte", /\bsvelte\b|\bsveltekit\b/],
|
|
264
|
+
["vue", /\bvue\b|\bnuxt\b/]
|
|
265
|
+
].filter(([, pattern]) => pattern.test(text));
|
|
266
|
+
if (frameworkSignals.length > 1) {
|
|
267
|
+
conflicts.push(
|
|
268
|
+
`Multiple framework doctrines appear in ambient docs: ${frameworkSignals.map(([name]) => name).join(", ")}.`
|
|
269
|
+
);
|
|
144
270
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
projectRoot,
|
|
150
|
-
success: input.success
|
|
151
|
-
});
|
|
152
|
-
await captureCliTelemetryEvent({
|
|
153
|
-
args: input.args,
|
|
154
|
-
name: "cli.command.completed",
|
|
155
|
-
projectRoot,
|
|
156
|
-
properties,
|
|
157
|
-
registrySource: properties.registrySource
|
|
158
|
-
});
|
|
159
|
-
const lifecycleEventName = lifecycleTelemetryEventName(command);
|
|
160
|
-
if (lifecycleEventName) {
|
|
161
|
-
await captureCliTelemetryEvent({
|
|
162
|
-
args: input.args,
|
|
163
|
-
name: lifecycleEventName,
|
|
164
|
-
projectRoot,
|
|
165
|
-
properties,
|
|
166
|
-
registrySource: properties.registrySource
|
|
167
|
-
});
|
|
271
|
+
const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
|
|
272
|
+
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
|
|
273
|
+
if (forbidsTailwind && endorsesTailwind) {
|
|
274
|
+
conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
|
|
168
275
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const projectRoot = input.projectRoot ?? process.cwd();
|
|
172
|
-
const properties = buildProjectHealthTelemetryProperties(input, projectRoot);
|
|
173
|
-
await captureCliTelemetryEvent({
|
|
174
|
-
name: "health.report.generated",
|
|
175
|
-
projectRoot,
|
|
176
|
-
properties
|
|
177
|
-
});
|
|
178
|
-
if (input.report.status === "healthy") {
|
|
179
|
-
await captureCliTelemetryEvent({
|
|
180
|
-
name: "decantr.health.healthy",
|
|
181
|
-
projectRoot,
|
|
182
|
-
properties
|
|
183
|
-
});
|
|
276
|
+
if (/\bclient component\b/.test(text) && /\bserver components? only\b/.test(text)) {
|
|
277
|
+
conflicts.push("Ambient docs may conflict on client vs server component boundaries.");
|
|
184
278
|
}
|
|
279
|
+
return conflicts;
|
|
185
280
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
281
|
+
function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
282
|
+
if (!items.some((item) => item.path === "decantr.essence.json")) return [];
|
|
283
|
+
const content = readSmallText(projectRoot, "decantr.essence.json");
|
|
284
|
+
if (!content) return [];
|
|
285
|
+
try {
|
|
286
|
+
const essence = JSON.parse(content);
|
|
287
|
+
const risks = [];
|
|
288
|
+
if (essence.version !== "4.0.0") {
|
|
289
|
+
risks.push(
|
|
290
|
+
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; run decantr migrate --to v4 or review before treating it as current brownfield doctrine.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
|
|
294
|
+
risks.push(
|
|
295
|
+
"decantr.essence.json looks like an older Decantr default scaffold; verify before importing its theme or page layout as brownfield truth."
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return risks;
|
|
299
|
+
} catch {
|
|
300
|
+
return [
|
|
301
|
+
"decantr.essence.json could not be parsed during ambient inventory; review before treating it as current doctrine."
|
|
302
|
+
];
|
|
303
|
+
}
|
|
209
304
|
}
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
projectRoot,
|
|
218
|
-
success: input.success
|
|
219
|
-
});
|
|
220
|
-
const properties = {
|
|
221
|
-
...base,
|
|
222
|
-
command: "new"
|
|
223
|
-
};
|
|
224
|
-
await captureCliTelemetryEvent({
|
|
225
|
-
args,
|
|
226
|
-
name: "decantr.new.completed",
|
|
227
|
-
projectRoot,
|
|
228
|
-
properties,
|
|
229
|
-
registrySource: properties.registrySource
|
|
230
|
-
});
|
|
305
|
+
function detectStaleRisks(projectRoot, items) {
|
|
306
|
+
const pathRisks = items.filter(
|
|
307
|
+
(item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
|
|
308
|
+
).slice(0, 12).map(
|
|
309
|
+
(item) => `${item.path} may be historical; verify before treating it as current doctrine.`
|
|
310
|
+
);
|
|
311
|
+
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
231
312
|
}
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
313
|
+
function scanAmbientContext(projectRoot) {
|
|
314
|
+
const items = [];
|
|
315
|
+
walk(projectRoot, projectRoot, items, 0);
|
|
316
|
+
const deduped = [...new Map(items.map((item) => [item.path, item])).values()].sort(
|
|
317
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
318
|
+
);
|
|
319
|
+
return {
|
|
320
|
+
version: 1,
|
|
321
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
322
|
+
items: deduped,
|
|
323
|
+
summary: summarize(deduped),
|
|
324
|
+
conflicts: detectConflicts(projectRoot, deduped),
|
|
325
|
+
staleRisks: detectStaleRisks(projectRoot, deduped)
|
|
244
326
|
};
|
|
245
|
-
await captureCliTelemetryEvent({
|
|
246
|
-
name: "health.finding.prompt_requested",
|
|
247
|
-
projectRoot,
|
|
248
|
-
properties
|
|
249
|
-
});
|
|
250
327
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
};
|
|
259
|
-
await captureCliTelemetryEvent({
|
|
260
|
-
name: "health.ci.failed",
|
|
261
|
-
projectRoot,
|
|
262
|
-
properties
|
|
263
|
-
});
|
|
328
|
+
|
|
329
|
+
// src/analyzers/routes.ts
|
|
330
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
331
|
+
import { join as join2, relative as relative2 } from "path";
|
|
332
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "api", "_app", "_document"]);
|
|
333
|
+
function shouldSkipDir2(name) {
|
|
334
|
+
return name.startsWith("_") || name.startsWith(".") || SKIP_DIRS2.has(name);
|
|
264
335
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
projectScope: inferProjectScope(projectRoot),
|
|
274
|
-
workflowMode: metadata.workflowMode
|
|
275
|
-
};
|
|
276
|
-
await captureCliTelemetryEvent({
|
|
277
|
-
name: "studio.started",
|
|
278
|
-
projectRoot,
|
|
279
|
-
properties
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
async function sendStudioHealthRefreshedTelemetry(input) {
|
|
283
|
-
const projectRoot = input.projectRoot ?? process.cwd();
|
|
284
|
-
const properties = {
|
|
285
|
-
...buildProjectHealthTelemetryProperties(input, projectRoot),
|
|
286
|
-
trigger: input.trigger ?? "api-refresh"
|
|
287
|
-
};
|
|
288
|
-
await captureCliTelemetryEvent({
|
|
289
|
-
name: "studio.health_refreshed",
|
|
290
|
-
projectRoot,
|
|
291
|
-
properties
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
function buildCliLifecycleProperties(input) {
|
|
295
|
-
const metadata = readProjectTelemetryMetadata(input.projectRoot);
|
|
296
|
-
const registrySource = inferRegistrySource(input.args);
|
|
297
|
-
return {
|
|
298
|
-
command: input.command,
|
|
299
|
-
success: input.success,
|
|
300
|
-
durationMs: input.durationMs,
|
|
301
|
-
adoptionMode: inferAdoptionMode(input.args) ?? metadata.adoptionMode,
|
|
302
|
-
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
303
|
-
offline: input.args.includes("--offline"),
|
|
304
|
-
projectScope: metadata.projectScope ?? inferProjectScope(input.projectRoot),
|
|
305
|
-
registrySource,
|
|
306
|
-
targetFramework: inferFlagValue(input.args, "--target"),
|
|
307
|
-
workflowMode: inferWorkflowMode(input.args) ?? metadata.workflowMode
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
function lifecycleTelemetryEventName(command) {
|
|
311
|
-
if (command === "check") return "decantr.check.completed";
|
|
312
|
-
if (command === "init") return "decantr.init.completed";
|
|
313
|
-
if (command === "refresh") return "decantr.refresh.completed";
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
function buildProjectHealthTelemetryProperties(input, projectRoot) {
|
|
317
|
-
const { report } = input;
|
|
318
|
-
return {
|
|
319
|
-
success: true,
|
|
320
|
-
status: report.status,
|
|
321
|
-
score: report.score,
|
|
322
|
-
durationMs: input.durationMs,
|
|
323
|
-
adoptionMode: normalizeAdoptionMode(report.summary.adoptionMode),
|
|
324
|
-
ci: input.ci ?? false,
|
|
325
|
-
errorCount: report.summary.errorCount,
|
|
326
|
-
failOn: input.failOn,
|
|
327
|
-
findingCount: report.summary.findingCount,
|
|
328
|
-
format: input.format,
|
|
329
|
-
infoCount: report.summary.infoCount,
|
|
330
|
-
outputWritten: input.outputWritten ?? false,
|
|
331
|
-
packManifestPresent: report.summary.packManifestPresent,
|
|
332
|
-
pageCount: report.summary.pageCount,
|
|
333
|
-
projectScope: inferProjectScope(projectRoot),
|
|
334
|
-
reviewPackPresent: report.summary.reviewPackPresent,
|
|
335
|
-
routeCount: report.routes.declared.length,
|
|
336
|
-
runtimeAuditChecked: report.summary.runtimeAuditChecked,
|
|
337
|
-
runtimeMatchedCount: report.routes.runtimeMatched,
|
|
338
|
-
runtimePassed: report.summary.runtimePassed,
|
|
339
|
-
runtimeRouteCheckedCount: report.routes.runtimeChecked.length,
|
|
340
|
-
warnCount: report.summary.warnCount,
|
|
341
|
-
workflowMode: normalizeWorkflowMode(report.summary.workflowMode)
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
function collectMetrics(essence, issues) {
|
|
345
|
-
const dna = essence.dna ?? {};
|
|
346
|
-
const blueprint = essence.blueprint ?? {};
|
|
347
|
-
const meta = essence.meta ?? {};
|
|
348
|
-
const guard = meta.guard ?? {};
|
|
349
|
-
const theme = dna.theme ?? {};
|
|
350
|
-
const sections = blueprint.sections ?? [];
|
|
351
|
-
const routes = blueprint.routes ?? {};
|
|
352
|
-
const byRule = {};
|
|
353
|
-
let dnaCount = 0;
|
|
354
|
-
let blueprintCount = 0;
|
|
355
|
-
for (const issue of issues) {
|
|
356
|
-
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
|
|
357
|
-
if (DNA_RULES.has(issue.rule)) {
|
|
358
|
-
dnaCount++;
|
|
359
|
-
} else {
|
|
360
|
-
blueprintCount++;
|
|
336
|
+
function segmentToRoute(segment) {
|
|
337
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
341
|
+
const param = segment.slice(1, -1);
|
|
342
|
+
if (param.startsWith("...")) {
|
|
343
|
+
return `:${param.slice(3)}*`;
|
|
361
344
|
}
|
|
345
|
+
if (param.startsWith("[...") && param.endsWith("]")) {
|
|
346
|
+
return `:${param.slice(4, -1)}*`;
|
|
347
|
+
}
|
|
348
|
+
return `:${param}`;
|
|
362
349
|
}
|
|
363
|
-
return
|
|
364
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
365
|
-
cli_version: getCliVersion(),
|
|
366
|
-
essence_version: essence.version ?? "unknown",
|
|
367
|
-
guard_mode: guard.mode ?? "unknown",
|
|
368
|
-
violations: {
|
|
369
|
-
dna: dnaCount,
|
|
370
|
-
blueprint: blueprintCount,
|
|
371
|
-
by_rule: byRule
|
|
372
|
-
},
|
|
373
|
-
resolution_rate: 0,
|
|
374
|
-
sections_count: sections.length,
|
|
375
|
-
routes_count: Object.keys(routes).length,
|
|
376
|
-
theme: theme.id ?? "unknown"
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
function getCliTelemetryIdentityStatus(projectRoot, options = {}) {
|
|
380
|
-
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
381
|
-
const hasProjectConfig = existsSync2(projectJsonPath);
|
|
382
|
-
const identities = options.create ? ensureTelemetryIdentities(projectRoot) : null;
|
|
383
|
-
const projectData = readProjectJson(projectRoot);
|
|
384
|
-
return {
|
|
385
|
-
enabled: projectData?.telemetry === true,
|
|
386
|
-
hasProjectConfig,
|
|
387
|
-
installId: identities?.installId ?? readExistingInstallId(),
|
|
388
|
-
projectId: identities?.projectId ?? readStringProperty(projectData, "telemetryProjectId"),
|
|
389
|
-
projectRoot
|
|
390
|
-
};
|
|
350
|
+
return segment;
|
|
391
351
|
}
|
|
392
|
-
function
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
if (!existsSync2(projectJsonPath)) {
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
352
|
+
function walkAppDir(dir, baseDir, segments) {
|
|
353
|
+
const routes = [];
|
|
354
|
+
let entries;
|
|
398
355
|
try {
|
|
399
|
-
|
|
400
|
-
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
401
|
-
if (!projectId) {
|
|
402
|
-
projectId = `project_${randomUUID()}`;
|
|
403
|
-
data.telemetryProjectId = projectId;
|
|
404
|
-
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
405
|
-
}
|
|
406
|
-
return { installId, projectId };
|
|
356
|
+
entries = readdirSync2(dir);
|
|
407
357
|
} catch {
|
|
408
|
-
return
|
|
358
|
+
return routes;
|
|
359
|
+
}
|
|
360
|
+
const hasPage = entries.some(
|
|
361
|
+
(e) => e === "page.tsx" || e === "page.ts" || e === "page.jsx" || e === "page.js"
|
|
362
|
+
);
|
|
363
|
+
const hasLayout = entries.some(
|
|
364
|
+
(e) => e === "layout.tsx" || e === "layout.ts" || e === "layout.jsx" || e === "layout.js"
|
|
365
|
+
);
|
|
366
|
+
if (hasPage) {
|
|
367
|
+
const routePath = "/" + segments.filter((s) => s !== "").join("/");
|
|
368
|
+
const pageFile = entries.find((e) => e.startsWith("page."));
|
|
369
|
+
routes.push({
|
|
370
|
+
path: routePath || "/",
|
|
371
|
+
file: relative2(baseDir, join2(dir, pageFile)),
|
|
372
|
+
hasLayout
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
if (shouldSkipDir2(entry)) continue;
|
|
377
|
+
const fullPath = join2(dir, entry);
|
|
378
|
+
try {
|
|
379
|
+
if (!statSync2(fullPath).isDirectory()) continue;
|
|
380
|
+
} catch {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const routeSegment = segmentToRoute(entry);
|
|
384
|
+
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
385
|
+
routes.push(...walkAppDir(fullPath, baseDir, nextSegments));
|
|
409
386
|
}
|
|
387
|
+
return routes;
|
|
410
388
|
}
|
|
411
|
-
function
|
|
412
|
-
const
|
|
413
|
-
|
|
389
|
+
function walkPagesDir(dir, baseDir, segments, extensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "md", "mdx"])) {
|
|
390
|
+
const routes = [];
|
|
391
|
+
let entries;
|
|
414
392
|
try {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
393
|
+
entries = readdirSync2(dir);
|
|
394
|
+
} catch {
|
|
395
|
+
return routes;
|
|
396
|
+
}
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
if (shouldSkipDir2(entry)) continue;
|
|
399
|
+
const fullPath = join2(dir, entry);
|
|
400
|
+
try {
|
|
401
|
+
const stat = statSync2(fullPath);
|
|
402
|
+
if (stat.isDirectory()) {
|
|
403
|
+
const routeSegment = segmentToRoute(entry);
|
|
404
|
+
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
405
|
+
routes.push(...walkPagesDir(fullPath, baseDir, nextSegments, extensions));
|
|
406
|
+
} else if (stat.isFile()) {
|
|
407
|
+
const match = entry.match(/^(.+)\.([^.]+)$/);
|
|
408
|
+
if (!match) continue;
|
|
409
|
+
const name = match[1];
|
|
410
|
+
const extension = match[2];
|
|
411
|
+
if (!extensions.has(extension)) continue;
|
|
412
|
+
if (name.startsWith("_")) continue;
|
|
413
|
+
const routeSegment = name === "index" ? "" : segmentToRoute(name) ?? name;
|
|
414
|
+
const routePath = "/" + [...segments, routeSegment].filter((s) => s !== "").join("/");
|
|
415
|
+
routes.push({
|
|
416
|
+
path: routePath || "/",
|
|
417
|
+
file: relative2(baseDir, fullPath),
|
|
418
|
+
hasLayout: false
|
|
419
|
+
});
|
|
419
420
|
}
|
|
420
|
-
|
|
421
|
-
data.telemetryInstallId = installId2;
|
|
422
|
-
writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
423
|
-
return installId2;
|
|
421
|
+
} catch {
|
|
424
422
|
}
|
|
425
|
-
mkdirSync(configDir, { recursive: true });
|
|
426
|
-
const installId = `install_${randomUUID()}`;
|
|
427
|
-
writeFileSync(
|
|
428
|
-
configPath,
|
|
429
|
-
JSON.stringify({ telemetryInstallId: installId }, null, 2) + "\n",
|
|
430
|
-
"utf-8"
|
|
431
|
-
);
|
|
432
|
-
return installId;
|
|
433
|
-
} catch {
|
|
434
|
-
return `install_${randomUUID()}`;
|
|
435
423
|
}
|
|
424
|
+
return routes;
|
|
436
425
|
}
|
|
437
|
-
function
|
|
438
|
-
const
|
|
439
|
-
|
|
426
|
+
function walkSvelteKitRoutes(dir, baseDir, segments) {
|
|
427
|
+
const routes = [];
|
|
428
|
+
let entries;
|
|
440
429
|
try {
|
|
441
|
-
|
|
442
|
-
return readStringProperty(data, "telemetryInstallId");
|
|
430
|
+
entries = readdirSync2(dir);
|
|
443
431
|
} catch {
|
|
444
|
-
return
|
|
432
|
+
return routes;
|
|
433
|
+
}
|
|
434
|
+
const pageFile = entries.find((entry) => /^\+page\.(svelte|ts|js)$/.test(entry));
|
|
435
|
+
const hasLayout = entries.some((entry) => /^\+layout\.(svelte|ts|js)$/.test(entry));
|
|
436
|
+
if (pageFile) {
|
|
437
|
+
const routePath = "/" + segments.filter((segment) => segment !== "").join("/");
|
|
438
|
+
routes.push({
|
|
439
|
+
path: routePath || "/",
|
|
440
|
+
file: relative2(baseDir, join2(dir, pageFile)),
|
|
441
|
+
hasLayout
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
if (shouldSkipDir2(entry)) continue;
|
|
446
|
+
const fullPath = join2(dir, entry);
|
|
447
|
+
try {
|
|
448
|
+
if (!statSync2(fullPath).isDirectory()) continue;
|
|
449
|
+
} catch {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const routeSegment = segmentToRoute(entry);
|
|
453
|
+
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
454
|
+
routes.push(...walkSvelteKitRoutes(fullPath, baseDir, nextSegments));
|
|
445
455
|
}
|
|
456
|
+
return routes;
|
|
446
457
|
}
|
|
447
|
-
|
|
448
|
-
|
|
458
|
+
var ROUTER_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
|
|
459
|
+
function collectRouteCandidateFiles(dir, files, depth = 0) {
|
|
460
|
+
if (depth > 5) return;
|
|
461
|
+
let entries;
|
|
462
|
+
try {
|
|
463
|
+
entries = readdirSync2(dir);
|
|
464
|
+
} catch {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const entry of entries) {
|
|
468
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
469
|
+
const fullPath = join2(dir, entry);
|
|
470
|
+
try {
|
|
471
|
+
const stat = statSync2(fullPath);
|
|
472
|
+
if (stat.isDirectory()) {
|
|
473
|
+
collectRouteCandidateFiles(fullPath, files, depth + 1);
|
|
474
|
+
} else if (stat.isFile()) {
|
|
475
|
+
const ext = entry.slice(entry.lastIndexOf("."));
|
|
476
|
+
if (ROUTER_FILE_EXTENSIONS.has(ext)) {
|
|
477
|
+
files.push(fullPath);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
449
483
|
}
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
484
|
+
function scanReactRouter(projectRoot) {
|
|
485
|
+
const candidateDirs = [join2(projectRoot, "src"), projectRoot];
|
|
486
|
+
const candidateFiles = [];
|
|
487
|
+
for (const dir of candidateDirs) {
|
|
488
|
+
if (existsSync2(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
489
|
+
}
|
|
490
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const absolutePath of candidateFiles) {
|
|
492
|
+
let content;
|
|
493
|
+
try {
|
|
494
|
+
content = readFileSync2(absolutePath, "utf-8");
|
|
495
|
+
} catch {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const isReactRouterFile = content.includes("react-router-dom") || content.includes("react-router") || content.includes("<Routes") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("RouterProvider") || content.includes("HashRouter") || content.includes("BrowserRouter");
|
|
499
|
+
if (!isReactRouterFile) continue;
|
|
500
|
+
const relativePath = relative2(projectRoot, absolutePath);
|
|
501
|
+
const pathMatches = /* @__PURE__ */ new Set();
|
|
502
|
+
for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
|
|
503
|
+
pathMatches.add(match[1]);
|
|
504
|
+
}
|
|
505
|
+
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
506
|
+
pathMatches.add(match[1]);
|
|
507
|
+
}
|
|
508
|
+
if (pathMatches.size === 0 && (content.includes("<Routes") || content.includes("RouterProvider"))) {
|
|
509
|
+
pathMatches.add("/");
|
|
510
|
+
}
|
|
511
|
+
for (const path of pathMatches) {
|
|
512
|
+
if (!routeMap.has(path)) {
|
|
513
|
+
routeMap.set(path, {
|
|
514
|
+
path,
|
|
515
|
+
file: relativePath,
|
|
516
|
+
hasLayout: false
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return [...routeMap.values()];
|
|
469
522
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
if (!projectFlag) return projectRoot;
|
|
473
|
-
const candidate = resolve(projectRoot, projectFlag);
|
|
474
|
-
return existsSync2(join2(candidate, ".decantr", "project.json")) ? candidate : projectRoot;
|
|
523
|
+
function hasReactRouterDependency(projectRoot) {
|
|
524
|
+
return hasDependency(projectRoot, ["react-router", "react-router-dom"]);
|
|
475
525
|
}
|
|
476
|
-
function
|
|
477
|
-
const
|
|
478
|
-
if (!existsSync2(
|
|
526
|
+
function hasDependency(projectRoot, names) {
|
|
527
|
+
const packageJsonPath = join2(projectRoot, "package.json");
|
|
528
|
+
if (!existsSync2(packageJsonPath)) return false;
|
|
479
529
|
try {
|
|
480
|
-
|
|
530
|
+
const pkg = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
531
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
532
|
+
return names.some((name) => Boolean(deps[name]));
|
|
481
533
|
} catch {
|
|
482
|
-
return
|
|
534
|
+
return false;
|
|
483
535
|
}
|
|
484
536
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
return typeof property === "string" && property.trim() ? property : void 0;
|
|
488
|
-
}
|
|
489
|
-
function isRecord(value) {
|
|
490
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
491
|
-
}
|
|
492
|
-
function normalizeCommand(command) {
|
|
493
|
-
if (!command) return null;
|
|
494
|
-
if (command === "--help" || command === "-h") return "help";
|
|
495
|
-
if (command === "--version" || command === "-v") return "version";
|
|
496
|
-
return command;
|
|
537
|
+
function hasAnyFile(projectRoot, relPaths) {
|
|
538
|
+
return relPaths.some((relPath) => existsSync2(join2(projectRoot, relPath)));
|
|
497
539
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
if (
|
|
501
|
-
return
|
|
540
|
+
function normalizeRoutePath(path) {
|
|
541
|
+
const cleaned = path.trim();
|
|
542
|
+
if (!cleaned || cleaned === "/") return "/";
|
|
543
|
+
if (cleaned === "**" || cleaned.startsWith("#")) return null;
|
|
544
|
+
return cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
|
|
502
545
|
}
|
|
503
|
-
function
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
546
|
+
function scanAngularRouter(projectRoot) {
|
|
547
|
+
const candidateDirs = [join2(projectRoot, "src", "app"), join2(projectRoot, "src")];
|
|
548
|
+
const candidateFiles = [];
|
|
549
|
+
for (const dir of candidateDirs) {
|
|
550
|
+
if (existsSync2(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
508
551
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
552
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
553
|
+
for (const absolutePath of candidateFiles) {
|
|
554
|
+
let content;
|
|
555
|
+
try {
|
|
556
|
+
content = readFileSync2(absolutePath, "utf-8");
|
|
557
|
+
} catch {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || content.includes("Routes =");
|
|
561
|
+
if (!isRouterFile) continue;
|
|
562
|
+
const relativePath = relative2(projectRoot, absolutePath);
|
|
563
|
+
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
|
|
564
|
+
const routePath = normalizeRoutePath(match[1]);
|
|
565
|
+
if (!routePath || routeMap.has(routePath)) continue;
|
|
566
|
+
routeMap.set(routePath, {
|
|
567
|
+
path: routePath,
|
|
568
|
+
file: relativePath,
|
|
569
|
+
hasLayout: false
|
|
570
|
+
});
|
|
571
|
+
}
|
|
512
572
|
}
|
|
513
|
-
return
|
|
514
|
-
}
|
|
515
|
-
function inferAdoptionMode(args) {
|
|
516
|
-
const value = inferFlagValue(args, "--adoption");
|
|
517
|
-
return normalizeAdoptionMode(value);
|
|
518
|
-
}
|
|
519
|
-
function inferWorkflowMode(args) {
|
|
520
|
-
const value = inferFlagValue(args, "--workflow");
|
|
521
|
-
return normalizeWorkflowMode(value);
|
|
573
|
+
return [...routeMap.values()];
|
|
522
574
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
575
|
+
function scanVueRouter(projectRoot) {
|
|
576
|
+
const candidateDirs = [join2(projectRoot, "src"), projectRoot];
|
|
577
|
+
const candidateFiles = [];
|
|
578
|
+
for (const dir of candidateDirs) {
|
|
579
|
+
if (existsSync2(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
526
580
|
}
|
|
527
|
-
|
|
581
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
582
|
+
for (const absolutePath of candidateFiles) {
|
|
583
|
+
let content;
|
|
584
|
+
try {
|
|
585
|
+
content = readFileSync2(absolutePath, "utf-8");
|
|
586
|
+
} catch {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const isRouterFile = content.includes("vue-router") || content.includes("createRouter") || content.includes("createWebHistory") || content.includes("createWebHashHistory");
|
|
590
|
+
if (!isRouterFile) continue;
|
|
591
|
+
const relativePath = relative2(projectRoot, absolutePath);
|
|
592
|
+
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
593
|
+
const routePath = normalizeRoutePath(match[1]);
|
|
594
|
+
if (!routePath || routeMap.has(routePath)) continue;
|
|
595
|
+
routeMap.set(routePath, {
|
|
596
|
+
path: routePath,
|
|
597
|
+
file: relativePath,
|
|
598
|
+
hasLayout: false
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return [...routeMap.values()];
|
|
528
603
|
}
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
|
|
604
|
+
function scanRoutes(projectRoot) {
|
|
605
|
+
const hasNext = hasDependency(projectRoot, ["next"]) || hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]);
|
|
606
|
+
const hasSvelteKit = hasDependency(projectRoot, ["@sveltejs/kit", "svelte"]) || hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]);
|
|
607
|
+
const hasNuxt = hasDependency(projectRoot, ["nuxt"]) || hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]);
|
|
608
|
+
const hasAngular = hasDependency(projectRoot, ["@angular/core", "@angular/router"]) || hasAnyFile(projectRoot, ["angular.json"]);
|
|
609
|
+
const hasVue = hasDependency(projectRoot, ["vue", "vue-router"]) || hasAnyFile(projectRoot, ["vite.config.js", "vite.config.ts"]);
|
|
610
|
+
const appDirs = [join2(projectRoot, "src", "app"), join2(projectRoot, "app")];
|
|
611
|
+
const appRoutes = appDirs.flatMap(
|
|
612
|
+
(appDir) => existsSync2(appDir) ? walkAppDir(appDir, projectRoot, []) : []
|
|
613
|
+
);
|
|
614
|
+
const pagesDirs = [join2(projectRoot, "src", "pages"), join2(projectRoot, "pages")];
|
|
615
|
+
const pagesRoutes = pagesDirs.flatMap(
|
|
616
|
+
(pagesDir) => existsSync2(pagesDir) ? walkPagesDir(pagesDir, projectRoot, []) : []
|
|
617
|
+
);
|
|
618
|
+
if (hasNext) {
|
|
619
|
+
if (appRoutes.length > 0 && pagesRoutes.length > 0) {
|
|
620
|
+
return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
|
|
621
|
+
}
|
|
622
|
+
if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
|
|
623
|
+
if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
|
|
624
|
+
} else if (appRoutes.length > 0) {
|
|
625
|
+
return { strategy: "app-router", routes: appRoutes };
|
|
532
626
|
}
|
|
533
|
-
if (
|
|
534
|
-
|
|
627
|
+
if (hasSvelteKit) {
|
|
628
|
+
const svelteRoutesDir = join2(projectRoot, "src", "routes");
|
|
629
|
+
if (existsSync2(svelteRoutesDir)) {
|
|
630
|
+
const routes = walkSvelteKitRoutes(svelteRoutesDir, projectRoot, []);
|
|
631
|
+
if (routes.length > 0) return { strategy: "sveltekit-router", routes };
|
|
632
|
+
}
|
|
535
633
|
}
|
|
536
|
-
if (
|
|
537
|
-
|
|
634
|
+
if (hasNuxt) {
|
|
635
|
+
const nuxtPagesDirs = [join2(projectRoot, "pages"), join2(projectRoot, "app", "pages")];
|
|
636
|
+
const routes = nuxtPagesDirs.flatMap(
|
|
637
|
+
(pagesDir) => existsSync2(pagesDir) ? walkPagesDir(pagesDir, projectRoot, [], /* @__PURE__ */ new Set(["vue"])) : []
|
|
638
|
+
);
|
|
639
|
+
if (routes.length > 0) return { strategy: "nuxt-router", routes };
|
|
538
640
|
}
|
|
539
|
-
|
|
540
|
-
|
|
641
|
+
const reactRouterRoutes = scanReactRouter(projectRoot);
|
|
642
|
+
if (reactRouterRoutes.length > 0 && hasReactRouterDependency(projectRoot)) {
|
|
643
|
+
return { strategy: "react-router", routes: reactRouterRoutes };
|
|
541
644
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (value === "single-app" || value === "workspace-app") return value;
|
|
546
|
-
return void 0;
|
|
547
|
-
}
|
|
548
|
-
function normalizeFindingSeverity(value) {
|
|
549
|
-
if (value === "error" || value === "info" || value === "warn") return value;
|
|
550
|
-
return void 0;
|
|
551
|
-
}
|
|
552
|
-
function normalizeFindingSource(value) {
|
|
553
|
-
if (value === "audit" || value === "brownfield" || value === "check" || value === "interaction" || value === "pack" || value === "runtime") {
|
|
554
|
-
return value;
|
|
645
|
+
if (hasAngular) {
|
|
646
|
+
const routes = scanAngularRouter(projectRoot);
|
|
647
|
+
if (routes.length > 0) return { strategy: "angular-router", routes };
|
|
555
648
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (args.includes("--offline")) {
|
|
560
|
-
return "cache";
|
|
649
|
+
if (hasVue) {
|
|
650
|
+
const routes = scanVueRouter(projectRoot);
|
|
651
|
+
if (routes.length > 0) return { strategy: "vue-router", routes };
|
|
561
652
|
}
|
|
562
|
-
if (
|
|
563
|
-
return "
|
|
653
|
+
if (pagesRoutes.length > 0) {
|
|
654
|
+
return { strategy: "pages-router", routes: pagesRoutes };
|
|
564
655
|
}
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
function isRegistrySource(value) {
|
|
568
|
-
return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
|
|
569
|
-
}
|
|
570
|
-
function inferProjectScope(projectRoot) {
|
|
571
|
-
return existsSync2(join2(projectRoot, "pnpm-workspace.yaml")) || existsSync2(join2(projectRoot, "turbo.json")) || existsSync2(join2(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
572
|
-
}
|
|
573
|
-
function getCliVersion() {
|
|
574
|
-
try {
|
|
575
|
-
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
576
|
-
const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
|
|
577
|
-
for (const candidate of candidates) {
|
|
578
|
-
if (existsSync2(candidate)) {
|
|
579
|
-
const pkg = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
580
|
-
if (pkg.version) {
|
|
581
|
-
return pkg.version;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
} catch {
|
|
656
|
+
if (reactRouterRoutes.length > 0) {
|
|
657
|
+
return { strategy: "react-router", routes: reactRouterRoutes };
|
|
586
658
|
}
|
|
587
|
-
return "
|
|
588
|
-
}
|
|
589
|
-
function isLoopbackHost(host) {
|
|
590
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
659
|
+
return { strategy: "none", routes: [] };
|
|
591
660
|
}
|
|
592
661
|
|
|
593
|
-
// src/
|
|
594
|
-
import { existsSync as existsSync3,
|
|
662
|
+
// src/analyzers/styling.ts
|
|
663
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
595
664
|
import { join as join3 } from "path";
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
665
|
+
var TAILWIND_CONFIGS = [
|
|
666
|
+
"tailwind.config.js",
|
|
667
|
+
"tailwind.config.ts",
|
|
668
|
+
"tailwind.config.mjs",
|
|
669
|
+
"tailwind.config.cjs"
|
|
670
|
+
];
|
|
671
|
+
var GLOBALS_CSS_PATHS = [
|
|
672
|
+
"src/app/globals.css",
|
|
673
|
+
"app/globals.css",
|
|
674
|
+
"src/styles/global.css",
|
|
675
|
+
"src/styles/globals.css",
|
|
676
|
+
"src/styles/main.css",
|
|
677
|
+
"src/styles.css",
|
|
678
|
+
"styles/globals.css",
|
|
679
|
+
"styles.css",
|
|
680
|
+
"assets/css/main.css",
|
|
681
|
+
"src/index.css",
|
|
682
|
+
"src/app.css",
|
|
683
|
+
"src/global.css"
|
|
684
|
+
];
|
|
685
|
+
var DECANTR_STYLE_PATHS = [
|
|
686
|
+
"src/styles/tokens.css",
|
|
687
|
+
"src/styles/treatments.css",
|
|
688
|
+
"src/styles/global.css"
|
|
689
|
+
];
|
|
690
|
+
function extractCSSVariables(content) {
|
|
691
|
+
const colors = {};
|
|
692
|
+
const variables = [];
|
|
693
|
+
const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
|
|
694
|
+
let match;
|
|
695
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
696
|
+
const name = match[1];
|
|
697
|
+
const value = match[2].trim();
|
|
698
|
+
variables.push(`--${name}`);
|
|
699
|
+
const colorPatterns = [
|
|
700
|
+
"primary",
|
|
701
|
+
"secondary",
|
|
702
|
+
"accent",
|
|
703
|
+
"bg",
|
|
704
|
+
"fg",
|
|
705
|
+
"border",
|
|
706
|
+
"success",
|
|
707
|
+
"warning",
|
|
708
|
+
"error",
|
|
709
|
+
"surface",
|
|
710
|
+
"muted"
|
|
711
|
+
];
|
|
712
|
+
if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || colorPatterns.some((p) => name.includes(p))) {
|
|
713
|
+
colors[name] = value;
|
|
614
714
|
}
|
|
615
715
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
716
|
+
return { colors, variables };
|
|
717
|
+
}
|
|
718
|
+
function detectDarkMode(projectRoot, cssContents) {
|
|
719
|
+
for (const cssContent of cssContents) {
|
|
720
|
+
if (cssContent.includes(".dark") || cssContent.includes('[data-theme="dark"]') || cssContent.includes("prefers-color-scheme: dark") || cssContent.includes("color-scheme: dark")) {
|
|
721
|
+
return true;
|
|
621
722
|
}
|
|
622
723
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
724
|
+
const layoutPaths = [
|
|
725
|
+
"src/app/layout.tsx",
|
|
726
|
+
"app/layout.tsx",
|
|
727
|
+
"src/app/layout.jsx",
|
|
728
|
+
"app/layout.jsx"
|
|
729
|
+
];
|
|
730
|
+
for (const rel of layoutPaths) {
|
|
731
|
+
const fullPath = join3(projectRoot, rel);
|
|
732
|
+
if (existsSync3(fullPath)) {
|
|
733
|
+
try {
|
|
734
|
+
const layoutContent = readFileSync3(fullPath, "utf-8");
|
|
735
|
+
if (layoutContent.includes('className="dark"') || layoutContent.includes("className='dark'") || layoutContent.includes('class="dark"')) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
626
740
|
}
|
|
627
741
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
742
|
+
const pkgPath = join3(projectRoot, "package.json");
|
|
743
|
+
if (existsSync3(pkgPath)) {
|
|
744
|
+
try {
|
|
745
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
746
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
747
|
+
if (allDeps["next-themes"] || allDeps["theme-toggle"] || allDeps["use-dark-mode"]) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
633
751
|
}
|
|
634
752
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
753
|
+
const essencePath = join3(projectRoot, "decantr.essence.json");
|
|
754
|
+
if (existsSync3(essencePath)) {
|
|
755
|
+
try {
|
|
756
|
+
const essence = JSON.parse(readFileSync3(essencePath, "utf-8"));
|
|
757
|
+
const mode = essence.dna?.theme?.mode;
|
|
758
|
+
if (mode === "dark" || mode === "auto") {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
638
762
|
}
|
|
639
763
|
}
|
|
640
|
-
return
|
|
764
|
+
return false;
|
|
641
765
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
649
|
-
"node_modules",
|
|
650
|
-
".decantr",
|
|
651
|
-
".git",
|
|
652
|
-
"dist",
|
|
653
|
-
"build",
|
|
654
|
-
".next",
|
|
655
|
-
".turbo",
|
|
656
|
-
"coverage",
|
|
657
|
-
".cache"
|
|
658
|
-
]);
|
|
659
|
-
var MAX_FILE_SIZE = 1024 * 1024;
|
|
660
|
-
function walkSourceTree(rootDir) {
|
|
661
|
-
const sources = /* @__PURE__ */ new Map();
|
|
662
|
-
function walk2(dir) {
|
|
663
|
-
let entries;
|
|
766
|
+
function scanStyling(projectRoot) {
|
|
767
|
+
let approach = "unknown";
|
|
768
|
+
let configFile;
|
|
769
|
+
let packageDeps = {};
|
|
770
|
+
const pkgPath = join3(projectRoot, "package.json");
|
|
771
|
+
if (existsSync3(pkgPath)) {
|
|
664
772
|
try {
|
|
665
|
-
|
|
773
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
774
|
+
packageDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
666
775
|
} catch {
|
|
667
|
-
|
|
776
|
+
packageDeps = {};
|
|
668
777
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
} catch {
|
|
676
|
-
continue;
|
|
677
|
-
}
|
|
678
|
-
if (s.isDirectory()) {
|
|
679
|
-
walk2(fullPath);
|
|
680
|
-
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
681
|
-
if (s.size > MAX_FILE_SIZE) continue;
|
|
682
|
-
try {
|
|
683
|
-
sources.set(relative(rootDir, fullPath) || entry, readFileSync4(fullPath, "utf8"));
|
|
684
|
-
} catch {
|
|
685
|
-
}
|
|
686
|
-
}
|
|
778
|
+
}
|
|
779
|
+
for (const cfg of TAILWIND_CONFIGS) {
|
|
780
|
+
if (existsSync3(join3(projectRoot, cfg))) {
|
|
781
|
+
approach = "tailwind";
|
|
782
|
+
configFile = cfg;
|
|
783
|
+
break;
|
|
687
784
|
}
|
|
688
785
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
} catch {
|
|
699
|
-
return [];
|
|
786
|
+
if (approach === "unknown") {
|
|
787
|
+
if (packageDeps["@decantr/css"]) {
|
|
788
|
+
approach = "decantr-css";
|
|
789
|
+
configFile = "src/styles/tokens.css";
|
|
790
|
+
}
|
|
791
|
+
if (packageDeps.tailwindcss || packageDeps["@tailwindcss/postcss"] || packageDeps["@tailwindcss/vite"]) {
|
|
792
|
+
approach = "tailwind";
|
|
793
|
+
configFile = configFile ?? "package.json";
|
|
794
|
+
}
|
|
700
795
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
continue;
|
|
796
|
+
if (approach === "unknown") {
|
|
797
|
+
if (packageDeps.bootstrap || packageDeps["react-bootstrap"]) {
|
|
798
|
+
approach = "bootstrap";
|
|
799
|
+
configFile = "package.json";
|
|
800
|
+
} else if (packageDeps["@mui/material"] || packageDeps["@mui/system"] || packageDeps["@mui/joy"]) {
|
|
801
|
+
approach = "mui";
|
|
802
|
+
configFile = "package.json";
|
|
803
|
+
} else if (packageDeps["@chakra-ui/react"] || packageDeps["@chakra-ui/vue-next"] || packageDeps["@chakra-ui/system"]) {
|
|
804
|
+
approach = "chakra";
|
|
805
|
+
configFile = "package.json";
|
|
712
806
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
807
|
+
}
|
|
808
|
+
const decantrStyleFiles = DECANTR_STYLE_PATHS.filter((rel) => existsSync3(join3(projectRoot, rel)));
|
|
809
|
+
if (decantrStyleFiles.length >= 2) {
|
|
810
|
+
approach = "decantr-css";
|
|
811
|
+
configFile = decantrStyleFiles.join(" + ");
|
|
812
|
+
}
|
|
813
|
+
const cssContents = [];
|
|
814
|
+
for (const rel of GLOBALS_CSS_PATHS) {
|
|
815
|
+
const fullPath = join3(projectRoot, rel);
|
|
816
|
+
if (existsSync3(fullPath)) {
|
|
817
|
+
try {
|
|
818
|
+
cssContents.push(readFileSync3(fullPath, "utf-8"));
|
|
819
|
+
} catch {
|
|
717
820
|
}
|
|
718
821
|
}
|
|
719
822
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
return missing.map(
|
|
729
|
-
({ interaction, suggestion, scannedFiles, scannedLocations, expectedSignals }) => {
|
|
730
|
-
const evidence = [
|
|
731
|
-
scannedFiles ? `scanned ${scannedFiles} source file(s)` : null,
|
|
732
|
-
scannedLocations?.length ? `checked: ${scannedLocations.slice(0, 4).map((location) => `${location.file}:${location.startLine}-${location.endLine}`).join(", ")}` : null,
|
|
733
|
-
expectedSignals?.length ? `expected signals: ${expectedSignals.slice(0, 4).join(", ")}` : null
|
|
734
|
-
].filter((entry) => Boolean(entry)).join("; ");
|
|
735
|
-
return `${interaction} \u2192 ${suggestion}${evidence ? ` (${evidence})` : ""}`;
|
|
823
|
+
for (const rel of DECANTR_STYLE_PATHS) {
|
|
824
|
+
if (GLOBALS_CSS_PATHS.includes(rel)) continue;
|
|
825
|
+
const fullPath = join3(projectRoot, rel);
|
|
826
|
+
if (existsSync3(fullPath)) {
|
|
827
|
+
try {
|
|
828
|
+
cssContents.push(readFileSync3(fullPath, "utf-8"));
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
736
831
|
}
|
|
737
|
-
|
|
832
|
+
}
|
|
833
|
+
let colors = {};
|
|
834
|
+
let cssVariables = [];
|
|
835
|
+
for (const cssContent of cssContents) {
|
|
836
|
+
const extracted = extractCSSVariables(cssContent);
|
|
837
|
+
colors = { ...colors, ...extracted.colors };
|
|
838
|
+
cssVariables.push(...extracted.variables);
|
|
839
|
+
}
|
|
840
|
+
cssVariables = [...new Set(cssVariables)];
|
|
841
|
+
const darkMode = detectDarkMode(projectRoot, cssContents);
|
|
842
|
+
if (approach === "unknown" && cssContents.length > 0) {
|
|
843
|
+
approach = "css";
|
|
844
|
+
configFile = GLOBALS_CSS_PATHS.find((rel) => existsSync3(join3(projectRoot, rel)));
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
approach,
|
|
848
|
+
configFile,
|
|
849
|
+
colors,
|
|
850
|
+
darkMode,
|
|
851
|
+
cssVariables
|
|
852
|
+
};
|
|
738
853
|
}
|
|
739
854
|
|
|
740
|
-
// src/
|
|
741
|
-
import { existsSync as
|
|
742
|
-
import {
|
|
743
|
-
var
|
|
744
|
-
"
|
|
745
|
-
"
|
|
746
|
-
"
|
|
747
|
-
"
|
|
748
|
-
"
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
"
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
"
|
|
759
|
-
"
|
|
760
|
-
"
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
".cursorignore",
|
|
766
|
-
".claudeignore",
|
|
767
|
-
"components.json",
|
|
768
|
-
"tailwind.config.js",
|
|
769
|
-
"tailwind.config.ts",
|
|
770
|
-
"tailwind.config.mjs",
|
|
771
|
-
"tailwind.config.cjs",
|
|
772
|
-
"next.config.js",
|
|
773
|
-
"next.config.ts",
|
|
774
|
-
"next.config.mjs",
|
|
775
|
-
"nuxt.config.js",
|
|
776
|
-
"nuxt.config.ts",
|
|
777
|
-
"astro.config.mjs",
|
|
778
|
-
"astro.config.ts",
|
|
779
|
-
"svelte.config.js",
|
|
780
|
-
"svelte.config.ts",
|
|
781
|
-
"angular.json",
|
|
782
|
-
"vite.config.js",
|
|
783
|
-
"vite.config.ts",
|
|
784
|
-
"vitest.config.ts",
|
|
785
|
-
"vitest.config.js",
|
|
786
|
-
"playwright.config.ts",
|
|
787
|
-
"playwright.config.js",
|
|
788
|
-
"tsconfig.json",
|
|
789
|
-
"package.json",
|
|
790
|
-
"decantr.essence.json"
|
|
791
|
-
]);
|
|
792
|
-
var CONTEXT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
793
|
-
".agents",
|
|
794
|
-
".claude",
|
|
795
|
-
".claude/initiatives",
|
|
796
|
-
".claude/rules",
|
|
797
|
-
".codex",
|
|
798
|
-
".cursor",
|
|
799
|
-
".cursor/rules",
|
|
800
|
-
".github/workflows",
|
|
801
|
-
"docs",
|
|
802
|
-
"docs/initiatives",
|
|
803
|
-
"initiatives",
|
|
804
|
-
"memory",
|
|
805
|
-
"memories",
|
|
806
|
-
"project-memory",
|
|
807
|
-
"supabase"
|
|
808
|
-
]);
|
|
809
|
-
function shouldSkipDir(name) {
|
|
810
|
-
return SKIP_DIRS.has(name);
|
|
811
|
-
}
|
|
812
|
-
function normalizedPath(relPath) {
|
|
813
|
-
return relPath.split(sep).join("/");
|
|
855
|
+
// src/doctrine-map.ts
|
|
856
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
857
|
+
import { join as join4 } from "path";
|
|
858
|
+
var PRECEDENCE_ORDER = [
|
|
859
|
+
"security-data",
|
|
860
|
+
"architecture",
|
|
861
|
+
"design-system",
|
|
862
|
+
"workflow-ci",
|
|
863
|
+
"feature-business",
|
|
864
|
+
"assistant-specific",
|
|
865
|
+
"stale-or-historical",
|
|
866
|
+
"unknown"
|
|
867
|
+
];
|
|
868
|
+
var BASE_PRECEDENCE = {
|
|
869
|
+
"security-data": 100,
|
|
870
|
+
architecture: 88,
|
|
871
|
+
"design-system": 82,
|
|
872
|
+
"workflow-ci": 74,
|
|
873
|
+
"feature-business": 66,
|
|
874
|
+
"assistant-specific": 58,
|
|
875
|
+
"stale-or-historical": 24,
|
|
876
|
+
unknown: 12
|
|
877
|
+
};
|
|
878
|
+
function normalized(path) {
|
|
879
|
+
return path.toLowerCase();
|
|
814
880
|
}
|
|
815
|
-
function
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
if (normalized2.startsWith(".claude/")) return true;
|
|
820
|
-
if (normalized2.startsWith(".agents/")) return true;
|
|
821
|
-
if (normalized2.startsWith(".codex/")) return true;
|
|
822
|
-
if (normalized2.startsWith(".cursor/")) return true;
|
|
823
|
-
if (normalized2.startsWith(".github/workflows/")) return true;
|
|
824
|
-
if (normalized2.startsWith("docs/")) return true;
|
|
825
|
-
if (normalized2.startsWith("initiatives/")) return true;
|
|
826
|
-
if (normalized2.startsWith("memory/")) return true;
|
|
827
|
-
if (normalized2.startsWith("memories/")) return true;
|
|
828
|
-
if (normalized2.startsWith("project-memory/")) return true;
|
|
829
|
-
if (normalized2.startsWith("supabase/")) return true;
|
|
830
|
-
if (normalized2.startsWith("migrations/")) return true;
|
|
831
|
-
if (normalized2.startsWith("db/")) return true;
|
|
832
|
-
if (normalized2.startsWith("ROLEMIGRATIONS/")) return true;
|
|
833
|
-
if (normalized2 === "src/middleware.ts" || normalized2 === "middleware.ts") return true;
|
|
834
|
-
if (normalized2.includes("/middleware.")) return true;
|
|
835
|
-
const ext = extname2(name).toLowerCase();
|
|
836
|
-
return ext === ".md" || ext === ".mdx" || ext === ".sql" || ext === ".yml" || ext === ".yaml";
|
|
881
|
+
function isStalePath(path, staleRisks) {
|
|
882
|
+
const lower = normalized(path);
|
|
883
|
+
if (/complete|summary|legacy|deprecated/.test(lower)) return true;
|
|
884
|
+
return staleRisks.some((risk) => risk.toLowerCase().startsWith(lower));
|
|
837
885
|
}
|
|
838
|
-
function
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (lower === "decantr.essence.json") {
|
|
843
|
-
return {
|
|
844
|
-
role: "architecture",
|
|
845
|
-
confidence: 0.82,
|
|
846
|
-
reason: "existing Decantr contract evidence"
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
if (lower === ".claude/initiatives" || lower === "docs/initiatives" || lower === "initiatives" || lower === "memory" || lower === "memories" || lower === "project-memory" || lower.startsWith(".claude/initiatives/") || lower.startsWith("docs/initiatives/") || lower.startsWith("initiatives/") || lower.startsWith("memory/") || lower.startsWith("memories/") || lower.startsWith("project-memory/") || lower.includes("/feature/") || lower.includes("feature") || lower.includes("rbac") || lower.includes("billing") || lower.includes("admin") || lower.includes("dashboard")) {
|
|
850
|
-
return {
|
|
851
|
-
role: "feature-business",
|
|
852
|
-
confidence: 0.78,
|
|
853
|
-
reason: "feature, initiative, memory, or business-domain evidence"
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
if (lower === ".agents" || lower === ".claude" || lower === ".codex" || lower === ".cursor" || lower === "claude.md" || lower === "agents.md" || lower === "gemini.md" || lower === "copilot-instructions.md" || lower === ".cursorrules" || lower === ".windsurfrules" || lower.startsWith(".claude/") || lower.startsWith(".agents/") || lower.startsWith(".codex/") || lower.startsWith(".cursor/rules/")) {
|
|
857
|
-
return {
|
|
858
|
-
role: "assistant-specific",
|
|
859
|
-
confidence: 0.98,
|
|
860
|
-
reason: "assistant or AI-agent instruction surface"
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
if (lower.includes("security") || lower.includes("auth") || lower.includes("rls") || lower.includes("schema") || lower.includes("migration") || lower.startsWith("supabase/") || lower.startsWith("migrations/") || lower.startsWith("db/") || lower.startsWith("rolemigrations/") || lower.includes("middleware.")) {
|
|
864
|
-
return {
|
|
865
|
-
role: "security-data",
|
|
866
|
-
confidence: 0.9,
|
|
867
|
-
reason: "security, auth, schema, middleware, or data-governance evidence"
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
if (lower.includes("design-system") || lower === "components.json" || lower.startsWith("tailwind.config") || lower.includes("ui-components") || lower.includes("colors") || lower.includes("typography") || lower.includes("spacing")) {
|
|
871
|
-
return {
|
|
872
|
-
role: "design-system",
|
|
873
|
-
confidence: 0.88,
|
|
874
|
-
reason: "design system or styling convention evidence"
|
|
875
|
-
};
|
|
886
|
+
function inferArea(item) {
|
|
887
|
+
const lower = normalized(item.path);
|
|
888
|
+
if (lower.includes("security") || lower.includes("auth") || lower.includes("rls") || lower.includes("schema") || lower.includes("database") || lower.includes("data-layer") || lower.includes("middleware") || lower.startsWith("supabase/") || lower.startsWith("migrations/") || lower.startsWith("rolemigrations/")) {
|
|
889
|
+
return "security-data";
|
|
876
890
|
}
|
|
877
|
-
if (lower.
|
|
878
|
-
return
|
|
879
|
-
role: "workflow-ci",
|
|
880
|
-
confidence: 0.84,
|
|
881
|
-
reason: "workflow, CI, deployment, or validation command evidence"
|
|
882
|
-
};
|
|
891
|
+
if (lower.includes("design-system") || lower.includes("ui-components") || lower.includes("colors") || lower.includes("typography") || lower.includes("spacing") || lower.includes("components.json") || lower.includes("tailwind.config")) {
|
|
892
|
+
return "design-system";
|
|
883
893
|
}
|
|
884
|
-
if (lower
|
|
885
|
-
return
|
|
886
|
-
role: "architecture",
|
|
887
|
-
confidence: 0.72,
|
|
888
|
-
reason: "architecture, setup, or framework configuration evidence"
|
|
889
|
-
};
|
|
894
|
+
if (lower.includes("architecture") || lower.includes("state-management") || lower.includes("setup") || lower.includes("readme") || lower.endsWith("config.ts") || lower.endsWith("config.js") || lower.endsWith("config.mjs")) {
|
|
895
|
+
return "architecture";
|
|
890
896
|
}
|
|
891
|
-
if (lower.includes("
|
|
892
|
-
return
|
|
893
|
-
role: "stale-or-historical",
|
|
894
|
-
confidence: 0.64,
|
|
895
|
-
reason: "historical or possibly stale project documentation"
|
|
896
|
-
};
|
|
897
|
+
if (lower.includes("workflow") || lower.includes("deployment") || lower.includes("quality") || lower.includes("testing") || lower.includes("vitest") || lower.includes("playwright") || lower.startsWith(".github/workflows/")) {
|
|
898
|
+
return "workflow-ci";
|
|
897
899
|
}
|
|
898
|
-
return
|
|
900
|
+
return item.role;
|
|
899
901
|
}
|
|
900
|
-
function
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (lower.
|
|
904
|
-
|
|
902
|
+
function precedenceFor(item, area, staleRisks) {
|
|
903
|
+
let score = BASE_PRECEDENCE[area];
|
|
904
|
+
const lower = normalized(item.path);
|
|
905
|
+
if (lower.startsWith(".claude/rules/") || lower.startsWith(".cursor/rules/")) score += 6;
|
|
906
|
+
if (lower === "claude.md" || lower === "agents.md" || lower === "copilot-instructions.md") {
|
|
907
|
+
score += 3;
|
|
905
908
|
}
|
|
906
|
-
|
|
909
|
+
if (item.type === "directory") score -= 8;
|
|
910
|
+
if (isStalePath(item.path, staleRisks)) score -= 35;
|
|
911
|
+
if (!item.safeToCite) score -= 20;
|
|
912
|
+
return Math.max(0, Math.min(100, score));
|
|
907
913
|
}
|
|
908
|
-
function
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const classified = classifyContext(relPath);
|
|
913
|
-
items.push({
|
|
914
|
-
path: normalizedPath(relPath),
|
|
915
|
-
type: "directory",
|
|
916
|
-
role: classified.role,
|
|
917
|
-
confidence: classified.confidence,
|
|
918
|
-
sizeBytes: stats.size,
|
|
919
|
-
safeToCite: isSafeToCite(relPath),
|
|
920
|
-
reason: classified.reason
|
|
921
|
-
});
|
|
914
|
+
function summarize2(sources) {
|
|
915
|
+
const summary = Object.fromEntries(PRECEDENCE_ORDER.map((area) => [area, 0]));
|
|
916
|
+
for (const source of sources) summary[source.area] += 1;
|
|
917
|
+
return summary;
|
|
922
918
|
}
|
|
923
|
-
function
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
} catch {
|
|
919
|
+
function topSources(sources, areas, limit = 5) {
|
|
920
|
+
return sources.filter((source) => source.currency === "current" && areas.includes(source.area)).slice(0, limit).map((source) => source.path);
|
|
921
|
+
}
|
|
922
|
+
function buildResolutions(conflicts, staleRisks, sources) {
|
|
923
|
+
const resolutions = [];
|
|
924
|
+
for (const conflict of conflicts) {
|
|
925
|
+
const lower = conflict.toLowerCase();
|
|
926
|
+
if (lower.includes("framework")) {
|
|
927
|
+
resolutions.push({
|
|
928
|
+
kind: "conflict",
|
|
929
|
+
issue: conflict,
|
|
930
|
+
recommendation: "Prefer package/config detection and current architecture sources over stale docs or assistant memory when deciding framework/runtime conventions.",
|
|
931
|
+
preferredSources: topSources(sources, ["architecture", "workflow-ci"]),
|
|
932
|
+
confidence: 0.78
|
|
933
|
+
});
|
|
939
934
|
continue;
|
|
940
935
|
}
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
936
|
+
if (lower.includes("tailwind")) {
|
|
937
|
+
resolutions.push({
|
|
938
|
+
kind: "conflict",
|
|
939
|
+
issue: conflict,
|
|
940
|
+
recommendation: "Preserve the existing styling system until the user approves migration; treat current design-system docs and Tailwind/shadcn config as the styling authority.",
|
|
941
|
+
preferredSources: topSources(sources, ["design-system", "architecture"]),
|
|
942
|
+
confidence: 0.82
|
|
943
|
+
});
|
|
946
944
|
continue;
|
|
947
945
|
}
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
946
|
+
if (lower.includes("client") && lower.includes("server")) {
|
|
947
|
+
resolutions.push({
|
|
948
|
+
kind: "conflict",
|
|
949
|
+
issue: conflict,
|
|
950
|
+
recommendation: "Prefer current framework architecture and security/data boundaries; stop and ask for review before moving client/server responsibilities.",
|
|
951
|
+
preferredSources: topSources(sources, ["architecture", "security-data"]),
|
|
952
|
+
confidence: 0.76
|
|
953
|
+
});
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
resolutions.push({
|
|
957
|
+
kind: "conflict",
|
|
958
|
+
issue: conflict,
|
|
959
|
+
recommendation: "Use the highest-precedence current sources in the doctrine map and report the conflict before enforcing either side.",
|
|
960
|
+
preferredSources: sources.filter((source) => source.currency === "current").slice(0, 5).map((source) => source.path),
|
|
961
|
+
confidence: 0.62
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
if (staleRisks.length > 0) {
|
|
965
|
+
resolutions.push({
|
|
966
|
+
kind: "stale-risk",
|
|
967
|
+
issue: `${staleRisks.length} stale or historical source(s) detected.`,
|
|
968
|
+
recommendation: "Treat stale-risk sources as historical evidence until confirmed by current security/data, architecture, design-system, workflow, or feature doctrine.",
|
|
969
|
+
preferredSources: sources.filter((source) => source.currency === "current" && source.area !== "assistant-specific").slice(0, 5).map((source) => source.path),
|
|
970
|
+
confidence: 0.84
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
return resolutions;
|
|
974
|
+
}
|
|
975
|
+
function createDoctrineMap(ambient) {
|
|
976
|
+
const sources = ambient.items.map((item) => {
|
|
977
|
+
const area = inferArea(item);
|
|
978
|
+
const stale = isStalePath(item.path, ambient.staleRisks);
|
|
979
|
+
return {
|
|
980
|
+
path: item.path,
|
|
981
|
+
type: item.type,
|
|
982
|
+
area: stale ? "stale-or-historical" : area,
|
|
983
|
+
originalRole: item.role,
|
|
984
|
+
precedence: precedenceFor(item, area, ambient.staleRisks),
|
|
985
|
+
confidence: item.confidence,
|
|
986
|
+
currency: !item.safeToCite ? "unsafe-to-cite" : stale ? "stale-risk" : "current",
|
|
987
|
+
safeToCite: item.safeToCite,
|
|
988
|
+
rationale: item.reason
|
|
989
|
+
};
|
|
990
|
+
}).sort((a, b) => b.precedence - a.precedence || a.path.localeCompare(b.path));
|
|
991
|
+
return {
|
|
992
|
+
version: 1,
|
|
993
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
994
|
+
precedenceOrder: PRECEDENCE_ORDER,
|
|
995
|
+
sources,
|
|
996
|
+
summary: summarize2(sources),
|
|
997
|
+
conflicts: ambient.conflicts,
|
|
998
|
+
staleRisks: ambient.staleRisks,
|
|
999
|
+
resolutions: buildResolutions(ambient.conflicts, ambient.staleRisks, sources),
|
|
1000
|
+
guidance: [
|
|
1001
|
+
"Treat security/data doctrine as highest precedence for implementation safety.",
|
|
1002
|
+
"Treat architecture and design-system sources as product conventions, not Decantr defaults.",
|
|
1003
|
+
"Treat workflow/CI sources as validation evidence for commands and release gates.",
|
|
1004
|
+
"Treat stale-risk sources as historical evidence until a current source confirms them.",
|
|
1005
|
+
"Do not cite unsafe sources directly in assistant context."
|
|
1006
|
+
]
|
|
971
1007
|
};
|
|
972
|
-
for (const item of items) summary[item.role] += 1;
|
|
973
|
-
return summary;
|
|
974
1008
|
}
|
|
975
|
-
function
|
|
976
|
-
|
|
1009
|
+
function doctrineMapPath(projectRoot) {
|
|
1010
|
+
return join4(projectRoot, ".decantr", "doctrine-map.json");
|
|
1011
|
+
}
|
|
1012
|
+
function writeDoctrineMap(projectRoot, doctrine) {
|
|
1013
|
+
writeFileSync(doctrineMapPath(projectRoot), JSON.stringify(doctrine, null, 2) + "\n", "utf-8");
|
|
1014
|
+
}
|
|
1015
|
+
function readDoctrineMap(projectRoot) {
|
|
1016
|
+
const path = doctrineMapPath(projectRoot);
|
|
1017
|
+
if (!existsSync4(path)) return null;
|
|
977
1018
|
try {
|
|
978
|
-
const
|
|
979
|
-
if (
|
|
980
|
-
return
|
|
1019
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1020
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.sources)) return null;
|
|
1021
|
+
return parsed;
|
|
981
1022
|
} catch {
|
|
982
|
-
return
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
function detectConflicts(projectRoot, items) {
|
|
986
|
-
const text = items.filter(
|
|
987
|
-
(item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
|
|
988
|
-
).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
|
|
989
|
-
const conflicts = [];
|
|
990
|
-
const frameworkSignals = [
|
|
991
|
-
["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
|
|
992
|
-
["angular", /\bangular\b/],
|
|
993
|
-
["svelte", /\bsvelte\b|\bsveltekit\b/],
|
|
994
|
-
["vue", /\bvue\b|\bnuxt\b/]
|
|
995
|
-
].filter(([, pattern]) => pattern.test(text));
|
|
996
|
-
if (frameworkSignals.length > 1) {
|
|
997
|
-
conflicts.push(
|
|
998
|
-
`Multiple framework doctrines appear in ambient docs: ${frameworkSignals.map(([name]) => name).join(", ")}.`
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
|
|
1002
|
-
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
|
|
1003
|
-
if (forbidsTailwind && endorsesTailwind) {
|
|
1004
|
-
conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
|
|
1005
|
-
}
|
|
1006
|
-
if (/\bclient component\b/.test(text) && /\bserver components? only\b/.test(text)) {
|
|
1007
|
-
conflicts.push("Ambient docs may conflict on client vs server component boundaries.");
|
|
1023
|
+
return null;
|
|
1008
1024
|
}
|
|
1009
|
-
return conflicts;
|
|
1010
1025
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1026
|
+
|
|
1027
|
+
// src/brownfield-check.ts
|
|
1028
|
+
function readProjectJson(projectRoot) {
|
|
1029
|
+
const path = join5(projectRoot, ".decantr", "project.json");
|
|
1030
|
+
if (!existsSync5(path)) return {};
|
|
1015
1031
|
try {
|
|
1016
|
-
|
|
1017
|
-
const risks = [];
|
|
1018
|
-
if (essence.version !== "4.0.0") {
|
|
1019
|
-
risks.push(
|
|
1020
|
-
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; run decantr migrate --to v4 or review before treating it as current brownfield doctrine.`
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
|
|
1024
|
-
risks.push(
|
|
1025
|
-
"decantr.essence.json looks like an older Decantr default scaffold; verify before importing its theme or page layout as brownfield truth."
|
|
1026
|
-
);
|
|
1027
|
-
}
|
|
1028
|
-
return risks;
|
|
1032
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
1029
1033
|
} catch {
|
|
1030
|
-
return
|
|
1031
|
-
"decantr.essence.json could not be parsed during ambient inventory; review before treating it as current doctrine."
|
|
1032
|
-
];
|
|
1034
|
+
return {};
|
|
1033
1035
|
}
|
|
1034
1036
|
}
|
|
1035
|
-
function
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
);
|
|
1041
|
-
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
1037
|
+
function essenceRoutes(essence) {
|
|
1038
|
+
const fromRouteMap = Object.keys(essence.blueprint.routes ?? {});
|
|
1039
|
+
const fromPages = essence.blueprint.sections?.flatMap(
|
|
1040
|
+
(section) => section.pages.map((page) => page.route).filter((route) => Boolean(route))
|
|
1041
|
+
) ?? essence.blueprint.pages?.map((page) => page.route).filter((route) => Boolean(route)) ?? [];
|
|
1042
|
+
return /* @__PURE__ */ new Set([...fromRouteMap, ...fromPages]);
|
|
1042
1043
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1044
|
+
function routeLabel(routes) {
|
|
1045
|
+
if (routes.length <= 6) return routes.join(", ");
|
|
1046
|
+
return `${routes.slice(0, 6).join(", ")} (+${routes.length - 6} more)`;
|
|
1047
|
+
}
|
|
1048
|
+
function hasDoctrineEffect(essence, key) {
|
|
1049
|
+
const effects = essence.dna.constraints?.effects;
|
|
1050
|
+
return Boolean(effects && effects[key]);
|
|
1051
|
+
}
|
|
1052
|
+
function hasActionableDoctrineSource(doctrine, area) {
|
|
1053
|
+
return doctrine.sources.some(
|
|
1054
|
+
(source) => source.area === area && source.currency === "current" && source.safeToCite && source.confidence >= 0.72 && source.precedence >= 75
|
|
1048
1055
|
);
|
|
1049
|
-
return {
|
|
1050
|
-
version: 1,
|
|
1051
|
-
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1052
|
-
items: deduped,
|
|
1053
|
-
summary: summarize(deduped),
|
|
1054
|
-
conflicts: detectConflicts(projectRoot, deduped),
|
|
1055
|
-
staleRisks: detectStaleRisks(projectRoot, deduped)
|
|
1056
|
-
};
|
|
1057
1056
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1057
|
+
function hasAssistantBridge(projectRoot) {
|
|
1058
|
+
const previewPath = join5(projectRoot, ".decantr", "context", "assistant-bridge.md");
|
|
1059
|
+
if (existsSync5(previewPath)) return true;
|
|
1060
|
+
const candidateFiles = [
|
|
1061
|
+
"CLAUDE.md",
|
|
1062
|
+
"AGENTS.md",
|
|
1063
|
+
"GEMINI.md",
|
|
1064
|
+
"copilot-instructions.md",
|
|
1065
|
+
".github/copilot-instructions.md",
|
|
1066
|
+
".cursorrules",
|
|
1067
|
+
".windsurfrules",
|
|
1068
|
+
".claude/rules/decantr.md",
|
|
1069
|
+
".cursor/rules/decantr.mdc"
|
|
1070
|
+
];
|
|
1071
|
+
return candidateFiles.some((rel) => {
|
|
1072
|
+
const path = join5(projectRoot, rel);
|
|
1073
|
+
if (!existsSync5(path)) return false;
|
|
1074
|
+
try {
|
|
1075
|
+
return readFileSync5(path, "utf-8").includes("decantr:assistant-bridge:start");
|
|
1076
|
+
} catch {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1065
1080
|
}
|
|
1066
|
-
function
|
|
1067
|
-
|
|
1068
|
-
|
|
1081
|
+
function scanBrownfieldIssues(projectRoot, essence) {
|
|
1082
|
+
const projectJson = readProjectJson(projectRoot);
|
|
1083
|
+
const routes = scanRoutes(projectRoot);
|
|
1084
|
+
const styling = scanStyling(projectRoot);
|
|
1085
|
+
const ambient = scanAmbientContext(projectRoot);
|
|
1086
|
+
const doctrine = readDoctrineMap(projectRoot) ?? createDoctrineMap(ambient);
|
|
1087
|
+
const issues = [];
|
|
1088
|
+
const declaredRoutes = essenceRoutes(essence);
|
|
1089
|
+
const observedRoutes = new Set(routes.routes.map((route) => route.path));
|
|
1090
|
+
const missingFromEssence = [...observedRoutes].filter((route) => !declaredRoutes.has(route));
|
|
1091
|
+
const missingFromSource = [...declaredRoutes].filter((route) => !observedRoutes.has(route));
|
|
1092
|
+
if (routes.routes.length > 0 && declaredRoutes.size === 0) {
|
|
1093
|
+
issues.push({
|
|
1094
|
+
type: "error",
|
|
1095
|
+
rule: "brownfield-route-coverage",
|
|
1096
|
+
message: `The app has ${routes.routes.length} observed route(s), but the Decantr essence declares no routes.`,
|
|
1097
|
+
suggestion: "Run `decantr analyze`, review the proposal, then `decantr init --existing --accept-proposal` or `--merge-proposal`."
|
|
1098
|
+
});
|
|
1099
|
+
} else if (missingFromEssence.length > 0) {
|
|
1100
|
+
issues.push({
|
|
1101
|
+
type: "error",
|
|
1102
|
+
rule: "brownfield-route-drift",
|
|
1103
|
+
message: `Observed routes are missing from the Decantr contract: ${routeLabel(missingFromEssence)}.`,
|
|
1104
|
+
suggestion: "Regenerate a brownfield proposal and merge the missing routes into the essence."
|
|
1105
|
+
});
|
|
1069
1106
|
}
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1107
|
+
if (routes.routes.length > 0 && declaredRoutes.size === 1 && declaredRoutes.has("/") && routes.routes.length > 1) {
|
|
1108
|
+
issues.push({
|
|
1109
|
+
type: "error",
|
|
1110
|
+
rule: "brownfield-generic-contract",
|
|
1111
|
+
message: "The essence only declares `/` while the app has multiple observed routes.",
|
|
1112
|
+
suggestion: "Accept or merge an observed brownfield proposal instead of using a generic scaffold contract."
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (missingFromSource.length > 0 && routes.routes.length > 0) {
|
|
1116
|
+
issues.push({
|
|
1117
|
+
type: "warning",
|
|
1118
|
+
rule: "brownfield-stale-route",
|
|
1119
|
+
message: `Essence routes were not observed in source: ${routeLabel(missingFromSource)}.`,
|
|
1120
|
+
suggestion: "Confirm whether these are generated/dynamic routes or stale contract entries."
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const adoptionMode = projectJson.initialized?.adoptionMode;
|
|
1124
|
+
const themeId = essence.dna.theme.id;
|
|
1125
|
+
if (adoptionMode === "contract-only" && themeId === "luminarum" && (styling.approach !== "unknown" || styling.cssVariables.length > 0)) {
|
|
1126
|
+
issues.push({
|
|
1127
|
+
type: "warning",
|
|
1128
|
+
rule: "brownfield-theme-default",
|
|
1129
|
+
message: "Contract-only brownfield essence still uses Decantr theme `luminarum` while the app has an existing styling system.",
|
|
1130
|
+
suggestion: 'Use an observed proposal with `theme.id = "existing"` unless the user explicitly opts into a Decantr theme.'
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
for (const conflict of ambient.conflicts) {
|
|
1134
|
+
issues.push({
|
|
1135
|
+
type: "warning",
|
|
1136
|
+
rule: "brownfield-doctrine-conflict",
|
|
1137
|
+
message: conflict,
|
|
1138
|
+
suggestion: "Resolve or document precedence before treating these rules as enforceable contract."
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if (ambient.items.length === 0) {
|
|
1142
|
+
issues.push({
|
|
1143
|
+
type: "warning",
|
|
1144
|
+
rule: "brownfield-context-missing",
|
|
1145
|
+
message: "No ambient project context was detected for this brownfield check.",
|
|
1146
|
+
suggestion: "Run `decantr analyze` to create `.decantr/ambient-context.json` and a proposal-backed report."
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const hasBrownfieldArtifacts = Boolean(
|
|
1150
|
+
projectJson.initialized?.workflowMode === "brownfield-attach"
|
|
1151
|
+
);
|
|
1152
|
+
if (hasBrownfieldArtifacts && !existsSync5(join5(projectRoot, ".decantr", "doctrine-map.json"))) {
|
|
1153
|
+
issues.push({
|
|
1154
|
+
type: "warning",
|
|
1155
|
+
rule: "brownfield-doctrine-map-missing",
|
|
1156
|
+
message: "Brownfield attach metadata exists, but `.decantr/doctrine-map.json` is missing.",
|
|
1157
|
+
suggestion: "Run `decantr analyze` to regenerate ranked doctrine evidence."
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (hasActionableDoctrineSource(doctrine, "security-data") && !hasDoctrineEffect(essence, "doctrine-security-data")) {
|
|
1161
|
+
issues.push({
|
|
1162
|
+
type: "warning",
|
|
1163
|
+
rule: "brownfield-doctrine-coverage",
|
|
1164
|
+
message: "Security/data doctrine was detected, but the essence does not record a security/data preservation constraint.",
|
|
1165
|
+
suggestion: "Regenerate and merge a brownfield proposal so security/data doctrine is represented in `dna.constraints.effects`."
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (hasActionableDoctrineSource(doctrine, "design-system") && !hasDoctrineEffect(essence, "doctrine-design-system")) {
|
|
1169
|
+
issues.push({
|
|
1170
|
+
type: "warning",
|
|
1171
|
+
rule: "brownfield-doctrine-coverage",
|
|
1172
|
+
message: "Design-system doctrine was detected, but the essence does not record a design-system preservation constraint.",
|
|
1173
|
+
suggestion: "Regenerate and merge a brownfield proposal so design-system doctrine is represented in `dna.constraints.effects`."
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (styling.approach !== "unknown") {
|
|
1177
|
+
const palette = String(essence.dna.color.palette ?? "");
|
|
1178
|
+
const observedPalette = palette === "observed" || palette === styling.approach;
|
|
1179
|
+
if (themeId === "existing" && !observedPalette) {
|
|
1180
|
+
issues.push({
|
|
1181
|
+
type: "warning",
|
|
1182
|
+
rule: "brownfield-style-drift",
|
|
1183
|
+
message: `Observed styling approach is ${styling.approach}, but the essence color palette is ${palette || "unset"}.`,
|
|
1184
|
+
suggestion: "Regenerate and merge a brownfield proposal so the contract reflects the existing styling system."
|
|
1185
|
+
});
|
|
1077
1186
|
}
|
|
1078
|
-
return `:${param}`;
|
|
1079
1187
|
}
|
|
1080
|
-
|
|
1188
|
+
if (ambient.items.some((item) => item.role === "assistant-specific") && hasBrownfieldArtifacts && !hasAssistantBridge(projectRoot)) {
|
|
1189
|
+
issues.push({
|
|
1190
|
+
type: "warning",
|
|
1191
|
+
rule: "brownfield-assistant-bridge-missing",
|
|
1192
|
+
message: "Assistant-specific rule files were detected, but no Decantr assistant bridge preview or applied bridge block was found.",
|
|
1193
|
+
suggestion: "Run `decantr rules preview` first, then `decantr rules apply` if the user explicitly approves rule-file mutation."
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
const unsafeSources = doctrine.sources.filter((source) => !source.safeToCite);
|
|
1197
|
+
if (unsafeSources.length > 0) {
|
|
1198
|
+
issues.push({
|
|
1199
|
+
type: "warning",
|
|
1200
|
+
rule: "brownfield-unsafe-context",
|
|
1201
|
+
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)` : ""}.`,
|
|
1202
|
+
suggestion: "Keep unsafe source paths in the inventory, but do not paste their contents into assistant context."
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return issues;
|
|
1081
1206
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1207
|
+
|
|
1208
|
+
// src/guard-context.ts
|
|
1209
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "fs";
|
|
1210
|
+
import { join as join7 } from "path";
|
|
1211
|
+
|
|
1212
|
+
// src/bundled-content.ts
|
|
1213
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
1214
|
+
import { dirname, join as join6 } from "path";
|
|
1215
|
+
import { fileURLToPath } from "url";
|
|
1216
|
+
function bundledDirCandidates(contentType) {
|
|
1217
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
1218
|
+
return [
|
|
1219
|
+
join6(currentDir, "bundled", contentType),
|
|
1220
|
+
join6(currentDir, "..", "src", "bundled", contentType),
|
|
1221
|
+
join6(currentDir, "..", "bundled", contentType)
|
|
1222
|
+
];
|
|
1223
|
+
}
|
|
1224
|
+
function getBundledContentPath(contentType, id) {
|
|
1225
|
+
for (const dir of bundledDirCandidates(contentType)) {
|
|
1226
|
+
const candidate = join6(dir, `${id}.json`);
|
|
1227
|
+
if (existsSync6(candidate)) return candidate;
|
|
1228
|
+
}
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
function loadBundledContentItem(contentType, id) {
|
|
1232
|
+
const path = getBundledContentPath(contentType, id);
|
|
1233
|
+
if (!path) return null;
|
|
1085
1234
|
try {
|
|
1086
|
-
|
|
1235
|
+
const data = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1236
|
+
return { id, data, path };
|
|
1087
1237
|
} catch {
|
|
1088
|
-
return
|
|
1089
|
-
}
|
|
1090
|
-
const hasPage = entries.some(
|
|
1091
|
-
(e) => e === "page.tsx" || e === "page.ts" || e === "page.jsx" || e === "page.js"
|
|
1092
|
-
);
|
|
1093
|
-
const hasLayout = entries.some(
|
|
1094
|
-
(e) => e === "layout.tsx" || e === "layout.ts" || e === "layout.jsx" || e === "layout.js"
|
|
1095
|
-
);
|
|
1096
|
-
if (hasPage) {
|
|
1097
|
-
const routePath = "/" + segments.filter((s) => s !== "").join("/");
|
|
1098
|
-
const pageFile = entries.find((e) => e.startsWith("page."));
|
|
1099
|
-
routes.push({
|
|
1100
|
-
path: routePath || "/",
|
|
1101
|
-
file: relative3(baseDir, join6(dir, pageFile)),
|
|
1102
|
-
hasLayout
|
|
1103
|
-
});
|
|
1238
|
+
return null;
|
|
1104
1239
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1240
|
+
}
|
|
1241
|
+
function loadBundledContentList(contentType) {
|
|
1242
|
+
const entries = [];
|
|
1243
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1244
|
+
for (const dir of bundledDirCandidates(contentType)) {
|
|
1245
|
+
if (!existsSync6(dir)) continue;
|
|
1108
1246
|
try {
|
|
1109
|
-
|
|
1247
|
+
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".json"))) {
|
|
1248
|
+
const id = file.replace(/\.json$/, "");
|
|
1249
|
+
if (seen.has(id)) continue;
|
|
1250
|
+
const path = join6(dir, file);
|
|
1251
|
+
const data = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1252
|
+
entries.push({ id, data, path });
|
|
1253
|
+
seen.add(id);
|
|
1254
|
+
}
|
|
1110
1255
|
} catch {
|
|
1111
|
-
continue;
|
|
1112
1256
|
}
|
|
1113
|
-
const routeSegment = segmentToRoute(entry);
|
|
1114
|
-
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
1115
|
-
routes.push(...walkAppDir(fullPath, baseDir, nextSegments));
|
|
1116
1257
|
}
|
|
1117
|
-
return
|
|
1258
|
+
return entries;
|
|
1118
1259
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1260
|
+
|
|
1261
|
+
// src/guard-context.ts
|
|
1262
|
+
function loadJsonEntries(dir) {
|
|
1263
|
+
if (!existsSync7(dir)) return [];
|
|
1122
1264
|
try {
|
|
1123
|
-
|
|
1265
|
+
return readdirSync4(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync7(join7(dir, file), "utf-8")));
|
|
1124
1266
|
} catch {
|
|
1125
|
-
return
|
|
1267
|
+
return [];
|
|
1126
1268
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (!match) continue;
|
|
1139
|
-
const name = match[1];
|
|
1140
|
-
const extension = match[2];
|
|
1141
|
-
if (!extensions.has(extension)) continue;
|
|
1142
|
-
if (name.startsWith("_")) continue;
|
|
1143
|
-
const routeSegment = name === "index" ? "" : segmentToRoute(name) ?? name;
|
|
1144
|
-
const routePath = "/" + [...segments, routeSegment].filter((s) => s !== "").join("/");
|
|
1145
|
-
routes.push({
|
|
1146
|
-
path: routePath || "/",
|
|
1147
|
-
file: relative3(baseDir, fullPath),
|
|
1148
|
-
hasLayout: false
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
} catch {
|
|
1269
|
+
}
|
|
1270
|
+
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
1271
|
+
const themeRegistry = /* @__PURE__ */ new Map();
|
|
1272
|
+
const patternRegistry = /* @__PURE__ */ new Map();
|
|
1273
|
+
const cacheDir = join7(projectRoot, ".decantr", "cache");
|
|
1274
|
+
const customDir = join7(projectRoot, ".decantr", "custom");
|
|
1275
|
+
for (const data of loadJsonEntries(join7(cacheDir, "@official", "themes"))) {
|
|
1276
|
+
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
1277
|
+
themeRegistry.set(data.id, {
|
|
1278
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
1279
|
+
});
|
|
1152
1280
|
}
|
|
1153
1281
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
entries = readdirSync5(dir);
|
|
1161
|
-
} catch {
|
|
1162
|
-
return routes;
|
|
1282
|
+
for (const data of loadJsonEntries(join7(customDir, "themes"))) {
|
|
1283
|
+
if (typeof data.id === "string") {
|
|
1284
|
+
themeRegistry.set(`custom:${data.id}`, {
|
|
1285
|
+
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1163
1288
|
}
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
routes.push({
|
|
1169
|
-
path: routePath || "/",
|
|
1170
|
-
file: relative3(baseDir, join6(dir, pageFile)),
|
|
1171
|
-
hasLayout
|
|
1172
|
-
});
|
|
1289
|
+
for (const data of loadJsonEntries(join7(cacheDir, "@official", "patterns"))) {
|
|
1290
|
+
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
1291
|
+
patternRegistry.set(data.id, data);
|
|
1292
|
+
}
|
|
1173
1293
|
}
|
|
1174
|
-
for (const entry of
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
} catch {
|
|
1180
|
-
continue;
|
|
1294
|
+
for (const entry of loadBundledContentList("patterns")) {
|
|
1295
|
+
const data = entry.data;
|
|
1296
|
+
const id = typeof data.id === "string" ? data.id : entry.id;
|
|
1297
|
+
if (!patternRegistry.has(id)) {
|
|
1298
|
+
patternRegistry.set(id, data);
|
|
1181
1299
|
}
|
|
1182
|
-
const routeSegment = segmentToRoute(entry);
|
|
1183
|
-
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
1184
|
-
routes.push(...walkSvelteKitRoutes(fullPath, baseDir, nextSegments));
|
|
1185
1300
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (depth > 5) return;
|
|
1191
|
-
let entries;
|
|
1192
|
-
try {
|
|
1193
|
-
entries = readdirSync5(dir);
|
|
1194
|
-
} catch {
|
|
1195
|
-
return;
|
|
1301
|
+
for (const data of loadJsonEntries(join7(customDir, "patterns"))) {
|
|
1302
|
+
if (typeof data.id === "string") {
|
|
1303
|
+
patternRegistry.set(data.id, data);
|
|
1304
|
+
}
|
|
1196
1305
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1306
|
+
return { themeRegistry, patternRegistry };
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/lib/scan-interactions.ts
|
|
1310
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
1311
|
+
import { extname as extname2, join as join8, relative as relative3 } from "path";
|
|
1312
|
+
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
1313
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
1314
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1315
|
+
"node_modules",
|
|
1316
|
+
".decantr",
|
|
1317
|
+
".git",
|
|
1318
|
+
"dist",
|
|
1319
|
+
"build",
|
|
1320
|
+
".next",
|
|
1321
|
+
".turbo",
|
|
1322
|
+
"coverage",
|
|
1323
|
+
".cache"
|
|
1324
|
+
]);
|
|
1325
|
+
var MAX_FILE_SIZE = 1024 * 1024;
|
|
1326
|
+
function walkSourceTree(rootDir) {
|
|
1327
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1328
|
+
function walk2(dir) {
|
|
1329
|
+
let entries;
|
|
1200
1330
|
try {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1331
|
+
entries = readdirSync5(dir);
|
|
1332
|
+
} catch {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
for (const entry of entries) {
|
|
1336
|
+
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
1337
|
+
const fullPath = join8(dir, entry);
|
|
1338
|
+
let s;
|
|
1339
|
+
try {
|
|
1340
|
+
s = statSync3(fullPath);
|
|
1341
|
+
} catch {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (s.isDirectory()) {
|
|
1345
|
+
walk2(fullPath);
|
|
1346
|
+
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname2(entry))) {
|
|
1347
|
+
if (s.size > MAX_FILE_SIZE) continue;
|
|
1348
|
+
try {
|
|
1349
|
+
sources.set(relative3(rootDir, fullPath) || entry, readFileSync8(fullPath, "utf8"));
|
|
1350
|
+
} catch {
|
|
1208
1351
|
}
|
|
1209
1352
|
}
|
|
1210
|
-
} catch {
|
|
1211
1353
|
}
|
|
1212
1354
|
}
|
|
1355
|
+
walk2(rootDir);
|
|
1356
|
+
return sources;
|
|
1213
1357
|
}
|
|
1214
|
-
function
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1358
|
+
function collectDeclaredInteractions(projectRoot) {
|
|
1359
|
+
const manifestPath = join8(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
1360
|
+
if (!existsSync8(manifestPath)) return [];
|
|
1361
|
+
let manifest;
|
|
1362
|
+
try {
|
|
1363
|
+
manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
1364
|
+
} catch {
|
|
1365
|
+
return [];
|
|
1219
1366
|
}
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1367
|
+
const all = [];
|
|
1368
|
+
const pages = manifest.pages ?? [];
|
|
1369
|
+
const contextDir = join8(projectRoot, ".decantr", "context");
|
|
1370
|
+
for (const page of pages) {
|
|
1371
|
+
const packPath = join8(contextDir, page.json);
|
|
1372
|
+
if (!existsSync8(packPath)) continue;
|
|
1373
|
+
let pack;
|
|
1223
1374
|
try {
|
|
1224
|
-
|
|
1375
|
+
pack = JSON.parse(readFileSync8(packPath, "utf8"));
|
|
1225
1376
|
} catch {
|
|
1226
1377
|
continue;
|
|
1227
1378
|
}
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
|
|
1233
|
-
pathMatches.add(match[1]);
|
|
1234
|
-
}
|
|
1235
|
-
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
1236
|
-
pathMatches.add(match[1]);
|
|
1237
|
-
}
|
|
1238
|
-
if (pathMatches.size === 0 && (content.includes("<Routes") || content.includes("RouterProvider"))) {
|
|
1239
|
-
pathMatches.add("/");
|
|
1240
|
-
}
|
|
1241
|
-
for (const path of pathMatches) {
|
|
1242
|
-
if (!routeMap.has(path)) {
|
|
1243
|
-
routeMap.set(path, {
|
|
1244
|
-
path,
|
|
1245
|
-
file: relativePath,
|
|
1246
|
-
hasLayout: false
|
|
1247
|
-
});
|
|
1379
|
+
const patterns = pack.data?.patterns ?? [];
|
|
1380
|
+
for (const pat of patterns) {
|
|
1381
|
+
if (Array.isArray(pat.interactions)) {
|
|
1382
|
+
all.push(...pat.interactions);
|
|
1248
1383
|
}
|
|
1249
1384
|
}
|
|
1250
1385
|
}
|
|
1251
|
-
return
|
|
1386
|
+
return all;
|
|
1252
1387
|
}
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1388
|
+
function scanProjectInteractions(projectRoot) {
|
|
1389
|
+
const declared = collectDeclaredInteractions(projectRoot);
|
|
1390
|
+
if (declared.length === 0) return [];
|
|
1391
|
+
const sources = walkSourceTree(projectRoot);
|
|
1392
|
+
if (sources.size === 0) return [];
|
|
1393
|
+
const missing = verifyInteractionsInSource(declared, sources);
|
|
1394
|
+
return missing.map(
|
|
1395
|
+
({ interaction, suggestion, scannedFiles, scannedLocations, expectedSignals }) => {
|
|
1396
|
+
const evidence = [
|
|
1397
|
+
scannedFiles ? `scanned ${scannedFiles} source file(s)` : null,
|
|
1398
|
+
scannedLocations?.length ? `checked: ${scannedLocations.slice(0, 4).map((location) => `${location.file}:${location.startLine}-${location.endLine}`).join(", ")}` : null,
|
|
1399
|
+
expectedSignals?.length ? `expected signals: ${expectedSignals.slice(0, 4).join(", ")}` : null
|
|
1400
|
+
].filter((entry) => Boolean(entry)).join("; ");
|
|
1401
|
+
return `${interaction} \u2192 ${suggestion}${evidence ? ` (${evidence})` : ""}`;
|
|
1402
|
+
}
|
|
1403
|
+
);
|
|
1255
1404
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1405
|
+
|
|
1406
|
+
// src/telemetry.ts
|
|
1407
|
+
import { randomUUID } from "crypto";
|
|
1408
|
+
import { existsSync as existsSync9, mkdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
|
|
1409
|
+
import { homedir } from "os";
|
|
1410
|
+
import { dirname as dirname2, join as join9, resolve } from "path";
|
|
1411
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1412
|
+
import {
|
|
1413
|
+
createFetchTelemetrySink,
|
|
1414
|
+
createTelemetryClient,
|
|
1415
|
+
isTelemetryActorType
|
|
1416
|
+
} from "@decantr/telemetry";
|
|
1417
|
+
var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
|
|
1418
|
+
var DEFAULT_TELEMETRY_EVENTS_ENDPOINT = "https://api.decantr.ai/v1/telemetry/events";
|
|
1419
|
+
var TELEMETRY_TIMEOUT_MS = 3e3;
|
|
1420
|
+
var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
|
|
1421
|
+
async function sendGuardMetrics(metrics) {
|
|
1259
1422
|
try {
|
|
1260
|
-
const
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1423
|
+
const controller = new AbortController();
|
|
1424
|
+
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
1425
|
+
await fetch(TELEMETRY_ENDPOINT, {
|
|
1426
|
+
method: "POST",
|
|
1427
|
+
headers: { "Content-Type": "application/json" },
|
|
1428
|
+
body: JSON.stringify(metrics),
|
|
1429
|
+
signal: controller.signal
|
|
1430
|
+
});
|
|
1431
|
+
clearTimeout(timer);
|
|
1263
1432
|
} catch {
|
|
1264
|
-
return false;
|
|
1265
1433
|
}
|
|
1266
1434
|
}
|
|
1267
|
-
function
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
function scanAngularRouter(projectRoot) {
|
|
1277
|
-
const candidateDirs = [join6(projectRoot, "src", "app"), join6(projectRoot, "src")];
|
|
1278
|
-
const candidateFiles = [];
|
|
1279
|
-
for (const dir of candidateDirs) {
|
|
1280
|
-
if (existsSync6(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
1435
|
+
function isOptedIn(projectRoot) {
|
|
1436
|
+
const projectJsonPath = join9(projectRoot, ".decantr", "project.json");
|
|
1437
|
+
if (!existsSync9(projectJsonPath)) return false;
|
|
1438
|
+
try {
|
|
1439
|
+
const data = JSON.parse(readFileSync9(projectJsonPath, "utf-8"));
|
|
1440
|
+
return data.telemetry === true;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return false;
|
|
1281
1443
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1444
|
+
}
|
|
1445
|
+
function optIn(projectRoot) {
|
|
1446
|
+
const projectJsonPath = join9(projectRoot, ".decantr", "project.json");
|
|
1447
|
+
let data = {};
|
|
1448
|
+
if (existsSync9(projectJsonPath)) {
|
|
1285
1449
|
try {
|
|
1286
|
-
|
|
1450
|
+
data = JSON.parse(readFileSync9(projectJsonPath, "utf-8"));
|
|
1287
1451
|
} catch {
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || content.includes("Routes =");
|
|
1291
|
-
if (!isRouterFile) continue;
|
|
1292
|
-
const relativePath = relative3(projectRoot, absolutePath);
|
|
1293
|
-
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
|
|
1294
|
-
const routePath = normalizeRoutePath(match[1]);
|
|
1295
|
-
if (!routePath || routeMap.has(routePath)) continue;
|
|
1296
|
-
routeMap.set(routePath, {
|
|
1297
|
-
path: routePath,
|
|
1298
|
-
file: relativePath,
|
|
1299
|
-
hasLayout: false
|
|
1300
|
-
});
|
|
1301
1452
|
}
|
|
1302
1453
|
}
|
|
1303
|
-
|
|
1454
|
+
data.telemetry = true;
|
|
1455
|
+
mkdirSync(dirname2(projectJsonPath), { recursive: true });
|
|
1456
|
+
writeFileSync2(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1304
1457
|
}
|
|
1305
|
-
function
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (existsSync6(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
1310
|
-
}
|
|
1311
|
-
const routeMap = /* @__PURE__ */ new Map();
|
|
1312
|
-
for (const absolutePath of candidateFiles) {
|
|
1313
|
-
let content;
|
|
1314
|
-
try {
|
|
1315
|
-
content = readFileSync6(absolutePath, "utf-8");
|
|
1316
|
-
} catch {
|
|
1317
|
-
continue;
|
|
1318
|
-
}
|
|
1319
|
-
const isRouterFile = content.includes("vue-router") || content.includes("createRouter") || content.includes("createWebHistory") || content.includes("createWebHashHistory");
|
|
1320
|
-
if (!isRouterFile) continue;
|
|
1321
|
-
const relativePath = relative3(projectRoot, absolutePath);
|
|
1322
|
-
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
1323
|
-
const routePath = normalizeRoutePath(match[1]);
|
|
1324
|
-
if (!routePath || routeMap.has(routePath)) continue;
|
|
1325
|
-
routeMap.set(routePath, {
|
|
1326
|
-
path: routePath,
|
|
1327
|
-
file: relativePath,
|
|
1328
|
-
hasLayout: false
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return [...routeMap.values()];
|
|
1333
|
-
}
|
|
1334
|
-
function scanRoutes(projectRoot) {
|
|
1335
|
-
const hasNext = hasDependency(projectRoot, ["next"]) || hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]);
|
|
1336
|
-
const hasSvelteKit = hasDependency(projectRoot, ["@sveltejs/kit", "svelte"]) || hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]);
|
|
1337
|
-
const hasNuxt = hasDependency(projectRoot, ["nuxt"]) || hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]);
|
|
1338
|
-
const hasAngular = hasDependency(projectRoot, ["@angular/core", "@angular/router"]) || hasAnyFile(projectRoot, ["angular.json"]);
|
|
1339
|
-
const hasVue = hasDependency(projectRoot, ["vue", "vue-router"]) || hasAnyFile(projectRoot, ["vite.config.js", "vite.config.ts"]);
|
|
1340
|
-
const appDirs = [join6(projectRoot, "src", "app"), join6(projectRoot, "app")];
|
|
1341
|
-
const appRoutes = appDirs.flatMap(
|
|
1342
|
-
(appDir) => existsSync6(appDir) ? walkAppDir(appDir, projectRoot, []) : []
|
|
1343
|
-
);
|
|
1344
|
-
const pagesDirs = [join6(projectRoot, "src", "pages"), join6(projectRoot, "pages")];
|
|
1345
|
-
const pagesRoutes = pagesDirs.flatMap(
|
|
1346
|
-
(pagesDir) => existsSync6(pagesDir) ? walkPagesDir(pagesDir, projectRoot, []) : []
|
|
1458
|
+
async function captureCliTelemetryEvent(input) {
|
|
1459
|
+
const projectRoot = resolveCliTelemetryProjectRoot(
|
|
1460
|
+
input.projectRoot ?? process.cwd(),
|
|
1461
|
+
input.args ?? []
|
|
1347
1462
|
);
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1350
|
-
return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
|
|
1351
|
-
}
|
|
1352
|
-
if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
|
|
1353
|
-
if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
|
|
1354
|
-
} else if (appRoutes.length > 0) {
|
|
1355
|
-
return { strategy: "app-router", routes: appRoutes };
|
|
1356
|
-
}
|
|
1357
|
-
if (hasSvelteKit) {
|
|
1358
|
-
const svelteRoutesDir = join6(projectRoot, "src", "routes");
|
|
1359
|
-
if (existsSync6(svelteRoutesDir)) {
|
|
1360
|
-
const routes = walkSvelteKitRoutes(svelteRoutesDir, projectRoot, []);
|
|
1361
|
-
if (routes.length > 0) return { strategy: "sveltekit-router", routes };
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
if (hasNuxt) {
|
|
1365
|
-
const nuxtPagesDirs = [join6(projectRoot, "pages"), join6(projectRoot, "app", "pages")];
|
|
1366
|
-
const routes = nuxtPagesDirs.flatMap(
|
|
1367
|
-
(pagesDir) => existsSync6(pagesDir) ? walkPagesDir(pagesDir, projectRoot, [], /* @__PURE__ */ new Set(["vue"])) : []
|
|
1368
|
-
);
|
|
1369
|
-
if (routes.length > 0) return { strategy: "nuxt-router", routes };
|
|
1370
|
-
}
|
|
1371
|
-
const reactRouterRoutes = scanReactRouter(projectRoot);
|
|
1372
|
-
if (reactRouterRoutes.length > 0 && hasReactRouterDependency(projectRoot)) {
|
|
1373
|
-
return { strategy: "react-router", routes: reactRouterRoutes };
|
|
1463
|
+
if (!isOptedIn(projectRoot)) {
|
|
1464
|
+
return;
|
|
1374
1465
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1466
|
+
const identities = ensureTelemetryIdentities(projectRoot);
|
|
1467
|
+
if (!identities) {
|
|
1468
|
+
return;
|
|
1378
1469
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1470
|
+
const registrySource = input.registrySource ?? getRegistrySourceProperty(input.properties) ?? inferRegistrySource(input.args ?? []);
|
|
1471
|
+
const client = createTelemetryClient({
|
|
1472
|
+
sink: createFetchTelemetrySink({
|
|
1473
|
+
endpoint: getTelemetryEventsEndpoint(),
|
|
1474
|
+
timeoutMs: TELEMETRY_TIMEOUT_MS
|
|
1475
|
+
})
|
|
1476
|
+
});
|
|
1477
|
+
const event = {
|
|
1478
|
+
name: input.name,
|
|
1479
|
+
context: {
|
|
1480
|
+
source: "cli",
|
|
1481
|
+
actorType: getTelemetryActorType(),
|
|
1482
|
+
environment: "production",
|
|
1483
|
+
decantrVersion: getCliVersion(),
|
|
1484
|
+
installId: identities.installId,
|
|
1485
|
+
projectId: identities.projectId,
|
|
1486
|
+
registrySource
|
|
1487
|
+
},
|
|
1488
|
+
properties: input.properties
|
|
1489
|
+
};
|
|
1490
|
+
try {
|
|
1491
|
+
await client.capture(event);
|
|
1492
|
+
} catch {
|
|
1382
1493
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1494
|
+
}
|
|
1495
|
+
async function sendCliCommandTelemetry(input) {
|
|
1496
|
+
const projectRoot = resolveCliTelemetryProjectRoot(input.projectRoot ?? process.cwd(), input.args);
|
|
1497
|
+
const command = normalizeCommand(input.args[0]);
|
|
1498
|
+
if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version" || isHelpOrVersionProbe(input.args)) {
|
|
1499
|
+
return;
|
|
1385
1500
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1501
|
+
const properties = buildCliLifecycleProperties({
|
|
1502
|
+
args: input.args,
|
|
1503
|
+
command,
|
|
1504
|
+
durationMs: input.durationMs,
|
|
1505
|
+
projectRoot,
|
|
1506
|
+
success: input.success
|
|
1507
|
+
});
|
|
1508
|
+
await captureCliTelemetryEvent({
|
|
1509
|
+
args: input.args,
|
|
1510
|
+
name: "cli.command.completed",
|
|
1511
|
+
projectRoot,
|
|
1512
|
+
properties,
|
|
1513
|
+
registrySource: properties.registrySource
|
|
1514
|
+
});
|
|
1515
|
+
const lifecycleEventName = lifecycleTelemetryEventName(command);
|
|
1516
|
+
if (lifecycleEventName) {
|
|
1517
|
+
await captureCliTelemetryEvent({
|
|
1518
|
+
args: input.args,
|
|
1519
|
+
name: lifecycleEventName,
|
|
1520
|
+
projectRoot,
|
|
1521
|
+
properties,
|
|
1522
|
+
registrySource: properties.registrySource
|
|
1523
|
+
});
|
|
1388
1524
|
}
|
|
1389
|
-
return { strategy: "none", routes: [] };
|
|
1390
1525
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
"src/styles/globals.css",
|
|
1406
|
-
"src/styles/main.css",
|
|
1407
|
-
"src/styles.css",
|
|
1408
|
-
"styles/globals.css",
|
|
1409
|
-
"styles.css",
|
|
1410
|
-
"assets/css/main.css",
|
|
1411
|
-
"src/index.css",
|
|
1412
|
-
"src/app.css",
|
|
1413
|
-
"src/global.css"
|
|
1414
|
-
];
|
|
1415
|
-
var DECANTR_STYLE_PATHS = [
|
|
1416
|
-
"src/styles/tokens.css",
|
|
1417
|
-
"src/styles/treatments.css",
|
|
1418
|
-
"src/styles/global.css"
|
|
1419
|
-
];
|
|
1420
|
-
function extractCSSVariables(content) {
|
|
1421
|
-
const colors = {};
|
|
1422
|
-
const variables = [];
|
|
1423
|
-
const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
|
|
1424
|
-
let match;
|
|
1425
|
-
while ((match = varRegex.exec(content)) !== null) {
|
|
1426
|
-
const name = match[1];
|
|
1427
|
-
const value = match[2].trim();
|
|
1428
|
-
variables.push(`--${name}`);
|
|
1429
|
-
const colorPatterns = [
|
|
1430
|
-
"primary",
|
|
1431
|
-
"secondary",
|
|
1432
|
-
"accent",
|
|
1433
|
-
"bg",
|
|
1434
|
-
"fg",
|
|
1435
|
-
"border",
|
|
1436
|
-
"success",
|
|
1437
|
-
"warning",
|
|
1438
|
-
"error",
|
|
1439
|
-
"surface",
|
|
1440
|
-
"muted"
|
|
1441
|
-
];
|
|
1442
|
-
if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || colorPatterns.some((p) => name.includes(p))) {
|
|
1443
|
-
colors[name] = value;
|
|
1444
|
-
}
|
|
1526
|
+
async function sendProjectHealthReportTelemetry(input) {
|
|
1527
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1528
|
+
const properties = buildProjectHealthTelemetryProperties(input, projectRoot);
|
|
1529
|
+
await captureCliTelemetryEvent({
|
|
1530
|
+
name: "health.report.generated",
|
|
1531
|
+
projectRoot,
|
|
1532
|
+
properties
|
|
1533
|
+
});
|
|
1534
|
+
if (input.report.status === "healthy") {
|
|
1535
|
+
await captureCliTelemetryEvent({
|
|
1536
|
+
name: "decantr.health.healthy",
|
|
1537
|
+
projectRoot,
|
|
1538
|
+
properties
|
|
1539
|
+
});
|
|
1445
1540
|
}
|
|
1446
|
-
return { colors, variables };
|
|
1447
1541
|
}
|
|
1448
|
-
function
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1542
|
+
async function sendAnalyzeCompletedTelemetry(input) {
|
|
1543
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1544
|
+
const metadata = readProjectTelemetryMetadata(projectRoot);
|
|
1545
|
+
const properties = {
|
|
1546
|
+
command: "analyze",
|
|
1547
|
+
success: input.success,
|
|
1548
|
+
durationMs: input.durationMs,
|
|
1549
|
+
adoptionMode: metadata.adoptionMode ?? "contract-only",
|
|
1550
|
+
componentCount: input.componentCount,
|
|
1551
|
+
dependencyCategoryCount: input.dependencyCategoryCount,
|
|
1552
|
+
errorCode: input.success ? void 0 : "analyze_failed",
|
|
1553
|
+
pageCount: input.pageCount,
|
|
1554
|
+
projectScope: metadata.projectScope ?? inferProjectScope(projectRoot),
|
|
1555
|
+
routeCount: input.routeCount,
|
|
1556
|
+
targetFramework: input.targetFramework,
|
|
1557
|
+
workflowMode: metadata.workflowMode ?? "brownfield-attach"
|
|
1558
|
+
};
|
|
1559
|
+
await captureCliTelemetryEvent({
|
|
1560
|
+
args: ["analyze"],
|
|
1561
|
+
name: "decantr.analyze.completed",
|
|
1562
|
+
projectRoot,
|
|
1563
|
+
properties
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
async function sendNewProjectCompletedTelemetry(input) {
|
|
1567
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1568
|
+
const args = input.args ?? ["new"];
|
|
1569
|
+
const base = buildCliLifecycleProperties({
|
|
1570
|
+
args,
|
|
1571
|
+
command: "new",
|
|
1572
|
+
durationMs: input.durationMs,
|
|
1573
|
+
projectRoot,
|
|
1574
|
+
success: input.success
|
|
1575
|
+
});
|
|
1576
|
+
const properties = {
|
|
1577
|
+
...base,
|
|
1578
|
+
command: "new"
|
|
1579
|
+
};
|
|
1580
|
+
await captureCliTelemetryEvent({
|
|
1581
|
+
args,
|
|
1582
|
+
name: "decantr.new.completed",
|
|
1583
|
+
projectRoot,
|
|
1584
|
+
properties,
|
|
1585
|
+
registrySource: properties.registrySource
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
async function sendProjectHealthPromptTelemetry(input) {
|
|
1589
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1590
|
+
const finding = input.finding;
|
|
1591
|
+
const properties = {
|
|
1592
|
+
success: Boolean(finding),
|
|
1593
|
+
findingFound: Boolean(finding),
|
|
1594
|
+
adoptionMode: normalizeAdoptionMode(input.report.summary.adoptionMode),
|
|
1595
|
+
ci: input.ci ?? false,
|
|
1596
|
+
findingSeverity: normalizeFindingSeverity(finding?.severity),
|
|
1597
|
+
findingSource: normalizeFindingSource(finding?.source),
|
|
1598
|
+
projectScope: inferProjectScope(projectRoot),
|
|
1599
|
+
workflowMode: normalizeWorkflowMode(input.report.summary.workflowMode)
|
|
1600
|
+
};
|
|
1601
|
+
await captureCliTelemetryEvent({
|
|
1602
|
+
name: "health.finding.prompt_requested",
|
|
1603
|
+
projectRoot,
|
|
1604
|
+
properties
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
async function sendProjectHealthCiFailedTelemetry(input) {
|
|
1608
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1609
|
+
const properties = {
|
|
1610
|
+
...buildProjectHealthTelemetryProperties(input, projectRoot),
|
|
1611
|
+
errorCode: "project_health_ci_failed",
|
|
1612
|
+
failOn: input.failOn,
|
|
1613
|
+
success: false
|
|
1614
|
+
};
|
|
1615
|
+
await captureCliTelemetryEvent({
|
|
1616
|
+
name: "health.ci.failed",
|
|
1617
|
+
projectRoot,
|
|
1618
|
+
properties
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
async function sendStudioStartedTelemetry(input) {
|
|
1622
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1623
|
+
const metadata = readProjectTelemetryMetadata(projectRoot);
|
|
1624
|
+
const properties = {
|
|
1625
|
+
success: true,
|
|
1626
|
+
hostMode: isLoopbackHost(input.host) ? "loopback" : "custom",
|
|
1627
|
+
port: input.port,
|
|
1628
|
+
adoptionMode: metadata.adoptionMode,
|
|
1629
|
+
projectScope: inferProjectScope(projectRoot),
|
|
1630
|
+
workflowMode: metadata.workflowMode
|
|
1631
|
+
};
|
|
1632
|
+
await captureCliTelemetryEvent({
|
|
1633
|
+
name: "studio.started",
|
|
1634
|
+
projectRoot,
|
|
1635
|
+
properties
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
async function sendStudioHealthRefreshedTelemetry(input) {
|
|
1639
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
1640
|
+
const properties = {
|
|
1641
|
+
...buildProjectHealthTelemetryProperties(input, projectRoot),
|
|
1642
|
+
trigger: input.trigger ?? "api-refresh"
|
|
1643
|
+
};
|
|
1644
|
+
await captureCliTelemetryEvent({
|
|
1645
|
+
name: "studio.health_refreshed",
|
|
1646
|
+
projectRoot,
|
|
1647
|
+
properties
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
function buildCliLifecycleProperties(input) {
|
|
1651
|
+
const metadata = readProjectTelemetryMetadata(input.projectRoot);
|
|
1652
|
+
const registrySource = inferRegistrySource(input.args);
|
|
1653
|
+
return {
|
|
1654
|
+
command: input.command,
|
|
1655
|
+
success: input.success,
|
|
1656
|
+
durationMs: input.durationMs,
|
|
1657
|
+
adoptionMode: inferAdoptionMode(input.args) ?? metadata.adoptionMode,
|
|
1658
|
+
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
1659
|
+
offline: input.args.includes("--offline"),
|
|
1660
|
+
projectScope: metadata.projectScope ?? inferProjectScope(input.projectRoot),
|
|
1661
|
+
registrySource,
|
|
1662
|
+
targetFramework: inferFlagValue(input.args, "--target"),
|
|
1663
|
+
workflowMode: inferWorkflowMode(input.args) ?? metadata.workflowMode
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
function lifecycleTelemetryEventName(command) {
|
|
1667
|
+
if (command === "check") return "decantr.check.completed";
|
|
1668
|
+
if (command === "init") return "decantr.init.completed";
|
|
1669
|
+
if (command === "refresh") return "decantr.refresh.completed";
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
function buildProjectHealthTelemetryProperties(input, projectRoot) {
|
|
1673
|
+
const { report } = input;
|
|
1674
|
+
return {
|
|
1675
|
+
success: true,
|
|
1676
|
+
status: report.status,
|
|
1677
|
+
score: report.score,
|
|
1678
|
+
durationMs: input.durationMs,
|
|
1679
|
+
adoptionMode: normalizeAdoptionMode(report.summary.adoptionMode),
|
|
1680
|
+
ci: input.ci ?? false,
|
|
1681
|
+
errorCount: report.summary.errorCount,
|
|
1682
|
+
failOn: input.failOn,
|
|
1683
|
+
findingCount: report.summary.findingCount,
|
|
1684
|
+
format: input.format,
|
|
1685
|
+
infoCount: report.summary.infoCount,
|
|
1686
|
+
outputWritten: input.outputWritten ?? false,
|
|
1687
|
+
packManifestPresent: report.summary.packManifestPresent,
|
|
1688
|
+
pageCount: report.summary.pageCount,
|
|
1689
|
+
projectScope: inferProjectScope(projectRoot),
|
|
1690
|
+
reviewPackPresent: report.summary.reviewPackPresent,
|
|
1691
|
+
routeCount: report.routes.declared.length,
|
|
1692
|
+
runtimeAuditChecked: report.summary.runtimeAuditChecked,
|
|
1693
|
+
runtimeMatchedCount: report.routes.runtimeMatched,
|
|
1694
|
+
runtimePassed: report.summary.runtimePassed,
|
|
1695
|
+
runtimeRouteCheckedCount: report.routes.runtimeChecked.length,
|
|
1696
|
+
warnCount: report.summary.warnCount,
|
|
1697
|
+
workflowMode: normalizeWorkflowMode(report.summary.workflowMode)
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
function collectMetrics(essence, issues) {
|
|
1701
|
+
const dna = essence.dna ?? {};
|
|
1702
|
+
const blueprint = essence.blueprint ?? {};
|
|
1703
|
+
const meta = essence.meta ?? {};
|
|
1704
|
+
const guard = meta.guard ?? {};
|
|
1705
|
+
const theme = dna.theme ?? {};
|
|
1706
|
+
const sections = blueprint.sections ?? [];
|
|
1707
|
+
const routes = blueprint.routes ?? {};
|
|
1708
|
+
const byRule = {};
|
|
1709
|
+
let dnaCount = 0;
|
|
1710
|
+
let blueprintCount = 0;
|
|
1711
|
+
for (const issue of issues) {
|
|
1712
|
+
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
|
|
1713
|
+
if (DNA_RULES.has(issue.rule)) {
|
|
1714
|
+
dnaCount++;
|
|
1715
|
+
} else {
|
|
1716
|
+
blueprintCount++;
|
|
1452
1717
|
}
|
|
1453
1718
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1719
|
+
return {
|
|
1720
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1721
|
+
cli_version: getCliVersion(),
|
|
1722
|
+
essence_version: essence.version ?? "unknown",
|
|
1723
|
+
guard_mode: guard.mode ?? "unknown",
|
|
1724
|
+
violations: {
|
|
1725
|
+
dna: dnaCount,
|
|
1726
|
+
blueprint: blueprintCount,
|
|
1727
|
+
by_rule: byRule
|
|
1728
|
+
},
|
|
1729
|
+
resolution_rate: 0,
|
|
1730
|
+
sections_count: sections.length,
|
|
1731
|
+
routes_count: Object.keys(routes).length,
|
|
1732
|
+
theme: theme.id ?? "unknown"
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function getCliTelemetryIdentityStatus(projectRoot, options = {}) {
|
|
1736
|
+
const projectJsonPath = join9(projectRoot, ".decantr", "project.json");
|
|
1737
|
+
const hasProjectConfig = existsSync9(projectJsonPath);
|
|
1738
|
+
const identities = options.create ? ensureTelemetryIdentities(projectRoot) : null;
|
|
1739
|
+
const projectData = readProjectJson2(projectRoot);
|
|
1740
|
+
return {
|
|
1741
|
+
enabled: projectData?.telemetry === true,
|
|
1742
|
+
hasProjectConfig,
|
|
1743
|
+
installId: identities?.installId ?? readExistingInstallId(),
|
|
1744
|
+
projectId: identities?.projectId ?? readStringProperty(projectData, "telemetryProjectId"),
|
|
1745
|
+
projectRoot
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function ensureTelemetryIdentities(projectRoot) {
|
|
1749
|
+
const installId = getOrCreateInstallId();
|
|
1750
|
+
const projectJsonPath = join9(projectRoot, ".decantr", "project.json");
|
|
1751
|
+
if (!existsSync9(projectJsonPath)) {
|
|
1752
|
+
return null;
|
|
1471
1753
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
}
|
|
1480
|
-
} catch {
|
|
1754
|
+
try {
|
|
1755
|
+
const data = JSON.parse(readFileSync9(projectJsonPath, "utf-8"));
|
|
1756
|
+
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
1757
|
+
if (!projectId) {
|
|
1758
|
+
projectId = `project_${randomUUID()}`;
|
|
1759
|
+
data.telemetryProjectId = projectId;
|
|
1760
|
+
writeFileSync2(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1481
1761
|
}
|
|
1762
|
+
return { installId, projectId };
|
|
1763
|
+
} catch {
|
|
1764
|
+
return null;
|
|
1482
1765
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1766
|
+
}
|
|
1767
|
+
function getOrCreateInstallId() {
|
|
1768
|
+
const configDir = getConfigDir();
|
|
1769
|
+
const configPath = join9(configDir, "config.json");
|
|
1770
|
+
try {
|
|
1771
|
+
if (existsSync9(configPath)) {
|
|
1772
|
+
const data = JSON.parse(readFileSync9(configPath, "utf-8"));
|
|
1773
|
+
if (typeof data.telemetryInstallId === "string") {
|
|
1774
|
+
return data.telemetryInstallId;
|
|
1490
1775
|
}
|
|
1491
|
-
|
|
1776
|
+
const installId2 = `install_${randomUUID()}`;
|
|
1777
|
+
data.telemetryInstallId = installId2;
|
|
1778
|
+
writeFileSync2(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1779
|
+
return installId2;
|
|
1492
1780
|
}
|
|
1781
|
+
mkdirSync(configDir, { recursive: true });
|
|
1782
|
+
const installId = `install_${randomUUID()}`;
|
|
1783
|
+
writeFileSync2(
|
|
1784
|
+
configPath,
|
|
1785
|
+
JSON.stringify({ telemetryInstallId: installId }, null, 2) + "\n",
|
|
1786
|
+
"utf-8"
|
|
1787
|
+
);
|
|
1788
|
+
return installId;
|
|
1789
|
+
} catch {
|
|
1790
|
+
return `install_${randomUUID()}`;
|
|
1493
1791
|
}
|
|
1494
|
-
return false;
|
|
1495
1792
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
packageDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1505
|
-
} catch {
|
|
1506
|
-
packageDeps = {};
|
|
1507
|
-
}
|
|
1793
|
+
function readExistingInstallId() {
|
|
1794
|
+
const configPath = join9(getConfigDir(), "config.json");
|
|
1795
|
+
if (!existsSync9(configPath)) return void 0;
|
|
1796
|
+
try {
|
|
1797
|
+
const data = JSON.parse(readFileSync9(configPath, "utf-8"));
|
|
1798
|
+
return readStringProperty(data, "telemetryInstallId");
|
|
1799
|
+
} catch {
|
|
1800
|
+
return void 0;
|
|
1508
1801
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1802
|
+
}
|
|
1803
|
+
function getConfigDir() {
|
|
1804
|
+
return process.env.DECANTR_CONFIG_DIR || join9(homedir(), ".config", "decantr");
|
|
1805
|
+
}
|
|
1806
|
+
function getTelemetryEventsEndpoint() {
|
|
1807
|
+
return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
|
|
1808
|
+
}
|
|
1809
|
+
function getTelemetryActorType() {
|
|
1810
|
+
const configured = process.env.DECANTR_TELEMETRY_ACTOR_TYPE;
|
|
1811
|
+
return isTelemetryActorType(configured) ? configured : "customer";
|
|
1812
|
+
}
|
|
1813
|
+
function getRegistrySourceProperty(properties) {
|
|
1814
|
+
const value = properties.registrySource;
|
|
1815
|
+
return isRegistrySource(value) ? value : void 0;
|
|
1816
|
+
}
|
|
1817
|
+
function readProjectTelemetryMetadata(projectRoot) {
|
|
1818
|
+
const data = readProjectJson2(projectRoot);
|
|
1819
|
+
const initialized = isRecord(data?.initialized) ? data.initialized : void 0;
|
|
1820
|
+
return {
|
|
1821
|
+
adoptionMode: normalizeAdoptionMode(initialized?.adoptionMode),
|
|
1822
|
+
projectScope: normalizeProjectScope(initialized?.projectScope),
|
|
1823
|
+
workflowMode: normalizeWorkflowMode(initialized?.workflowMode)
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
function resolveCliTelemetryProjectRoot(projectRoot, args) {
|
|
1827
|
+
const projectFlag = inferFlagValue(args, "--project");
|
|
1828
|
+
if (!projectFlag) return projectRoot;
|
|
1829
|
+
const candidate = resolve(projectRoot, projectFlag);
|
|
1830
|
+
return existsSync9(join9(candidate, ".decantr", "project.json")) ? candidate : projectRoot;
|
|
1831
|
+
}
|
|
1832
|
+
function readProjectJson2(projectRoot) {
|
|
1833
|
+
const projectJsonPath = join9(projectRoot, ".decantr", "project.json");
|
|
1834
|
+
if (!existsSync9(projectJsonPath)) return null;
|
|
1835
|
+
try {
|
|
1836
|
+
return JSON.parse(readFileSync9(projectJsonPath, "utf-8"));
|
|
1837
|
+
} catch {
|
|
1838
|
+
return null;
|
|
1515
1839
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1840
|
+
}
|
|
1841
|
+
function readStringProperty(value, key) {
|
|
1842
|
+
const property = value?.[key];
|
|
1843
|
+
return typeof property === "string" && property.trim() ? property : void 0;
|
|
1844
|
+
}
|
|
1845
|
+
function isRecord(value) {
|
|
1846
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1847
|
+
}
|
|
1848
|
+
function normalizeCommand(command) {
|
|
1849
|
+
if (!command) return null;
|
|
1850
|
+
if (command === "--help" || command === "-h") return "help";
|
|
1851
|
+
if (command === "--version" || command === "-v") return "version";
|
|
1852
|
+
return command;
|
|
1853
|
+
}
|
|
1854
|
+
function isHelpOrVersionProbe(args) {
|
|
1855
|
+
if (args.some((arg) => arg === "--help" || arg === "-h")) return true;
|
|
1856
|
+
if (args[1] === "help") return true;
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
function inferFlagValue(args, flag) {
|
|
1860
|
+
const equalsPrefix = `${flag}=`;
|
|
1861
|
+
const inline = args.find((arg) => arg.startsWith(equalsPrefix));
|
|
1862
|
+
if (inline) {
|
|
1863
|
+
return inline.slice(equalsPrefix.length) || void 0;
|
|
1525
1864
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
configFile = "package.json";
|
|
1530
|
-
} else if (packageDeps["@mui/material"] || packageDeps["@mui/system"] || packageDeps["@mui/joy"]) {
|
|
1531
|
-
approach = "mui";
|
|
1532
|
-
configFile = "package.json";
|
|
1533
|
-
} else if (packageDeps["@chakra-ui/react"] || packageDeps["@chakra-ui/vue-next"] || packageDeps["@chakra-ui/system"]) {
|
|
1534
|
-
approach = "chakra";
|
|
1535
|
-
configFile = "package.json";
|
|
1536
|
-
}
|
|
1865
|
+
const index = args.indexOf(flag);
|
|
1866
|
+
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
|
|
1867
|
+
return args[index + 1];
|
|
1537
1868
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1869
|
+
return void 0;
|
|
1870
|
+
}
|
|
1871
|
+
function inferAdoptionMode(args) {
|
|
1872
|
+
const value = inferFlagValue(args, "--adoption");
|
|
1873
|
+
return normalizeAdoptionMode(value);
|
|
1874
|
+
}
|
|
1875
|
+
function inferWorkflowMode(args) {
|
|
1876
|
+
const value = inferFlagValue(args, "--workflow");
|
|
1877
|
+
return normalizeWorkflowMode(value);
|
|
1878
|
+
}
|
|
1879
|
+
function normalizeAdoptionMode(value) {
|
|
1880
|
+
if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
|
|
1881
|
+
return value;
|
|
1542
1882
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
cssContents.push(readFileSync7(fullPath, "utf-8"));
|
|
1549
|
-
} catch {
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
function normalizeWorkflowMode(value) {
|
|
1886
|
+
if (value === "greenfield" || value === "greenfield-scaffold") {
|
|
1887
|
+
return "greenfield-scaffold";
|
|
1552
1888
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const fullPath = join7(projectRoot, rel);
|
|
1556
|
-
if (existsSync7(fullPath)) {
|
|
1557
|
-
try {
|
|
1558
|
-
cssContents.push(readFileSync7(fullPath, "utf-8"));
|
|
1559
|
-
} catch {
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1889
|
+
if (value === "contract" || value === "greenfield-contract-only") {
|
|
1890
|
+
return "greenfield-contract-only";
|
|
1562
1891
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
for (const cssContent of cssContents) {
|
|
1566
|
-
const extracted = extractCSSVariables(cssContent);
|
|
1567
|
-
colors = { ...colors, ...extracted.colors };
|
|
1568
|
-
cssVariables.push(...extracted.variables);
|
|
1892
|
+
if (value === "brownfield" || value === "brownfield-attach") {
|
|
1893
|
+
return "brownfield-attach";
|
|
1569
1894
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (approach === "unknown" && cssContents.length > 0) {
|
|
1573
|
-
approach = "css";
|
|
1574
|
-
configFile = GLOBALS_CSS_PATHS.find((rel) => existsSync7(join7(projectRoot, rel)));
|
|
1895
|
+
if (value === "hybrid" || value === "hybrid-compose") {
|
|
1896
|
+
return "hybrid-compose";
|
|
1575
1897
|
}
|
|
1576
|
-
return
|
|
1577
|
-
approach,
|
|
1578
|
-
configFile,
|
|
1579
|
-
colors,
|
|
1580
|
-
darkMode,
|
|
1581
|
-
cssVariables
|
|
1582
|
-
};
|
|
1898
|
+
return void 0;
|
|
1583
1899
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
import { join as join8 } from "path";
|
|
1588
|
-
var PRECEDENCE_ORDER = [
|
|
1589
|
-
"security-data",
|
|
1590
|
-
"architecture",
|
|
1591
|
-
"design-system",
|
|
1592
|
-
"workflow-ci",
|
|
1593
|
-
"feature-business",
|
|
1594
|
-
"assistant-specific",
|
|
1595
|
-
"stale-or-historical",
|
|
1596
|
-
"unknown"
|
|
1597
|
-
];
|
|
1598
|
-
var BASE_PRECEDENCE = {
|
|
1599
|
-
"security-data": 100,
|
|
1600
|
-
architecture: 88,
|
|
1601
|
-
"design-system": 82,
|
|
1602
|
-
"workflow-ci": 74,
|
|
1603
|
-
"feature-business": 66,
|
|
1604
|
-
"assistant-specific": 58,
|
|
1605
|
-
"stale-or-historical": 24,
|
|
1606
|
-
unknown: 12
|
|
1607
|
-
};
|
|
1608
|
-
function normalized(path) {
|
|
1609
|
-
return path.toLowerCase();
|
|
1900
|
+
function normalizeProjectScope(value) {
|
|
1901
|
+
if (value === "single-app" || value === "workspace-app") return value;
|
|
1902
|
+
return void 0;
|
|
1610
1903
|
}
|
|
1611
|
-
function
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
return staleRisks.some((risk) => risk.toLowerCase().startsWith(lower));
|
|
1904
|
+
function normalizeFindingSeverity(value) {
|
|
1905
|
+
if (value === "error" || value === "info" || value === "warn") return value;
|
|
1906
|
+
return void 0;
|
|
1615
1907
|
}
|
|
1616
|
-
function
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
return "security-data";
|
|
1620
|
-
}
|
|
1621
|
-
if (lower.includes("design-system") || lower.includes("ui-components") || lower.includes("colors") || lower.includes("typography") || lower.includes("spacing") || lower.includes("components.json") || lower.includes("tailwind.config")) {
|
|
1622
|
-
return "design-system";
|
|
1908
|
+
function normalizeFindingSource(value) {
|
|
1909
|
+
if (value === "audit" || value === "brownfield" || value === "check" || value === "interaction" || value === "pack" || value === "runtime") {
|
|
1910
|
+
return value;
|
|
1623
1911
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1912
|
+
return void 0;
|
|
1913
|
+
}
|
|
1914
|
+
function inferRegistrySource(args) {
|
|
1915
|
+
if (args.includes("--offline")) {
|
|
1916
|
+
return "cache";
|
|
1626
1917
|
}
|
|
1627
|
-
if (
|
|
1628
|
-
return "
|
|
1918
|
+
if (args.some((arg) => arg === "--registry" || arg.startsWith("--registry="))) {
|
|
1919
|
+
return "custom";
|
|
1629
1920
|
}
|
|
1630
|
-
return
|
|
1921
|
+
return "official";
|
|
1631
1922
|
}
|
|
1632
|
-
function
|
|
1633
|
-
|
|
1634
|
-
const lower = normalized(item.path);
|
|
1635
|
-
if (lower.startsWith(".claude/rules/") || lower.startsWith(".cursor/rules/")) score += 6;
|
|
1636
|
-
if (lower === "claude.md" || lower === "agents.md" || lower === "copilot-instructions.md") {
|
|
1637
|
-
score += 3;
|
|
1638
|
-
}
|
|
1639
|
-
if (item.type === "directory") score -= 8;
|
|
1640
|
-
if (isStalePath(item.path, staleRisks)) score -= 35;
|
|
1641
|
-
if (!item.safeToCite) score -= 20;
|
|
1642
|
-
return Math.max(0, Math.min(100, score));
|
|
1923
|
+
function isRegistrySource(value) {
|
|
1924
|
+
return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
|
|
1643
1925
|
}
|
|
1644
|
-
function
|
|
1645
|
-
|
|
1646
|
-
for (const source of sources) summary[source.area] += 1;
|
|
1647
|
-
return summary;
|
|
1926
|
+
function inferProjectScope(projectRoot) {
|
|
1927
|
+
return existsSync9(join9(projectRoot, "pnpm-workspace.yaml")) || existsSync9(join9(projectRoot, "turbo.json")) || existsSync9(join9(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
1648
1928
|
}
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1929
|
+
function getCliVersion() {
|
|
1930
|
+
try {
|
|
1931
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1932
|
+
const candidates = [join9(here, "..", "package.json"), join9(here, "..", "..", "package.json")];
|
|
1933
|
+
for (const candidate of candidates) {
|
|
1934
|
+
if (existsSync9(candidate)) {
|
|
1935
|
+
const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
|
|
1936
|
+
if (pkg.version) {
|
|
1937
|
+
return pkg.version;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
} catch {
|
|
1942
|
+
}
|
|
1943
|
+
return "unknown";
|
|
1651
1944
|
}
|
|
1652
|
-
function
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1945
|
+
function isLoopbackHost(host) {
|
|
1946
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/commands/heal.ts
|
|
1950
|
+
var GREEN = "\x1B[32m";
|
|
1951
|
+
var RED = "\x1B[31m";
|
|
1952
|
+
var YELLOW = "\x1B[33m";
|
|
1953
|
+
var CYAN = "\x1B[36m";
|
|
1954
|
+
var RESET = "\x1B[0m";
|
|
1955
|
+
var DIM = "\x1B[2m";
|
|
1956
|
+
function collectCheckIssues(projectRoot = process.cwd(), options = {}) {
|
|
1957
|
+
const essencePath = join10(projectRoot, "decantr.essence.json");
|
|
1958
|
+
if (!existsSync10(essencePath)) {
|
|
1959
|
+
return {
|
|
1960
|
+
essence: null,
|
|
1961
|
+
issues: [
|
|
1962
|
+
{
|
|
1963
|
+
type: "error",
|
|
1964
|
+
rule: "essence-missing",
|
|
1965
|
+
message: "No decantr.essence.json found. Run `decantr init` first."
|
|
1966
|
+
}
|
|
1967
|
+
],
|
|
1968
|
+
missingEssence: true
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const essence = JSON.parse(readFileSync10(essencePath, "utf-8"));
|
|
1972
|
+
const issues = [];
|
|
1973
|
+
const validation = validateEssence(essence);
|
|
1974
|
+
if (!validation.valid) {
|
|
1975
|
+
for (const err of validation.errors) {
|
|
1976
|
+
issues.push({
|
|
1977
|
+
type: "error",
|
|
1978
|
+
rule: "schema",
|
|
1979
|
+
message: err
|
|
1663
1980
|
});
|
|
1664
|
-
continue;
|
|
1665
1981
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1982
|
+
return { essence, issues, missingEssence: false };
|
|
1983
|
+
}
|
|
1984
|
+
let interactionIssues = [];
|
|
1985
|
+
try {
|
|
1986
|
+
interactionIssues = scanProjectInteractions(projectRoot);
|
|
1987
|
+
} catch {
|
|
1988
|
+
}
|
|
1989
|
+
try {
|
|
1990
|
+
const guardContext = buildGuardRegistryContext(projectRoot);
|
|
1991
|
+
const violations = evaluateGuard(essence, {
|
|
1992
|
+
...guardContext,
|
|
1993
|
+
interaction_issues: interactionIssues
|
|
1994
|
+
});
|
|
1995
|
+
for (const v of violations) {
|
|
1996
|
+
issues.push({
|
|
1997
|
+
type: v.severity === "error" ? "error" : "warning",
|
|
1998
|
+
rule: v.rule,
|
|
1999
|
+
message: v.message,
|
|
2000
|
+
suggestion: v.suggestion
|
|
1673
2001
|
});
|
|
1674
|
-
continue;
|
|
1675
2002
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2003
|
+
} catch {
|
|
2004
|
+
}
|
|
2005
|
+
if (options.brownfield) {
|
|
2006
|
+
try {
|
|
2007
|
+
if (isV4(essence)) {
|
|
2008
|
+
const brownfieldIssues = scanBrownfieldIssues(projectRoot, essence);
|
|
2009
|
+
issues.push(...brownfieldIssues);
|
|
2010
|
+
} else {
|
|
2011
|
+
issues.push({
|
|
2012
|
+
type: "warning",
|
|
2013
|
+
rule: "brownfield-check",
|
|
2014
|
+
message: "Brownfield checks require an Essence v4.0.0 Decantr contract."
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
} catch (e) {
|
|
2018
|
+
issues.push({
|
|
2019
|
+
type: "warning",
|
|
2020
|
+
rule: "brownfield-check",
|
|
2021
|
+
message: `Brownfield check could not complete: ${e.message}`
|
|
1683
2022
|
});
|
|
1684
|
-
continue;
|
|
1685
2023
|
}
|
|
1686
|
-
resolutions.push({
|
|
1687
|
-
kind: "conflict",
|
|
1688
|
-
issue: conflict,
|
|
1689
|
-
recommendation: "Use the highest-precedence current sources in the doctrine map and report the conflict before enforcing either side.",
|
|
1690
|
-
preferredSources: sources.filter((source) => source.currency === "current").slice(0, 5).map((source) => source.path),
|
|
1691
|
-
confidence: 0.62
|
|
1692
|
-
});
|
|
1693
2024
|
}
|
|
1694
|
-
|
|
1695
|
-
resolutions.push({
|
|
1696
|
-
kind: "stale-risk",
|
|
1697
|
-
issue: `${staleRisks.length} stale or historical source(s) detected.`,
|
|
1698
|
-
recommendation: "Treat stale-risk sources as historical evidence until confirmed by current security/data, architecture, design-system, workflow, or feature doctrine.",
|
|
1699
|
-
preferredSources: sources.filter((source) => source.currency === "current" && source.area !== "assistant-specific").slice(0, 5).map((source) => source.path),
|
|
1700
|
-
confidence: 0.84
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
return resolutions;
|
|
1704
|
-
}
|
|
1705
|
-
function createDoctrineMap(ambient) {
|
|
1706
|
-
const sources = ambient.items.map((item) => {
|
|
1707
|
-
const area = inferArea(item);
|
|
1708
|
-
const stale = isStalePath(item.path, ambient.staleRisks);
|
|
1709
|
-
return {
|
|
1710
|
-
path: item.path,
|
|
1711
|
-
type: item.type,
|
|
1712
|
-
area: stale ? "stale-or-historical" : area,
|
|
1713
|
-
originalRole: item.role,
|
|
1714
|
-
precedence: precedenceFor(item, area, ambient.staleRisks),
|
|
1715
|
-
confidence: item.confidence,
|
|
1716
|
-
currency: !item.safeToCite ? "unsafe-to-cite" : stale ? "stale-risk" : "current",
|
|
1717
|
-
safeToCite: item.safeToCite,
|
|
1718
|
-
rationale: item.reason
|
|
1719
|
-
};
|
|
1720
|
-
}).sort((a, b) => b.precedence - a.precedence || a.path.localeCompare(b.path));
|
|
1721
|
-
return {
|
|
1722
|
-
version: 1,
|
|
1723
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1724
|
-
precedenceOrder: PRECEDENCE_ORDER,
|
|
1725
|
-
sources,
|
|
1726
|
-
summary: summarize2(sources),
|
|
1727
|
-
conflicts: ambient.conflicts,
|
|
1728
|
-
staleRisks: ambient.staleRisks,
|
|
1729
|
-
resolutions: buildResolutions(ambient.conflicts, ambient.staleRisks, sources),
|
|
1730
|
-
guidance: [
|
|
1731
|
-
"Treat security/data doctrine as highest precedence for implementation safety.",
|
|
1732
|
-
"Treat architecture and design-system sources as product conventions, not Decantr defaults.",
|
|
1733
|
-
"Treat workflow/CI sources as validation evidence for commands and release gates.",
|
|
1734
|
-
"Treat stale-risk sources as historical evidence until a current source confirms them.",
|
|
1735
|
-
"Do not cite unsafe sources directly in assistant context."
|
|
1736
|
-
]
|
|
1737
|
-
};
|
|
2025
|
+
return { essence, issues, missingEssence: false };
|
|
1738
2026
|
}
|
|
1739
|
-
function
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2027
|
+
async function cmdHeal(projectRoot = process.cwd(), options = {}) {
|
|
2028
|
+
const result = collectCheckIssues(projectRoot, options);
|
|
2029
|
+
console.log("Scanning for issues...\n");
|
|
2030
|
+
if (result.missingEssence) {
|
|
2031
|
+
console.error(result.issues[0]?.message ?? "No decantr.essence.json found.");
|
|
2032
|
+
process.exitCode = 1;
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const issues = result.issues;
|
|
2036
|
+
const essence = result.essence ?? {};
|
|
2037
|
+
if (issues.length === 0) {
|
|
2038
|
+
console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
|
|
2039
|
+
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
console.log(`Found ${issues.length} issue(s):
|
|
2043
|
+
`);
|
|
2044
|
+
for (const issue of issues) {
|
|
2045
|
+
const icon = issue.type === "error" ? `${RED}x${RESET}` : `${YELLOW}!${RESET}`;
|
|
2046
|
+
console.log(`${icon} [${issue.rule}] ${issue.message}`);
|
|
2047
|
+
if (issue.suggestion) {
|
|
2048
|
+
console.log(` ${DIM}Suggestion: ${issue.suggestion}${RESET}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
console.log(`
|
|
2052
|
+
${YELLOW}Manual fixes required. Review the issues above.${RESET}`);
|
|
2053
|
+
const hasError = issues.some((i) => i.type === "error");
|
|
2054
|
+
if (hasError) {
|
|
2055
|
+
process.exitCode = 1;
|
|
2056
|
+
}
|
|
2057
|
+
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
1744
2058
|
}
|
|
1745
|
-
function
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2059
|
+
async function maybeSendTelemetry(projectRoot, essence, issues, options) {
|
|
2060
|
+
if (options.telemetry && !isOptedIn(projectRoot)) {
|
|
2061
|
+
optIn(projectRoot);
|
|
2062
|
+
console.log(
|
|
2063
|
+
`
|
|
2064
|
+
${CYAN}Telemetry enabled.${RESET} Decantr will send privacy-filtered CLI product telemetry for this project.`
|
|
2065
|
+
);
|
|
2066
|
+
console.log(`${DIM}Set "telemetry": false in .decantr/project.json to opt out.${RESET}`);
|
|
2067
|
+
}
|
|
2068
|
+
if (isOptedIn(projectRoot)) {
|
|
2069
|
+
const metrics = collectMetrics(essence, issues);
|
|
2070
|
+
sendGuardMetrics(metrics);
|
|
1754
2071
|
}
|
|
1755
2072
|
}
|
|
1756
2073
|
|
|
@@ -1762,8 +2079,6 @@ export {
|
|
|
1762
2079
|
scanStyling,
|
|
1763
2080
|
createDoctrineMap,
|
|
1764
2081
|
writeDoctrineMap,
|
|
1765
|
-
readDoctrineMap,
|
|
1766
|
-
sendGuardMetrics,
|
|
1767
2082
|
isOptedIn,
|
|
1768
2083
|
optIn,
|
|
1769
2084
|
sendCliCommandTelemetry,
|
|
@@ -1774,8 +2089,9 @@ export {
|
|
|
1774
2089
|
sendProjectHealthCiFailedTelemetry,
|
|
1775
2090
|
sendStudioStartedTelemetry,
|
|
1776
2091
|
sendStudioHealthRefreshedTelemetry,
|
|
1777
|
-
collectMetrics,
|
|
1778
2092
|
getCliTelemetryIdentityStatus,
|
|
1779
2093
|
buildGuardRegistryContext,
|
|
1780
|
-
scanProjectInteractions
|
|
2094
|
+
scanProjectInteractions,
|
|
2095
|
+
collectCheckIssues,
|
|
2096
|
+
cmdHeal
|
|
1781
2097
|
};
|