@ctxo/cli 0.7.2 → 0.8.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/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-GZA44VOV.js} +836 -184
- package/dist/cli-router-GZA44VOV.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 +5 -3
- 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,28 +12,32 @@ 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
|
|
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";
|
|
34
41
|
import { createRequire as createRequire2 } from "module";
|
|
35
42
|
|
|
36
43
|
// src/core/staleness/content-hasher.ts
|
|
@@ -603,15 +610,15 @@ function isLockedCI(env) {
|
|
|
603
610
|
return true;
|
|
604
611
|
}
|
|
605
612
|
function spawnInstall(command, args, cwd) {
|
|
606
|
-
return new Promise((
|
|
613
|
+
return new Promise((resolve4, reject) => {
|
|
607
614
|
const child = spawn(command, [...args], { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
608
|
-
child.on("exit", (code) =>
|
|
615
|
+
child.on("exit", (code) => resolve4(code ?? 0));
|
|
609
616
|
child.on("error", (err) => reject(err));
|
|
610
617
|
});
|
|
611
618
|
}
|
|
612
619
|
|
|
613
620
|
// src/adapters/storage/json-index-writer.ts
|
|
614
|
-
import {
|
|
621
|
+
import { mkdirSync, unlinkSync, existsSync as existsSync6 } from "fs";
|
|
615
622
|
import { dirname as dirname4, join as join5, resolve as resolve2, sep } from "path";
|
|
616
623
|
var JsonIndexWriter = class {
|
|
617
624
|
indexDir;
|
|
@@ -634,9 +641,7 @@ var JsonIndexWriter = class {
|
|
|
634
641
|
this.atomicWrite(targetPath, JSON.stringify(matrix, null, 2));
|
|
635
642
|
}
|
|
636
643
|
atomicWrite(targetPath, content) {
|
|
637
|
-
|
|
638
|
-
writeFileSync(tmpPath, content, "utf-8");
|
|
639
|
-
renameSync(tmpPath, targetPath);
|
|
644
|
+
atomicWrite(targetPath, content);
|
|
640
645
|
}
|
|
641
646
|
delete(relativePath) {
|
|
642
647
|
const targetPath = this.resolveIndexPath(relativePath);
|
|
@@ -668,7 +673,7 @@ var JsonIndexWriter = class {
|
|
|
668
673
|
};
|
|
669
674
|
|
|
670
675
|
// src/adapters/storage/schema-manager.ts
|
|
671
|
-
import { readFileSync as readFileSync4, writeFileSync
|
|
676
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
|
|
672
677
|
import { join as join6, dirname as dirname5 } from "path";
|
|
673
678
|
var CURRENT_SCHEMA_VERSION = "1.0.0";
|
|
674
679
|
var SchemaManager = class {
|
|
@@ -687,7 +692,7 @@ var SchemaManager = class {
|
|
|
687
692
|
}
|
|
688
693
|
writeVersion() {
|
|
689
694
|
mkdirSync2(dirname5(this.versionFilePath), { recursive: true });
|
|
690
|
-
|
|
695
|
+
writeFileSync(this.versionFilePath, CURRENT_SCHEMA_VERSION, "utf-8");
|
|
691
696
|
}
|
|
692
697
|
isCompatible() {
|
|
693
698
|
const stored = this.readStoredVersion();
|
|
@@ -696,6 +701,340 @@ var SchemaManager = class {
|
|
|
696
701
|
}
|
|
697
702
|
};
|
|
698
703
|
|
|
704
|
+
// src/adapters/storage/architecture-artifacts-writer.ts
|
|
705
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
706
|
+
import { join as join7, resolve as resolve3 } from "path";
|
|
707
|
+
import { tmpdir } from "os";
|
|
708
|
+
var log3 = createLogger("ctxo:architecture-artifacts");
|
|
709
|
+
var DRIFT_FILE_NAME = "drift-events.json";
|
|
710
|
+
var VIOLATIONS_FILE_NAME = "boundary-violations.json";
|
|
711
|
+
var ArchitectureArtifactsWriter = class {
|
|
712
|
+
indexDir;
|
|
713
|
+
constructor(ctxoRoot, options = {}) {
|
|
714
|
+
this.indexDir = join7(ctxoRoot, "index");
|
|
715
|
+
assertSafeWritePath(ctxoRoot, options.allowProductionPath ?? false);
|
|
716
|
+
}
|
|
717
|
+
writeDriftEvents(artifact) {
|
|
718
|
+
mkdirSync3(this.indexDir, { recursive: true });
|
|
719
|
+
atomicWrite(join7(this.indexDir, DRIFT_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
720
|
+
}
|
|
721
|
+
writeBoundaryViolations(artifact) {
|
|
722
|
+
mkdirSync3(this.indexDir, { recursive: true });
|
|
723
|
+
atomicWrite(join7(this.indexDir, VIOLATIONS_FILE_NAME), JSON.stringify(artifact, null, 2));
|
|
724
|
+
}
|
|
725
|
+
readDriftEvents() {
|
|
726
|
+
const path = join7(this.indexDir, DRIFT_FILE_NAME);
|
|
727
|
+
if (!existsSync8(path)) return void 0;
|
|
728
|
+
try {
|
|
729
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
730
|
+
} catch (err) {
|
|
731
|
+
log3.warn(`failed to parse ${DRIFT_FILE_NAME}: ${err.message}`);
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
readBoundaryViolations() {
|
|
736
|
+
const path = join7(this.indexDir, VIOLATIONS_FILE_NAME);
|
|
737
|
+
if (!existsSync8(path)) return void 0;
|
|
738
|
+
try {
|
|
739
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
740
|
+
} catch (err) {
|
|
741
|
+
log3.warn(`failed to parse ${VIOLATIONS_FILE_NAME}: ${err.message}`);
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function assertSafeWritePath(ctxoRoot, allowProductionPath) {
|
|
747
|
+
const resolvedRoot = resolve3(ctxoRoot);
|
|
748
|
+
const resolvedTmp = resolve3(tmpdir());
|
|
749
|
+
if (resolvedRoot.startsWith(resolvedTmp)) return;
|
|
750
|
+
if (allowProductionPath) return;
|
|
751
|
+
throw new Error(
|
|
752
|
+
`ArchitectureArtifactsWriter: refusing to write to non-tmp path ${resolvedRoot}. Pass { allowProductionPath: true } for production CLI writes, or target a tmpdir-based path in tests.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/core/overlay/community-detector.ts
|
|
757
|
+
import { UndirectedGraph } from "graphology";
|
|
758
|
+
import louvainModule from "graphology-communities-louvain";
|
|
759
|
+
|
|
760
|
+
// src/core/overlay/community-labeler.ts
|
|
761
|
+
var MIN_PREFIX_SEGMENTS = 1;
|
|
762
|
+
var PATH_SEPARATOR = "/";
|
|
763
|
+
function labelCommunities(membersByCommunity, pagerank) {
|
|
764
|
+
const labels = /* @__PURE__ */ new Map();
|
|
765
|
+
const used = /* @__PURE__ */ new Set();
|
|
766
|
+
for (const [communityId, members] of membersByCommunity) {
|
|
767
|
+
const label = pickLabel(members, pagerank, used);
|
|
768
|
+
labels.set(communityId, label);
|
|
769
|
+
used.add(label);
|
|
770
|
+
}
|
|
771
|
+
return labels;
|
|
772
|
+
}
|
|
773
|
+
function pickLabel(members, pagerank, used) {
|
|
774
|
+
const files = members.map(extractFilePath).filter((file) => Boolean(file));
|
|
775
|
+
const prefixLabel = longestCommonPathPrefix(files);
|
|
776
|
+
if (prefixLabel && !used.has(prefixLabel)) {
|
|
777
|
+
return prefixLabel;
|
|
778
|
+
}
|
|
779
|
+
const topSymbol = pickTopByPagerank(members, pagerank);
|
|
780
|
+
if (topSymbol) {
|
|
781
|
+
const name = extractSymbolName(topSymbol);
|
|
782
|
+
if (name) {
|
|
783
|
+
const candidate = `${name} area`;
|
|
784
|
+
if (!used.has(candidate)) return candidate;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (prefixLabel) {
|
|
788
|
+
return disambiguate(prefixLabel, used);
|
|
789
|
+
}
|
|
790
|
+
return disambiguate("cluster", used);
|
|
791
|
+
}
|
|
792
|
+
function extractFilePath(symbolId) {
|
|
793
|
+
const parts = symbolId.split("::");
|
|
794
|
+
return parts.length === 3 ? parts[0] : void 0;
|
|
795
|
+
}
|
|
796
|
+
function extractSymbolName(symbolId) {
|
|
797
|
+
const parts = symbolId.split("::");
|
|
798
|
+
return parts.length === 3 ? parts[1] : void 0;
|
|
799
|
+
}
|
|
800
|
+
function longestCommonPathPrefix(files) {
|
|
801
|
+
if (files.length === 0) return void 0;
|
|
802
|
+
const normalized = files.map((file) => file.replace(/\\/g, PATH_SEPARATOR));
|
|
803
|
+
const segments = normalized.map((file) => file.split(PATH_SEPARATOR));
|
|
804
|
+
const first = segments[0];
|
|
805
|
+
let matchLength = first.length;
|
|
806
|
+
for (let i = 1; i < segments.length; i++) {
|
|
807
|
+
const current = segments[i];
|
|
808
|
+
matchLength = Math.min(matchLength, current.length);
|
|
809
|
+
for (let j = 0; j < matchLength; j++) {
|
|
810
|
+
if (current[j] !== first[j]) {
|
|
811
|
+
matchLength = j;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (matchLength === 0) break;
|
|
816
|
+
}
|
|
817
|
+
const fullMatch = matchLength === first.length && files.length === 1;
|
|
818
|
+
const keep = fullMatch ? matchLength - 1 : matchLength;
|
|
819
|
+
if (keep < MIN_PREFIX_SEGMENTS) return void 0;
|
|
820
|
+
const prefix = first.slice(0, keep).join(PATH_SEPARATOR);
|
|
821
|
+
return prefix.length > 0 ? prefix : void 0;
|
|
822
|
+
}
|
|
823
|
+
function pickTopByPagerank(members, pagerank) {
|
|
824
|
+
let best;
|
|
825
|
+
let bestScore = -1;
|
|
826
|
+
for (const symbolId of members) {
|
|
827
|
+
const score = pagerank.get(symbolId) ?? 0;
|
|
828
|
+
if (score > bestScore || score === bestScore && best !== void 0 && symbolId < best) {
|
|
829
|
+
bestScore = score;
|
|
830
|
+
best = symbolId;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return best ?? members[0];
|
|
834
|
+
}
|
|
835
|
+
function disambiguate(base, used) {
|
|
836
|
+
if (!used.has(base)) return base;
|
|
837
|
+
let suffix = 2;
|
|
838
|
+
while (used.has(`${base} (${suffix})`)) suffix++;
|
|
839
|
+
return `${base} (${suffix})`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/core/overlay/community-detector.ts
|
|
843
|
+
var louvain = louvainModule.default ?? louvainModule;
|
|
844
|
+
var DEFAULT_RESOLUTION = 1;
|
|
845
|
+
var DEFAULT_GOD_NODE_MIN_BRIDGED = 3;
|
|
846
|
+
var DEFAULT_GOD_NODE_MAX_RESULTS = 25;
|
|
847
|
+
var CommunityDetector = class {
|
|
848
|
+
resolution;
|
|
849
|
+
godNodeMinBridged;
|
|
850
|
+
godNodeMaxResults;
|
|
851
|
+
constructor(options = {}) {
|
|
852
|
+
this.resolution = options.resolution ?? DEFAULT_RESOLUTION;
|
|
853
|
+
this.godNodeMinBridged = options.godNodeMinBridged ?? DEFAULT_GOD_NODE_MIN_BRIDGED;
|
|
854
|
+
this.godNodeMaxResults = options.godNodeMaxResults ?? DEFAULT_GOD_NODE_MAX_RESULTS;
|
|
855
|
+
}
|
|
856
|
+
detect(graph, pagerank, edgeQuality = "full") {
|
|
857
|
+
const nodes = graph.allNodes();
|
|
858
|
+
if (nodes.length === 0) {
|
|
859
|
+
return {
|
|
860
|
+
communities: [],
|
|
861
|
+
godNodes: [],
|
|
862
|
+
modularity: 0,
|
|
863
|
+
crossClusterEdges: 0,
|
|
864
|
+
edgeQuality
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
const sortedNodes = [...nodes].sort(
|
|
868
|
+
(a, b) => a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0
|
|
869
|
+
);
|
|
870
|
+
const gph = new UndirectedGraph();
|
|
871
|
+
for (const node of sortedNodes) {
|
|
872
|
+
gph.addNode(node.symbolId);
|
|
873
|
+
}
|
|
874
|
+
const weightByPair = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const edge of graph.allEdges()) {
|
|
876
|
+
if (edge.from === edge.to) continue;
|
|
877
|
+
if (!gph.hasNode(edge.from) || !gph.hasNode(edge.to)) continue;
|
|
878
|
+
const [a, b] = edge.from < edge.to ? [edge.from, edge.to] : [edge.to, edge.from];
|
|
879
|
+
const key = `${a}|${b}`;
|
|
880
|
+
weightByPair.set(key, (weightByPair.get(key) ?? 0) + 1);
|
|
881
|
+
}
|
|
882
|
+
for (const [key, weight] of weightByPair) {
|
|
883
|
+
const [from, to] = key.split("|");
|
|
884
|
+
gph.addEdge(from, to, { weight });
|
|
885
|
+
}
|
|
886
|
+
if (gph.size === 0) {
|
|
887
|
+
return {
|
|
888
|
+
communities: this.singletonCommunities(sortedNodes),
|
|
889
|
+
godNodes: [],
|
|
890
|
+
modularity: 0,
|
|
891
|
+
crossClusterEdges: 0,
|
|
892
|
+
edgeQuality
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const result = louvain.detailed(gph, {
|
|
896
|
+
resolution: this.resolution,
|
|
897
|
+
randomWalk: false,
|
|
898
|
+
fastLocalMoves: true,
|
|
899
|
+
getEdgeWeight: "weight"
|
|
900
|
+
});
|
|
901
|
+
const rawAssignments = result.communities;
|
|
902
|
+
const splitAssignments = this.splitDisconnectedCommunities(gph, rawAssignments);
|
|
903
|
+
const normalizedAssignments = this.normalizeCommunityIds(splitAssignments, sortedNodes);
|
|
904
|
+
const memberIndex = this.buildMemberIndex(normalizedAssignments);
|
|
905
|
+
const labels = labelCommunities(memberIndex, pagerank);
|
|
906
|
+
const communities = sortedNodes.map((node) => {
|
|
907
|
+
const communityId = normalizedAssignments.get(node.symbolId) ?? 0;
|
|
908
|
+
return {
|
|
909
|
+
symbolId: node.symbolId,
|
|
910
|
+
communityId,
|
|
911
|
+
communityLabel: labels.get(communityId) ?? `community-${communityId}`
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
const crossClusterEdges = this.countCrossClusterEdges(graph, normalizedAssignments);
|
|
915
|
+
const godNodes = this.detectGodNodes(graph, normalizedAssignments, pagerank);
|
|
916
|
+
return {
|
|
917
|
+
communities,
|
|
918
|
+
godNodes,
|
|
919
|
+
modularity: result.modularity,
|
|
920
|
+
crossClusterEdges,
|
|
921
|
+
edgeQuality
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
singletonCommunities(nodes) {
|
|
925
|
+
return nodes.map((node, i) => ({
|
|
926
|
+
symbolId: node.symbolId,
|
|
927
|
+
communityId: i,
|
|
928
|
+
communityLabel: `isolated-${i}`
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
splitDisconnectedCommunities(gph, assignments) {
|
|
932
|
+
const result = /* @__PURE__ */ new Map();
|
|
933
|
+
const membersByCommunity = /* @__PURE__ */ new Map();
|
|
934
|
+
for (const [node, communityId] of Object.entries(assignments)) {
|
|
935
|
+
const list = membersByCommunity.get(communityId) ?? [];
|
|
936
|
+
list.push(node);
|
|
937
|
+
membersByCommunity.set(communityId, list);
|
|
938
|
+
}
|
|
939
|
+
let nextCommunityId = 0;
|
|
940
|
+
const sortedCommunities = [...membersByCommunity.entries()].sort(([a], [b]) => a - b);
|
|
941
|
+
for (const [, members] of sortedCommunities) {
|
|
942
|
+
const visited = /* @__PURE__ */ new Set();
|
|
943
|
+
for (const start of [...members].sort()) {
|
|
944
|
+
if (visited.has(start)) continue;
|
|
945
|
+
const componentId = nextCommunityId++;
|
|
946
|
+
const queue = [start];
|
|
947
|
+
visited.add(start);
|
|
948
|
+
while (queue.length > 0) {
|
|
949
|
+
const node = queue.shift();
|
|
950
|
+
result.set(node, componentId);
|
|
951
|
+
for (const neighbor of gph.neighbors(node)) {
|
|
952
|
+
if (visited.has(neighbor)) continue;
|
|
953
|
+
if (!members.includes(neighbor)) continue;
|
|
954
|
+
visited.add(neighbor);
|
|
955
|
+
queue.push(neighbor);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return result;
|
|
961
|
+
}
|
|
962
|
+
normalizeCommunityIds(assignments, sortedNodes) {
|
|
963
|
+
const remap = /* @__PURE__ */ new Map();
|
|
964
|
+
const result = /* @__PURE__ */ new Map();
|
|
965
|
+
let nextId = 0;
|
|
966
|
+
for (const node of sortedNodes) {
|
|
967
|
+
const raw = assignments.get(node.symbolId);
|
|
968
|
+
if (raw === void 0) {
|
|
969
|
+
result.set(node.symbolId, nextId);
|
|
970
|
+
remap.set(nextId, nextId);
|
|
971
|
+
nextId++;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
let normalized = remap.get(raw);
|
|
975
|
+
if (normalized === void 0) {
|
|
976
|
+
normalized = nextId++;
|
|
977
|
+
remap.set(raw, normalized);
|
|
978
|
+
}
|
|
979
|
+
result.set(node.symbolId, normalized);
|
|
980
|
+
}
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
buildMemberIndex(assignments) {
|
|
984
|
+
const index = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const [symbolId, communityId] of assignments) {
|
|
986
|
+
const list = index.get(communityId) ?? [];
|
|
987
|
+
list.push(symbolId);
|
|
988
|
+
index.set(communityId, list);
|
|
989
|
+
}
|
|
990
|
+
for (const list of index.values()) list.sort();
|
|
991
|
+
return index;
|
|
992
|
+
}
|
|
993
|
+
countCrossClusterEdges(graph, assignments) {
|
|
994
|
+
let count = 0;
|
|
995
|
+
for (const edge of graph.allEdges()) {
|
|
996
|
+
const from = assignments.get(edge.from);
|
|
997
|
+
const to = assignments.get(edge.to);
|
|
998
|
+
if (from === void 0 || to === void 0) continue;
|
|
999
|
+
if (from !== to) count++;
|
|
1000
|
+
}
|
|
1001
|
+
return count;
|
|
1002
|
+
}
|
|
1003
|
+
detectGodNodes(graph, assignments, pagerank) {
|
|
1004
|
+
const candidates = [];
|
|
1005
|
+
for (const node of graph.allNodes()) {
|
|
1006
|
+
const ownCommunity = assignments.get(node.symbolId);
|
|
1007
|
+
if (ownCommunity === void 0) continue;
|
|
1008
|
+
const bridged = /* @__PURE__ */ new Set();
|
|
1009
|
+
const forward = graph.getForwardEdges(node.symbolId);
|
|
1010
|
+
const reverse = graph.getReverseEdges(node.symbolId);
|
|
1011
|
+
for (const edge of forward) {
|
|
1012
|
+
const other = assignments.get(edge.to);
|
|
1013
|
+
if (other !== void 0 && other !== ownCommunity) bridged.add(other);
|
|
1014
|
+
}
|
|
1015
|
+
for (const edge of reverse) {
|
|
1016
|
+
const other = assignments.get(edge.from);
|
|
1017
|
+
if (other !== void 0 && other !== ownCommunity) bridged.add(other);
|
|
1018
|
+
}
|
|
1019
|
+
if (bridged.size >= this.godNodeMinBridged) {
|
|
1020
|
+
const pr = pagerank.get(node.symbolId) ?? 0;
|
|
1021
|
+
candidates.push({
|
|
1022
|
+
symbolId: node.symbolId,
|
|
1023
|
+
bridgedCommunities: [...bridged].sort((a, b) => a - b),
|
|
1024
|
+
centralityScore: bridged.size * (pr > 0 ? pr : 1 / graph.nodeCount)
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
candidates.sort((a, b) => {
|
|
1029
|
+
if (b.centralityScore !== a.centralityScore) {
|
|
1030
|
+
return b.centralityScore - a.centralityScore;
|
|
1031
|
+
}
|
|
1032
|
+
return a.symbolId.localeCompare(b.symbolId);
|
|
1033
|
+
});
|
|
1034
|
+
return candidates.slice(0, this.godNodeMaxResults);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
699
1038
|
// src/cli/index-command.ts
|
|
700
1039
|
var requireCjs = createRequire2(import.meta.url);
|
|
701
1040
|
var IndexCommand = class {
|
|
@@ -704,7 +1043,7 @@ var IndexCommand = class {
|
|
|
704
1043
|
supportedExtensions;
|
|
705
1044
|
constructor(projectRoot, ctxoRoot) {
|
|
706
1045
|
this.projectRoot = projectRoot;
|
|
707
|
-
this.ctxoRoot = ctxoRoot ??
|
|
1046
|
+
this.ctxoRoot = ctxoRoot ?? join8(projectRoot, ".ctxo");
|
|
708
1047
|
this.supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".go", ".cs"]);
|
|
709
1048
|
}
|
|
710
1049
|
loadIgnoreConfig() {
|
|
@@ -742,7 +1081,7 @@ var IndexCommand = class {
|
|
|
742
1081
|
const revertDetector = new RevertDetector();
|
|
743
1082
|
let files;
|
|
744
1083
|
if (options.file) {
|
|
745
|
-
const fullPath =
|
|
1084
|
+
const fullPath = join8(this.projectRoot, options.file);
|
|
746
1085
|
files = [fullPath];
|
|
747
1086
|
console.error(`[ctxo] Incremental re-index: ${options.file}`);
|
|
748
1087
|
} else {
|
|
@@ -754,6 +1093,14 @@ var IndexCommand = class {
|
|
|
754
1093
|
}
|
|
755
1094
|
console.error(`[ctxo] Building codebase index... Found ${files.length} source files`);
|
|
756
1095
|
}
|
|
1096
|
+
try {
|
|
1097
|
+
const { sweepStaleTmpFiles } = await import("./atomic-write-AQNYJFAG.js");
|
|
1098
|
+
const swept = sweepStaleTmpFiles(join8(this.ctxoRoot, "index"));
|
|
1099
|
+
if (swept > 0) {
|
|
1100
|
+
console.error(`[ctxo] Cleaned ${swept} orphan .tmp file(s) from previous interrupted run`);
|
|
1101
|
+
}
|
|
1102
|
+
} catch {
|
|
1103
|
+
}
|
|
757
1104
|
if (roslynAdapter?.isReady()) {
|
|
758
1105
|
const hasCsFiles = files.some((f) => f.endsWith(".cs"));
|
|
759
1106
|
if (hasCsFiles) {
|
|
@@ -768,7 +1115,7 @@ var IndexCommand = class {
|
|
|
768
1115
|
if (!adapter) continue;
|
|
769
1116
|
const relativePath = relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
770
1117
|
try {
|
|
771
|
-
const source =
|
|
1118
|
+
const source = readFileSync6(filePath, "utf-8");
|
|
772
1119
|
const lastModified = Math.floor(Date.now() / 1e3);
|
|
773
1120
|
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
774
1121
|
const complexity = await adapter.extractComplexity(relativePath, source);
|
|
@@ -833,19 +1180,52 @@ var IndexCommand = class {
|
|
|
833
1180
|
writer.writeCoChanges(coChangeMatrix);
|
|
834
1181
|
console.error(`[ctxo] Co-change analysis: ${coChangeMatrix.entries.length} file pairs detected`);
|
|
835
1182
|
}
|
|
1183
|
+
let communitySnapshot;
|
|
1184
|
+
if (options.skipCommunity && pendingIndices.length > 0) {
|
|
1185
|
+
if (existsSync9(join8(this.ctxoRoot, "index", "communities.json"))) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`[ctxo] WARN stale communities.json present \u2014 --skip-community preserved existing snapshot. Run \`ctxo index\` (full) to refresh.`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
} else if (pendingIndices.length > 0) {
|
|
1191
|
+
try {
|
|
1192
|
+
const graphIndices = this.buildGraphInputIndices(pendingIndices);
|
|
1193
|
+
communitySnapshot = this.buildCommunitySnapshot(
|
|
1194
|
+
graphIndices,
|
|
1195
|
+
{
|
|
1196
|
+
csharpTier,
|
|
1197
|
+
goTier,
|
|
1198
|
+
tsCount: graphIndices.filter((idx) => /\.(ts|tsx|js|jsx)$/.test(idx.file)).length
|
|
1199
|
+
},
|
|
1200
|
+
gitAdapter
|
|
1201
|
+
);
|
|
1202
|
+
const scope = options.file ? " (incremental, full-graph recompute)" : "";
|
|
1203
|
+
console.error(
|
|
1204
|
+
`[ctxo] Community detection${scope}: ${countCommunities(communitySnapshot)} clusters (modularity ${communitySnapshot.modularity.toFixed(3)})`
|
|
1205
|
+
);
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
console.error(`[ctxo] Community detection skipped: ${err.message}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
836
1210
|
const indices = [];
|
|
837
1211
|
for (const { fileIndex } of pendingIndices) {
|
|
838
1212
|
writer.write(fileIndex);
|
|
839
1213
|
indices.push(fileIndex);
|
|
840
1214
|
}
|
|
841
1215
|
schemaManager.writeVersion();
|
|
842
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1216
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
843
1217
|
try {
|
|
844
1218
|
await storage.initEmpty();
|
|
845
1219
|
storage.bulkWrite(indices);
|
|
1220
|
+
if (communitySnapshot) {
|
|
1221
|
+
storage.writeCommunities(communitySnapshot);
|
|
1222
|
+
}
|
|
846
1223
|
} finally {
|
|
847
1224
|
storage.close();
|
|
848
1225
|
}
|
|
1226
|
+
if (communitySnapshot) {
|
|
1227
|
+
this.writeArchitectureArtifacts(indices, communitySnapshot);
|
|
1228
|
+
}
|
|
849
1229
|
if (!options.skipSideEffects) {
|
|
850
1230
|
this.ensureGitignore();
|
|
851
1231
|
}
|
|
@@ -927,36 +1307,36 @@ var IndexCommand = class {
|
|
|
927
1307
|
encoding: "utf-8",
|
|
928
1308
|
maxBuffer: 10 * 1024 * 1024
|
|
929
1309
|
});
|
|
930
|
-
return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => this.isSupportedExtension(line)).map((line) =>
|
|
1310
|
+
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
1311
|
const rel = relative(this.projectRoot, abs).replace(/\\/g, "/");
|
|
932
1312
|
return !ignoreFile(rel);
|
|
933
|
-
});
|
|
1313
|
+
}).filter((abs) => existsSync9(abs));
|
|
934
1314
|
} catch {
|
|
935
1315
|
console.error(`[ctxo] git ls-files failed for ${root}`);
|
|
936
1316
|
return [];
|
|
937
1317
|
}
|
|
938
1318
|
}
|
|
939
1319
|
discoverWorkspaces(ignoreProject = () => false) {
|
|
940
|
-
const pkgPath =
|
|
941
|
-
if (!
|
|
1320
|
+
const pkgPath = join8(this.projectRoot, "package.json");
|
|
1321
|
+
if (!existsSync9(pkgPath)) return [this.projectRoot];
|
|
942
1322
|
try {
|
|
943
|
-
const pkg = JSON.parse(
|
|
1323
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
944
1324
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
945
1325
|
if (!workspaces || workspaces.length === 0) return [this.projectRoot];
|
|
946
1326
|
const resolved = [];
|
|
947
1327
|
for (const ws of workspaces) {
|
|
948
1328
|
if (ws.endsWith("/*") || ws.endsWith("\\*")) {
|
|
949
|
-
const parentDir =
|
|
950
|
-
if (
|
|
1329
|
+
const parentDir = join8(this.projectRoot, ws.slice(0, -2));
|
|
1330
|
+
if (existsSync9(parentDir)) {
|
|
951
1331
|
for (const entry of readdirSync2(parentDir, { withFileTypes: true })) {
|
|
952
1332
|
if (entry.isDirectory()) {
|
|
953
|
-
resolved.push(
|
|
1333
|
+
resolved.push(join8(parentDir, entry.name));
|
|
954
1334
|
}
|
|
955
1335
|
}
|
|
956
1336
|
}
|
|
957
1337
|
} else {
|
|
958
|
-
const wsPath =
|
|
959
|
-
if (
|
|
1338
|
+
const wsPath = join8(this.projectRoot, ws);
|
|
1339
|
+
if (existsSync9(wsPath)) {
|
|
960
1340
|
resolved.push(wsPath);
|
|
961
1341
|
}
|
|
962
1342
|
}
|
|
@@ -989,7 +1369,7 @@ var IndexCommand = class {
|
|
|
989
1369
|
if (_roslyn) await _roslyn.dispose();
|
|
990
1370
|
const hasher = new ContentHasher();
|
|
991
1371
|
const files = this.discoverFilesIn(this.projectRoot);
|
|
992
|
-
const reader = new (await import("./json-index-reader-
|
|
1372
|
+
const reader = new (await import("./json-index-reader-XOEE6VMD.js")).JsonIndexReader(this.ctxoRoot);
|
|
993
1373
|
const indices = reader.readAll();
|
|
994
1374
|
const indexedMap = new Map(indices.map((i) => [i.file, i]));
|
|
995
1375
|
let staleCount = 0;
|
|
@@ -1002,7 +1382,7 @@ var IndexCommand = class {
|
|
|
1002
1382
|
staleCount++;
|
|
1003
1383
|
continue;
|
|
1004
1384
|
}
|
|
1005
|
-
if (!
|
|
1385
|
+
if (!existsSync9(filePath)) {
|
|
1006
1386
|
console.error(`[ctxo] DELETED: ${relativePath}`);
|
|
1007
1387
|
staleCount++;
|
|
1008
1388
|
continue;
|
|
@@ -1010,11 +1390,15 @@ var IndexCommand = class {
|
|
|
1010
1390
|
const mtime = Math.floor(statSync(filePath).mtimeMs / 1e3);
|
|
1011
1391
|
if (mtime <= indexed.lastModified) continue;
|
|
1012
1392
|
if (indexed.contentHash) {
|
|
1013
|
-
const source =
|
|
1393
|
+
const source = readFileSync6(filePath, "utf-8");
|
|
1014
1394
|
const currentHash = hasher.hash(source);
|
|
1015
1395
|
if (currentHash === indexed.contentHash) continue;
|
|
1396
|
+
console.error(`[ctxo] STALE: ${relativePath} (content hash differs)`);
|
|
1397
|
+
} else {
|
|
1398
|
+
console.error(
|
|
1399
|
+
`[ctxo] STALE: ${relativePath} (index predates contentHash \u2014 run \`ctxo index\` once to enable mtime-tolerant checks)`
|
|
1400
|
+
);
|
|
1016
1401
|
}
|
|
1017
|
-
console.error(`[ctxo] STALE: ${relativePath}`);
|
|
1018
1402
|
staleCount++;
|
|
1019
1403
|
}
|
|
1020
1404
|
if (staleCount > 0) {
|
|
@@ -1024,29 +1408,166 @@ var IndexCommand = class {
|
|
|
1024
1408
|
console.error("[ctxo] Index is up to date");
|
|
1025
1409
|
}
|
|
1026
1410
|
ensureGitignore() {
|
|
1027
|
-
const gitignorePath =
|
|
1411
|
+
const gitignorePath = join8(this.projectRoot, ".gitignore");
|
|
1028
1412
|
const cachePattern = ".ctxo/.cache/";
|
|
1029
1413
|
const suffix = `
|
|
1030
1414
|
# Ctxo local cache (never committed)
|
|
1031
1415
|
${cachePattern}
|
|
1032
1416
|
`;
|
|
1033
|
-
const existing =
|
|
1417
|
+
const existing = existsSync9(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
|
|
1034
1418
|
if (existing.includes(cachePattern)) return;
|
|
1035
|
-
|
|
1419
|
+
writeFileSync2(gitignorePath, existing + suffix, "utf-8");
|
|
1036
1420
|
console.error("[ctxo] Added .ctxo/.cache/ to .gitignore");
|
|
1037
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Produce the FileIndex list used as input to community detection.
|
|
1424
|
+
*
|
|
1425
|
+
* Full index run: returns pendingIndices as-is — they already cover every file.
|
|
1426
|
+
* Incremental `--file` run: pendingIndices holds only the just-updated file(s). We load the
|
|
1427
|
+
* rest of the committed JSON index from disk and splice in the fresh in-memory indices so
|
|
1428
|
+
* Louvain sees the COMPLETE project graph. Without this, Louvain would run on a 1-file
|
|
1429
|
+
* subgraph and emit a meaningless modularity-0 snapshot.
|
|
1430
|
+
*/
|
|
1431
|
+
buildGraphInputIndices(pendingIndices) {
|
|
1432
|
+
const pendingByPath = /* @__PURE__ */ new Map();
|
|
1433
|
+
for (const { fileIndex } of pendingIndices) {
|
|
1434
|
+
pendingByPath.set(fileIndex.file, fileIndex);
|
|
1435
|
+
}
|
|
1436
|
+
const reader = new JsonIndexReader(this.ctxoRoot);
|
|
1437
|
+
const committed = reader.readAll();
|
|
1438
|
+
const merged = [];
|
|
1439
|
+
for (const idx of committed) {
|
|
1440
|
+
const override = pendingByPath.get(idx.file);
|
|
1441
|
+
if (override) {
|
|
1442
|
+
merged.push(override);
|
|
1443
|
+
pendingByPath.delete(idx.file);
|
|
1444
|
+
} else {
|
|
1445
|
+
merged.push(idx);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
for (const remaining of pendingByPath.values()) merged.push(remaining);
|
|
1449
|
+
return merged;
|
|
1450
|
+
}
|
|
1451
|
+
buildCommunitySnapshot(indices, tiers, gitAdapter) {
|
|
1452
|
+
const graph = new SymbolGraph();
|
|
1453
|
+
for (const fileIndex of indices) {
|
|
1454
|
+
for (const symbol of fileIndex.symbols) graph.addNode(symbol);
|
|
1455
|
+
}
|
|
1456
|
+
for (const fileIndex of indices) {
|
|
1457
|
+
for (const edge of fileIndex.edges) graph.addEdge(edge);
|
|
1458
|
+
}
|
|
1459
|
+
const pagerank = new PageRankCalculator().calculate(graph, { limit: graph.nodeCount });
|
|
1460
|
+
const pagerankMap = /* @__PURE__ */ new Map();
|
|
1461
|
+
for (const entry of pagerank.rankings) pagerankMap.set(entry.symbolId, entry.score);
|
|
1462
|
+
const detector = new CommunityDetector();
|
|
1463
|
+
const edgeQuality = pickEdgeQuality(tiers);
|
|
1464
|
+
const detectResult = detector.detect(graph, pagerankMap, edgeQuality);
|
|
1465
|
+
return {
|
|
1466
|
+
version: 1,
|
|
1467
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1468
|
+
commitSha: safeHeadSha(gitAdapter),
|
|
1469
|
+
modularity: detectResult.modularity,
|
|
1470
|
+
communities: detectResult.communities,
|
|
1471
|
+
godNodes: detectResult.godNodes,
|
|
1472
|
+
edgeQuality: detectResult.edgeQuality,
|
|
1473
|
+
crossClusterEdges: detectResult.crossClusterEdges
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Writes drift-events.json and boundary-violations.json next to
|
|
1478
|
+
* communities.json. Must be called AFTER storage.writeCommunities so the
|
|
1479
|
+
* prior snapshot has already been archived into communities.history/ and
|
|
1480
|
+
* can serve as history[0] for the detectors.
|
|
1481
|
+
*/
|
|
1482
|
+
writeArchitectureArtifacts(indices, snapshot) {
|
|
1483
|
+
try {
|
|
1484
|
+
const snapshotWriter = new CommunitySnapshotWriter(this.ctxoRoot, {
|
|
1485
|
+
allowProductionPath: true
|
|
1486
|
+
});
|
|
1487
|
+
const history = snapshotWriter.listHistory();
|
|
1488
|
+
const graph = new SymbolGraph();
|
|
1489
|
+
for (const fileIndex of indices) {
|
|
1490
|
+
for (const symbol of fileIndex.symbols) graph.addNode(symbol);
|
|
1491
|
+
}
|
|
1492
|
+
for (const fileIndex of indices) {
|
|
1493
|
+
for (const edge of fileIndex.edges) graph.addEdge(edge);
|
|
1494
|
+
}
|
|
1495
|
+
const driftResult = new DriftDetector().detect(snapshot, history);
|
|
1496
|
+
const violationsResult = new BoundaryViolationDetector().detect(
|
|
1497
|
+
graph,
|
|
1498
|
+
snapshot,
|
|
1499
|
+
history
|
|
1500
|
+
);
|
|
1501
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1502
|
+
const driftArtifact = {
|
|
1503
|
+
version: 1,
|
|
1504
|
+
computedAt,
|
|
1505
|
+
commitSha: snapshot.commitSha,
|
|
1506
|
+
confidence: driftResult.confidence,
|
|
1507
|
+
snapshotsAvailable: driftResult.snapshotsAvailable,
|
|
1508
|
+
...driftResult.hint ? { hint: driftResult.hint } : {},
|
|
1509
|
+
events: driftResult.events
|
|
1510
|
+
};
|
|
1511
|
+
const violationsArtifact = {
|
|
1512
|
+
version: 1,
|
|
1513
|
+
computedAt,
|
|
1514
|
+
commitSha: snapshot.commitSha,
|
|
1515
|
+
confidence: violationsResult.confidence,
|
|
1516
|
+
snapshotsAvailable: violationsResult.snapshotsAvailable,
|
|
1517
|
+
...violationsResult.hint ? { hint: violationsResult.hint } : {},
|
|
1518
|
+
violations: violationsResult.violations
|
|
1519
|
+
};
|
|
1520
|
+
const writer = new ArchitectureArtifactsWriter(this.ctxoRoot, {
|
|
1521
|
+
allowProductionPath: true
|
|
1522
|
+
});
|
|
1523
|
+
writer.writeDriftEvents(driftArtifact);
|
|
1524
|
+
writer.writeBoundaryViolations(violationsArtifact);
|
|
1525
|
+
console.error(
|
|
1526
|
+
`[ctxo] Architecture artifacts: ${driftResult.events.length} drift events, ${violationsResult.violations.length} boundary violations (confidence: drift=${driftResult.confidence}, violations=${violationsResult.confidence})`
|
|
1527
|
+
);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
console.error(
|
|
1530
|
+
`[ctxo] Architecture artifacts skipped: ${err.message}`
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1038
1534
|
};
|
|
1535
|
+
function pickEdgeQuality(tiers) {
|
|
1536
|
+
const hasSyntaxOnly = tiers.csharpTier === "syntax" || tiers.goTier === "syntax";
|
|
1537
|
+
const hasFull = tiers.tsCount > 0 || tiers.csharpTier === "full" || tiers.goTier === "full";
|
|
1538
|
+
if (hasSyntaxOnly && hasFull) return "mixed";
|
|
1539
|
+
if (hasSyntaxOnly) return "syntax-only";
|
|
1540
|
+
return "full";
|
|
1541
|
+
}
|
|
1542
|
+
function safeHeadSha(gitAdapter) {
|
|
1543
|
+
try {
|
|
1544
|
+
const maybeSha = gitAdapter.getHeadSha?.();
|
|
1545
|
+
if (maybeSha) return maybeSha.slice(0, 8);
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
const out = execFileSync2("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf-8" });
|
|
1550
|
+
return out.trim();
|
|
1551
|
+
} catch {
|
|
1552
|
+
return "nocommit";
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
function countCommunities(snapshot) {
|
|
1556
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1557
|
+
for (const entry of snapshot.communities) ids.add(entry.communityId);
|
|
1558
|
+
return ids.size;
|
|
1559
|
+
}
|
|
1039
1560
|
|
|
1040
1561
|
// src/cli/sync-command.ts
|
|
1041
|
-
import { join as
|
|
1562
|
+
import { join as join9 } from "path";
|
|
1042
1563
|
var SyncCommand = class {
|
|
1043
1564
|
ctxoRoot;
|
|
1044
1565
|
constructor(projectRoot) {
|
|
1045
|
-
this.ctxoRoot =
|
|
1566
|
+
this.ctxoRoot = join9(projectRoot, ".ctxo");
|
|
1046
1567
|
}
|
|
1047
1568
|
async run() {
|
|
1048
1569
|
console.error("[ctxo] Rebuilding SQLite cache from committed JSON index...");
|
|
1049
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
1570
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
1050
1571
|
try {
|
|
1051
1572
|
await storage.init();
|
|
1052
1573
|
} finally {
|
|
@@ -1057,19 +1578,19 @@ var SyncCommand = class {
|
|
|
1057
1578
|
};
|
|
1058
1579
|
|
|
1059
1580
|
// src/cli/status-command.ts
|
|
1060
|
-
import { join as
|
|
1061
|
-
import { existsSync as
|
|
1581
|
+
import { join as join10 } from "path";
|
|
1582
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1062
1583
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1063
1584
|
var StatusCommand = class {
|
|
1064
1585
|
projectRoot;
|
|
1065
1586
|
ctxoRoot;
|
|
1066
1587
|
constructor(projectRoot) {
|
|
1067
1588
|
this.projectRoot = projectRoot;
|
|
1068
|
-
this.ctxoRoot =
|
|
1589
|
+
this.ctxoRoot = join10(projectRoot, ".ctxo");
|
|
1069
1590
|
}
|
|
1070
1591
|
run() {
|
|
1071
|
-
const indexDir =
|
|
1072
|
-
if (!
|
|
1592
|
+
const indexDir = join10(this.ctxoRoot, "index");
|
|
1593
|
+
if (!existsSync10(indexDir)) {
|
|
1073
1594
|
console.error('[ctxo] No index found. Run "ctxo index" first.');
|
|
1074
1595
|
return;
|
|
1075
1596
|
}
|
|
@@ -1084,7 +1605,7 @@ var StatusCommand = class {
|
|
|
1084
1605
|
console.error(` Indexed files: ${indices.length}`);
|
|
1085
1606
|
console.error(` Total symbols: ${totalSymbols}`);
|
|
1086
1607
|
console.error(` Total edges: ${totalEdges}`);
|
|
1087
|
-
const cacheExists =
|
|
1608
|
+
const cacheExists = existsSync10(join10(this.ctxoRoot, ".cache", "symbols.db"));
|
|
1088
1609
|
console.error(` SQLite cache: ${cacheExists ? "present" : "missing (run ctxo sync)"}`);
|
|
1089
1610
|
if (indices.length > 0) {
|
|
1090
1611
|
console.error("");
|
|
@@ -1113,8 +1634,8 @@ var StatusCommand = class {
|
|
|
1113
1634
|
|
|
1114
1635
|
// src/cli/verify-command.ts
|
|
1115
1636
|
import { mkdtempSync, rmSync } from "fs";
|
|
1116
|
-
import { join as
|
|
1117
|
-
import { tmpdir } from "os";
|
|
1637
|
+
import { join as join11 } from "path";
|
|
1638
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1118
1639
|
var VerifyCommand = class {
|
|
1119
1640
|
projectRoot;
|
|
1120
1641
|
constructor(projectRoot) {
|
|
@@ -1122,12 +1643,12 @@ var VerifyCommand = class {
|
|
|
1122
1643
|
}
|
|
1123
1644
|
async run() {
|
|
1124
1645
|
console.error("[ctxo] Verifying index freshness...");
|
|
1125
|
-
const tempDir = mkdtempSync(
|
|
1646
|
+
const tempDir = mkdtempSync(join11(tmpdir2(), "ctxo-verify-"));
|
|
1126
1647
|
try {
|
|
1127
|
-
const tempCtxo =
|
|
1648
|
+
const tempCtxo = join11(tempDir, ".ctxo");
|
|
1128
1649
|
const indexCmd = new IndexCommand(this.projectRoot, tempCtxo);
|
|
1129
1650
|
await indexCmd.run({ skipSideEffects: true });
|
|
1130
|
-
const committedReader = new JsonIndexReader(
|
|
1651
|
+
const committedReader = new JsonIndexReader(join11(this.projectRoot, ".ctxo"));
|
|
1131
1652
|
const freshReader = new JsonIndexReader(tempCtxo);
|
|
1132
1653
|
const committedIndices = committedReader.readAll();
|
|
1133
1654
|
const freshIndices = freshReader.readAll();
|
|
@@ -1160,13 +1681,13 @@ var VerifyCommand = class {
|
|
|
1160
1681
|
};
|
|
1161
1682
|
|
|
1162
1683
|
// src/cli/init-command.ts
|
|
1163
|
-
import { join as
|
|
1164
|
-
import { existsSync as
|
|
1684
|
+
import { join as join13 } from "path";
|
|
1685
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, chmodSync } from "fs";
|
|
1165
1686
|
import { createRequire as createRequire3 } from "module";
|
|
1166
1687
|
|
|
1167
1688
|
// src/cli/ai-rules.ts
|
|
1168
|
-
import { existsSync as
|
|
1169
|
-
import { join as
|
|
1689
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1690
|
+
import { join as join12, dirname as dirname6 } from "path";
|
|
1170
1691
|
var PLATFORMS = [
|
|
1171
1692
|
{ id: "claude-code", name: "Claude Code", file: "CLAUDE.md", mode: "append", detectPaths: ["CLAUDE.md", ".claude"], starred: true },
|
|
1172
1693
|
{ id: "cursor", name: "Cursor", file: ".cursor/rules/ctxo.mdc", mode: "create", detectPaths: [".cursor", ".cursorrules"], starred: false },
|
|
@@ -1179,7 +1700,7 @@ var PLATFORMS = [
|
|
|
1179
1700
|
function detectPlatforms(projectRoot) {
|
|
1180
1701
|
const detected = /* @__PURE__ */ new Set();
|
|
1181
1702
|
for (const p of PLATFORMS) {
|
|
1182
|
-
if (p.detectPaths.some((d) =>
|
|
1703
|
+
if (p.detectPaths.some((d) => existsSync11(join12(projectRoot, d)))) {
|
|
1183
1704
|
detected.add(p.id);
|
|
1184
1705
|
}
|
|
1185
1706
|
}
|
|
@@ -1241,31 +1762,31 @@ function generateRules(platformId) {
|
|
|
1241
1762
|
function installRules(projectRoot, platformId) {
|
|
1242
1763
|
const platform = PLATFORMS.find((p) => p.id === platformId);
|
|
1243
1764
|
if (!platform) throw new Error(`Unknown platform: ${platformId}`);
|
|
1244
|
-
const filePath =
|
|
1765
|
+
const filePath = join12(projectRoot, platform.file);
|
|
1245
1766
|
const content = generateRules(platformId);
|
|
1246
1767
|
const dir = dirname6(filePath);
|
|
1247
|
-
if (!
|
|
1768
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1248
1769
|
if (platform.mode === "create") {
|
|
1249
|
-
|
|
1250
|
-
return { file: platform.file, action:
|
|
1770
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1771
|
+
return { file: platform.file, action: existsSync11(filePath) ? "updated" : "created" };
|
|
1251
1772
|
}
|
|
1252
|
-
if (!
|
|
1253
|
-
|
|
1773
|
+
if (!existsSync11(filePath)) {
|
|
1774
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1254
1775
|
return { file: platform.file, action: "created" };
|
|
1255
1776
|
}
|
|
1256
|
-
const existing =
|
|
1777
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
1257
1778
|
if (existing.includes(SECTION_START)) {
|
|
1258
1779
|
const re = new RegExp(`${escapeRegExp(SECTION_START)}[\\s\\S]*?${escapeRegExp(SECTION_END)}`, "m");
|
|
1259
1780
|
const updated2 = existing.replace(re, `${SECTION_START}
|
|
1260
1781
|
${content}${SECTION_END}`);
|
|
1261
|
-
|
|
1782
|
+
writeFileSync3(filePath, updated2, "utf-8");
|
|
1262
1783
|
return { file: platform.file, action: "updated" };
|
|
1263
1784
|
}
|
|
1264
1785
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1265
1786
|
const updated = existing + separator + `${SECTION_START}
|
|
1266
1787
|
${content}${SECTION_END}
|
|
1267
1788
|
`;
|
|
1268
|
-
|
|
1789
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
1269
1790
|
return { file: platform.file, action: "updated" };
|
|
1270
1791
|
}
|
|
1271
1792
|
function escapeRegExp(s) {
|
|
@@ -1273,14 +1794,14 @@ function escapeRegExp(s) {
|
|
|
1273
1794
|
}
|
|
1274
1795
|
var GITIGNORE_ENTRY = ".ctxo/.cache/";
|
|
1275
1796
|
function ensureGitignore(projectRoot) {
|
|
1276
|
-
const filePath =
|
|
1277
|
-
if (!
|
|
1278
|
-
|
|
1797
|
+
const filePath = join12(projectRoot, ".gitignore");
|
|
1798
|
+
if (!existsSync11(filePath)) {
|
|
1799
|
+
writeFileSync3(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
|
|
1279
1800
|
${GITIGNORE_ENTRY}
|
|
1280
1801
|
`, "utf-8");
|
|
1281
1802
|
return { file: ".gitignore", action: "created" };
|
|
1282
1803
|
}
|
|
1283
|
-
const existing =
|
|
1804
|
+
const existing = readFileSync7(filePath, "utf-8");
|
|
1284
1805
|
if (existing.includes(GITIGNORE_ENTRY)) {
|
|
1285
1806
|
return { file: ".gitignore", action: "skipped" };
|
|
1286
1807
|
}
|
|
@@ -1288,7 +1809,7 @@ ${GITIGNORE_ENTRY}
|
|
|
1288
1809
|
const updated = existing + `${separator}# Ctxo local cache (auto-rebuilt, never committed)
|
|
1289
1810
|
${GITIGNORE_ENTRY}
|
|
1290
1811
|
`;
|
|
1291
|
-
|
|
1812
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
1292
1813
|
return { file: ".gitignore", action: "updated" };
|
|
1293
1814
|
}
|
|
1294
1815
|
var DEFAULT_CONFIG = `# ctxo project configuration
|
|
@@ -1311,13 +1832,13 @@ index:
|
|
|
1311
1832
|
# - "examples/*"
|
|
1312
1833
|
`;
|
|
1313
1834
|
function ensureConfig(projectRoot) {
|
|
1314
|
-
const filePath =
|
|
1835
|
+
const filePath = join12(projectRoot, ".ctxo", "config.yaml");
|
|
1315
1836
|
const dir = dirname6(filePath);
|
|
1316
|
-
if (!
|
|
1317
|
-
if (
|
|
1837
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1838
|
+
if (existsSync11(filePath)) {
|
|
1318
1839
|
return { file: ".ctxo/config.yaml", action: "skipped" };
|
|
1319
1840
|
}
|
|
1320
|
-
|
|
1841
|
+
writeFileSync3(filePath, DEFAULT_CONFIG, "utf-8");
|
|
1321
1842
|
return { file: ".ctxo/config.yaml", action: "created" };
|
|
1322
1843
|
}
|
|
1323
1844
|
var CTXO_MCP_ENTRY = { command: "npx", args: ["-y", "@ctxo/cli"] };
|
|
@@ -1343,21 +1864,21 @@ function getMcpConfigTargets(selectedPlatformIds) {
|
|
|
1343
1864
|
return [...targets.values()];
|
|
1344
1865
|
}
|
|
1345
1866
|
function ensureMcpConfig(projectRoot, target) {
|
|
1346
|
-
const filePath =
|
|
1867
|
+
const filePath = join12(projectRoot, target.file);
|
|
1347
1868
|
const dir = dirname6(filePath);
|
|
1348
|
-
if (!
|
|
1869
|
+
if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
|
|
1349
1870
|
const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
|
|
1350
|
-
if (!
|
|
1871
|
+
if (!existsSync11(filePath)) {
|
|
1351
1872
|
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1352
|
-
|
|
1873
|
+
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1353
1874
|
return { file: target.file, action: "created" };
|
|
1354
1875
|
}
|
|
1355
1876
|
let existing;
|
|
1356
1877
|
try {
|
|
1357
|
-
existing = JSON.parse(
|
|
1878
|
+
existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
1358
1879
|
} catch {
|
|
1359
1880
|
const config = { [target.serverKey]: { ctxo: entry } };
|
|
1360
|
-
|
|
1881
|
+
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1361
1882
|
return { file: target.file, action: "updated" };
|
|
1362
1883
|
}
|
|
1363
1884
|
const servers = existing[target.serverKey] ?? {};
|
|
@@ -1366,7 +1887,7 @@ function ensureMcpConfig(projectRoot, target) {
|
|
|
1366
1887
|
}
|
|
1367
1888
|
servers["ctxo"] = entry;
|
|
1368
1889
|
existing[target.serverKey] = servers;
|
|
1369
|
-
|
|
1890
|
+
writeFileSync3(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
1370
1891
|
return { file: target.file, action: "updated" };
|
|
1371
1892
|
}
|
|
1372
1893
|
|
|
@@ -1463,13 +1984,13 @@ var InitCommand = class {
|
|
|
1463
1984
|
this.projectRoot = projectRoot;
|
|
1464
1985
|
}
|
|
1465
1986
|
installHooks() {
|
|
1466
|
-
const hooksDir =
|
|
1467
|
-
|
|
1987
|
+
const hooksDir = join13(this.projectRoot, ".git", "hooks");
|
|
1988
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
1468
1989
|
this.installHook(hooksDir, "post-commit", POST_COMMIT_CONTENT);
|
|
1469
1990
|
this.installHook(hooksDir, "post-merge", POST_MERGE_CONTENT);
|
|
1470
1991
|
}
|
|
1471
1992
|
async run(options = {}) {
|
|
1472
|
-
if (!
|
|
1993
|
+
if (!existsSync12(join13(this.projectRoot, ".git"))) {
|
|
1473
1994
|
console.error('[ctxo] Not a git repository. Run "git init" first.');
|
|
1474
1995
|
process.exit(1);
|
|
1475
1996
|
}
|
|
@@ -1506,8 +2027,8 @@ var InitCommand = class {
|
|
|
1506
2027
|
clack.cancel("Setup cancelled.");
|
|
1507
2028
|
process.exit(0);
|
|
1508
2029
|
}
|
|
1509
|
-
const fullPath =
|
|
1510
|
-
if (!
|
|
2030
|
+
const fullPath = join13(this.projectRoot, indexDir);
|
|
2031
|
+
if (!existsSync12(fullPath)) mkdirSync5(fullPath, { recursive: true });
|
|
1511
2032
|
console.error("");
|
|
1512
2033
|
}
|
|
1513
2034
|
stepNum++;
|
|
@@ -1569,7 +2090,13 @@ var InitCommand = class {
|
|
|
1569
2090
|
let installGitHooks = false;
|
|
1570
2091
|
if (!options.rulesOnly) {
|
|
1571
2092
|
stepNum++;
|
|
1572
|
-
console.error(stepHeader(
|
|
2093
|
+
console.error(stepHeader(
|
|
2094
|
+
stepNum,
|
|
2095
|
+
totalSteps,
|
|
2096
|
+
`Git Hooks ${pc.yellow("recommended")}`,
|
|
2097
|
+
"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`).",
|
|
2098
|
+
pc
|
|
2099
|
+
));
|
|
1573
2100
|
console.error("");
|
|
1574
2101
|
const hooks = await clack.confirm({
|
|
1575
2102
|
message: pc.cyan("Install hooks?"),
|
|
@@ -1580,6 +2107,14 @@ var InitCommand = class {
|
|
|
1580
2107
|
process.exit(0);
|
|
1581
2108
|
}
|
|
1582
2109
|
installGitHooks = hooks;
|
|
2110
|
+
if (!installGitHooks) {
|
|
2111
|
+
console.error("");
|
|
2112
|
+
console.error(pc.yellow(" \u26A0 Drift & boundary-violation signals depend on repeated snapshots."));
|
|
2113
|
+
console.error(pc.dim(" Options without hooks:"));
|
|
2114
|
+
console.error(pc.dim(" \u2022 run `ctxo watch` during development"));
|
|
2115
|
+
console.error(pc.dim(" \u2022 add `ctxo index --check` to CI"));
|
|
2116
|
+
console.error(pc.dim(" \u2022 run `ctxo init` again to install hooks later"));
|
|
2117
|
+
}
|
|
1583
2118
|
console.error("");
|
|
1584
2119
|
}
|
|
1585
2120
|
const s = clack.spinner();
|
|
@@ -1650,8 +2185,8 @@ var InitCommand = class {
|
|
|
1650
2185
|
const gitignoreResult = ensureGitignore(this.projectRoot);
|
|
1651
2186
|
const configResult = ensureConfig(this.projectRoot);
|
|
1652
2187
|
if (!options.rulesOnly) {
|
|
1653
|
-
const indexDir =
|
|
1654
|
-
if (!
|
|
2188
|
+
const indexDir = join13(this.projectRoot, ".ctxo", "index");
|
|
2189
|
+
if (!existsSync12(indexDir)) mkdirSync5(indexDir, { recursive: true });
|
|
1655
2190
|
console.error("[ctxo] \u2713 .ctxo/index/ ready");
|
|
1656
2191
|
}
|
|
1657
2192
|
if (configResult.action !== "skipped") {
|
|
@@ -1703,7 +2238,7 @@ var InitCommand = class {
|
|
|
1703
2238
|
for (const id of toolIds) {
|
|
1704
2239
|
const platform = PLATFORMS.find((p) => p.id === id);
|
|
1705
2240
|
if (platform) {
|
|
1706
|
-
const exists =
|
|
2241
|
+
const exists = existsSync12(join13(this.projectRoot, platform.file));
|
|
1707
2242
|
const action = platform.mode === "append" && exists ? "append section" : "create";
|
|
1708
2243
|
console.error(` ${platform.file.padEnd(42)} (${action})`);
|
|
1709
2244
|
}
|
|
@@ -1718,9 +2253,9 @@ var InitCommand = class {
|
|
|
1718
2253
|
/* ---------------------------------------------------------------- */
|
|
1719
2254
|
async getVersion() {
|
|
1720
2255
|
try {
|
|
1721
|
-
const pkgPath =
|
|
1722
|
-
if (
|
|
1723
|
-
return JSON.parse(
|
|
2256
|
+
const pkgPath = join13(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
|
|
2257
|
+
if (existsSync12(pkgPath)) {
|
|
2258
|
+
return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
|
|
1724
2259
|
}
|
|
1725
2260
|
const { createRequire: createRequire7 } = await import("module");
|
|
1726
2261
|
const require4 = createRequire7(import.meta.url);
|
|
@@ -1730,23 +2265,23 @@ var InitCommand = class {
|
|
|
1730
2265
|
}
|
|
1731
2266
|
}
|
|
1732
2267
|
installHook(hooksDir, hookName, hookContent) {
|
|
1733
|
-
const hookPath =
|
|
2268
|
+
const hookPath = join13(hooksDir, hookName);
|
|
1734
2269
|
let existing = "";
|
|
1735
|
-
if (
|
|
1736
|
-
existing =
|
|
2270
|
+
if (existsSync12(hookPath)) {
|
|
2271
|
+
existing = readFileSync8(hookPath, "utf-8");
|
|
1737
2272
|
if (existing.includes(CTXO_START)) return;
|
|
1738
2273
|
} else {
|
|
1739
2274
|
existing = "#!/bin/sh\n";
|
|
1740
2275
|
}
|
|
1741
2276
|
const updated = existing.endsWith("\n") ? existing + "\n" + hookContent + "\n" : existing + "\n\n" + hookContent + "\n";
|
|
1742
|
-
|
|
2277
|
+
writeFileSync4(hookPath, updated, "utf-8");
|
|
1743
2278
|
chmodSync(hookPath, 493);
|
|
1744
2279
|
}
|
|
1745
2280
|
};
|
|
1746
2281
|
|
|
1747
2282
|
// src/cli/watch-command.ts
|
|
1748
|
-
import { join as
|
|
1749
|
-
import { readFileSync as
|
|
2283
|
+
import { join as join14, extname as extname4, relative as relative2 } from "path";
|
|
2284
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1750
2285
|
|
|
1751
2286
|
// src/adapters/watcher/chokidar-watcher-adapter.ts
|
|
1752
2287
|
import { watch } from "chokidar";
|
|
@@ -1787,13 +2322,15 @@ var ChokidarWatcherAdapter = class {
|
|
|
1787
2322
|
};
|
|
1788
2323
|
|
|
1789
2324
|
// src/cli/watch-command.ts
|
|
2325
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1790
2326
|
var DEBOUNCE_MS = 300;
|
|
2327
|
+
var COMMUNITY_SNAPSHOT_DEBOUNCE_MS = 5e3;
|
|
1791
2328
|
var WatchCommand = class {
|
|
1792
2329
|
projectRoot;
|
|
1793
2330
|
ctxoRoot;
|
|
1794
2331
|
constructor(projectRoot) {
|
|
1795
2332
|
this.projectRoot = projectRoot;
|
|
1796
|
-
this.ctxoRoot =
|
|
2333
|
+
this.ctxoRoot = join14(projectRoot, ".ctxo");
|
|
1797
2334
|
}
|
|
1798
2335
|
async run() {
|
|
1799
2336
|
console.error("[ctxo] Starting file watcher...");
|
|
@@ -1824,13 +2361,15 @@ var WatchCommand = class {
|
|
|
1824
2361
|
}
|
|
1825
2362
|
const supportedExtensions = registry.getSupportedExtensions();
|
|
1826
2363
|
const writer = new JsonIndexWriter(this.ctxoRoot);
|
|
1827
|
-
const storage = new SqliteStorageAdapter(this.ctxoRoot);
|
|
2364
|
+
const storage = new SqliteStorageAdapter(this.ctxoRoot, { allowProductionPath: true });
|
|
1828
2365
|
await storage.init();
|
|
1829
2366
|
const hasher = new ContentHasher();
|
|
1830
2367
|
const gitAdapter = new SimpleGitAdapter(this.projectRoot);
|
|
1831
2368
|
const revertDetector = new RevertDetector();
|
|
1832
2369
|
const watcher = new ChokidarWatcherAdapter(this.projectRoot);
|
|
1833
2370
|
const pendingFiles = /* @__PURE__ */ new Map();
|
|
2371
|
+
const snapshotThreshold = watchSnapshotMinFileChanges(loadConfig(this.ctxoRoot).config);
|
|
2372
|
+
const pathsChangedSinceSnapshot = /* @__PURE__ */ new Set();
|
|
1834
2373
|
const reindexFile = async (filePath) => {
|
|
1835
2374
|
const adapter = registry.getAdapter(filePath);
|
|
1836
2375
|
if (!adapter) return;
|
|
@@ -1847,7 +2386,7 @@ var WatchCommand = class {
|
|
|
1847
2386
|
kind: "commit"
|
|
1848
2387
|
}));
|
|
1849
2388
|
const antiPatterns2 = revertDetector.detect(commits2);
|
|
1850
|
-
const source2 =
|
|
2389
|
+
const source2 = readFileSync9(filePath, "utf-8");
|
|
1851
2390
|
const fileIndex2 = {
|
|
1852
2391
|
file: relativePath,
|
|
1853
2392
|
lastModified: Math.floor(Date.now() / 1e3),
|
|
@@ -1877,7 +2416,7 @@ var WatchCommand = class {
|
|
|
1877
2416
|
return;
|
|
1878
2417
|
}
|
|
1879
2418
|
}
|
|
1880
|
-
const source =
|
|
2419
|
+
const source = readFileSync9(filePath, "utf-8");
|
|
1881
2420
|
const lastModified = Math.floor(Date.now() / 1e3);
|
|
1882
2421
|
const symbols = await adapter.extractSymbols(relativePath, source);
|
|
1883
2422
|
const edges = await adapter.extractEdges(relativePath, source);
|
|
@@ -1907,14 +2446,38 @@ var WatchCommand = class {
|
|
|
1907
2446
|
console.error(`[ctxo] Failed to re-index ${relativePath}: ${err.message}`);
|
|
1908
2447
|
}
|
|
1909
2448
|
};
|
|
2449
|
+
let communitySnapshotTimer;
|
|
2450
|
+
const scheduleCommunitySnapshot = () => {
|
|
2451
|
+
if (pathsChangedSinceSnapshot.size < snapshotThreshold) return;
|
|
2452
|
+
if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
|
|
2453
|
+
communitySnapshotTimer = setTimeout(() => {
|
|
2454
|
+
communitySnapshotTimer = void 0;
|
|
2455
|
+
if (pathsChangedSinceSnapshot.size < snapshotThreshold) return;
|
|
2456
|
+
const changedCount = pathsChangedSinceSnapshot.size;
|
|
2457
|
+
pathsChangedSinceSnapshot.clear();
|
|
2458
|
+
try {
|
|
2459
|
+
const snapshot = buildWatchCommunitySnapshot(storage);
|
|
2460
|
+
if (snapshot) {
|
|
2461
|
+
storage.writeCommunities(snapshot);
|
|
2462
|
+
console.error(
|
|
2463
|
+
`[ctxo] Community snapshot refreshed after ${changedCount} file change(s) (modularity ${snapshot.modularity.toFixed(3)})`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
console.error(`[ctxo] Community snapshot skipped: ${err.message}`);
|
|
2468
|
+
}
|
|
2469
|
+
}, COMMUNITY_SNAPSHOT_DEBOUNCE_MS);
|
|
2470
|
+
};
|
|
1910
2471
|
const debouncedReindex = (filePath) => {
|
|
1911
2472
|
const existing = pendingFiles.get(filePath);
|
|
1912
2473
|
if (existing) clearTimeout(existing);
|
|
1913
2474
|
pendingFiles.set(
|
|
1914
2475
|
filePath,
|
|
1915
|
-
setTimeout(() => {
|
|
2476
|
+
setTimeout(async () => {
|
|
1916
2477
|
pendingFiles.delete(filePath);
|
|
1917
|
-
reindexFile(filePath);
|
|
2478
|
+
await reindexFile(filePath);
|
|
2479
|
+
pathsChangedSinceSnapshot.add(filePath);
|
|
2480
|
+
scheduleCommunitySnapshot();
|
|
1918
2481
|
}, DEBOUNCE_MS)
|
|
1919
2482
|
);
|
|
1920
2483
|
};
|
|
@@ -1936,6 +2499,7 @@ var WatchCommand = class {
|
|
|
1936
2499
|
for (const timeout of pendingFiles.values()) {
|
|
1937
2500
|
clearTimeout(timeout);
|
|
1938
2501
|
}
|
|
2502
|
+
if (communitySnapshotTimer) clearTimeout(communitySnapshotTimer);
|
|
1939
2503
|
const roslynShutdown = roslynAdapter ? roslynAdapter.dispose() : Promise.resolve();
|
|
1940
2504
|
roslynShutdown.then(() => {
|
|
1941
2505
|
watcher.stop().then(() => {
|
|
@@ -1949,23 +2513,53 @@ var WatchCommand = class {
|
|
|
1949
2513
|
process.on("SIGTERM", cleanup);
|
|
1950
2514
|
}
|
|
1951
2515
|
};
|
|
2516
|
+
function buildWatchCommunitySnapshot(storage) {
|
|
2517
|
+
const symbols = storage.getAllSymbols();
|
|
2518
|
+
if (symbols.length === 0) return void 0;
|
|
2519
|
+
const edges = storage.getAllEdges();
|
|
2520
|
+
const graph = new SymbolGraph();
|
|
2521
|
+
for (const symbol of symbols) graph.addNode(symbol);
|
|
2522
|
+
for (const edge of edges) graph.addEdge(edge);
|
|
2523
|
+
const pagerank = new PageRankCalculator().calculate(graph, { limit: graph.nodeCount });
|
|
2524
|
+
const pagerankMap = /* @__PURE__ */ new Map();
|
|
2525
|
+
for (const entry of pagerank.rankings) pagerankMap.set(entry.symbolId, entry.score);
|
|
2526
|
+
const detector = new CommunityDetector();
|
|
2527
|
+
const detectResult = detector.detect(graph, pagerankMap, "full");
|
|
2528
|
+
return {
|
|
2529
|
+
version: 1,
|
|
2530
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2531
|
+
commitSha: readHeadSha(),
|
|
2532
|
+
modularity: detectResult.modularity,
|
|
2533
|
+
communities: detectResult.communities,
|
|
2534
|
+
godNodes: detectResult.godNodes,
|
|
2535
|
+
edgeQuality: detectResult.edgeQuality,
|
|
2536
|
+
crossClusterEdges: detectResult.crossClusterEdges
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
function readHeadSha() {
|
|
2540
|
+
try {
|
|
2541
|
+
return execFileSync4("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf-8" }).trim();
|
|
2542
|
+
} catch {
|
|
2543
|
+
return "nocommit";
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
1952
2546
|
|
|
1953
2547
|
// src/cli/stats-command.ts
|
|
1954
|
-
import { join as
|
|
1955
|
-
import { existsSync as
|
|
2548
|
+
import { join as join15 } from "path";
|
|
2549
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
1956
2550
|
import initSqlJs from "sql.js";
|
|
1957
2551
|
var StatsCommand = class {
|
|
1958
2552
|
ctxoRoot;
|
|
1959
2553
|
constructor(projectRoot) {
|
|
1960
|
-
this.ctxoRoot =
|
|
2554
|
+
this.ctxoRoot = join15(projectRoot, ".ctxo");
|
|
1961
2555
|
}
|
|
1962
2556
|
async run(options) {
|
|
1963
2557
|
if (!this.isStatsEnabled()) {
|
|
1964
2558
|
console.error("[ctxo] Stats collection is disabled in .ctxo/config.yaml");
|
|
1965
2559
|
return;
|
|
1966
2560
|
}
|
|
1967
|
-
const dbPath =
|
|
1968
|
-
if (!
|
|
2561
|
+
const dbPath = join15(this.ctxoRoot, ".cache", "symbols.db");
|
|
2562
|
+
if (!existsSync13(dbPath)) {
|
|
1969
2563
|
if (options.json) {
|
|
1970
2564
|
process.stdout.write(JSON.stringify(this.emptyReport(options.days)) + "\n");
|
|
1971
2565
|
} else {
|
|
@@ -1974,15 +2568,15 @@ var StatsCommand = class {
|
|
|
1974
2568
|
return;
|
|
1975
2569
|
}
|
|
1976
2570
|
const SQL = await initSqlJs();
|
|
1977
|
-
const buffer =
|
|
2571
|
+
const buffer = readFileSync10(dbPath);
|
|
1978
2572
|
const db = new SQL.Database(buffer);
|
|
1979
2573
|
const recorder = new SessionRecorderAdapter(db);
|
|
1980
2574
|
try {
|
|
1981
2575
|
if (options.clear) {
|
|
1982
2576
|
recorder.clear();
|
|
1983
2577
|
const data = db.export();
|
|
1984
|
-
const { writeFileSync:
|
|
1985
|
-
|
|
2578
|
+
const { writeFileSync: writeFileSync6 } = await import("fs");
|
|
2579
|
+
writeFileSync6(dbPath, Buffer.from(data));
|
|
1986
2580
|
console.error("[ctxo] Session data cleared.");
|
|
1987
2581
|
return;
|
|
1988
2582
|
}
|
|
@@ -2012,10 +2606,10 @@ var StatsCommand = class {
|
|
|
2012
2606
|
}
|
|
2013
2607
|
}
|
|
2014
2608
|
isStatsEnabled() {
|
|
2015
|
-
const configPath =
|
|
2016
|
-
if (!
|
|
2609
|
+
const configPath = join15(this.ctxoRoot, "config.yaml");
|
|
2610
|
+
if (!existsSync13(configPath)) return true;
|
|
2017
2611
|
try {
|
|
2018
|
-
const raw =
|
|
2612
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
2019
2613
|
const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
|
|
2020
2614
|
if (match) return match[1] !== "false";
|
|
2021
2615
|
return true;
|
|
@@ -2104,15 +2698,17 @@ var StatsCommand = class {
|
|
|
2104
2698
|
};
|
|
2105
2699
|
|
|
2106
2700
|
// src/cli/doctor-command.ts
|
|
2107
|
-
import { join as
|
|
2108
|
-
import { existsSync as
|
|
2701
|
+
import { join as join22 } from "path";
|
|
2702
|
+
import { existsSync as existsSync20 } from "fs";
|
|
2109
2703
|
|
|
2110
2704
|
// src/adapters/diagnostics/health-checker.ts
|
|
2111
|
-
var
|
|
2705
|
+
var log4 = createLogger("ctxo:doctor");
|
|
2112
2706
|
var HealthChecker = class {
|
|
2113
2707
|
checks;
|
|
2114
|
-
|
|
2708
|
+
quiet;
|
|
2709
|
+
constructor(checks, options = {}) {
|
|
2115
2710
|
this.checks = checks;
|
|
2711
|
+
this.quiet = options.quiet ?? false;
|
|
2116
2712
|
}
|
|
2117
2713
|
async runAll(ctx) {
|
|
2118
2714
|
const settled = await Promise.allSettled(
|
|
@@ -2122,11 +2718,13 @@ var HealthChecker = class {
|
|
|
2122
2718
|
const check = this.checks[i];
|
|
2123
2719
|
if (outcome.status === "fulfilled") {
|
|
2124
2720
|
const r = outcome.value;
|
|
2125
|
-
|
|
2721
|
+
if (!this.quiet) {
|
|
2722
|
+
log4.info(`${r.id}: ${r.status.toUpperCase()} (${r.message})`);
|
|
2723
|
+
}
|
|
2126
2724
|
return r;
|
|
2127
2725
|
}
|
|
2128
2726
|
const message = outcome.reason?.message ?? "Unknown error";
|
|
2129
|
-
|
|
2727
|
+
if (!this.quiet) log4.error(`${check.id}: FAIL (${message})`);
|
|
2130
2728
|
return {
|
|
2131
2729
|
id: check.id,
|
|
2132
2730
|
title: check.title,
|
|
@@ -2437,15 +3035,15 @@ function pluginResolves(packageName) {
|
|
|
2437
3035
|
}
|
|
2438
3036
|
|
|
2439
3037
|
// src/adapters/diagnostics/checks/git-check.ts
|
|
2440
|
-
import { execFileSync as
|
|
2441
|
-
import { existsSync as
|
|
2442
|
-
import { join as
|
|
3038
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3039
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3040
|
+
import { join as join16 } from "path";
|
|
2443
3041
|
var GitBinaryCheck = class {
|
|
2444
3042
|
id = "git_binary";
|
|
2445
3043
|
title = "Git available";
|
|
2446
3044
|
async run(_ctx) {
|
|
2447
3045
|
try {
|
|
2448
|
-
const version =
|
|
3046
|
+
const version = execFileSync5("git", ["--version"], { encoding: "utf-8" }).trim();
|
|
2449
3047
|
return { id: this.id, title: this.title, status: "pass", message: version, value: version };
|
|
2450
3048
|
} catch {
|
|
2451
3049
|
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 +3054,7 @@ var GitRepoCheck = class {
|
|
|
2456
3054
|
id = "git_repo";
|
|
2457
3055
|
title = "Git repository";
|
|
2458
3056
|
async run(ctx) {
|
|
2459
|
-
if (
|
|
3057
|
+
if (existsSync14(join16(ctx.projectRoot, ".git"))) {
|
|
2460
3058
|
return { id: this.id, title: this.title, status: "pass", message: ctx.projectRoot, value: ctx.projectRoot };
|
|
2461
3059
|
}
|
|
2462
3060
|
return { id: this.id, title: this.title, status: "fail", message: "Not a git repository", fix: 'Run "git init" or clone a repository' };
|
|
@@ -2464,9 +3062,9 @@ var GitRepoCheck = class {
|
|
|
2464
3062
|
};
|
|
2465
3063
|
|
|
2466
3064
|
// src/adapters/diagnostics/checks/index-check.ts
|
|
2467
|
-
import { existsSync as
|
|
2468
|
-
import { execFileSync as
|
|
2469
|
-
import { join as
|
|
3065
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3066
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3067
|
+
import { join as join17 } from "path";
|
|
2470
3068
|
function pass2(id, title, message, value) {
|
|
2471
3069
|
return { id, title, status: "pass", message, value };
|
|
2472
3070
|
}
|
|
@@ -2483,8 +3081,8 @@ var IndexDirectoryCheck = class {
|
|
|
2483
3081
|
id = "index_directory";
|
|
2484
3082
|
title = "Index directory";
|
|
2485
3083
|
async run(ctx) {
|
|
2486
|
-
const indexDir =
|
|
2487
|
-
if (!
|
|
3084
|
+
const indexDir = join17(ctx.ctxoRoot, "index");
|
|
3085
|
+
if (!existsSync15(indexDir)) {
|
|
2488
3086
|
return fail(this.id, this.title, "No index directory", 'Run "ctxo index"');
|
|
2489
3087
|
}
|
|
2490
3088
|
const indices = getIndices(ctx);
|
|
@@ -2564,7 +3162,7 @@ var OrphanedFilesCheck = class {
|
|
|
2564
3162
|
}
|
|
2565
3163
|
getSourceFiles(projectRoot) {
|
|
2566
3164
|
try {
|
|
2567
|
-
const output =
|
|
3165
|
+
const output = execFileSync6("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
|
|
2568
3166
|
cwd: projectRoot,
|
|
2569
3167
|
encoding: "utf-8"
|
|
2570
3168
|
});
|
|
@@ -2578,8 +3176,8 @@ var CoChangesCacheCheck = class {
|
|
|
2578
3176
|
id = "co_changes_cache";
|
|
2579
3177
|
title = "Co-changes cache";
|
|
2580
3178
|
async run(ctx) {
|
|
2581
|
-
const coChangesPath =
|
|
2582
|
-
if (
|
|
3179
|
+
const coChangesPath = join17(ctx.ctxoRoot, "index", "co-changes.json");
|
|
3180
|
+
if (existsSync15(coChangesPath)) {
|
|
2583
3181
|
return pass2(this.id, this.title, "present");
|
|
2584
3182
|
}
|
|
2585
3183
|
return warn2(this.id, this.title, "missing (co-change analysis disabled)", 'Run "ctxo index" with git history');
|
|
@@ -2603,20 +3201,20 @@ var SchemaVersionCheck = class {
|
|
|
2603
3201
|
};
|
|
2604
3202
|
|
|
2605
3203
|
// src/adapters/diagnostics/checks/storage-check.ts
|
|
2606
|
-
import { existsSync as
|
|
2607
|
-
import { join as
|
|
3204
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
3205
|
+
import { join as join18 } from "path";
|
|
2608
3206
|
var SqliteCacheCheck = class {
|
|
2609
3207
|
id = "sqlite_cache";
|
|
2610
3208
|
title = "SQLite cache";
|
|
2611
3209
|
async run(ctx) {
|
|
2612
|
-
const dbPath =
|
|
2613
|
-
if (!
|
|
3210
|
+
const dbPath = join18(ctx.ctxoRoot, ".cache", "symbols.db");
|
|
3211
|
+
if (!existsSync16(dbPath)) {
|
|
2614
3212
|
return { id: this.id, title: this.title, status: "warn", message: "SQLite cache missing", fix: 'Run "ctxo sync" to rebuild' };
|
|
2615
3213
|
}
|
|
2616
3214
|
try {
|
|
2617
3215
|
const { default: initSqlJs2 } = await import("sql.js");
|
|
2618
3216
|
const SQL = await initSqlJs2();
|
|
2619
|
-
const buffer =
|
|
3217
|
+
const buffer = readFileSync11(dbPath);
|
|
2620
3218
|
const db = new SQL.Database(new Uint8Array(buffer));
|
|
2621
3219
|
const result = db.exec("PRAGMA integrity_check");
|
|
2622
3220
|
db.close();
|
|
@@ -2632,18 +3230,18 @@ var SqliteCacheCheck = class {
|
|
|
2632
3230
|
};
|
|
2633
3231
|
|
|
2634
3232
|
// src/adapters/diagnostics/checks/config-check.ts
|
|
2635
|
-
import { existsSync as
|
|
2636
|
-
import { join as
|
|
3233
|
+
import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
|
|
3234
|
+
import { join as join19 } from "path";
|
|
2637
3235
|
var ConfigFileCheck = class {
|
|
2638
3236
|
id = "config_file";
|
|
2639
3237
|
title = "Config file";
|
|
2640
3238
|
async run(ctx) {
|
|
2641
|
-
const configPath =
|
|
2642
|
-
if (!
|
|
3239
|
+
const configPath = join19(ctx.ctxoRoot, "config.yaml");
|
|
3240
|
+
if (!existsSync17(configPath)) {
|
|
2643
3241
|
return { id: this.id, title: this.title, status: "warn", message: "No config.yaml (using defaults)" };
|
|
2644
3242
|
}
|
|
2645
3243
|
try {
|
|
2646
|
-
const content =
|
|
3244
|
+
const content = readFileSync12(configPath, "utf-8");
|
|
2647
3245
|
if (content.trim().length === 0) {
|
|
2648
3246
|
return { id: this.id, title: this.title, status: "warn", message: "config.yaml is empty (using defaults)" };
|
|
2649
3247
|
}
|
|
@@ -2677,8 +3275,8 @@ var ConfigFileCheck = class {
|
|
|
2677
3275
|
};
|
|
2678
3276
|
|
|
2679
3277
|
// src/adapters/diagnostics/checks/disk-check.ts
|
|
2680
|
-
import { existsSync as
|
|
2681
|
-
import { join as
|
|
3278
|
+
import { existsSync as existsSync18, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
3279
|
+
import { join as join20 } from "path";
|
|
2682
3280
|
var DEFAULT_THRESHOLDS = { warnMb: 100, failMb: 500 };
|
|
2683
3281
|
var DiskUsageCheck = class {
|
|
2684
3282
|
id = "disk_usage";
|
|
@@ -2688,7 +3286,7 @@ var DiskUsageCheck = class {
|
|
|
2688
3286
|
this.thresholds = thresholds;
|
|
2689
3287
|
}
|
|
2690
3288
|
async run(ctx) {
|
|
2691
|
-
if (!
|
|
3289
|
+
if (!existsSync18(ctx.ctxoRoot)) {
|
|
2692
3290
|
return { id: this.id, title: this.title, status: "pass", message: "no .ctxo/ directory", value: "0" };
|
|
2693
3291
|
}
|
|
2694
3292
|
const bytes = this.getDirSize(ctx.ctxoRoot);
|
|
@@ -2710,7 +3308,7 @@ var DiskUsageCheck = class {
|
|
|
2710
3308
|
if (entry.isFile()) {
|
|
2711
3309
|
try {
|
|
2712
3310
|
const parent = entry.parentPath ?? entry.path ?? dirPath;
|
|
2713
|
-
total += statSync2(
|
|
3311
|
+
total += statSync2(join20(parent, entry.name)).size;
|
|
2714
3312
|
} catch {
|
|
2715
3313
|
}
|
|
2716
3314
|
}
|
|
@@ -2722,10 +3320,10 @@ var DiskUsageCheck = class {
|
|
|
2722
3320
|
};
|
|
2723
3321
|
|
|
2724
3322
|
// src/cli/doctor-fix.ts
|
|
2725
|
-
import { execFileSync as
|
|
2726
|
-
import { appendFileSync, existsSync as
|
|
3323
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
3324
|
+
import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2727
3325
|
import { createRequire as createRequire6 } from "module";
|
|
2728
|
-
import { dirname as dirname7, join as
|
|
3326
|
+
import { dirname as dirname7, join as join21 } from "path";
|
|
2729
3327
|
async function applyFixes(report, options) {
|
|
2730
3328
|
if (isCI(process.env) && !options.yes && !options.dryRun) {
|
|
2731
3329
|
return {
|
|
@@ -2860,8 +3458,8 @@ async function fixIndex(projectRoot, dryRun) {
|
|
|
2860
3458
|
}
|
|
2861
3459
|
async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
2862
3460
|
if (dryRun) return;
|
|
2863
|
-
const indexDir =
|
|
2864
|
-
if (!
|
|
3461
|
+
const indexDir = join21(ctxoRoot, "index");
|
|
3462
|
+
if (!existsSync19(indexDir)) return;
|
|
2865
3463
|
const sourceFiles = listGitFiles2(projectRoot);
|
|
2866
3464
|
if (sourceFiles.size === 0) return;
|
|
2867
3465
|
const reader = new JsonIndexReader(ctxoRoot);
|
|
@@ -2869,9 +3467,9 @@ async function pruneOrphans(projectRoot, ctxoRoot, dryRun) {
|
|
|
2869
3467
|
let removed = 0;
|
|
2870
3468
|
for (const idx of indices) {
|
|
2871
3469
|
if (sourceFiles.has(idx.file)) continue;
|
|
2872
|
-
const jsonPath =
|
|
3470
|
+
const jsonPath = join21(indexDir, `${idx.file}.json`);
|
|
2873
3471
|
try {
|
|
2874
|
-
if (
|
|
3472
|
+
if (existsSync19(jsonPath)) {
|
|
2875
3473
|
unlinkSync2(jsonPath);
|
|
2876
3474
|
removed += 1;
|
|
2877
3475
|
pruneEmptyParents(jsonPath, indexDir);
|
|
@@ -2894,7 +3492,7 @@ function pruneEmptyParents(filePath, stopAt) {
|
|
|
2894
3492
|
}
|
|
2895
3493
|
function listGitFiles2(projectRoot) {
|
|
2896
3494
|
try {
|
|
2897
|
-
const out =
|
|
3495
|
+
const out = execFileSync7(
|
|
2898
3496
|
"git",
|
|
2899
3497
|
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
2900
3498
|
{ cwd: projectRoot, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
|
|
@@ -2905,9 +3503,9 @@ function listGitFiles2(projectRoot) {
|
|
|
2905
3503
|
}
|
|
2906
3504
|
}
|
|
2907
3505
|
async function fixSqliteCache(projectRoot, ctxoRoot, dryRun) {
|
|
2908
|
-
const cacheDir =
|
|
3506
|
+
const cacheDir = join21(ctxoRoot, ".cache");
|
|
2909
3507
|
if (dryRun) return;
|
|
2910
|
-
if (
|
|
3508
|
+
if (existsSync19(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
|
|
2911
3509
|
await new SyncCommand(projectRoot).run();
|
|
2912
3510
|
}
|
|
2913
3511
|
var doctorRequire = createRequire6(import.meta.url);
|
|
@@ -2924,8 +3522,8 @@ function isCI(env) {
|
|
|
2924
3522
|
}
|
|
2925
3523
|
function writeLog(ctxoRoot, attempts, halted, reason) {
|
|
2926
3524
|
try {
|
|
2927
|
-
if (!
|
|
2928
|
-
const path =
|
|
3525
|
+
if (!existsSync19(ctxoRoot)) mkdirSync6(ctxoRoot, { recursive: true });
|
|
3526
|
+
const path = join21(ctxoRoot, "doctor-fix.log");
|
|
2929
3527
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2930
3528
|
const lines = [`# ${ts} doctor --fix${halted ? " (halted: " + reason + ")" : ""}`];
|
|
2931
3529
|
for (const a of attempts) {
|
|
@@ -2958,10 +3556,10 @@ var DoctorCommand = class {
|
|
|
2958
3556
|
ctxoRoot;
|
|
2959
3557
|
constructor(projectRoot) {
|
|
2960
3558
|
this.projectRoot = projectRoot;
|
|
2961
|
-
this.ctxoRoot =
|
|
3559
|
+
this.ctxoRoot = join22(projectRoot, ".ctxo");
|
|
2962
3560
|
}
|
|
2963
3561
|
async run(options = {}) {
|
|
2964
|
-
const { getVersion: getVersion2 } = await import("./cli-router-
|
|
3562
|
+
const { getVersion: getVersion2 } = await import("./cli-router-GZA44VOV.js");
|
|
2965
3563
|
console.error(`ctxo v${getVersion2()} \u2014 health check
|
|
2966
3564
|
`);
|
|
2967
3565
|
const checks = [
|
|
@@ -2983,9 +3581,9 @@ var DoctorCommand = class {
|
|
|
2983
3581
|
new CoChangesCacheCheck(),
|
|
2984
3582
|
new SchemaVersionCheck()
|
|
2985
3583
|
];
|
|
2986
|
-
const indexDir =
|
|
2987
|
-
const indices =
|
|
2988
|
-
const checker = new HealthChecker(checks);
|
|
3584
|
+
const indexDir = join22(this.ctxoRoot, "index");
|
|
3585
|
+
const indices = existsSync20(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
|
|
3586
|
+
const checker = new HealthChecker(checks, { quiet: Boolean(options.quiet) });
|
|
2989
3587
|
const report = await checker.runAll({
|
|
2990
3588
|
projectRoot: this.projectRoot,
|
|
2991
3589
|
ctxoRoot: this.ctxoRoot,
|
|
@@ -3025,13 +3623,13 @@ var DoctorCommand = class {
|
|
|
3025
3623
|
};
|
|
3026
3624
|
|
|
3027
3625
|
// src/cli/visualize-command.ts
|
|
3028
|
-
import { readFileSync as
|
|
3029
|
-
import { join as
|
|
3626
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync5, existsSync as existsSync21 } from "fs";
|
|
3627
|
+
import { join as join23, basename } from "path";
|
|
3030
3628
|
import { execFile } from "child_process";
|
|
3031
3629
|
var VisualizeCommand = class {
|
|
3032
3630
|
constructor(projectRoot) {
|
|
3033
3631
|
this.projectRoot = projectRoot;
|
|
3034
|
-
this.ctxoRoot =
|
|
3632
|
+
this.ctxoRoot = join23(projectRoot, ".ctxo");
|
|
3035
3633
|
}
|
|
3036
3634
|
projectRoot;
|
|
3037
3635
|
ctxoRoot;
|
|
@@ -3091,10 +3689,37 @@ var VisualizeCommand = class {
|
|
|
3091
3689
|
fileLayerMap.set(file, layer);
|
|
3092
3690
|
}
|
|
3093
3691
|
}
|
|
3692
|
+
const communityWriter = new CommunitySnapshotWriter(this.ctxoRoot, {
|
|
3693
|
+
allowProductionPath: true
|
|
3694
|
+
});
|
|
3695
|
+
const currentSnapshot = communityWriter.readCurrent();
|
|
3696
|
+
const communityByIdForSymbol = /* @__PURE__ */ new Map();
|
|
3697
|
+
if (currentSnapshot) {
|
|
3698
|
+
for (const entry of currentSnapshot.communities) {
|
|
3699
|
+
communityByIdForSymbol.set(entry.symbolId, {
|
|
3700
|
+
id: entry.communityId,
|
|
3701
|
+
label: entry.communityLabel
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const architectureWriter = new ArchitectureArtifactsWriter(this.ctxoRoot, {
|
|
3706
|
+
allowProductionPath: true
|
|
3707
|
+
});
|
|
3708
|
+
const violationsArtifact = architectureWriter.readBoundaryViolations();
|
|
3709
|
+
const violationEdgeSeverity = /* @__PURE__ */ new Map();
|
|
3710
|
+
if (violationsArtifact) {
|
|
3711
|
+
for (const v of violationsArtifact.violations) {
|
|
3712
|
+
violationEdgeSeverity.set(
|
|
3713
|
+
`${v.from.symbolId}|${v.to.symbolId}`,
|
|
3714
|
+
v.severity
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3094
3718
|
const totalSymbols = graph.nodeCount;
|
|
3095
3719
|
let nodes = graph.allNodes().map((sym) => {
|
|
3096
3720
|
const file = sym.symbolId.split("::")[0] ?? "";
|
|
3097
3721
|
const dead = deadMap.get(sym.symbolId);
|
|
3722
|
+
const community = communityByIdForSymbol.get(sym.symbolId);
|
|
3098
3723
|
return {
|
|
3099
3724
|
id: sym.symbolId,
|
|
3100
3725
|
name: sym.name,
|
|
@@ -3108,7 +3733,9 @@ var VisualizeCommand = class {
|
|
|
3108
3733
|
cyclomatic: complexityMap.get(sym.symbolId),
|
|
3109
3734
|
hasAntiPattern: antiPatternFiles.has(file),
|
|
3110
3735
|
inDegree: graph.getReverseEdges(sym.symbolId).length,
|
|
3111
|
-
outDegree: graph.getForwardEdges(sym.symbolId).length
|
|
3736
|
+
outDegree: graph.getForwardEdges(sym.symbolId).length,
|
|
3737
|
+
communityId: community?.id ?? -1,
|
|
3738
|
+
communityLabel: community?.label ?? "unassigned"
|
|
3112
3739
|
};
|
|
3113
3740
|
});
|
|
3114
3741
|
if (options.maxNodes && nodes.length > options.maxNodes) {
|
|
@@ -3116,12 +3743,31 @@ var VisualizeCommand = class {
|
|
|
3116
3743
|
nodes = nodes.slice(0, options.maxNodes);
|
|
3117
3744
|
}
|
|
3118
3745
|
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) =>
|
|
3746
|
+
const edges = graph.allEdges().filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to)).map((e) => {
|
|
3747
|
+
const severity = violationEdgeSeverity.get(`${e.from}|${e.to}`);
|
|
3748
|
+
return {
|
|
3749
|
+
source: e.from,
|
|
3750
|
+
target: e.to,
|
|
3751
|
+
kind: e.kind,
|
|
3752
|
+
...severity ? { violationSeverity: severity } : {}
|
|
3753
|
+
};
|
|
3754
|
+
});
|
|
3120
3755
|
const filesInfo = indices.filter((fi) => nodeIds.has(fi.symbols[0]?.symbolId ?? "___") || fi.symbols.some((s) => nodeIds.has(s.symbolId))).map((fi) => ({
|
|
3121
3756
|
file: fi.file,
|
|
3122
3757
|
intent: fi.intent.map((i) => ({ hash: i.hash, message: i.message, date: i.date })),
|
|
3123
3758
|
antiPatterns: fi.antiPatterns.map((a) => ({ hash: a.hash, message: a.message, date: a.date }))
|
|
3124
3759
|
}));
|
|
3760
|
+
const legendCounts = /* @__PURE__ */ new Map();
|
|
3761
|
+
for (const node of nodes) {
|
|
3762
|
+
if (node.communityId < 0) continue;
|
|
3763
|
+
const entry = legendCounts.get(node.communityId);
|
|
3764
|
+
if (entry) {
|
|
3765
|
+
entry.count += 1;
|
|
3766
|
+
} else {
|
|
3767
|
+
legendCounts.set(node.communityId, { label: node.communityLabel, count: 1 });
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
const communities = [...legendCounts.entries()].map(([id, { label, count }]) => ({ id, label, memberCount: count })).sort((a, b) => b.memberCount - a.memberCount);
|
|
3125
3771
|
const payload = {
|
|
3126
3772
|
projectName: basename(this.projectRoot),
|
|
3127
3773
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
@@ -3130,13 +3776,16 @@ var VisualizeCommand = class {
|
|
|
3130
3776
|
nodes,
|
|
3131
3777
|
edges,
|
|
3132
3778
|
layers: overlayResult.layers,
|
|
3133
|
-
files: filesInfo
|
|
3779
|
+
files: filesInfo,
|
|
3780
|
+
communities,
|
|
3781
|
+
modularity: currentSnapshot?.modularity ?? 0,
|
|
3782
|
+
violationCount: violationsArtifact?.violations.length ?? 0
|
|
3134
3783
|
};
|
|
3135
3784
|
const templatePath = this.getTemplatePath();
|
|
3136
|
-
const template =
|
|
3785
|
+
const template = readFileSync13(templatePath, "utf-8");
|
|
3137
3786
|
const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
|
|
3138
|
-
const outputPath = options.output ??
|
|
3139
|
-
|
|
3787
|
+
const outputPath = options.output ?? join23(this.ctxoRoot, "visualize.html");
|
|
3788
|
+
writeFileSync5(outputPath, html, "utf-8");
|
|
3140
3789
|
console.error(`[ctxo] Wrote visualization to ${outputPath}`);
|
|
3141
3790
|
console.error(`[ctxo] ${nodes.length} symbols, ${edges.length} edges, ${deadResult.deadSymbols.length} dead`);
|
|
3142
3791
|
if (!options.noBrowser) {
|
|
@@ -3146,9 +3795,9 @@ var VisualizeCommand = class {
|
|
|
3146
3795
|
getTemplatePath() {
|
|
3147
3796
|
let dir = import.meta.dirname;
|
|
3148
3797
|
for (let i = 0; i < 10; i++) {
|
|
3149
|
-
const candidate =
|
|
3150
|
-
if (
|
|
3151
|
-
const parent =
|
|
3798
|
+
const candidate = join23(dir, "pages", "visualize-template.html");
|
|
3799
|
+
if (existsSync21(candidate)) return candidate;
|
|
3800
|
+
const parent = join23(dir, "..");
|
|
3152
3801
|
if (parent === dir) break;
|
|
3153
3802
|
dir = parent;
|
|
3154
3803
|
}
|
|
@@ -3175,16 +3824,16 @@ var VisualizeCommand = class {
|
|
|
3175
3824
|
function getVersion() {
|
|
3176
3825
|
let dir = import.meta.dirname;
|
|
3177
3826
|
for (let i = 0; i < 10; i++) {
|
|
3178
|
-
const pkg =
|
|
3179
|
-
if (
|
|
3827
|
+
const pkg = join24(dir, "package.json");
|
|
3828
|
+
if (existsSync22(pkg)) {
|
|
3180
3829
|
try {
|
|
3181
|
-
const json = JSON.parse(
|
|
3830
|
+
const json = JSON.parse(readFileSync14(pkg, "utf-8"));
|
|
3182
3831
|
return json.version ?? "unknown";
|
|
3183
3832
|
} catch {
|
|
3184
3833
|
break;
|
|
3185
3834
|
}
|
|
3186
3835
|
}
|
|
3187
|
-
const parent =
|
|
3836
|
+
const parent = join24(dir, "..");
|
|
3188
3837
|
if (parent === dir) break;
|
|
3189
3838
|
dir = parent;
|
|
3190
3839
|
}
|
|
@@ -3193,7 +3842,7 @@ function getVersion() {
|
|
|
3193
3842
|
function locateGitRoot(startDir) {
|
|
3194
3843
|
let dir = startDir;
|
|
3195
3844
|
for (let i = 0; i < 12; i++) {
|
|
3196
|
-
if (
|
|
3845
|
+
if (existsSync22(join24(dir, ".git"))) return dir;
|
|
3197
3846
|
const parent = dirname8(dir);
|
|
3198
3847
|
if (parent === dir) break;
|
|
3199
3848
|
dir = parent;
|
|
@@ -3237,6 +3886,7 @@ var CliRouter = class {
|
|
|
3237
3886
|
}
|
|
3238
3887
|
const checkArg = args.includes("--check");
|
|
3239
3888
|
const skipHistory = args.includes("--skip-history");
|
|
3889
|
+
const skipCommunity = args.includes("--skip-community");
|
|
3240
3890
|
const installMissing = args.includes("--install-missing");
|
|
3241
3891
|
const maxHistoryIdx = args.indexOf("--max-history");
|
|
3242
3892
|
const maxHistoryArg = maxHistoryIdx !== -1 ? Number(args[maxHistoryIdx + 1]) : void 0;
|
|
@@ -3249,6 +3899,7 @@ var CliRouter = class {
|
|
|
3249
3899
|
file: fileArg,
|
|
3250
3900
|
check: checkArg,
|
|
3251
3901
|
skipHistory,
|
|
3902
|
+
skipCommunity,
|
|
3252
3903
|
maxHistory: maxHistoryArg,
|
|
3253
3904
|
installMissing
|
|
3254
3905
|
});
|
|
@@ -3372,6 +4023,7 @@ ctxo v${v} \u2014 MCP server for dependency-aware codebase context
|
|
|
3372
4023
|
Usage:
|
|
3373
4024
|
ctxo Start MCP server (stdio transport)
|
|
3374
4025
|
ctxo index Build full codebase index (--max-history N, default 20)
|
|
4026
|
+
ctxo index --skip-community Skip Louvain community detection for faster indexing
|
|
3375
4027
|
ctxo sync Rebuild SQLite cache from committed JSON index
|
|
3376
4028
|
ctxo watch Start file watcher for incremental re-indexing
|
|
3377
4029
|
ctxo verify-index CI gate: fail if index is stale
|
|
@@ -3403,4 +4055,4 @@ export {
|
|
|
3403
4055
|
getVersion,
|
|
3404
4056
|
locateGitRoot
|
|
3405
4057
|
};
|
|
3406
|
-
//# sourceMappingURL=cli-router-
|
|
4058
|
+
//# sourceMappingURL=cli-router-GZA44VOV.js.map
|