@ctxo/cli 0.7.1 → 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/index.js CHANGED
@@ -1,23 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ArchitecturalOverlay,
4
+ BoundaryViolationDetector,
4
5
  DeadCodeDetector,
6
+ DriftDetector,
5
7
  PageRankCalculator,
6
8
  RevertDetector,
7
9
  SessionRecorderAdapter,
8
10
  SimpleGitAdapter,
9
11
  SqliteStorageAdapter,
10
12
  SymbolGraph,
11
- createLogger,
12
13
  detectWorkspace,
13
14
  loadCoChangeMap,
14
15
  loadConfig,
16
+ makeGlobMatcher,
17
+ maskingClusterLabelPatterns,
15
18
  statsEnabled
16
- } from "./chunk-4M4V5V36.js";
19
+ } from "./chunk-HXEPS2XY.js";
20
+ import {
21
+ createLogger
22
+ } from "./chunk-6EKERTJ7.js";
17
23
  import {
18
24
  DetailLevelSchema,
19
25
  JsonIndexReader
20
- } from "./chunk-WXA2JJLH.js";
26
+ } from "./chunk-2TVHKCCU.js";
21
27
 
22
28
  // src/index.ts
23
29
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -301,7 +307,9 @@ var TRUNCATABLE_FIELDS = {
301
307
  hierarchies: "Use get_class_hierarchy with a specific symbolId.",
302
308
  scaffolding: "Review the scaffolding markers directly in the listed files.",
303
309
  deadFiles: "Use find_dead_code with includeTests to adjust scope.",
304
- files: "Use get_changed_symbols with a narrower since ref or smaller maxFiles."
310
+ files: "Use get_changed_symbols with a narrower since ref or smaller maxFiles.",
311
+ clusters: "Use get_architectural_overlay with mode=communities and layer filter for a focused view.",
312
+ clustersAffected: "Use get_blast_radius on individual changed symbols for per-cluster impact."
305
313
  };
306
314
  function getThreshold() {
307
315
  const env = process.env["CTXO_RESPONSE_LIMIT"];
@@ -322,10 +330,14 @@ function findTruncatableArray(data) {
322
330
  }
323
331
  return best;
324
332
  }
325
- function buildMeta(base) {
326
- return workspaceMeta ? { ...base, workspace: workspaceMeta } : base;
333
+ function buildMeta(base, extras) {
334
+ const meta = workspaceMeta ? { ...base, workspace: workspaceMeta } : base;
335
+ if (extras?.snapshotStaleness) {
336
+ return { ...meta, snapshotStaleness: extras.snapshotStaleness };
337
+ }
338
+ return meta;
327
339
  }
328
- function wrapResponse(data) {
340
+ function wrapResponse(data, extras) {
329
341
  const threshold = getThreshold();
330
342
  const fullJson = JSON.stringify(data);
331
343
  const totalBytes = Buffer.byteLength(fullJson, "utf-8");
@@ -334,23 +346,29 @@ function wrapResponse(data) {
334
346
  if (totalBytes <= threshold) {
335
347
  return {
336
348
  ...data,
337
- _meta: buildMeta({
338
- totalItems,
339
- returnedItems: totalItems,
340
- truncated: false,
341
- totalBytes
342
- })
349
+ _meta: buildMeta(
350
+ {
351
+ totalItems,
352
+ returnedItems: totalItems,
353
+ truncated: false,
354
+ totalBytes
355
+ },
356
+ extras
357
+ )
343
358
  };
344
359
  }
345
360
  if (!truncatable || truncatable.arr.length <= 1) {
346
361
  return {
347
362
  ...data,
348
- _meta: buildMeta({
349
- totalItems,
350
- returnedItems: totalItems,
351
- truncated: false,
352
- totalBytes
353
- })
363
+ _meta: buildMeta(
364
+ {
365
+ totalItems,
366
+ returnedItems: totalItems,
367
+ truncated: false,
368
+ totalBytes
369
+ },
370
+ extras
371
+ )
354
372
  };
355
373
  }
356
374
  let lo = 1;
@@ -360,13 +378,16 @@ function wrapResponse(data) {
360
378
  const trial = {
361
379
  ...data,
362
380
  [truncatable.key]: truncatable.arr.slice(0, mid),
363
- _meta: buildMeta({
364
- totalItems,
365
- returnedItems: mid,
366
- truncated: true,
367
- totalBytes,
368
- hint: TRUNCATABLE_FIELDS[truncatable.key]
369
- })
381
+ _meta: buildMeta(
382
+ {
383
+ totalItems,
384
+ returnedItems: mid,
385
+ truncated: true,
386
+ totalBytes,
387
+ hint: TRUNCATABLE_FIELDS[truncatable.key]
388
+ },
389
+ extras
390
+ )
370
391
  };
371
392
  const size = Buffer.byteLength(JSON.stringify(trial), "utf-8");
372
393
  if (size <= threshold) {
@@ -379,13 +400,16 @@ function wrapResponse(data) {
379
400
  return {
380
401
  ...data,
381
402
  [truncatable.key]: truncatable.arr.slice(0, lo),
382
- _meta: buildMeta({
383
- totalItems,
384
- returnedItems: lo,
385
- truncated: true,
386
- totalBytes,
387
- hint
388
- })
403
+ _meta: buildMeta(
404
+ {
405
+ totalItems,
406
+ returnedItems: lo,
407
+ truncated: true,
408
+ totalBytes,
409
+ hint
410
+ },
411
+ extras
412
+ )
389
413
  };
390
414
  }
391
415
 
@@ -517,13 +541,54 @@ function handleGetLogicSlice(storage, masking, staleness, ctxoRoot = ".ctxo") {
517
541
 
518
542
  // src/adapters/mcp/get-why-context.ts
519
543
  import { z as z2 } from "zod";
544
+
545
+ // src/core/overlay/snapshot-staleness.ts
546
+ async function buildSnapshotStaleness(storage, git) {
547
+ const snapshot = storage.readCommunities();
548
+ if (!snapshot || !snapshot.commitSha || snapshot.commitSha === "nocommit") return void 0;
549
+ const getHead = git.getHeadSha?.bind(git);
550
+ const countBetween = git.countCommitsBetween?.bind(git);
551
+ const head = getHead ? await getHead() : void 0;
552
+ if (!head) {
553
+ return { snapshotCommit: snapshot.commitSha };
554
+ }
555
+ if (head === snapshot.commitSha) {
556
+ return {
557
+ snapshotCommit: snapshot.commitSha,
558
+ currentHeadCommit: head,
559
+ commitsBehind: 0
560
+ };
561
+ }
562
+ const behind = countBetween ? await countBetween(snapshot.commitSha) : void 0;
563
+ const result = {
564
+ snapshotCommit: snapshot.commitSha,
565
+ currentHeadCommit: head,
566
+ ...typeof behind === "number" ? { commitsBehind: behind } : {}
567
+ };
568
+ if (typeof behind === "number" && behind > 0) {
569
+ return {
570
+ ...result,
571
+ hint: `Community snapshot is ${behind} commit${behind === 1 ? "" : "s"} behind HEAD. Run \`ctxo index\` to refresh cluster / drift / boundary data.`
572
+ };
573
+ }
574
+ if (behind === void 0) {
575
+ return {
576
+ ...result,
577
+ hint: "Snapshot commit is not reachable from HEAD \u2014 it may have been rebased away. Run `ctxo index` to re-anchor."
578
+ };
579
+ }
580
+ return result;
581
+ }
582
+
583
+ // src/adapters/mcp/get-why-context.ts
520
584
  var InputSchema2 = z2.object({
521
585
  symbolId: z2.string().min(1),
522
586
  maxCommits: z2.number().int().min(1).optional()
523
587
  });
524
- function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
588
+ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo", clusterLabelMasker) {
525
589
  const revertDetector = new RevertDetector();
526
590
  const indexReader = new JsonIndexReader(ctxoRoot);
591
+ const driftDetector = new DriftDetector();
527
592
  return async (args) => {
528
593
  try {
529
594
  const parsed = InputSchema2.safeParse(args);
@@ -565,6 +630,26 @@ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo
565
630
  if (antiPatterns.length > 0) {
566
631
  responsePayload.warningBadge = "\u26A0 Anti-pattern detected";
567
632
  }
633
+ const rawSnapshot = storage.readCommunities();
634
+ const currentSnapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
635
+ if (currentSnapshot) {
636
+ const history = storage.listCommunityHistory();
637
+ const drift = driftDetector.detect(currentSnapshot, history);
638
+ const relevant = drift.events.filter((event) => event.symbolId === symbolId);
639
+ if (relevant.length > 0 || drift.hint) {
640
+ responsePayload.driftSignals = {
641
+ events: relevant,
642
+ confidence: drift.confidence,
643
+ snapshotsAvailable: drift.snapshotsAvailable,
644
+ stabilitySummary: drift.stability,
645
+ ...drift.hint ? { hint: drift.hint } : {}
646
+ };
647
+ }
648
+ }
649
+ const stalenessMeta = await buildSnapshotStaleness(storage, git);
650
+ if (stalenessMeta) {
651
+ responsePayload._meta = { snapshotStaleness: stalenessMeta };
652
+ }
568
653
  const payload = masking.mask(JSON.stringify(responsePayload));
569
654
  const content = [];
570
655
  if (staleness) {
@@ -763,15 +848,35 @@ var BlastRadiusCalculator = class {
763
848
  };
764
849
 
765
850
  // src/adapters/mcp/get-blast-radius.ts
851
+ function computeClusterBreakdown(snapshot, rootSymbolId, impactedSymbolIds) {
852
+ if (impactedSymbolIds.length === 0) return void 0;
853
+ const assignment = /* @__PURE__ */ new Map();
854
+ for (const entry of snapshot.communities) {
855
+ assignment.set(entry.symbolId, { id: entry.communityId, label: entry.communityLabel });
856
+ }
857
+ const rootCluster = assignment.get(rootSymbolId);
858
+ const byCluster = {};
859
+ let crossClusterCount = 0;
860
+ for (const symbolId of impactedSymbolIds) {
861
+ const cluster = assignment.get(symbolId);
862
+ if (!cluster) continue;
863
+ const label = cluster.label || `community-${cluster.id}`;
864
+ byCluster[label] = (byCluster[label] ?? 0) + 1;
865
+ if (rootCluster && cluster.id !== rootCluster.id) crossClusterCount++;
866
+ }
867
+ const clusterCount = Object.keys(byCluster).length;
868
+ const multiClusterHint = clusterCount >= 3 ? `Change impacts ${clusterCount} clusters \u2014 multi-team review recommended.` : void 0;
869
+ return { byCluster, crossClusterCount, multiClusterHint };
870
+ }
766
871
  var InputSchema4 = z4.object({
767
872
  symbolId: z4.string().min(1),
768
873
  confidence: z4.enum(["confirmed", "likely", "potential"]).optional(),
769
874
  intent: z4.string().optional().describe('Filter results by intent keywords (e.g., "test", "adapter", "security")')
770
875
  });
771
- function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
876
+ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo", git, clusterLabelMasker) {
772
877
  const calculator = new BlastRadiusCalculator();
773
878
  const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
774
- return (args) => {
879
+ return async (args) => {
775
880
  try {
776
881
  const parsed = InputSchema4.safeParse(args);
777
882
  if (!parsed.success) {
@@ -795,7 +900,10 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
795
900
  const confirmedCount = symbols.filter((s) => s.confidence === "confirmed").length;
796
901
  const likelyCount = symbols.filter((s) => s.confidence === "likely").length;
797
902
  const potentialCount = symbols.filter((s) => s.confidence === "potential").length;
798
- const payload = masking.mask(JSON.stringify(wrapResponse({
903
+ const rawSnapshot = storage.readCommunities();
904
+ const snapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
905
+ const clusterBreakdown = snapshot ? computeClusterBreakdown(snapshot, symbolId, symbols.map((s) => s.symbolId)) : void 0;
906
+ const body = {
799
907
  symbolId,
800
908
  impactScore: symbols.length,
801
909
  directDependentsCount: result.directDependentsCount,
@@ -804,7 +912,17 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
804
912
  potentialCount,
805
913
  overallRiskScore: result.overallRiskScore,
806
914
  impactedSymbols: symbols
807
- })));
915
+ };
916
+ if (clusterBreakdown) {
917
+ body.byCluster = clusterBreakdown.byCluster;
918
+ body.crossClusterEdges = clusterBreakdown.crossClusterCount;
919
+ if (clusterBreakdown.multiClusterHint) {
920
+ body.multiClusterHint = clusterBreakdown.multiClusterHint;
921
+ }
922
+ }
923
+ const stalenessMeta = git ? await buildSnapshotStaleness(storage, git) : void 0;
924
+ const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
925
+ const payload = masking.mask(JSON.stringify(wrapResponse(body, extras)));
808
926
  const content = [];
809
927
  if (staleness) {
810
928
  const warning = staleness.check(indexedFiles);
@@ -822,12 +940,57 @@ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
822
940
 
823
941
  // src/adapters/mcp/get-architectural-overlay.ts
824
942
  import { z as z5 } from "zod";
943
+ var OVERLAY_MODES = ["regex", "communities", "both"];
825
944
  var InputSchema5 = z5.object({
826
- layer: z5.string().optional()
945
+ layer: z5.string().optional(),
946
+ mode: z5.enum(OVERLAY_MODES).optional()
827
947
  });
828
- function handleGetArchitecturalOverlay(storage, masking, staleness) {
948
+ function buildCommunityOverlay(snapshot, options = {}) {
949
+ const previewLimit = options.memberPreviewLimit ?? 15;
950
+ const byCluster = /* @__PURE__ */ new Map();
951
+ for (const entry of snapshot.communities) {
952
+ const list = byCluster.get(entry.communityId) ?? [];
953
+ list.push(entry.symbolId);
954
+ byCluster.set(entry.communityId, list);
955
+ }
956
+ const labelFor = /* @__PURE__ */ new Map();
957
+ for (const entry of snapshot.communities) {
958
+ if (!labelFor.has(entry.communityId)) {
959
+ labelFor.set(entry.communityId, entry.communityLabel);
960
+ }
961
+ }
962
+ const godNodesByCommunity = /* @__PURE__ */ new Map();
963
+ for (const god of snapshot.godNodes) {
964
+ for (const communityId of god.bridgedCommunities) {
965
+ const set = godNodesByCommunity.get(communityId) ?? /* @__PURE__ */ new Set();
966
+ set.add(god.symbolId);
967
+ godNodesByCommunity.set(communityId, set);
968
+ }
969
+ }
970
+ const clusters = [];
971
+ for (const [id, members] of byCluster) {
972
+ members.sort();
973
+ clusters.push({
974
+ id,
975
+ label: labelFor.get(id) ?? `community-${id}`,
976
+ memberCount: members.length,
977
+ members: members.slice(0, previewLimit),
978
+ godNodes: [...godNodesByCommunity.get(id) ?? []].sort()
979
+ });
980
+ }
981
+ clusters.sort((a, b) => b.memberCount - a.memberCount || a.id - b.id);
982
+ return {
983
+ clusters,
984
+ modularity: snapshot.modularity,
985
+ edgeQuality: snapshot.edgeQuality,
986
+ crossClusterEdges: snapshot.crossClusterEdges,
987
+ computedAt: snapshot.computedAt,
988
+ commitSha: snapshot.commitSha
989
+ };
990
+ }
991
+ function handleGetArchitecturalOverlay(storage, masking, staleness, git, clusterLabelMasker) {
829
992
  const overlay = new ArchitecturalOverlay();
830
- return (args) => {
993
+ return async (args) => {
831
994
  try {
832
995
  const parsed = InputSchema5.safeParse(args);
833
996
  if (!parsed.success) {
@@ -837,6 +1000,7 @@ function handleGetArchitecturalOverlay(storage, masking, staleness) {
837
1000
  }
838
1001
  const files = storage.listIndexedFiles();
839
1002
  const result = overlay.classify(files);
1003
+ const mode = parsed.data.mode ?? "both";
840
1004
  const buildContent = (payloadStr) => {
841
1005
  const content = [];
842
1006
  if (staleness) {
@@ -846,12 +1010,25 @@ function handleGetArchitecturalOverlay(storage, masking, staleness) {
846
1010
  content.push({ type: "text", text: payloadStr });
847
1011
  return content;
848
1012
  };
1013
+ const stalenessMeta = git ? await buildSnapshotStaleness(storage, git) : void 0;
1014
+ const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
849
1015
  if (parsed.data.layer) {
850
1016
  const filtered = result.layers[parsed.data.layer];
851
- const payload2 = masking.mask(JSON.stringify({ layer: parsed.data.layer, files: filtered ?? [] }));
1017
+ const payload2 = masking.mask(
1018
+ JSON.stringify(wrapResponse({ layer: parsed.data.layer, files: filtered ?? [] }, extras))
1019
+ );
852
1020
  return { content: buildContent(payload2) };
853
1021
  }
854
- const payload = masking.mask(JSON.stringify(result));
1022
+ const rawSnapshot = mode === "regex" ? void 0 : storage.readCommunities();
1023
+ const snapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
1024
+ const community = snapshot ? buildCommunityOverlay(snapshot) : void 0;
1025
+ const body = {};
1026
+ if (mode !== "communities") body.layers = result.layers;
1027
+ if (community) body.communities = community;
1028
+ if (mode === "communities" && !community) {
1029
+ body.hint = "No community snapshot available. Run `ctxo index` to generate one.";
1030
+ }
1031
+ const payload = masking.mask(JSON.stringify(wrapResponse(body, extras)));
855
1032
  return { content: buildContent(payload) };
856
1033
  } catch (err) {
857
1034
  return {
@@ -2294,7 +2471,20 @@ function loadCoChanges(ctxoRoot) {
2294
2471
  return void 0;
2295
2472
  }
2296
2473
  }
2297
- function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
2474
+ function computeClustersAffected(snapshot, changedSymbolIds) {
2475
+ const totals = /* @__PURE__ */ new Map();
2476
+ for (const entry of snapshot.communities) {
2477
+ if (!changedSymbolIds.has(entry.symbolId)) continue;
2478
+ const existing = totals.get(entry.communityId);
2479
+ if (existing) {
2480
+ existing.count += 1;
2481
+ } else {
2482
+ totals.set(entry.communityId, { label: entry.communityLabel, count: 1 });
2483
+ }
2484
+ }
2485
+ return [...totals.entries()].map(([id, { label, count }]) => ({ id, label, symbolCount: count })).sort((a, b) => b.symbolCount - a.symbolCount || a.id - b.id);
2486
+ }
2487
+ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo", clusterLabelMasker) {
2298
2488
  const calculator = new BlastRadiusCalculator();
2299
2489
  const getGraph = () => getGraphAndFiles(ctxoRoot, storage);
2300
2490
  return async (args) => {
@@ -2387,7 +2577,30 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo")
2387
2577
  }, 0);
2388
2578
  const riskLevel = maxRisk > 0.7 ? "high" : maxRisk > 0.3 ? "medium" : "low";
2389
2579
  const changedSymbols = files.reduce((sum, f) => sum + f.symbols.length, 0);
2390
- const payload = masking.mask(JSON.stringify(wrapResponse({
2580
+ const rawSnapshot = storage.readCommunities();
2581
+ const snapshot = rawSnapshot && clusterLabelMasker?.isEnabled() ? clusterLabelMasker.maskSnapshot(rawSnapshot) : rawSnapshot;
2582
+ const history = snapshot ? storage.listCommunityHistory() : [];
2583
+ let boundarySection;
2584
+ let clustersAffected;
2585
+ if (snapshot) {
2586
+ const detector = new BoundaryViolationDetector();
2587
+ const violationResult = detector.detect(graph, snapshot, history);
2588
+ const changedSymbolIds = /* @__PURE__ */ new Set();
2589
+ for (const file of files) {
2590
+ for (const sym of file.symbols) changedSymbolIds.add(sym.symbolId);
2591
+ }
2592
+ const relevant = violationResult.violations.filter(
2593
+ (v) => changedSymbolIds.has(v.from.symbolId) || changedSymbolIds.has(v.to.symbolId)
2594
+ );
2595
+ boundarySection = {
2596
+ violations: relevant,
2597
+ confidence: violationResult.confidence,
2598
+ snapshotsAvailable: violationResult.snapshotsAvailable,
2599
+ ...violationResult.hint ? { hint: violationResult.hint } : {}
2600
+ };
2601
+ clustersAffected = computeClustersAffected(snapshot, changedSymbolIds);
2602
+ }
2603
+ const body = {
2391
2604
  since,
2392
2605
  changedFiles: files.length,
2393
2606
  changedSymbols,
@@ -2400,7 +2613,12 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo")
2400
2613
  potentialTotal,
2401
2614
  highRiskSymbols: highRiskSymbols.slice(0, 10)
2402
2615
  }
2403
- })));
2616
+ };
2617
+ if (boundarySection) body.boundaryViolations = boundarySection;
2618
+ if (clustersAffected && clustersAffected.length > 0) body.clustersAffected = clustersAffected;
2619
+ const stalenessMeta = await buildSnapshotStaleness(storage, git);
2620
+ const extras = stalenessMeta ? { snapshotStaleness: stalenessMeta } : void 0;
2621
+ const payload = masking.mask(JSON.stringify(wrapResponse(body, extras)));
2404
2622
  const content = [];
2405
2623
  if (staleness) {
2406
2624
  const warning = staleness.check(indexedFiles);
@@ -2462,6 +2680,40 @@ function recordEvent(toolName, args, result, latencyMs, recorder) {
2462
2680
  }
2463
2681
  }
2464
2682
 
2683
+ // src/core/overlay/cluster-label-masker.ts
2684
+ var ClusterLabelMasker = class {
2685
+ matcher;
2686
+ enabled;
2687
+ labelCache = /* @__PURE__ */ new Map();
2688
+ constructor(patterns) {
2689
+ this.enabled = patterns.length > 0;
2690
+ this.matcher = this.enabled ? makeGlobMatcher([...patterns]) : () => false;
2691
+ }
2692
+ isEnabled() {
2693
+ return this.enabled;
2694
+ }
2695
+ mask(label) {
2696
+ if (!this.enabled) return label;
2697
+ if (!this.matcher(label)) return label;
2698
+ const cached = this.labelCache.get(label);
2699
+ if (cached) return cached;
2700
+ const masked = `[masked-cluster-${this.labelCache.size + 1}]`;
2701
+ this.labelCache.set(label, masked);
2702
+ return masked;
2703
+ }
2704
+ maskSnapshot(snapshot) {
2705
+ if (!this.enabled) return snapshot;
2706
+ const uniqueLabels = [...new Set(snapshot.communities.map((c) => c.communityLabel))].sort();
2707
+ for (const label of uniqueLabels) this.mask(label);
2708
+ const maskedCommunities = snapshot.communities.map((entry) => ({
2709
+ ...entry,
2710
+ communityLabel: this.mask(entry.communityLabel)
2711
+ }));
2712
+ const maskedGodNodes = snapshot.godNodes.map((g) => ({ ...g }));
2713
+ return { ...snapshot, communities: maskedCommunities, godNodes: maskedGodNodes };
2714
+ }
2715
+ };
2716
+
2465
2717
  // src/index.ts
2466
2718
  var log4 = createLogger("ctxo:mcp");
2467
2719
  function loadMaskingConfig(ctxoRoot) {
@@ -2495,7 +2747,7 @@ async function main() {
2495
2747
  const args = process.argv.slice(2);
2496
2748
  const httpPortStr = process.env.CTXO_HTTP_PORT || (args.includes("--http") ? args[args.indexOf("--port") + 1] || "3001" : null);
2497
2749
  if (!httpPortStr && args.length > 0) {
2498
- const { CliRouter } = await import("./cli-router-T3VZZJD6.js");
2750
+ const { CliRouter } = await import("./cli-router-GZA44VOV.js");
2499
2751
  const router = new CliRouter(process.cwd());
2500
2752
  await router.route(args);
2501
2753
  return;
@@ -2503,19 +2755,22 @@ async function main() {
2503
2755
  const ctxoRoot = ".ctxo";
2504
2756
  const workspace = detectWorkspace(process.cwd());
2505
2757
  setWorkspaceMeta({ root: workspace.root });
2506
- const storage = new SqliteStorageAdapter(ctxoRoot);
2758
+ const storage = new SqliteStorageAdapter(ctxoRoot, { allowProductionPath: true });
2507
2759
  await storage.init();
2508
2760
  const masking = loadMaskingConfig(ctxoRoot);
2761
+ const clusterLabelMasker = new ClusterLabelMasker(
2762
+ maskingClusterLabelPatterns(loadConfig(ctxoRoot).config)
2763
+ );
2509
2764
  const git = new SimpleGitAdapter(process.cwd());
2510
2765
  const recorder = readStatsEnabled(ctxoRoot) ? new SessionRecorderAdapter(storage.getDb(), () => storage.persist()) : null;
2511
2766
  const server = new McpServer({ name: "ctxo", version: PKG_VERSION });
2512
2767
  const { StalenessDetector } = await import("./staleness-detector-4HJYBQ7Z.js");
2513
2768
  const staleness = new StalenessDetector(process.cwd(), ctxoRoot);
2514
- registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot);
2769
+ registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker);
2515
2770
  if (httpPortStr) {
2516
2771
  await startHttpTransport(async () => {
2517
2772
  const s = new McpServer({ name: "ctxo", version: PKG_VERSION });
2518
- registerTools(s, storage, masking, git, staleness, recorder, ctxoRoot);
2773
+ registerTools(s, storage, masking, git, staleness, recorder, ctxoRoot, clusterLabelMasker);
2519
2774
  return s;
2520
2775
  }, parseInt(httpPortStr, 10));
2521
2776
  } else {
@@ -2523,7 +2778,7 @@ async function main() {
2523
2778
  await server.connect(transport);
2524
2779
  }
2525
2780
  }
2526
- function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo") {
2781
+ function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo", clusterLabelMasker) {
2527
2782
  const toolAnnotations = {
2528
2783
  readOnlyHint: true,
2529
2784
  destructiveHint: false,
@@ -2531,7 +2786,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
2531
2786
  openWorldHint: false
2532
2787
  };
2533
2788
  const logicSliceHandler = withRecording("get_logic_slice", handleGetLogicSlice(storage, masking, staleness, ctxoRoot), recorder);
2534
- const whyContextHandler = withRecording("get_why_context", handleGetWhyContext(storage, git, masking, staleness, ctxoRoot), recorder);
2789
+ const whyContextHandler = withRecording("get_why_context", handleGetWhyContext(storage, git, masking, staleness, ctxoRoot, clusterLabelMasker), recorder);
2535
2790
  const changeIntelligenceHandler = withRecording("get_change_intelligence", handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot), recorder);
2536
2791
  server.registerTool(
2537
2792
  "get_logic_slice",
@@ -2570,7 +2825,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
2570
2825
  },
2571
2826
  (args) => changeIntelligenceHandler(args)
2572
2827
  );
2573
- const blastRadiusHandler = withRecording("get_blast_radius", handleGetBlastRadius(storage, masking, staleness, ctxoRoot), recorder);
2828
+ const blastRadiusHandler = withRecording("get_blast_radius", handleGetBlastRadius(storage, masking, staleness, ctxoRoot, git, clusterLabelMasker), recorder);
2574
2829
  server.registerTool(
2575
2830
  "get_blast_radius",
2576
2831
  {
@@ -2584,7 +2839,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
2584
2839
  },
2585
2840
  (args) => blastRadiusHandler(args)
2586
2841
  );
2587
- const overlayHandler = withRecording("get_architectural_overlay", handleGetArchitecturalOverlay(storage, masking, staleness), recorder);
2842
+ const overlayHandler = withRecording("get_architectural_overlay", handleGetArchitecturalOverlay(storage, masking, staleness, git, clusterLabelMasker), recorder);
2588
2843
  server.registerTool(
2589
2844
  "get_architectural_overlay",
2590
2845
  {
@@ -2709,7 +2964,7 @@ function registerTools(server, storage, masking, git, staleness, recorder, ctxoR
2709
2964
  },
2710
2965
  (args) => symbolImportanceHandler(args)
2711
2966
  );
2712
- const prImpactHandler = withRecording("get_pr_impact", handleGetPrImpact(storage, git, masking, staleness, ctxoRoot), recorder);
2967
+ const prImpactHandler = withRecording("get_pr_impact", handleGetPrImpact(storage, git, masking, staleness, ctxoRoot, clusterLabelMasker), recorder);
2713
2968
  server.registerTool(
2714
2969
  "get_pr_impact",
2715
2970
  {