@decantr/cli 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/bin.js +2 -2
- package/dist/{chunk-NBJCO4G5.js → chunk-3TH5PLFO.js} +1 -1
- package/dist/{chunk-AUQXYJ7T.js → chunk-ICSLIYSX.js} +1 -1
- package/dist/{chunk-IEW2QFYI.js → chunk-KT2ROK2D.js} +553 -486
- package/dist/{chunk-OD46PCR6.js → chunk-PAF4PBD3.js} +219 -9
- package/dist/{chunk-HO3OLDPQ.js → chunk-Q5OFRQY6.js} +852 -359
- package/dist/{heal-M6PRCIIF.js → heal-ZYD6NVGE.js} +2 -2
- package/dist/{health-ZXOPGNBZ.js → health-ETZXWGTW.js} +3 -3
- package/dist/index.js +2 -2
- package/dist/{studio-LHQXHBE7.js → studio-MKLBUC3A.js} +4 -4
- package/dist/{workspace-MOLAGT2B.js → workspace-KSFWRZEX.js} +4 -4
- package/package.json +4 -4
- package/src/templates/DECANTR.md.template +5 -5
|
@@ -1,9 +1,58 @@
|
|
|
1
|
+
// src/bundled-content.ts
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
function bundledDirCandidates(contentType) {
|
|
6
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
return [
|
|
8
|
+
join(currentDir, "bundled", contentType),
|
|
9
|
+
join(currentDir, "..", "src", "bundled", contentType),
|
|
10
|
+
join(currentDir, "..", "bundled", contentType)
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
function getBundledContentPath(contentType, id) {
|
|
14
|
+
for (const dir of bundledDirCandidates(contentType)) {
|
|
15
|
+
const candidate = join(dir, `${id}.json`);
|
|
16
|
+
if (existsSync(candidate)) return candidate;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function loadBundledContentItem(contentType, id) {
|
|
21
|
+
const path = getBundledContentPath(contentType, id);
|
|
22
|
+
if (!path) return null;
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
return { id, data, path };
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function loadBundledContentList(contentType) {
|
|
31
|
+
const entries = [];
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const dir of bundledDirCandidates(contentType)) {
|
|
34
|
+
if (!existsSync(dir)) continue;
|
|
35
|
+
try {
|
|
36
|
+
for (const file of readdirSync(dir).filter((name) => name.endsWith(".json"))) {
|
|
37
|
+
const id = file.replace(/\.json$/, "");
|
|
38
|
+
if (seen.has(id)) continue;
|
|
39
|
+
const path = join(dir, file);
|
|
40
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
41
|
+
entries.push({ id, data, path });
|
|
42
|
+
seen.add(id);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
1
50
|
// src/telemetry.ts
|
|
2
51
|
import { randomUUID } from "crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
52
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4
53
|
import { homedir } from "os";
|
|
5
|
-
import { dirname, join, resolve } from "path";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
54
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
55
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
56
|
import {
|
|
8
57
|
createFetchTelemetrySink,
|
|
9
58
|
createTelemetryClient,
|
|
@@ -28,26 +77,26 @@ async function sendGuardMetrics(metrics) {
|
|
|
28
77
|
}
|
|
29
78
|
}
|
|
30
79
|
function isOptedIn(projectRoot) {
|
|
31
|
-
const projectJsonPath =
|
|
32
|
-
if (!
|
|
80
|
+
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
81
|
+
if (!existsSync2(projectJsonPath)) return false;
|
|
33
82
|
try {
|
|
34
|
-
const data = JSON.parse(
|
|
83
|
+
const data = JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
|
|
35
84
|
return data.telemetry === true;
|
|
36
85
|
} catch {
|
|
37
86
|
return false;
|
|
38
87
|
}
|
|
39
88
|
}
|
|
40
89
|
function optIn(projectRoot) {
|
|
41
|
-
const projectJsonPath =
|
|
90
|
+
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
42
91
|
let data = {};
|
|
43
|
-
if (
|
|
92
|
+
if (existsSync2(projectJsonPath)) {
|
|
44
93
|
try {
|
|
45
|
-
data = JSON.parse(
|
|
94
|
+
data = JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
|
|
46
95
|
} catch {
|
|
47
96
|
}
|
|
48
97
|
}
|
|
49
98
|
data.telemetry = true;
|
|
50
|
-
mkdirSync(
|
|
99
|
+
mkdirSync(dirname2(projectJsonPath), { recursive: true });
|
|
51
100
|
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
52
101
|
}
|
|
53
102
|
async function captureCliTelemetryEvent(input) {
|
|
@@ -328,8 +377,8 @@ function collectMetrics(essence, issues) {
|
|
|
328
377
|
};
|
|
329
378
|
}
|
|
330
379
|
function getCliTelemetryIdentityStatus(projectRoot, options = {}) {
|
|
331
|
-
const projectJsonPath =
|
|
332
|
-
const hasProjectConfig =
|
|
380
|
+
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
381
|
+
const hasProjectConfig = existsSync2(projectJsonPath);
|
|
333
382
|
const identities = options.create ? ensureTelemetryIdentities(projectRoot) : null;
|
|
334
383
|
const projectData = readProjectJson(projectRoot);
|
|
335
384
|
return {
|
|
@@ -342,12 +391,12 @@ function getCliTelemetryIdentityStatus(projectRoot, options = {}) {
|
|
|
342
391
|
}
|
|
343
392
|
function ensureTelemetryIdentities(projectRoot) {
|
|
344
393
|
const installId = getOrCreateInstallId();
|
|
345
|
-
const projectJsonPath =
|
|
346
|
-
if (!
|
|
394
|
+
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
395
|
+
if (!existsSync2(projectJsonPath)) {
|
|
347
396
|
return null;
|
|
348
397
|
}
|
|
349
398
|
try {
|
|
350
|
-
const data = JSON.parse(
|
|
399
|
+
const data = JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
|
|
351
400
|
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
352
401
|
if (!projectId) {
|
|
353
402
|
projectId = `project_${randomUUID()}`;
|
|
@@ -361,10 +410,10 @@ function ensureTelemetryIdentities(projectRoot) {
|
|
|
361
410
|
}
|
|
362
411
|
function getOrCreateInstallId() {
|
|
363
412
|
const configDir = getConfigDir();
|
|
364
|
-
const configPath =
|
|
413
|
+
const configPath = join2(configDir, "config.json");
|
|
365
414
|
try {
|
|
366
|
-
if (
|
|
367
|
-
const data = JSON.parse(
|
|
415
|
+
if (existsSync2(configPath)) {
|
|
416
|
+
const data = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
368
417
|
if (typeof data.telemetryInstallId === "string") {
|
|
369
418
|
return data.telemetryInstallId;
|
|
370
419
|
}
|
|
@@ -386,17 +435,17 @@ function getOrCreateInstallId() {
|
|
|
386
435
|
}
|
|
387
436
|
}
|
|
388
437
|
function readExistingInstallId() {
|
|
389
|
-
const configPath =
|
|
390
|
-
if (!
|
|
438
|
+
const configPath = join2(getConfigDir(), "config.json");
|
|
439
|
+
if (!existsSync2(configPath)) return void 0;
|
|
391
440
|
try {
|
|
392
|
-
const data = JSON.parse(
|
|
441
|
+
const data = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
393
442
|
return readStringProperty(data, "telemetryInstallId");
|
|
394
443
|
} catch {
|
|
395
444
|
return void 0;
|
|
396
445
|
}
|
|
397
446
|
}
|
|
398
447
|
function getConfigDir() {
|
|
399
|
-
return process.env.DECANTR_CONFIG_DIR ||
|
|
448
|
+
return process.env.DECANTR_CONFIG_DIR || join2(homedir(), ".config", "decantr");
|
|
400
449
|
}
|
|
401
450
|
function getTelemetryEventsEndpoint() {
|
|
402
451
|
return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
|
|
@@ -422,13 +471,13 @@ function resolveCliTelemetryProjectRoot(projectRoot, args) {
|
|
|
422
471
|
const projectFlag = inferFlagValue(args, "--project");
|
|
423
472
|
if (!projectFlag) return projectRoot;
|
|
424
473
|
const candidate = resolve(projectRoot, projectFlag);
|
|
425
|
-
return
|
|
474
|
+
return existsSync2(join2(candidate, ".decantr", "project.json")) ? candidate : projectRoot;
|
|
426
475
|
}
|
|
427
476
|
function readProjectJson(projectRoot) {
|
|
428
|
-
const projectJsonPath =
|
|
429
|
-
if (!
|
|
477
|
+
const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
|
|
478
|
+
if (!existsSync2(projectJsonPath)) return null;
|
|
430
479
|
try {
|
|
431
|
-
return JSON.parse(
|
|
480
|
+
return JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
|
|
432
481
|
} catch {
|
|
433
482
|
return null;
|
|
434
483
|
}
|
|
@@ -519,15 +568,15 @@ function isRegistrySource(value) {
|
|
|
519
568
|
return value === "cache" || value === "custom" || value === "none" || value === "official" || value === "private";
|
|
520
569
|
}
|
|
521
570
|
function inferProjectScope(projectRoot) {
|
|
522
|
-
return
|
|
571
|
+
return existsSync2(join2(projectRoot, "pnpm-workspace.yaml")) || existsSync2(join2(projectRoot, "turbo.json")) || existsSync2(join2(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
523
572
|
}
|
|
524
573
|
function getCliVersion() {
|
|
525
574
|
try {
|
|
526
|
-
const here =
|
|
527
|
-
const candidates = [
|
|
575
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
576
|
+
const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
|
|
528
577
|
for (const candidate of candidates) {
|
|
529
|
-
if (
|
|
530
|
-
const pkg = JSON.parse(
|
|
578
|
+
if (existsSync2(candidate)) {
|
|
579
|
+
const pkg = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
531
580
|
if (pkg.version) {
|
|
532
581
|
return pkg.version;
|
|
533
582
|
}
|
|
@@ -542,12 +591,12 @@ function isLoopbackHost(host) {
|
|
|
542
591
|
}
|
|
543
592
|
|
|
544
593
|
// src/guard-context.ts
|
|
545
|
-
import { existsSync as
|
|
546
|
-
import { join as
|
|
594
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
595
|
+
import { join as join3 } from "path";
|
|
547
596
|
function loadJsonEntries(dir) {
|
|
548
|
-
if (!
|
|
597
|
+
if (!existsSync3(dir)) return [];
|
|
549
598
|
try {
|
|
550
|
-
return
|
|
599
|
+
return readdirSync2(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync3(join3(dir, file), "utf-8")));
|
|
551
600
|
} catch {
|
|
552
601
|
return [];
|
|
553
602
|
}
|
|
@@ -555,28 +604,35 @@ function loadJsonEntries(dir) {
|
|
|
555
604
|
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
556
605
|
const themeRegistry = /* @__PURE__ */ new Map();
|
|
557
606
|
const patternRegistry = /* @__PURE__ */ new Map();
|
|
558
|
-
const cacheDir =
|
|
559
|
-
const customDir =
|
|
560
|
-
for (const data of loadJsonEntries(
|
|
607
|
+
const cacheDir = join3(projectRoot, ".decantr", "cache");
|
|
608
|
+
const customDir = join3(projectRoot, ".decantr", "custom");
|
|
609
|
+
for (const data of loadJsonEntries(join3(cacheDir, "@official", "themes"))) {
|
|
561
610
|
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
562
611
|
themeRegistry.set(data.id, {
|
|
563
612
|
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
564
613
|
});
|
|
565
614
|
}
|
|
566
615
|
}
|
|
567
|
-
for (const data of loadJsonEntries(
|
|
616
|
+
for (const data of loadJsonEntries(join3(customDir, "themes"))) {
|
|
568
617
|
if (typeof data.id === "string") {
|
|
569
618
|
themeRegistry.set(`custom:${data.id}`, {
|
|
570
619
|
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
571
620
|
});
|
|
572
621
|
}
|
|
573
622
|
}
|
|
574
|
-
for (const data of loadJsonEntries(
|
|
623
|
+
for (const data of loadJsonEntries(join3(cacheDir, "@official", "patterns"))) {
|
|
575
624
|
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
576
625
|
patternRegistry.set(data.id, data);
|
|
577
626
|
}
|
|
578
627
|
}
|
|
579
|
-
for (const
|
|
628
|
+
for (const entry of loadBundledContentList("patterns")) {
|
|
629
|
+
const data = entry.data;
|
|
630
|
+
const id = typeof data.id === "string" ? data.id : entry.id;
|
|
631
|
+
if (!patternRegistry.has(id)) {
|
|
632
|
+
patternRegistry.set(id, data);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (const data of loadJsonEntries(join3(customDir, "patterns"))) {
|
|
580
636
|
if (typeof data.id === "string") {
|
|
581
637
|
patternRegistry.set(data.id, data);
|
|
582
638
|
}
|
|
@@ -585,8 +641,8 @@ function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
|
585
641
|
}
|
|
586
642
|
|
|
587
643
|
// src/lib/scan-interactions.ts
|
|
588
|
-
import { existsSync as
|
|
589
|
-
import { extname, join as
|
|
644
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
|
|
645
|
+
import { extname, join as join4, relative } from "path";
|
|
590
646
|
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
591
647
|
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
592
648
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -606,13 +662,13 @@ function walkSourceTree(rootDir) {
|
|
|
606
662
|
function walk2(dir) {
|
|
607
663
|
let entries;
|
|
608
664
|
try {
|
|
609
|
-
entries =
|
|
665
|
+
entries = readdirSync3(dir);
|
|
610
666
|
} catch {
|
|
611
667
|
return;
|
|
612
668
|
}
|
|
613
669
|
for (const entry of entries) {
|
|
614
670
|
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
615
|
-
const fullPath =
|
|
671
|
+
const fullPath = join4(dir, entry);
|
|
616
672
|
let s;
|
|
617
673
|
try {
|
|
618
674
|
s = statSync(fullPath);
|
|
@@ -624,7 +680,7 @@ function walkSourceTree(rootDir) {
|
|
|
624
680
|
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
625
681
|
if (s.size > MAX_FILE_SIZE) continue;
|
|
626
682
|
try {
|
|
627
|
-
sources.set(fullPath,
|
|
683
|
+
sources.set(relative(rootDir, fullPath) || entry, readFileSync4(fullPath, "utf8"));
|
|
628
684
|
} catch {
|
|
629
685
|
}
|
|
630
686
|
}
|
|
@@ -634,23 +690,23 @@ function walkSourceTree(rootDir) {
|
|
|
634
690
|
return sources;
|
|
635
691
|
}
|
|
636
692
|
function collectDeclaredInteractions(projectRoot) {
|
|
637
|
-
const manifestPath =
|
|
638
|
-
if (!
|
|
693
|
+
const manifestPath = join4(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
694
|
+
if (!existsSync4(manifestPath)) return [];
|
|
639
695
|
let manifest;
|
|
640
696
|
try {
|
|
641
|
-
manifest = JSON.parse(
|
|
697
|
+
manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
642
698
|
} catch {
|
|
643
699
|
return [];
|
|
644
700
|
}
|
|
645
701
|
const all = [];
|
|
646
702
|
const pages = manifest.pages ?? [];
|
|
647
|
-
const contextDir =
|
|
703
|
+
const contextDir = join4(projectRoot, ".decantr", "context");
|
|
648
704
|
for (const page of pages) {
|
|
649
|
-
const packPath =
|
|
650
|
-
if (!
|
|
705
|
+
const packPath = join4(contextDir, page.json);
|
|
706
|
+
if (!existsSync4(packPath)) continue;
|
|
651
707
|
let pack;
|
|
652
708
|
try {
|
|
653
|
-
pack = JSON.parse(
|
|
709
|
+
pack = JSON.parse(readFileSync4(packPath, "utf8"));
|
|
654
710
|
} catch {
|
|
655
711
|
continue;
|
|
656
712
|
}
|
|
@@ -669,82 +725,410 @@ function scanProjectInteractions(projectRoot) {
|
|
|
669
725
|
const sources = walkSourceTree(projectRoot);
|
|
670
726
|
if (sources.size === 0) return [];
|
|
671
727
|
const missing = verifyInteractionsInSource(declared, sources);
|
|
672
|
-
return missing.map(
|
|
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})` : ""}`;
|
|
736
|
+
}
|
|
737
|
+
);
|
|
673
738
|
}
|
|
674
739
|
|
|
675
|
-
// src/
|
|
676
|
-
import { existsSync as
|
|
677
|
-
import { join as
|
|
678
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
740
|
+
// src/ambient-context.ts
|
|
741
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
742
|
+
import { basename, extname as extname2, join as join5, relative as relative2, sep } from "path";
|
|
743
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
744
|
+
".decantr",
|
|
745
|
+
".git",
|
|
746
|
+
".next",
|
|
747
|
+
".nuxt",
|
|
748
|
+
".svelte-kit",
|
|
749
|
+
".turbo",
|
|
750
|
+
".vercel",
|
|
751
|
+
"build",
|
|
752
|
+
"coverage",
|
|
753
|
+
"dist",
|
|
754
|
+
"node_modules",
|
|
755
|
+
"playwright-report"
|
|
756
|
+
]);
|
|
757
|
+
var ROOT_CONTEXT_FILES = /* @__PURE__ */ new Set([
|
|
758
|
+
"AGENTS.md",
|
|
759
|
+
"CLAUDE.md",
|
|
760
|
+
"GEMINI.md",
|
|
761
|
+
"README.md",
|
|
762
|
+
"copilot-instructions.md",
|
|
763
|
+
".cursorrules",
|
|
764
|
+
".windsurfrules",
|
|
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
|
+
]);
|
|
679
809
|
function shouldSkipDir(name) {
|
|
680
|
-
return
|
|
810
|
+
return SKIP_DIRS.has(name);
|
|
681
811
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
|
|
812
|
+
function normalizedPath(relPath) {
|
|
813
|
+
return relPath.split(sep).join("/");
|
|
814
|
+
}
|
|
815
|
+
function isPotentialContextFile(relPath, name) {
|
|
816
|
+
const normalized2 = normalizedPath(relPath);
|
|
817
|
+
if (ROOT_CONTEXT_FILES.has(name)) return true;
|
|
818
|
+
if (name.startsWith(".env")) return true;
|
|
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";
|
|
837
|
+
}
|
|
838
|
+
function classifyContext(relPath) {
|
|
839
|
+
const normalized2 = normalizedPath(relPath);
|
|
840
|
+
const lower = normalized2.toLowerCase();
|
|
841
|
+
const name = basename(normalized2);
|
|
842
|
+
if (lower === "decantr.essence.json") {
|
|
843
|
+
return {
|
|
844
|
+
role: "architecture",
|
|
845
|
+
confidence: 0.82,
|
|
846
|
+
reason: "existing Decantr contract evidence"
|
|
847
|
+
};
|
|
685
848
|
}
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return `:${param.slice(4, -1)}*`;
|
|
693
|
-
}
|
|
694
|
-
return `:${param}`;
|
|
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
|
+
};
|
|
695
855
|
}
|
|
696
|
-
|
|
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
|
+
};
|
|
876
|
+
}
|
|
877
|
+
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") {
|
|
878
|
+
return {
|
|
879
|
+
role: "workflow-ci",
|
|
880
|
+
confidence: 0.84,
|
|
881
|
+
reason: "workflow, CI, deployment, or validation command evidence"
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
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")) {
|
|
885
|
+
return {
|
|
886
|
+
role: "architecture",
|
|
887
|
+
confidence: 0.72,
|
|
888
|
+
reason: "architecture, setup, or framework configuration evidence"
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (lower.includes("complete") || lower.includes("summary") || lower.includes("deprecated") || lower.includes("legacy") || lower.includes("migration")) {
|
|
892
|
+
return {
|
|
893
|
+
role: "stale-or-historical",
|
|
894
|
+
confidence: 0.64,
|
|
895
|
+
reason: "historical or possibly stale project documentation"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
return { role: "unknown", confidence: 0.35, reason: "unclassified context candidate" };
|
|
697
899
|
}
|
|
698
|
-
function
|
|
699
|
-
const
|
|
900
|
+
function isSafeToCite(relPath) {
|
|
901
|
+
const lower = normalizedPath(relPath).toLowerCase();
|
|
902
|
+
if (lower.startsWith(".env") && lower !== ".env.example" && lower !== ".env.sample") return false;
|
|
903
|
+
if (lower.includes("secret") || lower.includes("private-key") || lower.includes("credentials")) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
function addDirectoryContext(items, projectRoot, relPath) {
|
|
909
|
+
const fullPath = join5(projectRoot, relPath);
|
|
910
|
+
if (!existsSync5(fullPath)) return;
|
|
911
|
+
const stats = statSync2(fullPath);
|
|
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
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function walk(projectRoot, dir, items, depth) {
|
|
924
|
+
if (depth > 6) return;
|
|
700
925
|
let entries;
|
|
701
926
|
try {
|
|
702
|
-
entries =
|
|
927
|
+
entries = readdirSync4(dir);
|
|
703
928
|
} catch {
|
|
704
|
-
return
|
|
705
|
-
}
|
|
706
|
-
const hasPage = entries.some(
|
|
707
|
-
(e) => e === "page.tsx" || e === "page.ts" || e === "page.jsx" || e === "page.js"
|
|
708
|
-
);
|
|
709
|
-
const hasLayout = entries.some(
|
|
710
|
-
(e) => e === "layout.tsx" || e === "layout.ts" || e === "layout.jsx" || e === "layout.js"
|
|
711
|
-
);
|
|
712
|
-
if (hasPage) {
|
|
713
|
-
const routePath = "/" + segments.filter((s) => s !== "").join("/");
|
|
714
|
-
const pageFile = entries.find((e) => e.startsWith("page."));
|
|
715
|
-
routes.push({
|
|
716
|
-
path: routePath || "/",
|
|
717
|
-
file: relative(baseDir, join4(dir, pageFile)),
|
|
718
|
-
hasLayout
|
|
719
|
-
});
|
|
929
|
+
return;
|
|
720
930
|
}
|
|
721
931
|
for (const entry of entries) {
|
|
722
932
|
if (shouldSkipDir(entry)) continue;
|
|
723
|
-
const fullPath =
|
|
933
|
+
const fullPath = join5(dir, entry);
|
|
934
|
+
const relPath = normalizedPath(relative2(projectRoot, fullPath));
|
|
935
|
+
let stats;
|
|
724
936
|
try {
|
|
725
|
-
|
|
937
|
+
stats = statSync2(fullPath);
|
|
726
938
|
} catch {
|
|
727
939
|
continue;
|
|
728
940
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
941
|
+
if (stats.isDirectory()) {
|
|
942
|
+
if (CONTEXT_DIRECTORIES.has(relPath)) {
|
|
943
|
+
addDirectoryContext(items, projectRoot, relPath);
|
|
944
|
+
}
|
|
945
|
+
walk(projectRoot, fullPath, items, depth + 1);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!stats.isFile() || !isPotentialContextFile(relPath, entry)) continue;
|
|
949
|
+
const classified = classifyContext(relPath);
|
|
950
|
+
items.push({
|
|
951
|
+
path: relPath,
|
|
952
|
+
type: "file",
|
|
953
|
+
role: classified.role,
|
|
954
|
+
confidence: classified.confidence,
|
|
955
|
+
sizeBytes: stats.size,
|
|
956
|
+
safeToCite: isSafeToCite(relPath),
|
|
957
|
+
reason: classified.reason
|
|
958
|
+
});
|
|
732
959
|
}
|
|
733
|
-
return routes;
|
|
734
960
|
}
|
|
735
|
-
function
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
961
|
+
function summarize(items) {
|
|
962
|
+
const summary = {
|
|
963
|
+
"assistant-specific": 0,
|
|
964
|
+
"security-data": 0,
|
|
965
|
+
architecture: 0,
|
|
966
|
+
"design-system": 0,
|
|
967
|
+
"workflow-ci": 0,
|
|
968
|
+
"feature-business": 0,
|
|
969
|
+
"stale-or-historical": 0,
|
|
970
|
+
unknown: 0
|
|
971
|
+
};
|
|
972
|
+
for (const item of items) summary[item.role] += 1;
|
|
973
|
+
return summary;
|
|
974
|
+
}
|
|
975
|
+
function readSmallText(projectRoot, relPath) {
|
|
976
|
+
const fullPath = join5(projectRoot, relPath);
|
|
977
|
+
try {
|
|
978
|
+
const stat = statSync2(fullPath);
|
|
979
|
+
if (stat.size > 64e3) return "";
|
|
980
|
+
return readFileSync5(fullPath, "utf-8");
|
|
981
|
+
} 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.");
|
|
1008
|
+
}
|
|
1009
|
+
return conflicts;
|
|
1010
|
+
}
|
|
1011
|
+
function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
1012
|
+
if (!items.some((item) => item.path === "decantr.essence.json")) return [];
|
|
1013
|
+
const content = readSmallText(projectRoot, "decantr.essence.json");
|
|
1014
|
+
if (!content) return [];
|
|
1015
|
+
try {
|
|
1016
|
+
const essence = JSON.parse(content);
|
|
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;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return [
|
|
1031
|
+
"decantr.essence.json could not be parsed during ambient inventory; review before treating it as current doctrine."
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function detectStaleRisks(projectRoot, items) {
|
|
1036
|
+
const pathRisks = items.filter(
|
|
1037
|
+
(item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
|
|
1038
|
+
).slice(0, 12).map(
|
|
1039
|
+
(item) => `${item.path} may be historical; verify before treating it as current doctrine.`
|
|
1040
|
+
);
|
|
1041
|
+
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
1042
|
+
}
|
|
1043
|
+
function scanAmbientContext(projectRoot) {
|
|
1044
|
+
const items = [];
|
|
1045
|
+
walk(projectRoot, projectRoot, items, 0);
|
|
1046
|
+
const deduped = [...new Map(items.map((item) => [item.path, item])).values()].sort(
|
|
1047
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
1048
|
+
);
|
|
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
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/analyzers/routes.ts
|
|
1060
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
1061
|
+
import { join as join6, relative as relative3 } from "path";
|
|
1062
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "api", "_app", "_document"]);
|
|
1063
|
+
function shouldSkipDir2(name) {
|
|
1064
|
+
return name.startsWith("_") || name.startsWith(".") || SKIP_DIRS2.has(name);
|
|
1065
|
+
}
|
|
1066
|
+
function segmentToRoute(segment) {
|
|
1067
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
1071
|
+
const param = segment.slice(1, -1);
|
|
1072
|
+
if (param.startsWith("...")) {
|
|
1073
|
+
return `:${param.slice(3)}*`;
|
|
1074
|
+
}
|
|
1075
|
+
if (param.startsWith("[...") && param.endsWith("]")) {
|
|
1076
|
+
return `:${param.slice(4, -1)}*`;
|
|
1077
|
+
}
|
|
1078
|
+
return `:${param}`;
|
|
1079
|
+
}
|
|
1080
|
+
return segment;
|
|
1081
|
+
}
|
|
1082
|
+
function walkAppDir(dir, baseDir, segments) {
|
|
1083
|
+
const routes = [];
|
|
1084
|
+
let entries;
|
|
1085
|
+
try {
|
|
1086
|
+
entries = readdirSync5(dir);
|
|
740
1087
|
} catch {
|
|
741
1088
|
return routes;
|
|
742
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
|
+
});
|
|
1104
|
+
}
|
|
743
1105
|
for (const entry of entries) {
|
|
744
|
-
if (
|
|
745
|
-
const fullPath =
|
|
1106
|
+
if (shouldSkipDir2(entry)) continue;
|
|
1107
|
+
const fullPath = join6(dir, entry);
|
|
746
1108
|
try {
|
|
747
|
-
|
|
1109
|
+
if (!statSync3(fullPath).isDirectory()) continue;
|
|
1110
|
+
} catch {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const routeSegment = segmentToRoute(entry);
|
|
1114
|
+
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
1115
|
+
routes.push(...walkAppDir(fullPath, baseDir, nextSegments));
|
|
1116
|
+
}
|
|
1117
|
+
return routes;
|
|
1118
|
+
}
|
|
1119
|
+
function walkPagesDir(dir, baseDir, segments, extensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "md", "mdx"])) {
|
|
1120
|
+
const routes = [];
|
|
1121
|
+
let entries;
|
|
1122
|
+
try {
|
|
1123
|
+
entries = readdirSync5(dir);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return routes;
|
|
1126
|
+
}
|
|
1127
|
+
for (const entry of entries) {
|
|
1128
|
+
if (shouldSkipDir2(entry)) continue;
|
|
1129
|
+
const fullPath = join6(dir, entry);
|
|
1130
|
+
try {
|
|
1131
|
+
const stat = statSync3(fullPath);
|
|
748
1132
|
if (stat.isDirectory()) {
|
|
749
1133
|
const routeSegment = segmentToRoute(entry);
|
|
750
1134
|
const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
|
|
@@ -760,7 +1144,7 @@ function walkPagesDir(dir, baseDir, segments, extensions = /* @__PURE__ */ new S
|
|
|
760
1144
|
const routePath = "/" + [...segments, routeSegment].filter((s) => s !== "").join("/");
|
|
761
1145
|
routes.push({
|
|
762
1146
|
path: routePath || "/",
|
|
763
|
-
file:
|
|
1147
|
+
file: relative3(baseDir, fullPath),
|
|
764
1148
|
hasLayout: false
|
|
765
1149
|
});
|
|
766
1150
|
}
|
|
@@ -773,7 +1157,7 @@ function walkSvelteKitRoutes(dir, baseDir, segments) {
|
|
|
773
1157
|
const routes = [];
|
|
774
1158
|
let entries;
|
|
775
1159
|
try {
|
|
776
|
-
entries =
|
|
1160
|
+
entries = readdirSync5(dir);
|
|
777
1161
|
} catch {
|
|
778
1162
|
return routes;
|
|
779
1163
|
}
|
|
@@ -783,15 +1167,15 @@ function walkSvelteKitRoutes(dir, baseDir, segments) {
|
|
|
783
1167
|
const routePath = "/" + segments.filter((segment) => segment !== "").join("/");
|
|
784
1168
|
routes.push({
|
|
785
1169
|
path: routePath || "/",
|
|
786
|
-
file:
|
|
1170
|
+
file: relative3(baseDir, join6(dir, pageFile)),
|
|
787
1171
|
hasLayout
|
|
788
1172
|
});
|
|
789
1173
|
}
|
|
790
1174
|
for (const entry of entries) {
|
|
791
|
-
if (
|
|
792
|
-
const fullPath =
|
|
1175
|
+
if (shouldSkipDir2(entry)) continue;
|
|
1176
|
+
const fullPath = join6(dir, entry);
|
|
793
1177
|
try {
|
|
794
|
-
if (!
|
|
1178
|
+
if (!statSync3(fullPath).isDirectory()) continue;
|
|
795
1179
|
} catch {
|
|
796
1180
|
continue;
|
|
797
1181
|
}
|
|
@@ -806,15 +1190,15 @@ function collectRouteCandidateFiles(dir, files, depth = 0) {
|
|
|
806
1190
|
if (depth > 5) return;
|
|
807
1191
|
let entries;
|
|
808
1192
|
try {
|
|
809
|
-
entries =
|
|
1193
|
+
entries = readdirSync5(dir);
|
|
810
1194
|
} catch {
|
|
811
1195
|
return;
|
|
812
1196
|
}
|
|
813
1197
|
for (const entry of entries) {
|
|
814
1198
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
815
|
-
const fullPath =
|
|
1199
|
+
const fullPath = join6(dir, entry);
|
|
816
1200
|
try {
|
|
817
|
-
const stat =
|
|
1201
|
+
const stat = statSync3(fullPath);
|
|
818
1202
|
if (stat.isDirectory()) {
|
|
819
1203
|
collectRouteCandidateFiles(fullPath, files, depth + 1);
|
|
820
1204
|
} else if (stat.isFile()) {
|
|
@@ -828,22 +1212,22 @@ function collectRouteCandidateFiles(dir, files, depth = 0) {
|
|
|
828
1212
|
}
|
|
829
1213
|
}
|
|
830
1214
|
function scanReactRouter(projectRoot) {
|
|
831
|
-
const candidateDirs = [
|
|
1215
|
+
const candidateDirs = [join6(projectRoot, "src"), projectRoot];
|
|
832
1216
|
const candidateFiles = [];
|
|
833
1217
|
for (const dir of candidateDirs) {
|
|
834
|
-
if (
|
|
1218
|
+
if (existsSync6(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
835
1219
|
}
|
|
836
1220
|
const routeMap = /* @__PURE__ */ new Map();
|
|
837
1221
|
for (const absolutePath of candidateFiles) {
|
|
838
1222
|
let content;
|
|
839
1223
|
try {
|
|
840
|
-
content =
|
|
1224
|
+
content = readFileSync6(absolutePath, "utf-8");
|
|
841
1225
|
} catch {
|
|
842
1226
|
continue;
|
|
843
1227
|
}
|
|
844
1228
|
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");
|
|
845
1229
|
if (!isReactRouterFile) continue;
|
|
846
|
-
const relativePath =
|
|
1230
|
+
const relativePath = relative3(projectRoot, absolutePath);
|
|
847
1231
|
const pathMatches = /* @__PURE__ */ new Set();
|
|
848
1232
|
for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
|
|
849
1233
|
pathMatches.add(match[1]);
|
|
@@ -870,10 +1254,10 @@ function hasReactRouterDependency(projectRoot) {
|
|
|
870
1254
|
return hasDependency(projectRoot, ["react-router", "react-router-dom"]);
|
|
871
1255
|
}
|
|
872
1256
|
function hasDependency(projectRoot, names) {
|
|
873
|
-
const packageJsonPath =
|
|
874
|
-
if (!
|
|
1257
|
+
const packageJsonPath = join6(projectRoot, "package.json");
|
|
1258
|
+
if (!existsSync6(packageJsonPath)) return false;
|
|
875
1259
|
try {
|
|
876
|
-
const pkg = JSON.parse(
|
|
1260
|
+
const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
877
1261
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
878
1262
|
return names.some((name) => Boolean(deps[name]));
|
|
879
1263
|
} catch {
|
|
@@ -881,7 +1265,7 @@ function hasDependency(projectRoot, names) {
|
|
|
881
1265
|
}
|
|
882
1266
|
}
|
|
883
1267
|
function hasAnyFile(projectRoot, relPaths) {
|
|
884
|
-
return relPaths.some((relPath) =>
|
|
1268
|
+
return relPaths.some((relPath) => existsSync6(join6(projectRoot, relPath)));
|
|
885
1269
|
}
|
|
886
1270
|
function normalizeRoutePath(path) {
|
|
887
1271
|
const cleaned = path.trim();
|
|
@@ -890,22 +1274,22 @@ function normalizeRoutePath(path) {
|
|
|
890
1274
|
return cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
|
|
891
1275
|
}
|
|
892
1276
|
function scanAngularRouter(projectRoot) {
|
|
893
|
-
const candidateDirs = [
|
|
1277
|
+
const candidateDirs = [join6(projectRoot, "src", "app"), join6(projectRoot, "src")];
|
|
894
1278
|
const candidateFiles = [];
|
|
895
1279
|
for (const dir of candidateDirs) {
|
|
896
|
-
if (
|
|
1280
|
+
if (existsSync6(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
897
1281
|
}
|
|
898
1282
|
const routeMap = /* @__PURE__ */ new Map();
|
|
899
1283
|
for (const absolutePath of candidateFiles) {
|
|
900
1284
|
let content;
|
|
901
1285
|
try {
|
|
902
|
-
content =
|
|
1286
|
+
content = readFileSync6(absolutePath, "utf-8");
|
|
903
1287
|
} catch {
|
|
904
1288
|
continue;
|
|
905
1289
|
}
|
|
906
1290
|
const isRouterFile = content.includes("@angular/router") || content.includes("RouterModule.forRoot") || content.includes("provideRouter") || content.includes("Routes =");
|
|
907
1291
|
if (!isRouterFile) continue;
|
|
908
|
-
const relativePath =
|
|
1292
|
+
const relativePath = relative3(projectRoot, absolutePath);
|
|
909
1293
|
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
|
|
910
1294
|
const routePath = normalizeRoutePath(match[1]);
|
|
911
1295
|
if (!routePath || routeMap.has(routePath)) continue;
|
|
@@ -919,22 +1303,22 @@ function scanAngularRouter(projectRoot) {
|
|
|
919
1303
|
return [...routeMap.values()];
|
|
920
1304
|
}
|
|
921
1305
|
function scanVueRouter(projectRoot) {
|
|
922
|
-
const candidateDirs = [
|
|
1306
|
+
const candidateDirs = [join6(projectRoot, "src"), projectRoot];
|
|
923
1307
|
const candidateFiles = [];
|
|
924
1308
|
for (const dir of candidateDirs) {
|
|
925
|
-
if (
|
|
1309
|
+
if (existsSync6(dir)) collectRouteCandidateFiles(dir, candidateFiles);
|
|
926
1310
|
}
|
|
927
1311
|
const routeMap = /* @__PURE__ */ new Map();
|
|
928
1312
|
for (const absolutePath of candidateFiles) {
|
|
929
1313
|
let content;
|
|
930
1314
|
try {
|
|
931
|
-
content =
|
|
1315
|
+
content = readFileSync6(absolutePath, "utf-8");
|
|
932
1316
|
} catch {
|
|
933
1317
|
continue;
|
|
934
1318
|
}
|
|
935
1319
|
const isRouterFile = content.includes("vue-router") || content.includes("createRouter") || content.includes("createWebHistory") || content.includes("createWebHashHistory");
|
|
936
1320
|
if (!isRouterFile) continue;
|
|
937
|
-
const relativePath =
|
|
1321
|
+
const relativePath = relative3(projectRoot, absolutePath);
|
|
938
1322
|
for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
939
1323
|
const routePath = normalizeRoutePath(match[1]);
|
|
940
1324
|
if (!routePath || routeMap.has(routePath)) continue;
|
|
@@ -953,13 +1337,13 @@ function scanRoutes(projectRoot) {
|
|
|
953
1337
|
const hasNuxt = hasDependency(projectRoot, ["nuxt"]) || hasAnyFile(projectRoot, ["nuxt.config.js", "nuxt.config.ts"]);
|
|
954
1338
|
const hasAngular = hasDependency(projectRoot, ["@angular/core", "@angular/router"]) || hasAnyFile(projectRoot, ["angular.json"]);
|
|
955
1339
|
const hasVue = hasDependency(projectRoot, ["vue", "vue-router"]) || hasAnyFile(projectRoot, ["vite.config.js", "vite.config.ts"]);
|
|
956
|
-
const appDirs = [
|
|
1340
|
+
const appDirs = [join6(projectRoot, "src", "app"), join6(projectRoot, "app")];
|
|
957
1341
|
const appRoutes = appDirs.flatMap(
|
|
958
|
-
(appDir) =>
|
|
1342
|
+
(appDir) => existsSync6(appDir) ? walkAppDir(appDir, projectRoot, []) : []
|
|
959
1343
|
);
|
|
960
|
-
const pagesDirs = [
|
|
1344
|
+
const pagesDirs = [join6(projectRoot, "src", "pages"), join6(projectRoot, "pages")];
|
|
961
1345
|
const pagesRoutes = pagesDirs.flatMap(
|
|
962
|
-
(pagesDir) =>
|
|
1346
|
+
(pagesDir) => existsSync6(pagesDir) ? walkPagesDir(pagesDir, projectRoot, []) : []
|
|
963
1347
|
);
|
|
964
1348
|
if (hasNext) {
|
|
965
1349
|
if (appRoutes.length > 0 && pagesRoutes.length > 0) {
|
|
@@ -971,16 +1355,16 @@ function scanRoutes(projectRoot) {
|
|
|
971
1355
|
return { strategy: "app-router", routes: appRoutes };
|
|
972
1356
|
}
|
|
973
1357
|
if (hasSvelteKit) {
|
|
974
|
-
const svelteRoutesDir =
|
|
975
|
-
if (
|
|
1358
|
+
const svelteRoutesDir = join6(projectRoot, "src", "routes");
|
|
1359
|
+
if (existsSync6(svelteRoutesDir)) {
|
|
976
1360
|
const routes = walkSvelteKitRoutes(svelteRoutesDir, projectRoot, []);
|
|
977
1361
|
if (routes.length > 0) return { strategy: "sveltekit-router", routes };
|
|
978
1362
|
}
|
|
979
1363
|
}
|
|
980
1364
|
if (hasNuxt) {
|
|
981
|
-
const nuxtPagesDirs = [
|
|
1365
|
+
const nuxtPagesDirs = [join6(projectRoot, "pages"), join6(projectRoot, "app", "pages")];
|
|
982
1366
|
const routes = nuxtPagesDirs.flatMap(
|
|
983
|
-
(pagesDir) =>
|
|
1367
|
+
(pagesDir) => existsSync6(pagesDir) ? walkPagesDir(pagesDir, projectRoot, [], /* @__PURE__ */ new Set(["vue"])) : []
|
|
984
1368
|
);
|
|
985
1369
|
if (routes.length > 0) return { strategy: "nuxt-router", routes };
|
|
986
1370
|
}
|
|
@@ -1006,8 +1390,8 @@ function scanRoutes(projectRoot) {
|
|
|
1006
1390
|
}
|
|
1007
1391
|
|
|
1008
1392
|
// src/analyzers/styling.ts
|
|
1009
|
-
import { existsSync as
|
|
1010
|
-
import { join as
|
|
1393
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
1394
|
+
import { join as join7 } from "path";
|
|
1011
1395
|
var TAILWIND_CONFIGS = [
|
|
1012
1396
|
"tailwind.config.js",
|
|
1013
1397
|
"tailwind.config.ts",
|
|
@@ -1074,10 +1458,10 @@ function detectDarkMode(projectRoot, cssContents) {
|
|
|
1074
1458
|
"app/layout.jsx"
|
|
1075
1459
|
];
|
|
1076
1460
|
for (const rel of layoutPaths) {
|
|
1077
|
-
const fullPath =
|
|
1078
|
-
if (
|
|
1461
|
+
const fullPath = join7(projectRoot, rel);
|
|
1462
|
+
if (existsSync7(fullPath)) {
|
|
1079
1463
|
try {
|
|
1080
|
-
const layoutContent =
|
|
1464
|
+
const layoutContent = readFileSync7(fullPath, "utf-8");
|
|
1081
1465
|
if (layoutContent.includes('className="dark"') || layoutContent.includes("className='dark'") || layoutContent.includes('class="dark"')) {
|
|
1082
1466
|
return true;
|
|
1083
1467
|
}
|
|
@@ -1085,10 +1469,10 @@ function detectDarkMode(projectRoot, cssContents) {
|
|
|
1085
1469
|
}
|
|
1086
1470
|
}
|
|
1087
1471
|
}
|
|
1088
|
-
const pkgPath =
|
|
1089
|
-
if (
|
|
1472
|
+
const pkgPath = join7(projectRoot, "package.json");
|
|
1473
|
+
if (existsSync7(pkgPath)) {
|
|
1090
1474
|
try {
|
|
1091
|
-
const pkg = JSON.parse(
|
|
1475
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
1092
1476
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1093
1477
|
if (allDeps["next-themes"] || allDeps["theme-toggle"] || allDeps["use-dark-mode"]) {
|
|
1094
1478
|
return true;
|
|
@@ -1096,10 +1480,10 @@ function detectDarkMode(projectRoot, cssContents) {
|
|
|
1096
1480
|
} catch {
|
|
1097
1481
|
}
|
|
1098
1482
|
}
|
|
1099
|
-
const essencePath =
|
|
1100
|
-
if (
|
|
1483
|
+
const essencePath = join7(projectRoot, "decantr.essence.json");
|
|
1484
|
+
if (existsSync7(essencePath)) {
|
|
1101
1485
|
try {
|
|
1102
|
-
const essence = JSON.parse(
|
|
1486
|
+
const essence = JSON.parse(readFileSync7(essencePath, "utf-8"));
|
|
1103
1487
|
const mode = essence.dna?.theme?.mode;
|
|
1104
1488
|
if (mode === "dark" || mode === "auto") {
|
|
1105
1489
|
return true;
|
|
@@ -1113,17 +1497,17 @@ function scanStyling(projectRoot) {
|
|
|
1113
1497
|
let approach = "unknown";
|
|
1114
1498
|
let configFile;
|
|
1115
1499
|
let packageDeps = {};
|
|
1116
|
-
const pkgPath =
|
|
1117
|
-
if (
|
|
1500
|
+
const pkgPath = join7(projectRoot, "package.json");
|
|
1501
|
+
if (existsSync7(pkgPath)) {
|
|
1118
1502
|
try {
|
|
1119
|
-
const pkg = JSON.parse(
|
|
1503
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
1120
1504
|
packageDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1121
1505
|
} catch {
|
|
1122
1506
|
packageDeps = {};
|
|
1123
1507
|
}
|
|
1124
1508
|
}
|
|
1125
1509
|
for (const cfg of TAILWIND_CONFIGS) {
|
|
1126
|
-
if (
|
|
1510
|
+
if (existsSync7(join7(projectRoot, cfg))) {
|
|
1127
1511
|
approach = "tailwind";
|
|
1128
1512
|
configFile = cfg;
|
|
1129
1513
|
break;
|
|
@@ -1151,27 +1535,27 @@ function scanStyling(projectRoot) {
|
|
|
1151
1535
|
configFile = "package.json";
|
|
1152
1536
|
}
|
|
1153
1537
|
}
|
|
1154
|
-
const decantrStyleFiles = DECANTR_STYLE_PATHS.filter((rel) =>
|
|
1538
|
+
const decantrStyleFiles = DECANTR_STYLE_PATHS.filter((rel) => existsSync7(join7(projectRoot, rel)));
|
|
1155
1539
|
if (decantrStyleFiles.length >= 2) {
|
|
1156
1540
|
approach = "decantr-css";
|
|
1157
1541
|
configFile = decantrStyleFiles.join(" + ");
|
|
1158
1542
|
}
|
|
1159
1543
|
const cssContents = [];
|
|
1160
1544
|
for (const rel of GLOBALS_CSS_PATHS) {
|
|
1161
|
-
const fullPath =
|
|
1162
|
-
if (
|
|
1545
|
+
const fullPath = join7(projectRoot, rel);
|
|
1546
|
+
if (existsSync7(fullPath)) {
|
|
1163
1547
|
try {
|
|
1164
|
-
cssContents.push(
|
|
1548
|
+
cssContents.push(readFileSync7(fullPath, "utf-8"));
|
|
1165
1549
|
} catch {
|
|
1166
1550
|
}
|
|
1167
1551
|
}
|
|
1168
1552
|
}
|
|
1169
1553
|
for (const rel of DECANTR_STYLE_PATHS) {
|
|
1170
1554
|
if (GLOBALS_CSS_PATHS.includes(rel)) continue;
|
|
1171
|
-
const fullPath =
|
|
1172
|
-
if (
|
|
1555
|
+
const fullPath = join7(projectRoot, rel);
|
|
1556
|
+
if (existsSync7(fullPath)) {
|
|
1173
1557
|
try {
|
|
1174
|
-
cssContents.push(
|
|
1558
|
+
cssContents.push(readFileSync7(fullPath, "utf-8"));
|
|
1175
1559
|
} catch {
|
|
1176
1560
|
}
|
|
1177
1561
|
}
|
|
@@ -1187,7 +1571,7 @@ function scanStyling(projectRoot) {
|
|
|
1187
1571
|
const darkMode = detectDarkMode(projectRoot, cssContents);
|
|
1188
1572
|
if (approach === "unknown" && cssContents.length > 0) {
|
|
1189
1573
|
approach = "css";
|
|
1190
|
-
configFile = GLOBALS_CSS_PATHS.find((rel) =>
|
|
1574
|
+
configFile = GLOBALS_CSS_PATHS.find((rel) => existsSync7(join7(projectRoot, rel)));
|
|
1191
1575
|
}
|
|
1192
1576
|
return {
|
|
1193
1577
|
approach,
|
|
@@ -1198,328 +1582,9 @@ function scanStyling(projectRoot) {
|
|
|
1198
1582
|
};
|
|
1199
1583
|
}
|
|
1200
1584
|
|
|
1201
|
-
// src/ambient-context.ts
|
|
1202
|
-
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
1203
|
-
import { basename, extname as extname2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
1204
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
1205
|
-
".decantr",
|
|
1206
|
-
".git",
|
|
1207
|
-
".next",
|
|
1208
|
-
".nuxt",
|
|
1209
|
-
".svelte-kit",
|
|
1210
|
-
".turbo",
|
|
1211
|
-
".vercel",
|
|
1212
|
-
"build",
|
|
1213
|
-
"coverage",
|
|
1214
|
-
"dist",
|
|
1215
|
-
"node_modules",
|
|
1216
|
-
"playwright-report"
|
|
1217
|
-
]);
|
|
1218
|
-
var ROOT_CONTEXT_FILES = /* @__PURE__ */ new Set([
|
|
1219
|
-
"AGENTS.md",
|
|
1220
|
-
"CLAUDE.md",
|
|
1221
|
-
"GEMINI.md",
|
|
1222
|
-
"README.md",
|
|
1223
|
-
"copilot-instructions.md",
|
|
1224
|
-
".cursorrules",
|
|
1225
|
-
".windsurfrules",
|
|
1226
|
-
".cursorignore",
|
|
1227
|
-
".claudeignore",
|
|
1228
|
-
"components.json",
|
|
1229
|
-
"tailwind.config.js",
|
|
1230
|
-
"tailwind.config.ts",
|
|
1231
|
-
"tailwind.config.mjs",
|
|
1232
|
-
"tailwind.config.cjs",
|
|
1233
|
-
"next.config.js",
|
|
1234
|
-
"next.config.ts",
|
|
1235
|
-
"next.config.mjs",
|
|
1236
|
-
"nuxt.config.js",
|
|
1237
|
-
"nuxt.config.ts",
|
|
1238
|
-
"astro.config.mjs",
|
|
1239
|
-
"astro.config.ts",
|
|
1240
|
-
"svelte.config.js",
|
|
1241
|
-
"svelte.config.ts",
|
|
1242
|
-
"angular.json",
|
|
1243
|
-
"vite.config.js",
|
|
1244
|
-
"vite.config.ts",
|
|
1245
|
-
"vitest.config.ts",
|
|
1246
|
-
"vitest.config.js",
|
|
1247
|
-
"playwright.config.ts",
|
|
1248
|
-
"playwright.config.js",
|
|
1249
|
-
"tsconfig.json",
|
|
1250
|
-
"package.json",
|
|
1251
|
-
"decantr.essence.json"
|
|
1252
|
-
]);
|
|
1253
|
-
var CONTEXT_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1254
|
-
".agents",
|
|
1255
|
-
".claude",
|
|
1256
|
-
".claude/initiatives",
|
|
1257
|
-
".claude/rules",
|
|
1258
|
-
".codex",
|
|
1259
|
-
".cursor",
|
|
1260
|
-
".cursor/rules",
|
|
1261
|
-
".github/workflows",
|
|
1262
|
-
"docs",
|
|
1263
|
-
"docs/initiatives",
|
|
1264
|
-
"initiatives",
|
|
1265
|
-
"memory",
|
|
1266
|
-
"memories",
|
|
1267
|
-
"project-memory",
|
|
1268
|
-
"supabase"
|
|
1269
|
-
]);
|
|
1270
|
-
function shouldSkipDir2(name) {
|
|
1271
|
-
return SKIP_DIRS2.has(name);
|
|
1272
|
-
}
|
|
1273
|
-
function normalizedPath(relPath) {
|
|
1274
|
-
return relPath.split(sep2).join("/");
|
|
1275
|
-
}
|
|
1276
|
-
function isPotentialContextFile(relPath, name) {
|
|
1277
|
-
const normalized2 = normalizedPath(relPath);
|
|
1278
|
-
if (ROOT_CONTEXT_FILES.has(name)) return true;
|
|
1279
|
-
if (name.startsWith(".env")) return true;
|
|
1280
|
-
if (normalized2.startsWith(".claude/")) return true;
|
|
1281
|
-
if (normalized2.startsWith(".agents/")) return true;
|
|
1282
|
-
if (normalized2.startsWith(".codex/")) return true;
|
|
1283
|
-
if (normalized2.startsWith(".cursor/")) return true;
|
|
1284
|
-
if (normalized2.startsWith(".github/workflows/")) return true;
|
|
1285
|
-
if (normalized2.startsWith("docs/")) return true;
|
|
1286
|
-
if (normalized2.startsWith("initiatives/")) return true;
|
|
1287
|
-
if (normalized2.startsWith("memory/")) return true;
|
|
1288
|
-
if (normalized2.startsWith("memories/")) return true;
|
|
1289
|
-
if (normalized2.startsWith("project-memory/")) return true;
|
|
1290
|
-
if (normalized2.startsWith("supabase/")) return true;
|
|
1291
|
-
if (normalized2.startsWith("migrations/")) return true;
|
|
1292
|
-
if (normalized2.startsWith("db/")) return true;
|
|
1293
|
-
if (normalized2.startsWith("ROLEMIGRATIONS/")) return true;
|
|
1294
|
-
if (normalized2 === "src/middleware.ts" || normalized2 === "middleware.ts") return true;
|
|
1295
|
-
if (normalized2.includes("/middleware.")) return true;
|
|
1296
|
-
const ext = extname2(name).toLowerCase();
|
|
1297
|
-
return ext === ".md" || ext === ".mdx" || ext === ".sql" || ext === ".yml" || ext === ".yaml";
|
|
1298
|
-
}
|
|
1299
|
-
function classifyContext(relPath) {
|
|
1300
|
-
const normalized2 = normalizedPath(relPath);
|
|
1301
|
-
const lower = normalized2.toLowerCase();
|
|
1302
|
-
const name = basename(normalized2);
|
|
1303
|
-
if (lower === "decantr.essence.json") {
|
|
1304
|
-
return {
|
|
1305
|
-
role: "architecture",
|
|
1306
|
-
confidence: 0.82,
|
|
1307
|
-
reason: "existing Decantr contract evidence"
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
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")) {
|
|
1311
|
-
return {
|
|
1312
|
-
role: "feature-business",
|
|
1313
|
-
confidence: 0.78,
|
|
1314
|
-
reason: "feature, initiative, memory, or business-domain evidence"
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
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/")) {
|
|
1318
|
-
return {
|
|
1319
|
-
role: "assistant-specific",
|
|
1320
|
-
confidence: 0.98,
|
|
1321
|
-
reason: "assistant or AI-agent instruction surface"
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
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.")) {
|
|
1325
|
-
return {
|
|
1326
|
-
role: "security-data",
|
|
1327
|
-
confidence: 0.9,
|
|
1328
|
-
reason: "security, auth, schema, middleware, or data-governance evidence"
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
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")) {
|
|
1332
|
-
return {
|
|
1333
|
-
role: "design-system",
|
|
1334
|
-
confidence: 0.88,
|
|
1335
|
-
reason: "design system or styling convention evidence"
|
|
1336
|
-
};
|
|
1337
|
-
}
|
|
1338
|
-
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") {
|
|
1339
|
-
return {
|
|
1340
|
-
role: "workflow-ci",
|
|
1341
|
-
confidence: 0.84,
|
|
1342
|
-
reason: "workflow, CI, deployment, or validation command evidence"
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
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")) {
|
|
1346
|
-
return {
|
|
1347
|
-
role: "architecture",
|
|
1348
|
-
confidence: 0.72,
|
|
1349
|
-
reason: "architecture, setup, or framework configuration evidence"
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1352
|
-
if (lower.includes("complete") || lower.includes("summary") || lower.includes("deprecated") || lower.includes("legacy") || lower.includes("migration")) {
|
|
1353
|
-
return {
|
|
1354
|
-
role: "stale-or-historical",
|
|
1355
|
-
confidence: 0.64,
|
|
1356
|
-
reason: "historical or possibly stale project documentation"
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
return { role: "unknown", confidence: 0.35, reason: "unclassified context candidate" };
|
|
1360
|
-
}
|
|
1361
|
-
function isSafeToCite(relPath) {
|
|
1362
|
-
const lower = normalizedPath(relPath).toLowerCase();
|
|
1363
|
-
if (lower.startsWith(".env") && lower !== ".env.example" && lower !== ".env.sample") return false;
|
|
1364
|
-
if (lower.includes("secret") || lower.includes("private-key") || lower.includes("credentials")) {
|
|
1365
|
-
return false;
|
|
1366
|
-
}
|
|
1367
|
-
return true;
|
|
1368
|
-
}
|
|
1369
|
-
function addDirectoryContext(items, projectRoot, relPath) {
|
|
1370
|
-
const fullPath = join6(projectRoot, relPath);
|
|
1371
|
-
if (!existsSync6(fullPath)) return;
|
|
1372
|
-
const stats = statSync3(fullPath);
|
|
1373
|
-
const classified = classifyContext(relPath);
|
|
1374
|
-
items.push({
|
|
1375
|
-
path: normalizedPath(relPath),
|
|
1376
|
-
type: "directory",
|
|
1377
|
-
role: classified.role,
|
|
1378
|
-
confidence: classified.confidence,
|
|
1379
|
-
sizeBytes: stats.size,
|
|
1380
|
-
safeToCite: isSafeToCite(relPath),
|
|
1381
|
-
reason: classified.reason
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
function walk(projectRoot, dir, items, depth) {
|
|
1385
|
-
if (depth > 6) return;
|
|
1386
|
-
let entries;
|
|
1387
|
-
try {
|
|
1388
|
-
entries = readdirSync4(dir);
|
|
1389
|
-
} catch {
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
for (const entry of entries) {
|
|
1393
|
-
if (shouldSkipDir2(entry)) continue;
|
|
1394
|
-
const fullPath = join6(dir, entry);
|
|
1395
|
-
const relPath = normalizedPath(relative2(projectRoot, fullPath));
|
|
1396
|
-
let stats;
|
|
1397
|
-
try {
|
|
1398
|
-
stats = statSync3(fullPath);
|
|
1399
|
-
} catch {
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
if (stats.isDirectory()) {
|
|
1403
|
-
if (CONTEXT_DIRECTORIES.has(relPath)) {
|
|
1404
|
-
addDirectoryContext(items, projectRoot, relPath);
|
|
1405
|
-
}
|
|
1406
|
-
walk(projectRoot, fullPath, items, depth + 1);
|
|
1407
|
-
continue;
|
|
1408
|
-
}
|
|
1409
|
-
if (!stats.isFile() || !isPotentialContextFile(relPath, entry)) continue;
|
|
1410
|
-
const classified = classifyContext(relPath);
|
|
1411
|
-
items.push({
|
|
1412
|
-
path: relPath,
|
|
1413
|
-
type: "file",
|
|
1414
|
-
role: classified.role,
|
|
1415
|
-
confidence: classified.confidence,
|
|
1416
|
-
sizeBytes: stats.size,
|
|
1417
|
-
safeToCite: isSafeToCite(relPath),
|
|
1418
|
-
reason: classified.reason
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
function summarize(items) {
|
|
1423
|
-
const summary = {
|
|
1424
|
-
"assistant-specific": 0,
|
|
1425
|
-
"security-data": 0,
|
|
1426
|
-
architecture: 0,
|
|
1427
|
-
"design-system": 0,
|
|
1428
|
-
"workflow-ci": 0,
|
|
1429
|
-
"feature-business": 0,
|
|
1430
|
-
"stale-or-historical": 0,
|
|
1431
|
-
unknown: 0
|
|
1432
|
-
};
|
|
1433
|
-
for (const item of items) summary[item.role] += 1;
|
|
1434
|
-
return summary;
|
|
1435
|
-
}
|
|
1436
|
-
function readSmallText(projectRoot, relPath) {
|
|
1437
|
-
const fullPath = join6(projectRoot, relPath);
|
|
1438
|
-
try {
|
|
1439
|
-
const stat = statSync3(fullPath);
|
|
1440
|
-
if (stat.size > 64e3) return "";
|
|
1441
|
-
return readFileSync6(fullPath, "utf-8");
|
|
1442
|
-
} catch {
|
|
1443
|
-
return "";
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
function detectConflicts(projectRoot, items) {
|
|
1447
|
-
const text = items.filter(
|
|
1448
|
-
(item) => item.type === "file" && item.safeToCite && item.path.match(/\.(md|mdx|json|ts|js|yml|yaml)$/)
|
|
1449
|
-
).slice(0, 80).map((item) => readSmallText(projectRoot, item.path)).join("\n").toLowerCase();
|
|
1450
|
-
const conflicts = [];
|
|
1451
|
-
const frameworkSignals = [
|
|
1452
|
-
["next", /\bnext\.?js\b|\bapp router\b|\bpages router\b/],
|
|
1453
|
-
["angular", /\bangular\b/],
|
|
1454
|
-
["svelte", /\bsvelte\b|\bsveltekit\b/],
|
|
1455
|
-
["vue", /\bvue\b|\bnuxt\b/]
|
|
1456
|
-
].filter(([, pattern]) => pattern.test(text));
|
|
1457
|
-
if (frameworkSignals.length > 1) {
|
|
1458
|
-
conflicts.push(
|
|
1459
|
-
`Multiple framework doctrines appear in ambient docs: ${frameworkSignals.map(([name]) => name).join(", ")}.`
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
const forbidsTailwind = /\b(do not|don't|avoid|forbid|forbidden)\s+use\s+tailwind\b|\bno\s+tailwind\b/.test(text);
|
|
1463
|
-
const endorsesTailwind = /\btailwind\.config\b|\btailwindcss\b|\b@tailwind\b|\btailwind\s+classes\b/.test(text);
|
|
1464
|
-
if (forbidsTailwind && endorsesTailwind) {
|
|
1465
|
-
conflicts.push("Ambient docs contain both Tailwind usage and anti-Tailwind language.");
|
|
1466
|
-
}
|
|
1467
|
-
if (/\bclient component\b/.test(text) && /\bserver components? only\b/.test(text)) {
|
|
1468
|
-
conflicts.push("Ambient docs may conflict on client vs server component boundaries.");
|
|
1469
|
-
}
|
|
1470
|
-
return conflicts;
|
|
1471
|
-
}
|
|
1472
|
-
function detectDecantrEssenceStaleRisk(projectRoot, items) {
|
|
1473
|
-
if (!items.some((item) => item.path === "decantr.essence.json")) return [];
|
|
1474
|
-
const content = readSmallText(projectRoot, "decantr.essence.json");
|
|
1475
|
-
if (!content) return [];
|
|
1476
|
-
try {
|
|
1477
|
-
const essence = JSON.parse(content);
|
|
1478
|
-
const risks = [];
|
|
1479
|
-
if (essence.version !== "4.0.0") {
|
|
1480
|
-
risks.push(
|
|
1481
|
-
`decantr.essence.json uses Decantr essence version ${essence.version ?? "unknown"}; run decantr migrate --to v4 or review before treating it as current brownfield doctrine.`
|
|
1482
|
-
);
|
|
1483
|
-
}
|
|
1484
|
-
if (essence.dna?.theme?.id === "luminarum" && essence.structure) {
|
|
1485
|
-
risks.push(
|
|
1486
|
-
"decantr.essence.json looks like an older Decantr default scaffold; verify before importing its theme or page layout as brownfield truth."
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
return risks;
|
|
1490
|
-
} catch {
|
|
1491
|
-
return [
|
|
1492
|
-
"decantr.essence.json could not be parsed during ambient inventory; review before treating it as current doctrine."
|
|
1493
|
-
];
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
function detectStaleRisks(projectRoot, items) {
|
|
1497
|
-
const pathRisks = items.filter(
|
|
1498
|
-
(item) => item.role === "stale-or-historical" || /complete|summary|legacy|deprecated/i.test(item.path)
|
|
1499
|
-
).slice(0, 12).map(
|
|
1500
|
-
(item) => `${item.path} may be historical; verify before treating it as current doctrine.`
|
|
1501
|
-
);
|
|
1502
|
-
return [...pathRisks, ...detectDecantrEssenceStaleRisk(projectRoot, items)];
|
|
1503
|
-
}
|
|
1504
|
-
function scanAmbientContext(projectRoot) {
|
|
1505
|
-
const items = [];
|
|
1506
|
-
walk(projectRoot, projectRoot, items, 0);
|
|
1507
|
-
const deduped = [...new Map(items.map((item) => [item.path, item])).values()].sort(
|
|
1508
|
-
(a, b) => a.path.localeCompare(b.path)
|
|
1509
|
-
);
|
|
1510
|
-
return {
|
|
1511
|
-
version: 1,
|
|
1512
|
-
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1513
|
-
items: deduped,
|
|
1514
|
-
summary: summarize(deduped),
|
|
1515
|
-
conflicts: detectConflicts(projectRoot, deduped),
|
|
1516
|
-
staleRisks: detectStaleRisks(projectRoot, deduped)
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
1585
|
// src/doctrine-map.ts
|
|
1521
|
-
import { existsSync as
|
|
1522
|
-
import { join as
|
|
1586
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
1587
|
+
import { join as join8 } from "path";
|
|
1523
1588
|
var PRECEDENCE_ORDER = [
|
|
1524
1589
|
"security-data",
|
|
1525
1590
|
"architecture",
|
|
@@ -1672,16 +1737,16 @@ function createDoctrineMap(ambient) {
|
|
|
1672
1737
|
};
|
|
1673
1738
|
}
|
|
1674
1739
|
function doctrineMapPath(projectRoot) {
|
|
1675
|
-
return
|
|
1740
|
+
return join8(projectRoot, ".decantr", "doctrine-map.json");
|
|
1676
1741
|
}
|
|
1677
1742
|
function writeDoctrineMap(projectRoot, doctrine) {
|
|
1678
1743
|
writeFileSync2(doctrineMapPath(projectRoot), JSON.stringify(doctrine, null, 2) + "\n", "utf-8");
|
|
1679
1744
|
}
|
|
1680
1745
|
function readDoctrineMap(projectRoot) {
|
|
1681
1746
|
const path = doctrineMapPath(projectRoot);
|
|
1682
|
-
if (!
|
|
1747
|
+
if (!existsSync8(path)) return null;
|
|
1683
1748
|
try {
|
|
1684
|
-
const parsed = JSON.parse(
|
|
1749
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
1685
1750
|
if (parsed.version !== 1 || !Array.isArray(parsed.sources)) return null;
|
|
1686
1751
|
return parsed;
|
|
1687
1752
|
} catch {
|
|
@@ -1690,9 +1755,11 @@ function readDoctrineMap(projectRoot) {
|
|
|
1690
1755
|
}
|
|
1691
1756
|
|
|
1692
1757
|
export {
|
|
1758
|
+
loadBundledContentItem,
|
|
1759
|
+
loadBundledContentList,
|
|
1760
|
+
scanAmbientContext,
|
|
1693
1761
|
scanRoutes,
|
|
1694
1762
|
scanStyling,
|
|
1695
|
-
scanAmbientContext,
|
|
1696
1763
|
createDoctrineMap,
|
|
1697
1764
|
writeDoctrineMap,
|
|
1698
1765
|
readDoctrineMap,
|