@ctxo/cli 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -262
- package/dist/atomic-write-AQNYJFAG.js +10 -0
- package/dist/{chunk-WXA2JJLH.js → chunk-2TVHKCCU.js} +3 -2
- package/dist/chunk-2TVHKCCU.js.map +1 -0
- package/dist/chunk-6EKERTJ7.js +80 -0
- package/dist/chunk-6EKERTJ7.js.map +1 -0
- package/dist/{chunk-4M4V5V36.js → chunk-HXEPS2XY.js} +466 -78
- package/dist/chunk-HXEPS2XY.js.map +1 -0
- package/dist/{cli-router-T3VZZJD6.js → cli-router-HEZER4G7.js} +892 -201
- package/dist/cli-router-HEZER4G7.js.map +1 -0
- package/dist/index.js +310 -55
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-XOEE6VMD.js +8 -0
- package/dist/json-index-reader-XOEE6VMD.js.map +1 -0
- package/package.json +4 -2
- package/pages/visualize-template.html +285 -19
- package/dist/chunk-4M4V5V36.js.map +0 -1
- package/dist/chunk-WXA2JJLH.js.map +0 -1
- package/dist/cli-router-T3VZZJD6.js.map +0 -1
- package/dist/json-index-reader-LME4EDA5.js +0 -8
- /package/dist/{json-index-reader-LME4EDA5.js.map → atomic-write-AQNYJFAG.js.map} +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ArchitecturalOverlay,
|
|
4
|
+
BoundaryViolationDetector,
|
|
5
|
+
CommunitySnapshotWriter,
|
|
4
6
|
DeadCodeDetector,
|
|
7
|
+
DriftDetector,
|
|
5
8
|
PageRankCalculator,
|
|
6
9
|
RevertDetector,
|
|
7
10
|
SessionRecorderAdapter,
|
|
@@ -9,29 +12,33 @@ import {
|
|
|
9
12
|
SqliteStorageAdapter,
|
|
10
13
|
SymbolGraph,
|
|
11
14
|
aggregateCoChanges,
|
|
12
|
-
createLogger,
|
|
13
15
|
detectWorkspace,
|
|
14
16
|
indexIgnorePatterns,
|
|
15
17
|
indexIgnoreProjectPatterns,
|
|
16
18
|
loadConfig,
|
|
17
|
-
makeGlobMatcher
|
|
18
|
-
|
|
19
|
+
makeGlobMatcher,
|
|
20
|
+
watchSnapshotMinFileChanges
|
|
21
|
+
} from "./chunk-HXEPS2XY.js";
|
|
22
|
+
import {
|
|
23
|
+
atomicWrite,
|
|
24
|
+
createLogger
|
|
25
|
+
} from "./chunk-6EKERTJ7.js";
|
|
19
26
|
import {
|
|
20
27
|
JsonIndexReader
|
|
21
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-2TVHKCCU.js";
|
|
22
29
|
import {
|
|
23
30
|
StalenessDetector
|
|
24
31
|
} from "./chunk-PSKDDLSF.js";
|
|
25
32
|
|
|
26
33
|
// src/cli/cli-router.ts
|
|
27
|
-
import { readFileSync as
|
|
28
|
-
import { dirname as dirname8, join as
|
|
34
|
+
import { readFileSync as readFileSync14, existsSync as existsSync22 } from "fs";
|
|
35
|
+
import { dirname as dirname8, join as join24 } from "path";
|
|
29
36
|
|
|
30
37
|
// src/cli/index-command.ts
|
|
31
38
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
32
|
-
import { readFileSync as
|
|
33
|
-
import { join as
|
|
34
|
-
import { createRequire as
|
|
39
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync9, statSync, readdirSync as readdirSync2 } from "fs";
|
|
40
|
+
import { join as join8, relative, extname as extname3 } from "path";
|
|
41
|
+
import { createRequire as createRequire3 } from "module";
|
|
35
42
|
|
|
36
43
|
// src/core/staleness/content-hasher.ts
|
|
37
44
|
import { createHash } from "crypto";
|
|
@@ -67,6 +74,7 @@ import { dirname as dirname2, join } from "path";
|
|
|
67
74
|
|
|
68
75
|
// src/adapters/language/plugin-discovery.ts
|
|
69
76
|
import { existsSync, readFileSync } from "fs";
|
|
77
|
+
import { createRequire } from "module";
|
|
70
78
|
import { dirname, isAbsolute, resolve } from "path";
|
|
71
79
|
import { pathToFileURL } from "url";
|
|
72
80
|
import { SUPPORTED_API_VERSION } from "@ctxo/plugin-api";
|
|
@@ -93,9 +101,46 @@ function readManifestDeps(manifestPath) {
|
|
|
93
101
|
function isLocalSpecifier(spec) {
|
|
94
102
|
return spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec);
|
|
95
103
|
}
|
|
96
|
-
|
|
104
|
+
function pickEsmEntry(exportsField) {
|
|
105
|
+
if (!exportsField) return void 0;
|
|
106
|
+
if (typeof exportsField === "string") return exportsField;
|
|
107
|
+
if (typeof exportsField !== "object") return void 0;
|
|
108
|
+
const table = exportsField;
|
|
109
|
+
const dot = "." in table ? table["."] : exportsField;
|
|
110
|
+
if (typeof dot === "string") return dot;
|
|
111
|
+
if (!dot || typeof dot !== "object") return void 0;
|
|
112
|
+
for (const key of ["import", "node", "default"]) {
|
|
113
|
+
const nested = dot[key];
|
|
114
|
+
if (typeof nested === "string") return nested;
|
|
115
|
+
if (nested && typeof nested === "object") {
|
|
116
|
+
for (const inner of ["default", "import", "node"]) {
|
|
117
|
+
const v = nested[inner];
|
|
118
|
+
if (typeof v === "string") return v;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
var cliFallbackRequire = createRequire(import.meta.url);
|
|
125
|
+
function resolvePluginEntryUrl(specifier, manifestRequire) {
|
|
126
|
+
let pkgJsonPath;
|
|
127
|
+
try {
|
|
128
|
+
pkgJsonPath = manifestRequire.resolve(`${specifier}/package.json`);
|
|
129
|
+
} catch (primary) {
|
|
130
|
+
try {
|
|
131
|
+
pkgJsonPath = cliFallbackRequire.resolve(`${specifier}/package.json`);
|
|
132
|
+
} catch {
|
|
133
|
+
throw primary;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const pkgDir = dirname(pkgJsonPath);
|
|
137
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
138
|
+
const entry = pickEsmEntry(pkgJson.exports) ?? pkgJson.module ?? pkgJson.main ?? "index.js";
|
|
139
|
+
return pathToFileURL(resolve(pkgDir, entry)).href;
|
|
140
|
+
}
|
|
141
|
+
async function tryLoad(specifier, baseDir, manifestRequire) {
|
|
97
142
|
try {
|
|
98
|
-
const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : specifier;
|
|
143
|
+
const target = isLocalSpecifier(specifier) ? pathToFileURL(resolve(baseDir, specifier)).href : resolvePluginEntryUrl(specifier, manifestRequire);
|
|
99
144
|
const mod = await import(target);
|
|
100
145
|
const candidate = mod.default ?? mod.plugin ?? mod;
|
|
101
146
|
if (!candidate || typeof candidate !== "object") {
|
|
@@ -132,6 +177,7 @@ async function discoverPlugins(options) {
|
|
|
132
177
|
}
|
|
133
178
|
const baseDir = dirname(manifestPath);
|
|
134
179
|
const specifiers = options.explicit ?? readManifestDeps(manifestPath);
|
|
180
|
+
const manifestRequire = createRequire(pathToFileURL(manifestPath).href);
|
|
135
181
|
const plugins = [];
|
|
136
182
|
const failures = [];
|
|
137
183
|
for (const spec of specifiers) {
|
|
@@ -139,7 +185,7 @@ async function discoverPlugins(options) {
|
|
|
139
185
|
log.info(`Skipping plugin ${spec} (matched ignoreProjects)`);
|
|
140
186
|
continue;
|
|
141
187
|
}
|
|
142
|
-
const result = await tryLoad(spec, baseDir);
|
|
188
|
+
const result = await tryLoad(spec, baseDir, manifestRequire);
|
|
143
189
|
if ("apiVersion" in result) {
|
|
144
190
|
plugins.push({ plugin: result, specifier: spec });
|
|
145
191
|
} else {
|
|
@@ -219,7 +265,7 @@ async function loadPlugins(projectRoot, options = {}) {
|
|
|
219
265
|
// src/cli/install-command.ts
|
|
220
266
|
import { existsSync as existsSync5 } from "fs";
|
|
221
267
|
import { spawn } from "child_process";
|
|
222
|
-
import { createRequire } from "module";
|
|
268
|
+
import { createRequire as createRequire2 } from "module";
|
|
223
269
|
import { join as join4 } from "path";
|
|
224
270
|
|
|
225
271
|
// src/core/detection/detect-languages.ts
|
|
@@ -589,7 +635,7 @@ function normalizeLanguage(input) {
|
|
|
589
635
|
};
|
|
590
636
|
return aliases[lowered] ?? null;
|
|
591
637
|
}
|
|
592
|
-
var installRequire =
|
|
638
|
+
var installRequire = createRequire2(import.meta.url);
|
|
593
639
|
function isPluginInstalled(lang) {
|
|
594
640
|
try {
|
|
595
641
|
installRequire.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
@@ -603,15 +649,15 @@ function isLockedCI(env) {
|
|
|
603
649
|
return true;
|
|
604
650
|
}
|
|
605
651
|
function spawnInstall(command, args, cwd) {
|
|
606
|
-
return new Promise((
|
|
652
|
+
return new Promise((resolve4, reject) => {
|
|
607
653
|
const child = spawn(command, [...args], { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
608
|
-
child.on("exit", (code) =>
|
|
654
|
+
child.on("exit", (code) => resolve4(code ?? 0));
|
|
609
655
|
child.on("error", (err) => reject(err));
|
|
610
656
|
});
|
|
611
657
|
}
|
|
612
658
|
|
|
613
659
|
// src/adapters/storage/json-index-writer.ts
|
|
614
|
-
import {
|
|
660
|
+
import { mkdirSync, unlinkSync, existsSync as existsSync6 } from "fs";
|
|
615
661
|
import { dirname as dirname4, join as join5, resolve as resolve2, sep } from "path";
|
|
616
662
|
var JsonIndexWriter = class {
|
|
617
663
|
indexDir;
|
|
@@ -634,9 +680,7 @@ var JsonIndexWriter = class {
|
|
|
634
680
|
this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
|
|
635
681
|
}
|
|
636
682
|
atomicWrite(targetPath, content) {
|
|
637
|
-
|
|
638
|
-
writeFileSync(tmpPath, content, "utf-8");
|
|
639
|
-
renameSync(tmpPath, targetPath);
|
|
683
|
+
atomicWrite(targetPath, content);
|
|
640
684
|
}
|
|
641
685
|
delete(relativePath) {
|
|
642
686
|
const targetPath = this.resolveIndexPath(relativePath);
|
|
@@ -668,7 +712,7 @@ var JsonIndexWriter = class {
|
|
|
668
712
|
};
|
|
669
713
|
|
|
670
714
|
// src/adapters/storage/schema-manager.ts
|
|
671
|
-
import { readFileSync as readFileSync4, writeFileSync
|
|
715
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
672
716
|
import { join as join6, dirname as dirname5 } from "path";
|
|
673
717
|
var CURRENT_SCHEMA_VERSION = "1.0.0";
|
|
674
718
|
var SchemaManager = class {
|
|
@@ -687,7 +731,7 @@ var SchemaManager = class {
|
|
|
687
731
|
}
|
|
688
732
|
writeVersion() {
|
|
689
733
|
mkdirSync2(dirname5(this.versionFilePath), { recursive: true });
|
|
690
|
-
|
|
734
|
+
writeFileSync(this.versionFilePath, CURRENT_SCHEMA_VERSION, "utf-8");
|
|
691
735
|
}
|
|
692
736
|
isCompatible() {
|
|
693
737
|
const stored = this.readStoredVersion();
|
|
@@ -696,15 +740,349 @@ var SchemaManager = class {
|
|
|
696
740
|
}
|
|
697
741
|
};
|
|
698
742
|
|
|
743
|
+
// src/adapters/storage/architecture-artifacts-writer.ts
|
|
744
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
745
|
+
import { join as join7, resolve as resolve3 } from "path";
|
|
746
|
+
import { tmpdir } from "os";
|
|
747
|
+
var log3 = createLogger("ctxo:architecture-artifacts");
|
|
748
|
+
var DRIFT_FILE_NAME = "drift-events.json";
|
|
749
|
+
var VIOLATIONS_FILE_NAME = "boundary-violations.json";
|
|
750
|
+
var ArchitectureArtifactsWriter = class {
|
|
751
|
+
indexDir;
|
|
752
|
+
constructor(ctxoRoot, options = {}) {
|
|
753
|
+
this.indexDir = join7(ctxoRoot, "index");
|
|
754
|
+
assertSafeWritePath(ctxoRoot, options.allowProductionPath ?? false);
|
|
755
|
+
}
|
|
756
|
+
writeDriftEvents(artifact) {
|
|
757
|
+
mkdirSync3(this.indexDir, { recursive: true });
|
|
758
|
+
atomicWrite(join7(this.indexDir, DRIFT_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
759
|
+
}
|
|
760
|
+
writeBoundaryViolations(artifact) {
|
|
761
|
+
mkdirSync3(this.indexDir, { recursive: true });
|
|
762
|
+
atomicWrite(join7(this.indexDir, VIOLATIONS_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
763
|
+
}
|
|
764
|
+
readDriftEvents() {
|
|
765
|
+
const path = join7(this.indexDir, DRIFT_FILE_NAME);
|
|
766
|
+
if (!existsSync8(path)) return void 0;
|
|
767
|
+
try {
|
|
768
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
769
|
+
} catch (err) {
|
|
770
|
+
log3.warn(`failed to parse ${DRIFT_FILE_NAME}: ${err.message}`);
|
|
771
|
+
return void 0;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
readBoundaryViolations() {
|
|
775
|
+
const path = join7(this.indexDir, VIOLATIONS_FILE_NAME);
|
|
776
|
+
if (!existsSync8(path)) return void 0;
|
|
777
|
+
try {
|
|
778
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
779
|
+
} catch (err) {
|
|
780
|
+
log3.warn(`failed to parse ${VIOLATIONS_FILE_NAME}: ${err.message}`);
|
|
781
|
+
return void 0;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
function assertSafeWritePath(ctxoRoot, allowProductionPath) {
|
|
786
|
+
const resolvedRoot = resolve3(ctxoRoot);
|
|
787
|
+
const resolvedTmp = resolve3(tmpdir());
|
|
788
|
+
if (resolvedRoot.startsWith(resolvedTmp)) return;
|
|
789
|
+
if (allowProductionPath) return;
|
|
790
|
+
throw new Error(
|
|
791
|
+
`ArchitectureArtifactsWriter: refusing to write to non-tmp path ${resolvedRoot}. Pass { allowProductionPath: true } for production CLI writes, or target a tmpdir-based path in tests.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/core/overlay/community-detector.ts
|
|
796
|
+
import { UndirectedGraph } from "graphology";
|
|
797
|
+
import louvainModule from "graphology-communities-louvain";
|
|
798
|
+
|
|
799
|
+
// src/core/overlay/community-labeler.ts
|
|
800
|
+
var MIN_PREFIX_SEGMENTS = 1;
|
|
801
|
+
var PATH_SEPARATOR = "/";
|
|
802
|
+
function labelCommunities(membersByCommunity, pagerank) {
|
|
803
|
+
const labels = /* @__PURE__ */ new Map();
|
|
804
|
+
const used = /* @__PURE__ */ new Set();
|
|
805
|
+
for (const [communityId, members] of membersByCommunity) {
|
|
806
|
+
const label = pickLabel(members, pagerank, used);
|
|
807
|
+
labels.set(communityId, label);
|
|
808
|
+
used.add(label);
|
|
809
|
+
}
|
|
810
|
+
return labels;
|
|
811
|
+
}
|
|
812
|
+
function pickLabel(members, pagerank, used) {
|
|
813
|
+
const files = members.map(extractFilePath).filter((file) => Boolean(file));
|
|
814
|
+
const prefixLabel = longestCommonPathPrefix(files);
|
|
815
|
+
if (prefixLabel && !used.has(prefixLabel)) {
|
|
816
|
+
return prefixLabel;
|
|
817
|
+
}
|
|
818
|
+
const topSymbol = pickTopByPagerank(members, pagerank);
|
|
819
|
+
if (topSymbol) {
|
|
820
|
+
const name = extractSymbolName(topSymbol);
|
|
821
|
+
if (name) {
|
|
822
|
+
const candidate = `${name} area`;
|
|
823
|
+
if (!used.has(candidate)) return candidate;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (prefixLabel) {
|
|
827
|
+
return disambiguate(prefixLabel, used);
|
|
828
|
+
}
|
|
829
|
+
return disambiguate("cluster", used);
|
|
830
|
+
}
|
|
831
|
+
function extractFilePath(symbolId) {
|
|
832
|
+
const parts = symbolId.split("::");
|
|
833
|
+
return parts.length === 3 ? parts[0] : void 0;
|
|
834
|
+
}
|
|
835
|
+
function extractSymbolName(symbolId) {
|
|
836
|
+
const parts = symbolId.split("::");
|
|
837
|
+
return parts.length === 3 ? parts[1] : void 0;
|
|
838
|
+
}
|
|
839
|
+
function longestCommonPathPrefix(files) {
|
|
840
|
+
if (files.length === 0) return void 0;
|
|
841
|
+
const normalized = files.map((file) => file.replace(/\\/g, PATH_SEPARATOR));
|
|
842
|
+
const segments = normalized.map((file) => file.split(PATH_SEPARATOR));
|
|
843
|
+
const first = segments[0];
|
|
844
|
+
let matchLength = first.length;
|
|
845
|
+
for (let i = 1; i < segments.length; i++) {
|
|
846
|
+
const current = segments[i];
|
|
847
|
+
matchLength = Math.min(matchLength, current.length);
|
|
848
|
+
for (let j = 0; j < matchLength; j++) {
|
|
849
|
+
if (current[j] !== first[j]) {
|
|
850
|
+
matchLength = j;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (matchLength === 0) break;
|
|
855
|
+
}
|
|
856
|
+
const fullMatch = matchLength === first.length && files.length === 1;
|
|
857
|
+
const keep = fullMatch ? matchLength - 1 : matchLength;
|
|
858
|
+
if (keep < MIN_PREFIX_SEGMENTS) return void 0;
|
|
859
|
+
const prefix = first.slice(0, keep).join(PATH_SEPARATOR);
|
|
860
|
+
return prefix.length > 0 ? prefix : void 0;
|
|
861
|
+
}
|
|
862
|
+
function pickTopByPagerank(members, pagerank) {
|
|
863
|
+
let best;
|
|
864
|
+
let bestScore = -1;
|
|
865
|
+
for (const symbolId of members) {
|
|
866
|
+
const score = pagerank.get(symbolId) ?? 0;
|
|
867
|
+
if (score > bestScore || score === bestScore && best !== void 0 && symbolId < best) {
|
|
868
|
+
bestScore = score;
|
|
869
|
+
best = symbolId;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return best ?? members[0];
|
|
873
|
+
}
|
|
874
|
+
function disambiguate(base, used) {
|
|
875
|
+
if (!used.has(base)) return base;
|
|
876
|
+
let suffix = 2;
|
|
877
|
+
while (used.has(`${base} (${suffix})`)) suffix++;
|
|
878
|
+
return `${base} (${suffix})`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/core/overlay/community-detector.ts
|
|
882
|
+
var louvain = louvainModule.default ?? louvainModule;
|
|
883
|
+
var DEFAULT_RESOLUTION = 1;
|
|
884
|
+
var DEFAULT_GOD_NODE_MIN_BRIDGED = 3;
|
|
885
|
+
var DEFAULT_GOD_NODE_MAX_RESULTS = 25;
|
|
886
|
+
var CommunityDetector = class {
|
|
887
|
+
resolution;
|
|
888
|
+
godNodeMinBridged;
|
|
889
|
+
godNodeMaxResults;
|
|
890
|
+
constructor(options = {}) {
|
|
891
|
+
this.resolution = options.resolution ?? DEFAULT_RESOLUTION;
|
|
892
|
+
this.godNodeMinBridged = options.godNodeMinBridged ?? DEFAULT_GOD_NODE_MIN_BRIDGED;
|
|
893
|
+
this.godNodeMaxResults = options.godNodeMaxResults ?? DEFAULT_GOD_NODE_MAX_RESULTS;
|
|
894
|
+
}
|
|
895
|
+
detect(graph, pagerank, edgeQuality = "full") {
|
|
896
|
+
const nodes = graph.allNodes();
|
|
897
|
+
if (nodes.length === 0) {
|
|
898
|
+
return {
|
|
899
|
+
communities: [],
|
|
900
|
+
godNodes: [],
|
|
901
|
+
modularity: 0,
|
|
902
|
+
crossClusterEdges: 0,
|
|
903
|
+
edgeQuality
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const sortedNodes = [...nodes].sort(
|
|
907
|
+
(a, b) => a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0
|
|
908
|
+
);
|
|
909
|
+
const gph = new UndirectedGraph();
|
|
910
|
+
for (const node of sortedNodes) {
|
|
911
|
+
gph.addNode(node.symbolId);
|
|
912
|
+
}
|
|
913
|
+
const weightByPair = /* @__PURE__ */ new Map();
|
|
914
|
+
for (const edge of graph.allEdges()) {
|
|
915
|
+
if (edge.from === edge.to) continue;
|
|
916
|
+
if (!gph.hasNode(edge.from) || !gph.hasNode(edge.to)) continue;
|
|
917
|
+
const [a, b] = edge.from < edge.to ? [edge.from, edge.to] : [edge.to, edge.from];
|
|
918
|
+
const key = `${a}|${b}`;
|
|
919
|
+
weightByPair.set(key, (weightByPair.get(key) ?? 0) + 1);
|
|
920
|
+
}
|
|
921
|
+
for (const [key, weight] of weightByPair) {
|
|
922
|
+
const [from, to] = key.split("|");
|
|
923
|
+
gph.addEdge(from, to, { weight });
|
|
924
|
+
}
|
|
925
|
+
if (gph.size === 0) {
|
|
926
|
+
return {
|
|
927
|
+
communities: this.singletonCommunities(sortedNodes),
|
|
928
|
+
godNodes: [],
|
|
929
|
+
modularity: 0,
|
|
930
|
+
crossClusterEdges: 0,
|
|
931
|
+
edgeQuality
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const result = louvain.detailed(gph, {
|
|
935
|
+
resolution: this.resolution,
|
|
936
|
+
randomWalk: false,
|
|
937
|
+
fastLocalMoves: true,
|
|
938
|
+
getEdgeWeight: "weight"
|
|
939
|
+
});
|
|
940
|
+
const rawAssignments = result.communities;
|
|
941
|
+
const splitAssignments = this.splitDisconnectedCommunities(gph, rawAssignments);
|
|
942
|
+
const normalizedAssignments = this.normalizeCommunityIds(splitAssignments, sortedNodes);
|
|
943
|
+
const memberIndex = this.buildMemberIndex(normalizedAssignments);
|
|
944
|
+
const labels = labelCommunities(memberIndex, pagerank);
|
|
945
|
+
const communities = sortedNodes.map((node) => {
|
|
946
|
+
const communityId = normalizedAssignments.get(node.symbolId) ?? 0;
|
|
947
|
+
return {
|
|
948
|
+
symbolId: node.symbolId,
|
|
949
|
+
communityId,
|
|
950
|
+
communityLabel: labels.get(communityId) ?? `community-${communityId}`
|
|
951
|
+
};
|
|
952
|
+
});
|
|
953
|
+
const crossClusterEdges = this.countCrossClusterEdges(graph, normalizedAssignments);
|
|
954
|
+
const godNodes = this.detectGodNodes(graph, normalizedAssignments, pagerank);
|
|
955
|
+
return {
|
|
956
|
+
communities,
|
|
957
|
+
godNodes,
|
|
958
|
+
modularity: result.modularity,
|
|
959
|
+
crossClusterEdges,
|
|
960
|
+
edgeQuality
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
singletonCommunities(nodes) {
|
|
964
|
+
return nodes.map((node, i) => ({
|
|
965
|
+
symbolId: node.symbolId,
|
|
966
|
+
communityId: i,
|
|
967
|
+
communityLabel: `isolated-${i}`
|
|
968
|
+
}));
|
|
969
|
+
}
|
|
970
|
+
splitDisconnectedCommunities(gph, assignments) {
|
|
971
|
+
const result = /* @__PURE__ */ new Map();
|
|
972
|
+
const membersByCommunity = /* @__PURE__ */ new Map();
|
|
973
|
+
for (const [node, communityId] of Object.entries(assignments)) {
|
|
974
|
+
const list = membersByCommunity.get(communityId) ?? [];
|
|
975
|
+
list.push(node);
|
|
976
|
+
membersByCommunity.set(communityId, list);
|
|
977
|
+
}
|
|
978
|
+
let nextCommunityId = 0;
|
|
979
|
+
const sortedCommunities = [...membersByCommunity.entries()].sort(([a], [b]) => a - b);
|
|
980
|
+
for (const [, members] of sortedCommunities) {
|
|
981
|
+
const visited = /* @__PURE__ */ new Set();
|
|
982
|
+
for (const start of [...members].sort()) {
|
|
983
|
+
if (visited.has(start)) continue;
|
|
984
|
+
const componentId = nextCommunityId++;
|
|
985
|
+
const queue = [start];
|
|
986
|
+
visited.add(start);
|
|
987
|
+
while (queue.length > 0) {
|
|
988
|
+
const node = queue.shift();
|
|
989
|
+
result.set(node, componentId);
|
|
990
|
+
for (const neighbor of gph.neighbors(node)) {
|
|
991
|
+
if (visited.has(neighbor)) continue;
|
|
992
|
+
if (!members.includes(neighbor)) continue;
|
|
993
|
+
visited.add(neighbor);
|
|
994
|
+
queue.push(neighbor);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
normalizeCommunityIds(assignments, sortedNodes) {
|
|
1002
|
+
const remap = /* @__PURE__ */ new Map();
|
|
1003
|
+
const result = /* @__PURE__ */ new Map();
|
|
1004
|
+
let nextId = 0;
|
|
1005
|
+
for (const node of sortedNodes) {
|
|
1006
|
+
const raw = assignments.get(node.symbolId);
|
|
1007
|
+
if (raw === void 0) {
|
|
1008
|
+
result.set(node.symbolId, nextId);
|
|
1009
|
+
remap.set(nextId, nextId);
|
|
1010
|
+
nextId++;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
let normalized = remap.get(raw);
|
|
1014
|
+
if (normalized === void 0) {
|
|
1015
|
+
normalized = nextId++;
|
|
1016
|
+
remap.set(raw, normalized);
|
|
1017
|
+
}
|
|
1018
|
+
result.set(node.symbolId, normalized);
|
|
1019
|
+
}
|
|
1020
|
+
return result;
|
|
1021
|
+
}
|
|
1022
|
+
buildMemberIndex(assignments) {
|
|
1023
|
+
const index = /* @__PURE__ */ new Map();
|
|
1024
|
+
for (const [symbolId, communityId] of assignments) {
|
|
1025
|
+
const list = index.get(communityId) ?? [];
|
|
1026
|
+
list.push(symbolId);
|
|
1027
|
+
index.set(communityId, list);
|
|
1028
|
+
}
|
|
1029
|
+
for (const list of index.values()) list.sort();
|
|
1030
|
+
return index;
|
|
1031
|
+
}
|
|
1032
|
+
countCrossClusterEdges(graph, assignments) {
|
|
1033
|
+
let count = 0;
|
|
1034
|
+
for (const edge of graph.allEdges()) {
|
|
1035
|
+
const from = assignments.get(edge.from);
|
|
1036
|
+
const to = assignments.get(edge.to);
|
|
1037
|
+
if (from === void 0 || to === void 0) continue;
|
|
1038
|
+
if (from !== to) count++;
|
|
1039
|
+
}
|
|
1040
|
+
return count;
|
|
1041
|
+
}
|
|
1042
|
+
detectGodNodes(graph, assignments, pagerank) {
|
|
1043
|
+
const candidates = [];
|
|
1044
|
+
for (const node of graph.allNodes()) {
|
|
1045
|
+
const ownCommunity = assignments.get(node.symbolId);
|
|
1046
|
+
if (ownCommunity === void 0) continue;
|
|
1047
|
+
const bridged = /* @__PURE__ */ new Set();
|
|
1048
|
+
const forward = graph.getForwardEdges(node.symbolId);
|
|
1049
|
+
const reverse = graph.getReverseEdges(node.symbolId);
|
|
1050
|
+
for (const edge of forward) {
|
|
1051
|
+
const other = assignments.get(edge.to);
|
|
1052
|
+
if (other !== void 0 && other !== ownCommunity) bridged.add(other);
|
|
1053
|
+
}
|
|
1054
|
+
for (const edge of reverse) {
|
|
1055
|
+
const other = assignments.get(edge.from);
|
|
1056
|
+
if (other !== void 0 && other !== ownCommunity) bridged.add(other);
|
|
1057
|
+
}
|
|
1058
|
+
if (bridged.size >= this.godNodeMinBridged) {
|
|
1059
|
+
const pr = pagerank.get(node.symbolId) ?? 0;
|
|
1060
|
+
candidates.push({
|
|
1061
|
+
symbolId: node.symbolId,
|
|
1062
|
+
bridgedCommunities: [...bridged].sort((a, b) => a - b),
|
|
1063
|
+
centralityScore: bridged.size * (pr > 0 ? pr : 1 / graph.nodeCount)
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
candidates.sort((a, b) => {
|
|
1068
|
+
if (b.centralityScore !== a.centralityScore) {
|
|
1069
|
+
return b.centralityScore - a.centralityScore;
|
|
1070
|
+
}
|
|
1071
|
+
return a.symbolId.localeCompare(b.symbolId);
|
|
1072
|
+
});
|
|
1073
|
+
return candidates.slice(0, this.godNodeMaxResults);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
699
1077
|
// src/cli/index-command.ts
|
|
700
|
-
var requireCjs =
|
|
1078
|
+
var requireCjs = createRequire3(import.meta.url);
|
|
701
1079
|
var IndexCommand = class {
|
|
702
1080
|
projectRoot;
|
|
703
1081
|
ctxoRoot;
|
|
704
1082
|
supportedExtensions;
|
|
705
1083
|
constructor(projectRoot, ctxoRoot) {
|
|
706
1084
|
this.projectRoot = projectRoot;
|
|
707
|
-
this.ctxoRoot = ctxoRoot ??
|
|
1085
|
+
this.ctxoRoot = ctxoRoot ?? join8(projectRoot, ".ctxo");
|
|
708
1086
|
this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
|
|
709
1087
|
}
|
|
710
1088
|
loadIgnoreConfig() {
|
|
@@ -742,7 +1120,7 @@ var IndexCommand = class {
|
|
|
742
1120
|
const revertDetector = new RevertDetector();
|
|
743
1121
|
let files;
|
|
744
1122
|
if (options.file) {
|
|
745
|
-
const fullPath =
|
|
1123
|
+
const fullPath = join8(this.projectRoot, options.file);
|
|
746
1124
|
files = [fullPath];
|
|
747
1125
|
console.error(`[ctxo] Incremental re-index: ${options.file}`);
|
|
748
1126
|
} else {
|
|
@@ -754,6 +1132,14 @@ var IndexCommand = class {
|
|
|
754
1132
|
}
|
|
755
1133
|
console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
|
|
756
1134
|
}
|
|
1135
|
+
try {
|
|
1136
|
+
const { sweepStaleTmpFiles } = await import("./atomic-write-AQNYJFAG.js");
|
|
1137
|
+
const swept = sweepStaleTmpFiles(join8(this.ctxoRoot, "index"));
|
|
1138
|
+
if (swept > 0) {
|
|
1139
|
+
console.error(`[ctxo] Cleaned ${swept} orphan .tmp file(s) from previous interrupted run`);
|
|
1140
|
+
}
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
757
1143
|
if (roslynAdapter?.isReady()) {
|
|
758
1144
|
const hasCsFiles = files.some((f) => f.endsWith(".cs"));
|
|
759
1145
|
if (hasCsFiles) {
|
|
@@ -768,7 +1154,7 @@ var IndexCommand = class {
|
|
|
768
1154
|
if (!adapter) continue;
|
|
769
1155
|
const relativePath = relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
770
1156
|
try {
|
|
771
|
-
const source =
|
|
1157
|
+
const source = readFileSync6(filePath, "utf-8");
|
|
772
1158
|
const lastModified = Math.floor(Date.now() / 1e3);
|
|
773
1159
|
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
774
1160
|
const complexity = await adapter.extractComplexity(relativePath, source);
|
|
@@ -833,19 +1219,52 @@ var IndexCommand = class {
|
|
|
833
1219
|
writer.writeCoChanges(coChangeMatrix);
|
|
834
1220
|
console.error(`[ctxo] Co-change analysis: ${coChangeMatrix.entries.length} file pairs detected`);
|
|
835
1221
|
}
|
|
1222
|
+
let communitySnapshot;
|
|
1223
|
+
if (options.skipCommunity && pendingIndices.length > 0) {
|
|
1224
|
+
if (existsSync9(join8(this.ctxoRoot, "index", "communities.json"))) {
|
|
1225
|
+
console.error(
|
|
1226
|
+
`[ctxo] WARN stale communities.json present \u2014 --skip-community preserved existing snapshot. Run \`ctxo index\` (full) to refresh.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
} else if (pendingIndices.length > 0) {
|
|
1230
|
+
try {
|
|
1231
|
+
const graphIndices = this.buildGraphInputIndices(pendingIndices);
|
|
1232
|
+
communitySnapshot = this.buildCommunitySnapshot(
|
|
1233
|
+
graphIndices,
|
|
1234
|
+
{
|
|
1235
|
+
csharpTier,
|
|
1236
|
+
goTier,
|
|
1237
|
+
tsCount: graphIndices.filter((idx) => /\.(ts|tsx|js|jsx)$/.test(idx.file)).length
|
|
1238
|
+
},
|
|
1239
|
+
gitAdapter
|
|
1240
|
+
);
|
|
1241
|
+
const scope = options.file ? " (incremental, full-graph recompute)" : "";
|
|
1242
|
+
console.error(
|
|
1243
|
+
`[ctxo] Community detection${scope}: ${countCommunities(communitySnapshot)} clusters (modularity ${communitySnapshot.modularity.toFixed(3)})`
|
|
1244
|
+
);
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
console.error(`[ctxo] Community detection skipped: ${err.message}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
836
1249
|
const indices = [];
|
|
837
1250
|
for (const { fileIndex } of pendingIndices) {
|
|
838
1251
|
writer.write(fileIndex);
|
|
839
1252
|
indices.push(fileIndex);
|
|
840
1253
|
}
|
|
841
1254
|
schemaManager.writeVersion();
|
|
842
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1255
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
843
1256
|
try {
|
|
844
1257
|
await storage.initEmpty();
|
|
845
1258
|
storage.bulkWrite(indices);
|
|
1259
|
+
if (communitySnapshot) {
|
|
1260
|
+
storage.writeCommunities(communitySnapshot);
|
|
1261
|
+
}
|
|
846
1262
|
} finally {
|
|
847
1263
|
storage.close();
|
|
848
1264
|
}
|
|
1265
|
+
if (communitySnapshot) {
|
|
1266
|
+
this.writeArchitectureArtifacts(indices, communitySnapshot);
|
|
1267
|
+
}
|
|
849
1268
|
if (!options.skipSideEffects) {
|
|
850
1269
|
this.ensureGitignore();
|
|
851
1270
|
}
|
|
@@ -927,36 +1346,36 @@ var IndexCommand = class {
|
|
|
927
1346
|
encoding: "utf-8",
|
|
928
1347
|
maxBuffer: 10 * 1024 * 1024
|
|
929
1348
|
});
|
|
930
|
-
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) =>
|
|
1349
|
+
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) => join8(root, line)).filter((abs) => {
|
|
931
1350
|
const rel = relative(this.projectRoot, abs).replace(/\\/g, "/");
|
|
932
1351
|
return !ignoreFile(rel);
|
|
933
|
-
});
|
|
1352
|
+
}).filter((abs) => existsSync9(abs));
|
|
934
1353
|
} catch {
|
|
935
1354
|
console.error(`[ctxo] git ls-files failed for ${root}`);
|
|
936
1355
|
return [];
|
|
937
1356
|
}
|
|
938
1357
|
}
|
|
939
1358
|
discoverWorkspaces(ignoreProject = () => false) {
|
|
940
|
-
const pkgPath =
|
|
941
|
-
if (!
|
|
1359
|
+
const pkgPath = join8(this.projectRoot, "package.json");
|
|
1360
|
+
if (!existsSync9(pkgPath)) return [this.projectRoot];
|
|
942
1361
|
try {
|
|
943
|
-
const pkg = JSON.parse(
|
|
1362
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
944
1363
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
945
1364
|
if (!workspaces || workspaces.length === 0) return [this.projectRoot];
|
|
946
1365
|
const resolved = [];
|
|
947
1366
|
for (const ws of workspaces) {
|
|
948
1367
|
if (ws.endsWith("/*") || ws.endsWith("\\*")) {
|
|
949
|
-
const parentDir =
|
|
950
|
-
if (
|
|
1368
|
+
const parentDir = join8(this.projectRoot, ws.slice(0, -2));
|
|
1369
|
+
if (existsSync9(parentDir)) {
|
|
951
1370
|
for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
|
|
952
1371
|
if (entry.isDirectory()) {
|
|
953
|
-
resolved.push(
|
|
1372
|
+
resolved.push(join8(parentDir, entry.name));
|
|
954
1373
|
}
|
|
955
1374
|
}
|
|
956
1375
|
}
|
|
957
1376
|
} else {
|
|
958
|
-
const wsPath =
|
|
959
|
-
if (
|
|
1377
|
+
const wsPath = join8(this.projectRoot, ws);
|
|
1378
|
+
if (existsSync9(wsPath)) {
|
|
960
1379
|
resolved.push(wsPath);
|
|
961
1380
|
}
|
|
962
1381
|
}
|
|
@@ -989,7 +1408,7 @@ var IndexCommand = class {
|
|
|
989
1408
|
if (_roslyn) await _roslyn.dispose();
|
|
990
1409
|
const hasher = new ContentHasher();
|
|
991
1410
|
const files = this.discoverFilesIn(this.projectRoot);
|
|
992
|
-
const reader = new (await import("./json-index-reader-
|
|
1411
|
+
const reader = new (await import("./json-index-reader-XOEE6VMD.js")).JsonIndexReader(this.ctxoRoot);
|
|
993
1412
|
const indices = reader.readAll();
|
|
994
1413
|
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
995
1414
|
let staleCount = 0;
|
|
@@ -1002,7 +1421,7 @@ var IndexCommand = class {
|
|
|
1002
1421
|
staleCount++;
|
|
1003
1422
|
continue;
|
|
1004
1423
|
}
|
|
1005
|
-
if (!
|
|
1424
|
+
if (!existsSync9(filePath)) {
|
|
1006
1425
|
console.error(`[ctxo] DELETED: ${relativePath}`);
|
|
1007
1426
|
staleCount++;
|
|
1008
1427
|
continue;
|
|
@@ -1010,11 +1429,15 @@ var IndexCommand = class {
|
|
|
1010
1429
|
const mtime = Math.floor(statSync(filePath).mtimeMs / 1e3);
|
|
1011
1430
|
if (mtime <= indexed.lastModified) continue;
|
|
1012
1431
|
if (indexed.contentHash) {
|
|
1013
|
-
const source =
|
|
1432
|
+
const source = readFileSync6(filePath, "utf-8");
|
|
1014
1433
|
const currentHash = hasher.hash(source);
|
|
1015
1434
|
if (currentHash === indexed.contentHash) continue;
|
|
1435
|
+
console.error(`[ctxo] STALE: ${relativePath} (content hash differs)`);
|
|
1436
|
+
} else {
|
|
1437
|
+
console.error(
|
|
1438
|
+
`[ctxo] STALE: ${relativePath} (index predates contentHash \u2014 run \`ctxo index\` once to enable mtime-tolerant checks)`
|
|
1439
|
+
);
|
|
1016
1440
|
}
|
|
1017
|
-
console.error(`[ctxo] STALE: ${relativePath}`);
|
|
1018
1441
|
staleCount++;
|
|
1019
1442
|
}
|
|
1020
1443
|
if (staleCount > 0) {
|
|
@@ -1024,29 +1447,166 @@ var IndexCommand = class {
|
|
|
1024
1447
|
console.error("[ctxo] Index is up to date");
|
|
1025
1448
|
}
|
|
1026
1449
|
ensureGitignore() {
|
|
1027
|
-
const gitignorePath =
|
|
1450
|
+
const gitignorePath = join8(this.projectRoot, ".gitignore");
|
|
1028
1451
|
const cachePattern = ".ctxo/.cache/";
|
|
1029
1452
|
const suffix = `
|
|
1030
1453
|
# Ctxo local cache (never committed)
|
|
1031
1454
|
${cachePattern}
|
|
1032
1455
|
`;
|
|
1033
|
-
const existing =
|
|
1456
|
+
const existing = existsSync9(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
1034
1457
|
if (existing.includes(cachePattern)) return;
|
|
1035
|
-
|
|
1458
|
+
writeFileSync2(gitignorePath, existing + suffix, "utf-8");
|
|
1036
1459
|
console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
|
|
1037
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Produce the FileIndex list used as input to community detection.
|
|
1463
|
+
*
|
|
1464
|
+
* Full index run: returns pendingIndices as-is — they already cover every file.
|
|
1465
|
+
* Incremental `--file` run: pendingIndices holds only the just-updated file(s). We load the
|
|
1466
|
+
* rest of the committed JSON index from disk and splice in the fresh in-memory indices so
|
|
1467
|
+
* Louvain sees the COMPLETE project graph. Without this, Louvain would run on a 1-file
|
|
1468
|
+
* subgraph and emit a meaningless modularity-0 snapshot.
|
|
1469
|
+
*/
|
|
1470
|
+
buildGraphInputIndices(pendingIndices) {
|
|
1471
|
+
const pendingByPath = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const { fileIndex } of pendingIndices) {
|
|
1473
|
+
pendingByPath.set(fileIndex.file, fileIndex);
|
|
1474
|
+
}
|
|
1475
|
+
const reader = new JsonIndexReader(this.ctxoRoot);
|
|
1476
|
+
const committed = reader.readAll();
|
|
1477
|
+
const merged = [];
|
|
1478
|
+
for (const idx of committed) {
|
|
1479
|
+
const override = pendingByPath.get(idx.file);
|
|
1480
|
+
if (override) {
|
|
1481
|
+
merged.push(override);
|
|
1482
|
+
pendingByPath.delete(idx.file);
|
|
1483
|
+
} else {
|
|
1484
|
+
merged.push(idx);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
for (const remaining of pendingByPath.values()) merged.push(remaining);
|
|
1488
|
+
return merged;
|
|
1489
|
+
}
|
|
1490
|
+
buildCommunitySnapshot(indices, tiers, gitAdapter) {
|
|
1491
|
+
const graph = new SymbolGraph();
|
|
1492
|
+
for (const fileIndex of indices) {
|
|
1493
|
+
for (const symbol of fileIndex.symbols) graph.addNode(symbol);
|
|
1494
|
+
}
|
|
1495
|
+
for (const fileIndex of indices) {
|
|
1496
|
+
for (const edge of fileIndex.edges) graph.addEdge(edge);
|
|
1497
|
+
}
|
|
1498
|
+
const pagerank = new PageRankCalculator().calculate(graph, { limit: graph.nodeCount });
|
|
1499
|
+
const pagerankMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const entry of pagerank.rankings) pagerankMap.set(entry.symbolId, entry.score);
|
|
1501
|
+
const detector = new CommunityDetector();
|
|
1502
|
+
const edgeQuality = pickEdgeQuality(tiers);
|
|
1503
|
+
const detectResult = detector.detect(graph, pagerankMap, edgeQuality);
|
|
1504
|
+
return {
|
|
1505
|
+
version: 1,
|
|
1506
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1507
|
+
commitSha: safeHeadSha(gitAdapter),
|
|
1508
|
+
modularity: detectResult.modularity,
|
|
1509
|
+
communities: detectResult.communities,
|
|
1510
|
+
godNodes: detectResult.godNodes,
|
|
1511
|
+
edgeQuality: detectResult.edgeQuality,
|
|
1512
|
+
crossClusterEdges: detectResult.crossClusterEdges
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Writes drift-events.json and boundary-violations.json next to
|
|
1517
|
+
* communities.json. Must be called AFTER storage.writeCommunities so the
|
|
1518
|
+
* prior snapshot has already been archived into communities.history/ and
|
|
1519
|
+
* can serve as history[0] for the detectors.
|
|
1520
|
+
*/
|
|
1521
|
+
writeArchitectureArtifacts(indices, snapshot) {
|
|
1522
|
+
try {
|
|
1523
|
+
const snapshotWriter = new CommunitySnapshotWriter(this.ctxoRoot, {
|
|
1524
|
+
allowProductionPath: true
|
|
1525
|
+
});
|
|
1526
|
+
const history = snapshotWriter.listHistory();
|
|
1527
|
+
const graph = new SymbolGraph();
|
|
1528
|
+
for (const fileIndex of indices) {
|
|
1529
|
+
for (const symbol of fileIndex.symbols) graph.addNode(symbol);
|
|
1530
|
+
}
|
|
1531
|
+
for (const fileIndex of indices) {
|
|
1532
|
+
for (const edge of fileIndex.edges) graph.addEdge(edge);
|
|
1533
|
+
}
|
|
1534
|
+
const driftResult = new DriftDetector().detect(snapshot, history);
|
|
1535
|
+
const violationsResult = new BoundaryViolationDetector().detect(
|
|
1536
|
+
graph,
|
|
1537
|
+
snapshot,
|
|
1538
|
+
history
|
|
1539
|
+
);
|
|
1540
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1541
|
+
const driftArtifact = {
|
|
1542
|
+
version: 1,
|
|
1543
|
+
computedAt,
|
|
1544
|
+
commitSha: snapshot.commitSha,
|
|
1545
|
+
confidence: driftResult.confidence,
|
|
1546
|
+
snapshotsAvailable: driftResult.snapshotsAvailable,
|
|
1547
|
+
...driftResult.hint ? { hint: driftResult.hint } : {},
|
|
1548
|
+
events: driftResult.events
|
|
1549
|
+
};
|
|
1550
|
+
const violationsArtifact = {
|
|
1551
|
+
version: 1,
|
|
1552
|
+
computedAt,
|
|
1553
|
+
commitSha: snapshot.commitSha,
|
|
1554
|
+
confidence: violationsResult.confidence,
|
|
1555
|
+
snapshotsAvailable: violationsResult.snapshotsAvailable,
|
|
1556
|
+
...violationsResult.hint ? { hint: violationsResult.hint } : {},
|
|
1557
|
+
violations: violationsResult.violations
|
|
1558
|
+
};
|
|
1559
|
+
const writer = new ArchitectureArtifactsWriter(this.ctxoRoot, {
|
|
1560
|
+
allowProductionPath: true
|
|
1561
|
+
});
|
|
1562
|
+
writer.writeDriftEvents(driftArtifact);
|
|
1563
|
+
writer.writeBoundaryViolations(violationsArtifact);
|
|
1564
|
+
console.error(
|
|
1565
|
+
`[ctxo] Architecture artifacts: ${driftResult.events.length} drift events, ${violationsResult.violations.length} boundary violations (confidence: drift=${driftResult.confidence}, violations=${violationsResult.confidence})`
|
|
1566
|
+
);
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
console.error(
|
|
1569
|
+
`[ctxo] Architecture artifacts skipped: ${err.message}`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1038
1573
|
};
|
|
1574
|
+
function pickEdgeQuality(tiers) {
|
|
1575
|
+
const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax";
|
|
1576
|
+
const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full";
|
|
1577
|
+
if (hasSyntaxOnly && hasFull) return "mixed";
|
|
1578
|
+
if (hasSyntaxOnly) return "syntax-only";
|
|
1579
|
+
return "full";
|
|
1580
|
+
}
|
|
1581
|
+
function safeHeadSha(gitAdapter) {
|
|
1582
|
+
try {
|
|
1583
|
+
const maybeSha = gitAdapter.getHeadSha?.();
|
|
1584
|
+
if (maybeSha) return maybeSha.slice(0, 8);
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const out = execFileSync2("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf-8" });
|
|
1589
|
+
return out.trim();
|
|
1590
|
+
} catch {
|
|
1591
|
+
return "nocommit";
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function countCommunities(snapshot) {
|
|
1595
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1596
|
+
for (const entry of snapshot.communities) ids.add(entry.communityId);
|
|
1597
|
+
return ids.size;
|
|
1598
|
+
}
|
|
1039
1599
|
|
|
1040
1600
|
// src/cli/sync-command.ts
|
|
1041
|
-
import { join as
|
|
1601
|
+
import { join as join9 } from "path";
|
|
1042
1602
|
var SyncCommand = class {
|
|
1043
1603
|
ctxoRoot;
|
|
1044
1604
|
constructor(projectRoot) {
|
|
1045
|
-
this.ctxoRoot =
|
|
1605
|
+
this.ctxoRoot = join9(projectRoot, ".ctxo");
|
|
1046
1606
|
}
|
|
1047
1607
|
async run() {
|
|
1048
1608
|
console.error("[ctxo] Rebuilding SQLite cache from committed JSON index...");
|
|
1049
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1609
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
1050
1610
|
try {
|
|
1051
1611
|
await storage.init();
|
|
1052
1612
|
} finally {
|
|
@@ -1057,19 +1617,19 @@ var SyncCommand = class {
|
|
|
1057
1617
|
};
|
|
1058
1618
|
|
|
1059
1619
|
// src/cli/status-command.ts
|
|
1060
|
-
import { join as
|
|
1061
|
-
import { existsSync as
|
|
1620
|
+
import { join as join10 } from "path";
|
|
1621
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1062
1622
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1063
1623
|
var StatusCommand = class {
|
|
1064
1624
|
projectRoot;
|
|
1065
1625
|
ctxoRoot;
|
|
1066
1626
|
constructor(projectRoot) {
|
|
1067
1627
|
this.projectRoot = projectRoot;
|
|
1068
|
-
this.ctxoRoot =
|
|
1628
|
+
this.ctxoRoot = join10(projectRoot, ".ctxo");
|
|
1069
1629
|
}
|
|
1070
1630
|
run() {
|
|
1071
|
-
const indexDir =
|
|
1072
|
-
if (!
|
|
1631
|
+
const indexDir = join10(this.ctxoRoot, "index");
|
|
1632
|
+
if (!existsSync10(indexDir)) {
|
|
1073
1633
|
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
1074
1634
|
return;
|
|
1075
1635
|
}
|
|
@@ -1084,7 +1644,7 @@ var StatusCommand = class {
|
|
|
1084
1644
|
console.error(` Indexed files: ${indices.length}`);
|
|
1085
1645
|
console.error(` Total symbols: ${totalSymbols}`);
|
|
1086
1646
|
console.error(` Total edges: ${totalEdges}`);
|
|
1087
|
-
const cacheExists =
|
|
1647
|
+
const cacheExists = existsSync10(join10(this.ctxoRoot, ".cache", "symbols.db"));
|
|
1088
1648
|
console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
|
|
1089
1649
|
if (indices.length > 0) {
|
|
1090
1650
|
console.error("");
|
|
@@ -1113,8 +1673,8 @@ var StatusCommand = class {
|
|
|
1113
1673
|
|
|
1114
1674
|
// src/cli/verify-command.ts
|
|
1115
1675
|
import { mkdtempSync, rmSync } from "fs";
|
|
1116
|
-
import { join as
|
|
1117
|
-
import { tmpdir } from "os";
|
|
1676
|
+
import { join as join11 } from "path";
|
|
1677
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1118
1678
|
var VerifyCommand = class {
|
|
1119
1679
|
projectRoot;
|
|
1120
1680
|
constructor(projectRoot) {
|
|
@@ -1122,12 +1682,12 @@ var VerifyCommand = class {
|
|
|
1122
1682
|
}
|
|
1123
1683
|
async run() {
|
|
1124
1684
|
console.error("[ctxo] Verifying index freshness...");
|
|
1125
|
-
const tempDir = mkdtempSync(
|
|
1685
|
+
const tempDir = mkdtempSync(join11(tmpdir2(), "ctxo-verify-"));
|
|
1126
1686
|
try {
|
|
1127
|
-
const tempCtxo =
|
|
1687
|
+
const tempCtxo = join11(tempDir, ".ctxo");
|
|
1128
1688
|
const indexCmd = new IndexCommand(this.projectRoot, tempCtxo);
|
|
1129
1689
|
await indexCmd.run({ skipSideEffects: true });
|
|
1130
|
-
const committedReader = new JsonIndexReader(
|
|
1690
|
+
const committedReader = new JsonIndexReader(join11(this.projectRoot, ".ctxo"));
|
|
1131
1691
|
const freshReader = new JsonIndexReader(tempCtxo);
|
|
1132
1692
|
const committedIndices = committedReader.readAll();
|
|
1133
1693
|
const freshIndices = freshReader.readAll();
|
|
@@ -1160,13 +1720,13 @@ var VerifyCommand = class {
|
|
|
1160
1720
|
};
|
|
1161
1721
|
|
|
1162
1722
|
// src/cli/init-command.ts
|
|
1163
|
-
import { join as
|
|
1164
|
-
import { existsSync as
|
|
1165
|
-
import { createRequire as
|
|
1723
|
+
import { join as join13 } from "path";
|
|
1724
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
|
|
1725
|
+
import { createRequire as createRequire4 } from "module";
|
|
1166
1726
|
|
|
1167
1727
|
// src/cli/ai-rules.ts
|
|
1168
|
-
import { existsSync as
|
|
1169
|
-
import { join as
|
|
1728
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1729
|
+
import { join as join12, dirname as dirname6 } from "path";
|
|
1170
1730
|
var PLATFORMS = [
|
|
1171
1731
|
{ id: "claude-code", name: "Claude Code", file: "CLAUDE.md", mode: "append", detectPaths: ["CLAUDE.md", ".claude"], starred: true },
|
|
1172
1732
|
{ id: "cursor", name: "Cursor", file: ".cursor/rules/ctxo.mdc", mode: "create", detectPaths: [".cursor", ".cursorrules"], starred: false },
|
|
@@ -1179,7 +1739,7 @@ var PLATFORMS = [
|
|
|
1179
1739
|
function detectPlatforms(projectRoot) {
|
|
1180
1740
|
const detected = /* @__PURE__ */ new Set();
|
|
1181
1741
|
for (const p of PLATFORMS) {
|
|
1182
|
-
if (p.detectPaths.some((d) =>
|
|
1742
|
+
if (p.detectPaths.some((d) => existsSync11(join12(projectRoot, d)))) {
|
|
1183
1743
|
detected.add(p.id);
|
|
1184
1744
|
}
|
|
1185
1745
|
}
|
|
@@ -1241,31 +1801,31 @@ function generateRules(platformId) {
|
|
|
1241
1801
|
function installRules(projectRoot, platformId) {
|
|
1242
1802
|
const platform = PLATFORMS.find((p) => p.id === platformId);
|
|
1243
1803
|
if (!platform) throw new Error(`Unknown platform: ${platformId}`);
|
|
1244
|
-
const filePath =
|
|
1804
|
+
const filePath = join12(projectRoot, platform.file);
|
|
1245
1805
|
const content = generateRules(platformId);
|
|
1246
1806
|
const dir = dirname6(filePath);
|
|
1247
|
-
if (!
|
|
1807
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1248
1808
|
if (platform.mode === "create") {
|
|
1249
|
-
|
|
1250
|
-
return { file: platform.file, action:
|
|
1809
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1810
|
+
return { file: platform.file, action: existsSync11(filePath) ? "updated" : "created" };
|
|
1251
1811
|
}
|
|
1252
|
-
if (!
|
|
1253
|
-
|
|
1812
|
+
if (!existsSync11(filePath)) {
|
|
1813
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1254
1814
|
return { file: platform.file, action: "created" };
|
|
1255
1815
|
}
|
|
1256
|
-
const existing =
|
|
1816
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
1257
1817
|
if (existing.includes(SECTION_START)) {
|
|
1258
1818
|
const re = new RegExp(`${escapeRegExp(SECTION_START)}[\\s\\S]*?${escapeRegExp(SECTION_END)}`, "m");
|
|
1259
1819
|
const updated2 = existing.replace(re, `${SECTION_START}
|
|
1260
1820
|
${content}${SECTION_END}`);
|
|
1261
|
-
|
|
1821
|
+
writeFileSync3(filePath, updated2, "utf-8");
|
|
1262
1822
|
return { file: platform.file, action: "updated" };
|
|
1263
1823
|
}
|
|
1264
1824
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1265
1825
|
const updated = existing + separator + `${SECTION_START}
|
|
1266
1826
|
${content}${SECTION_END}
|
|
1267
1827
|
`;
|
|
1268
|
-
|
|
1828
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
1269
1829
|
return { file: platform.file, action: "updated" };
|
|
1270
1830
|
}
|
|
1271
1831
|
function escapeRegExp(s) {
|
|
@@ -1273,14 +1833,14 @@ function escapeRegExp(s) {
|
|
|
1273
1833
|
}
|
|
1274
1834
|
var GITIGNORE_ENTRY = ".ctxo/.cache/";
|
|
1275
1835
|
function ensureGitignore(projectRoot) {
|
|
1276
|
-
const filePath =
|
|
1277
|
-
if (!
|
|
1278
|
-
|
|
1836
|
+
const filePath = join12(projectRoot, ".gitignore");
|
|
1837
|
+
if (!existsSync11(filePath)) {
|
|
1838
|
+
writeFileSync3(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
|
|
1279
1839
|
${GITIGNORE_ENTRY}
|
|
1280
1840
|
`, "utf-8");
|
|
1281
1841
|
return { file: ".gitignore", action: "created" };
|
|
1282
1842
|
}
|
|
1283
|
-
const existing =
|
|
1843
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
1284
1844
|
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
1285
1845
|
return { file: ".gitignore", action: "skipped" };
|
|
1286
1846
|
}
|
|
@@ -1288,7 +1848,7 @@ ${GITIGNORE_ENTRY}
|
|
|
1288
1848
|
const updated = existing + `${separator}# Ctxo local cache (auto-rebuilt, never committed)
|
|
1289
1849
|
${GITIGNORE_ENTRY}
|
|
1290
1850
|
`;
|
|
1291
|
-
|
|
1851
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
1292
1852
|
return { file: ".gitignore", action: "updated" };
|
|
1293
1853
|
}
|
|
1294
1854
|
var DEFAULT_CONFIG = `# ctxo project configuration
|
|
@@ -1311,13 +1871,13 @@ index:
|
|
|
1311
1871
|
# - "examples/*"
|
|
1312
1872
|
`;
|
|
1313
1873
|
function ensureConfig(projectRoot) {
|
|
1314
|
-
const filePath =
|
|
1874
|
+
const filePath = join12(projectRoot, ".ctxo", "config.yaml");
|
|
1315
1875
|
const dir = dirname6(filePath);
|
|
1316
|
-
if (!
|
|
1317
|
-
if (
|
|
1876
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1877
|
+
if (existsSync11(filePath)) {
|
|
1318
1878
|
return { file: ".ctxo/config.yaml", action: "skipped" };
|
|
1319
1879
|
}
|
|
1320
|
-
|
|
1880
|
+
writeFileSync3(filePath, DEFAULT_CONFIG, "utf-8");
|
|
1321
1881
|
return { file: ".ctxo/config.yaml", action: "created" };
|
|
1322
1882
|
}
|
|
1323
1883
|
var CTXO_MCP_ENTRY = { command: "npx", args: ["-y", "@ctxo/cli"] };
|
|
@@ -1343,21 +1903,21 @@ function getMcpConfigTargets(selectedPlatformIds) {
|
|
|
1343
1903
|
return [...targets.values()];
|
|
1344
1904
|
}
|
|
1345
1905
|
function ensureMcpConfig(projectRoot, target) {
|
|
1346
|
-
const filePath =
|
|
1906
|
+
const filePath = join12(projectRoot, target.file);
|
|
1347
1907
|
const dir = dirname6(filePath);
|
|
1348
|
-
if (!
|
|
1908
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1349
1909
|
const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
|
|
1350
|
-
if (!
|
|
1910
|
+
if (!existsSync11(filePath)) {
|
|
1351
1911
|
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1352
|
-
|
|
1912
|
+
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1353
1913
|
return { file: target.file, action: "created" };
|
|
1354
1914
|
}
|
|
1355
1915
|
let existing;
|
|
1356
1916
|
try {
|
|
1357
|
-
existing = JSON.parse(
|
|
1917
|
+
existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
1358
1918
|
} catch {
|
|
1359
1919
|
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1360
|
-
|
|
1920
|
+
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1361
1921
|
return { file: target.file, action: "updated" };
|
|
1362
1922
|
}
|
|
1363
1923
|
const servers = existing[target.serverKey] ?? {};
|
|
@@ -1366,12 +1926,12 @@ function ensureMcpConfig(projectRoot, target) {
|
|
|
1366
1926
|
}
|
|
1367
1927
|
servers["ctxo"] = entry;
|
|
1368
1928
|
existing[target.serverKey] = servers;
|
|
1369
|
-
|
|
1929
|
+
writeFileSync3(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1370
1930
|
return { file: target.file, action: "updated" };
|
|
1371
1931
|
}
|
|
1372
1932
|
|
|
1373
1933
|
// src/cli/init-command.ts
|
|
1374
|
-
var requireCjs2 =
|
|
1934
|
+
var requireCjs2 = createRequire4(import.meta.url);
|
|
1375
1935
|
function resolveInstalledPlugins(needed) {
|
|
1376
1936
|
const installed = [];
|
|
1377
1937
|
const missing = [];
|
|
@@ -1463,13 +2023,13 @@ var InitCommand = class {
|
|
|
1463
2023
|
this.projectRoot = projectRoot;
|
|
1464
2024
|
}
|
|
1465
2025
|
installHooks() {
|
|
1466
|
-
const hooksDir =
|
|
1467
|
-
|
|
2026
|
+
const hooksDir = join13(this.projectRoot, ".git", "hooks");
|
|
2027
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
1468
2028
|
this.installHook(hooksDir, "post-commit", POST_COMMIT_CONTENT);
|
|
1469
2029
|
this.installHook(hooksDir, "post-merge", POST_MERGE_CONTENT);
|
|
1470
2030
|
}
|
|
1471
2031
|
async run(options = {}) {
|
|
1472
|
-
if (!
|
|
2032
|
+
if (!existsSync12(join13(this.projectRoot, ".git"))) {
|
|
1473
2033
|
console.error('[ctxo] Not a git repository. Run "git init" first.');
|
|
1474
2034
|
process.exit(1);
|
|
1475
2035
|
}
|
|
@@ -1506,8 +2066,8 @@ var InitCommand = class {
|
|
|
1506
2066
|
clack.cancel("Setup cancelled.");
|
|
1507
2067
|
process.exit(0);
|
|
1508
2068
|
}
|
|
1509
|
-
const fullPath =
|
|
1510
|
-
if (!
|
|
2069
|
+
const fullPath = join13(this.projectRoot, indexDir);
|
|
2070
|
+
if (!existsSync12(fullPath)) mkdirSync5(fullPath, { recursive: true });
|
|
1511
2071
|
console.error("");
|
|
1512
2072
|
}
|
|
1513
2073
|
stepNum++;
|
|
@@ -1569,7 +2129,13 @@ var InitCommand = class {
|
|
|
1569
2129
|
let installGitHooks = false;
|
|
1570
2130
|
if (!options.rulesOnly) {
|
|
1571
2131
|
stepNum++;
|
|
1572
|
-
console.error(stepHeader(
|
|
2132
|
+
console.error(stepHeader(
|
|
2133
|
+
stepNum,
|
|
2134
|
+
totalSteps,
|
|
2135
|
+
`Git Hooks ${pc.yellow("recommended")}`,
|
|
2136
|
+
"Auto-updates index and community snapshots on every commit and pull.\n Without hooks, architectural drift & boundary violation signals stay empty\n until you re-run `ctxo index` manually (or start `ctxo watch`).",
|
|
2137
|
+
pc
|
|
2138
|
+
));
|
|
1573
2139
|
console.error("");
|
|
1574
2140
|
const hooks = await clack.confirm({
|
|
1575
2141
|
message: pc.cyan("Install hooks?"),
|
|
@@ -1580,6 +2146,14 @@ var InitCommand = class {
|
|
|
1580
2146
|
process.exit(0);
|
|
1581
2147
|
}
|
|
1582
2148
|
installGitHooks = hooks;
|
|
2149
|
+
if (!installGitHooks) {
|
|
2150
|
+
console.error("");
|
|
2151
|
+
console.error(pc.yellow(" \u26A0 Drift & boundary-violation signals depend on repeated snapshots."));
|
|
2152
|
+
console.error(pc.dim(" Options without hooks:"));
|
|
2153
|
+
console.error(pc.dim(" \u2022 run `ctxo watch` during development"));
|
|
2154
|
+
console.error(pc.dim(" \u2022 add `ctxo index --check` to CI"));
|
|
2155
|
+
console.error(pc.dim(" \u2022 run `ctxo init` again to install hooks later"));
|
|
2156
|
+
}
|
|
1583
2157
|
console.error("");
|
|
1584
2158
|
}
|
|
1585
2159
|
const s = clack.spinner();
|
|
@@ -1650,8 +2224,8 @@ var InitCommand = class {
|
|
|
1650
2224
|
const gitignoreResult = ensureGitignore(this.projectRoot);
|
|
1651
2225
|
const configResult = ensureConfig(this.projectRoot);
|
|
1652
2226
|
if (!options.rulesOnly) {
|
|
1653
|
-
const indexDir =
|
|
1654
|
-
if (!
|
|
2227
|
+
const indexDir = join13(this.projectRoot, ".ctxo", "index");
|
|
2228
|
+
if (!existsSync12(indexDir)) mkdirSync5(indexDir, { recursive: true });
|
|
1655
2229
|
console.error("[ctxo] \u2713 .ctxo/index/ ready");
|
|
1656
2230
|
}
|
|
1657
2231
|
if (configResult.action !== "skipped") {
|
|
@@ -1703,7 +2277,7 @@ var InitCommand = class {
|
|
|
1703
2277
|
for (const id of toolIds) {
|
|
1704
2278
|
const platform = PLATFORMS.find((p) => p.id === id);
|
|
1705
2279
|
if (platform) {
|
|
1706
|
-
const exists =
|
|
2280
|
+
const exists = existsSync12(join13(this.projectRoot, platform.file));
|
|
1707
2281
|
const action = platform.mode === "append" && exists ? "append section" : "create";
|
|
1708
2282
|
console.error(` ${platform.file.padEnd(42)} (${action})`);
|
|
1709
2283
|
}
|
|
@@ -1718,35 +2292,35 @@ var InitCommand = class {
|
|
|
1718
2292
|
/* ---------------------------------------------------------------- */
|
|
1719
2293
|
async getVersion() {
|
|
1720
2294
|
try {
|
|
1721
|
-
const pkgPath =
|
|
1722
|
-
if (
|
|
1723
|
-
return JSON.parse(
|
|
2295
|
+
const pkgPath = join13(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
|
|
2296
|
+
if (existsSync12(pkgPath)) {
|
|
2297
|
+
return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
|
|
1724
2298
|
}
|
|
1725
|
-
const { createRequire:
|
|
1726
|
-
const require4 =
|
|
2299
|
+
const { createRequire: createRequire8 } = await import("module");
|
|
2300
|
+
const require4 = createRequire8(import.meta.url);
|
|
1727
2301
|
return require4("../../package.json").version;
|
|
1728
2302
|
} catch {
|
|
1729
2303
|
return void 0;
|
|
1730
2304
|
}
|
|
1731
2305
|
}
|
|
1732
2306
|
installHook(hooksDir, hookName, hookContent) {
|
|
1733
|
-
const hookPath =
|
|
2307
|
+
const hookPath = join13(hooksDir, hookName);
|
|
1734
2308
|
let existing = "";
|
|
1735
|
-
if (
|
|
1736
|
-
existing =
|
|
2309
|
+
if (existsSync12(hookPath)) {
|
|
2310
|
+
existing = readFileSync8(hookPath, "utf-8");
|
|
1737
2311
|
if (existing.includes(CTXO_START)) return;
|
|
1738
2312
|
} else {
|
|
1739
2313
|
existing = "#!/bin/sh\n";
|
|
1740
2314
|
}
|
|
1741
2315
|
const updated = existing.endsWith("\n") ? existing + "\n" + hookContent + "\n" : existing + "\n\n" + hookContent + "\n";
|
|
1742
|
-
|
|
2316
|
+
writeFileSync4(hookPath, updated, "utf-8");
|
|
1743
2317
|
chmodSync(hookPath, 493);
|
|
1744
2318
|
}
|
|
1745
2319
|
};
|
|
1746
2320
|
|
|
1747
2321
|
// src/cli/watch-command.ts
|
|
1748
|
-
import { join as
|
|
1749
|
-
import { readFileSync as
|
|
2322
|
+
import { join as join14, extname as extname4, relative as relative2 } from "path";
|
|
2323
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1750
2324
|
|
|
1751
2325
|
// src/adapters/watcher/chokidar-watcher-adapter.ts
|
|
1752
2326
|
import { watch } from "chokidar";
|
|
@@ -1787,13 +2361,15 @@ var ChokidarWatcherAdapter = class {
|
|
|
1787
2361
|
};
|
|
1788
2362
|
|
|
1789
2363
|
// src/cli/watch-command.ts
|
|
2364
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1790
2365
|
var DEBOUNCE_MS = 300;
|
|
2366
|
+
var COMMUNITY_SNAPSHOT_DEBOUNCE_MS = 5e3;
|
|
1791
2367
|
var WatchCommand = class {
|
|
1792
2368
|
projectRoot;
|
|
1793
2369
|
ctxoRoot;
|
|
1794
2370
|
constructor(projectRoot) {
|
|
1795
2371
|
this.projectRoot = projectRoot;
|
|
1796
|
-
this.ctxoRoot =
|
|
2372
|
+
this.ctxoRoot = join14(projectRoot, ".ctxo");
|
|
1797
2373
|
}
|
|
1798
2374
|
async run() {
|
|
1799
2375
|
console.error("[ctxo] Starting file watcher...");
|
|
@@ -1824,13 +2400,15 @@ var WatchCommand = class {
|
|
|
1824
2400
|
}
|
|
1825
2401
|
const supportedExtensions = registry.getSupportedExtensions();
|
|
1826
2402
|
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
1827
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
2403
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
1828
2404
|
await storage.init();
|
|
1829
2405
|
const hasher = new ContentHasher();
|
|
1830
2406
|
const gitAdapter = new SimpleGitAdapter(this.projectRoot);
|
|
1831
2407
|
const revertDetector = new RevertDetector();
|
|
1832
2408
|
const watcher = new ChokidarWatcherAdapter(this.projectRoot);
|
|
1833
2409
|
const pendingFiles = /* @__PURE__ */ new Map();
|
|
2410
|
+
const snapshotThreshold = watchSnapshotMinFileChanges(loadConfig(this.ctxoRoot).config);
|
|
2411
|
+
const pathsChangedSinceSnapshot = /* @__PURE__ */ new Set();
|
|
1834
2412
|
const reindexFile = async (filePath) => {
|
|
1835
2413
|
const adapter = registry.getAdapter(filePath);
|
|
1836
2414
|
if (!adapter) return;
|
|
@@ -1847,7 +2425,7 @@ var WatchCommand = class {
|
|
|
1847
2425
|
kind: "commit"
|
|
1848
2426
|
}));
|
|
1849
2427
|
const antiPatterns2 = revertDetector.detect(commits2);
|
|
1850
|
-
const source2 =
|
|
2428
|
+
const source2 = readFileSync9(filePath, "utf-8");
|
|
1851
2429
|
const fileIndex2 = {
|
|
1852
2430
|
file: relativePath,
|
|
1853
2431
|
lastModified: Math.floor(Date.now() / 1e3),
|
|
@@ -1877,7 +2455,7 @@ var WatchCommand = class {
|
|
|
1877
2455
|
return;
|
|
1878
2456
|
}
|
|
1879
2457
|
}
|
|
1880
|
-
const source =
|
|
2458
|
+
const source = readFileSync9(filePath, "utf-8");
|
|
1881
2459
|
const lastModified = Math.floor(Date.now() / 1e3);
|
|
1882
2460
|
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
1883
2461
|
const edges = await adapter.extractEdges(relativePath, source);
|
|
@@ -1907,14 +2485,38 @@ var WatchCommand = class {
|
|
|
1907
2485
|
console.error(`[ctxo] Failed to re-index ${relativePath}: ${err.message}`);
|
|
1908
2486
|
}
|
|
1909
2487
|
};
|
|
2488
|
+
let communitySnapshotTimer;
|
|
2489
|
+
const scheduleCommunitySnapshot = () => {
|
|
2490
|
+
if (pathsChangedSinceSnapshot.size < snapshotThreshold) return;
|
|
2491
|
+
if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
|
|
2492
|
+
communitySnapshotTimer = setTimeout(() => {
|
|
2493
|
+
communitySnapshotTimer = void 0;
|
|
2494
|
+
if (pathsChangedSinceSnapshot.size < snapshotThreshold) return;
|
|
2495
|
+
const changedCount = pathsChangedSinceSnapshot.size;
|
|
2496
|
+
pathsChangedSinceSnapshot.clear();
|
|
2497
|
+
try {
|
|
2498
|
+
const snapshot = buildWatchCommunitySnapshot(storage);
|
|
2499
|
+
if (snapshot) {
|
|
2500
|
+
storage.writeCommunities(snapshot);
|
|
2501
|
+
console.error(
|
|
2502
|
+
`[ctxo] Community snapshot refreshed after ${changedCount} file change(s) (modularity ${snapshot.modularity.toFixed(3)})`
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
console.error(`[ctxo] Community snapshot skipped: ${err.message}`);
|
|
2507
|
+
}
|
|
2508
|
+
}, COMMUNITY_SNAPSHOT_DEBOUNCE_MS);
|
|
2509
|
+
};
|
|
1910
2510
|
const debouncedReindex = (filePath) => {
|
|
1911
2511
|
const existing = pendingFiles.get(filePath);
|
|
1912
2512
|
if (existing) clearTimeout(existing);
|
|
1913
2513
|
pendingFiles.set(
|
|
1914
2514
|
filePath,
|
|
1915
|
-
setTimeout(() => {
|
|
2515
|
+
setTimeout(async () => {
|
|
1916
2516
|
pendingFiles.delete(filePath);
|
|
1917
|
-
reindexFile(filePath);
|
|
2517
|
+
await reindexFile(filePath);
|
|
2518
|
+
pathsChangedSinceSnapshot.add(filePath);
|
|
2519
|
+
scheduleCommunitySnapshot();
|
|
1918
2520
|
}, DEBOUNCE_MS)
|
|
1919
2521
|
);
|
|
1920
2522
|
};
|
|
@@ -1936,6 +2538,7 @@ var WatchCommand = class {
|
|
|
1936
2538
|
for (const timeout of pendingFiles.values()) {
|
|
1937
2539
|
clearTimeout(timeout);
|
|
1938
2540
|
}
|
|
2541
|
+
if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
|
|
1939
2542
|
const roslynShutdown = roslynAdapter ? roslynAdapter.dispose() : Promise.resolve();
|
|
1940
2543
|
roslynShutdown.then(() => {
|
|
1941
2544
|
watcher.stop().then(() => {
|
|
@@ -1949,23 +2552,53 @@ var WatchCommand = class {
|
|
|
1949
2552
|
process.on("SIGTERM", cleanup);
|
|
1950
2553
|
}
|
|
1951
2554
|
};
|
|
2555
|
+
function buildWatchCommunitySnapshot(storage) {
|
|
2556
|
+
const symbols = storage.getAllSymbols();
|
|
2557
|
+
if (symbols.length === 0) return void 0;
|
|
2558
|
+
const edges = storage.getAllEdges();
|
|
2559
|
+
const graph = new SymbolGraph();
|
|
2560
|
+
for (const symbol of symbols) graph.addNode(symbol);
|
|
2561
|
+
for (const edge of edges) graph.addEdge(edge);
|
|
2562
|
+
const pagerank = new PageRankCalculator().calculate(graph, { limit: graph.nodeCount });
|
|
2563
|
+
const pagerankMap = /* @__PURE__ */ new Map();
|
|
2564
|
+
for (const entry of pagerank.rankings) pagerankMap.set(entry.symbolId, entry.score);
|
|
2565
|
+
const detector = new CommunityDetector();
|
|
2566
|
+
const detectResult = detector.detect(graph, pagerankMap, "full");
|
|
2567
|
+
return {
|
|
2568
|
+
version: 1,
|
|
2569
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2570
|
+
commitSha: readHeadSha(),
|
|
2571
|
+
modularity: detectResult.modularity,
|
|
2572
|
+
communities: detectResult.communities,
|
|
2573
|
+
godNodes: detectResult.godNodes,
|
|
2574
|
+
edgeQuality: detectResult.edgeQuality,
|
|
2575
|
+
crossClusterEdges: detectResult.crossClusterEdges
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function readHeadSha() {
|
|
2579
|
+
try {
|
|
2580
|
+
return execFileSync4("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf-8" }).trim();
|
|
2581
|
+
} catch {
|
|
2582
|
+
return "nocommit";
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
1952
2585
|
|
|
1953
2586
|
// src/cli/stats-command.ts
|
|
1954
|
-
import { join as
|
|
1955
|
-
import { existsSync as
|
|
2587
|
+
import { join as join15 } from "path";
|
|
2588
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
1956
2589
|
import initSqlJs from "sql.js";
|
|
1957
2590
|
var StatsCommand = class {
|
|
1958
2591
|
ctxoRoot;
|
|
1959
2592
|
constructor(projectRoot) {
|
|
1960
|
-
this.ctxoRoot =
|
|
2593
|
+
this.ctxoRoot = join15(projectRoot, ".ctxo");
|
|
1961
2594
|
}
|
|
1962
2595
|
async run(options) {
|
|
1963
2596
|
if (!this.isStatsEnabled()) {
|
|
1964
2597
|
console.error("[ctxo] Stats collection is disabled in .ctxo/config.yaml");
|
|
1965
2598
|
return;
|
|
1966
2599
|
}
|
|
1967
|
-
const dbPath =
|
|
1968
|
-
if (!
|
|
2600
|
+
const dbPath = join15(this.ctxoRoot, ".cache", "symbols.db");
|
|
2601
|
+
if (!existsSync13(dbPath)) {
|
|
1969
2602
|
if (options.json) {
|
|
1970
2603
|
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
1971
2604
|
} else {
|
|
@@ -1974,15 +2607,15 @@ var StatsCommand = class {
|
|
|
1974
2607
|
return;
|
|
1975
2608
|
}
|
|
1976
2609
|
const SQL = await initSqlJs();
|
|
1977
|
-
const buffer =
|
|
2610
|
+
const buffer = readFileSync10(dbPath);
|
|
1978
2611
|
const db = new SQL.Database(buffer);
|
|
1979
2612
|
const recorder = new SessionRecorderAdapter(db);
|
|
1980
2613
|
try {
|
|
1981
2614
|
if (options.clear) {
|
|
1982
2615
|
recorder.clear();
|
|
1983
2616
|
const data = db.export();
|
|
1984
|
-
const { writeFileSync:
|
|
1985
|
-
|
|
2617
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
2618
|
+
writeFileSync6(dbPath, Buffer.from(data));
|
|
1986
2619
|
console.error("[ctxo] Session data cleared.");
|
|
1987
2620
|
return;
|
|
1988
2621
|
}
|
|
@@ -2012,10 +2645,10 @@ var StatsCommand = class {
|
|
|
2012
2645
|
}
|
|
2013
2646
|
}
|
|
2014
2647
|
isStatsEnabled() {
|
|
2015
|
-
const configPath =
|
|
2016
|
-
if (!
|
|
2648
|
+
const configPath = join15(this.ctxoRoot, "config.yaml");
|
|
2649
|
+
if (!existsSync13(configPath)) return true;
|
|
2017
2650
|
try {
|
|
2018
|
-
const raw =
|
|
2651
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
2019
2652
|
const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
|
|
2020
2653
|
if (match) return match[1] !== "false";
|
|
2021
2654
|
return true;
|
|
@@ -2104,15 +2737,17 @@ var StatsCommand = class {
|
|
|
2104
2737
|
};
|
|
2105
2738
|
|
|
2106
2739
|
// src/cli/doctor-command.ts
|
|
2107
|
-
import { join as
|
|
2108
|
-
import { existsSync as
|
|
2740
|
+
import { join as join22 } from "path";
|
|
2741
|
+
import { existsSync as existsSync20 } from "fs";
|
|
2109
2742
|
|
|
2110
2743
|
// src/adapters/diagnostics/health-checker.ts
|
|
2111
|
-
var
|
|
2744
|
+
var log4 = createLogger("ctxo:doctor");
|
|
2112
2745
|
var HealthChecker = class {
|
|
2113
2746
|
checks;
|
|
2114
|
-
|
|
2747
|
+
quiet;
|
|
2748
|
+
constructor(checks, options = {}) {
|
|
2115
2749
|
this.checks = checks;
|
|
2750
|
+
this.quiet = options.quiet ?? false;
|
|
2116
2751
|
}
|
|
2117
2752
|
async runAll(ctx) {
|
|
2118
2753
|
const settled = await Promise.allSettled(
|
|
@@ -2122,11 +2757,13 @@ var HealthChecker = class {
|
|
|
2122
2757
|
const check = this.checks[i];
|
|
2123
2758
|
if (outcome.status === "fulfilled") {
|
|
2124
2759
|
const r = outcome.value;
|
|
2125
|
-
|
|
2760
|
+
if (!this.quiet) {
|
|
2761
|
+
log4.info(`${r.id}: ${r.status.toUpperCase()} (${r.message})`);
|
|
2762
|
+
}
|
|
2126
2763
|
return r;
|
|
2127
2764
|
}
|
|
2128
2765
|
const message = outcome.reason?.message ?? "Unknown error";
|
|
2129
|
-
|
|
2766
|
+
if (!this.quiet) log4.error(`${check.id}: FAIL (${message})`);
|
|
2130
2767
|
return {
|
|
2131
2768
|
id: check.id,
|
|
2132
2769
|
title: check.title,
|
|
@@ -2193,8 +2830,8 @@ var DoctorReporter = class {
|
|
|
2193
2830
|
};
|
|
2194
2831
|
|
|
2195
2832
|
// src/adapters/diagnostics/checks/runtime-check.ts
|
|
2196
|
-
import { createRequire as
|
|
2197
|
-
var require2 =
|
|
2833
|
+
import { createRequire as createRequire5 } from "module";
|
|
2834
|
+
var require2 = createRequire5(import.meta.url);
|
|
2198
2835
|
function pass(id, title, message) {
|
|
2199
2836
|
return { id, title, status: "pass", message };
|
|
2200
2837
|
}
|
|
@@ -2385,8 +3022,8 @@ function buildSummary(info) {
|
|
|
2385
3022
|
}
|
|
2386
3023
|
|
|
2387
3024
|
// src/adapters/diagnostics/checks/language-coverage-check.ts
|
|
2388
|
-
import { createRequire as
|
|
2389
|
-
var require3 =
|
|
3025
|
+
import { createRequire as createRequire6 } from "module";
|
|
3026
|
+
var require3 = createRequire6(import.meta.url);
|
|
2390
3027
|
var LanguageCoverageCheck = class {
|
|
2391
3028
|
id = "language_coverage";
|
|
2392
3029
|
title = "Language coverage";
|
|
@@ -2437,15 +3074,15 @@ function pluginResolves(packageName) {
|
|
|
2437
3074
|
}
|
|
2438
3075
|
|
|
2439
3076
|
// src/adapters/diagnostics/checks/git-check.ts
|
|
2440
|
-
import { execFileSync as
|
|
2441
|
-
import { existsSync as
|
|
2442
|
-
import { join as
|
|
3077
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3078
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3079
|
+
import { join as join16 } from "path";
|
|
2443
3080
|
var GitBinaryCheck = class {
|
|
2444
3081
|
id = "git_binary";
|
|
2445
3082
|
title = "Git available";
|
|
2446
3083
|
async run(_ctx) {
|
|
2447
3084
|
try {
|
|
2448
|
-
const version =
|
|
3085
|
+
const version = execFileSync5("git", ["--version"], { encoding: "utf-8" }).trim();
|
|
2449
3086
|
return { id: this.id, title: this.title, status: "pass", message: version, value: version };
|
|
2450
3087
|
} catch {
|
|
2451
3088
|
return { id: this.id, title: this.title, status: "fail", message: "git not found in PATH", fix: "Install git and ensure it is in your PATH" };
|
|
@@ -2456,7 +3093,7 @@ var GitRepoCheck = class {
|
|
|
2456
3093
|
id = "git_repo";
|
|
2457
3094
|
title = "Git repository";
|
|
2458
3095
|
async run(ctx) {
|
|
2459
|
-
if (
|
|
3096
|
+
if (existsSync14(join16(ctx.projectRoot, ".git"))) {
|
|
2460
3097
|
return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
|
|
2461
3098
|
}
|
|
2462
3099
|
return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
|
|
@@ -2464,9 +3101,9 @@ var GitRepoCheck = class {
|
|
|
2464
3101
|
};
|
|
2465
3102
|
|
|
2466
3103
|
// src/adapters/diagnostics/checks/index-check.ts
|
|
2467
|
-
import { existsSync as
|
|
2468
|
-
import { execFileSync as
|
|
2469
|
-
import { join as
|
|
3104
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3105
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3106
|
+
import { join as join17 } from "path";
|
|
2470
3107
|
function pass2(id, title, message, value) {
|
|
2471
3108
|
return { id, title, status: "pass", message, value };
|
|
2472
3109
|
}
|
|
@@ -2483,8 +3120,8 @@ var IndexDirectoryCheck = class {
|
|
|
2483
3120
|
id = "index_directory";
|
|
2484
3121
|
title = "Index directory";
|
|
2485
3122
|
async run(ctx) {
|
|
2486
|
-
const indexDir =
|
|
2487
|
-
if (!
|
|
3123
|
+
const indexDir = join17(ctx.ctxoRoot, "index");
|
|
3124
|
+
if (!existsSync15(indexDir)) {
|
|
2488
3125
|
return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
|
|
2489
3126
|
}
|
|
2490
3127
|
const indices = getIndices(ctx);
|
|
@@ -2564,7 +3201,7 @@ var OrphanedFilesCheck = class {
|
|
|
2564
3201
|
}
|
|
2565
3202
|
getSourceFiles(projectRoot) {
|
|
2566
3203
|
try {
|
|
2567
|
-
const output =
|
|
3204
|
+
const output = execFileSync6("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
2568
3205
|
cwd: projectRoot,
|
|
2569
3206
|
encoding: "utf-8"
|
|
2570
3207
|
});
|
|
@@ -2578,8 +3215,8 @@ var CoChangesCacheCheck = class {
|
|
|
2578
3215
|
id = "co_changes_cache";
|
|
2579
3216
|
title = "Co-changes cache";
|
|
2580
3217
|
async run(ctx) {
|
|
2581
|
-
const coChangesPath =
|
|
2582
|
-
if (
|
|
3218
|
+
const coChangesPath = join17(ctx.ctxoRoot, "index", "co-changes.json");
|
|
3219
|
+
if (existsSync15(coChangesPath)) {
|
|
2583
3220
|
return pass2(this.id, this.title, "present");
|
|
2584
3221
|
}
|
|
2585
3222
|
return warn2(this.id, this.title, "missing (co-change analysis disabled)", 'Run "ctxo index" with git history');
|
|
@@ -2603,20 +3240,20 @@ var SchemaVersionCheck = class {
|
|
|
2603
3240
|
};
|
|
2604
3241
|
|
|
2605
3242
|
// src/adapters/diagnostics/checks/storage-check.ts
|
|
2606
|
-
import { existsSync as
|
|
2607
|
-
import { join as
|
|
3243
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
3244
|
+
import { join as join18 } from "path";
|
|
2608
3245
|
var SqliteCacheCheck = class {
|
|
2609
3246
|
id = "sqlite_cache";
|
|
2610
3247
|
title = "SQLite cache";
|
|
2611
3248
|
async run(ctx) {
|
|
2612
|
-
const dbPath =
|
|
2613
|
-
if (!
|
|
3249
|
+
const dbPath = join18(ctx.ctxoRoot, ".cache", "symbols.db");
|
|
3250
|
+
if (!existsSync16(dbPath)) {
|
|
2614
3251
|
return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
|
|
2615
3252
|
}
|
|
2616
3253
|
try {
|
|
2617
3254
|
const { default: initSqlJs2 } = await import("sql.js");
|
|
2618
3255
|
const SQL = await initSqlJs2();
|
|
2619
|
-
const buffer =
|
|
3256
|
+
const buffer = readFileSync11(dbPath);
|
|
2620
3257
|
const db = new SQL.Database(new Uint8Array(buffer));
|
|
2621
3258
|
const result = db.exec("PRAGMA integrity_check");
|
|
2622
3259
|
db.close();
|
|
@@ -2632,18 +3269,18 @@ var SqliteCacheCheck = class {
|
|
|
2632
3269
|
};
|
|
2633
3270
|
|
|
2634
3271
|
// src/adapters/diagnostics/checks/config-check.ts
|
|
2635
|
-
import { existsSync as
|
|
2636
|
-
import { join as
|
|
3272
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
|
|
3273
|
+
import { join as join19 } from "path";
|
|
2637
3274
|
var ConfigFileCheck = class {
|
|
2638
3275
|
id = "config_file";
|
|
2639
3276
|
title = "Config file";
|
|
2640
3277
|
async run(ctx) {
|
|
2641
|
-
const configPath =
|
|
2642
|
-
if (!
|
|
3278
|
+
const configPath = join19(ctx.ctxoRoot, "config.yaml");
|
|
3279
|
+
if (!existsSync17(configPath)) {
|
|
2643
3280
|
return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
|
|
2644
3281
|
}
|
|
2645
3282
|
try {
|
|
2646
|
-
const content =
|
|
3283
|
+
const content = readFileSync12(configPath, "utf-8");
|
|
2647
3284
|
if (content.trim().length === 0) {
|
|
2648
3285
|
return { id: this.id, title: this.title, status: "warn", message: "config.yaml is empty (using defaults)" };
|
|
2649
3286
|
}
|
|
@@ -2677,8 +3314,8 @@ var ConfigFileCheck = class {
|
|
|
2677
3314
|
};
|
|
2678
3315
|
|
|
2679
3316
|
// src/adapters/diagnostics/checks/disk-check.ts
|
|
2680
|
-
import { existsSync as
|
|
2681
|
-
import { join as
|
|
3317
|
+
import { existsSync as existsSync18, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
3318
|
+
import { join as join20 } from "path";
|
|
2682
3319
|
var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
|
|
2683
3320
|
var DiskUsageCheck = class {
|
|
2684
3321
|
id = "disk_usage";
|
|
@@ -2688,7 +3325,7 @@ var DiskUsageCheck = class {
|
|
|
2688
3325
|
this.thresholds = thresholds;
|
|
2689
3326
|
}
|
|
2690
3327
|
async run(ctx) {
|
|
2691
|
-
if (!
|
|
3328
|
+
if (!existsSync18(ctx.ctxoRoot)) {
|
|
2692
3329
|
return { id: this.id, title: this.title, status: "pass", message: "no .ctxo/ directory", value: "0" };
|
|
2693
3330
|
}
|
|
2694
3331
|
const bytes = this.getDirSize(ctx.ctxoRoot);
|
|
@@ -2710,7 +3347,7 @@ var DiskUsageCheck = class {
|
|
|
2710
3347
|
if (entry.isFile()) {
|
|
2711
3348
|
try {
|
|
2712
3349
|
const parent = entry.parentPath ?? entry.path ?? dirPath;
|
|
2713
|
-
total += statSync2(
|
|
3350
|
+
total += statSync2(join20(parent, entry.name)).size;
|
|
2714
3351
|
} catch {
|
|
2715
3352
|
}
|
|
2716
3353
|
}
|
|
@@ -2722,10 +3359,10 @@ var DiskUsageCheck = class {
|
|
|
2722
3359
|
};
|
|
2723
3360
|
|
|
2724
3361
|
// src/cli/doctor-fix.ts
|
|
2725
|
-
import { execFileSync as
|
|
2726
|
-
import { appendFileSync, existsSync as
|
|
2727
|
-
import { createRequire as
|
|
2728
|
-
import { dirname as dirname7, join as
|
|
3362
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
3363
|
+
import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
3364
|
+
import { createRequire as createRequire7 } from "module";
|
|
3365
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
2729
3366
|
async function applyFixes(report, options) {
|
|
2730
3367
|
if (isCI(process.env) && !options.yes && !options.dryRun) {
|
|
2731
3368
|
return {
|
|
@@ -2860,8 +3497,8 @@ async function fixIndex(projectRoot, dryRun) {
|
|
|
2860
3497
|
}
|
|
2861
3498
|
async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
2862
3499
|
if (dryRun) return;
|
|
2863
|
-
const indexDir =
|
|
2864
|
-
if (!
|
|
3500
|
+
const indexDir = join21(ctxoRoot, "index");
|
|
3501
|
+
if (!existsSync19(indexDir)) return;
|
|
2865
3502
|
const sourceFiles = listGitFiles2(projectRoot);
|
|
2866
3503
|
if (sourceFiles.size === 0) return;
|
|
2867
3504
|
const reader = new JsonIndexReader(ctxoRoot);
|
|
@@ -2869,9 +3506,9 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
2869
3506
|
let removed = 0;
|
|
2870
3507
|
for (const idx of indices) {
|
|
2871
3508
|
if (sourceFiles.has(idx.file)) continue;
|
|
2872
|
-
const jsonPath =
|
|
3509
|
+
const jsonPath = join21(indexDir, `${idx.file}.json`);
|
|
2873
3510
|
try {
|
|
2874
|
-
if (
|
|
3511
|
+
if (existsSync19(jsonPath)) {
|
|
2875
3512
|
unlinkSync2(jsonPath);
|
|
2876
3513
|
removed += 1;
|
|
2877
3514
|
pruneEmptyParents(jsonPath, indexDir);
|
|
@@ -2894,7 +3531,7 @@ function pruneEmptyParents(filePath, stopAt) {
|
|
|
2894
3531
|
}
|
|
2895
3532
|
function listGitFiles2(projectRoot) {
|
|
2896
3533
|
try {
|
|
2897
|
-
const out =
|
|
3534
|
+
const out = execFileSync7(
|
|
2898
3535
|
"git",
|
|
2899
3536
|
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
2900
3537
|
{ cwd: projectRoot, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
@@ -2905,12 +3542,12 @@ function listGitFiles2(projectRoot) {
|
|
|
2905
3542
|
}
|
|
2906
3543
|
}
|
|
2907
3544
|
async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
|
|
2908
|
-
const cacheDir =
|
|
3545
|
+
const cacheDir = join21(ctxoRoot, ".cache");
|
|
2909
3546
|
if (dryRun) return;
|
|
2910
|
-
if (
|
|
3547
|
+
if (existsSync19(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
2911
3548
|
await new SyncCommand(projectRoot).run();
|
|
2912
3549
|
}
|
|
2913
|
-
var doctorRequire =
|
|
3550
|
+
var doctorRequire = createRequire7(import.meta.url);
|
|
2914
3551
|
function canResolvePlugin(lang) {
|
|
2915
3552
|
try {
|
|
2916
3553
|
doctorRequire.resolve(`${officialPluginFor(lang)}/package.json`);
|
|
@@ -2924,8 +3561,8 @@ function isCI(env) {
|
|
|
2924
3561
|
}
|
|
2925
3562
|
function writeLog(ctxoRoot, attempts, halted, reason) {
|
|
2926
3563
|
try {
|
|
2927
|
-
if (!
|
|
2928
|
-
const path =
|
|
3564
|
+
if (!existsSync19(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
|
|
3565
|
+
const path = join21(ctxoRoot, "doctor-fix.log");
|
|
2929
3566
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2930
3567
|
const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
|
|
2931
3568
|
for (const a of attempts) {
|
|
@@ -2958,10 +3595,10 @@ var DoctorCommand = class {
|
|
|
2958
3595
|
ctxoRoot;
|
|
2959
3596
|
constructor(projectRoot) {
|
|
2960
3597
|
this.projectRoot = projectRoot;
|
|
2961
|
-
this.ctxoRoot =
|
|
3598
|
+
this.ctxoRoot = join22(projectRoot, ".ctxo");
|
|
2962
3599
|
}
|
|
2963
3600
|
async run(options = {}) {
|
|
2964
|
-
const { getVersion: getVersion2 } = await import("./cli-router-
|
|
3601
|
+
const { getVersion: getVersion2 } = await import("./cli-router-HEZER4G7.js");
|
|
2965
3602
|
console.error(`ctxo v${getVersion2()} \u2014 health check
|
|
2966
3603
|
`);
|
|
2967
3604
|
const checks = [
|
|
@@ -2983,9 +3620,9 @@ var DoctorCommand = class {
|
|
|
2983
3620
|
new CoChangesCacheCheck(),
|
|
2984
3621
|
new SchemaVersionCheck()
|
|
2985
3622
|
];
|
|
2986
|
-
const indexDir =
|
|
2987
|
-
const indices =
|
|
2988
|
-
const checker = new HealthChecker(checks);
|
|
3623
|
+
const indexDir = join22(this.ctxoRoot, "index");
|
|
3624
|
+
const indices = existsSync20(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
|
|
3625
|
+
const checker = new HealthChecker(checks, { quiet: Boolean(options.quiet) });
|
|
2989
3626
|
const report = await checker.runAll({
|
|
2990
3627
|
projectRoot: this.projectRoot,
|
|
2991
3628
|
ctxoRoot: this.ctxoRoot,
|
|
@@ -3025,13 +3662,13 @@ var DoctorCommand = class {
|
|
|
3025
3662
|
};
|
|
3026
3663
|
|
|
3027
3664
|
// src/cli/visualize-command.ts
|
|
3028
|
-
import { readFileSync as
|
|
3029
|
-
import { join as
|
|
3665
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as existsSync21 } from "fs";
|
|
3666
|
+
import { join as join23, basename } from "path";
|
|
3030
3667
|
import { execFile } from "child_process";
|
|
3031
3668
|
var VisualizeCommand = class {
|
|
3032
3669
|
constructor(projectRoot) {
|
|
3033
3670
|
this.projectRoot = projectRoot;
|
|
3034
|
-
this.ctxoRoot =
|
|
3671
|
+
this.ctxoRoot = join23(projectRoot, ".ctxo");
|
|
3035
3672
|
}
|
|
3036
3673
|
projectRoot;
|
|
3037
3674
|
ctxoRoot;
|
|
@@ -3091,10 +3728,37 @@ var VisualizeCommand = class {
|
|
|
3091
3728
|
fileLayerMap.set(file, layer);
|
|
3092
3729
|
}
|
|
3093
3730
|
}
|
|
3731
|
+
const communityWriter = new CommunitySnapshotWriter(this.ctxoRoot, {
|
|
3732
|
+
allowProductionPath: true
|
|
3733
|
+
});
|
|
3734
|
+
const currentSnapshot = communityWriter.readCurrent();
|
|
3735
|
+
const communityByIdForSymbol = /* @__PURE__ */ new Map();
|
|
3736
|
+
if (currentSnapshot) {
|
|
3737
|
+
for (const entry of currentSnapshot.communities) {
|
|
3738
|
+
communityByIdForSymbol.set(entry.symbolId, {
|
|
3739
|
+
id: entry.communityId,
|
|
3740
|
+
label: entry.communityLabel
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
const architectureWriter = new ArchitectureArtifactsWriter(this.ctxoRoot, {
|
|
3745
|
+
allowProductionPath: true
|
|
3746
|
+
});
|
|
3747
|
+
const violationsArtifact = architectureWriter.readBoundaryViolations();
|
|
3748
|
+
const violationEdgeSeverity = /* @__PURE__ */ new Map();
|
|
3749
|
+
if (violationsArtifact) {
|
|
3750
|
+
for (const v of violationsArtifact.violations) {
|
|
3751
|
+
violationEdgeSeverity.set(
|
|
3752
|
+
`${v.from.symbolId}|${v.to.symbolId}`,
|
|
3753
|
+
v.severity
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3094
3757
|
const totalSymbols = graph.nodeCount;
|
|
3095
3758
|
let nodes = graph.allNodes().map((sym) => {
|
|
3096
3759
|
const file = sym.symbolId.split("::")[0] ?? "";
|
|
3097
3760
|
const dead = deadMap.get(sym.symbolId);
|
|
3761
|
+
const community = communityByIdForSymbol.get(sym.symbolId);
|
|
3098
3762
|
return {
|
|
3099
3763
|
id: sym.symbolId,
|
|
3100
3764
|
name: sym.name,
|
|
@@ -3108,7 +3772,9 @@ var VisualizeCommand = class {
|
|
|
3108
3772
|
cyclomatic: complexityMap.get(sym.symbolId),
|
|
3109
3773
|
hasAntiPattern: antiPatternFiles.has(file),
|
|
3110
3774
|
inDegree: graph.getReverseEdges(sym.symbolId).length,
|
|
3111
|
-
outDegree: graph.getForwardEdges(sym.symbolId).length
|
|
3775
|
+
outDegree: graph.getForwardEdges(sym.symbolId).length,
|
|
3776
|
+
communityId: community?.id ?? -1,
|
|
3777
|
+
communityLabel: community?.label ?? "unassigned"
|
|
3112
3778
|
};
|
|
3113
3779
|
});
|
|
3114
3780
|
if (options.maxNodes && nodes.length > options.maxNodes) {
|
|
@@ -3116,12 +3782,31 @@ var VisualizeCommand = class {
|
|
|
3116
3782
|
nodes = nodes.slice(0, options.maxNodes);
|
|
3117
3783
|
}
|
|
3118
3784
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
3119
|
-
const edges = graph.allEdges().filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to)).map((e) =>
|
|
3785
|
+
const edges = graph.allEdges().filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to)).map((e) => {
|
|
3786
|
+
const severity = violationEdgeSeverity.get(`${e.from}|${e.to}`);
|
|
3787
|
+
return {
|
|
3788
|
+
source: e.from,
|
|
3789
|
+
target: e.to,
|
|
3790
|
+
kind: e.kind,
|
|
3791
|
+
...severity ? { violationSeverity: severity } : {}
|
|
3792
|
+
};
|
|
3793
|
+
});
|
|
3120
3794
|
const filesInfo = indices.filter((fi) => nodeIds.has(fi.symbols[0]?.symbolId ?? "___") || fi.symbols.some((s) => nodeIds.has(s.symbolId))).map((fi) => ({
|
|
3121
3795
|
file: fi.file,
|
|
3122
3796
|
intent: fi.intent.map((i) => ({ hash: i.hash, message: i.message, date: i.date })),
|
|
3123
3797
|
antiPatterns: fi.antiPatterns.map((a) => ({ hash: a.hash, message: a.message, date: a.date }))
|
|
3124
3798
|
}));
|
|
3799
|
+
const legendCounts = /* @__PURE__ */ new Map();
|
|
3800
|
+
for (const node of nodes) {
|
|
3801
|
+
if (node.communityId < 0) continue;
|
|
3802
|
+
const entry = legendCounts.get(node.communityId);
|
|
3803
|
+
if (entry) {
|
|
3804
|
+
entry.count += 1;
|
|
3805
|
+
} else {
|
|
3806
|
+
legendCounts.set(node.communityId, { label: node.communityLabel, count: 1 });
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
const communities = [...legendCounts.entries()].map(([id, { label, count }]) => ({ id, label, memberCount: count })).sort((a, b) => b.memberCount - a.memberCount);
|
|
3125
3810
|
const payload = {
|
|
3126
3811
|
projectName: basename(this.projectRoot),
|
|
3127
3812
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
@@ -3130,13 +3815,16 @@ var VisualizeCommand = class {
|
|
|
3130
3815
|
nodes,
|
|
3131
3816
|
edges,
|
|
3132
3817
|
layers: overlayResult.layers,
|
|
3133
|
-
files: filesInfo
|
|
3818
|
+
files: filesInfo,
|
|
3819
|
+
communities,
|
|
3820
|
+
modularity: currentSnapshot?.modularity ?? 0,
|
|
3821
|
+
violationCount: violationsArtifact?.violations.length ?? 0
|
|
3134
3822
|
};
|
|
3135
3823
|
const templatePath = this.getTemplatePath();
|
|
3136
|
-
const template =
|
|
3824
|
+
const template = readFileSync13(templatePath, "utf-8");
|
|
3137
3825
|
const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
|
|
3138
|
-
const outputPath = options.output ??
|
|
3139
|
-
|
|
3826
|
+
const outputPath = options.output ?? join23(this.ctxoRoot, "visualize.html");
|
|
3827
|
+
writeFileSync5(outputPath, html, "utf-8");
|
|
3140
3828
|
console.error(`[ctxo] Wrote visualization to ${outputPath}`);
|
|
3141
3829
|
console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
|
|
3142
3830
|
if (!options.noBrowser) {
|
|
@@ -3146,9 +3834,9 @@ var VisualizeCommand = class {
|
|
|
3146
3834
|
getTemplatePath() {
|
|
3147
3835
|
let dir = import.meta.dirname;
|
|
3148
3836
|
for (let i = 0; i < 10; i++) {
|
|
3149
|
-
const candidate =
|
|
3150
|
-
if (
|
|
3151
|
-
const parent =
|
|
3837
|
+
const candidate = join23(dir, "pages", "visualize-template.html");
|
|
3838
|
+
if (existsSync21(candidate)) return candidate;
|
|
3839
|
+
const parent = join23(dir, "..");
|
|
3152
3840
|
if (parent === dir) break;
|
|
3153
3841
|
dir = parent;
|
|
3154
3842
|
}
|
|
@@ -3175,16 +3863,16 @@ var VisualizeCommand = class {
|
|
|
3175
3863
|
function getVersion() {
|
|
3176
3864
|
let dir = import.meta.dirname;
|
|
3177
3865
|
for (let i = 0; i < 10; i++) {
|
|
3178
|
-
const pkg =
|
|
3179
|
-
if (
|
|
3866
|
+
const pkg = join24(dir, "package.json");
|
|
3867
|
+
if (existsSync22(pkg)) {
|
|
3180
3868
|
try {
|
|
3181
|
-
const json = JSON.parse(
|
|
3869
|
+
const json = JSON.parse(readFileSync14(pkg, "utf-8"));
|
|
3182
3870
|
return json.version ?? "unknown";
|
|
3183
3871
|
} catch {
|
|
3184
3872
|
break;
|
|
3185
3873
|
}
|
|
3186
3874
|
}
|
|
3187
|
-
const parent =
|
|
3875
|
+
const parent = join24(dir, "..");
|
|
3188
3876
|
if (parent === dir) break;
|
|
3189
3877
|
dir = parent;
|
|
3190
3878
|
}
|
|
@@ -3193,7 +3881,7 @@ function getVersion() {
|
|
|
3193
3881
|
function locateGitRoot(startDir) {
|
|
3194
3882
|
let dir = startDir;
|
|
3195
3883
|
for (let i = 0; i < 12; i++) {
|
|
3196
|
-
if (
|
|
3884
|
+
if (existsSync22(join24(dir, ".git"))) return dir;
|
|
3197
3885
|
const parent = dirname8(dir);
|
|
3198
3886
|
if (parent === dir) break;
|
|
3199
3887
|
dir = parent;
|
|
@@ -3237,6 +3925,7 @@ var CliRouter = class {
|
|
|
3237
3925
|
}
|
|
3238
3926
|
const checkArg = args.includes("--check");
|
|
3239
3927
|
const skipHistory = args.includes("--skip-history");
|
|
3928
|
+
const skipCommunity = args.includes("--skip-community");
|
|
3240
3929
|
const installMissing = args.includes("--install-missing");
|
|
3241
3930
|
const maxHistoryIdx = args.indexOf("--max-history");
|
|
3242
3931
|
const maxHistoryArg = maxHistoryIdx !== -1 ? Number(args[maxHistoryIdx + 1]) : void 0;
|
|
@@ -3249,6 +3938,7 @@ var CliRouter = class {
|
|
|
3249
3938
|
file: fileArg,
|
|
3250
3939
|
check: checkArg,
|
|
3251
3940
|
skipHistory,
|
|
3941
|
+
skipCommunity,
|
|
3252
3942
|
maxHistory: maxHistoryArg,
|
|
3253
3943
|
installMissing
|
|
3254
3944
|
});
|
|
@@ -3372,6 +4062,7 @@ ctxo v${v} \u2014 MCP server for dependency-aware codebase context
|
|
|
3372
4062
|
Usage:
|
|
3373
4063
|
ctxo Start MCP server (stdio transport)
|
|
3374
4064
|
ctxo index Build full codebase index (--max-history N, default 20)
|
|
4065
|
+
ctxo index --skip-community Skip Louvain community detection for faster indexing
|
|
3375
4066
|
ctxo sync Rebuild SQLite cache from committed JSON index
|
|
3376
4067
|
ctxo watch Start file watcher for incremental re-indexing
|
|
3377
4068
|
ctxo verify-index CI gate: fail if index is stale
|
|
@@ -3403,4 +4094,4 @@ export {
|
|
|
3403
4094
|
getVersion,
|
|
3404
4095
|
locateGitRoot
|
|
3405
4096
|
};
|
|
3406
|
-
//# sourceMappingURL=cli-router-
|
|
4097
|
+
//# sourceMappingURL=cli-router-HEZER4G7.js.map
|