@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.
@@ -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
- } from "./chunk-4M4V5V36.js";
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-WXA2JJLH.js";
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 readFileSync13, existsSync as existsSync21 } from "fs";
28
- import { dirname as dirname8, join as join23 } from "path";
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 readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync, readdirSync as readdirSync2 } from "fs";
33
- import { join as join7, relative, extname as extname3 } from "path";
34
- import { createRequire as createRequire2 } from "module";
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
- async function tryLoad(specifier, baseDir) {
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 = createRequire(import.meta.url);
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((resolve3, reject) => {
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) => resolve3(code ?? 0));
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 { writeFileSync, renameSync, mkdirSync, unlinkSync, existsSync as existsSync6 } from "fs";
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
- const tmpPath = `${targetPath}.${process.pid}.tmp`;
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 as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
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
- writeFileSync2(this.versionFilePath, CURRENT_SCHEMA_VERSION, "utf-8");
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 = createRequire2(import.meta.url);
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 ?? join7(projectRoot, ".ctxo");
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 = join7(this.projectRoot, options.file);
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 = readFileSync5(filePath, "utf-8");
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) => join7(root, line)).filter((abs) => {
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 = join7(this.projectRoot, "package.json");
941
- if (!existsSync8(pkgPath)) return [this.projectRoot];
1359
+ const pkgPath = join8(this.projectRoot, "package.json");
1360
+ if (!existsSync9(pkgPath)) return [this.projectRoot];
942
1361
  try {
943
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
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 = join7(this.projectRoot, ws.slice(0, -2));
950
- if (existsSync8(parentDir)) {
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(join7(parentDir, entry.name));
1372
+ resolved.push(join8(parentDir, entry.name));
954
1373
  }
955
1374
  }
956
1375
  }
957
1376
  } else {
958
- const wsPath = join7(this.projectRoot, ws);
959
- if (existsSync8(wsPath)) {
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-LME4EDA5.js")).JsonIndexReader(this.ctxoRoot);
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 (!existsSync8(filePath)) {
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 = readFileSync5(filePath, "utf-8");
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 = join7(this.projectRoot, ".gitignore");
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 = existsSync8(gitignorePath) ? readFileSync5(gitignorePath, "utf-8") : "";
1456
+ const existing = existsSync9(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
1034
1457
  if (existing.includes(cachePattern)) return;
1035
- writeFileSync3(gitignorePath, existing + suffix, "utf-8");
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 join8 } from "path";
1601
+ import { join as join9 } from "path";
1042
1602
  var SyncCommand = class {
1043
1603
  ctxoRoot;
1044
1604
  constructor(projectRoot) {
1045
- this.ctxoRoot = join8(projectRoot, ".ctxo");
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 join9 } from "path";
1061
- import { existsSync as existsSync9 } from "fs";
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 = join9(projectRoot, ".ctxo");
1628
+ this.ctxoRoot = join10(projectRoot, ".ctxo");
1069
1629
  }
1070
1630
  run() {
1071
- const indexDir = join9(this.ctxoRoot, "index");
1072
- if (!existsSync9(indexDir)) {
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 = existsSync9(join9(this.ctxoRoot, ".cache", "symbols.db"));
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 join10 } from "path";
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(join10(tmpdir(), "ctxo-verify-"));
1685
+ const tempDir = mkdtempSync(join11(tmpdir2(), "ctxo-verify-"));
1126
1686
  try {
1127
- const tempCtxo = join10(tempDir, ".ctxo");
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(join10(this.projectRoot, ".ctxo"));
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 join12 } from "path";
1164
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, chmodSync } from "fs";
1165
- import { createRequire as createRequire3 } from "module";
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 existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
1169
- import { join as join11, dirname as dirname6 } from "path";
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) => existsSync10(join11(projectRoot, 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 = join11(projectRoot, platform.file);
1804
+ const filePath = join12(projectRoot, platform.file);
1245
1805
  const content = generateRules(platformId);
1246
1806
  const dir = dirname6(filePath);
1247
- if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
1807
+ if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
1248
1808
  if (platform.mode === "create") {
1249
- writeFileSync4(filePath, content, "utf-8");
1250
- return { file: platform.file, action: existsSync10(filePath) ? "updated" : "created" };
1809
+ writeFileSync3(filePath, content, "utf-8");
1810
+ return { file: platform.file, action: existsSync11(filePath) ? "updated" : "created" };
1251
1811
  }
1252
- if (!existsSync10(filePath)) {
1253
- writeFileSync4(filePath, content, "utf-8");
1812
+ if (!existsSync11(filePath)) {
1813
+ writeFileSync3(filePath, content, "utf-8");
1254
1814
  return { file: platform.file, action: "created" };
1255
1815
  }
1256
- const existing = readFileSync6(filePath, "utf-8");
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
- writeFileSync4(filePath, updated2, "utf-8");
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
- writeFileSync4(filePath, updated, "utf-8");
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 = join11(projectRoot, ".gitignore");
1277
- if (!existsSync10(filePath)) {
1278
- writeFileSync4(filePath, `# Ctxo local cache (auto-rebuilt, never committed)
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 = readFileSync6(filePath, "utf-8");
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
- writeFileSync4(filePath, updated, "utf-8");
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 = join11(projectRoot, ".ctxo", "config.yaml");
1874
+ const filePath = join12(projectRoot, ".ctxo", "config.yaml");
1315
1875
  const dir = dirname6(filePath);
1316
- if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
1317
- if (existsSync10(filePath)) {
1876
+ if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
1877
+ if (existsSync11(filePath)) {
1318
1878
  return { file: ".ctxo/config.yaml", action: "skipped" };
1319
1879
  }
1320
- writeFileSync4(filePath, DEFAULT_CONFIG, "utf-8");
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 = join11(projectRoot, target.file);
1906
+ const filePath = join12(projectRoot, target.file);
1347
1907
  const dir = dirname6(filePath);
1348
- if (!existsSync10(dir)) mkdirSync3(dir, { recursive: true });
1908
+ if (!existsSync11(dir)) mkdirSync4(dir, { recursive: true });
1349
1909
  const entry = target.extraFields ? { ...target.extraFields, ...CTXO_MCP_ENTRY } : { ...CTXO_MCP_ENTRY };
1350
- if (!existsSync10(filePath)) {
1910
+ if (!existsSync11(filePath)) {
1351
1911
  const config = { [target.serverKey]: { ctxo: entry } };
1352
- writeFileSync4(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
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(readFileSync6(filePath, "utf-8"));
1917
+ existing = JSON.parse(readFileSync7(filePath, "utf-8"));
1358
1918
  } catch {
1359
1919
  const config = { [target.serverKey]: { ctxo: entry } };
1360
- writeFileSync4(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
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
- writeFileSync4(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
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 = createRequire3(import.meta.url);
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 = join12(this.projectRoot, ".git", "hooks");
1467
- mkdirSync4(hooksDir, { recursive: true });
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 (!existsSync11(join12(this.projectRoot, ".git"))) {
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 = join12(this.projectRoot, indexDir);
1510
- if (!existsSync11(fullPath)) mkdirSync4(fullPath, { recursive: true });
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(stepNum, totalSteps, `Git Hooks ${pc.yellow("recommended")}`, "Auto-updates index on every commit and pull.", pc));
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 = join12(this.projectRoot, ".ctxo", "index");
1654
- if (!existsSync11(indexDir)) mkdirSync4(indexDir, { recursive: true });
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 = existsSync11(join12(this.projectRoot, platform.file));
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 = join12(this.projectRoot, "node_modules", "@ctxo", "cli", "package.json");
1722
- if (existsSync11(pkgPath)) {
1723
- return JSON.parse(readFileSync7(pkgPath, "utf-8")).version;
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: createRequire7 } = await import("module");
1726
- const require4 = createRequire7(import.meta.url);
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 = join12(hooksDir, hookName);
2307
+ const hookPath = join13(hooksDir, hookName);
1734
2308
  let existing = "";
1735
- if (existsSync11(hookPath)) {
1736
- existing = readFileSync7(hookPath, "utf-8");
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
- writeFileSync5(hookPath, updated, "utf-8");
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 join13, extname as extname4, relative as relative2 } from "path";
1749
- import { readFileSync as readFileSync8 } from "fs";
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 = join13(projectRoot, ".ctxo");
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 = readFileSync8(filePath, "utf-8");
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 = readFileSync8(filePath, "utf-8");
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 join14 } from "path";
1955
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
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 = join14(projectRoot, ".ctxo");
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 = join14(this.ctxoRoot, ".cache", "symbols.db");
1968
- if (!existsSync12(dbPath)) {
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 = readFileSync9(dbPath);
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: writeFileSync7 } = await import("fs");
1985
- writeFileSync7(dbPath, Buffer.from(data));
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 = join14(this.ctxoRoot, "config.yaml");
2016
- if (!existsSync12(configPath)) return true;
2648
+ const configPath = join15(this.ctxoRoot, "config.yaml");
2649
+ if (!existsSync13(configPath)) return true;
2017
2650
  try {
2018
- const raw = readFileSync9(configPath, "utf-8");
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 join21 } from "path";
2108
- import { existsSync as existsSync19 } from "fs";
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 log3 = createLogger("ctxo:doctor");
2744
+ var log4 = createLogger("ctxo:doctor");
2112
2745
  var HealthChecker = class {
2113
2746
  checks;
2114
- constructor(checks) {
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
- log3.info(`${r.id}: ${r.status.toUpperCase()} (${r.message})`);
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
- log3.error(`${check.id}: FAIL (${message})`);
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 createRequire4 } from "module";
2197
- var require2 = createRequire4(import.meta.url);
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 createRequire5 } from "module";
2389
- var require3 = createRequire5(import.meta.url);
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 execFileSync4 } from "child_process";
2441
- import { existsSync as existsSync13 } from "fs";
2442
- import { join as join15 } from "path";
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 = execFileSync4("git", ["--version"], { encoding: "utf-8" }).trim();
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 (existsSync13(join15(ctx.projectRoot, ".git"))) {
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 existsSync14 } from "fs";
2468
- import { execFileSync as execFileSync5 } from "child_process";
2469
- import { join as join16 } from "path";
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 = join16(ctx.ctxoRoot, "index");
2487
- if (!existsSync14(indexDir)) {
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 = execFileSync5("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
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 = join16(ctx.ctxoRoot, "index", "co-changes.json");
2582
- if (existsSync14(coChangesPath)) {
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 existsSync15, readFileSync as readFileSync10 } from "fs";
2607
- import { join as join17 } from "path";
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 = join17(ctx.ctxoRoot, ".cache", "symbols.db");
2613
- if (!existsSync15(dbPath)) {
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 = readFileSync10(dbPath);
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 existsSync16, readFileSync as readFileSync11 } from "fs";
2636
- import { join as join18 } from "path";
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 = join18(ctx.ctxoRoot, "config.yaml");
2642
- if (!existsSync16(configPath)) {
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 = readFileSync11(configPath, "utf-8");
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 existsSync17, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
2681
- import { join as join19 } from "path";
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 (!existsSync17(ctx.ctxoRoot)) {
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(join19(parent, entry.name)).size;
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 execFileSync6 } from "child_process";
2726
- import { appendFileSync, existsSync as existsSync18, mkdirSync as mkdirSync5, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "fs";
2727
- import { createRequire as createRequire6 } from "module";
2728
- import { dirname as dirname7, join as join20 } from "path";
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 = join20(ctxoRoot, "index");
2864
- if (!existsSync18(indexDir)) return;
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 = join20(indexDir, `${idx.file}.json`);
3509
+ const jsonPath = join21(indexDir, `${idx.file}.json`);
2873
3510
  try {
2874
- if (existsSync18(jsonPath)) {
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 = execFileSync6(
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 = join20(ctxoRoot, ".cache");
3545
+ const cacheDir = join21(ctxoRoot, ".cache");
2909
3546
  if (dryRun) return;
2910
- if (existsSync18(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
3547
+ if (existsSync19(cacheDir)) rmSync2(cacheDir, { recursive: true, force: true });
2911
3548
  await new SyncCommand(projectRoot).run();
2912
3549
  }
2913
- var doctorRequire = createRequire6(import.meta.url);
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 (!existsSync18(ctxoRoot)) mkdirSync5(ctxoRoot, { recursive: true });
2928
- const path = join20(ctxoRoot, "doctor-fix.log");
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 = join21(projectRoot, ".ctxo");
3598
+ this.ctxoRoot = join22(projectRoot, ".ctxo");
2962
3599
  }
2963
3600
  async run(options = {}) {
2964
- const { getVersion: getVersion2 } = await import("./cli-router-T3VZZJD6.js");
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 = join21(this.ctxoRoot, "index");
2987
- const indices = existsSync19(indexDir) ? new JsonIndexReader(this.ctxoRoot).readAll() : [];
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 readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync20 } from "fs";
3029
- import { join as join22, basename } from "path";
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 = join22(projectRoot, ".ctxo");
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) => ({ source: e.from, target: e.to, kind: e.kind }));
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 = readFileSync12(templatePath, "utf-8");
3824
+ const template = readFileSync13(templatePath, "utf-8");
3137
3825
  const html = template.replace("/*__CTXO_DATA__*/null", JSON.stringify(payload));
3138
- const outputPath = options.output ?? join22(this.ctxoRoot, "visualize.html");
3139
- writeFileSync6(outputPath, html, "utf-8");
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 = join22(dir, "pages", "visualize-template.html");
3150
- if (existsSync20(candidate)) return candidate;
3151
- const parent = join22(dir, "..");
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 = join23(dir, "package.json");
3179
- if (existsSync21(pkg)) {
3866
+ const pkg = join24(dir, "package.json");
3867
+ if (existsSync22(pkg)) {
3180
3868
  try {
3181
- const json = JSON.parse(readFileSync13(pkg, "utf-8"));
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 = join23(dir, "..");
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 (existsSync21(join23(dir, ".git"))) return dir;
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-T3VZZJD6.js.map
4097
+ //# sourceMappingURL=cli-router-HEZER4G7.js.map