@ctxo/cli 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/chunk-2Q76LH2H.js +186 -0
- package/dist/chunk-72CEQEQD.js +53 -0
- package/dist/{chunk-WB7WZKX4.js → chunk-A4URKKPP.js} +916 -1938
- package/dist/chunk-BBSJB4CG.js +128 -0
- package/dist/{chunk-DT3ZH4AU.js → chunk-EC6INGPV.js} +6 -1
- package/dist/chunk-I7RPXT6L.js +1241 -0
- package/dist/chunk-IT426P3F.js +1036 -0
- package/dist/chunk-JDL6GGTO.js +87 -0
- package/dist/chunk-LAX6VVBU.js +98 -0
- package/dist/{cli-router-DDHH4WQH.js → cli-router-W56INOQ3.js} +980 -504
- package/dist/index.js +13 -2874
- package/dist/internal.d.ts +834 -0
- package/dist/internal.js +13 -0
- package/dist/{json-index-reader-OXTRTGE6.js → json-index-reader-DKEIP2QR.js} +6 -4
- package/dist/mcp-server-IYCCE74E.js +2336 -0
- package/dist/module-host-KTGDODES.js +13 -0
- package/dist/module-loader-B3LYF3W7.js +14 -0
- package/dist/plugin-discovery-XRXI3IJF.js +13 -0
- package/llms-full.txt +121 -2
- package/llms.txt +24 -1
- package/package.json +15 -3
- package/dist/atomic-write-BAFH464Q.js.map +0 -1
- package/dist/chunk-5DSKZJNN.js.map +0 -1
- package/dist/chunk-DT3ZH4AU.js.map +0 -1
- package/dist/chunk-PSKDDLSF.js.map +0 -1
- package/dist/chunk-RFEXMBTG.js.map +0 -1
- package/dist/chunk-WB7WZKX4.js.map +0 -1
- package/dist/cli-router-DDHH4WQH.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/json-index-reader-OXTRTGE6.js.map +0 -1
- package/dist/staleness-detector-4HJYBQ7Z.js.map +0 -1
|
@@ -1,40 +1,248 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
DATA_EDGE_KINDS,
|
|
4
|
+
createTestFileClassifier,
|
|
5
|
+
filesWithTestAnnotations
|
|
6
|
+
} from "./chunk-I7RPXT6L.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-
|
|
15
|
+
} from "./chunk-EC6INGPV.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
|
|
18
|
-
import { join as
|
|
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 =
|
|
50
|
-
this.historyDir =
|
|
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 =
|
|
58
|
-
if (
|
|
265
|
+
const currentPath = join3(this.indexDir, CURRENT_FILE_NAME);
|
|
266
|
+
if (existsSync2(currentPath)) {
|
|
59
267
|
try {
|
|
60
|
-
const existingRaw =
|
|
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(
|
|
273
|
+
this.atomicWrite(join3(this.historyDir, historyFile), existingRaw);
|
|
66
274
|
}
|
|
67
275
|
} catch (err) {
|
|
68
|
-
|
|
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 =
|
|
77
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
316
|
+
const base = join3(this.historyDir, `${ts}-${sha}`);
|
|
109
317
|
let candidate = `${base}.json`;
|
|
110
318
|
let n = 1;
|
|
111
|
-
while (
|
|
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
|
|
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 (!
|
|
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(
|
|
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 =
|
|
357
|
+
const target = join3(this.historyDir, name);
|
|
150
358
|
try {
|
|
151
359
|
unlinkSync(target);
|
|
152
360
|
} catch (err) {
|
|
153
|
-
|
|
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 =
|
|
367
|
+
const raw = readFileSync2(path, "utf-8");
|
|
160
368
|
return JSON.parse(raw);
|
|
161
369
|
} catch (err) {
|
|
162
|
-
|
|
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 =
|
|
182
|
-
const tmpRoot =
|
|
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
|
|
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 =
|
|
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 (
|
|
414
|
+
if (existsSync3(this.dbPath)) {
|
|
207
415
|
try {
|
|
208
|
-
const buffer =
|
|
416
|
+
const buffer = readFileSync3(this.dbPath);
|
|
209
417
|
this.db = new SQL.Database(buffer);
|
|
210
418
|
this.verifyIntegrity();
|
|
211
419
|
} catch {
|
|
212
|
-
|
|
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 (
|
|
432
|
+
if (existsSync3(this.dbPath)) {
|
|
225
433
|
try {
|
|
226
|
-
const buffer =
|
|
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
|
-
|
|
699
|
+
log3.error(`Failed to persist DB: ${err.message}`);
|
|
492
700
|
}
|
|
493
701
|
}
|
|
494
702
|
deleteFileData(db, filePath) {
|
|
@@ -534,1335 +742,301 @@ var SqliteStorageAdapter = class {
|
|
|
534
742
|
}
|
|
535
743
|
};
|
|
536
744
|
|
|
537
|
-
// src/adapters/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
}
|
|
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;
|
|
581
754
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
755
|
+
ensureTable() {
|
|
756
|
+
if (this.tableCreated) return;
|
|
757
|
+
this.db.run(`
|
|
758
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
759
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
760
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
761
|
+
tool TEXT NOT NULL,
|
|
762
|
+
symbol_id TEXT,
|
|
763
|
+
detail_level TEXT,
|
|
764
|
+
response_tokens INTEGER,
|
|
765
|
+
response_bytes INTEGER,
|
|
766
|
+
latency_ms REAL,
|
|
767
|
+
truncated BOOLEAN DEFAULT 0
|
|
768
|
+
)
|
|
769
|
+
`);
|
|
770
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_session_timestamp ON session_events(timestamp)");
|
|
771
|
+
this.db.run("CREATE INDEX IF NOT EXISTS idx_session_tool ON session_events(tool)");
|
|
772
|
+
this.tableCreated = true;
|
|
599
773
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
774
|
+
record(event) {
|
|
775
|
+
try {
|
|
776
|
+
this.ensureTable();
|
|
777
|
+
this.db.run(
|
|
778
|
+
`INSERT INTO session_events (tool, symbol_id, detail_level, response_tokens, response_bytes, latency_ms, truncated)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
780
|
+
[
|
|
781
|
+
event.tool,
|
|
782
|
+
event.symbolId,
|
|
783
|
+
event.detailLevel,
|
|
784
|
+
event.responseTokens,
|
|
785
|
+
event.responseBytes,
|
|
786
|
+
event.latencyMs,
|
|
787
|
+
event.truncated ? 1 : 0
|
|
788
|
+
]
|
|
789
|
+
);
|
|
790
|
+
this.flushToDisk();
|
|
791
|
+
} catch (err) {
|
|
792
|
+
log4.error(`Failed to record session event: ${err.message}`);
|
|
612
793
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
794
|
+
}
|
|
795
|
+
queryStats(days) {
|
|
796
|
+
this.ensureTable();
|
|
797
|
+
const whereClause = days != null && days > 0 ? `WHERE timestamp >= datetime('now', '-${Math.floor(days)} days')` : "";
|
|
798
|
+
const summaryResult = this.db.exec(
|
|
799
|
+
`SELECT COUNT(*) as total_calls, COALESCE(SUM(response_tokens), 0) as total_tokens
|
|
800
|
+
FROM session_events ${whereClause}`
|
|
801
|
+
);
|
|
802
|
+
const summaryRow = summaryResult[0]?.values[0];
|
|
803
|
+
const totalCalls = Number(summaryRow?.[0] ?? 0);
|
|
804
|
+
const totalTokensServed = Number(summaryRow?.[1] ?? 0);
|
|
805
|
+
const toolsResult = this.db.exec(
|
|
806
|
+
`SELECT tool, COUNT(*) as calls, CAST(AVG(response_tokens) AS INTEGER) as avg_tokens
|
|
807
|
+
FROM session_events ${whereClause}
|
|
808
|
+
GROUP BY tool ORDER BY calls DESC LIMIT 5`
|
|
809
|
+
);
|
|
810
|
+
const topTools = (toolsResult[0]?.values ?? []).map((row) => ({
|
|
811
|
+
tool: String(row[0]),
|
|
812
|
+
calls: Number(row[1]),
|
|
813
|
+
avgTokens: Number(row[2])
|
|
814
|
+
}));
|
|
815
|
+
const symbolWherePrefix = whereClause ? whereClause + " AND" : "WHERE";
|
|
816
|
+
const symbolsResult = this.db.exec(
|
|
817
|
+
`SELECT symbol_id, COUNT(*) as queries
|
|
818
|
+
FROM session_events ${symbolWherePrefix} symbol_id IS NOT NULL
|
|
819
|
+
GROUP BY symbol_id ORDER BY queries DESC LIMIT 5`
|
|
820
|
+
);
|
|
821
|
+
const topSymbols = (symbolsResult[0]?.values ?? []).map((row) => {
|
|
822
|
+
const symbolId = String(row[0]);
|
|
823
|
+
const parts = symbolId.split("::");
|
|
824
|
+
const name = parts.length >= 2 ? parts[1] : symbolId;
|
|
825
|
+
return {
|
|
826
|
+
symbolId,
|
|
827
|
+
name,
|
|
828
|
+
queries: Number(row[1])
|
|
829
|
+
};
|
|
830
|
+
});
|
|
831
|
+
const levelResult = this.db.exec(
|
|
832
|
+
`SELECT detail_level, COUNT(*) as count
|
|
833
|
+
FROM session_events ${symbolWherePrefix} detail_level IS NOT NULL
|
|
834
|
+
GROUP BY detail_level ORDER BY detail_level`
|
|
835
|
+
);
|
|
836
|
+
const levelRows = levelResult[0]?.values ?? [];
|
|
837
|
+
const levelTotal = levelRows.reduce((sum, row) => sum + Number(row[1]), 0);
|
|
838
|
+
const detailLevelDistribution = levelRows.map((row) => ({
|
|
839
|
+
level: String(row[0]),
|
|
840
|
+
count: Number(row[1]),
|
|
841
|
+
percentage: levelTotal > 0 ? Math.round(Number(row[1]) / levelTotal * 100) : 0
|
|
842
|
+
}));
|
|
843
|
+
return {
|
|
844
|
+
totalCalls,
|
|
845
|
+
totalTokensServed,
|
|
846
|
+
topTools,
|
|
847
|
+
topSymbols,
|
|
848
|
+
detailLevelDistribution
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
clear() {
|
|
852
|
+
this.ensureTable();
|
|
853
|
+
this.db.run("DELETE FROM session_events");
|
|
854
|
+
this.flushToDisk();
|
|
855
|
+
}
|
|
856
|
+
flushToDisk() {
|
|
857
|
+
try {
|
|
858
|
+
this.onWrite?.();
|
|
859
|
+
} catch (err) {
|
|
860
|
+
log4.error(`Failed to flush stats to disk: ${err.message}`);
|
|
618
861
|
}
|
|
619
|
-
list2.push(entry);
|
|
620
862
|
}
|
|
621
|
-
|
|
622
|
-
}
|
|
863
|
+
};
|
|
623
864
|
|
|
624
|
-
// src/adapters/storage/
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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";
|
|
868
|
+
|
|
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
|
+
});
|
|
964
|
+
|
|
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;
|
|
975
|
+
}
|
|
976
|
+
var ContextsViewReader = class {
|
|
629
977
|
filePath;
|
|
630
978
|
cached;
|
|
631
|
-
/** What the last warning was about, so one problem is reported once. */
|
|
632
979
|
lastWarning;
|
|
633
980
|
constructor(ctxoRoot) {
|
|
634
|
-
this.filePath =
|
|
981
|
+
this.filePath = join5(ctxoRoot, "index", "contexts-view.json");
|
|
635
982
|
}
|
|
636
|
-
|
|
983
|
+
readContextsView() {
|
|
637
984
|
let stats;
|
|
638
985
|
try {
|
|
639
|
-
stats =
|
|
986
|
+
stats = statSync2(this.filePath);
|
|
640
987
|
} catch (err) {
|
|
641
988
|
this.cached = void 0;
|
|
642
989
|
const { code, message } = err;
|
|
643
990
|
if (code === "ENOENT") {
|
|
644
991
|
this.lastWarning = void 0;
|
|
645
992
|
} else {
|
|
646
|
-
this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`,
|
|
993
|
+
this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY2);
|
|
647
994
|
}
|
|
648
995
|
return void 0;
|
|
649
996
|
}
|
|
650
|
-
const version = `${stats.mtimeMs}:${stats.size}`;
|
|
651
|
-
if (this.cached?.version === version) return this.cached.
|
|
997
|
+
const version = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
998
|
+
if (this.cached?.version === version) return this.cached.view;
|
|
652
999
|
return this.load(version);
|
|
653
1000
|
}
|
|
654
1001
|
load(version) {
|
|
655
1002
|
let raw;
|
|
656
1003
|
try {
|
|
657
|
-
raw =
|
|
1004
|
+
raw = readFileSync4(this.filePath, "utf-8");
|
|
658
1005
|
} catch (err) {
|
|
659
1006
|
this.cached = void 0;
|
|
660
|
-
this.warnOnce(`read ${version}`, `cannot be read (${err.message})`,
|
|
1007
|
+
this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY2);
|
|
661
1008
|
return void 0;
|
|
662
1009
|
}
|
|
663
|
-
const
|
|
664
|
-
this.cached = { version,
|
|
665
|
-
if (
|
|
666
|
-
return
|
|
1010
|
+
const view = this.parse(raw, version);
|
|
1011
|
+
this.cached = { version, view };
|
|
1012
|
+
if (view) this.lastWarning = void 0;
|
|
1013
|
+
return view;
|
|
667
1014
|
}
|
|
668
1015
|
parse(raw, version) {
|
|
669
1016
|
let data;
|
|
670
1017
|
try {
|
|
671
1018
|
data = JSON.parse(raw);
|
|
672
1019
|
} catch (err) {
|
|
673
|
-
|
|
1020
|
+
this.warnOnce(`content ${version}`, `is not valid JSON (${err.message})`, UNTIL_REWRITTEN2);
|
|
1021
|
+
return void 0;
|
|
674
1022
|
}
|
|
675
|
-
const parsed =
|
|
1023
|
+
const parsed = ContextsViewSchema.safeParse(data);
|
|
676
1024
|
if (!parsed.success) {
|
|
677
1025
|
const issue = parsed.error.issues[0];
|
|
678
1026
|
const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
|
|
679
|
-
|
|
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;
|
|
680
1033
|
}
|
|
681
|
-
return
|
|
682
|
-
}
|
|
683
|
-
unusable(version, problem) {
|
|
684
|
-
this.warnOnce(`content ${version}`, problem, UNTIL_REWRITTEN);
|
|
685
|
-
return void 0;
|
|
1034
|
+
return deepFreeze(parsed.data);
|
|
686
1035
|
}
|
|
687
1036
|
warnOnce(key, problem, consequence) {
|
|
688
1037
|
if (this.lastWarning === key) return;
|
|
689
1038
|
this.lastWarning = key;
|
|
690
|
-
|
|
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;
|
|
1447
|
-
}
|
|
1448
|
-
ensureTable() {
|
|
1449
|
-
if (this.tableCreated) return;
|
|
1450
|
-
this.db.run(`
|
|
1451
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
1452
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1453
|
-
timestamp TEXT DEFAULT (datetime('now')),
|
|
1454
|
-
tool TEXT NOT NULL,
|
|
1455
|
-
symbol_id TEXT,
|
|
1456
|
-
detail_level TEXT,
|
|
1457
|
-
response_tokens INTEGER,
|
|
1458
|
-
response_bytes INTEGER,
|
|
1459
|
-
latency_ms REAL,
|
|
1460
|
-
truncated BOOLEAN DEFAULT 0
|
|
1461
|
-
)
|
|
1462
|
-
`);
|
|
1463
|
-
this.db.run("CREATE INDEX IF NOT EXISTS idx_session_timestamp ON session_events(timestamp)");
|
|
1464
|
-
this.db.run("CREATE INDEX IF NOT EXISTS idx_session_tool ON session_events(tool)");
|
|
1465
|
-
this.tableCreated = true;
|
|
1466
|
-
}
|
|
1467
|
-
record(event) {
|
|
1468
|
-
try {
|
|
1469
|
-
this.ensureTable();
|
|
1470
|
-
this.db.run(
|
|
1471
|
-
`INSERT INTO session_events (tool, symbol_id, detail_level, response_tokens, response_bytes, latency_ms, truncated)
|
|
1472
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1473
|
-
[
|
|
1474
|
-
event.tool,
|
|
1475
|
-
event.symbolId,
|
|
1476
|
-
event.detailLevel,
|
|
1477
|
-
event.responseTokens,
|
|
1478
|
-
event.responseBytes,
|
|
1479
|
-
event.latencyMs,
|
|
1480
|
-
event.truncated ? 1 : 0
|
|
1481
|
-
]
|
|
1482
|
-
);
|
|
1483
|
-
this.flushToDisk();
|
|
1484
|
-
} catch (err) {
|
|
1485
|
-
log7.error(`Failed to record session event: ${err.message}`);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
queryStats(days) {
|
|
1489
|
-
this.ensureTable();
|
|
1490
|
-
const whereClause = days != null && days > 0 ? `WHERE timestamp >= datetime('now', '-${Math.floor(days)} days')` : "";
|
|
1491
|
-
const summaryResult = this.db.exec(
|
|
1492
|
-
`SELECT COUNT(*) as total_calls, COALESCE(SUM(response_tokens), 0) as total_tokens
|
|
1493
|
-
FROM session_events ${whereClause}`
|
|
1494
|
-
);
|
|
1495
|
-
const summaryRow = summaryResult[0]?.values[0];
|
|
1496
|
-
const totalCalls = Number(summaryRow?.[0] ?? 0);
|
|
1497
|
-
const totalTokensServed = Number(summaryRow?.[1] ?? 0);
|
|
1498
|
-
const toolsResult = this.db.exec(
|
|
1499
|
-
`SELECT tool, COUNT(*) as calls, CAST(AVG(response_tokens) AS INTEGER) as avg_tokens
|
|
1500
|
-
FROM session_events ${whereClause}
|
|
1501
|
-
GROUP BY tool ORDER BY calls DESC LIMIT 5`
|
|
1502
|
-
);
|
|
1503
|
-
const topTools = (toolsResult[0]?.values ?? []).map((row) => ({
|
|
1504
|
-
tool: String(row[0]),
|
|
1505
|
-
calls: Number(row[1]),
|
|
1506
|
-
avgTokens: Number(row[2])
|
|
1507
|
-
}));
|
|
1508
|
-
const symbolWherePrefix = whereClause ? whereClause + " AND" : "WHERE";
|
|
1509
|
-
const symbolsResult = this.db.exec(
|
|
1510
|
-
`SELECT symbol_id, COUNT(*) as queries
|
|
1511
|
-
FROM session_events ${symbolWherePrefix} symbol_id IS NOT NULL
|
|
1512
|
-
GROUP BY symbol_id ORDER BY queries DESC LIMIT 5`
|
|
1513
|
-
);
|
|
1514
|
-
const topSymbols = (symbolsResult[0]?.values ?? []).map((row) => {
|
|
1515
|
-
const symbolId = String(row[0]);
|
|
1516
|
-
const parts = symbolId.split("::");
|
|
1517
|
-
const name = parts.length >= 2 ? parts[1] : symbolId;
|
|
1518
|
-
return {
|
|
1519
|
-
symbolId,
|
|
1520
|
-
name,
|
|
1521
|
-
queries: Number(row[1])
|
|
1522
|
-
};
|
|
1523
|
-
});
|
|
1524
|
-
const levelResult = this.db.exec(
|
|
1525
|
-
`SELECT detail_level, COUNT(*) as count
|
|
1526
|
-
FROM session_events ${symbolWherePrefix} detail_level IS NOT NULL
|
|
1527
|
-
GROUP BY detail_level ORDER BY detail_level`
|
|
1528
|
-
);
|
|
1529
|
-
const levelRows = levelResult[0]?.values ?? [];
|
|
1530
|
-
const levelTotal = levelRows.reduce((sum, row) => sum + Number(row[1]), 0);
|
|
1531
|
-
const detailLevelDistribution = levelRows.map((row) => ({
|
|
1532
|
-
level: String(row[0]),
|
|
1533
|
-
count: Number(row[1]),
|
|
1534
|
-
percentage: levelTotal > 0 ? Math.round(Number(row[1]) / levelTotal * 100) : 0
|
|
1535
|
-
}));
|
|
1536
|
-
return {
|
|
1537
|
-
totalCalls,
|
|
1538
|
-
totalTokensServed,
|
|
1539
|
-
topTools,
|
|
1540
|
-
topSymbols,
|
|
1541
|
-
detailLevelDistribution
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
clear() {
|
|
1545
|
-
this.ensureTable();
|
|
1546
|
-
this.db.run("DELETE FROM session_events");
|
|
1547
|
-
this.flushToDisk();
|
|
1548
|
-
}
|
|
1549
|
-
flushToDisk() {
|
|
1550
|
-
try {
|
|
1551
|
-
this.onWrite?.();
|
|
1552
|
-
} catch (err) {
|
|
1553
|
-
log7.error(`Failed to flush stats to disk: ${err.message}`);
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
};
|
|
1557
|
-
|
|
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
|
-
}
|
|
1613
|
-
|
|
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
|
-
}
|
|
1662
|
-
|
|
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);
|
|
1677
|
-
}
|
|
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;
|
|
1761
|
-
}
|
|
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;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
return best;
|
|
1781
|
-
}
|
|
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
|
-
}
|
|
1804
|
-
}
|
|
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 };
|
|
1809
|
-
}
|
|
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);
|
|
1816
|
-
}
|
|
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);
|
|
1864
|
-
}
|
|
1865
|
-
return edges;
|
|
1039
|
+
log5.warn(`${this.filePath} ${problem}; ${consequence}`);
|
|
1866
1040
|
}
|
|
1867
1041
|
};
|
|
1868
1042
|
|
|
@@ -2021,26 +1195,128 @@ var DriftDetector = class {
|
|
|
2021
1195
|
mapping.set(prevId, bestCurrentId);
|
|
2022
1196
|
}
|
|
2023
1197
|
}
|
|
2024
|
-
return mapping;
|
|
1198
|
+
return mapping;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
function indexByCommunityId(entries) {
|
|
1202
|
+
const index = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (const entry of entries) {
|
|
1204
|
+
const bucket = index.get(entry.communityId) ?? /* @__PURE__ */ new Set();
|
|
1205
|
+
bucket.add(entry.symbolId);
|
|
1206
|
+
index.set(entry.communityId, bucket);
|
|
1207
|
+
}
|
|
1208
|
+
return index;
|
|
1209
|
+
}
|
|
1210
|
+
function assignmentMap(entries) {
|
|
1211
|
+
const map = /* @__PURE__ */ new Map();
|
|
1212
|
+
for (const entry of entries) {
|
|
1213
|
+
map.set(entry.symbolId, entry.communityId);
|
|
1214
|
+
}
|
|
1215
|
+
return map;
|
|
1216
|
+
}
|
|
1217
|
+
function labelMap(entries) {
|
|
1218
|
+
const labels = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const entry of entries) {
|
|
1220
|
+
if (!labels.has(entry.communityId)) {
|
|
1221
|
+
labels.set(entry.communityId, entry.communityLabel);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return labels;
|
|
1225
|
+
}
|
|
1226
|
+
function jaccard(a, b) {
|
|
1227
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
1228
|
+
let intersection = 0;
|
|
1229
|
+
for (const x of a) if (b.has(x)) intersection++;
|
|
1230
|
+
const union = a.size + b.size - intersection;
|
|
1231
|
+
return union === 0 ? 0 : intersection / union;
|
|
1232
|
+
}
|
|
1233
|
+
function confidenceFor(distinctCommits) {
|
|
1234
|
+
if (distinctCommits >= 7) return "high";
|
|
1235
|
+
if (distinctCommits >= 3) return "medium";
|
|
1236
|
+
return "low";
|
|
1237
|
+
}
|
|
1238
|
+
function hintFor(distinctCommits) {
|
|
1239
|
+
if (distinctCommits >= 3) return void 0;
|
|
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.";
|
|
1241
|
+
}
|
|
1242
|
+
|
|
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"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
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
|
+
};
|
|
2025
1300
|
}
|
|
2026
1301
|
};
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
for (const
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
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++;
|
|
2033
1309
|
}
|
|
2034
|
-
return
|
|
1310
|
+
return count2;
|
|
2035
1311
|
}
|
|
2036
|
-
function
|
|
1312
|
+
function assignmentMap2(entries) {
|
|
2037
1313
|
const map = /* @__PURE__ */ new Map();
|
|
2038
1314
|
for (const entry of entries) {
|
|
2039
1315
|
map.set(entry.symbolId, entry.communityId);
|
|
2040
1316
|
}
|
|
2041
1317
|
return map;
|
|
2042
1318
|
}
|
|
2043
|
-
function
|
|
1319
|
+
function labelMap2(entries) {
|
|
2044
1320
|
const labels = /* @__PURE__ */ new Map();
|
|
2045
1321
|
for (const entry of entries) {
|
|
2046
1322
|
if (!labels.has(entry.communityId)) {
|
|
@@ -2049,200 +1325,96 @@ function labelMap(entries) {
|
|
|
2049
1325
|
}
|
|
2050
1326
|
return labels;
|
|
2051
1327
|
}
|
|
2052
|
-
function
|
|
2053
|
-
if (a.size === 0 && b.size === 0) return 1;
|
|
2054
|
-
let intersection = 0;
|
|
2055
|
-
for (const x of a) if (b.has(x)) intersection++;
|
|
2056
|
-
const union = a.size + b.size - intersection;
|
|
2057
|
-
return union === 0 ? 0 : intersection / union;
|
|
2058
|
-
}
|
|
2059
|
-
function confidenceFor(distinctCommits) {
|
|
1328
|
+
function confidenceFor2(distinctCommits) {
|
|
2060
1329
|
if (distinctCommits >= 7) return "high";
|
|
2061
1330
|
if (distinctCommits >= 3) return "medium";
|
|
2062
1331
|
return "low";
|
|
2063
1332
|
}
|
|
2064
|
-
function
|
|
2065
|
-
if (distinctCommits >=
|
|
1333
|
+
function hintFor2(distinctCommits) {
|
|
1334
|
+
if (distinctCommits >= MIN_DISTINCT_COMMITS_FOR_CONFIDENCE) return void 0;
|
|
2066
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.";
|
|
2067
1336
|
}
|
|
2068
1337
|
|
|
2069
|
-
// src/core/
|
|
2070
|
-
var
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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 };
|
|
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 };
|
|
2099
1353
|
}
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
|
|
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
|
|
2149
|
-
});
|
|
2150
|
-
queue.push({ id: nodeId, depth });
|
|
2151
|
-
}
|
|
1354
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1355
|
+
const initialScore = 1 / n;
|
|
1356
|
+
for (const node of nodes) {
|
|
1357
|
+
scores.set(node.symbolId, initialScore);
|
|
2152
1358
|
}
|
|
2153
|
-
const
|
|
2154
|
-
const
|
|
2155
|
-
|
|
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 };
|
|
2166
|
-
}
|
|
2167
|
-
};
|
|
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);
|
|
2182
|
-
}
|
|
2183
|
-
return [...strongestByFile.values()].sort(compareStrongestFirst).slice(0, Math.max(0, limit));
|
|
2184
|
-
}
|
|
2185
|
-
function compareStrongestFirst(a, b) {
|
|
2186
|
-
return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file, b.file);
|
|
2187
|
-
}
|
|
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);
|
|
2194
|
-
}
|
|
2195
|
-
function fileOf(symbolId) {
|
|
2196
|
-
return symbolId.split("::")[0];
|
|
2197
|
-
}
|
|
2198
|
-
|
|
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;
|
|
1359
|
+
const outDegree = /* @__PURE__ */ new Map();
|
|
1360
|
+
for (const node of nodes) {
|
|
1361
|
+
outDegree.set(node.symbolId, graph.getForwardEdges(node.symbolId).length);
|
|
2236
1362
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -2403,456 +1575,262 @@ var DeadCodeDetector = class {
|
|
|
2403
1575
|
return "Zero importers \u2014 no code references this symbol";
|
|
2404
1576
|
case 0.9:
|
|
2405
1577
|
return "Only referenced from test/config files";
|
|
2406
|
-
case 0.7:
|
|
2407
|
-
return "All importers are themselves dead (cascading)";
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
isFrameworkSymbol(name) {
|
|
2411
|
-
return FRAMEWORK_PATTERNS.some((p) => p.test(name));
|
|
2412
|
-
}
|
|
2413
|
-
computeCascadeDepths(graph, deadIds, candidateIds) {
|
|
2414
|
-
const depths = /* @__PURE__ */ new Map();
|
|
2415
|
-
const rootDead = /* @__PURE__ */ new Set();
|
|
2416
|
-
for (const id of deadIds) {
|
|
2417
|
-
const reverseEdges = graph.getReverseEdges(id);
|
|
2418
|
-
const deadImporters = reverseEdges.filter((e) => deadIds.has(e.from) && e.from !== id);
|
|
2419
|
-
if (deadImporters.length === 0) {
|
|
2420
|
-
rootDead.add(id);
|
|
2421
|
-
depths.set(id, 0);
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
const queue = [...rootDead].map((id) => ({ id, depth: 0 }));
|
|
2425
|
-
const visited = new Set(rootDead);
|
|
2426
|
-
while (queue.length > 0) {
|
|
2427
|
-
const current = queue.shift();
|
|
2428
|
-
for (const edge of graph.getForwardEdges(current.id)) {
|
|
2429
|
-
if (deadIds.has(edge.to) && !visited.has(edge.to) && candidateIds.has(edge.to)) {
|
|
2430
|
-
const newDepth = current.depth + 1;
|
|
2431
|
-
depths.set(edge.to, newDepth);
|
|
2432
|
-
visited.add(edge.to);
|
|
2433
|
-
queue.push({ id: edge.to, depth: newDepth });
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
return depths;
|
|
2438
|
-
}
|
|
2439
|
-
detectScaffolding(isExcluded, sourceContents) {
|
|
2440
|
-
if (!sourceContents || sourceContents.size === 0) return [];
|
|
2441
|
-
const results = [];
|
|
2442
|
-
for (const [file, content] of sourceContents) {
|
|
2443
|
-
if (isExcluded(file)) continue;
|
|
2444
|
-
const lines = content.split("\n");
|
|
2445
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2446
|
-
const line = lines[i];
|
|
2447
|
-
for (const { regex, pattern } of SCAFFOLDING_PATTERNS) {
|
|
2448
|
-
if (regex.test(line)) {
|
|
2449
|
-
results.push({
|
|
2450
|
-
file,
|
|
2451
|
-
line: i + 1,
|
|
2452
|
-
pattern,
|
|
2453
|
-
text: line.trim().slice(0, 120)
|
|
2454
|
-
});
|
|
2455
|
-
break;
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
return results;
|
|
2461
|
-
}
|
|
2462
|
-
matchesAny(value, patterns) {
|
|
2463
|
-
return patterns.some((p) => p.test(value));
|
|
2464
|
-
}
|
|
2465
|
-
};
|
|
2466
|
-
|
|
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;
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
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
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
};
|
|
2549
|
-
|
|
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"
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
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
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
};
|
|
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);
|
|
2623
|
-
}
|
|
2624
|
-
return map;
|
|
2625
|
-
}
|
|
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);
|
|
1578
|
+
case 0.7:
|
|
1579
|
+
return "All importers are themselves dead (cascading)";
|
|
2631
1580
|
}
|
|
2632
1581
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
function confidenceFor2(distinctCommits) {
|
|
2636
|
-
if (distinctCommits >= 7) return "high";
|
|
2637
|
-
if (distinctCommits >= 3) return "medium";
|
|
2638
|
-
return "low";
|
|
2639
|
-
}
|
|
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.";
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
// src/core/search/symbol-tokenizer.ts
|
|
2646
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2647
|
-
"get",
|
|
2648
|
-
"set",
|
|
2649
|
-
"is",
|
|
2650
|
-
"has",
|
|
2651
|
-
"can",
|
|
2652
|
-
"do",
|
|
2653
|
-
"the",
|
|
2654
|
-
"a",
|
|
2655
|
-
"an",
|
|
2656
|
-
"of",
|
|
2657
|
-
"to",
|
|
2658
|
-
"in",
|
|
2659
|
-
"for",
|
|
2660
|
-
"on",
|
|
2661
|
-
"with",
|
|
2662
|
-
"at",
|
|
2663
|
-
"by",
|
|
2664
|
-
"from",
|
|
2665
|
-
"as",
|
|
2666
|
-
"into",
|
|
2667
|
-
"not",
|
|
2668
|
-
"no",
|
|
2669
|
-
"or"
|
|
2670
|
-
]);
|
|
2671
|
-
var DEFAULT_OPTIONS = {
|
|
2672
|
-
includeStopWords: false,
|
|
2673
|
-
includeOriginal: true,
|
|
2674
|
-
includeFilePath: false
|
|
2675
|
-
};
|
|
2676
|
-
var SymbolTokenizer = class _SymbolTokenizer {
|
|
2677
|
-
options;
|
|
2678
|
-
constructor(options) {
|
|
2679
|
-
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1582
|
+
isFrameworkSymbol(name) {
|
|
1583
|
+
return FRAMEWORK_PATTERNS.some((p) => p.test(name));
|
|
2680
1584
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
*/
|
|
2691
|
-
tokenize(symbolName, filePath) {
|
|
2692
|
-
const splitTokens = _SymbolTokenizer.splitName(symbolName);
|
|
2693
|
-
const lowered = splitTokens.map((t) => t.toLowerCase());
|
|
2694
|
-
const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
|
|
2695
|
-
if (this.options.includeOriginal) {
|
|
2696
|
-
const original = symbolName.toLowerCase();
|
|
2697
|
-
if (!tokens.includes(original)) {
|
|
2698
|
-
tokens.unshift(original);
|
|
1585
|
+
computeCascadeDepths(graph, deadIds, candidateIds) {
|
|
1586
|
+
const depths = /* @__PURE__ */ new Map();
|
|
1587
|
+
const rootDead = /* @__PURE__ */ new Set();
|
|
1588
|
+
for (const id of deadIds) {
|
|
1589
|
+
const reverseEdges = graph.getReverseEdges(id);
|
|
1590
|
+
const deadImporters = reverseEdges.filter((e) => deadIds.has(e.from) && e.from !== id);
|
|
1591
|
+
if (deadImporters.length === 0) {
|
|
1592
|
+
rootDead.add(id);
|
|
1593
|
+
depths.set(id, 0);
|
|
2699
1594
|
}
|
|
2700
1595
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
1596
|
+
const queue = [...rootDead].map((id) => ({ id, depth: 0 }));
|
|
1597
|
+
const visited = new Set(rootDead);
|
|
1598
|
+
while (queue.length > 0) {
|
|
1599
|
+
const current = queue.shift();
|
|
1600
|
+
for (const edge of graph.getForwardEdges(current.id)) {
|
|
1601
|
+
if (deadIds.has(edge.to) && !visited.has(edge.to) && candidateIds.has(edge.to)) {
|
|
1602
|
+
const newDepth = current.depth + 1;
|
|
1603
|
+
depths.set(edge.to, newDepth);
|
|
1604
|
+
visited.add(edge.to);
|
|
1605
|
+
queue.push({ id: edge.to, depth: newDepth });
|
|
2706
1606
|
}
|
|
2707
1607
|
}
|
|
2708
1608
|
}
|
|
2709
|
-
return
|
|
1609
|
+
return depths;
|
|
2710
1610
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
1611
|
+
detectScaffolding(isExcluded, sourceContents) {
|
|
1612
|
+
if (!sourceContents || sourceContents.size === 0) return [];
|
|
1613
|
+
const results = [];
|
|
1614
|
+
for (const [file, content] of sourceContents) {
|
|
1615
|
+
if (isExcluded(file)) continue;
|
|
1616
|
+
const lines = content.split("\n");
|
|
1617
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1618
|
+
const line = lines[i];
|
|
1619
|
+
for (const { regex, pattern } of SCAFFOLDING_PATTERNS) {
|
|
1620
|
+
if (regex.test(line)) {
|
|
1621
|
+
results.push({
|
|
1622
|
+
file,
|
|
1623
|
+
line: i + 1,
|
|
1624
|
+
pattern,
|
|
1625
|
+
text: line.trim().slice(0, 120)
|
|
1626
|
+
});
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
2723
1629
|
}
|
|
2724
1630
|
}
|
|
2725
1631
|
}
|
|
2726
|
-
return
|
|
1632
|
+
return results;
|
|
2727
1633
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
1634
|
+
matchesAny(value, patterns) {
|
|
1635
|
+
return patterns.some((p) => p.test(value));
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
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;
|
|
2743
1676
|
}
|
|
2744
|
-
return
|
|
1677
|
+
return { layers };
|
|
2745
1678
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
const tokens = [];
|
|
2751
|
-
let current = "";
|
|
2752
|
-
for (let i = 0; i < segment.length; i++) {
|
|
2753
|
-
const ch = segment[i];
|
|
2754
|
-
const prev = i > 0 ? segment[i - 1] : "";
|
|
2755
|
-
if (current.length > 0) {
|
|
2756
|
-
const prevIsDigit = isDigit(prev);
|
|
2757
|
-
const currIsDigit = isDigit(ch);
|
|
2758
|
-
if (prevIsDigit !== currIsDigit) {
|
|
2759
|
-
tokens.push(current);
|
|
2760
|
-
current = ch;
|
|
2761
|
-
continue;
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
if (current.length > 0 && isLower(prev) && isUpper(ch)) {
|
|
2765
|
-
tokens.push(current);
|
|
2766
|
-
current = ch;
|
|
2767
|
-
continue;
|
|
2768
|
-
}
|
|
2769
|
-
if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
|
|
2770
|
-
const lastChar = current[current.length - 1];
|
|
2771
|
-
if (isUpper(lastChar)) {
|
|
2772
|
-
tokens.push(current.slice(0, -1));
|
|
2773
|
-
current = lastChar + ch;
|
|
2774
|
-
continue;
|
|
2775
|
-
}
|
|
1679
|
+
matchLayer(filePath) {
|
|
1680
|
+
for (const rule of this.rules) {
|
|
1681
|
+
if (rule.pattern.test(filePath)) {
|
|
1682
|
+
return rule.layer;
|
|
2776
1683
|
}
|
|
2777
|
-
current += ch;
|
|
2778
1684
|
}
|
|
2779
|
-
|
|
2780
|
-
tokens.push(current);
|
|
2781
|
-
}
|
|
2782
|
-
return tokens;
|
|
1685
|
+
return "Unknown";
|
|
2783
1686
|
}
|
|
1687
|
+
};
|
|
1688
|
+
|
|
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 {
|
|
2784
1709
|
/**
|
|
2785
|
-
*
|
|
2786
|
-
*
|
|
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.
|
|
2787
1714
|
*/
|
|
2788
|
-
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
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
|
+
}
|
|
2798
1761
|
}
|
|
1762
|
+
rawEntries.push({
|
|
1763
|
+
symbolId: nodeId,
|
|
1764
|
+
depth,
|
|
1765
|
+
riskScore: Math.round(riskScore * 1e3) / 1e3,
|
|
1766
|
+
confidence,
|
|
1767
|
+
edgeKinds: [...info.kinds],
|
|
1768
|
+
coChangeFrequency
|
|
1769
|
+
});
|
|
1770
|
+
queue.push({ id: nodeId, depth });
|
|
2799
1771
|
}
|
|
2800
1772
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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 };
|
|
2806
1786
|
}
|
|
2807
1787
|
};
|
|
2808
|
-
|
|
2809
|
-
|
|
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);
|
|
1802
|
+
}
|
|
1803
|
+
return [...strongestByFile.values()].sort(compareStrongestFirst).slice(0, Math.max(0, limit));
|
|
1804
|
+
}
|
|
1805
|
+
function compareStrongestFirst(a, b) {
|
|
1806
|
+
return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file, b.file);
|
|
2810
1807
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
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);
|
|
2813
1814
|
}
|
|
2814
|
-
function
|
|
2815
|
-
return
|
|
1815
|
+
function fileOf(symbolId) {
|
|
1816
|
+
return symbolId.split("::")[0];
|
|
2816
1817
|
}
|
|
2817
1818
|
|
|
2818
1819
|
export {
|
|
2819
|
-
|
|
2820
|
-
CommunitySnapshotWriter,
|
|
2821
|
-
SqliteStorageAdapter,
|
|
1820
|
+
detectWorkspace,
|
|
2822
1821
|
buildCoChangeMatrix,
|
|
2823
1822
|
CoChangeReader,
|
|
2824
|
-
|
|
2825
|
-
|
|
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,
|
|
1823
|
+
CommunitySnapshotWriter,
|
|
1824
|
+
SqliteStorageAdapter,
|
|
2844
1825
|
RevertDetector,
|
|
2845
1826
|
DriftDetector,
|
|
2846
|
-
SimpleGitAdapter,
|
|
2847
|
-
BlastRadiusCalculator,
|
|
2848
|
-
findCoChangedFilesForBlastRadius,
|
|
2849
|
-
ArchitecturalOverlay,
|
|
2850
|
-
ContextsViewReader,
|
|
2851
|
-
DeadCodeDetector,
|
|
2852
|
-
SymbolTokenizer,
|
|
2853
|
-
PageRankCalculator,
|
|
2854
1827
|
BoundaryViolationDetector,
|
|
1828
|
+
PageRankCalculator,
|
|
2855
1829
|
SessionRecorderAdapter,
|
|
2856
|
-
|
|
1830
|
+
DeadCodeDetector,
|
|
1831
|
+
ArchitecturalOverlay,
|
|
1832
|
+
BlastRadiusCalculator,
|
|
1833
|
+
findCoChangedFilesForBlastRadius,
|
|
1834
|
+
ContextsViewReader
|
|
2857
1835
|
};
|
|
2858
|
-
//# sourceMappingURL=chunk-
|
|
1836
|
+
//# sourceMappingURL=chunk-A4URKKPP.js.map
|