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