@decantr/cli 1.7.27 → 1.7.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1418 @@
1
+ // src/lib/scan-interactions.ts
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
3
+ import { extname, join } from "path";
4
+ import { verifyInteractionsInSource } from "@decantr/verifier";
5
+ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
6
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
7
+ "node_modules",
8
+ ".decantr",
9
+ ".git",
10
+ "dist",
11
+ "build",
12
+ ".next",
13
+ ".turbo",
14
+ "coverage",
15
+ ".cache"
16
+ ]);
17
+ var MAX_FILE_SIZE = 1024 * 1024;
18
+ function walkSourceTree(rootDir) {
19
+ const sources = /* @__PURE__ */ new Map();
20
+ function walk2(dir) {
21
+ let entries;
22
+ try {
23
+ entries = readdirSync(dir);
24
+ } catch {
25
+ return;
26
+ }
27
+ for (const entry of entries) {
28
+ if (SKIP_DIRECTORIES.has(entry)) continue;
29
+ const fullPath = join(dir, entry);
30
+ let s;
31
+ try {
32
+ s = statSync(fullPath);
33
+ } catch {
34
+ continue;
35
+ }
36
+ if (s.isDirectory()) {
37
+ walk2(fullPath);
38
+ } else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
39
+ if (s.size > MAX_FILE_SIZE) continue;
40
+ try {
41
+ sources.set(fullPath, readFileSync(fullPath, "utf8"));
42
+ } catch {
43
+ }
44
+ }
45
+ }
46
+ }
47
+ walk2(rootDir);
48
+ return sources;
49
+ }
50
+ function collectDeclaredInteractions(projectRoot) {
51
+ const manifestPath = join(projectRoot, ".decantr", "context", "pack-manifest.json");
52
+ if (!existsSync(manifestPath)) return [];
53
+ let manifest;
54
+ try {
55
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
56
+ } catch {
57
+ return [];
58
+ }
59
+ const all = [];
60
+ const pages = manifest.pages ?? [];
61
+ const contextDir = join(projectRoot, ".decantr", "context");
62
+ for (const page of pages) {
63
+ const packPath = join(contextDir, page.json);
64
+ if (!existsSync(packPath)) continue;
65
+ let pack;
66
+ try {
67
+ pack = JSON.parse(readFileSync(packPath, "utf8"));
68
+ } catch {
69
+ continue;
70
+ }
71
+ const patterns = pack.data?.patterns ?? [];
72
+ for (const pat of patterns) {
73
+ if (Array.isArray(pat.interactions)) {
74
+ all.push(...pat.interactions);
75
+ }
76
+ }
77
+ }
78
+ return all;
79
+ }
80
+ function scanProjectInteractions(projectRoot) {
81
+ const declared = collectDeclaredInteractions(projectRoot);
82
+ if (declared.length === 0) return [];
83
+ const sources = walkSourceTree(projectRoot);
84
+ if (sources.size === 0) return [];
85
+ const missing = verifyInteractionsInSource(declared, sources);
86
+ return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
87
+ }
88
+
89
+ // src/guard-context.ts
90
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
91
+ import { join as join2 } from "path";
92
+ function loadJsonEntries(dir) {
93
+ if (!existsSync2(dir)) return [];
94
+ try {
95
+ return readdirSync2(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+ function buildGuardRegistryContext(projectRoot = process.cwd()) {
101
+ const themeRegistry = /* @__PURE__ */ new Map();
102
+ const patternRegistry = /* @__PURE__ */ new Map();
103
+ const cacheDir = join2(projectRoot, ".decantr", "cache");
104
+ const customDir = join2(projectRoot, ".decantr", "custom");
105
+ for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
106
+ if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
107
+ themeRegistry.set(data.id, {
108
+ modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
109
+ });
110
+ }
111
+ }
112
+ for (const data of loadJsonEntries(join2(customDir, "themes"))) {
113
+ if (typeof data.id === "string") {
114
+ themeRegistry.set(`custom:${data.id}`, {
115
+ modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
116
+ });
117
+ }
118
+ }
119
+ for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
120
+ if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
121
+ patternRegistry.set(data.id, data);
122
+ }
123
+ }
124
+ for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
125
+ if (typeof data.id === "string") {
126
+ patternRegistry.set(data.id, data);
127
+ }
128
+ }
129
+ return { themeRegistry, patternRegistry };
130
+ }
131
+
132
+ // src/telemetry.ts
133
+ import { randomUUID } from "crypto";
134
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
135
+ import { homedir } from "os";
136
+ import { dirname, join as join3 } from "path";
137
+ import { fileURLToPath } from "url";
138
+ import {
139
+ createFetchTelemetrySink,
140
+ createTelemetryClient,
141
+ isTelemetryActorType
142
+ } from "@decantr/telemetry";
143
+ var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
144
+ var DEFAULT_TELEMETRY_EVENTS_ENDPOINT = "https://api.decantr.ai/v1/telemetry/events";
145
+ var TELEMETRY_TIMEOUT_MS = 3e3;
146
+ var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
147
+ async function sendGuardMetrics(metrics) {
148
+ try {
149
+ const controller = new AbortController();
150
+ const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
151
+ await fetch(TELEMETRY_ENDPOINT, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify(metrics),
155
+ signal: controller.signal
156
+ });
157
+ clearTimeout(timer);
158
+ } catch {
159
+ }
160
+ }
161
+ function isOptedIn(projectRoot) {
162
+ const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
163
+ if (!existsSync3(projectJsonPath)) return false;
164
+ try {
165
+ const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
166
+ return data.telemetry === true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function optIn(projectRoot) {
172
+ const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
173
+ let data = {};
174
+ if (existsSync3(projectJsonPath)) {
175
+ try {
176
+ data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
177
+ } catch {
178
+ }
179
+ }
180
+ data.telemetry = true;
181
+ mkdirSync(dirname(projectJsonPath), { recursive: true });
182
+ writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
183
+ }
184
+ async function sendCliCommandTelemetry(input) {
185
+ const projectRoot = input.projectRoot ?? process.cwd();
186
+ const command = normalizeCommand(input.args[0]);
187
+ if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
188
+ return;
189
+ }
190
+ const identities = ensureTelemetryIdentities(projectRoot);
191
+ if (!identities) {
192
+ return;
193
+ }
194
+ const client = createTelemetryClient({
195
+ sink: createFetchTelemetrySink({
196
+ endpoint: getTelemetryEventsEndpoint(),
197
+ timeoutMs: TELEMETRY_TIMEOUT_MS
198
+ })
199
+ });
200
+ const event = {
201
+ name: "cli.command.completed",
202
+ context: {
203
+ source: "cli",
204
+ actorType: getTelemetryActorType(),
205
+ environment: "production",
206
+ decantrVersion: getCliVersion(),
207
+ installId: identities.installId,
208
+ projectId: identities.projectId,
209
+ registrySource: inferRegistrySource(input.args)
210
+ },
211
+ properties: {
212
+ command,
213
+ success: input.success,
214
+ durationMs: input.durationMs,
215
+ adoptionMode: inferAdoptionMode(input.args),
216
+ errorCode: input.success ? void 0 : "cli_command_failed",
217
+ offline: input.args.includes("--offline"),
218
+ projectScope: inferProjectScope(projectRoot),
219
+ registrySource: inferRegistrySource(input.args),
220
+ targetFramework: inferFlagValue(input.args, "--target"),
221
+ workflowMode: inferWorkflowMode(input.args)
222
+ }
223
+ };
224
+ try {
225
+ await client.capture(event);
226
+ } catch {
227
+ }
228
+ }
229
+ function collectMetrics(essence, issues) {
230
+ const dna = essence.dna ?? {};
231
+ const blueprint = essence.blueprint ?? {};
232
+ const meta = essence.meta ?? {};
233
+ const guard = meta.guard ?? {};
234
+ const theme = dna.theme ?? {};
235
+ const sections = blueprint.sections ?? [];
236
+ const routes = blueprint.routes ?? {};
237
+ const byRule = {};
238
+ let dnaCount = 0;
239
+ let blueprintCount = 0;
240
+ for (const issue of issues) {
241
+ byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
242
+ if (DNA_RULES.has(issue.rule)) {
243
+ dnaCount++;
244
+ } else {
245
+ blueprintCount++;
246
+ }
247
+ }
248
+ return {
249
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
250
+ cli_version: getCliVersion(),
251
+ essence_version: essence.version ?? "unknown",
252
+ guard_mode: guard.mode ?? "unknown",
253
+ violations: {
254
+ dna: dnaCount,
255
+ blueprint: blueprintCount,
256
+ by_rule: byRule
257
+ },
258
+ resolution_rate: 0,
259
+ sections_count: sections.length,
260
+ routes_count: Object.keys(routes).length,
261
+ theme: theme.id ?? "unknown"
262
+ };
263
+ }
264
+ function ensureTelemetryIdentities(projectRoot) {
265
+ const installId = getOrCreateInstallId();
266
+ const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
267
+ if (!existsSync3(projectJsonPath)) {
268
+ return null;
269
+ }
270
+ try {
271
+ const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
272
+ let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
273
+ if (!projectId) {
274
+ projectId = `project_${randomUUID()}`;
275
+ data.telemetryProjectId = projectId;
276
+ writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
277
+ }
278
+ return { installId, projectId };
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+ function getOrCreateInstallId() {
284
+ const configDir = getConfigDir();
285
+ const configPath = join3(configDir, "config.json");
286
+ try {
287
+ if (existsSync3(configPath)) {
288
+ const data = JSON.parse(readFileSync3(configPath, "utf-8"));
289
+ if (typeof data.telemetryInstallId === "string") {
290
+ return data.telemetryInstallId;
291
+ }
292
+ const installId2 = `install_${randomUUID()}`;
293
+ data.telemetryInstallId = installId2;
294
+ writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
295
+ return installId2;
296
+ }
297
+ mkdirSync(configDir, { recursive: true });
298
+ const installId = `install_${randomUUID()}`;
299
+ writeFileSync(
300
+ configPath,
301
+ JSON.stringify({ telemetryInstallId: installId }, null, 2) + "\n",
302
+ "utf-8"
303
+ );
304
+ return installId;
305
+ } catch {
306
+ return `install_${randomUUID()}`;
307
+ }
308
+ }
309
+ function getConfigDir() {
310
+ return process.env.DECANTR_CONFIG_DIR || join3(homedir(), ".config", "decantr");
311
+ }
312
+ function getTelemetryEventsEndpoint() {
313
+ return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
314
+ }
315
+ function getTelemetryActorType() {
316
+ const configured = process.env.DECANTR_TELEMETRY_ACTOR_TYPE;
317
+ return isTelemetryActorType(configured) ? configured : "customer";
318
+ }
319
+ function normalizeCommand(command) {
320
+ if (!command) return null;
321
+ if (command === "--help" || command === "-h") return "help";
322
+ if (command === "--version" || command === "-v") return "version";
323
+ return command;
324
+ }
325
+ function inferFlagValue(args, flag) {
326
+ const equalsPrefix = `${flag}=`;
327
+ const inline = args.find((arg) => arg.startsWith(equalsPrefix));
328
+ if (inline) {
329
+ return inline.slice(equalsPrefix.length) || void 0;
330
+ }
331
+ const index = args.indexOf(flag);
332
+ if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
333
+ return args[index + 1];
334
+ }
335
+ return void 0;
336
+ }
337
+ function inferAdoptionMode(args) {
338
+ const value = inferFlagValue(args, "--adoption");
339
+ if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
340
+ return value;
341
+ }
342
+ return void 0;
343
+ }
344
+ function inferWorkflowMode(args) {
345
+ const value = inferFlagValue(args, "--workflow");
346
+ if (value === "greenfield" || value === "greenfield-scaffold") {
347
+ return "greenfield-scaffold";
348
+ }
349
+ if (value === "contract" || value === "greenfield-contract-only") {
350
+ return "greenfield-contract-only";
351
+ }
352
+ if (value === "brownfield" || value === "brownfield-attach") {
353
+ return "brownfield-attach";
354
+ }
355
+ if (value === "hybrid" || value === "hybrid-compose") {
356
+ return "hybrid-compose";
357
+ }
358
+ return void 0;
359
+ }
360
+ function inferRegistrySource(args) {
361
+ if (args.includes("--offline")) {
362
+ return "cache";
363
+ }
364
+ if (args.some((arg) => arg === "--registry" || arg.startsWith("--registry="))) {
365
+ return "custom";
366
+ }
367
+ return "official";
368
+ }
369
+ function inferProjectScope(projectRoot) {
370
+ return existsSync3(join3(projectRoot, "pnpm-workspace.yaml")) || existsSync3(join3(projectRoot, "turbo.json")) || existsSync3(join3(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
371
+ }
372
+ function getCliVersion() {
373
+ try {
374
+ const here = dirname(fileURLToPath(import.meta.url));
375
+ const candidates = [join3(here, "..", "package.json"), join3(here, "..", "..", "package.json")];
376
+ for (const candidate of candidates) {
377
+ if (existsSync3(candidate)) {
378
+ const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
379
+ if (pkg.version) {
380
+ return pkg.version;
381
+ }
382
+ }
383
+ }
384
+ } catch {
385
+ }
386
+ return "unknown";
387
+ }
388
+
389
+ // src/analyzers/routes.ts
390
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
391
+ import { join as join4, relative } from "path";
392
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "api", "_app", "_document"]);
393
+ function shouldSkipDir(name) {
394
+ return name.startsWith("_") || name.startsWith(".") || SKIP_DIRS.has(name);
395
+ }
396
+ function segmentToRoute(segment) {
397
+ if (segment.startsWith("(") && segment.endsWith(")")) {
398
+ return null;
399
+ }
400
+ if (segment.startsWith("[") && segment.endsWith("]")) {
401
+ const param = segment.slice(1, -1);
402
+ if (param.startsWith("...")) {
403
+ return `:${param.slice(3)}*`;
404
+ }
405
+ if (param.startsWith("[...") && param.endsWith("]")) {
406
+ return `:${param.slice(4, -1)}*`;
407
+ }
408
+ return `:${param}`;
409
+ }
410
+ return segment;
411
+ }
412
+ function walkAppDir(dir, baseDir, segments) {
413
+ const routes = [];
414
+ let entries;
415
+ try {
416
+ entries = readdirSync3(dir);
417
+ } catch {
418
+ return routes;
419
+ }
420
+ const hasPage = entries.some(
421
+ (e) => e === "page.tsx" || e === "page.ts" || e === "page.jsx" || e === "page.js"
422
+ );
423
+ const hasLayout = entries.some(
424
+ (e) => e === "layout.tsx" || e === "layout.ts" || e === "layout.jsx" || e === "layout.js"
425
+ );
426
+ if (hasPage) {
427
+ const routePath = "/" + segments.filter((s) => s !== "").join("/");
428
+ const pageFile = entries.find((e) => e.startsWith("page."));
429
+ routes.push({
430
+ path: routePath || "/",
431
+ file: relative(baseDir, join4(dir, pageFile)),
432
+ hasLayout
433
+ });
434
+ }
435
+ for (const entry of entries) {
436
+ if (shouldSkipDir(entry)) continue;
437
+ const fullPath = join4(dir, entry);
438
+ try {
439
+ if (!statSync2(fullPath).isDirectory()) continue;
440
+ } catch {
441
+ continue;
442
+ }
443
+ const routeSegment = segmentToRoute(entry);
444
+ const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
445
+ routes.push(...walkAppDir(fullPath, baseDir, nextSegments));
446
+ }
447
+ return routes;
448
+ }
449
+ function walkPagesDir(dir, baseDir, segments, extensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "md", "mdx"])) {
450
+ const routes = [];
451
+ let entries;
452
+ try {
453
+ entries = readdirSync3(dir);
454
+ } catch {
455
+ return routes;
456
+ }
457
+ for (const entry of entries) {
458
+ if (shouldSkipDir(entry)) continue;
459
+ const fullPath = join4(dir, entry);
460
+ try {
461
+ const stat = statSync2(fullPath);
462
+ if (stat.isDirectory()) {
463
+ const routeSegment = segmentToRoute(entry);
464
+ const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
465
+ routes.push(...walkPagesDir(fullPath, baseDir, nextSegments, extensions));
466
+ } else if (stat.isFile()) {
467
+ const match = entry.match(/^(.+)\.([^.]+)$/);
468
+ if (!match) continue;
469
+ const name = match[1];
470
+ const extension = match[2];
471
+ if (!extensions.has(extension)) continue;
472
+ if (name.startsWith("_")) continue;
473
+ const routeSegment = name === "index" ? "" : segmentToRoute(name) ?? name;
474
+ const routePath = "/" + [...segments, routeSegment].filter((s) => s !== "").join("/");
475
+ routes.push({
476
+ path: routePath || "/",
477
+ file: relative(baseDir, fullPath),
478
+ hasLayout: false
479
+ });
480
+ }
481
+ } catch {
482
+ }
483
+ }
484
+ return routes;
485
+ }
486
+ function walkSvelteKitRoutes(dir, baseDir, segments) {
487
+ const routes = [];
488
+ let entries;
489
+ try {
490
+ entries = readdirSync3(dir);
491
+ } catch {
492
+ return routes;
493
+ }
494
+ const pageFile = entries.find((entry) => /^\+page\.(svelte|ts|js)$/.test(entry));
495
+ const hasLayout = entries.some((entry) => /^\+layout\.(svelte|ts|js)$/.test(entry));
496
+ if (pageFile) {
497
+ const routePath = "/" + segments.filter((segment) => segment !== "").join("/");
498
+ routes.push({
499
+ path: routePath || "/",
500
+ file: relative(baseDir, join4(dir, pageFile)),
501
+ hasLayout
502
+ });
503
+ }
504
+ for (const entry of entries) {
505
+ if (shouldSkipDir(entry)) continue;
506
+ const fullPath = join4(dir, entry);
507
+ try {
508
+ if (!statSync2(fullPath).isDirectory()) continue;
509
+ } catch {
510
+ continue;
511
+ }
512
+ const routeSegment = segmentToRoute(entry);
513
+ const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
514
+ routes.push(...walkSvelteKitRoutes(fullPath, baseDir, nextSegments));
515
+ }
516
+ return routes;
517
+ }
518
+ var ROUTER_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
519
+ function collectRouteCandidateFiles(dir, files, depth = 0) {
520
+ if (depth > 5) return;
521
+ let entries;
522
+ try {
523
+ entries = readdirSync3(dir);
524
+ } catch {
525
+ return;
526
+ }
527
+ for (const entry of entries) {
528
+ if (entry.startsWith(".") || entry === "node_modules") continue;
529
+ const fullPath = join4(dir, entry);
530
+ try {
531
+ const stat = statSync2(fullPath);
532
+ if (stat.isDirectory()) {
533
+ collectRouteCandidateFiles(fullPath, files, depth + 1);
534
+ } else if (stat.isFile()) {
535
+ const ext = entry.slice(entry.lastIndexOf("."));
536
+ if (ROUTER_FILE_EXTENSIONS.has(ext)) {
537
+ files.push(fullPath);
538
+ }
539
+ }
540
+ } catch {
541
+ }
542
+ }
543
+ }
544
+ function scanReactRouter(projectRoot) {
545
+ const candidateDirs = [join4(projectRoot, "src"), projectRoot];
546
+ const candidateFiles = [];
547
+ for (const dir of candidateDirs) {
548
+ if (existsSync4(dir)) collectRouteCandidateFiles(dir, candidateFiles);
549
+ }
550
+ const routeMap = /* @__PURE__ */ new Map();
551
+ for (const absolutePath of candidateFiles) {
552
+ let content;
553
+ try {
554
+ content = readFileSync4(absolutePath, "utf-8");
555
+ } catch {
556
+ continue;
557
+ }
558
+ 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");
559
+ if (!isReactRouterFile) continue;
560
+ const relativePath = relative(projectRoot, absolutePath);
561
+ const pathMatches = /* @__PURE__ */ new Set();
562
+ for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
563
+ pathMatches.add(match[1]);
564
+ }
565
+ for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
566
+ pathMatches.add(match[1]);
567
+ }
568
+ if (pathMatches.size === 0 && (content.includes("<Routes") || content.includes("RouterProvider"))) {
569
+ pathMatches.add("/");
570
+ }
571
+ for (const path of pathMatches) {
572
+ if (!routeMap.has(path)) {
573
+ routeMap.set(path, {
574
+ path,
575
+ file: relativePath,
576
+ hasLayout: false
577
+ });
578
+ }
579
+ }
580
+ }
581
+ return [...routeMap.values()];
582
+ }
583
+ function hasReactRouterDependency(projectRoot) {
584
+ return hasDependency(projectRoot, ["react-router", "react-router-dom"]);
585
+ }
586
+ function hasDependency(projectRoot, names) {
587
+ const packageJsonPath = join4(projectRoot, "package.json");
588
+ if (!existsSync4(packageJsonPath)) return false;
589
+ try {
590
+ const pkg = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
591
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
592
+ return names.some((name) => Boolean(deps[name]));
593
+ } catch {
594
+ return false;
595
+ }
596
+ }
597
+ function hasAnyFile(projectRoot, relPaths) {
598
+ return relPaths.some((relPath) => existsSync4(join4(projectRoot, relPath)));
599
+ }
600
+ function normalizeRoutePath(path) {
601
+ const cleaned = path.trim();
602
+ if (!cleaned || cleaned === "/") return "/";
603
+ if (cleaned === "**" || cleaned.startsWith("#")) return null;
604
+ return cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
605
+ }
606
+ function scanAngularRouter(projectRoot) {
607
+ const candidateDirs = [join4(projectRoot, "src", "app"), join4(projectRoot, "src")];
608
+ const candidateFiles = [];
609
+ for (const dir of candidateDirs) {
610
+ if (existsSync4(dir)) collectRouteCandidateFiles(dir, candidateFiles);
611
+ }
612
+ const routeMap = /* @__PURE__ */ new Map();
613
+ for (const absolutePath of candidateFiles) {
614
+ let content;
615
+ try {
616
+ content = readFileSync4(absolutePath, "utf-8");
617
+ } catch {
618
+ continue;
619
+ }
620
+ const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || content.includes("Routes =");
621
+ if (!isRouterFile) continue;
622
+ const relativePath = relative(projectRoot, absolutePath);
623
+ for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
624
+ const routePath = normalizeRoutePath(match[1]);
625
+ if (!routePath || routeMap.has(routePath)) continue;
626
+ routeMap.set(routePath, {
627
+ path: routePath,
628
+ file: relativePath,
629
+ hasLayout: false
630
+ });
631
+ }
632
+ }
633
+ return [...routeMap.values()];
634
+ }
635
+ function scanVueRouter(projectRoot) {
636
+ const candidateDirs = [join4(projectRoot, "src"), projectRoot];
637
+ const candidateFiles = [];
638
+ for (const dir of candidateDirs) {
639
+ if (existsSync4(dir)) collectRouteCandidateFiles(dir, candidateFiles);
640
+ }
641
+ const routeMap = /* @__PURE__ */ new Map();
642
+ for (const absolutePath of candidateFiles) {
643
+ let content;
644
+ try {
645
+ content = readFileSync4(absolutePath, "utf-8");
646
+ } catch {
647
+ continue;
648
+ }
649
+ const isRouterFile = content.includes("vue-router") || content.includes("createRouter") || content.includes("createWebHistory") || content.includes("createWebHashHistory");
650
+ if (!isRouterFile) continue;
651
+ const relativePath = relative(projectRoot, absolutePath);
652
+ for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
653
+ const routePath = normalizeRoutePath(match[1]);
654
+ if (!routePath || routeMap.has(routePath)) continue;
655
+ routeMap.set(routePath, {
656
+ path: routePath,
657
+ file: relativePath,
658
+ hasLayout: false
659
+ });
660
+ }
661
+ }
662
+ return [...routeMap.values()];
663
+ }
664
+ function scanRoutes(projectRoot) {
665
+ const hasNext = hasDependency(projectRoot, ["next"]) || hasAnyFile(projectRoot, ["next.config.js", "next.config.ts", "next.config.mjs"]);
666
+ const hasSvelteKit = hasDependency(projectRoot, ["@sveltejs/kit", "svelte"]) || hasAnyFile(projectRoot, ["svelte.config.js", "svelte.config.ts"]);
667
+ const hasNuxt = hasDependency(projectRoot, ["nuxt"]) || hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]);
668
+ const hasAngular = hasDependency(projectRoot, ["@angular/core", "@angular/router"]) || hasAnyFile(projectRoot, ["angular.json"]);
669
+ const hasVue = hasDependency(projectRoot, ["vue", "vue-router"]) || hasAnyFile(projectRoot, ["vite.config.js", "vite.config.ts"]);
670
+ const appDirs = [join4(projectRoot, "src", "app"), join4(projectRoot, "app")];
671
+ const appRoutes = appDirs.flatMap(
672
+ (appDir) => existsSync4(appDir) ? walkAppDir(appDir, projectRoot, []) : []
673
+ );
674
+ const pagesDirs = [join4(projectRoot, "src", "pages"), join4(projectRoot, "pages")];
675
+ const pagesRoutes = pagesDirs.flatMap(
676
+ (pagesDir) => existsSync4(pagesDir) ? walkPagesDir(pagesDir, projectRoot, []) : []
677
+ );
678
+ if (hasNext) {
679
+ if (appRoutes.length > 0 && pagesRoutes.length > 0) {
680
+ return { strategy: "mixed-next-router", routes: [...appRoutes, ...pagesRoutes] };
681
+ }
682
+ if (appRoutes.length > 0) return { strategy: "app-router", routes: appRoutes };
683
+ if (pagesRoutes.length > 0) return { strategy: "pages-router", routes: pagesRoutes };
684
+ } else if (appRoutes.length > 0) {
685
+ return { strategy: "app-router", routes: appRoutes };
686
+ }
687
+ if (hasSvelteKit) {
688
+ const svelteRoutesDir = join4(projectRoot, "src", "routes");
689
+ if (existsSync4(svelteRoutesDir)) {
690
+ const routes = walkSvelteKitRoutes(svelteRoutesDir, projectRoot, []);
691
+ if (routes.length > 0) return { strategy: "sveltekit-router", routes };
692
+ }
693
+ }
694
+ if (hasNuxt) {
695
+ const nuxtPagesDirs = [join4(projectRoot, "pages"), join4(projectRoot, "app", "pages")];
696
+ const routes = nuxtPagesDirs.flatMap(
697
+ (pagesDir) => existsSync4(pagesDir) ? walkPagesDir(pagesDir, projectRoot, [], /* @__PURE__ */ new Set(["vue"])) : []
698
+ );
699
+ if (routes.length > 0) return { strategy: "nuxt-router", routes };
700
+ }
701
+ const reactRouterRoutes = scanReactRouter(projectRoot);
702
+ if (reactRouterRoutes.length > 0 && hasReactRouterDependency(projectRoot)) {
703
+ return { strategy: "react-router", routes: reactRouterRoutes };
704
+ }
705
+ if (hasAngular) {
706
+ const routes = scanAngularRouter(projectRoot);
707
+ if (routes.length > 0) return { strategy: "angular-router", routes };
708
+ }
709
+ if (hasVue) {
710
+ const routes = scanVueRouter(projectRoot);
711
+ if (routes.length > 0) return { strategy: "vue-router", routes };
712
+ }
713
+ if (pagesRoutes.length > 0) {
714
+ return { strategy: "pages-router", routes: pagesRoutes };
715
+ }
716
+ if (reactRouterRoutes.length > 0) {
717
+ return { strategy: "react-router", routes: reactRouterRoutes };
718
+ }
719
+ return { strategy: "none", routes: [] };
720
+ }
721
+
722
+ // src/analyzers/styling.ts
723
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
724
+ import { join as join5 } from "path";
725
+ var TAILWIND_CONFIGS = [
726
+ "tailwind.config.js",
727
+ "tailwind.config.ts",
728
+ "tailwind.config.mjs",
729
+ "tailwind.config.cjs"
730
+ ];
731
+ var GLOBALS_CSS_PATHS = [
732
+ "src/app/globals.css",
733
+ "app/globals.css",
734
+ "src/styles/global.css",
735
+ "src/styles/globals.css",
736
+ "src/styles/main.css",
737
+ "src/styles.css",
738
+ "styles/globals.css",
739
+ "styles.css",
740
+ "assets/css/main.css",
741
+ "src/index.css",
742
+ "src/app.css",
743
+ "src/global.css"
744
+ ];
745
+ var DECANTR_STYLE_PATHS = [
746
+ "src/styles/tokens.css",
747
+ "src/styles/treatments.css",
748
+ "src/styles/global.css"
749
+ ];
750
+ function extractCSSVariables(content) {
751
+ const colors = {};
752
+ const variables = [];
753
+ const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
754
+ let match;
755
+ while ((match = varRegex.exec(content)) !== null) {
756
+ const name = match[1];
757
+ const value = match[2].trim();
758
+ variables.push(`--${name}`);
759
+ const colorPatterns = [
760
+ "primary",
761
+ "secondary",
762
+ "accent",
763
+ "bg",
764
+ "fg",
765
+ "border",
766
+ "success",
767
+ "warning",
768
+ "error",
769
+ "surface",
770
+ "muted"
771
+ ];
772
+ if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || colorPatterns.some((p) => name.includes(p))) {
773
+ colors[name] = value;
774
+ }
775
+ }
776
+ return { colors, variables };
777
+ }
778
+ function detectDarkMode(projectRoot, cssContents) {
779
+ for (const cssContent of cssContents) {
780
+ if (cssContent.includes(".dark") || cssContent.includes('[data-theme="dark"]') || cssContent.includes("prefers-color-scheme: dark") || cssContent.includes("color-scheme: dark")) {
781
+ return true;
782
+ }
783
+ }
784
+ const layoutPaths = [
785
+ "src/app/layout.tsx",
786
+ "app/layout.tsx",
787
+ "src/app/layout.jsx",
788
+ "app/layout.jsx"
789
+ ];
790
+ for (const rel of layoutPaths) {
791
+ const fullPath = join5(projectRoot, rel);
792
+ if (existsSync5(fullPath)) {
793
+ try {
794
+ const layoutContent = readFileSync5(fullPath, "utf-8");
795
+ if (layoutContent.includes('className="dark"') || layoutContent.includes("className='dark'") || layoutContent.includes('class="dark"')) {
796
+ return true;
797
+ }
798
+ } catch {
799
+ }
800
+ }
801
+ }
802
+ const pkgPath = join5(projectRoot, "package.json");
803
+ if (existsSync5(pkgPath)) {
804
+ try {
805
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
806
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
807
+ if (allDeps["next-themes"] || allDeps["theme-toggle"] || allDeps["use-dark-mode"]) {
808
+ return true;
809
+ }
810
+ } catch {
811
+ }
812
+ }
813
+ const essencePath = join5(projectRoot, "decantr.essence.json");
814
+ if (existsSync5(essencePath)) {
815
+ try {
816
+ const essence = JSON.parse(readFileSync5(essencePath, "utf-8"));
817
+ const mode = essence.dna?.theme?.mode;
818
+ if (mode === "dark" || mode === "auto") {
819
+ return true;
820
+ }
821
+ } catch {
822
+ }
823
+ }
824
+ return false;
825
+ }
826
+ function scanStyling(projectRoot) {
827
+ let approach = "unknown";
828
+ let configFile;
829
+ let packageDeps = {};
830
+ const pkgPath = join5(projectRoot, "package.json");
831
+ if (existsSync5(pkgPath)) {
832
+ try {
833
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
834
+ packageDeps = { ...pkg.dependencies, ...pkg.devDependencies };
835
+ } catch {
836
+ packageDeps = {};
837
+ }
838
+ }
839
+ for (const cfg of TAILWIND_CONFIGS) {
840
+ if (existsSync5(join5(projectRoot, cfg))) {
841
+ approach = "tailwind";
842
+ configFile = cfg;
843
+ break;
844
+ }
845
+ }
846
+ if (approach === "unknown") {
847
+ if (packageDeps["@decantr/css"]) {
848
+ approach = "decantr-css";
849
+ configFile = "src/styles/tokens.css";
850
+ }
851
+ if (packageDeps.tailwindcss || packageDeps["@tailwindcss/postcss"] || packageDeps["@tailwindcss/vite"]) {
852
+ approach = "tailwind";
853
+ configFile = configFile ?? "package.json";
854
+ }
855
+ }
856
+ if (approach === "unknown") {
857
+ if (packageDeps.bootstrap || packageDeps["react-bootstrap"]) {
858
+ approach = "bootstrap";
859
+ configFile = "package.json";
860
+ } else if (packageDeps["@mui/material"] || packageDeps["@mui/system"] || packageDeps["@mui/joy"]) {
861
+ approach = "mui";
862
+ configFile = "package.json";
863
+ } else if (packageDeps["@chakra-ui/react"] || packageDeps["@chakra-ui/vue-next"] || packageDeps["@chakra-ui/system"]) {
864
+ approach = "chakra";
865
+ configFile = "package.json";
866
+ }
867
+ }
868
+ const decantrStyleFiles = DECANTR_STYLE_PATHS.filter((rel) => existsSync5(join5(projectRoot, rel)));
869
+ if (decantrStyleFiles.length >= 2) {
870
+ approach = "decantr-css";
871
+ configFile = decantrStyleFiles.join(" + ");
872
+ }
873
+ const cssContents = [];
874
+ for (const rel of GLOBALS_CSS_PATHS) {
875
+ const fullPath = join5(projectRoot, rel);
876
+ if (existsSync5(fullPath)) {
877
+ try {
878
+ cssContents.push(readFileSync5(fullPath, "utf-8"));
879
+ } catch {
880
+ }
881
+ }
882
+ }
883
+ for (const rel of DECANTR_STYLE_PATHS) {
884
+ if (GLOBALS_CSS_PATHS.includes(rel)) continue;
885
+ const fullPath = join5(projectRoot, rel);
886
+ if (existsSync5(fullPath)) {
887
+ try {
888
+ cssContents.push(readFileSync5(fullPath, "utf-8"));
889
+ } catch {
890
+ }
891
+ }
892
+ }
893
+ let colors = {};
894
+ let cssVariables = [];
895
+ for (const cssContent of cssContents) {
896
+ const extracted = extractCSSVariables(cssContent);
897
+ colors = { ...colors, ...extracted.colors };
898
+ cssVariables.push(...extracted.variables);
899
+ }
900
+ cssVariables = [...new Set(cssVariables)];
901
+ const darkMode = detectDarkMode(projectRoot, cssContents);
902
+ if (approach === "unknown" && cssContents.length > 0) {
903
+ approach = "css";
904
+ configFile = GLOBALS_CSS_PATHS.find((rel) => existsSync5(join5(projectRoot, rel)));
905
+ }
906
+ return {
907
+ approach,
908
+ configFile,
909
+ colors,
910
+ darkMode,
911
+ cssVariables
912
+ };
913
+ }
914
+
915
+ // src/ambient-context.ts
916
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
917
+ import { basename, extname as extname2, join as join6, relative as relative2, sep as sep2 } from "path";
918
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
919
+ ".decantr",
920
+ ".git",
921
+ ".next",
922
+ ".nuxt",
923
+ ".svelte-kit",
924
+ ".turbo",
925
+ ".vercel",
926
+ "build",
927
+ "coverage",
928
+ "dist",
929
+ "node_modules",
930
+ "playwright-report"
931
+ ]);
932
+ var ROOT_CONTEXT_FILES = /* @__PURE__ */ new Set([
933
+ "AGENTS.md",
934
+ "CLAUDE.md",
935
+ "GEMINI.md",
936
+ "README.md",
937
+ "copilot-instructions.md",
938
+ ".cursorrules",
939
+ ".windsurfrules",
940
+ ".cursorignore",
941
+ ".claudeignore",
942
+ "components.json",
943
+ "tailwind.config.js",
944
+ "tailwind.config.ts",
945
+ "tailwind.config.mjs",
946
+ "tailwind.config.cjs",
947
+ "next.config.js",
948
+ "next.config.ts",
949
+ "next.config.mjs",
950
+ "nuxt.config.js",
951
+ "nuxt.config.ts",
952
+ "astro.config.mjs",
953
+ "astro.config.ts",
954
+ "svelte.config.js",
955
+ "svelte.config.ts",
956
+ "angular.json",
957
+ "vite.config.js",
958
+ "vite.config.ts",
959
+ "vitest.config.ts",
960
+ "vitest.config.js",
961
+ "playwright.config.ts",
962
+ "playwright.config.js",
963
+ "tsconfig.json",
964
+ "package.json",
965
+ "decantr.essence.json"
966
+ ]);
967
+ var CONTEXT_DIRECTORIES = /* @__PURE__ */ new Set([
968
+ ".agents",
969
+ ".claude",
970
+ ".claude/initiatives",
971
+ ".claude/rules",
972
+ ".codex",
973
+ ".cursor",
974
+ ".cursor/rules",
975
+ ".github/workflows",
976
+ "docs",
977
+ "docs/initiatives",
978
+ "initiatives",
979
+ "memory",
980
+ "memories",
981
+ "project-memory",
982
+ "supabase"
983
+ ]);
984
+ function shouldSkipDir2(name) {
985
+ return SKIP_DIRS2.has(name);
986
+ }
987
+ function normalizedPath(relPath) {
988
+ return relPath.split(sep2).join("/");
989
+ }
990
+ function isPotentialContextFile(relPath, name) {
991
+ const normalized2 = normalizedPath(relPath);
992
+ if (ROOT_CONTEXT_FILES.has(name)) return true;
993
+ if (name.startsWith(".env")) return true;
994
+ if (normalized2.startsWith(".claude/")) return true;
995
+ if (normalized2.startsWith(".agents/")) return true;
996
+ if (normalized2.startsWith(".codex/")) return true;
997
+ if (normalized2.startsWith(".cursor/")) return true;
998
+ if (normalized2.startsWith(".github/workflows/")) return true;
999
+ if (normalized2.startsWith("docs/")) return true;
1000
+ if (normalized2.startsWith("initiatives/")) return true;
1001
+ if (normalized2.startsWith("memory/")) return true;
1002
+ if (normalized2.startsWith("memories/")) return true;
1003
+ if (normalized2.startsWith("project-memory/")) return true;
1004
+ if (normalized2.startsWith("supabase/")) return true;
1005
+ if (normalized2.startsWith("migrations/")) return true;
1006
+ if (normalized2.startsWith("db/")) return true;
1007
+ if (normalized2.startsWith("ROLEMIGRATIONS/")) return true;
1008
+ if (normalized2 === "src/middleware.ts" || normalized2 === "middleware.ts") return true;
1009
+ if (normalized2.includes("/middleware.")) return true;
1010
+ const ext = extname2(name).toLowerCase();
1011
+ return ext === ".md" || ext === ".mdx" || ext === ".sql" || ext === ".yml" || ext === ".yaml";
1012
+ }
1013
+ function classifyContext(relPath) {
1014
+ const normalized2 = normalizedPath(relPath);
1015
+ const lower = normalized2.toLowerCase();
1016
+ const name = basename(normalized2);
1017
+ if (lower === "decantr.essence.json") {
1018
+ return {
1019
+ role: "architecture",
1020
+ confidence: 0.82,
1021
+ reason: "existing Decantr contract evidence"
1022
+ };
1023
+ }
1024
+ 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")) {
1025
+ return {
1026
+ role: "feature-business",
1027
+ confidence: 0.78,
1028
+ reason: "feature, initiative, memory, or business-domain evidence"
1029
+ };
1030
+ }
1031
+ 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/")) {
1032
+ return {
1033
+ role: "assistant-specific",
1034
+ confidence: 0.98,
1035
+ reason: "assistant or AI-agent instruction surface"
1036
+ };
1037
+ }
1038
+ 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.")) {
1039
+ return {
1040
+ role: "security-data",
1041
+ confidence: 0.9,
1042
+ reason: "security, auth, schema, middleware, or data-governance evidence"
1043
+ };
1044
+ }
1045
+ 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")) {
1046
+ return {
1047
+ role: "design-system",
1048
+ confidence: 0.88,
1049
+ reason: "design system or styling convention evidence"
1050
+ };
1051
+ }
1052
+ 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") {
1053
+ return {
1054
+ role: "workflow-ci",
1055
+ confidence: 0.84,
1056
+ reason: "workflow, CI, deployment, or validation command evidence"
1057
+ };
1058
+ }
1059
+ 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")) {
1060
+ return {
1061
+ role: "architecture",
1062
+ confidence: 0.72,
1063
+ reason: "architecture, setup, or framework configuration evidence"
1064
+ };
1065
+ }
1066
+ if (lower.includes("complete") || lower.includes("summary") || lower.includes("deprecated") || lower.includes("legacy") || lower.includes("migration")) {
1067
+ return {
1068
+ role: "stale-or-historical",
1069
+ confidence: 0.64,
1070
+ reason: "historical or possibly stale project documentation"
1071
+ };
1072
+ }
1073
+ return { role: "unknown", confidence: 0.35, reason: "unclassified context candidate" };
1074
+ }
1075
+ function isSafeToCite(relPath) {
1076
+ const lower = normalizedPath(relPath).toLowerCase();
1077
+ if (lower.startsWith(".env") && lower !== ".env.example" && lower !== ".env.sample") return false;
1078
+ if (lower.includes("secret") || lower.includes("private-key") || lower.includes("credentials")) {
1079
+ return false;
1080
+ }
1081
+ return true;
1082
+ }
1083
+ function addDirectoryContext(items, projectRoot, relPath) {
1084
+ const fullPath = join6(projectRoot, relPath);
1085
+ if (!existsSync6(fullPath)) return;
1086
+ const stats = statSync3(fullPath);
1087
+ const classified = classifyContext(relPath);
1088
+ items.push({
1089
+ path: normalizedPath(relPath),
1090
+ type: "directory",
1091
+ role: classified.role,
1092
+ confidence: classified.confidence,
1093
+ sizeBytes: stats.size,
1094
+ safeToCite: isSafeToCite(relPath),
1095
+ reason: classified.reason
1096
+ });
1097
+ }
1098
+ function walk(projectRoot, dir, items, depth) {
1099
+ if (depth > 6) return;
1100
+ let entries;
1101
+ try {
1102
+ entries = readdirSync4(dir);
1103
+ } catch {
1104
+ return;
1105
+ }
1106
+ for (const entry of entries) {
1107
+ if (shouldSkipDir2(entry)) continue;
1108
+ const fullPath = join6(dir, entry);
1109
+ const relPath = normalizedPath(relative2(projectRoot, fullPath));
1110
+ let stats;
1111
+ try {
1112
+ stats = statSync3(fullPath);
1113
+ } catch {
1114
+ continue;
1115
+ }
1116
+ if (stats.isDirectory()) {
1117
+ if (CONTEXT_DIRECTORIES.has(relPath)) {
1118
+ addDirectoryContext(items, projectRoot, relPath);
1119
+ }
1120
+ walk(projectRoot, fullPath, items, depth + 1);
1121
+ continue;
1122
+ }
1123
+ if (!stats.isFile() || !isPotentialContextFile(relPath, entry)) continue;
1124
+ const classified = classifyContext(relPath);
1125
+ items.push({
1126
+ path: relPath,
1127
+ type: "file",
1128
+ role: classified.role,
1129
+ confidence: classified.confidence,
1130
+ sizeBytes: stats.size,
1131
+ safeToCite: isSafeToCite(relPath),
1132
+ reason: classified.reason
1133
+ });
1134
+ }
1135
+ }
1136
+ function summarize(items) {
1137
+ const summary = {
1138
+ "assistant-specific": 0,
1139
+ "security-data": 0,
1140
+ architecture: 0,
1141
+ "design-system": 0,
1142
+ "workflow-ci": 0,
1143
+ "feature-business": 0,
1144
+ "stale-or-historical": 0,
1145
+ unknown: 0
1146
+ };
1147
+ for (const item of items) summary[item.role] += 1;
1148
+ return summary;
1149
+ }
1150
+ function readSmallText(projectRoot, relPath) {
1151
+ const fullPath = join6(projectRoot, relPath);
1152
+ try {
1153
+ const stat = statSync3(fullPath);
1154
+ if (stat.size > 64e3) return "";
1155
+ return readFileSync6(fullPath, "utf-8");
1156
+ } catch {
1157
+ return "";
1158
+ }
1159
+ }
1160
+ function detectConflicts(projectRoot, items) {
1161
+ const text = items.filter((item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
1162
+ const conflicts = [];
1163
+ const frameworkSignals = [
1164
+ ["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
1165
+ ["angular", /\bangular\b/],
1166
+ ["svelte", /\bsvelte\b|\bsveltekit\b/],
1167
+ ["vue", /\bvue\b|\bnuxt\b/]
1168
+ ].filter(([, pattern]) => pattern.test(text));
1169
+ if (frameworkSignals.length > 1) {
1170
+ conflicts.push(
1171
+ `Multiple framework doctrines appear in ambient docs: ${frameworkSignals.map(([name]) => name).join(", ")}.`
1172
+ );
1173
+ }
1174
+ const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
1175
+ const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(
1176
+ text
1177
+ );
1178
+ if (forbidsTailwind && endorsesTailwind) {
1179
+ conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
1180
+ }
1181
+ if (/\bclient component\b/.test(text) && /\bserver components? only\b/.test(text)) {
1182
+ conflicts.push("Ambient docs may conflict on client vs server component boundaries.");
1183
+ }
1184
+ return conflicts;
1185
+ }
1186
+ function detectDecantrEssenceStaleRisk(projectRoot, items) {
1187
+ if (!items.some((item) => item.path === "decantr.essence.json")) return [];
1188
+ const content = readSmallText(projectRoot, "decantr.essence.json");
1189
+ if (!content) return [];
1190
+ try {
1191
+ const essence = JSON.parse(content);
1192
+ const risks = [];
1193
+ if (essence.version !== "3.1.0") {
1194
+ risks.push(
1195
+ `decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; migrate or review before treating it as current brownfield doctrine.`
1196
+ );
1197
+ }
1198
+ if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
1199
+ risks.push(
1200
+ "decantr.essence.json looks like an older Decantr default scaffold; verify before importing its theme or page layout as brownfield truth."
1201
+ );
1202
+ }
1203
+ return risks;
1204
+ } catch {
1205
+ return [
1206
+ "decantr.essence.json could not be parsed during ambient inventory; review before treating it as current doctrine."
1207
+ ];
1208
+ }
1209
+ }
1210
+ function detectStaleRisks(projectRoot, items) {
1211
+ const pathRisks = items.filter(
1212
+ (item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
1213
+ ).slice(0, 12).map((item) => `${item.path} may be historical; verify before treating it as current doctrine.`);
1214
+ return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
1215
+ }
1216
+ function scanAmbientContext(projectRoot) {
1217
+ const items = [];
1218
+ walk(projectRoot, projectRoot, items, 0);
1219
+ const deduped = [...new Map(items.map((item) => [item.path, item])).values()].sort(
1220
+ (a, b) => a.path.localeCompare(b.path)
1221
+ );
1222
+ return {
1223
+ version: 1,
1224
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
1225
+ items: deduped,
1226
+ summary: summarize(deduped),
1227
+ conflicts: detectConflicts(projectRoot, deduped),
1228
+ staleRisks: detectStaleRisks(projectRoot, deduped)
1229
+ };
1230
+ }
1231
+
1232
+ // src/doctrine-map.ts
1233
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1234
+ import { join as join7 } from "path";
1235
+ var PRECEDENCE_ORDER = [
1236
+ "security-data",
1237
+ "architecture",
1238
+ "design-system",
1239
+ "workflow-ci",
1240
+ "feature-business",
1241
+ "assistant-specific",
1242
+ "stale-or-historical",
1243
+ "unknown"
1244
+ ];
1245
+ var BASE_PRECEDENCE = {
1246
+ "security-data": 100,
1247
+ architecture: 88,
1248
+ "design-system": 82,
1249
+ "workflow-ci": 74,
1250
+ "feature-business": 66,
1251
+ "assistant-specific": 58,
1252
+ "stale-or-historical": 24,
1253
+ unknown: 12
1254
+ };
1255
+ function normalized(path) {
1256
+ return path.toLowerCase();
1257
+ }
1258
+ function isStalePath(path, staleRisks) {
1259
+ const lower = normalized(path);
1260
+ if (/complete|summary|legacy|deprecated/.test(lower)) return true;
1261
+ return staleRisks.some((risk) => risk.toLowerCase().startsWith(lower));
1262
+ }
1263
+ function inferArea(item) {
1264
+ const lower = normalized(item.path);
1265
+ 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/")) {
1266
+ return "security-data";
1267
+ }
1268
+ 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")) {
1269
+ return "design-system";
1270
+ }
1271
+ 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")) {
1272
+ return "architecture";
1273
+ }
1274
+ if (lower.includes("workflow") || lower.includes("deployment") || lower.includes("quality") || lower.includes("testing") || lower.includes("vitest") || lower.includes("playwright") || lower.startsWith(".github/workflows/")) {
1275
+ return "workflow-ci";
1276
+ }
1277
+ return item.role;
1278
+ }
1279
+ function precedenceFor(item, area, staleRisks) {
1280
+ let score = BASE_PRECEDENCE[area];
1281
+ const lower = normalized(item.path);
1282
+ if (lower.startsWith(".claude/rules/") || lower.startsWith(".cursor/rules/")) score += 6;
1283
+ if (lower === "claude.md" || lower === "agents.md" || lower === "copilot-instructions.md") {
1284
+ score += 3;
1285
+ }
1286
+ if (item.type === "directory") score -= 8;
1287
+ if (isStalePath(item.path, staleRisks)) score -= 35;
1288
+ if (!item.safeToCite) score -= 20;
1289
+ return Math.max(0, Math.min(100, score));
1290
+ }
1291
+ function summarize2(sources) {
1292
+ const summary = Object.fromEntries(PRECEDENCE_ORDER.map((area) => [area, 0]));
1293
+ for (const source of sources) summary[source.area] += 1;
1294
+ return summary;
1295
+ }
1296
+ function topSources(sources, areas, limit = 5) {
1297
+ return sources.filter((source) => source.currency === "current" && areas.includes(source.area)).slice(0, limit).map((source) => source.path);
1298
+ }
1299
+ function buildResolutions(conflicts, staleRisks, sources) {
1300
+ const resolutions = [];
1301
+ for (const conflict of conflicts) {
1302
+ const lower = conflict.toLowerCase();
1303
+ if (lower.includes("framework")) {
1304
+ resolutions.push({
1305
+ kind: "conflict",
1306
+ issue: conflict,
1307
+ recommendation: "Prefer package/config detection and current architecture sources over stale docs or assistant memory when deciding framework/runtime conventions.",
1308
+ preferredSources: topSources(sources, ["architecture", "workflow-ci"]),
1309
+ confidence: 0.78
1310
+ });
1311
+ continue;
1312
+ }
1313
+ if (lower.includes("tailwind")) {
1314
+ resolutions.push({
1315
+ kind: "conflict",
1316
+ issue: conflict,
1317
+ recommendation: "Preserve the existing styling system until the user approves migration; treat current design-system docs and Tailwind/shadcn config as the styling authority.",
1318
+ preferredSources: topSources(sources, ["design-system", "architecture"]),
1319
+ confidence: 0.82
1320
+ });
1321
+ continue;
1322
+ }
1323
+ if (lower.includes("client") && lower.includes("server")) {
1324
+ resolutions.push({
1325
+ kind: "conflict",
1326
+ issue: conflict,
1327
+ recommendation: "Prefer current framework architecture and security/data boundaries; stop and ask for review before moving client/server responsibilities.",
1328
+ preferredSources: topSources(sources, ["architecture", "security-data"]),
1329
+ confidence: 0.76
1330
+ });
1331
+ continue;
1332
+ }
1333
+ resolutions.push({
1334
+ kind: "conflict",
1335
+ issue: conflict,
1336
+ recommendation: "Use the highest-precedence current sources in the doctrine map and report the conflict before enforcing either side.",
1337
+ preferredSources: sources.filter((source) => source.currency === "current").slice(0, 5).map((source) => source.path),
1338
+ confidence: 0.62
1339
+ });
1340
+ }
1341
+ if (staleRisks.length > 0) {
1342
+ resolutions.push({
1343
+ kind: "stale-risk",
1344
+ issue: `${staleRisks.length} stale or historical source(s) detected.`,
1345
+ recommendation: "Treat stale-risk sources as historical evidence until confirmed by current security/data, architecture, design-system, workflow, or feature doctrine.",
1346
+ preferredSources: sources.filter((source) => source.currency === "current" && source.area !== "assistant-specific").slice(0, 5).map((source) => source.path),
1347
+ confidence: 0.84
1348
+ });
1349
+ }
1350
+ return resolutions;
1351
+ }
1352
+ function createDoctrineMap(ambient) {
1353
+ const sources = ambient.items.map((item) => {
1354
+ const area = inferArea(item);
1355
+ const stale = isStalePath(item.path, ambient.staleRisks);
1356
+ return {
1357
+ path: item.path,
1358
+ type: item.type,
1359
+ area: stale ? "stale-or-historical" : area,
1360
+ originalRole: item.role,
1361
+ precedence: precedenceFor(item, area, ambient.staleRisks),
1362
+ confidence: item.confidence,
1363
+ currency: !item.safeToCite ? "unsafe-to-cite" : stale ? "stale-risk" : "current",
1364
+ safeToCite: item.safeToCite,
1365
+ rationale: item.reason
1366
+ };
1367
+ }).sort((a, b) => b.precedence - a.precedence || a.path.localeCompare(b.path));
1368
+ return {
1369
+ version: 1,
1370
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1371
+ precedenceOrder: PRECEDENCE_ORDER,
1372
+ sources,
1373
+ summary: summarize2(sources),
1374
+ conflicts: ambient.conflicts,
1375
+ staleRisks: ambient.staleRisks,
1376
+ resolutions: buildResolutions(ambient.conflicts, ambient.staleRisks, sources),
1377
+ guidance: [
1378
+ "Treat security/data doctrine as highest precedence for implementation safety.",
1379
+ "Treat architecture and design-system sources as product conventions, not Decantr defaults.",
1380
+ "Treat workflow/CI sources as validation evidence for commands and release gates.",
1381
+ "Treat stale-risk sources as historical evidence until a current source confirms them.",
1382
+ "Do not cite unsafe sources directly in assistant context."
1383
+ ]
1384
+ };
1385
+ }
1386
+ function doctrineMapPath(projectRoot) {
1387
+ return join7(projectRoot, ".decantr", "doctrine-map.json");
1388
+ }
1389
+ function writeDoctrineMap(projectRoot, doctrine) {
1390
+ writeFileSync2(doctrineMapPath(projectRoot), JSON.stringify(doctrine, null, 2) + "\n", "utf-8");
1391
+ }
1392
+ function readDoctrineMap(projectRoot) {
1393
+ const path = doctrineMapPath(projectRoot);
1394
+ if (!existsSync7(path)) return null;
1395
+ try {
1396
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
1397
+ if (parsed.version !== 1 || !Array.isArray(parsed.sources)) return null;
1398
+ return parsed;
1399
+ } catch {
1400
+ return null;
1401
+ }
1402
+ }
1403
+
1404
+ export {
1405
+ scanProjectInteractions,
1406
+ scanRoutes,
1407
+ scanStyling,
1408
+ scanAmbientContext,
1409
+ createDoctrineMap,
1410
+ writeDoctrineMap,
1411
+ readDoctrineMap,
1412
+ buildGuardRegistryContext,
1413
+ sendGuardMetrics,
1414
+ isOptedIn,
1415
+ optIn,
1416
+ sendCliCommandTelemetry,
1417
+ collectMetrics
1418
+ };