@ctxo/cli 0.13.1 → 0.14.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.
Files changed (42) hide show
  1. package/README.md +16 -0
  2. package/dist/chunk-2Q76LH2H.js +186 -0
  3. package/dist/chunk-2Q76LH2H.js.map +1 -0
  4. package/dist/chunk-72CEQEQD.js +53 -0
  5. package/dist/chunk-72CEQEQD.js.map +1 -0
  6. package/dist/chunk-7HSA5JVP.js +1049 -0
  7. package/dist/chunk-7HSA5JVP.js.map +1 -0
  8. package/dist/chunk-G4BMKCKG.js +76 -0
  9. package/dist/chunk-G4BMKCKG.js.map +1 -0
  10. package/dist/chunk-L2CAUER6.js +508 -0
  11. package/dist/chunk-L2CAUER6.js.map +1 -0
  12. package/dist/chunk-LAX6VVBU.js +98 -0
  13. package/dist/chunk-LAX6VVBU.js.map +1 -0
  14. package/dist/chunk-SRQZDGFZ.js +128 -0
  15. package/dist/chunk-SRQZDGFZ.js.map +1 -0
  16. package/dist/{chunk-DT3ZH4AU.js → chunk-X4TVFJ4C.js} +6 -1
  17. package/dist/chunk-X4TVFJ4C.js.map +1 -0
  18. package/dist/{chunk-WB7WZKX4.js → chunk-ZL36YS2V.js} +766 -1614
  19. package/dist/chunk-ZL36YS2V.js.map +1 -0
  20. package/dist/{cli-router-DDHH4WQH.js → cli-router-AZCDNX5R.js} +980 -504
  21. package/dist/cli-router-AZCDNX5R.js.map +1 -0
  22. package/dist/index.js +13 -2874
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal.d.ts +747 -0
  25. package/dist/internal.js +13 -0
  26. package/dist/{json-index-reader-OXTRTGE6.js → json-index-reader-DBW32TCN.js} +6 -4
  27. package/dist/json-index-reader-DBW32TCN.js.map +1 -0
  28. package/dist/mcp-server-6YG2QE6E.js +2604 -0
  29. package/dist/mcp-server-6YG2QE6E.js.map +1 -0
  30. package/dist/module-host-KNMKS62R.js +13 -0
  31. package/dist/module-host-KNMKS62R.js.map +1 -0
  32. package/dist/module-loader-B3LYF3W7.js +14 -0
  33. package/dist/module-loader-B3LYF3W7.js.map +1 -0
  34. package/dist/plugin-discovery-XRXI3IJF.js +13 -0
  35. package/dist/plugin-discovery-XRXI3IJF.js.map +1 -0
  36. package/llms-full.txt +54 -0
  37. package/llms.txt +11 -0
  38. package/package.json +13 -2
  39. package/dist/chunk-DT3ZH4AU.js.map +0 -1
  40. package/dist/chunk-WB7WZKX4.js.map +0 -1
  41. package/dist/cli-router-DDHH4WQH.js.map +0 -1
  42. /package/dist/{json-index-reader-OXTRTGE6.js.map → internal.js.map} +0 -0
@@ -1,40 +1,248 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- atomicWrite
4
- } from "./chunk-RFEXMBTG.js";
3
+ DATA_EDGE_KINDS,
4
+ createTestFileClassifier,
5
+ filesWithTestAnnotations
6
+ } from "./chunk-7HSA5JVP.js";
7
+ import {
8
+ compareCodepoint
9
+ } from "./chunk-72CEQEQD.js";
5
10
  import {
6
11
  CoChangeMatrixSchema,
7
12
  EDGE_KINDS,
8
13
  EdgeKindSchema,
9
14
  JsonIndexReader
10
- } from "./chunk-DT3ZH4AU.js";
15
+ } from "./chunk-X4TVFJ4C.js";
16
+ import {
17
+ atomicWrite
18
+ } from "./chunk-RFEXMBTG.js";
11
19
  import {
12
20
  createLogger
13
21
  } from "./chunk-5DSKZJNN.js";
14
22
 
23
+ // src/adapters/workspace/single-package-workspace.ts
24
+ import { existsSync, readdirSync } from "fs";
25
+ import { join, resolve } from "path";
26
+ var MANIFEST_CANDIDATES = [
27
+ "package.json",
28
+ "pyproject.toml",
29
+ "setup.py",
30
+ "pom.xml",
31
+ "build.gradle",
32
+ "build.gradle.kts",
33
+ "go.mod",
34
+ "Cargo.toml"
35
+ ];
36
+ function findManifest(dir) {
37
+ for (const name of MANIFEST_CANDIDATES) {
38
+ const candidate = join(dir, name);
39
+ if (existsSync(candidate)) return candidate;
40
+ }
41
+ const csproj = findFirstByExt(dir, ".csproj") ?? findFirstByExt(dir, ".sln");
42
+ if (csproj) return csproj;
43
+ return join(dir, "package.json");
44
+ }
45
+ function findFirstByExt(dir, ext) {
46
+ try {
47
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
48
+ if (entry.isFile() && entry.name.endsWith(ext)) return join(dir, entry.name);
49
+ }
50
+ } catch {
51
+ }
52
+ return null;
53
+ }
54
+ function detectWorkspace(cwd) {
55
+ const root = resolve(cwd);
56
+ const pkg = {
57
+ root,
58
+ manifest: findManifest(root)
59
+ };
60
+ return {
61
+ root,
62
+ packages: [pkg]
63
+ };
64
+ }
65
+
66
+ // src/adapters/storage/co-change-reader.ts
67
+ import { readFileSync, statSync } from "fs";
68
+ import { join as join2 } from "path";
69
+
70
+ // src/core/co-change/co-change-analyzer.ts
71
+ var MIN_FREQUENCY = 0.1;
72
+ function buildCoChangeMatrix(windowed, options) {
73
+ const commitsPerFile = /* @__PURE__ */ new Map();
74
+ const sharedPerPair = /* @__PURE__ */ new Map();
75
+ let commitsUsed = 0;
76
+ let massSkipped = 0;
77
+ for (const commit of windowed) {
78
+ const files = includedFiles(commit, options.isIncluded);
79
+ if (files.length === 0) continue;
80
+ if (files.length > options.maxFilesPerCommit) {
81
+ massSkipped++;
82
+ continue;
83
+ }
84
+ commitsUsed++;
85
+ for (const file of files) {
86
+ commitsPerFile.set(file, (commitsPerFile.get(file) ?? 0) + 1);
87
+ }
88
+ for (let i = 0; i < files.length - 1; i++) {
89
+ const file1 = files[i];
90
+ let partners = sharedPerPair.get(file1);
91
+ if (!partners) {
92
+ partners = /* @__PURE__ */ new Map();
93
+ sharedPerPair.set(file1, partners);
94
+ }
95
+ for (let j = i + 1; j < files.length; j++) {
96
+ const file2 = files[j];
97
+ partners.set(file2, (partners.get(file2) ?? 0) + 1);
98
+ }
99
+ }
100
+ }
101
+ const entries = [];
102
+ for (const [file1, partners] of sharedPerPair) {
103
+ for (const [file2, sharedCommits] of partners) {
104
+ if (sharedCommits < options.minShared) continue;
105
+ const smallerHistory = Math.min(commitsPerFile.get(file1), commitsPerFile.get(file2));
106
+ const frequency = Math.round(sharedCommits / smallerHistory * 1e3) / 1e3;
107
+ if (frequency < MIN_FREQUENCY) continue;
108
+ entries.push({ file1, file2, sharedCommits, frequency });
109
+ }
110
+ }
111
+ entries.sort(compareEntries);
112
+ const window = {
113
+ maxCommits: options.maxCommits,
114
+ ...options.since !== void 0 ? { since: options.since } : {},
115
+ head: options.head,
116
+ commitsScanned: windowed.length,
117
+ commitsUsed,
118
+ massSkipped,
119
+ maxFilesPerCommit: options.maxFilesPerCommit,
120
+ minShared: options.minShared
121
+ };
122
+ return { version: 1, timestamp: options.timestamp, window, entries };
123
+ }
124
+ function includedFiles(commit, isIncluded) {
125
+ const included = /* @__PURE__ */ new Set();
126
+ for (const file of commit.files) {
127
+ if (isIncluded(file)) included.add(file);
128
+ }
129
+ return [...included].sort(compareCodepoint);
130
+ }
131
+ function compareEntries(a, b) {
132
+ return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file1, b.file1) || compareCodepoint(a.file2, b.file2);
133
+ }
134
+ function loadCoChangeMap(matrix) {
135
+ const map = /* @__PURE__ */ new Map();
136
+ for (const entry of matrix.entries) {
137
+ let list1 = map.get(entry.file1);
138
+ if (!list1) {
139
+ list1 = [];
140
+ map.set(entry.file1, list1);
141
+ }
142
+ list1.push(entry);
143
+ let list2 = map.get(entry.file2);
144
+ if (!list2) {
145
+ list2 = [];
146
+ map.set(entry.file2, list2);
147
+ }
148
+ list2.push(entry);
149
+ }
150
+ return map;
151
+ }
152
+
153
+ // src/adapters/storage/co-change-reader.ts
154
+ var log = createLogger("ctxo:co-change");
155
+ var UNTIL_REWRITTEN = "co-change is ignored until ctxo index rewrites it";
156
+ var UNTIL_RETRY = "co-change is ignored for now and the next call tries again";
157
+ var CoChangeReader = class {
158
+ filePath;
159
+ cached;
160
+ /** What the last warning was about, so one problem is reported once. */
161
+ lastWarning;
162
+ constructor(ctxoRoot) {
163
+ this.filePath = join2(ctxoRoot, "index", "co-changes.json");
164
+ }
165
+ readCoChangeMap() {
166
+ let stats;
167
+ try {
168
+ stats = statSync(this.filePath);
169
+ } catch (err) {
170
+ this.cached = void 0;
171
+ const { code, message } = err;
172
+ if (code === "ENOENT") {
173
+ this.lastWarning = void 0;
174
+ } else {
175
+ this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY);
176
+ }
177
+ return void 0;
178
+ }
179
+ const version = `${stats.mtimeMs}:${stats.size}`;
180
+ if (this.cached?.version === version) return this.cached.map;
181
+ return this.load(version);
182
+ }
183
+ load(version) {
184
+ let raw;
185
+ try {
186
+ raw = readFileSync(this.filePath, "utf-8");
187
+ } catch (err) {
188
+ this.cached = void 0;
189
+ this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY);
190
+ return void 0;
191
+ }
192
+ const map = this.parse(raw, version);
193
+ this.cached = { version, map };
194
+ if (map) this.lastWarning = void 0;
195
+ return map;
196
+ }
197
+ parse(raw, version) {
198
+ let data;
199
+ try {
200
+ data = JSON.parse(raw);
201
+ } catch (err) {
202
+ return this.unusable(version, `is not valid JSON (${err.message})`);
203
+ }
204
+ const parsed = CoChangeMatrixSchema.safeParse(data);
205
+ if (!parsed.success) {
206
+ const issue = parsed.error.issues[0];
207
+ const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
208
+ return this.unusable(version, `is not a valid co-change matrix (${where}${issue?.message ?? "unknown problem"})`);
209
+ }
210
+ return freezeEntries(loadCoChangeMap(parsed.data));
211
+ }
212
+ unusable(version, problem) {
213
+ this.warnOnce(`content ${version}`, problem, UNTIL_REWRITTEN);
214
+ return void 0;
215
+ }
216
+ warnOnce(key, problem, consequence) {
217
+ if (this.lastWarning === key) return;
218
+ this.lastWarning = key;
219
+ log.warn(`${this.filePath} ${problem}; ${consequence}`);
220
+ }
221
+ };
222
+ function freezeEntries(map) {
223
+ for (const list of map.values()) {
224
+ for (const entry of list) Object.freeze(entry);
225
+ Object.freeze(list);
226
+ }
227
+ return map;
228
+ }
229
+
15
230
  // src/adapters/storage/sqlite-storage-adapter.ts
16
231
  import initSqlJs from "sql.js";
17
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
18
- import { join as join2, dirname } from "path";
232
+ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
233
+ import { join as join4, dirname } from "path";
19
234
 
20
235
  // src/adapters/storage/community-snapshot-writer.ts
21
236
  import {
22
- existsSync,
237
+ existsSync as existsSync2,
23
238
  mkdirSync,
24
- readFileSync,
25
- readdirSync,
239
+ readFileSync as readFileSync2,
240
+ readdirSync as readdirSync2,
26
241
  unlinkSync
27
242
  } from "fs";
28
243
  import { tmpdir } from "os";
29
- import { basename, join, resolve } from "path";
30
-
31
- // src/core/compare-codepoint.ts
32
- function compareCodepoint(a, b) {
33
- return a < b ? -1 : a > b ? 1 : 0;
34
- }
35
-
36
- // src/adapters/storage/community-snapshot-writer.ts
37
- var log = createLogger("ctxo:community-snapshot");
244
+ import { basename, join as join3, resolve as resolve2 } from "path";
245
+ var log2 = createLogger("ctxo:community-snapshot");
38
246
  var DEFAULT_HISTORY_LIMIT = 10;
39
247
  var HISTORY_DIR_NAME = "communities.history";
40
248
  var CURRENT_FILE_NAME = "communities.json";
@@ -46,26 +254,26 @@ var CommunitySnapshotWriter = class {
46
254
  historyLimit;
47
255
  constructor(ctxoRoot, historyLimitOrOptions = DEFAULT_HISTORY_LIMIT) {
48
256
  const options = typeof historyLimitOrOptions === "number" ? { historyLimit: historyLimitOrOptions } : historyLimitOrOptions;
49
- this.indexDir = join(ctxoRoot, "index");
50
- this.historyDir = join(this.indexDir, HISTORY_DIR_NAME);
257
+ this.indexDir = join3(ctxoRoot, "index");
258
+ this.historyDir = join3(this.indexDir, HISTORY_DIR_NAME);
51
259
  this.historyLimit = options.historyLimit ?? DEFAULT_HISTORY_LIMIT;
52
260
  assertSafeWritePath(ctxoRoot, options.allowProductionPath ?? false);
53
261
  }
54
262
  writeSnapshot(snapshot) {
55
263
  mkdirSync(this.indexDir, { recursive: true });
56
264
  mkdirSync(this.historyDir, { recursive: true });
57
- const currentPath = join(this.indexDir, CURRENT_FILE_NAME);
58
- if (existsSync(currentPath)) {
265
+ const currentPath = join3(this.indexDir, CURRENT_FILE_NAME);
266
+ if (existsSync2(currentPath)) {
59
267
  try {
60
- const existingRaw = readFileSync(currentPath, "utf-8");
268
+ const existingRaw = readFileSync2(currentPath, "utf-8");
61
269
  const existing = JSON.parse(existingRaw);
62
270
  const existingCommit = archivedCommit(existing);
63
271
  if (existingCommit !== void 0 && existingCommit !== snapshot.commitSha) {
64
272
  const historyFile = this.historyFilename(existing);
65
- this.atomicWrite(join(this.historyDir, historyFile), existingRaw);
273
+ this.atomicWrite(join3(this.historyDir, historyFile), existingRaw);
66
274
  }
67
275
  } catch (err) {
68
- log.error(`failed to archive existing snapshot: ${err.message}`);
276
+ log2.error(`failed to archive existing snapshot: ${err.message}`);
69
277
  }
70
278
  }
71
279
  const json = JSON.stringify(snapshot, null, 2);
@@ -73,8 +281,8 @@ var CommunitySnapshotWriter = class {
73
281
  this.rotateHistory();
74
282
  }
75
283
  readCurrent() {
76
- const path = join(this.indexDir, CURRENT_FILE_NAME);
77
- if (!existsSync(path)) return void 0;
284
+ const path = join3(this.indexDir, CURRENT_FILE_NAME);
285
+ if (!existsSync2(path)) return void 0;
78
286
  return this.safeParse(path);
79
287
  }
80
288
  /**
@@ -85,11 +293,11 @@ var CommunitySnapshotWriter = class {
85
293
  * write rotates them out.
86
294
  */
87
295
  listHistory(limit) {
88
- if (!existsSync(this.historyDir)) return [];
296
+ if (!existsSync2(this.historyDir)) return [];
89
297
  const snapshots = [];
90
298
  const seen = /* @__PURE__ */ new Set();
91
299
  for (const name of this.historyNames().reverse()) {
92
- const snap = this.safeParse(join(this.historyDir, name));
300
+ const snap = this.safeParse(join3(this.historyDir, name));
93
301
  const commit = archivedCommit(snap);
94
302
  if (!snap || commit === void 0) continue;
95
303
  if (seen.has(commit)) continue;
@@ -105,10 +313,10 @@ var CommunitySnapshotWriter = class {
105
313
  historyFilename(snapshot) {
106
314
  const ts = snapshot.computedAt.replace(/[:.]/g, "-");
107
315
  const sha = snapshot.commitSha || "nocommit";
108
- const base = join(this.historyDir, `${ts}-${sha}`);
316
+ const base = join3(this.historyDir, `${ts}-${sha}`);
109
317
  let candidate = `${base}.json`;
110
318
  let n = 1;
111
- while (existsSync(candidate)) {
319
+ while (existsSync2(candidate)) {
112
320
  candidate = `${base}.${n++}.json`;
113
321
  if (n > 1e3) break;
114
322
  }
@@ -116,7 +324,7 @@ var CommunitySnapshotWriter = class {
116
324
  }
117
325
  /** The `.json` files in the history folder, oldest first. */
118
326
  historyNames() {
119
- return readdirSync(this.historyDir).filter((name) => name.endsWith(".json")).sort(compareHistoryNames);
327
+ return readdirSync2(this.historyDir).filter((name) => name.endsWith(".json")).sort(compareHistoryNames);
120
328
  }
121
329
  /**
122
330
  * Rotation in two steps. First the duplicates go: every file that is not
@@ -129,12 +337,12 @@ var CommunitySnapshotWriter = class {
129
337
  * duplicates of anything and age out by count like the rest.
130
338
  */
131
339
  rotateHistory() {
132
- if (!existsSync(this.historyDir)) return;
340
+ if (!existsSync2(this.historyDir)) return;
133
341
  const seen = /* @__PURE__ */ new Set();
134
342
  const unique = [];
135
343
  const doomed = [];
136
344
  for (const name of this.historyNames().reverse()) {
137
- const commit = archivedCommit(this.safeParse(join(this.historyDir, name)));
345
+ const commit = archivedCommit(this.safeParse(join3(this.historyDir, name)));
138
346
  if (commit !== void 0) {
139
347
  if (seen.has(commit)) {
140
348
  doomed.push(name);
@@ -146,20 +354,20 @@ var CommunitySnapshotWriter = class {
146
354
  }
147
355
  doomed.push(...unique.slice(this.historyLimit));
148
356
  for (const name of doomed) {
149
- const target = join(this.historyDir, name);
357
+ const target = join3(this.historyDir, name);
150
358
  try {
151
359
  unlinkSync(target);
152
360
  } catch (err) {
153
- log.error(`failed to rotate ${basename(target)}: ${err.message}`);
361
+ log2.error(`failed to rotate ${basename(target)}: ${err.message}`);
154
362
  }
155
363
  }
156
364
  }
157
365
  safeParse(path) {
158
366
  try {
159
- const raw = readFileSync(path, "utf-8");
367
+ const raw = readFileSync2(path, "utf-8");
160
368
  return JSON.parse(raw);
161
369
  } catch (err) {
162
- log.error(`failed to parse ${basename(path)}: ${err.message}`);
370
+ log2.error(`failed to parse ${basename(path)}: ${err.message}`);
163
371
  return void 0;
164
372
  }
165
373
  }
@@ -178,8 +386,8 @@ function splitCollisionCounter(name) {
178
386
  return match ? [`${match[1]}.json`, Number(match[2])] : [name, 0];
179
387
  }
180
388
  function assertSafeWritePath(ctxoRoot, allowProduction) {
181
- const resolved = resolve(ctxoRoot);
182
- const tmpRoot = resolve(tmpdir());
389
+ const resolved = resolve2(ctxoRoot);
390
+ const tmpRoot = resolve2(tmpdir());
183
391
  const inTmp = resolved.startsWith(tmpRoot);
184
392
  if (inTmp || allowProduction) return;
185
393
  throw new Error(
@@ -188,7 +396,7 @@ function assertSafeWritePath(ctxoRoot, allowProduction) {
188
396
  }
189
397
 
190
398
  // src/adapters/storage/sqlite-storage-adapter.ts
191
- var log2 = createLogger("ctxo:storage");
399
+ var log3 = createLogger("ctxo:storage");
192
400
  var SqliteStorageAdapter = class {
193
401
  db;
194
402
  dbPath;
@@ -196,20 +404,20 @@ var SqliteStorageAdapter = class {
196
404
  communityWriter;
197
405
  constructor(ctxoRoot, options = {}) {
198
406
  this.ctxoRoot = ctxoRoot;
199
- this.dbPath = join2(ctxoRoot, ".cache", "symbols.db");
407
+ this.dbPath = join4(ctxoRoot, ".cache", "symbols.db");
200
408
  this.communityWriter = new CommunitySnapshotWriter(ctxoRoot, {
201
409
  allowProductionPath: options.allowProductionPath
202
410
  });
203
411
  }
204
412
  async init() {
205
413
  const SQL = await initSqlJs();
206
- if (existsSync2(this.dbPath)) {
414
+ if (existsSync3(this.dbPath)) {
207
415
  try {
208
- const buffer = readFileSync2(this.dbPath);
416
+ const buffer = readFileSync3(this.dbPath);
209
417
  this.db = new SQL.Database(buffer);
210
418
  this.verifyIntegrity();
211
419
  } catch {
212
- log2.warn("Corrupt DB detected, rebuilding from JSON index");
420
+ log3.warn("Corrupt DB detected, rebuilding from JSON index");
213
421
  this.db = new SQL.Database();
214
422
  this.createTables();
215
423
  }
@@ -221,9 +429,9 @@ var SqliteStorageAdapter = class {
221
429
  }
222
430
  async initEmpty() {
223
431
  const SQL = await initSqlJs();
224
- if (existsSync2(this.dbPath)) {
432
+ if (existsSync3(this.dbPath)) {
225
433
  try {
226
- const buffer = readFileSync2(this.dbPath);
434
+ const buffer = readFileSync3(this.dbPath);
227
435
  this.db = new SQL.Database(buffer);
228
436
  } catch {
229
437
  this.db = new SQL.Database();
@@ -488,7 +696,7 @@ var SqliteStorageAdapter = class {
488
696
  try {
489
697
  this.persist();
490
698
  } catch (err) {
491
- log2.error(`Failed to persist DB: ${err.message}`);
699
+ log3.error(`Failed to persist DB: ${err.message}`);
492
700
  }
493
701
  }
494
702
  deleteFileData(db, filePath) {
@@ -534,916 +742,15 @@ var SqliteStorageAdapter = class {
534
742
  }
535
743
  };
536
744
 
537
- // src/adapters/storage/co-change-reader.ts
538
- import { readFileSync as readFileSync3, statSync } from "fs";
539
- import { join as join3 } from "path";
540
-
541
- // src/core/co-change/co-change-analyzer.ts
542
- var MIN_FREQUENCY = 0.1;
543
- function buildCoChangeMatrix(windowed, options) {
544
- const commitsPerFile = /* @__PURE__ */ new Map();
545
- const sharedPerPair = /* @__PURE__ */ new Map();
546
- let commitsUsed = 0;
547
- let massSkipped = 0;
548
- for (const commit of windowed) {
549
- const files = includedFiles(commit, options.isIncluded);
550
- if (files.length === 0) continue;
551
- if (files.length > options.maxFilesPerCommit) {
552
- massSkipped++;
553
- continue;
554
- }
555
- commitsUsed++;
556
- for (const file of files) {
557
- commitsPerFile.set(file, (commitsPerFile.get(file) ?? 0) + 1);
558
- }
559
- for (let i = 0; i < files.length - 1; i++) {
560
- const file1 = files[i];
561
- let partners = sharedPerPair.get(file1);
562
- if (!partners) {
563
- partners = /* @__PURE__ */ new Map();
564
- sharedPerPair.set(file1, partners);
565
- }
566
- for (let j = i + 1; j < files.length; j++) {
567
- const file2 = files[j];
568
- partners.set(file2, (partners.get(file2) ?? 0) + 1);
569
- }
570
- }
571
- }
572
- const entries = [];
573
- for (const [file1, partners] of sharedPerPair) {
574
- for (const [file2, sharedCommits] of partners) {
575
- if (sharedCommits < options.minShared) continue;
576
- const smallerHistory = Math.min(commitsPerFile.get(file1), commitsPerFile.get(file2));
577
- const frequency = Math.round(sharedCommits / smallerHistory * 1e3) / 1e3;
578
- if (frequency < MIN_FREQUENCY) continue;
579
- entries.push({ file1, file2, sharedCommits, frequency });
580
- }
581
- }
582
- entries.sort(compareEntries);
583
- const window = {
584
- maxCommits: options.maxCommits,
585
- ...options.since !== void 0 ? { since: options.since } : {},
586
- head: options.head,
587
- commitsScanned: windowed.length,
588
- commitsUsed,
589
- massSkipped,
590
- maxFilesPerCommit: options.maxFilesPerCommit,
591
- minShared: options.minShared
592
- };
593
- return { version: 1, timestamp: options.timestamp, window, entries };
594
- }
595
- function includedFiles(commit, isIncluded) {
596
- const included = /* @__PURE__ */ new Set();
597
- for (const file of commit.files) {
598
- if (isIncluded(file)) included.add(file);
599
- }
600
- return [...included].sort(compareCodepoint);
601
- }
602
- function compareEntries(a, b) {
603
- return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file1, b.file1) || compareCodepoint(a.file2, b.file2);
604
- }
605
- function loadCoChangeMap(matrix) {
606
- const map = /* @__PURE__ */ new Map();
607
- for (const entry of matrix.entries) {
608
- let list1 = map.get(entry.file1);
609
- if (!list1) {
610
- list1 = [];
611
- map.set(entry.file1, list1);
612
- }
613
- list1.push(entry);
614
- let list2 = map.get(entry.file2);
615
- if (!list2) {
616
- list2 = [];
617
- map.set(entry.file2, list2);
618
- }
619
- list2.push(entry);
620
- }
621
- return map;
622
- }
623
-
624
- // src/adapters/storage/co-change-reader.ts
625
- var log3 = createLogger("ctxo:co-change");
626
- var UNTIL_REWRITTEN = "co-change is ignored until ctxo index rewrites it";
627
- var UNTIL_RETRY = "co-change is ignored for now and the next call tries again";
628
- var CoChangeReader = class {
629
- filePath;
630
- cached;
631
- /** What the last warning was about, so one problem is reported once. */
632
- lastWarning;
633
- constructor(ctxoRoot) {
634
- this.filePath = join3(ctxoRoot, "index", "co-changes.json");
635
- }
636
- readCoChangeMap() {
637
- let stats;
638
- try {
639
- stats = statSync(this.filePath);
640
- } catch (err) {
641
- this.cached = void 0;
642
- const { code, message } = err;
643
- if (code === "ENOENT") {
644
- this.lastWarning = void 0;
645
- } else {
646
- this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY);
647
- }
648
- return void 0;
649
- }
650
- const version = `${stats.mtimeMs}:${stats.size}`;
651
- if (this.cached?.version === version) return this.cached.map;
652
- return this.load(version);
653
- }
654
- load(version) {
655
- let raw;
656
- try {
657
- raw = readFileSync3(this.filePath, "utf-8");
658
- } catch (err) {
659
- this.cached = void 0;
660
- this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY);
661
- return void 0;
662
- }
663
- const map = this.parse(raw, version);
664
- this.cached = { version, map };
665
- if (map) this.lastWarning = void 0;
666
- return map;
667
- }
668
- parse(raw, version) {
669
- let data;
670
- try {
671
- data = JSON.parse(raw);
672
- } catch (err) {
673
- return this.unusable(version, `is not valid JSON (${err.message})`);
674
- }
675
- const parsed = CoChangeMatrixSchema.safeParse(data);
676
- if (!parsed.success) {
677
- const issue = parsed.error.issues[0];
678
- const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
679
- return this.unusable(version, `is not a valid co-change matrix (${where}${issue?.message ?? "unknown problem"})`);
680
- }
681
- return freezeEntries(loadCoChangeMap(parsed.data));
682
- }
683
- unusable(version, problem) {
684
- this.warnOnce(`content ${version}`, problem, UNTIL_REWRITTEN);
685
- return void 0;
686
- }
687
- warnOnce(key, problem, consequence) {
688
- if (this.lastWarning === key) return;
689
- this.lastWarning = key;
690
- log3.warn(`${this.filePath} ${problem}; ${consequence}`);
691
- }
692
- };
693
- function freezeEntries(map) {
694
- for (const list of map.values()) {
695
- for (const entry of list) Object.freeze(entry);
696
- Object.freeze(list);
697
- }
698
- return map;
699
- }
700
-
701
- // src/core/co-change/co-change-window.ts
702
- var SINCE_PATTERN = /^(\d+)([dwmy])$/;
703
- var SECONDS_PER_DAY = 86400;
704
- function parseCoChangeSince(value) {
705
- const match = SINCE_PATTERN.exec(value);
706
- if (!match) return void 0;
707
- const amount = Number.parseInt(match[1], 10);
708
- if (!Number.isSafeInteger(amount)) return void 0;
709
- return { amount, unit: match[2] };
710
- }
711
- function sinceCutoff(headCommittedAt, since) {
712
- const cutoff = subtractSince(headCommittedAt, since);
713
- return Number.isFinite(cutoff) ? cutoff : Number.NEGATIVE_INFINITY;
714
- }
715
- function subtractSince(headCommittedAt, since) {
716
- switch (since.unit) {
717
- case "d":
718
- return headCommittedAt - since.amount * SECONDS_PER_DAY;
719
- case "w":
720
- return headCommittedAt - since.amount * 7 * SECONDS_PER_DAY;
721
- case "m":
722
- return subtractUtcMonths(headCommittedAt, since.amount);
723
- case "y":
724
- return subtractUtcMonths(headCommittedAt, since.amount * 12);
725
- }
726
- }
727
- function subtractUtcMonths(epochSeconds, months) {
728
- const date = new Date(epochSeconds * 1e3);
729
- const dayOfMonth = date.getUTCDate();
730
- date.setUTCDate(1);
731
- date.setUTCMonth(date.getUTCMonth() - months);
732
- date.setUTCDate(Math.min(dayOfMonth, daysInUtcMonth(date)));
733
- return date.getTime() / 1e3;
734
- }
735
- function daysInUtcMonth(date) {
736
- const lastDay = new Date(date.getTime());
737
- lastDay.setUTCMonth(lastDay.getUTCMonth() + 1, 0);
738
- return lastDay.getUTCDate();
739
- }
740
- function selectWindow(commits, maxCommits, cutoff) {
741
- const newest = maxCommits === "all" ? [...commits] : commits.slice(0, wholeCommitCount(maxCommits));
742
- return cutoff === void 0 ? newest : newest.filter((commit) => commit.committedAt >= cutoff);
743
- }
744
- function wholeCommitCount(maxCommits) {
745
- return maxCommits >= 1 ? Math.floor(maxCommits) : 0;
746
- }
747
-
748
- // src/core/config/load-config.ts
749
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
750
- import { join as join4 } from "path";
751
- import { parse as parseYaml } from "yaml";
752
- import picomatch from "picomatch";
753
-
754
- // src/core/config/config-schema.ts
755
- import { z } from "zod";
756
- var StatsConfigSchema = z.object({
757
- enabled: z.boolean().optional()
758
- }).strict();
759
- var IndexConfigSchema = z.object({
760
- ignore: z.array(z.string().min(1)).optional(),
761
- ignoreProjects: z.array(z.string().min(1)).optional(),
762
- facts: z.boolean().optional(),
763
- tests: z.array(z.string().min(1)).optional()
764
- }).strict();
765
- var MaskingConfigSchema = z.object({
766
- /**
767
- * Glob patterns matched against community cluster labels. Matching labels
768
- * are replaced with `[masked-cluster-N]` in MCP tool responses so
769
- * org-sensitive folder names (e.g. `src/payment-vault`, `internal-*`)
770
- * never surface to AI agents over the stdio channel.
771
- */
772
- clusterLabels: z.array(z.string().min(1)).optional()
773
- }).strict();
774
- var WatchConfigSchema = z.object({
775
- /**
776
- * Minimum number of files that must be re-indexed since the last
777
- * community snapshot before `ctxo watch` recomputes one. Default: 5.
778
- * On large monorepos Louvain + PageRank cost ~1 s of blocking CPU;
779
- * bumping this (e.g. 25) avoids recomputing on every save.
780
- */
781
- snapshotMinFileChanges: z.number().int().min(1).optional()
782
- }).strict();
783
- var GateConfigSchema = z.object({
784
- enabled: z.boolean().optional(),
785
- sensitivity: z.enum(["strict", "balanced", "lenient"]).optional(),
786
- /** Absolute floor: minimum confirmed+likely dependents before the gate can fire. */
787
- minDependents: z.number().int().min(0).optional(),
788
- /** Repo-relative significance: symbol must be in the top `percentile`% by PageRank. */
789
- percentile: z.number().int().min(1).max(100).optional()
790
- }).strict();
791
- var CoChangeConfigSchema = z.object({
792
- /**
793
- * Size of the co-change window: the last N non-merge commits from HEAD, or "all".
794
- * Default: 2000. Independent of `ctxo index --max-history`, which only sets how much
795
- * per-file commit history is kept.
796
- */
797
- maxCommits: z.union([z.number().int().min(1), z.literal("all")], { error: 'must be a positive integer or "all"' }).optional(),
798
- /**
799
- * Narrows the window to a duration counted back from HEAD's committer date (never the wall
800
- * clock): `30d`, `2w`, `12m` or `1y`. Applies together with `maxCommits`.
801
- */
802
- since: z.string().refine((value) => parseCoChangeSince(value) !== void 0, {
803
- error: "must be a duration such as 30d, 2w, 12m or 1y"
804
- }).optional(),
805
- /** Commits touching more analysed files than this are skipped as mass changes. Default: 30. */
806
- maxFilesPerCommit: z.number().int().min(1).optional(),
807
- /** A file pair becomes an entry only after this many shared commits. Default: 2. */
808
- minShared: z.number().int().min(1).optional(),
809
- /** Extra tracked files to analyse that no language plugin indexes, such as SQL scripts (globs). */
810
- include: z.array(z.string().min(1)).optional(),
811
- /** Indexed files to leave out of the analysis (globs). */
812
- exclude: z.array(z.string().min(1)).optional()
813
- }).strict();
814
- var CommunitiesConfigSchema = z.object({
815
- /** Louvain resolution: higher gives more, smaller communities. Default: 1.0. */
816
- resolution: z.number().positive().optional(),
817
- /** Minimum number of communities a symbol must bridge to count as a god node. Default: 3. */
818
- godNodeMinBridged: z.number().int().min(1).optional(),
819
- /** Maximum number of god nodes reported. Default: 25. */
820
- godNodeMaxResults: z.number().int().min(0).optional()
821
- }).strict();
822
- var ContextsViewConfigSchema = z.object({
823
- /** Blend of the file-edge signals. Defaults: dependency 1.0, coChange 1.0, data 0 (shared tables are a finding, not a clustering weight). */
824
- weights: z.object({
825
- dependency: z.number().min(0).optional(),
826
- coChange: z.number().min(0).optional(),
827
- data: z.number().min(0).optional()
828
- }).strict().optional(),
829
- /** Files to leave out of the file graph (globs). */
830
- exclude: z.array(z.string().min(1)).optional(),
831
- hubs: z.object({
832
- /** Detect hubs automatically (fan-in and god nodes). Default: true. */
833
- auto: z.boolean().optional(),
834
- /** A file is a fan-in hub when it is in the top `topPercent`% by dependents. Default: 1. */
835
- topPercent: z.number().positive().max(100).optional(),
836
- /** Minimum distinct dependent files for a fan-in hub. Default: 10. */
837
- minDependents: z.number().int().min(1).optional(),
838
- /** Files that lose all their edges (globs). */
839
- paths: z.array(z.string().min(1)).optional()
840
- }).strict().optional(),
841
- /** Louvain resolution for the file graph. Default: 1.0. */
842
- resolution: z.number().positive().optional(),
843
- /** Candidates with fewer files fold into a neighbour. Default: 3. */
844
- minCandidateFiles: z.number().int().min(1).optional(),
845
- /** Words dropped from candidate terms; replaces the default list. */
846
- technicalTerms: z.array(z.string().min(1)).optional()
847
- }).strict();
848
- var CtxoConfigSchema = z.object({
849
- version: z.union([z.string(), z.number()]).optional(),
850
- stats: StatsConfigSchema.optional(),
851
- index: IndexConfigSchema.optional(),
852
- masking: MaskingConfigSchema.optional(),
853
- watch: WatchConfigSchema.optional(),
854
- gate: GateConfigSchema.optional(),
855
- cochange: CoChangeConfigSchema.optional(),
856
- communities: CommunitiesConfigSchema.optional(),
857
- contextsView: ContextsViewConfigSchema.optional()
858
- }).passthrough();
859
- var DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES = 5;
860
- var DEFAULT_COCHANGE_MAX_COMMITS = 2e3;
861
- var DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT = 30;
862
- var DEFAULT_COCHANGE_MIN_SHARED = 2;
863
- var DEFAULT_TECHNICAL_TERMS = [
864
- "Service",
865
- "Repository",
866
- "Controller",
867
- "Handler",
868
- "Manager",
869
- "Dto",
870
- "Impl",
871
- "Async"
872
- ];
873
- var DEFAULT_CONFIG = {
874
- version: "1.0",
875
- stats: { enabled: true },
876
- index: { ignore: [], ignoreProjects: [] },
877
- masking: { clusterLabels: [] },
878
- watch: { snapshotMinFileChanges: DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES },
879
- gate: { enabled: true, sensitivity: "balanced" },
880
- cochange: {
881
- maxCommits: DEFAULT_COCHANGE_MAX_COMMITS,
882
- maxFilesPerCommit: DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT,
883
- minShared: DEFAULT_COCHANGE_MIN_SHARED,
884
- include: [],
885
- exclude: []
886
- }
887
- };
888
-
889
- // src/core/config/load-config.ts
890
- var log4 = createLogger("ctxo:config");
891
- function isValidGlob(pattern) {
892
- try {
893
- picomatch(pattern);
894
- return true;
895
- } catch {
896
- return false;
897
- }
898
- }
899
- function validateGlobs(patterns, field, invalid) {
900
- if (!patterns) return;
901
- for (const p of patterns) {
902
- if (!isValidGlob(p)) invalid.push(`${field}: ${p}`);
903
- }
904
- }
905
- function loadConfig(ctxoRoot) {
906
- const path = join4(ctxoRoot, "config.yaml");
907
- const errors = [];
908
- const invalidGlobs = [];
909
- if (!existsSync3(path)) {
910
- return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: false };
911
- }
912
- let raw;
913
- try {
914
- raw = readFileSync4(path, "utf-8");
915
- } catch (err) {
916
- errors.push(`Cannot read ${path}: ${err.message}`);
917
- return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
918
- }
919
- let parsed;
920
- try {
921
- parsed = parseYaml(raw);
922
- } catch (err) {
923
- errors.push(`Invalid YAML in ${path}: ${err.message}`);
924
- log4.warn(`Invalid YAML in config.yaml, falling back to defaults: ${err.message}`);
925
- return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
926
- }
927
- if (parsed === null || parsed === void 0) {
928
- return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
929
- }
930
- const result = CtxoConfigSchema.safeParse(parsed);
931
- if (!result.success) {
932
- for (const issue of result.error.issues) {
933
- errors.push(`${issue.path.join(".") || "<root>"}: ${issue.message}`);
934
- }
935
- log4.warn(`Invalid config.yaml (${errors.length} issue(s)), falling back to defaults`);
936
- return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
937
- }
938
- validateGlobs(result.data.index?.ignore, "index.ignore", invalidGlobs);
939
- validateGlobs(result.data.index?.ignoreProjects, "index.ignoreProjects", invalidGlobs);
940
- validateGlobs(result.data.index?.tests, "index.tests", invalidGlobs);
941
- validateGlobs(result.data.cochange?.include, "cochange.include", invalidGlobs);
942
- validateGlobs(result.data.cochange?.exclude, "cochange.exclude", invalidGlobs);
943
- validateGlobs(result.data.contextsView?.exclude, "contextsView.exclude", invalidGlobs);
944
- validateGlobs(result.data.contextsView?.hubs?.paths, "contextsView.hubs.paths", invalidGlobs);
945
- for (const pattern of invalidGlobs) {
946
- log4.warn(`Invalid glob pattern in config.yaml \u2014 ${pattern}`);
947
- }
948
- return { config: result.data, errors, invalidGlobs, path, exists: true };
949
- }
950
- function statsEnabled(config) {
951
- return config.stats?.enabled !== false;
952
- }
953
- function indexIgnorePatterns(config) {
954
- return (config.index?.ignore ?? []).filter(isValidGlob);
955
- }
956
- function indexIgnoreProjectPatterns(config) {
957
- return (config.index?.ignoreProjects ?? []).filter(isValidGlob);
958
- }
959
- function factsRequested(config) {
960
- return config.index?.facts === true;
961
- }
962
- function indexTestPatterns(config) {
963
- return (config.index?.tests ?? []).filter(isValidGlob);
964
- }
965
- function communityDetectorOptions(config) {
966
- const section = config.communities;
967
- const resolution = section?.resolution;
968
- const minBridged = section?.godNodeMinBridged;
969
- const maxResults = section?.godNodeMaxResults;
970
- return {
971
- ...typeof resolution === "number" && Number.isFinite(resolution) && resolution > 0 ? { resolution } : {},
972
- ...typeof minBridged === "number" && Number.isInteger(minBridged) && minBridged >= 1 ? { godNodeMinBridged: minBridged } : {},
973
- ...typeof maxResults === "number" && Number.isInteger(maxResults) && maxResults >= 0 ? { godNodeMaxResults: maxResults } : {}
974
- };
975
- }
976
- function maskingClusterLabelPatterns(config) {
977
- return (config.masking?.clusterLabels ?? []).filter(isValidGlob);
978
- }
979
- function watchSnapshotMinFileChanges(config) {
980
- const value = config.watch?.snapshotMinFileChanges;
981
- if (typeof value === "number" && Number.isFinite(value) && value >= 1) return value;
982
- return DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES;
983
- }
984
- function isPositiveInteger(value) {
985
- return typeof value === "number" && Number.isInteger(value) && value >= 1;
986
- }
987
- function positiveIntegerOr(value, fallback) {
988
- return isPositiveInteger(value) ? value : fallback;
989
- }
990
- function validMaxCommits(value) {
991
- if (value === "all") return "all";
992
- return isPositiveInteger(value) ? value : void 0;
993
- }
994
- function validSince(value) {
995
- return typeof value === "string" && parseCoChangeSince(value) !== void 0 ? value : void 0;
996
- }
997
- function resolveCoChangeConfig(config, overrides = {}) {
998
- const section = config.cochange;
999
- const since = validSince(overrides.since) ?? validSince(section?.since);
1000
- return {
1001
- maxCommits: validMaxCommits(overrides.maxCommits) ?? validMaxCommits(section?.maxCommits) ?? DEFAULT_COCHANGE_MAX_COMMITS,
1002
- ...since !== void 0 ? { since } : {},
1003
- maxFilesPerCommit: positiveIntegerOr(section?.maxFilesPerCommit, DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT),
1004
- minShared: positiveIntegerOr(section?.minShared, DEFAULT_COCHANGE_MIN_SHARED),
1005
- include: (section?.include ?? []).filter(isValidGlob),
1006
- exclude: (section?.exclude ?? []).filter(isValidGlob)
1007
- };
1008
- }
1009
- function nonNegativeOr(value, fallback) {
1010
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
1011
- }
1012
- function positiveOr(value, fallback) {
1013
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1014
- }
1015
- function resolveContextsViewConfig(config) {
1016
- const section = config.contextsView;
1017
- const hubs = section?.hubs;
1018
- const topPercent = hubs?.topPercent;
1019
- return {
1020
- weights: {
1021
- dependency: nonNegativeOr(section?.weights?.dependency, 1),
1022
- coChange: nonNegativeOr(section?.weights?.coChange, 1),
1023
- data: nonNegativeOr(section?.weights?.data, 0)
1024
- },
1025
- exclude: (section?.exclude ?? []).filter(isValidGlob),
1026
- hubs: {
1027
- auto: hubs?.auto !== false,
1028
- topPercent: typeof topPercent === "number" && topPercent > 0 && topPercent <= 100 ? topPercent : 1,
1029
- minDependents: positiveIntegerOr(hubs?.minDependents, 10),
1030
- paths: (hubs?.paths ?? []).filter(isValidGlob)
1031
- },
1032
- resolution: positiveOr(section?.resolution, 1),
1033
- minCandidateFiles: positiveIntegerOr(section?.minCandidateFiles, 3),
1034
- technicalTerms: section?.technicalTerms ? [...section.technicalTerms] : [...DEFAULT_TECHNICAL_TERMS]
1035
- };
1036
- }
1037
- function makeGlobMatcher(patterns) {
1038
- if (patterns.length === 0) return () => false;
1039
- const matchers = patterns.map((p) => picomatch(p, { dot: true }));
1040
- return (input) => {
1041
- const normalised = input.replace(/\\/g, "/");
1042
- return matchers.some((m) => m(normalised));
1043
- };
1044
- }
1045
-
1046
- // src/adapters/git/simple-git-adapter.ts
1047
- import { spawnSync } from "child_process";
1048
- import { simpleGit } from "simple-git";
1049
- var log5 = createLogger("ctxo:git");
1050
- var NUL = "\0";
1051
- var RECORD_SEPARATOR = "";
1052
- var FIELD_SEPARATOR = "";
1053
- var CHANGE_LOG_FORMAT = "--format=%x1e%H%x1f%aI%x1f%ct%x1f%an%x1f%s";
1054
- var STABLE_OUTPUT_CONFIG = [
1055
- "-c",
1056
- "core.quotepath=off",
1057
- "-c",
1058
- "log.showSignature=false"
1059
- ];
1060
- function toForwardSlashes(path) {
1061
- return path.replace(/\\/g, "/");
1062
- }
1063
- function isFileChangeStatus(value) {
1064
- return value === "A" || value === "M" || value === "D" || value === "R" || value === "C" || value === "T";
1065
- }
1066
- function parseNameStatus(tokens) {
1067
- const changes = [];
1068
- let i = 0;
1069
- while (i < tokens.length) {
1070
- const kind = (tokens[i] ?? "").trim().charAt(0);
1071
- if (kind === "") {
1072
- i += 1;
1073
- } else if (kind === "R" || kind === "C") {
1074
- const oldPath = tokens[i + 1];
1075
- const path = tokens[i + 2];
1076
- i += 3;
1077
- if (oldPath !== void 0 && path !== void 0) {
1078
- changes.push({ status: kind, path: toForwardSlashes(path), oldPath: toForwardSlashes(oldPath) });
1079
- }
1080
- } else {
1081
- const path = tokens[i + 1];
1082
- i += 2;
1083
- if (path !== void 0 && isFileChangeStatus(kind)) {
1084
- changes.push({ status: kind, path: toForwardSlashes(path) });
1085
- }
1086
- }
1087
- }
1088
- return changes;
1089
- }
1090
- function parseSkipWorktree(raw) {
1091
- const paths = /* @__PURE__ */ new Set();
1092
- for (const record of raw.split(NUL)) {
1093
- if (record.length > 2 && record.charAt(0) === "S" && record.charAt(1) === " ") {
1094
- paths.add(toForwardSlashes(record.slice(2)));
1095
- }
1096
- }
1097
- return paths;
1098
- }
1099
- function parseChangeLog(raw) {
1100
- const commits = [];
1101
- for (const record of raw.split(RECORD_SEPARATOR)) {
1102
- const [header = "", ...tokens] = record.split(NUL);
1103
- const [hash, date, time, author, ...subject] = header.split(FIELD_SEPARATOR);
1104
- const committedAt = Number.parseInt(time ?? "", 10);
1105
- if (!hash || date === void 0 || author === void 0 || Number.isNaN(committedAt)) continue;
1106
- commits.push({
1107
- hash,
1108
- message: subject.join(FIELD_SEPARATOR).trim(),
1109
- date,
1110
- author,
1111
- committedAt,
1112
- changes: parseNameStatus(tokens)
1113
- });
1114
- }
1115
- return commits;
1116
- }
1117
- var SimpleGitAdapter = class {
1118
- git;
1119
- projectRoot;
1120
- constructor(projectRoot) {
1121
- this.projectRoot = projectRoot;
1122
- this.git = simpleGit(projectRoot);
1123
- log5.debug("Initialized with root: %s", projectRoot);
1124
- }
1125
- async isAvailable() {
1126
- try {
1127
- await this.git.version();
1128
- return true;
1129
- } catch {
1130
- return false;
1131
- }
1132
- }
1133
- async getCommitHistory(filePath, maxCount) {
1134
- try {
1135
- const log8 = await this.git.log({
1136
- file: filePath,
1137
- "--follow": null,
1138
- ...maxCount ? { maxCount } : {}
1139
- });
1140
- return log8.all.map((entry) => ({
1141
- hash: entry.hash,
1142
- message: entry.message,
1143
- date: entry.date,
1144
- author: entry.author_name
1145
- }));
1146
- } catch (err) {
1147
- log5.error(` Failed to get history for ${filePath}: ${err.message}`);
1148
- return [];
1149
- }
1150
- }
1151
- async getChangedFiles(since) {
1152
- try {
1153
- const diff = await this.git.diffSummary([since]);
1154
- return diff.files.map((f) => f.file.replace(/\\/g, "/"));
1155
- } catch (err) {
1156
- log5.error(` Failed to get changed files since ${since}: ${err.message}`);
1157
- return [];
1158
- }
1159
- }
1160
- async getFileChurn(filePath) {
1161
- try {
1162
- const log8 = await this.git.log({ file: filePath, "--follow": null });
1163
- return {
1164
- filePath,
1165
- commitCount: log8.total
1166
- };
1167
- } catch (err) {
1168
- log5.error(` Failed to get churn for ${filePath}: ${err.message}`);
1169
- return { filePath, commitCount: 0 };
1170
- }
1171
- }
1172
- async getHeadSha() {
1173
- try {
1174
- const raw = await this.git.revparse(["--short", "HEAD"]);
1175
- const sha = raw.trim();
1176
- return sha.length > 0 ? sha : void 0;
1177
- } catch (err) {
1178
- log5.error(`Failed to resolve HEAD: ${err.message}`);
1179
- return void 0;
1180
- }
1181
- }
1182
- async countCommitsBetween(base) {
1183
- try {
1184
- const raw = await this.git.raw(["rev-list", "--count", `${base}..HEAD`]);
1185
- const n = parseInt(raw.trim(), 10);
1186
- return Number.isFinite(n) && n >= 0 ? n : void 0;
1187
- } catch (err) {
1188
- log5.error(`Failed to count commits since ${base}: ${err.message}`);
1189
- return void 0;
1190
- }
1191
- }
1192
- async getChangeLog(maxCommits) {
1193
- try {
1194
- const headRaw = await this.git.raw([...STABLE_OUTPUT_CONFIG, "log", "-1", "--format=%h%x1f%ct", "HEAD"]);
1195
- const [head, headTime] = headRaw.trim().split(FIELD_SEPARATOR);
1196
- const headCommittedAt = Number.parseInt(headTime ?? "", 10);
1197
- if (!head || Number.isNaN(headCommittedAt)) {
1198
- throw new Error(`unexpected HEAD description ${JSON.stringify(headRaw)}`);
1199
- }
1200
- const args = [...STABLE_OUTPUT_CONFIG, "log", "--no-merges", "-M", "--name-status", "-z", CHANGE_LOG_FORMAT];
1201
- if (maxCommits !== "all") args.push(`--max-count=${maxCommits}`);
1202
- args.push("HEAD");
1203
- return { head, headCommittedAt, commits: parseChangeLog(await this.git.raw(args)) };
1204
- } catch (err) {
1205
- log5.error(`Failed to read change log: ${err.message}`);
1206
- return void 0;
1207
- }
1208
- }
1209
- async getSkipWorktreePaths() {
1210
- try {
1211
- return parseSkipWorktree(await this.git.raw([...STABLE_OUTPUT_CONFIG, "ls-files", "-t", "-z"]));
1212
- } catch (err) {
1213
- log5.error(`Failed to list skip-worktree paths: ${err.message}`);
1214
- return void 0;
1215
- }
1216
- }
1217
- async getIgnoredPaths(paths) {
1218
- if (paths.length === 0) return /* @__PURE__ */ new Set();
1219
- const result = spawnSync("git", [...STABLE_OUTPUT_CONFIG, "check-ignore", "-z", "--stdin"], {
1220
- cwd: this.projectRoot,
1221
- input: paths.map((path) => `${path}${NUL}`).join(""),
1222
- encoding: "utf-8",
1223
- maxBuffer: 64 * 1024 * 1024
1224
- });
1225
- if (result.error === void 0 && result.status === 1) return /* @__PURE__ */ new Set();
1226
- if (result.error !== void 0 || result.status !== 0) {
1227
- const reason = (result.error?.message ?? result.stderr ?? "").replace(/\s+/g, " ").trim();
1228
- log5.error(`Failed to check ignored paths: ${reason}`);
1229
- return void 0;
1230
- }
1231
- return new Set(
1232
- result.stdout.split(NUL).filter((path) => path.length > 0).map(toForwardSlashes)
1233
- );
1234
- }
1235
- async getCommitFileChanges(rev) {
1236
- if (rev.startsWith("-")) {
1237
- log5.error(`Refusing to read file changes for option-like revision ${rev}`);
1238
- return void 0;
1239
- }
1240
- try {
1241
- const raw = await this.git.raw([
1242
- ...STABLE_OUTPUT_CONFIG,
1243
- "diff-tree",
1244
- "--root",
1245
- "--no-commit-id",
1246
- "-r",
1247
- "-M",
1248
- "--name-status",
1249
- "-z",
1250
- rev
1251
- ]);
1252
- return parseNameStatus(raw.split(NUL));
1253
- } catch (err) {
1254
- log5.error(`Failed to read file changes of ${rev}: ${err.message}`);
1255
- return void 0;
1256
- }
1257
- }
1258
- };
1259
-
1260
- // src/adapters/storage/contexts-view-reader.ts
1261
- import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
1262
- import { join as join5 } from "path";
1263
-
1264
- // src/core/contexts-view/contexts-view-schema.ts
1265
- import { DATA_RESOURCE_KINDS } from "@ctxo/plugin-api";
1266
- import { z as z2 } from "zod";
1267
- var count = z2.number().int().nonnegative();
1268
- var SettingsSchema = z2.object({
1269
- // A PR 1A artifact has no data weight; it was 0 then.
1270
- weights: z2.object({ dependency: z2.number(), coChange: z2.number(), data: z2.number().default(0) }),
1271
- exclude: z2.array(z2.string()),
1272
- hubs: z2.object({
1273
- auto: z2.boolean(),
1274
- topPercent: z2.number(),
1275
- minDependents: z2.number(),
1276
- paths: z2.array(z2.string())
1277
- }),
1278
- resolution: z2.number(),
1279
- minCandidateFiles: z2.number(),
1280
- technicalTerms: z2.array(z2.string())
1281
- });
1282
- var DataAccessSchema = z2.object({
1283
- candidate: z2.number().int(),
1284
- modes: z2.array(z2.enum(["reads", "writes"]))
1285
- });
1286
- var SharedTableSchema = z2.object({
1287
- name: z2.string(),
1288
- kind: z2.enum(DATA_RESOURCE_KINDS).optional(),
1289
- external: z2.boolean(),
1290
- accesses: z2.array(DataAccessSchema),
1291
- owner: z2.number().int().optional()
1292
- });
1293
- var DataResourceSchema = z2.object({
1294
- name: z2.string(),
1295
- kind: z2.enum(DATA_RESOURCE_KINDS).optional(),
1296
- external: z2.boolean(),
1297
- accesses: z2.array(DataAccessSchema),
1298
- writers: z2.array(z2.number().int()),
1299
- owner: z2.number().int().optional()
1300
- });
1301
- var CandidateSchema = z2.object({
1302
- id: z2.number().int(),
1303
- label: z2.string(),
1304
- fingerprint: z2.string(),
1305
- fileCount: count,
1306
- files: z2.array(z2.string()),
1307
- cohesion: z2.number(),
1308
- topTerms: z2.array(z2.object({ term: z2.string(), files: count })),
1309
- crossEdges: z2.array(
1310
- z2.object({
1311
- candidate: z2.number().int(),
1312
- total: count,
1313
- byKind: z2.partialRecord(EdgeKindSchema, count)
1314
- })
1315
- ),
1316
- coChangePairs: z2.array(
1317
- z2.object({
1318
- file: z2.string(),
1319
- otherFile: z2.string(),
1320
- otherCandidate: z2.number().int(),
1321
- frequency: z2.number(),
1322
- sharedCommits: count
1323
- })
1324
- ),
1325
- // Absent in a PR 1A artifact.
1326
- sharedTables: z2.array(SharedTableSchema).default([])
1327
- });
1328
- var HubSchema = z2.object({
1329
- file: z2.string(),
1330
- reasons: z2.array(z2.enum(["configured", "fan-in", "god-node"])),
1331
- dependents: count,
1332
- damping: z2.number(),
1333
- candidate: z2.number().int().optional()
1334
- });
1335
- var CollisionSchema = z2.object({
1336
- name: z2.string(),
1337
- minJaccard: z2.number(),
1338
- occurrences: z2.array(
1339
- z2.object({
1340
- candidate: z2.number().int(),
1341
- file: z2.string(),
1342
- symbolId: z2.string(),
1343
- members: z2.array(z2.string())
1344
- })
1345
- )
1346
- });
1347
- var ContextsViewSchema = z2.object({
1348
- version: z2.literal(1),
1349
- commitSha: z2.string(),
1350
- settings: SettingsSchema,
1351
- coverage: z2.object({ files: count, covered: count, ratio: z2.number() }),
1352
- candidates: z2.array(CandidateSchema),
1353
- hubs: z2.array(HubSchema),
1354
- unclustered: z2.object({ count, files: z2.array(z2.string()) }),
1355
- collisions: z2.array(CollisionSchema),
1356
- // Absent in a PR 1A artifact.
1357
- dataResources: z2.array(DataResourceSchema).default([])
1358
- });
1359
-
1360
- // src/adapters/storage/contexts-view-reader.ts
1361
- var log6 = createLogger("ctxo:contexts-view");
1362
- var UNTIL_REWRITTEN2 = "the contexts view is ignored until ctxo index rewrites it";
1363
- var UNTIL_RETRY2 = "the contexts view is ignored for now and the next call tries again";
1364
- function deepFreeze(value) {
1365
- if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
1366
- Object.freeze(value);
1367
- for (const child of Object.values(value)) deepFreeze(child);
1368
- }
1369
- return value;
1370
- }
1371
- var ContextsViewReader = class {
1372
- filePath;
1373
- cached;
1374
- lastWarning;
1375
- constructor(ctxoRoot) {
1376
- this.filePath = join5(ctxoRoot, "index", "contexts-view.json");
1377
- }
1378
- readContextsView() {
1379
- let stats;
1380
- try {
1381
- stats = statSync2(this.filePath);
1382
- } catch (err) {
1383
- this.cached = void 0;
1384
- const { code, message } = err;
1385
- if (code === "ENOENT") {
1386
- this.lastWarning = void 0;
1387
- } else {
1388
- this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY2);
1389
- }
1390
- return void 0;
1391
- }
1392
- const version = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
1393
- if (this.cached?.version === version) return this.cached.view;
1394
- return this.load(version);
1395
- }
1396
- load(version) {
1397
- let raw;
1398
- try {
1399
- raw = readFileSync5(this.filePath, "utf-8");
1400
- } catch (err) {
1401
- this.cached = void 0;
1402
- this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY2);
1403
- return void 0;
1404
- }
1405
- const view = this.parse(raw, version);
1406
- this.cached = { version, view };
1407
- if (view) this.lastWarning = void 0;
1408
- return view;
1409
- }
1410
- parse(raw, version) {
1411
- let data;
1412
- try {
1413
- data = JSON.parse(raw);
1414
- } catch (err) {
1415
- this.warnOnce(`content ${version}`, `is not valid JSON (${err.message})`, UNTIL_REWRITTEN2);
1416
- return void 0;
1417
- }
1418
- const parsed = ContextsViewSchema.safeParse(data);
1419
- if (!parsed.success) {
1420
- const issue = parsed.error.issues[0];
1421
- const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
1422
- this.warnOnce(
1423
- `content ${version}`,
1424
- `is not a valid contexts view (${where}${issue?.message ?? "unknown problem"})`,
1425
- UNTIL_REWRITTEN2
1426
- );
1427
- return void 0;
1428
- }
1429
- return deepFreeze(parsed.data);
1430
- }
1431
- warnOnce(key, problem, consequence) {
1432
- if (this.lastWarning === key) return;
1433
- this.lastWarning = key;
1434
- log6.warn(`${this.filePath} ${problem}; ${consequence}`);
1435
- }
1436
- };
1437
-
1438
- // src/adapters/stats/session-recorder-adapter.ts
1439
- var log7 = createLogger("ctxo:stats");
1440
- var SessionRecorderAdapter = class {
1441
- db;
1442
- onWrite;
1443
- tableCreated = false;
1444
- constructor(db, onWrite) {
1445
- this.db = db;
1446
- this.onWrite = onWrite ?? null;
745
+ // src/adapters/stats/session-recorder-adapter.ts
746
+ var log4 = createLogger("ctxo:stats");
747
+ var SessionRecorderAdapter = class {
748
+ db;
749
+ onWrite;
750
+ tableCreated = false;
751
+ constructor(db, onWrite) {
752
+ this.db = db;
753
+ this.onWrite = onWrite ?? null;
1447
754
  }
1448
755
  ensureTable() {
1449
756
  if (this.tableCreated) return;
@@ -1482,7 +789,7 @@ var SessionRecorderAdapter = class {
1482
789
  );
1483
790
  this.flushToDisk();
1484
791
  } catch (err) {
1485
- log7.error(`Failed to record session event: ${err.message}`);
792
+ log4.error(`Failed to record session event: ${err.message}`);
1486
793
  }
1487
794
  }
1488
795
  queryStats(days) {
@@ -1550,319 +857,186 @@ var SessionRecorderAdapter = class {
1550
857
  try {
1551
858
  this.onWrite?.();
1552
859
  } catch (err) {
1553
- log7.error(`Failed to flush stats to disk: ${err.message}`);
860
+ log4.error(`Failed to flush stats to disk: ${err.message}`);
1554
861
  }
1555
862
  }
1556
863
  };
1557
864
 
1558
- // src/adapters/workspace/single-package-workspace.ts
1559
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
1560
- import { join as join6, resolve as resolve2 } from "path";
1561
- var MANIFEST_CANDIDATES = [
1562
- "package.json",
1563
- "pyproject.toml",
1564
- "setup.py",
1565
- "pom.xml",
1566
- "build.gradle",
1567
- "build.gradle.kts",
1568
- "go.mod",
1569
- "Cargo.toml"
1570
- ];
1571
- function findManifest(dir) {
1572
- for (const name of MANIFEST_CANDIDATES) {
1573
- const candidate = join6(dir, name);
1574
- if (existsSync4(candidate)) return candidate;
1575
- }
1576
- const csproj = findFirstByExt(dir, ".csproj") ?? findFirstByExt(dir, ".sln");
1577
- if (csproj) return csproj;
1578
- return join6(dir, "package.json");
1579
- }
1580
- function findFirstByExt(dir, ext) {
1581
- try {
1582
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1583
- if (entry.isFile() && entry.name.endsWith(ext)) return join6(dir, entry.name);
1584
- }
1585
- } catch {
1586
- }
1587
- return null;
1588
- }
1589
- function detectWorkspace(cwd) {
1590
- const root = resolve2(cwd);
1591
- const pkg = {
1592
- root,
1593
- manifest: findManifest(root)
1594
- };
1595
- return {
1596
- root,
1597
- packages: [pkg]
1598
- };
1599
- }
1600
-
1601
- // src/core/data-coupling/data-kinds.ts
1602
- import { DATA_RESOURCE_KINDS as DATA_RESOURCE_KINDS2 } from "@ctxo/plugin-api";
1603
- var DATA_EDGE_KINDS = /* @__PURE__ */ new Set(["reads", "writes", "executes"]);
1604
- var PROCEDURE_KINDS = ["procedure"];
1605
- function isDataResourceKind(kind) {
1606
- return DATA_RESOURCE_KINDS2.includes(kind);
1607
- }
1608
- function dataReferenceKinds(kind) {
1609
- if (isDataResourceKind(kind)) return DATA_RESOURCE_KINDS2;
1610
- if (kind === "procedure") return PROCEDURE_KINDS;
1611
- return void 0;
1612
- }
865
+ // src/adapters/storage/contexts-view-reader.ts
866
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
867
+ import { join as join5 } from "path";
1613
868
 
1614
- // src/core/test-files/test-file-classifier.ts
1615
- var DEFAULT_TEST_GLOBS = [
1616
- "**/__tests__/**",
1617
- "**/*.test.*",
1618
- "**/*.spec.*",
1619
- "**/test/**",
1620
- "**/tests/**",
1621
- "**/*_test.go",
1622
- "**/src/test/**",
1623
- "**/*.Tests/**",
1624
- "**/*.Test/**",
1625
- "**/*Tests.cs",
1626
- "**/*Test.cs",
1627
- "**/test_*.py",
1628
- "**/*_test.py"
1629
- ];
1630
- var NAME_ONLY_TEST_GLOBS = ["**/*Tests.cs", "**/*Test.cs"];
1631
- var TEST_FRAMEWORK_ANNOTATIONS = [
1632
- "Fact",
1633
- "Theory",
1634
- "SkippableFact",
1635
- "SkippableTheory",
1636
- "Test",
1637
- "TestCase",
1638
- "TestCaseSource",
1639
- "TestFixture",
1640
- "TestMethod",
1641
- "DataTestMethod",
1642
- "TestClass"
1643
- ];
1644
- function normalisePath(path) {
1645
- return path.replace(/\\/g, "/");
1646
- }
1647
- function filesWithTestAnnotations(indices) {
1648
- const names = new Set(TEST_FRAMEWORK_ANNOTATIONS);
1649
- const files = /* @__PURE__ */ new Set();
1650
- for (const index of indices) {
1651
- const marked = index.symbols.some((symbol) => symbol.annotations?.some((annotation) => names.has(annotation.name)));
1652
- if (marked) files.add(normalisePath(index.file));
1653
- }
1654
- return files;
1655
- }
1656
- function createTestFileClassifier(extraGlobs = [], annotatedFiles, options = {}) {
1657
- const nameOnly = new Set(NAME_ONLY_TEST_GLOBS);
1658
- const defaults = options.annotationAware === true ? DEFAULT_TEST_GLOBS.filter((glob) => !nameOnly.has(glob)) : DEFAULT_TEST_GLOBS;
1659
- const matchesGlob = makeGlobMatcher([...defaults, ...extraGlobs]);
1660
- return (path) => annotatedFiles?.has(normalisePath(path)) === true || matchesGlob(path);
1661
- }
869
+ // src/core/contexts-view/contexts-view-schema.ts
870
+ import { DATA_RESOURCE_KINDS } from "@ctxo/plugin-api";
871
+ import { z } from "zod";
872
+ var count = z.number().int().nonnegative();
873
+ var SettingsSchema = z.object({
874
+ // A PR 1A artifact has no data weight; it was 0 then.
875
+ weights: z.object({ dependency: z.number(), coChange: z.number(), data: z.number().default(0) }),
876
+ exclude: z.array(z.string()),
877
+ hubs: z.object({
878
+ auto: z.boolean(),
879
+ topPercent: z.number(),
880
+ minDependents: z.number(),
881
+ paths: z.array(z.string())
882
+ }),
883
+ resolution: z.number(),
884
+ minCandidateFiles: z.number(),
885
+ technicalTerms: z.array(z.string())
886
+ });
887
+ var DataAccessSchema = z.object({
888
+ candidate: z.number().int(),
889
+ modes: z.array(z.enum(["reads", "writes"]))
890
+ });
891
+ var SharedTableSchema = z.object({
892
+ name: z.string(),
893
+ kind: z.enum(DATA_RESOURCE_KINDS).optional(),
894
+ external: z.boolean(),
895
+ accesses: z.array(DataAccessSchema),
896
+ owner: z.number().int().optional()
897
+ });
898
+ var DataResourceSchema = z.object({
899
+ name: z.string(),
900
+ kind: z.enum(DATA_RESOURCE_KINDS).optional(),
901
+ external: z.boolean(),
902
+ accesses: z.array(DataAccessSchema),
903
+ writers: z.array(z.number().int()),
904
+ owner: z.number().int().optional()
905
+ });
906
+ var CandidateSchema = z.object({
907
+ id: z.number().int(),
908
+ label: z.string(),
909
+ fingerprint: z.string(),
910
+ fileCount: count,
911
+ files: z.array(z.string()),
912
+ cohesion: z.number(),
913
+ topTerms: z.array(z.object({ term: z.string(), files: count })),
914
+ crossEdges: z.array(
915
+ z.object({
916
+ candidate: z.number().int(),
917
+ total: count,
918
+ byKind: z.partialRecord(EdgeKindSchema, count)
919
+ })
920
+ ),
921
+ coChangePairs: z.array(
922
+ z.object({
923
+ file: z.string(),
924
+ otherFile: z.string(),
925
+ otherCandidate: z.number().int(),
926
+ frequency: z.number(),
927
+ sharedCommits: count
928
+ })
929
+ ),
930
+ // Absent in a PR 1A artifact.
931
+ sharedTables: z.array(SharedTableSchema).default([])
932
+ });
933
+ var HubSchema = z.object({
934
+ file: z.string(),
935
+ reasons: z.array(z.enum(["configured", "fan-in", "god-node"])),
936
+ dependents: count,
937
+ damping: z.number(),
938
+ candidate: z.number().int().optional()
939
+ });
940
+ var CollisionSchema = z.object({
941
+ name: z.string(),
942
+ minJaccard: z.number(),
943
+ occurrences: z.array(
944
+ z.object({
945
+ candidate: z.number().int(),
946
+ file: z.string(),
947
+ symbolId: z.string(),
948
+ members: z.array(z.string())
949
+ })
950
+ )
951
+ });
952
+ var ContextsViewSchema = z.object({
953
+ version: z.literal(1),
954
+ commitSha: z.string(),
955
+ settings: SettingsSchema,
956
+ coverage: z.object({ files: count, covered: count, ratio: z.number() }),
957
+ candidates: z.array(CandidateSchema),
958
+ hubs: z.array(HubSchema),
959
+ unclustered: z.object({ count, files: z.array(z.string()) }),
960
+ collisions: z.array(CollisionSchema),
961
+ // Absent in a PR 1A artifact.
962
+ dataResources: z.array(DataResourceSchema).default([])
963
+ });
1662
964
 
1663
- // src/core/graph/symbol-graph.ts
1664
- var isDefaultTestFile = createTestFileClassifier();
1665
- var DOCUMENTATION_FOLDERS = /* @__PURE__ */ new Set(["docs", "doc", "documentation"]);
1666
- function asciiLower(text) {
1667
- return text.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
1668
- }
1669
- function isUnderDocumentationFolder(file) {
1670
- const folders = file.replace(/\\/g, "/").split("/").slice(0, -1);
1671
- return folders.some((folder) => DOCUMENTATION_FOLDERS.has(asciiLower(folder)));
1672
- }
1673
- function definitionRank(symbolId) {
1674
- const at = symbolId.indexOf("::");
1675
- const file = at < 0 ? symbolId : symbolId.slice(0, at);
1676
- return (isDefaultTestFile(file) ? 2 : 0) + (isUnderDocumentationFolder(file) ? 1 : 0);
965
+ // src/adapters/storage/contexts-view-reader.ts
966
+ var log5 = createLogger("ctxo:contexts-view");
967
+ var UNTIL_REWRITTEN2 = "the contexts view is ignored until ctxo index rewrites it";
968
+ var UNTIL_RETRY2 = "the contexts view is ignored for now and the next call tries again";
969
+ function deepFreeze(value) {
970
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
971
+ Object.freeze(value);
972
+ for (const child of Object.values(value)) deepFreeze(child);
973
+ }
974
+ return value;
1677
975
  }
1678
- var SymbolGraph = class {
1679
- nodes = /* @__PURE__ */ new Map();
1680
- nodesByFileAndName = /* @__PURE__ */ new Map();
1681
- nodesByNameAndKind = /* @__PURE__ */ new Map();
1682
- forwardEdges = /* @__PURE__ */ new Map();
1683
- reverseEdges = /* @__PURE__ */ new Map();
1684
- definitionRanks = /* @__PURE__ */ new Map();
1685
- edgeSet = /* @__PURE__ */ new Set();
1686
- addNode(node) {
1687
- this.nodes.set(node.symbolId, node);
1688
- const parts = node.symbolId.split("::");
1689
- const [p0, p1] = parts;
1690
- if (p0 !== void 0 && p1 !== void 0) {
1691
- this.nodesByFileAndName.set(`${p0}::${p1}`, node);
1692
- }
1693
- if (parts.length === 3) {
1694
- const nk = `${parts[1]}::${parts[2]}`;
1695
- const list = this.nodesByNameAndKind.get(nk);
1696
- if (list) list.push(node);
1697
- else this.nodesByNameAndKind.set(nk, [node]);
1698
- }
1699
- }
1700
- addEdge(edge) {
1701
- const resolvedFrom = this.resolveNodeId(edge.from);
1702
- const resolvedTo = this.resolveNodeId(edge.to);
1703
- const resolvedEdge = { from: resolvedFrom, to: resolvedTo, kind: edge.kind };
1704
- const edgeKey = `${resolvedEdge.from}|${resolvedEdge.to}|${resolvedEdge.kind}`;
1705
- if (this.edgeSet.has(edgeKey)) return;
1706
- this.edgeSet.add(edgeKey);
1707
- const forward = this.forwardEdges.get(resolvedEdge.from) ?? [];
1708
- forward.push(resolvedEdge);
1709
- this.forwardEdges.set(resolvedEdge.from, forward);
1710
- const reverse = this.reverseEdges.get(resolvedEdge.to) ?? [];
1711
- reverse.push(resolvedEdge);
1712
- this.reverseEdges.set(resolvedEdge.to, reverse);
1713
- }
1714
- getNode(symbolId) {
1715
- return this.nodes.get(symbolId) ?? this.resolveNodeFuzzy(symbolId);
1716
- }
1717
- resolveNodeId(id) {
1718
- if (this.nodes.has(id)) return id;
1719
- const segs = id.split("::");
1720
- const [p0, p1] = segs;
1721
- if (p0 !== void 0 && p1 !== void 0) {
1722
- const fuzzyKey = `${p0}::${p1}`;
1723
- const match = this.nodesByFileAndName.get(fuzzyKey);
1724
- if (match) return match.symbolId;
1725
- const jsToTs = p0.replace(/\.js$/, ".ts");
1726
- if (jsToTs !== p0) {
1727
- const altKey = `${jsToTs}::${p1}`;
1728
- const altMatch = this.nodesByFileAndName.get(altKey);
1729
- if (altMatch) return altMatch.symbolId;
1730
- }
1731
- }
1732
- const dataMatch = this.resolveDataReference(segs);
1733
- if (dataMatch) return dataMatch.symbolId;
1734
- const nameKind = this.extractNameKind(segs);
1735
- if (nameKind) {
1736
- const candidates = this.nodesByNameAndKind.get(nameKind);
1737
- if (candidates && candidates.length === 1) return candidates[0].symbolId;
1738
- }
1739
- return id;
1740
- }
1741
- resolveNodeFuzzy(id) {
1742
- const segs = id.split("::");
1743
- const [p0, p1] = segs;
1744
- if (p0 !== void 0 && p1 !== void 0) {
1745
- const match = this.nodesByFileAndName.get(`${p0}::${p1}`);
1746
- if (match) return match;
1747
- const jsToTs = p0.replace(/\.js$/, ".ts");
1748
- if (jsToTs !== p0) {
1749
- const tsMatch = this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
1750
- if (tsMatch) return tsMatch;
1751
- }
1752
- }
1753
- const dataMatch = this.resolveDataReference(segs);
1754
- if (dataMatch) return dataMatch;
1755
- const nameKind = this.extractNameKind(segs);
1756
- if (nameKind) {
1757
- const candidates = this.nodesByNameAndKind.get(nameKind);
1758
- if (candidates && candidates.length === 1) return candidates[0];
1759
- }
1760
- return void 0;
976
+ var ContextsViewReader = class {
977
+ filePath;
978
+ cached;
979
+ lastWarning;
980
+ constructor(ctxoRoot) {
981
+ this.filePath = join5(ctxoRoot, "index", "contexts-view.json");
1761
982
  }
1762
- /**
1763
- * Resolves a 2-part data reference (`dbo.loans::table`). A table or view reference is kind-agnostic
1764
- * across data resource kinds, a procedure reference matches procedures only. Several definitions
1765
- * resolve by `definitionRank` (a production copy before a documentation copy before a test copy), then
1766
- * to the codepoint-smallest symbolId, which is the first file in sorted path order. The result does
1767
- * not depend on insertion order.
1768
- */
1769
- resolveDataReference(segs) {
1770
- if (segs.length !== 2) return void 0;
1771
- const [name, kind] = segs;
1772
- const kinds = dataReferenceKinds(kind);
1773
- if (!kinds) return void 0;
1774
- let best;
1775
- for (const k of kinds) {
1776
- for (const node of this.nodesByNameAndKind.get(`${name}::${k}`) ?? []) {
1777
- if (!best || this.compareDefinitions(node, best) < 0) best = node;
983
+ readContextsView() {
984
+ let stats;
985
+ try {
986
+ stats = statSync2(this.filePath);
987
+ } catch (err) {
988
+ this.cached = void 0;
989
+ const { code, message } = err;
990
+ if (code === "ENOENT") {
991
+ this.lastWarning = void 0;
992
+ } else {
993
+ this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY2);
1778
994
  }
995
+ return void 0;
1779
996
  }
1780
- return best;
997
+ const version = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
998
+ if (this.cached?.version === version) return this.cached.view;
999
+ return this.load(version);
1781
1000
  }
1782
- /** The tie-break order of same-name definitions: `definitionRank`, then the codepoint-smaller symbolId. */
1783
- compareDefinitions = (a, b) => this.definitionRankOf(a.symbolId) - this.definitionRankOf(b.symbolId) || compareCodepoint(a.symbolId, b.symbolId);
1784
- /**
1785
- * The identity of the table, view or procedure that `id` names: a full id of any of its definitions, or a
1786
- * 2-part reference such as `dbo.loans::table`. Undefined when `id` names no node, or a node of another
1787
- * kind. A table or view identity spans both kinds, as a 2-part `::table` reference does.
1788
- */
1789
- dataResourceIdentity(id) {
1790
- const node = this.getNode(id);
1791
- return node ? this.identityOf(node) : void 0;
1792
- }
1793
- identityOf(node) {
1794
- const segs = node.symbolId.split("::");
1795
- if (segs.length !== 3) return void 0;
1796
- const [, name, kind] = segs;
1797
- const kinds = dataReferenceKinds(kind);
1798
- if (!kinds) return void 0;
1799
- const byId = /* @__PURE__ */ new Map();
1800
- for (const k of kinds) {
1801
- for (const definition of this.nodesByNameAndKind.get(`${name}::${k}`) ?? []) {
1802
- byId.set(definition.symbolId, this.nodes.get(definition.symbolId) ?? definition);
1803
- }
1001
+ load(version) {
1002
+ let raw;
1003
+ try {
1004
+ raw = readFileSync4(this.filePath, "utf-8");
1005
+ } catch (err) {
1006
+ this.cached = void 0;
1007
+ this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY2);
1008
+ return void 0;
1804
1009
  }
1805
- const definitions = [...byId.values()].sort(this.compareDefinitions);
1806
- const winner = definitions[0] ?? node;
1807
- const production = definitions.filter((definition) => this.definitionRankOf(definition.symbolId) === 0);
1808
- return { canonicalId: `${name}::${winner.kind}`, winner, definitions, production };
1010
+ const view = this.parse(raw, version);
1011
+ this.cached = { version, view };
1012
+ if (view) this.lastWarning = void 0;
1013
+ return view;
1809
1014
  }
1810
- /** `definitionRank`, cached per symbolId (the glob match runs once per definition). */
1811
- definitionRankOf(symbolId) {
1812
- let rank = this.definitionRanks.get(symbolId);
1813
- if (rank === void 0) {
1814
- rank = definitionRank(symbolId);
1815
- this.definitionRanks.set(symbolId, rank);
1015
+ parse(raw, version) {
1016
+ let data;
1017
+ try {
1018
+ data = JSON.parse(raw);
1019
+ } catch (err) {
1020
+ this.warnOnce(`content ${version}`, `is not valid JSON (${err.message})`, UNTIL_REWRITTEN2);
1021
+ return void 0;
1816
1022
  }
1817
- return rank;
1818
- }
1819
- /**
1820
- * Extracts a `name::kind` key from the segments of a reference ID, supporting:
1821
- * - 2-part: `Bar::class` → `Bar::class`
1822
- * - 3-part: `pkg.Bar::helper::method` → `helper::method`
1823
- * Returns undefined for 1-part IDs (no kind information available).
1824
- */
1825
- extractNameKind(segs) {
1826
- if (segs.length === 3) return `${segs[1]}::${segs[2]}`;
1827
- if (segs.length === 2) return `${segs[0]}::${segs[1]}`;
1828
- return void 0;
1829
- }
1830
- getForwardEdges(symbolId) {
1831
- return this.forwardEdges.get(symbolId) ?? [];
1832
- }
1833
- getReverseEdges(symbolId) {
1834
- return this.reverseEdges.get(symbolId) ?? [];
1835
- }
1836
- /**
1837
- * The dependents of a resolved symbolId, as the impact tools read them. For a definition of a table, view
1838
- * or procedure these are the incoming edges of every same-name definition, which in practice are the
1839
- * winner's: any copy then reports the dependents of the resource. For every other id they are its own
1840
- * incoming edges, as `getReverseEdges` gives them.
1841
- */
1842
- getDependentEdges(symbolId) {
1843
- const node = this.nodes.get(symbolId);
1844
- const definitions = node ? this.identityOf(node)?.definitions : void 0;
1845
- if (!definitions || definitions.length < 2) return this.getReverseEdges(symbolId);
1846
- return definitions.flatMap((definition) => this.getReverseEdges(definition.symbolId));
1847
- }
1848
- hasNode(symbolId) {
1849
- return this.nodes.has(symbolId) || this.resolveNodeFuzzy(symbolId) !== void 0;
1850
- }
1851
- get nodeCount() {
1852
- return this.nodes.size;
1853
- }
1854
- get edgeCount() {
1855
- return this.edgeSet.size;
1856
- }
1857
- allNodes() {
1858
- return [...this.nodes.values()];
1859
- }
1860
- allEdges() {
1861
- const edges = [];
1862
- for (const list of this.forwardEdges.values()) {
1863
- edges.push(...list);
1023
+ const parsed = ContextsViewSchema.safeParse(data);
1024
+ if (!parsed.success) {
1025
+ const issue = parsed.error.issues[0];
1026
+ const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
1027
+ this.warnOnce(
1028
+ `content ${version}`,
1029
+ `is not a valid contexts view (${where}${issue?.message ?? "unknown problem"})`,
1030
+ UNTIL_REWRITTEN2
1031
+ );
1032
+ return void 0;
1864
1033
  }
1865
- return edges;
1034
+ return deepFreeze(parsed.data);
1035
+ }
1036
+ warnOnce(key, problem, consequence) {
1037
+ if (this.lastWarning === key) return;
1038
+ this.lastWarning = key;
1039
+ log5.warn(`${this.filePath} ${problem}; ${consequence}`);
1866
1040
  }
1867
1041
  };
1868
1042
 
@@ -2066,183 +1240,181 @@ function hintFor(distinctCommits) {
2066
1240
  return "Install the post-commit hook (`ctxo init`) or run `ctxo index` after commits to build history; `ctxo index --check` does not record snapshots.";
2067
1241
  }
2068
1242
 
2069
- // src/core/blast-radius/blast-radius-calculator.ts
2070
- var EDGE_KIND_CONFIDENCE = {
2071
- calls: "confirmed",
2072
- extends: "confirmed",
2073
- implements: "confirmed",
2074
- reads: "confirmed",
2075
- writes: "confirmed",
2076
- executes: "confirmed",
2077
- uses: "likely",
2078
- imports: "potential"
2079
- };
2080
- var CONFIDENCE_RANK = {
2081
- confirmed: 2,
2082
- likely: 1,
2083
- potential: 0
2084
- };
2085
- function edgeKindToConfidence(kind) {
2086
- return EDGE_KINDS.includes(kind) ? EDGE_KIND_CONFIDENCE[kind] : "potential";
2087
- }
2088
- var BlastRadiusCalculator = class {
2089
- /**
2090
- * `symbolId` may be any id `SymbolGraph.getNode` resolves, a 2-part data id included. The root is the
2091
- * node it names, and the root's own file is the one co-change is matched against. Dependents come from
2092
- * `getDependentEdges`, so every copy of a table, view or procedure, and its 2-part id, has the dependents
2093
- * of the resource, and no definition of the resource counts as a dependent of another.
2094
- */
2095
- calculate(graph, symbolId, coChangeMap) {
2096
- const root = graph.getNode(symbolId);
2097
- if (!root) {
2098
- return { impactedSymbols: [], directDependentsCount: 0, confirmedCount: 0, likelyCount: 0, potentialCount: 0, overallRiskScore: 0 };
2099
- }
2100
- const rootId = root.symbolId;
2101
- const sourceFile = rootId.split("::")[0];
2102
- const definitions = graph.dataResourceIdentity(rootId)?.definitions.map((definition) => definition.symbolId) ?? [];
2103
- const visited = /* @__PURE__ */ new Set([rootId, ...definitions]);
2104
- const rawEntries = [];
2105
- const queue = [{ id: rootId, depth: 0 }];
2106
- while (queue.length > 0) {
2107
- const current = queue.shift();
2108
- const reverseEdges = graph.getDependentEdges(current.id);
2109
- const bestByNode = /* @__PURE__ */ new Map();
2110
- for (const edge of reverseEdges) {
2111
- if (visited.has(edge.from)) continue;
2112
- const conf = edgeKindToConfidence(edge.kind);
2113
- const existing = bestByNode.get(edge.from);
2114
- if (!existing) {
2115
- bestByNode.set(edge.from, { confidence: conf, kinds: /* @__PURE__ */ new Set([edge.kind]) });
2116
- } else {
2117
- existing.kinds.add(edge.kind);
2118
- if (CONFIDENCE_RANK[conf] > CONFIDENCE_RANK[existing.confidence]) {
2119
- existing.confidence = conf;
2120
- }
2121
- }
2122
- }
2123
- for (const [nodeId, info] of bestByNode) {
2124
- visited.add(nodeId);
2125
- const depth = current.depth + 1;
2126
- const riskScore = 1 / Math.pow(depth, 0.7);
2127
- let confidence = info.confidence;
2128
- let coChangeFrequency;
2129
- if (coChangeMap) {
2130
- const nodeFile = nodeId.split("::")[0];
2131
- const coEntries = coChangeMap.get(sourceFile);
2132
- if (coEntries) {
2133
- const match = coEntries.find((e) => (e.file1 === sourceFile ? e.file2 : e.file1) === nodeFile);
2134
- if (match) {
2135
- coChangeFrequency = match.frequency;
2136
- if (confidence === "potential" && match.frequency > 0.5) {
2137
- confidence = "likely";
2138
- }
2139
- }
2140
- }
2141
- }
2142
- rawEntries.push({
2143
- symbolId: nodeId,
2144
- depth,
2145
- riskScore: Math.round(riskScore * 1e3) / 1e3,
2146
- confidence,
2147
- edgeKinds: [...info.kinds],
2148
- coChangeFrequency
1243
+ // src/core/overlay/boundary-violation-detector.ts
1244
+ var HIGH_SEVERITY_THRESHOLD = 0;
1245
+ var MEDIUM_SEVERITY_THRESHOLD = 2;
1246
+ var MIN_DISTINCT_COMMITS_FOR_CONFIDENCE = 2;
1247
+ var BoundaryViolationDetector = class {
1248
+ detect(graph, current, history) {
1249
+ const snapshotsAvailable = history.length;
1250
+ const distinctCommits = distinctCommitCount(history, current.commitSha);
1251
+ const currentAssignments = assignmentMap2(current.communities);
1252
+ const currentLabels = labelMap2(current.communities);
1253
+ const earlierCommits = priorCommitSnapshots(history, current.commitSha);
1254
+ const historicalAssignments = earlierCommits.map((snapshot) => ({
1255
+ snapshot,
1256
+ assignments: assignmentMap2(snapshot.communities),
1257
+ labels: labelMap2(snapshot.communities)
1258
+ }));
1259
+ const violations = [];
1260
+ for (const edge of graph.allEdges()) {
1261
+ if (edge.from === edge.to) continue;
1262
+ if (DATA_EDGE_KINDS.has(edge.kind)) continue;
1263
+ const fromCommunity = currentAssignments.get(edge.from);
1264
+ const toCommunity = currentAssignments.get(edge.to);
1265
+ if (fromCommunity === void 0 || toCommunity === void 0) continue;
1266
+ if (fromCommunity === toCommunity) continue;
1267
+ const fromLabel = currentLabels.get(fromCommunity) ?? `community-${fromCommunity}`;
1268
+ const toLabel = currentLabels.get(toCommunity) ?? `community-${toCommunity}`;
1269
+ const historicalCount = distinctCommits < MIN_DISTINCT_COMMITS_FOR_CONFIDENCE ? -1 : countHistoricalCrossCluster(historicalAssignments, edge.from, edge.to);
1270
+ if (historicalCount === -1 || historicalCount <= MEDIUM_SEVERITY_THRESHOLD) {
1271
+ violations.push({
1272
+ from: {
1273
+ symbolId: edge.from,
1274
+ communityId: fromCommunity,
1275
+ label: fromLabel
1276
+ },
1277
+ to: {
1278
+ symbolId: edge.to,
1279
+ communityId: toCommunity,
1280
+ label: toLabel
1281
+ },
1282
+ edgeKind: edge.kind,
1283
+ historicalEdgesBetweenClusters: historicalCount,
1284
+ severity: historicalCount === HIGH_SEVERITY_THRESHOLD ? "high" : "medium"
2149
1285
  });
2150
- queue.push({ id: nodeId, depth });
2151
1286
  }
2152
1287
  }
2153
- const blastSet = /* @__PURE__ */ new Set([rootId, ...definitions, ...rawEntries.map((e) => e.symbolId)]);
2154
- const entries = rawEntries.map((e) => ({
2155
- ...e,
2156
- dependentCount: graph.getDependentEdges(e.symbolId).filter((re) => blastSet.has(re.from)).length
2157
- }));
2158
- entries.sort((a, b) => a.depth - b.depth);
2159
- const directDependentsCount = entries.filter((e) => e.depth === 1).length;
2160
- const totalRisk = entries.reduce((sum, e) => sum + e.riskScore, 0);
2161
- const overallRiskScore = Math.round(Math.min(totalRisk / Math.max(directDependentsCount, 1), 1) * 1e3) / 1e3;
2162
- const confirmedCount = entries.filter((e) => e.confidence === "confirmed").length;
2163
- const likelyCount = entries.filter((e) => e.confidence === "likely").length;
2164
- const potentialCount = entries.filter((e) => e.confidence === "potential").length;
2165
- return { impactedSymbols: entries, directDependentsCount, confirmedCount, likelyCount, potentialCount, overallRiskScore };
1288
+ violations.sort((a, b) => {
1289
+ if (a.severity !== b.severity) return a.severity === "high" ? -1 : 1;
1290
+ const cmp = compareCodepoint(a.from.symbolId, b.from.symbolId);
1291
+ return cmp !== 0 ? cmp : compareCodepoint(a.to.symbolId, b.to.symbolId);
1292
+ });
1293
+ return {
1294
+ violations,
1295
+ confidence: confidenceFor2(distinctCommits),
1296
+ snapshotsAvailable,
1297
+ distinctCommits,
1298
+ hint: hintFor2(distinctCommits)
1299
+ };
2166
1300
  }
2167
1301
  };
2168
-
2169
- // src/core/blast-radius/co-changed-files.ts
2170
- var DEFAULT_LIMIT = 10;
2171
- var DEFAULT_MIN_FREQUENCY = 0.5;
2172
- function findCoChangedFiles(coChangeMap, rootFile, impactedFiles, options = {}) {
2173
- const limit = options.limit ?? DEFAULT_LIMIT;
2174
- const minFrequency = options.minFrequency ?? DEFAULT_MIN_FREQUENCY;
2175
- const strongestByFile = /* @__PURE__ */ new Map();
2176
- for (const entry of coChangeMap.get(rootFile) ?? []) {
2177
- const file = entry.file1 === rootFile ? entry.file2 : entry.file1;
2178
- if (file === rootFile || impactedFiles.has(file) || entry.frequency < minFrequency) continue;
2179
- const candidate = { file, frequency: entry.frequency, sharedCommits: entry.sharedCommits };
2180
- const known = strongestByFile.get(file);
2181
- if (!known || compareStrongestFirst(candidate, known) < 0) strongestByFile.set(file, candidate);
1302
+ function countHistoricalCrossCluster(history, fromSymbol, toSymbol) {
1303
+ let count2 = 0;
1304
+ for (const snap of history) {
1305
+ const fromId = snap.assignments.get(fromSymbol);
1306
+ const toId = snap.assignments.get(toSymbol);
1307
+ if (fromId === void 0 || toId === void 0) continue;
1308
+ if (fromId !== toId) count2++;
2182
1309
  }
2183
- return [...strongestByFile.values()].sort(compareStrongestFirst).slice(0, Math.max(0, limit));
1310
+ return count2;
2184
1311
  }
2185
- function compareStrongestFirst(a, b) {
2186
- return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file, b.file);
1312
+ function assignmentMap2(entries) {
1313
+ const map = /* @__PURE__ */ new Map();
1314
+ for (const entry of entries) {
1315
+ map.set(entry.symbolId, entry.communityId);
1316
+ }
1317
+ return map;
2187
1318
  }
2188
- function findCoChangedFilesForBlastRadius(coChangeMap, rootSymbolId, impactedSymbols) {
2189
- if (!coChangeMap) return [];
2190
- const rootFile = fileOf(rootSymbolId);
2191
- const impactedFiles = /* @__PURE__ */ new Set([rootFile]);
2192
- for (const { symbolId } of impactedSymbols) impactedFiles.add(fileOf(symbolId));
2193
- return findCoChangedFiles(coChangeMap, rootFile, impactedFiles);
1319
+ function labelMap2(entries) {
1320
+ const labels = /* @__PURE__ */ new Map();
1321
+ for (const entry of entries) {
1322
+ if (!labels.has(entry.communityId)) {
1323
+ labels.set(entry.communityId, entry.communityLabel);
1324
+ }
1325
+ }
1326
+ return labels;
2194
1327
  }
2195
- function fileOf(symbolId) {
2196
- return symbolId.split("::")[0];
1328
+ function confidenceFor2(distinctCommits) {
1329
+ if (distinctCommits >= 7) return "high";
1330
+ if (distinctCommits >= 3) return "medium";
1331
+ return "low";
1332
+ }
1333
+ function hintFor2(distinctCommits) {
1334
+ if (distinctCommits >= MIN_DISTINCT_COMMITS_FOR_CONFIDENCE) return void 0;
1335
+ return "Install the post-commit hook (`ctxo init`) or run `ctxo index` after commits to build history; `ctxo index --check` does not record snapshots.";
2197
1336
  }
2198
1337
 
2199
- // src/core/overlay/architectural-overlay.ts
2200
- var DEFAULT_RULES = [
2201
- // Test layer (matched first — __tests__, .test.ts, tests/, fixtures)
2202
- { pattern: /__tests__/, layer: "Test" },
2203
- { pattern: /\.test\.ts$/, layer: "Test" },
2204
- { pattern: /\btests\b/, layer: "Test" },
2205
- { pattern: /\bfixtures?\b/, layer: "Test" },
2206
- // Composition root
2207
- { pattern: /src\/index\.ts$/, layer: "Composition" },
2208
- // Domain
2209
- { pattern: /\bcore\b/, layer: "Domain" },
2210
- { pattern: /\bports?\b/, layer: "Domain" },
2211
- // Adapter
2212
- { pattern: /\badapters?\b/, layer: "Adapter" },
2213
- { pattern: /\bcli\b/, layer: "Adapter" },
2214
- // Infrastructure
2215
- { pattern: /\binfra\b/, layer: "Infrastructure" },
2216
- { pattern: /\bdb\b/, layer: "Infrastructure" },
2217
- { pattern: /\bqueue\b/, layer: "Infrastructure" },
2218
- // Config files
2219
- { pattern: /\.(config|rc)\.(ts|js|json)$/, layer: "Configuration" }
2220
- ];
2221
- var ArchitecturalOverlay = class {
2222
- rules;
2223
- constructor(customRules) {
2224
- this.rules = (customRules ?? DEFAULT_RULES).map(({ pattern, layer }) => ({
2225
- pattern: new RegExp(pattern.source, pattern.flags),
2226
- layer
2227
- }));
2228
- }
2229
- classify(filePaths) {
2230
- const layers = {};
2231
- for (const filePath of filePaths) {
2232
- const layer = this.matchLayer(filePath);
2233
- const list = layers[layer] ?? [];
2234
- list.push(filePath);
2235
- layers[layer] = list;
1338
+ // src/core/importance/pagerank-calculator.ts
1339
+ var DEFAULT_DAMPING = 0.85;
1340
+ var DEFAULT_MAX_ITERATIONS = 100;
1341
+ var DEFAULT_TOLERANCE = 1e-6;
1342
+ var DEFAULT_LIMIT = 25;
1343
+ var PageRankCalculator = class {
1344
+ calculate(graph, options = {}) {
1345
+ const damping = options.damping ?? DEFAULT_DAMPING;
1346
+ const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
1347
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
1348
+ const limit = options.limit ?? DEFAULT_LIMIT;
1349
+ const nodes = graph.allNodes();
1350
+ const n = nodes.length;
1351
+ if (n === 0) {
1352
+ return { rankings: [], totalSymbols: 0, iterations: 0, converged: true };
2236
1353
  }
2237
- return { layers };
2238
- }
2239
- matchLayer(filePath) {
2240
- for (const rule of this.rules) {
2241
- if (rule.pattern.test(filePath)) {
2242
- return rule.layer;
1354
+ const scores = /* @__PURE__ */ new Map();
1355
+ const initialScore = 1 / n;
1356
+ for (const node of nodes) {
1357
+ scores.set(node.symbolId, initialScore);
1358
+ }
1359
+ const outDegree = /* @__PURE__ */ new Map();
1360
+ for (const node of nodes) {
1361
+ outDegree.set(node.symbolId, graph.getForwardEdges(node.symbolId).length);
1362
+ }
1363
+ let iterations = 0;
1364
+ let converged = false;
1365
+ while (iterations < maxIterations) {
1366
+ const newScores = /* @__PURE__ */ new Map();
1367
+ const base = (1 - damping) / n;
1368
+ let danglingSum = 0;
1369
+ for (const node of nodes) {
1370
+ if ((outDegree.get(node.symbolId) ?? 0) === 0) {
1371
+ danglingSum += scores.get(node.symbolId) ?? 0;
1372
+ }
1373
+ }
1374
+ const danglingContrib = damping * danglingSum / n;
1375
+ for (const node of nodes) {
1376
+ let incomingScore = 0;
1377
+ const reverseEdges = graph.getReverseEdges(node.symbolId);
1378
+ const contributors = /* @__PURE__ */ new Set();
1379
+ for (const edge of reverseEdges) {
1380
+ if (contributors.has(edge.from)) continue;
1381
+ contributors.add(edge.from);
1382
+ const fromScore = scores.get(edge.from) ?? 0;
1383
+ const fromOut = outDegree.get(edge.from) ?? 1;
1384
+ incomingScore += fromScore / fromOut;
1385
+ }
1386
+ newScores.set(node.symbolId, base + damping * incomingScore + danglingContrib);
1387
+ }
1388
+ let maxDelta = 0;
1389
+ for (const node of nodes) {
1390
+ const delta = Math.abs((newScores.get(node.symbolId) ?? 0) - (scores.get(node.symbolId) ?? 0));
1391
+ if (delta > maxDelta) maxDelta = delta;
1392
+ }
1393
+ for (const [id, score] of newScores) {
1394
+ scores.set(id, score);
1395
+ }
1396
+ iterations++;
1397
+ if (maxDelta < tolerance) {
1398
+ converged = true;
1399
+ break;
2243
1400
  }
2244
1401
  }
2245
- return "Unknown";
1402
+ const entries = nodes.map((node) => ({
1403
+ symbolId: node.symbolId,
1404
+ name: node.name,
1405
+ kind: node.kind,
1406
+ file: node.symbolId.split("::")[0] ?? "",
1407
+ score: Math.round((scores.get(node.symbolId) ?? 0) * 1e6) / 1e6,
1408
+ inDegree: graph.getReverseEdges(node.symbolId).length,
1409
+ outDegree: outDegree.get(node.symbolId) ?? 0
1410
+ }));
1411
+ entries.sort((a, b) => b.score - a.score);
1412
+ return {
1413
+ rankings: entries.slice(0, limit),
1414
+ totalSymbols: n,
1415
+ iterations,
1416
+ converged
1417
+ };
2246
1418
  }
2247
1419
  };
2248
1420
 
@@ -2464,182 +1636,184 @@ var DeadCodeDetector = class {
2464
1636
  }
2465
1637
  };
2466
1638
 
2467
- // src/core/importance/pagerank-calculator.ts
2468
- var DEFAULT_DAMPING = 0.85;
2469
- var DEFAULT_MAX_ITERATIONS = 100;
2470
- var DEFAULT_TOLERANCE = 1e-6;
2471
- var DEFAULT_LIMIT2 = 25;
2472
- var PageRankCalculator = class {
2473
- calculate(graph, options = {}) {
2474
- const damping = options.damping ?? DEFAULT_DAMPING;
2475
- const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
2476
- const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
2477
- const limit = options.limit ?? DEFAULT_LIMIT2;
2478
- const nodes = graph.allNodes();
2479
- const n = nodes.length;
2480
- if (n === 0) {
2481
- return { rankings: [], totalSymbols: 0, iterations: 0, converged: true };
2482
- }
2483
- const scores = /* @__PURE__ */ new Map();
2484
- const initialScore = 1 / n;
2485
- for (const node of nodes) {
2486
- scores.set(node.symbolId, initialScore);
2487
- }
2488
- const outDegree = /* @__PURE__ */ new Map();
2489
- for (const node of nodes) {
2490
- outDegree.set(node.symbolId, graph.getForwardEdges(node.symbolId).length);
2491
- }
2492
- let iterations = 0;
2493
- let converged = false;
2494
- while (iterations < maxIterations) {
2495
- const newScores = /* @__PURE__ */ new Map();
2496
- const base = (1 - damping) / n;
2497
- let danglingSum = 0;
2498
- for (const node of nodes) {
2499
- if ((outDegree.get(node.symbolId) ?? 0) === 0) {
2500
- danglingSum += scores.get(node.symbolId) ?? 0;
2501
- }
2502
- }
2503
- const danglingContrib = damping * danglingSum / n;
2504
- for (const node of nodes) {
2505
- let incomingScore = 0;
2506
- const reverseEdges = graph.getReverseEdges(node.symbolId);
2507
- const contributors = /* @__PURE__ */ new Set();
2508
- for (const edge of reverseEdges) {
2509
- if (contributors.has(edge.from)) continue;
2510
- contributors.add(edge.from);
2511
- const fromScore = scores.get(edge.from) ?? 0;
2512
- const fromOut = outDegree.get(edge.from) ?? 1;
2513
- incomingScore += fromScore / fromOut;
2514
- }
2515
- newScores.set(node.symbolId, base + damping * incomingScore + danglingContrib);
2516
- }
2517
- let maxDelta = 0;
2518
- for (const node of nodes) {
2519
- const delta = Math.abs((newScores.get(node.symbolId) ?? 0) - (scores.get(node.symbolId) ?? 0));
2520
- if (delta > maxDelta) maxDelta = delta;
2521
- }
2522
- for (const [id, score] of newScores) {
2523
- scores.set(id, score);
2524
- }
2525
- iterations++;
2526
- if (maxDelta < tolerance) {
2527
- converged = true;
2528
- break;
1639
+ // src/core/overlay/architectural-overlay.ts
1640
+ var DEFAULT_RULES = [
1641
+ // Test layer (matched first — __tests__, .test.ts, tests/, fixtures)
1642
+ { pattern: /__tests__/, layer: "Test" },
1643
+ { pattern: /\.test\.ts$/, layer: "Test" },
1644
+ { pattern: /\btests\b/, layer: "Test" },
1645
+ { pattern: /\bfixtures?\b/, layer: "Test" },
1646
+ // Composition root
1647
+ { pattern: /src\/index\.ts$/, layer: "Composition" },
1648
+ // Domain
1649
+ { pattern: /\bcore\b/, layer: "Domain" },
1650
+ { pattern: /\bports?\b/, layer: "Domain" },
1651
+ // Adapter
1652
+ { pattern: /\badapters?\b/, layer: "Adapter" },
1653
+ { pattern: /\bcli\b/, layer: "Adapter" },
1654
+ // Infrastructure
1655
+ { pattern: /\binfra\b/, layer: "Infrastructure" },
1656
+ { pattern: /\bdb\b/, layer: "Infrastructure" },
1657
+ { pattern: /\bqueue\b/, layer: "Infrastructure" },
1658
+ // Config files
1659
+ { pattern: /\.(config|rc)\.(ts|js|json)$/, layer: "Configuration" }
1660
+ ];
1661
+ var ArchitecturalOverlay = class {
1662
+ rules;
1663
+ constructor(customRules) {
1664
+ this.rules = (customRules ?? DEFAULT_RULES).map(({ pattern, layer }) => ({
1665
+ pattern: new RegExp(pattern.source, pattern.flags),
1666
+ layer
1667
+ }));
1668
+ }
1669
+ classify(filePaths) {
1670
+ const layers = {};
1671
+ for (const filePath of filePaths) {
1672
+ const layer = this.matchLayer(filePath);
1673
+ const list = layers[layer] ?? [];
1674
+ list.push(filePath);
1675
+ layers[layer] = list;
1676
+ }
1677
+ return { layers };
1678
+ }
1679
+ matchLayer(filePath) {
1680
+ for (const rule of this.rules) {
1681
+ if (rule.pattern.test(filePath)) {
1682
+ return rule.layer;
2529
1683
  }
2530
1684
  }
2531
- const entries = nodes.map((node) => ({
2532
- symbolId: node.symbolId,
2533
- name: node.name,
2534
- kind: node.kind,
2535
- file: node.symbolId.split("::")[0] ?? "",
2536
- score: Math.round((scores.get(node.symbolId) ?? 0) * 1e6) / 1e6,
2537
- inDegree: graph.getReverseEdges(node.symbolId).length,
2538
- outDegree: outDegree.get(node.symbolId) ?? 0
2539
- }));
2540
- entries.sort((a, b) => b.score - a.score);
2541
- return {
2542
- rankings: entries.slice(0, limit),
2543
- totalSymbols: n,
2544
- iterations,
2545
- converged
2546
- };
1685
+ return "Unknown";
2547
1686
  }
2548
1687
  };
2549
1688
 
2550
- // src/core/overlay/boundary-violation-detector.ts
2551
- var HIGH_SEVERITY_THRESHOLD = 0;
2552
- var MEDIUM_SEVERITY_THRESHOLD = 2;
2553
- var MIN_DISTINCT_COMMITS_FOR_CONFIDENCE = 2;
2554
- var BoundaryViolationDetector = class {
2555
- detect(graph, current, history) {
2556
- const snapshotsAvailable = history.length;
2557
- const distinctCommits = distinctCommitCount(history, current.commitSha);
2558
- const currentAssignments = assignmentMap2(current.communities);
2559
- const currentLabels = labelMap2(current.communities);
2560
- const earlierCommits = priorCommitSnapshots(history, current.commitSha);
2561
- const historicalAssignments = earlierCommits.map((snapshot) => ({
2562
- snapshot,
2563
- assignments: assignmentMap2(snapshot.communities),
2564
- labels: labelMap2(snapshot.communities)
2565
- }));
2566
- const violations = [];
2567
- for (const edge of graph.allEdges()) {
2568
- if (edge.from === edge.to) continue;
2569
- if (DATA_EDGE_KINDS.has(edge.kind)) continue;
2570
- const fromCommunity = currentAssignments.get(edge.from);
2571
- const toCommunity = currentAssignments.get(edge.to);
2572
- if (fromCommunity === void 0 || toCommunity === void 0) continue;
2573
- if (fromCommunity === toCommunity) continue;
2574
- const fromLabel = currentLabels.get(fromCommunity) ?? `community-${fromCommunity}`;
2575
- const toLabel = currentLabels.get(toCommunity) ?? `community-${toCommunity}`;
2576
- const historicalCount = distinctCommits < MIN_DISTINCT_COMMITS_FOR_CONFIDENCE ? -1 : countHistoricalCrossCluster(historicalAssignments, edge.from, edge.to);
2577
- if (historicalCount === -1 || historicalCount <= MEDIUM_SEVERITY_THRESHOLD) {
2578
- violations.push({
2579
- from: {
2580
- symbolId: edge.from,
2581
- communityId: fromCommunity,
2582
- label: fromLabel
2583
- },
2584
- to: {
2585
- symbolId: edge.to,
2586
- communityId: toCommunity,
2587
- label: toLabel
2588
- },
2589
- edgeKind: edge.kind,
2590
- historicalEdgesBetweenClusters: historicalCount,
2591
- severity: historicalCount === HIGH_SEVERITY_THRESHOLD ? "high" : "medium"
1689
+ // src/core/blast-radius/blast-radius-calculator.ts
1690
+ var EDGE_KIND_CONFIDENCE = {
1691
+ calls: "confirmed",
1692
+ extends: "confirmed",
1693
+ implements: "confirmed",
1694
+ reads: "confirmed",
1695
+ writes: "confirmed",
1696
+ executes: "confirmed",
1697
+ uses: "likely",
1698
+ imports: "potential"
1699
+ };
1700
+ var CONFIDENCE_RANK = {
1701
+ confirmed: 2,
1702
+ likely: 1,
1703
+ potential: 0
1704
+ };
1705
+ function edgeKindToConfidence(kind) {
1706
+ return EDGE_KINDS.includes(kind) ? EDGE_KIND_CONFIDENCE[kind] : "potential";
1707
+ }
1708
+ var BlastRadiusCalculator = class {
1709
+ /**
1710
+ * `symbolId` may be any id `SymbolGraph.getNode` resolves, a 2-part data id included. The root is the
1711
+ * node it names, and the root's own file is the one co-change is matched against. Dependents come from
1712
+ * `getDependentEdges`, so every copy of a table, view or procedure, and its 2-part id, has the dependents
1713
+ * of the resource, and no definition of the resource counts as a dependent of another.
1714
+ */
1715
+ calculate(graph, symbolId, coChangeMap) {
1716
+ const root = graph.getNode(symbolId);
1717
+ if (!root) {
1718
+ return { impactedSymbols: [], directDependentsCount: 0, confirmedCount: 0, likelyCount: 0, potentialCount: 0, overallRiskScore: 0 };
1719
+ }
1720
+ const rootId = root.symbolId;
1721
+ const sourceFile = rootId.split("::")[0];
1722
+ const definitions = graph.dataResourceIdentity(rootId)?.definitions.map((definition) => definition.symbolId) ?? [];
1723
+ const visited = /* @__PURE__ */ new Set([rootId, ...definitions]);
1724
+ const rawEntries = [];
1725
+ const queue = [{ id: rootId, depth: 0 }];
1726
+ while (queue.length > 0) {
1727
+ const current = queue.shift();
1728
+ const reverseEdges = graph.getDependentEdges(current.id);
1729
+ const bestByNode = /* @__PURE__ */ new Map();
1730
+ for (const edge of reverseEdges) {
1731
+ if (visited.has(edge.from)) continue;
1732
+ const conf = edgeKindToConfidence(edge.kind);
1733
+ const existing = bestByNode.get(edge.from);
1734
+ if (!existing) {
1735
+ bestByNode.set(edge.from, { confidence: conf, kinds: /* @__PURE__ */ new Set([edge.kind]) });
1736
+ } else {
1737
+ existing.kinds.add(edge.kind);
1738
+ if (CONFIDENCE_RANK[conf] > CONFIDENCE_RANK[existing.confidence]) {
1739
+ existing.confidence = conf;
1740
+ }
1741
+ }
1742
+ }
1743
+ for (const [nodeId, info] of bestByNode) {
1744
+ visited.add(nodeId);
1745
+ const depth = current.depth + 1;
1746
+ const riskScore = 1 / Math.pow(depth, 0.7);
1747
+ let confidence = info.confidence;
1748
+ let coChangeFrequency;
1749
+ if (coChangeMap) {
1750
+ const nodeFile = nodeId.split("::")[0];
1751
+ const coEntries = coChangeMap.get(sourceFile);
1752
+ if (coEntries) {
1753
+ const match = coEntries.find((e) => (e.file1 === sourceFile ? e.file2 : e.file1) === nodeFile);
1754
+ if (match) {
1755
+ coChangeFrequency = match.frequency;
1756
+ if (confidence === "potential" && match.frequency > 0.5) {
1757
+ confidence = "likely";
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ rawEntries.push({
1763
+ symbolId: nodeId,
1764
+ depth,
1765
+ riskScore: Math.round(riskScore * 1e3) / 1e3,
1766
+ confidence,
1767
+ edgeKinds: [...info.kinds],
1768
+ coChangeFrequency
2592
1769
  });
1770
+ queue.push({ id: nodeId, depth });
2593
1771
  }
2594
1772
  }
2595
- violations.sort((a, b) => {
2596
- if (a.severity !== b.severity) return a.severity === "high" ? -1 : 1;
2597
- const cmp = compareCodepoint(a.from.symbolId, b.from.symbolId);
2598
- return cmp !== 0 ? cmp : compareCodepoint(a.to.symbolId, b.to.symbolId);
2599
- });
2600
- return {
2601
- violations,
2602
- confidence: confidenceFor2(distinctCommits),
2603
- snapshotsAvailable,
2604
- distinctCommits,
2605
- hint: hintFor2(distinctCommits)
2606
- };
1773
+ const blastSet = /* @__PURE__ */ new Set([rootId, ...definitions, ...rawEntries.map((e) => e.symbolId)]);
1774
+ const entries = rawEntries.map((e) => ({
1775
+ ...e,
1776
+ dependentCount: graph.getDependentEdges(e.symbolId).filter((re) => blastSet.has(re.from)).length
1777
+ }));
1778
+ entries.sort((a, b) => a.depth - b.depth);
1779
+ const directDependentsCount = entries.filter((e) => e.depth === 1).length;
1780
+ const totalRisk = entries.reduce((sum, e) => sum + e.riskScore, 0);
1781
+ const overallRiskScore = Math.round(Math.min(totalRisk / Math.max(directDependentsCount, 1), 1) * 1e3) / 1e3;
1782
+ const confirmedCount = entries.filter((e) => e.confidence === "confirmed").length;
1783
+ const likelyCount = entries.filter((e) => e.confidence === "likely").length;
1784
+ const potentialCount = entries.filter((e) => e.confidence === "potential").length;
1785
+ return { impactedSymbols: entries, directDependentsCount, confirmedCount, likelyCount, potentialCount, overallRiskScore };
2607
1786
  }
2608
1787
  };
2609
- function countHistoricalCrossCluster(history, fromSymbol, toSymbol) {
2610
- let count2 = 0;
2611
- for (const snap of history) {
2612
- const fromId = snap.assignments.get(fromSymbol);
2613
- const toId = snap.assignments.get(toSymbol);
2614
- if (fromId === void 0 || toId === void 0) continue;
2615
- if (fromId !== toId) count2++;
2616
- }
2617
- return count2;
2618
- }
2619
- function assignmentMap2(entries) {
2620
- const map = /* @__PURE__ */ new Map();
2621
- for (const entry of entries) {
2622
- map.set(entry.symbolId, entry.communityId);
1788
+
1789
+ // src/core/blast-radius/co-changed-files.ts
1790
+ var DEFAULT_LIMIT2 = 10;
1791
+ var DEFAULT_MIN_FREQUENCY = 0.5;
1792
+ function findCoChangedFiles(coChangeMap, rootFile, impactedFiles, options = {}) {
1793
+ const limit = options.limit ?? DEFAULT_LIMIT2;
1794
+ const minFrequency = options.minFrequency ?? DEFAULT_MIN_FREQUENCY;
1795
+ const strongestByFile = /* @__PURE__ */ new Map();
1796
+ for (const entry of coChangeMap.get(rootFile) ?? []) {
1797
+ const file = entry.file1 === rootFile ? entry.file2 : entry.file1;
1798
+ if (file === rootFile || impactedFiles.has(file) || entry.frequency < minFrequency) continue;
1799
+ const candidate = { file, frequency: entry.frequency, sharedCommits: entry.sharedCommits };
1800
+ const known = strongestByFile.get(file);
1801
+ if (!known || compareStrongestFirst(candidate, known) < 0) strongestByFile.set(file, candidate);
2623
1802
  }
2624
- return map;
1803
+ return [...strongestByFile.values()].sort(compareStrongestFirst).slice(0, Math.max(0, limit));
2625
1804
  }
2626
- function labelMap2(entries) {
2627
- const labels = /* @__PURE__ */ new Map();
2628
- for (const entry of entries) {
2629
- if (!labels.has(entry.communityId)) {
2630
- labels.set(entry.communityId, entry.communityLabel);
2631
- }
2632
- }
2633
- return labels;
1805
+ function compareStrongestFirst(a, b) {
1806
+ return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file, b.file);
2634
1807
  }
2635
- function confidenceFor2(distinctCommits) {
2636
- if (distinctCommits >= 7) return "high";
2637
- if (distinctCommits >= 3) return "medium";
2638
- return "low";
1808
+ function findCoChangedFilesForBlastRadius(coChangeMap, rootSymbolId, impactedSymbols) {
1809
+ if (!coChangeMap) return [];
1810
+ const rootFile = fileOf(rootSymbolId);
1811
+ const impactedFiles = /* @__PURE__ */ new Set([rootFile]);
1812
+ for (const { symbolId } of impactedSymbols) impactedFiles.add(fileOf(symbolId));
1813
+ return findCoChangedFiles(coChangeMap, rootFile, impactedFiles);
2639
1814
  }
2640
- function hintFor2(distinctCommits) {
2641
- if (distinctCommits >= MIN_DISTINCT_COMMITS_FOR_CONFIDENCE) return void 0;
2642
- return "Install the post-commit hook (`ctxo init`) or run `ctxo index` after commits to build history; `ctxo index --check` does not record snapshots.";
1815
+ function fileOf(symbolId) {
1816
+ return symbolId.split("::")[0];
2643
1817
  }
2644
1818
 
2645
1819
  // src/core/search/symbol-tokenizer.ts
@@ -2816,43 +1990,21 @@ function isDigit(ch) {
2816
1990
  }
2817
1991
 
2818
1992
  export {
2819
- compareCodepoint,
2820
- CommunitySnapshotWriter,
2821
- SqliteStorageAdapter,
1993
+ detectWorkspace,
2822
1994
  buildCoChangeMatrix,
2823
1995
  CoChangeReader,
2824
- DATA_EDGE_KINDS,
2825
- isDataResourceKind,
2826
- parseCoChangeSince,
2827
- sinceCutoff,
2828
- selectWindow,
2829
- loadConfig,
2830
- statsEnabled,
2831
- indexIgnorePatterns,
2832
- indexIgnoreProjectPatterns,
2833
- factsRequested,
2834
- indexTestPatterns,
2835
- communityDetectorOptions,
2836
- maskingClusterLabelPatterns,
2837
- watchSnapshotMinFileChanges,
2838
- resolveCoChangeConfig,
2839
- resolveContextsViewConfig,
2840
- makeGlobMatcher,
2841
- filesWithTestAnnotations,
2842
- createTestFileClassifier,
2843
- SymbolGraph,
1996
+ SymbolTokenizer,
1997
+ CommunitySnapshotWriter,
1998
+ SqliteStorageAdapter,
2844
1999
  RevertDetector,
2845
2000
  DriftDetector,
2846
- SimpleGitAdapter,
2847
- BlastRadiusCalculator,
2848
- findCoChangedFilesForBlastRadius,
2849
- ArchitecturalOverlay,
2850
- ContextsViewReader,
2851
- DeadCodeDetector,
2852
- SymbolTokenizer,
2853
- PageRankCalculator,
2854
2001
  BoundaryViolationDetector,
2002
+ PageRankCalculator,
2855
2003
  SessionRecorderAdapter,
2856
- detectWorkspace
2004
+ DeadCodeDetector,
2005
+ ArchitecturalOverlay,
2006
+ BlastRadiusCalculator,
2007
+ findCoChangedFilesForBlastRadius,
2008
+ ContextsViewReader
2857
2009
  };
2858
- //# sourceMappingURL=chunk-WB7WZKX4.js.map
2010
+ //# sourceMappingURL=chunk-ZL36YS2V.js.map