@ctxo/cli 0.12.0 → 0.13.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 +51 -5
- package/dist/{chunk-HZ4UGA6J.js → chunk-BYX3UFBJ.js} +1374 -324
- package/dist/chunk-BYX3UFBJ.js.map +1 -0
- package/dist/{chunk-CDGQGCVN.js → chunk-DT3ZH4AU.js} +90 -25
- package/dist/chunk-DT3ZH4AU.js.map +1 -0
- package/dist/{cli-router-LO2SWFC4.js → cli-router-RR4CM62C.js} +3717 -945
- package/dist/cli-router-RR4CM62C.js.map +1 -0
- package/dist/index.js +272 -273
- package/dist/index.js.map +1 -1
- package/dist/json-index-reader-OXTRTGE6.js +13 -0
- package/llms-full.txt +220 -28
- package/llms.txt +53 -10
- package/package.json +8 -6
- package/pages/visualize-template.html +8 -2
- package/skills/ctxo-review-pr/SKILL.md +2 -0
- package/skills/ctxo-safe-edit/SKILL.md +2 -0
- package/skills/ctxo-understand/SKILL.md +28 -4
- package/dist/chunk-CDGQGCVN.js.map +0 -1
- package/dist/chunk-HZ4UGA6J.js.map +0 -1
- package/dist/cli-router-LO2SWFC4.js.map +0 -1
- package/dist/json-index-reader-UYS74USB.js +0 -9
- /package/dist/{json-index-reader-UYS74USB.js.map → json-index-reader-OXTRTGE6.js.map} +0 -0
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
atomicWrite
|
|
4
4
|
} from "./chunk-RFEXMBTG.js";
|
|
5
5
|
import {
|
|
6
|
+
CoChangeMatrixSchema,
|
|
7
|
+
EDGE_KINDS,
|
|
8
|
+
EdgeKindSchema,
|
|
6
9
|
JsonIndexReader
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-DT3ZH4AU.js";
|
|
8
11
|
import {
|
|
9
12
|
createLogger
|
|
10
13
|
} from "./chunk-5DSKZJNN.js";
|
|
@@ -24,11 +27,19 @@ import {
|
|
|
24
27
|
} from "fs";
|
|
25
28
|
import { tmpdir } from "os";
|
|
26
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
|
|
27
37
|
var log = createLogger("ctxo:community-snapshot");
|
|
28
38
|
var DEFAULT_HISTORY_LIMIT = 10;
|
|
29
39
|
var HISTORY_DIR_NAME = "communities.history";
|
|
30
40
|
var CURRENT_FILE_NAME = "communities.json";
|
|
31
41
|
var NOCOMMIT_SHA = "nocommit";
|
|
42
|
+
var COLLISION_COUNTER_RE = /^(.*)\.(\d+)\.json$/;
|
|
32
43
|
var CommunitySnapshotWriter = class {
|
|
33
44
|
indexDir;
|
|
34
45
|
historyDir;
|
|
@@ -48,7 +59,8 @@ var CommunitySnapshotWriter = class {
|
|
|
48
59
|
try {
|
|
49
60
|
const existingRaw = readFileSync(currentPath, "utf-8");
|
|
50
61
|
const existing = JSON.parse(existingRaw);
|
|
51
|
-
|
|
62
|
+
const existingCommit = archivedCommit(existing);
|
|
63
|
+
if (existingCommit !== void 0 && existingCommit !== snapshot.commitSha) {
|
|
52
64
|
const historyFile = this.historyFilename(existing);
|
|
53
65
|
this.atomicWrite(join(this.historyDir, historyFile), existingRaw);
|
|
54
66
|
}
|
|
@@ -65,14 +77,23 @@ var CommunitySnapshotWriter = class {
|
|
|
65
77
|
if (!existsSync(path)) return void 0;
|
|
66
78
|
return this.safeParse(path);
|
|
67
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Archived snapshots, newest first, at most one per commit. Where several
|
|
82
|
+
* files hold the same commitSha (older versions archived once per
|
|
83
|
+
* `ctxo index --file` run) the newest file wins. `limit` caps the number of
|
|
84
|
+
* commits returned. Read-only: the duplicates stay on disk until the next
|
|
85
|
+
* write rotates them out.
|
|
86
|
+
*/
|
|
68
87
|
listHistory(limit) {
|
|
69
88
|
if (!existsSync(this.historyDir)) return [];
|
|
70
|
-
const entries = readdirSync(this.historyDir).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
71
89
|
const snapshots = [];
|
|
72
|
-
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
for (const name of this.historyNames().reverse()) {
|
|
73
92
|
const snap = this.safeParse(join(this.historyDir, name));
|
|
74
|
-
|
|
75
|
-
if (!snap
|
|
93
|
+
const commit = archivedCommit(snap);
|
|
94
|
+
if (!snap || commit === void 0) continue;
|
|
95
|
+
if (seen.has(commit)) continue;
|
|
96
|
+
seen.add(commit);
|
|
76
97
|
snapshots.push(snap);
|
|
77
98
|
if (typeof limit === "number" && snapshots.length >= limit) break;
|
|
78
99
|
}
|
|
@@ -93,13 +114,39 @@ var CommunitySnapshotWriter = class {
|
|
|
93
114
|
}
|
|
94
115
|
return candidate.slice(this.historyDir.length + 1);
|
|
95
116
|
}
|
|
117
|
+
/** The `.json` files in the history folder, oldest first. */
|
|
118
|
+
historyNames() {
|
|
119
|
+
return readdirSync(this.historyDir).filter((name) => name.endsWith(".json")).sort(compareHistoryNames);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Rotation in two steps. First the duplicates go: every file that is not
|
|
123
|
+
* the newest readable file of its commit (the copies older versions left
|
|
124
|
+
* behind, one per `ctxo index --file` run). Then, by file count, the oldest
|
|
125
|
+
* files beyond `historyLimit`. Copies therefore never use up slots, history
|
|
126
|
+
* keeps each commit at most once, and a commit's newest file is removed
|
|
127
|
+
* only together with its commit, when that is among the oldest beyond the
|
|
128
|
+
* limit. Files that stand for no commit (unreadable, 'nocommit') are not
|
|
129
|
+
* duplicates of anything and age out by count like the rest.
|
|
130
|
+
*/
|
|
96
131
|
rotateHistory() {
|
|
97
132
|
if (!existsSync(this.historyDir)) return;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
for (
|
|
102
|
-
const
|
|
133
|
+
const seen = /* @__PURE__ */ new Set();
|
|
134
|
+
const unique = [];
|
|
135
|
+
const doomed = [];
|
|
136
|
+
for (const name of this.historyNames().reverse()) {
|
|
137
|
+
const commit = archivedCommit(this.safeParse(join(this.historyDir, name)));
|
|
138
|
+
if (commit !== void 0) {
|
|
139
|
+
if (seen.has(commit)) {
|
|
140
|
+
doomed.push(name);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
seen.add(commit);
|
|
144
|
+
}
|
|
145
|
+
unique.push(name);
|
|
146
|
+
}
|
|
147
|
+
doomed.push(...unique.slice(this.historyLimit));
|
|
148
|
+
for (const name of doomed) {
|
|
149
|
+
const target = join(this.historyDir, name);
|
|
103
150
|
try {
|
|
104
151
|
unlinkSync(target);
|
|
105
152
|
} catch (err) {
|
|
@@ -117,6 +164,19 @@ var CommunitySnapshotWriter = class {
|
|
|
117
164
|
}
|
|
118
165
|
}
|
|
119
166
|
};
|
|
167
|
+
function archivedCommit(snapshot) {
|
|
168
|
+
const sha = snapshot?.commitSha;
|
|
169
|
+
return sha && sha !== NOCOMMIT_SHA ? sha : void 0;
|
|
170
|
+
}
|
|
171
|
+
function compareHistoryNames(a, b) {
|
|
172
|
+
const [baseA, counterA] = splitCollisionCounter(a);
|
|
173
|
+
const [baseB, counterB] = splitCollisionCounter(b);
|
|
174
|
+
return compareCodepoint(baseA, baseB) || counterA - counterB;
|
|
175
|
+
}
|
|
176
|
+
function splitCollisionCounter(name) {
|
|
177
|
+
const match = COLLISION_COUNTER_RE.exec(name);
|
|
178
|
+
return match ? [`${match[1]}.json`, Number(match[2])] : [name, 0];
|
|
179
|
+
}
|
|
120
180
|
function assertSafeWritePath(ctxoRoot, allowProduction) {
|
|
121
181
|
const resolved = resolve(ctxoRoot);
|
|
122
182
|
const tmpRoot = resolve(tmpdir());
|
|
@@ -474,14 +534,593 @@ var SqliteStorageAdapter = class {
|
|
|
474
534
|
}
|
|
475
535
|
};
|
|
476
536
|
|
|
537
|
+
// src/adapters/storage/co-change-reader.ts
|
|
538
|
+
import { readFileSync as readFileSync3, statSync } from "fs";
|
|
539
|
+
import { join as join3 } from "path";
|
|
540
|
+
|
|
541
|
+
// src/core/co-change/co-change-analyzer.ts
|
|
542
|
+
var MIN_FREQUENCY = 0.1;
|
|
543
|
+
function buildCoChangeMatrix(windowed, options) {
|
|
544
|
+
const commitsPerFile = /* @__PURE__ */ new Map();
|
|
545
|
+
const sharedPerPair = /* @__PURE__ */ new Map();
|
|
546
|
+
let commitsUsed = 0;
|
|
547
|
+
let massSkipped = 0;
|
|
548
|
+
for (const commit of windowed) {
|
|
549
|
+
const files = includedFiles(commit, options.isIncluded);
|
|
550
|
+
if (files.length === 0) continue;
|
|
551
|
+
if (files.length > options.maxFilesPerCommit) {
|
|
552
|
+
massSkipped++;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
commitsUsed++;
|
|
556
|
+
for (const file of files) {
|
|
557
|
+
commitsPerFile.set(file, (commitsPerFile.get(file) ?? 0) + 1);
|
|
558
|
+
}
|
|
559
|
+
for (let i = 0; i < files.length - 1; i++) {
|
|
560
|
+
const file1 = files[i];
|
|
561
|
+
let partners = sharedPerPair.get(file1);
|
|
562
|
+
if (!partners) {
|
|
563
|
+
partners = /* @__PURE__ */ new Map();
|
|
564
|
+
sharedPerPair.set(file1, partners);
|
|
565
|
+
}
|
|
566
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
567
|
+
const file2 = files[j];
|
|
568
|
+
partners.set(file2, (partners.get(file2) ?? 0) + 1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const entries = [];
|
|
573
|
+
for (const [file1, partners] of sharedPerPair) {
|
|
574
|
+
for (const [file2, sharedCommits] of partners) {
|
|
575
|
+
if (sharedCommits < options.minShared) continue;
|
|
576
|
+
const smallerHistory = Math.min(commitsPerFile.get(file1), commitsPerFile.get(file2));
|
|
577
|
+
const frequency = Math.round(sharedCommits / smallerHistory * 1e3) / 1e3;
|
|
578
|
+
if (frequency < MIN_FREQUENCY) continue;
|
|
579
|
+
entries.push({ file1, file2, sharedCommits, frequency });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
entries.sort(compareEntries);
|
|
583
|
+
const window = {
|
|
584
|
+
maxCommits: options.maxCommits,
|
|
585
|
+
...options.since !== void 0 ? { since: options.since } : {},
|
|
586
|
+
head: options.head,
|
|
587
|
+
commitsScanned: windowed.length,
|
|
588
|
+
commitsUsed,
|
|
589
|
+
massSkipped,
|
|
590
|
+
maxFilesPerCommit: options.maxFilesPerCommit,
|
|
591
|
+
minShared: options.minShared
|
|
592
|
+
};
|
|
593
|
+
return { version: 1, timestamp: options.timestamp, window, entries };
|
|
594
|
+
}
|
|
595
|
+
function includedFiles(commit, isIncluded) {
|
|
596
|
+
const included = /* @__PURE__ */ new Set();
|
|
597
|
+
for (const file of commit.files) {
|
|
598
|
+
if (isIncluded(file)) included.add(file);
|
|
599
|
+
}
|
|
600
|
+
return [...included].sort(compareCodepoint);
|
|
601
|
+
}
|
|
602
|
+
function compareEntries(a, b) {
|
|
603
|
+
return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file1, b.file1) || compareCodepoint(a.file2, b.file2);
|
|
604
|
+
}
|
|
605
|
+
function loadCoChangeMap(matrix) {
|
|
606
|
+
const map = /* @__PURE__ */ new Map();
|
|
607
|
+
for (const entry of matrix.entries) {
|
|
608
|
+
let list1 = map.get(entry.file1);
|
|
609
|
+
if (!list1) {
|
|
610
|
+
list1 = [];
|
|
611
|
+
map.set(entry.file1, list1);
|
|
612
|
+
}
|
|
613
|
+
list1.push(entry);
|
|
614
|
+
let list2 = map.get(entry.file2);
|
|
615
|
+
if (!list2) {
|
|
616
|
+
list2 = [];
|
|
617
|
+
map.set(entry.file2, list2);
|
|
618
|
+
}
|
|
619
|
+
list2.push(entry);
|
|
620
|
+
}
|
|
621
|
+
return map;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/adapters/storage/co-change-reader.ts
|
|
625
|
+
var log3 = createLogger("ctxo:co-change");
|
|
626
|
+
var UNTIL_REWRITTEN = "co-change is ignored until ctxo index rewrites it";
|
|
627
|
+
var UNTIL_RETRY = "co-change is ignored for now and the next call tries again";
|
|
628
|
+
var CoChangeReader = class {
|
|
629
|
+
filePath;
|
|
630
|
+
cached;
|
|
631
|
+
/** What the last warning was about, so one problem is reported once. */
|
|
632
|
+
lastWarning;
|
|
633
|
+
constructor(ctxoRoot) {
|
|
634
|
+
this.filePath = join3(ctxoRoot, "index", "co-changes.json");
|
|
635
|
+
}
|
|
636
|
+
readCoChangeMap() {
|
|
637
|
+
let stats;
|
|
638
|
+
try {
|
|
639
|
+
stats = statSync(this.filePath);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
this.cached = void 0;
|
|
642
|
+
const { code, message } = err;
|
|
643
|
+
if (code === "ENOENT") {
|
|
644
|
+
this.lastWarning = void 0;
|
|
645
|
+
} else {
|
|
646
|
+
this.warnOnce(`stat ${code ?? message}`, `cannot be inspected (${message})`, UNTIL_RETRY);
|
|
647
|
+
}
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
const version = `${stats.mtimeMs}:${stats.size}`;
|
|
651
|
+
if (this.cached?.version === version) return this.cached.map;
|
|
652
|
+
return this.load(version);
|
|
653
|
+
}
|
|
654
|
+
load(version) {
|
|
655
|
+
let raw;
|
|
656
|
+
try {
|
|
657
|
+
raw = readFileSync3(this.filePath, "utf-8");
|
|
658
|
+
} catch (err) {
|
|
659
|
+
this.cached = void 0;
|
|
660
|
+
this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY);
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
const map = this.parse(raw, version);
|
|
664
|
+
this.cached = { version, map };
|
|
665
|
+
if (map) this.lastWarning = void 0;
|
|
666
|
+
return map;
|
|
667
|
+
}
|
|
668
|
+
parse(raw, version) {
|
|
669
|
+
let data;
|
|
670
|
+
try {
|
|
671
|
+
data = JSON.parse(raw);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
return this.unusable(version, `is not valid JSON (${err.message})`);
|
|
674
|
+
}
|
|
675
|
+
const parsed = CoChangeMatrixSchema.safeParse(data);
|
|
676
|
+
if (!parsed.success) {
|
|
677
|
+
const issue = parsed.error.issues[0];
|
|
678
|
+
const where = issue && issue.path.length > 0 ? `${issue.path.map(String).join(".")}: ` : "";
|
|
679
|
+
return this.unusable(version, `is not a valid co-change matrix (${where}${issue?.message ?? "unknown problem"})`);
|
|
680
|
+
}
|
|
681
|
+
return freezeEntries(loadCoChangeMap(parsed.data));
|
|
682
|
+
}
|
|
683
|
+
unusable(version, problem) {
|
|
684
|
+
this.warnOnce(`content ${version}`, problem, UNTIL_REWRITTEN);
|
|
685
|
+
return void 0;
|
|
686
|
+
}
|
|
687
|
+
warnOnce(key, problem, consequence) {
|
|
688
|
+
if (this.lastWarning === key) return;
|
|
689
|
+
this.lastWarning = key;
|
|
690
|
+
log3.warn(`${this.filePath} ${problem}; ${consequence}`);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function freezeEntries(map) {
|
|
694
|
+
for (const list of map.values()) {
|
|
695
|
+
for (const entry of list) Object.freeze(entry);
|
|
696
|
+
Object.freeze(list);
|
|
697
|
+
}
|
|
698
|
+
return map;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/core/co-change/co-change-window.ts
|
|
702
|
+
var SINCE_PATTERN = /^(\d+)([dwmy])$/;
|
|
703
|
+
var SECONDS_PER_DAY = 86400;
|
|
704
|
+
function parseCoChangeSince(value) {
|
|
705
|
+
const match = SINCE_PATTERN.exec(value);
|
|
706
|
+
if (!match) return void 0;
|
|
707
|
+
const amount = Number.parseInt(match[1], 10);
|
|
708
|
+
if (!Number.isSafeInteger(amount)) return void 0;
|
|
709
|
+
return { amount, unit: match[2] };
|
|
710
|
+
}
|
|
711
|
+
function sinceCutoff(headCommittedAt, since) {
|
|
712
|
+
const cutoff = subtractSince(headCommittedAt, since);
|
|
713
|
+
return Number.isFinite(cutoff) ? cutoff : Number.NEGATIVE_INFINITY;
|
|
714
|
+
}
|
|
715
|
+
function subtractSince(headCommittedAt, since) {
|
|
716
|
+
switch (since.unit) {
|
|
717
|
+
case "d":
|
|
718
|
+
return headCommittedAt - since.amount * SECONDS_PER_DAY;
|
|
719
|
+
case "w":
|
|
720
|
+
return headCommittedAt - since.amount * 7 * SECONDS_PER_DAY;
|
|
721
|
+
case "m":
|
|
722
|
+
return subtractUtcMonths(headCommittedAt, since.amount);
|
|
723
|
+
case "y":
|
|
724
|
+
return subtractUtcMonths(headCommittedAt, since.amount * 12);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function subtractUtcMonths(epochSeconds, months) {
|
|
728
|
+
const date = new Date(epochSeconds * 1e3);
|
|
729
|
+
const dayOfMonth = date.getUTCDate();
|
|
730
|
+
date.setUTCDate(1);
|
|
731
|
+
date.setUTCMonth(date.getUTCMonth() - months);
|
|
732
|
+
date.setUTCDate(Math.min(dayOfMonth, daysInUtcMonth(date)));
|
|
733
|
+
return date.getTime() / 1e3;
|
|
734
|
+
}
|
|
735
|
+
function daysInUtcMonth(date) {
|
|
736
|
+
const lastDay = new Date(date.getTime());
|
|
737
|
+
lastDay.setUTCMonth(lastDay.getUTCMonth() + 1, 0);
|
|
738
|
+
return lastDay.getUTCDate();
|
|
739
|
+
}
|
|
740
|
+
function selectWindow(commits, maxCommits, cutoff) {
|
|
741
|
+
const newest = maxCommits === "all" ? [...commits] : commits.slice(0, wholeCommitCount(maxCommits));
|
|
742
|
+
return cutoff === void 0 ? newest : newest.filter((commit) => commit.committedAt >= cutoff);
|
|
743
|
+
}
|
|
744
|
+
function wholeCommitCount(maxCommits) {
|
|
745
|
+
return maxCommits >= 1 ? Math.floor(maxCommits) : 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/core/config/load-config.ts
|
|
749
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
750
|
+
import { join as join4 } from "path";
|
|
751
|
+
import { parse as parseYaml } from "yaml";
|
|
752
|
+
import picomatch from "picomatch";
|
|
753
|
+
|
|
754
|
+
// src/core/config/config-schema.ts
|
|
755
|
+
import { z } from "zod";
|
|
756
|
+
var StatsConfigSchema = z.object({
|
|
757
|
+
enabled: z.boolean().optional()
|
|
758
|
+
}).strict();
|
|
759
|
+
var IndexConfigSchema = z.object({
|
|
760
|
+
ignore: z.array(z.string().min(1)).optional(),
|
|
761
|
+
ignoreProjects: z.array(z.string().min(1)).optional(),
|
|
762
|
+
facts: z.boolean().optional(),
|
|
763
|
+
tests: z.array(z.string().min(1)).optional()
|
|
764
|
+
}).strict();
|
|
765
|
+
var MaskingConfigSchema = z.object({
|
|
766
|
+
/**
|
|
767
|
+
* Glob patterns matched against community cluster labels. Matching labels
|
|
768
|
+
* are replaced with `[masked-cluster-N]` in MCP tool responses so
|
|
769
|
+
* org-sensitive folder names (e.g. `src/payment-vault`, `internal-*`)
|
|
770
|
+
* never surface to AI agents over the stdio channel.
|
|
771
|
+
*/
|
|
772
|
+
clusterLabels: z.array(z.string().min(1)).optional()
|
|
773
|
+
}).strict();
|
|
774
|
+
var WatchConfigSchema = z.object({
|
|
775
|
+
/**
|
|
776
|
+
* Minimum number of files that must be re-indexed since the last
|
|
777
|
+
* community snapshot before `ctxo watch` recomputes one. Default: 5.
|
|
778
|
+
* On large monorepos Louvain + PageRank cost ~1 s of blocking CPU;
|
|
779
|
+
* bumping this (e.g. 25) avoids recomputing on every save.
|
|
780
|
+
*/
|
|
781
|
+
snapshotMinFileChanges: z.number().int().min(1).optional()
|
|
782
|
+
}).strict();
|
|
783
|
+
var GateConfigSchema = z.object({
|
|
784
|
+
enabled: z.boolean().optional(),
|
|
785
|
+
sensitivity: z.enum(["strict", "balanced", "lenient"]).optional(),
|
|
786
|
+
/** Absolute floor: minimum confirmed+likely dependents before the gate can fire. */
|
|
787
|
+
minDependents: z.number().int().min(0).optional(),
|
|
788
|
+
/** Repo-relative significance: symbol must be in the top `percentile`% by PageRank. */
|
|
789
|
+
percentile: z.number().int().min(1).max(100).optional()
|
|
790
|
+
}).strict();
|
|
791
|
+
var CoChangeConfigSchema = z.object({
|
|
792
|
+
/**
|
|
793
|
+
* Size of the co-change window: the last N non-merge commits from HEAD, or "all".
|
|
794
|
+
* Default: 2000. Independent of `ctxo index --max-history`, which only sets how much
|
|
795
|
+
* per-file commit history is kept.
|
|
796
|
+
*/
|
|
797
|
+
maxCommits: z.union([z.number().int().min(1), z.literal("all")], { error: 'must be a positive integer or "all"' }).optional(),
|
|
798
|
+
/**
|
|
799
|
+
* Narrows the window to a duration counted back from HEAD's committer date (never the wall
|
|
800
|
+
* clock): `30d`, `2w`, `12m` or `1y`. Applies together with `maxCommits`.
|
|
801
|
+
*/
|
|
802
|
+
since: z.string().refine((value) => parseCoChangeSince(value) !== void 0, {
|
|
803
|
+
error: "must be a duration such as 30d, 2w, 12m or 1y"
|
|
804
|
+
}).optional(),
|
|
805
|
+
/** Commits touching more analysed files than this are skipped as mass changes. Default: 30. */
|
|
806
|
+
maxFilesPerCommit: z.number().int().min(1).optional(),
|
|
807
|
+
/** A file pair becomes an entry only after this many shared commits. Default: 2. */
|
|
808
|
+
minShared: z.number().int().min(1).optional(),
|
|
809
|
+
/** Extra tracked files to analyse that no language plugin indexes, such as SQL scripts (globs). */
|
|
810
|
+
include: z.array(z.string().min(1)).optional(),
|
|
811
|
+
/** Indexed files to leave out of the analysis (globs). */
|
|
812
|
+
exclude: z.array(z.string().min(1)).optional()
|
|
813
|
+
}).strict();
|
|
814
|
+
var CommunitiesConfigSchema = z.object({
|
|
815
|
+
/** Louvain resolution: higher gives more, smaller communities. Default: 1.0. */
|
|
816
|
+
resolution: z.number().positive().optional(),
|
|
817
|
+
/** Minimum number of communities a symbol must bridge to count as a god node. Default: 3. */
|
|
818
|
+
godNodeMinBridged: z.number().int().min(1).optional(),
|
|
819
|
+
/** Maximum number of god nodes reported. Default: 25. */
|
|
820
|
+
godNodeMaxResults: z.number().int().min(0).optional()
|
|
821
|
+
}).strict();
|
|
822
|
+
var ContextsViewConfigSchema = z.object({
|
|
823
|
+
/** Blend of the file-edge signals. Defaults: dependency 1.0, coChange 1.0, data 0 (shared tables are a finding, not a clustering weight). */
|
|
824
|
+
weights: z.object({
|
|
825
|
+
dependency: z.number().min(0).optional(),
|
|
826
|
+
coChange: z.number().min(0).optional(),
|
|
827
|
+
data: z.number().min(0).optional()
|
|
828
|
+
}).strict().optional(),
|
|
829
|
+
/** Files to leave out of the file graph (globs). */
|
|
830
|
+
exclude: z.array(z.string().min(1)).optional(),
|
|
831
|
+
hubs: z.object({
|
|
832
|
+
/** Detect hubs automatically (fan-in and god nodes). Default: true. */
|
|
833
|
+
auto: z.boolean().optional(),
|
|
834
|
+
/** A file is a fan-in hub when it is in the top `topPercent`% by dependents. Default: 1. */
|
|
835
|
+
topPercent: z.number().positive().max(100).optional(),
|
|
836
|
+
/** Minimum distinct dependent files for a fan-in hub. Default: 10. */
|
|
837
|
+
minDependents: z.number().int().min(1).optional(),
|
|
838
|
+
/** Files that lose all their edges (globs). */
|
|
839
|
+
paths: z.array(z.string().min(1)).optional()
|
|
840
|
+
}).strict().optional(),
|
|
841
|
+
/** Louvain resolution for the file graph. Default: 1.0. */
|
|
842
|
+
resolution: z.number().positive().optional(),
|
|
843
|
+
/** Candidates with fewer files fold into a neighbour. Default: 3. */
|
|
844
|
+
minCandidateFiles: z.number().int().min(1).optional(),
|
|
845
|
+
/** Words dropped from candidate terms; replaces the default list. */
|
|
846
|
+
technicalTerms: z.array(z.string().min(1)).optional()
|
|
847
|
+
}).strict();
|
|
848
|
+
var CtxoConfigSchema = z.object({
|
|
849
|
+
version: z.union([z.string(), z.number()]).optional(),
|
|
850
|
+
stats: StatsConfigSchema.optional(),
|
|
851
|
+
index: IndexConfigSchema.optional(),
|
|
852
|
+
masking: MaskingConfigSchema.optional(),
|
|
853
|
+
watch: WatchConfigSchema.optional(),
|
|
854
|
+
gate: GateConfigSchema.optional(),
|
|
855
|
+
cochange: CoChangeConfigSchema.optional(),
|
|
856
|
+
communities: CommunitiesConfigSchema.optional(),
|
|
857
|
+
contextsView: ContextsViewConfigSchema.optional()
|
|
858
|
+
}).passthrough();
|
|
859
|
+
var DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES = 5;
|
|
860
|
+
var DEFAULT_COCHANGE_MAX_COMMITS = 2e3;
|
|
861
|
+
var DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT = 30;
|
|
862
|
+
var DEFAULT_COCHANGE_MIN_SHARED = 2;
|
|
863
|
+
var DEFAULT_TECHNICAL_TERMS = [
|
|
864
|
+
"Service",
|
|
865
|
+
"Repository",
|
|
866
|
+
"Controller",
|
|
867
|
+
"Handler",
|
|
868
|
+
"Manager",
|
|
869
|
+
"Dto",
|
|
870
|
+
"Impl",
|
|
871
|
+
"Async"
|
|
872
|
+
];
|
|
873
|
+
var DEFAULT_CONFIG = {
|
|
874
|
+
version: "1.0",
|
|
875
|
+
stats: { enabled: true },
|
|
876
|
+
index: { ignore: [], ignoreProjects: [] },
|
|
877
|
+
masking: { clusterLabels: [] },
|
|
878
|
+
watch: { snapshotMinFileChanges: DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES },
|
|
879
|
+
gate: { enabled: true, sensitivity: "balanced" },
|
|
880
|
+
cochange: {
|
|
881
|
+
maxCommits: DEFAULT_COCHANGE_MAX_COMMITS,
|
|
882
|
+
maxFilesPerCommit: DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT,
|
|
883
|
+
minShared: DEFAULT_COCHANGE_MIN_SHARED,
|
|
884
|
+
include: [],
|
|
885
|
+
exclude: []
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/core/config/load-config.ts
|
|
890
|
+
var log4 = createLogger("ctxo:config");
|
|
891
|
+
function isValidGlob(pattern) {
|
|
892
|
+
try {
|
|
893
|
+
picomatch(pattern);
|
|
894
|
+
return true;
|
|
895
|
+
} catch {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function validateGlobs(patterns, field, invalid) {
|
|
900
|
+
if (!patterns) return;
|
|
901
|
+
for (const p of patterns) {
|
|
902
|
+
if (!isValidGlob(p)) invalid.push(`${field}: ${p}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function loadConfig(ctxoRoot) {
|
|
906
|
+
const path = join4(ctxoRoot, "config.yaml");
|
|
907
|
+
const errors = [];
|
|
908
|
+
const invalidGlobs = [];
|
|
909
|
+
if (!existsSync3(path)) {
|
|
910
|
+
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: false };
|
|
911
|
+
}
|
|
912
|
+
let raw;
|
|
913
|
+
try {
|
|
914
|
+
raw = readFileSync4(path, "utf-8");
|
|
915
|
+
} catch (err) {
|
|
916
|
+
errors.push(`Cannot read ${path}: ${err.message}`);
|
|
917
|
+
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
918
|
+
}
|
|
919
|
+
let parsed;
|
|
920
|
+
try {
|
|
921
|
+
parsed = parseYaml(raw);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
errors.push(`Invalid YAML in ${path}: ${err.message}`);
|
|
924
|
+
log4.warn(`Invalid YAML in config.yaml, falling back to defaults: ${err.message}`);
|
|
925
|
+
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
926
|
+
}
|
|
927
|
+
if (parsed === null || parsed === void 0) {
|
|
928
|
+
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
929
|
+
}
|
|
930
|
+
const result = CtxoConfigSchema.safeParse(parsed);
|
|
931
|
+
if (!result.success) {
|
|
932
|
+
for (const issue of result.error.issues) {
|
|
933
|
+
errors.push(`${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
934
|
+
}
|
|
935
|
+
log4.warn(`Invalid config.yaml (${errors.length} issue(s)), falling back to defaults`);
|
|
936
|
+
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
937
|
+
}
|
|
938
|
+
validateGlobs(result.data.index?.ignore, "index.ignore", invalidGlobs);
|
|
939
|
+
validateGlobs(result.data.index?.ignoreProjects, "index.ignoreProjects", invalidGlobs);
|
|
940
|
+
validateGlobs(result.data.index?.tests, "index.tests", invalidGlobs);
|
|
941
|
+
validateGlobs(result.data.cochange?.include, "cochange.include", invalidGlobs);
|
|
942
|
+
validateGlobs(result.data.cochange?.exclude, "cochange.exclude", invalidGlobs);
|
|
943
|
+
validateGlobs(result.data.contextsView?.exclude, "contextsView.exclude", invalidGlobs);
|
|
944
|
+
validateGlobs(result.data.contextsView?.hubs?.paths, "contextsView.hubs.paths", invalidGlobs);
|
|
945
|
+
for (const pattern of invalidGlobs) {
|
|
946
|
+
log4.warn(`Invalid glob pattern in config.yaml \u2014 ${pattern}`);
|
|
947
|
+
}
|
|
948
|
+
return { config: result.data, errors, invalidGlobs, path, exists: true };
|
|
949
|
+
}
|
|
950
|
+
function statsEnabled(config) {
|
|
951
|
+
return config.stats?.enabled !== false;
|
|
952
|
+
}
|
|
953
|
+
function indexIgnorePatterns(config) {
|
|
954
|
+
return (config.index?.ignore ?? []).filter(isValidGlob);
|
|
955
|
+
}
|
|
956
|
+
function indexIgnoreProjectPatterns(config) {
|
|
957
|
+
return (config.index?.ignoreProjects ?? []).filter(isValidGlob);
|
|
958
|
+
}
|
|
959
|
+
function factsRequested(config) {
|
|
960
|
+
return config.index?.facts === true;
|
|
961
|
+
}
|
|
962
|
+
function indexTestPatterns(config) {
|
|
963
|
+
return (config.index?.tests ?? []).filter(isValidGlob);
|
|
964
|
+
}
|
|
965
|
+
function communityDetectorOptions(config) {
|
|
966
|
+
const section = config.communities;
|
|
967
|
+
const resolution = section?.resolution;
|
|
968
|
+
const minBridged = section?.godNodeMinBridged;
|
|
969
|
+
const maxResults = section?.godNodeMaxResults;
|
|
970
|
+
return {
|
|
971
|
+
...typeof resolution === "number" && Number.isFinite(resolution) && resolution > 0 ? { resolution } : {},
|
|
972
|
+
...typeof minBridged === "number" && Number.isInteger(minBridged) && minBridged >= 1 ? { godNodeMinBridged: minBridged } : {},
|
|
973
|
+
...typeof maxResults === "number" && Number.isInteger(maxResults) && maxResults >= 0 ? { godNodeMaxResults: maxResults } : {}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function maskingClusterLabelPatterns(config) {
|
|
977
|
+
return (config.masking?.clusterLabels ?? []).filter(isValidGlob);
|
|
978
|
+
}
|
|
979
|
+
function watchSnapshotMinFileChanges(config) {
|
|
980
|
+
const value = config.watch?.snapshotMinFileChanges;
|
|
981
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 1) return value;
|
|
982
|
+
return DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES;
|
|
983
|
+
}
|
|
984
|
+
function isPositiveInteger(value) {
|
|
985
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
|
986
|
+
}
|
|
987
|
+
function positiveIntegerOr(value, fallback) {
|
|
988
|
+
return isPositiveInteger(value) ? value : fallback;
|
|
989
|
+
}
|
|
990
|
+
function validMaxCommits(value) {
|
|
991
|
+
if (value === "all") return "all";
|
|
992
|
+
return isPositiveInteger(value) ? value : void 0;
|
|
993
|
+
}
|
|
994
|
+
function validSince(value) {
|
|
995
|
+
return typeof value === "string" && parseCoChangeSince(value) !== void 0 ? value : void 0;
|
|
996
|
+
}
|
|
997
|
+
function resolveCoChangeConfig(config, overrides = {}) {
|
|
998
|
+
const section = config.cochange;
|
|
999
|
+
const since = validSince(overrides.since) ?? validSince(section?.since);
|
|
1000
|
+
return {
|
|
1001
|
+
maxCommits: validMaxCommits(overrides.maxCommits) ?? validMaxCommits(section?.maxCommits) ?? DEFAULT_COCHANGE_MAX_COMMITS,
|
|
1002
|
+
...since !== void 0 ? { since } : {},
|
|
1003
|
+
maxFilesPerCommit: positiveIntegerOr(section?.maxFilesPerCommit, DEFAULT_COCHANGE_MAX_FILES_PER_COMMIT),
|
|
1004
|
+
minShared: positiveIntegerOr(section?.minShared, DEFAULT_COCHANGE_MIN_SHARED),
|
|
1005
|
+
include: (section?.include ?? []).filter(isValidGlob),
|
|
1006
|
+
exclude: (section?.exclude ?? []).filter(isValidGlob)
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function nonNegativeOr(value, fallback) {
|
|
1010
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1011
|
+
}
|
|
1012
|
+
function positiveOr(value, fallback) {
|
|
1013
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1014
|
+
}
|
|
1015
|
+
function resolveContextsViewConfig(config) {
|
|
1016
|
+
const section = config.contextsView;
|
|
1017
|
+
const hubs = section?.hubs;
|
|
1018
|
+
const topPercent = hubs?.topPercent;
|
|
1019
|
+
return {
|
|
1020
|
+
weights: {
|
|
1021
|
+
dependency: nonNegativeOr(section?.weights?.dependency, 1),
|
|
1022
|
+
coChange: nonNegativeOr(section?.weights?.coChange, 1),
|
|
1023
|
+
data: nonNegativeOr(section?.weights?.data, 0)
|
|
1024
|
+
},
|
|
1025
|
+
exclude: (section?.exclude ?? []).filter(isValidGlob),
|
|
1026
|
+
hubs: {
|
|
1027
|
+
auto: hubs?.auto !== false,
|
|
1028
|
+
topPercent: typeof topPercent === "number" && topPercent > 0 && topPercent <= 100 ? topPercent : 1,
|
|
1029
|
+
minDependents: positiveIntegerOr(hubs?.minDependents, 10),
|
|
1030
|
+
paths: (hubs?.paths ?? []).filter(isValidGlob)
|
|
1031
|
+
},
|
|
1032
|
+
resolution: positiveOr(section?.resolution, 1),
|
|
1033
|
+
minCandidateFiles: positiveIntegerOr(section?.minCandidateFiles, 3),
|
|
1034
|
+
technicalTerms: section?.technicalTerms ? [...section.technicalTerms] : [...DEFAULT_TECHNICAL_TERMS]
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function makeGlobMatcher(patterns) {
|
|
1038
|
+
if (patterns.length === 0) return () => false;
|
|
1039
|
+
const matchers = patterns.map((p) => picomatch(p, { dot: true }));
|
|
1040
|
+
return (input) => {
|
|
1041
|
+
const normalised = input.replace(/\\/g, "/");
|
|
1042
|
+
return matchers.some((m) => m(normalised));
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
477
1046
|
// src/adapters/git/simple-git-adapter.ts
|
|
1047
|
+
import { spawnSync } from "child_process";
|
|
478
1048
|
import { simpleGit } from "simple-git";
|
|
479
|
-
var
|
|
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
|
+
}
|
|
480
1117
|
var SimpleGitAdapter = class {
|
|
481
1118
|
git;
|
|
1119
|
+
projectRoot;
|
|
482
1120
|
constructor(projectRoot) {
|
|
1121
|
+
this.projectRoot = projectRoot;
|
|
483
1122
|
this.git = simpleGit(projectRoot);
|
|
484
|
-
|
|
1123
|
+
log5.debug("Initialized with root: %s", projectRoot);
|
|
485
1124
|
}
|
|
486
1125
|
async isAvailable() {
|
|
487
1126
|
try {
|
|
@@ -493,97 +1132,311 @@ var SimpleGitAdapter = class {
|
|
|
493
1132
|
}
|
|
494
1133
|
async getCommitHistory(filePath, maxCount) {
|
|
495
1134
|
try {
|
|
496
|
-
const
|
|
1135
|
+
const log8 = await this.git.log({
|
|
497
1136
|
file: filePath,
|
|
498
1137
|
"--follow": null,
|
|
499
1138
|
...maxCount ? { maxCount } : {}
|
|
500
1139
|
});
|
|
501
|
-
return
|
|
1140
|
+
return log8.all.map((entry) => ({
|
|
502
1141
|
hash: entry.hash,
|
|
503
1142
|
message: entry.message,
|
|
504
1143
|
date: entry.date,
|
|
505
1144
|
author: entry.author_name
|
|
506
1145
|
}));
|
|
507
1146
|
} catch (err) {
|
|
508
|
-
|
|
1147
|
+
log5.error(` Failed to get history for ${filePath}: ${err.message}`);
|
|
509
1148
|
return [];
|
|
510
1149
|
}
|
|
511
1150
|
}
|
|
512
|
-
async
|
|
1151
|
+
async getChangedFiles(since) {
|
|
513
1152
|
try {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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)}`);
|
|
535
1199
|
}
|
|
536
|
-
|
|
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)) };
|
|
537
1204
|
} catch (err) {
|
|
538
|
-
|
|
539
|
-
return
|
|
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;
|
|
540
1215
|
}
|
|
541
1216
|
}
|
|
542
|
-
async
|
|
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
|
+
}
|
|
543
1240
|
try {
|
|
544
|
-
const
|
|
545
|
-
|
|
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));
|
|
546
1253
|
} catch (err) {
|
|
547
|
-
|
|
548
|
-
return
|
|
1254
|
+
log5.error(`Failed to read file changes of ${rev}: ${err.message}`);
|
|
1255
|
+
return void 0;
|
|
549
1256
|
}
|
|
550
1257
|
}
|
|
551
|
-
|
|
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;
|
|
552
1380
|
try {
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
filePath,
|
|
556
|
-
commitCount: log6.total
|
|
557
|
-
};
|
|
1381
|
+
stats = statSync2(this.filePath);
|
|
558
1382
|
} catch (err) {
|
|
559
|
-
|
|
560
|
-
|
|
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;
|
|
561
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);
|
|
562
1395
|
}
|
|
563
|
-
|
|
1396
|
+
load(version) {
|
|
1397
|
+
let raw;
|
|
564
1398
|
try {
|
|
565
|
-
|
|
566
|
-
const sha = raw.trim();
|
|
567
|
-
return sha.length > 0 ? sha : void 0;
|
|
1399
|
+
raw = readFileSync5(this.filePath, "utf-8");
|
|
568
1400
|
} catch (err) {
|
|
569
|
-
|
|
1401
|
+
this.cached = void 0;
|
|
1402
|
+
this.warnOnce(`read ${version}`, `cannot be read (${err.message})`, UNTIL_RETRY2);
|
|
570
1403
|
return void 0;
|
|
571
1404
|
}
|
|
1405
|
+
const view = this.parse(raw, version);
|
|
1406
|
+
this.cached = { version, view };
|
|
1407
|
+
if (view) this.lastWarning = void 0;
|
|
1408
|
+
return view;
|
|
572
1409
|
}
|
|
573
|
-
|
|
1410
|
+
parse(raw, version) {
|
|
1411
|
+
let data;
|
|
574
1412
|
try {
|
|
575
|
-
|
|
576
|
-
const n = parseInt(raw.trim(), 10);
|
|
577
|
-
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
1413
|
+
data = JSON.parse(raw);
|
|
578
1414
|
} catch (err) {
|
|
579
|
-
|
|
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
|
+
);
|
|
580
1427
|
return void 0;
|
|
581
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}`);
|
|
582
1435
|
}
|
|
583
1436
|
};
|
|
584
1437
|
|
|
585
1438
|
// src/adapters/stats/session-recorder-adapter.ts
|
|
586
|
-
var
|
|
1439
|
+
var log7 = createLogger("ctxo:stats");
|
|
587
1440
|
var SessionRecorderAdapter = class {
|
|
588
1441
|
db;
|
|
589
1442
|
onWrite;
|
|
@@ -629,7 +1482,7 @@ var SessionRecorderAdapter = class {
|
|
|
629
1482
|
);
|
|
630
1483
|
this.flushToDisk();
|
|
631
1484
|
} catch (err) {
|
|
632
|
-
|
|
1485
|
+
log7.error(`Failed to record session event: ${err.message}`);
|
|
633
1486
|
}
|
|
634
1487
|
}
|
|
635
1488
|
queryStats(days) {
|
|
@@ -697,14 +1550,14 @@ var SessionRecorderAdapter = class {
|
|
|
697
1550
|
try {
|
|
698
1551
|
this.onWrite?.();
|
|
699
1552
|
} catch (err) {
|
|
700
|
-
|
|
1553
|
+
log7.error(`Failed to flush stats to disk: ${err.message}`);
|
|
701
1554
|
}
|
|
702
1555
|
}
|
|
703
1556
|
};
|
|
704
1557
|
|
|
705
1558
|
// src/adapters/workspace/single-package-workspace.ts
|
|
706
|
-
import { existsSync as
|
|
707
|
-
import { join as
|
|
1559
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
1560
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
708
1561
|
var MANIFEST_CANDIDATES = [
|
|
709
1562
|
"package.json",
|
|
710
1563
|
"pyproject.toml",
|
|
@@ -717,17 +1570,17 @@ var MANIFEST_CANDIDATES = [
|
|
|
717
1570
|
];
|
|
718
1571
|
function findManifest(dir) {
|
|
719
1572
|
for (const name of MANIFEST_CANDIDATES) {
|
|
720
|
-
const candidate =
|
|
721
|
-
if (
|
|
1573
|
+
const candidate = join6(dir, name);
|
|
1574
|
+
if (existsSync4(candidate)) return candidate;
|
|
722
1575
|
}
|
|
723
1576
|
const csproj = findFirstByExt(dir, ".csproj") ?? findFirstByExt(dir, ".sln");
|
|
724
1577
|
if (csproj) return csproj;
|
|
725
|
-
return
|
|
1578
|
+
return join6(dir, "package.json");
|
|
726
1579
|
}
|
|
727
1580
|
function findFirstByExt(dir, ext) {
|
|
728
1581
|
try {
|
|
729
1582
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
730
|
-
if (entry.isFile() && entry.name.endsWith(ext)) return
|
|
1583
|
+
if (entry.isFile() && entry.name.endsWith(ext)) return join6(dir, entry.name);
|
|
731
1584
|
}
|
|
732
1585
|
} catch {
|
|
733
1586
|
}
|
|
@@ -745,154 +1598,90 @@ function detectWorkspace(cwd) {
|
|
|
745
1598
|
};
|
|
746
1599
|
}
|
|
747
1600
|
|
|
748
|
-
// src/core/
|
|
749
|
-
import {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
}).strict();
|
|
763
|
-
var MaskingConfigSchema = z.object({
|
|
764
|
-
/**
|
|
765
|
-
* Glob patterns matched against community cluster labels. Matching labels
|
|
766
|
-
* are replaced with `[masked-cluster-N]` in MCP tool responses so
|
|
767
|
-
* org-sensitive folder names (e.g. `src/payment-vault`, `internal-*`)
|
|
768
|
-
* never surface to AI agents over the stdio channel.
|
|
769
|
-
*/
|
|
770
|
-
clusterLabels: z.array(z.string().min(1)).optional()
|
|
771
|
-
}).strict();
|
|
772
|
-
var WatchConfigSchema = z.object({
|
|
773
|
-
/**
|
|
774
|
-
* Minimum number of files that must be re-indexed since the last
|
|
775
|
-
* community snapshot before `ctxo watch` recomputes one. Default: 5.
|
|
776
|
-
* On large monorepos Louvain + PageRank cost ~1 s of blocking CPU;
|
|
777
|
-
* bumping this (e.g. 25) avoids recomputing on every save.
|
|
778
|
-
*/
|
|
779
|
-
snapshotMinFileChanges: z.number().int().min(1).optional()
|
|
780
|
-
}).strict();
|
|
781
|
-
var GateConfigSchema = z.object({
|
|
782
|
-
enabled: z.boolean().optional(),
|
|
783
|
-
sensitivity: z.enum(["strict", "balanced", "lenient"]).optional(),
|
|
784
|
-
/** Absolute floor: minimum confirmed+likely dependents before the gate can fire. */
|
|
785
|
-
minDependents: z.number().int().min(0).optional(),
|
|
786
|
-
/** Repo-relative significance: symbol must be in the top `percentile`% by PageRank. */
|
|
787
|
-
percentile: z.number().int().min(1).max(100).optional()
|
|
788
|
-
}).strict();
|
|
789
|
-
var CtxoConfigSchema = z.object({
|
|
790
|
-
version: z.union([z.string(), z.number()]).optional(),
|
|
791
|
-
stats: StatsConfigSchema.optional(),
|
|
792
|
-
index: IndexConfigSchema.optional(),
|
|
793
|
-
masking: MaskingConfigSchema.optional(),
|
|
794
|
-
watch: WatchConfigSchema.optional(),
|
|
795
|
-
gate: GateConfigSchema.optional()
|
|
796
|
-
}).passthrough();
|
|
797
|
-
var DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES = 5;
|
|
798
|
-
var DEFAULT_CONFIG = {
|
|
799
|
-
version: "1.0",
|
|
800
|
-
stats: { enabled: true },
|
|
801
|
-
index: { ignore: [], ignoreProjects: [] },
|
|
802
|
-
masking: { clusterLabels: [] },
|
|
803
|
-
watch: { snapshotMinFileChanges: DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES },
|
|
804
|
-
gate: { enabled: true, sensitivity: "balanced" }
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
// src/core/config/load-config.ts
|
|
808
|
-
var log5 = createLogger("ctxo:config");
|
|
809
|
-
function isValidGlob(pattern) {
|
|
810
|
-
try {
|
|
811
|
-
picomatch(pattern);
|
|
812
|
-
return true;
|
|
813
|
-
} catch {
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
function validateGlobs(patterns, field, invalid) {
|
|
818
|
-
if (!patterns) return;
|
|
819
|
-
for (const p of patterns) {
|
|
820
|
-
if (!isValidGlob(p)) invalid.push(`${field}: ${p}`);
|
|
821
|
-
}
|
|
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);
|
|
822
1607
|
}
|
|
823
|
-
function
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (!existsSync4(path)) {
|
|
828
|
-
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: false };
|
|
829
|
-
}
|
|
830
|
-
let raw;
|
|
831
|
-
try {
|
|
832
|
-
raw = readFileSync3(path, "utf-8");
|
|
833
|
-
} catch (err) {
|
|
834
|
-
errors.push(`Cannot read ${path}: ${err.message}`);
|
|
835
|
-
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
836
|
-
}
|
|
837
|
-
let parsed;
|
|
838
|
-
try {
|
|
839
|
-
parsed = parseYaml(raw);
|
|
840
|
-
} catch (err) {
|
|
841
|
-
errors.push(`Invalid YAML in ${path}: ${err.message}`);
|
|
842
|
-
log5.warn(`Invalid YAML in config.yaml, falling back to defaults: ${err.message}`);
|
|
843
|
-
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
844
|
-
}
|
|
845
|
-
if (parsed === null || parsed === void 0) {
|
|
846
|
-
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
847
|
-
}
|
|
848
|
-
const result = CtxoConfigSchema.safeParse(parsed);
|
|
849
|
-
if (!result.success) {
|
|
850
|
-
for (const issue of result.error.issues) {
|
|
851
|
-
errors.push(`${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
852
|
-
}
|
|
853
|
-
log5.warn(`Invalid config.yaml (${errors.length} issue(s)), falling back to defaults`);
|
|
854
|
-
return { config: DEFAULT_CONFIG, errors, invalidGlobs, path, exists: true };
|
|
855
|
-
}
|
|
856
|
-
validateGlobs(result.data.index?.ignore, "index.ignore", invalidGlobs);
|
|
857
|
-
validateGlobs(result.data.index?.ignoreProjects, "index.ignoreProjects", invalidGlobs);
|
|
858
|
-
for (const pattern of invalidGlobs) {
|
|
859
|
-
log5.warn(`Invalid glob pattern in config.yaml \u2014 ${pattern}`);
|
|
860
|
-
}
|
|
861
|
-
return { config: result.data, errors, invalidGlobs, path, exists: true };
|
|
1608
|
+
function dataReferenceKinds(kind) {
|
|
1609
|
+
if (isDataResourceKind(kind)) return DATA_RESOURCE_KINDS2;
|
|
1610
|
+
if (kind === "procedure") return PROCEDURE_KINDS;
|
|
1611
|
+
return void 0;
|
|
862
1612
|
}
|
|
863
|
-
|
|
864
|
-
|
|
1613
|
+
|
|
1614
|
+
// src/core/test-files/test-file-classifier.ts
|
|
1615
|
+
var DEFAULT_TEST_GLOBS = [
|
|
1616
|
+
"**/__tests__/**",
|
|
1617
|
+
"**/*.test.*",
|
|
1618
|
+
"**/*.spec.*",
|
|
1619
|
+
"**/test/**",
|
|
1620
|
+
"**/tests/**",
|
|
1621
|
+
"**/*_test.go",
|
|
1622
|
+
"**/src/test/**",
|
|
1623
|
+
"**/*.Tests/**",
|
|
1624
|
+
"**/*.Test/**",
|
|
1625
|
+
"**/*Tests.cs",
|
|
1626
|
+
"**/*Test.cs",
|
|
1627
|
+
"**/test_*.py",
|
|
1628
|
+
"**/*_test.py"
|
|
1629
|
+
];
|
|
1630
|
+
var NAME_ONLY_TEST_GLOBS = ["**/*Tests.cs", "**/*Test.cs"];
|
|
1631
|
+
var TEST_FRAMEWORK_ANNOTATIONS = [
|
|
1632
|
+
"Fact",
|
|
1633
|
+
"Theory",
|
|
1634
|
+
"SkippableFact",
|
|
1635
|
+
"SkippableTheory",
|
|
1636
|
+
"Test",
|
|
1637
|
+
"TestCase",
|
|
1638
|
+
"TestCaseSource",
|
|
1639
|
+
"TestFixture",
|
|
1640
|
+
"TestMethod",
|
|
1641
|
+
"DataTestMethod",
|
|
1642
|
+
"TestClass"
|
|
1643
|
+
];
|
|
1644
|
+
function normalisePath(path) {
|
|
1645
|
+
return path.replace(/\\/g, "/");
|
|
865
1646
|
}
|
|
866
|
-
function
|
|
867
|
-
|
|
1647
|
+
function filesWithTestAnnotations(indices) {
|
|
1648
|
+
const names = new Set(TEST_FRAMEWORK_ANNOTATIONS);
|
|
1649
|
+
const files = /* @__PURE__ */ new Set();
|
|
1650
|
+
for (const index of indices) {
|
|
1651
|
+
const marked = index.symbols.some((symbol) => symbol.annotations?.some((annotation) => names.has(annotation.name)));
|
|
1652
|
+
if (marked) files.add(normalisePath(index.file));
|
|
1653
|
+
}
|
|
1654
|
+
return files;
|
|
868
1655
|
}
|
|
869
|
-
function
|
|
870
|
-
|
|
1656
|
+
function createTestFileClassifier(extraGlobs = [], annotatedFiles, options = {}) {
|
|
1657
|
+
const nameOnly = new Set(NAME_ONLY_TEST_GLOBS);
|
|
1658
|
+
const defaults = options.annotationAware === true ? DEFAULT_TEST_GLOBS.filter((glob) => !nameOnly.has(glob)) : DEFAULT_TEST_GLOBS;
|
|
1659
|
+
const matchesGlob = makeGlobMatcher([...defaults, ...extraGlobs]);
|
|
1660
|
+
return (path) => annotatedFiles?.has(normalisePath(path)) === true || matchesGlob(path);
|
|
871
1661
|
}
|
|
872
|
-
|
|
873
|
-
|
|
1662
|
+
|
|
1663
|
+
// src/core/graph/symbol-graph.ts
|
|
1664
|
+
var isDefaultTestFile = createTestFileClassifier();
|
|
1665
|
+
var DOCUMENTATION_FOLDERS = /* @__PURE__ */ new Set(["docs", "doc", "documentation"]);
|
|
1666
|
+
function asciiLower(text) {
|
|
1667
|
+
return text.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
874
1668
|
}
|
|
875
|
-
function
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
return DEFAULT_WATCH_SNAPSHOT_MIN_FILE_CHANGES;
|
|
1669
|
+
function isUnderDocumentationFolder(file) {
|
|
1670
|
+
const folders = file.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
1671
|
+
return folders.some((folder) => DOCUMENTATION_FOLDERS.has(asciiLower(folder)));
|
|
879
1672
|
}
|
|
880
|
-
function
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
return (
|
|
884
|
-
const normalised = input.replace(/\\/g, "/");
|
|
885
|
-
return matchers.some((m) => m(normalised));
|
|
886
|
-
};
|
|
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);
|
|
887
1677
|
}
|
|
888
|
-
|
|
889
|
-
// src/core/graph/symbol-graph.ts
|
|
890
1678
|
var SymbolGraph = class {
|
|
891
1679
|
nodes = /* @__PURE__ */ new Map();
|
|
892
1680
|
nodesByFileAndName = /* @__PURE__ */ new Map();
|
|
893
1681
|
nodesByNameAndKind = /* @__PURE__ */ new Map();
|
|
894
1682
|
forwardEdges = /* @__PURE__ */ new Map();
|
|
895
1683
|
reverseEdges = /* @__PURE__ */ new Map();
|
|
1684
|
+
definitionRanks = /* @__PURE__ */ new Map();
|
|
896
1685
|
edgeSet = /* @__PURE__ */ new Set();
|
|
897
1686
|
addNode(node) {
|
|
898
1687
|
this.nodes.set(node.symbolId, node);
|
|
@@ -940,6 +1729,8 @@ var SymbolGraph = class {
|
|
|
940
1729
|
if (altMatch) return altMatch.symbolId;
|
|
941
1730
|
}
|
|
942
1731
|
}
|
|
1732
|
+
const dataMatch = this.resolveDataReference(segs);
|
|
1733
|
+
if (dataMatch) return dataMatch.symbolId;
|
|
943
1734
|
const nameKind = this.extractNameKind(segs);
|
|
944
1735
|
if (nameKind) {
|
|
945
1736
|
const candidates = this.nodesByNameAndKind.get(nameKind);
|
|
@@ -959,6 +1750,8 @@ var SymbolGraph = class {
|
|
|
959
1750
|
if (tsMatch) return tsMatch;
|
|
960
1751
|
}
|
|
961
1752
|
}
|
|
1753
|
+
const dataMatch = this.resolveDataReference(segs);
|
|
1754
|
+
if (dataMatch) return dataMatch;
|
|
962
1755
|
const nameKind = this.extractNameKind(segs);
|
|
963
1756
|
if (nameKind) {
|
|
964
1757
|
const candidates = this.nodesByNameAndKind.get(nameKind);
|
|
@@ -966,6 +1759,63 @@ var SymbolGraph = class {
|
|
|
966
1759
|
}
|
|
967
1760
|
return void 0;
|
|
968
1761
|
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Resolves a 2-part data reference (`dbo.loans::table`). A table or view reference is kind-agnostic
|
|
1764
|
+
* across data resource kinds, a procedure reference matches procedures only. Several definitions
|
|
1765
|
+
* resolve by `definitionRank` (a production copy before a documentation copy before a test copy), then
|
|
1766
|
+
* to the codepoint-smallest symbolId, which is the first file in sorted path order. The result does
|
|
1767
|
+
* not depend on insertion order.
|
|
1768
|
+
*/
|
|
1769
|
+
resolveDataReference(segs) {
|
|
1770
|
+
if (segs.length !== 2) return void 0;
|
|
1771
|
+
const [name, kind] = segs;
|
|
1772
|
+
const kinds = dataReferenceKinds(kind);
|
|
1773
|
+
if (!kinds) return void 0;
|
|
1774
|
+
let best;
|
|
1775
|
+
for (const k of kinds) {
|
|
1776
|
+
for (const node of this.nodesByNameAndKind.get(`${name}::${k}`) ?? []) {
|
|
1777
|
+
if (!best || this.compareDefinitions(node, best) < 0) best = node;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return best;
|
|
1781
|
+
}
|
|
1782
|
+
/** The tie-break order of same-name definitions: `definitionRank`, then the codepoint-smaller symbolId. */
|
|
1783
|
+
compareDefinitions = (a, b) => this.definitionRankOf(a.symbolId) - this.definitionRankOf(b.symbolId) || compareCodepoint(a.symbolId, b.symbolId);
|
|
1784
|
+
/**
|
|
1785
|
+
* The identity of the table, view or procedure that `id` names: a full id of any of its definitions, or a
|
|
1786
|
+
* 2-part reference such as `dbo.loans::table`. Undefined when `id` names no node, or a node of another
|
|
1787
|
+
* kind. A table or view identity spans both kinds, as a 2-part `::table` reference does.
|
|
1788
|
+
*/
|
|
1789
|
+
dataResourceIdentity(id) {
|
|
1790
|
+
const node = this.getNode(id);
|
|
1791
|
+
return node ? this.identityOf(node) : void 0;
|
|
1792
|
+
}
|
|
1793
|
+
identityOf(node) {
|
|
1794
|
+
const segs = node.symbolId.split("::");
|
|
1795
|
+
if (segs.length !== 3) return void 0;
|
|
1796
|
+
const [, name, kind] = segs;
|
|
1797
|
+
const kinds = dataReferenceKinds(kind);
|
|
1798
|
+
if (!kinds) return void 0;
|
|
1799
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1800
|
+
for (const k of kinds) {
|
|
1801
|
+
for (const definition of this.nodesByNameAndKind.get(`${name}::${k}`) ?? []) {
|
|
1802
|
+
byId.set(definition.symbolId, this.nodes.get(definition.symbolId) ?? definition);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const definitions = [...byId.values()].sort(this.compareDefinitions);
|
|
1806
|
+
const winner = definitions[0] ?? node;
|
|
1807
|
+
const production = definitions.filter((definition) => this.definitionRankOf(definition.symbolId) === 0);
|
|
1808
|
+
return { canonicalId: `${name}::${winner.kind}`, winner, definitions, production };
|
|
1809
|
+
}
|
|
1810
|
+
/** `definitionRank`, cached per symbolId (the glob match runs once per definition). */
|
|
1811
|
+
definitionRankOf(symbolId) {
|
|
1812
|
+
let rank = this.definitionRanks.get(symbolId);
|
|
1813
|
+
if (rank === void 0) {
|
|
1814
|
+
rank = definitionRank(symbolId);
|
|
1815
|
+
this.definitionRanks.set(symbolId, rank);
|
|
1816
|
+
}
|
|
1817
|
+
return rank;
|
|
1818
|
+
}
|
|
969
1819
|
/**
|
|
970
1820
|
* Extracts a `name::kind` key from the segments of a reference ID, supporting:
|
|
971
1821
|
* - 2-part: `Bar::class` → `Bar::class`
|
|
@@ -983,6 +1833,18 @@ var SymbolGraph = class {
|
|
|
983
1833
|
getReverseEdges(symbolId) {
|
|
984
1834
|
return this.reverseEdges.get(symbolId) ?? [];
|
|
985
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
|
+
}
|
|
986
1848
|
hasNode(symbolId) {
|
|
987
1849
|
return this.nodes.has(symbolId) || this.resolveNodeFuzzy(symbolId) !== void 0;
|
|
988
1850
|
}
|
|
@@ -1043,6 +1905,21 @@ var RevertDetector = class {
|
|
|
1043
1905
|
}
|
|
1044
1906
|
};
|
|
1045
1907
|
|
|
1908
|
+
// src/core/overlay/snapshot-history.ts
|
|
1909
|
+
function priorCommitSnapshots(history, currentSha) {
|
|
1910
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1911
|
+
const prior = [];
|
|
1912
|
+
for (const snapshot of history) {
|
|
1913
|
+
if (snapshot.commitSha === currentSha || seen.has(snapshot.commitSha)) continue;
|
|
1914
|
+
seen.add(snapshot.commitSha);
|
|
1915
|
+
prior.push(snapshot);
|
|
1916
|
+
}
|
|
1917
|
+
return prior;
|
|
1918
|
+
}
|
|
1919
|
+
function distinctCommitCount(history, currentSha) {
|
|
1920
|
+
return priorCommitSnapshots(history, currentSha).length;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1046
1923
|
// src/core/overlay/drift-detector.ts
|
|
1047
1924
|
var DEFAULT_JACCARD_THRESHOLD = 0.3;
|
|
1048
1925
|
var DriftDetector = class {
|
|
@@ -1054,15 +1931,18 @@ var DriftDetector = class {
|
|
|
1054
1931
|
}
|
|
1055
1932
|
detect(current, history) {
|
|
1056
1933
|
const snapshotsAvailable = history.length;
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1934
|
+
const distinctCommits = distinctCommitCount(history, current.commitSha);
|
|
1935
|
+
const confidence = confidenceFor(distinctCommits);
|
|
1936
|
+
const hint = hintFor(distinctCommits);
|
|
1937
|
+
const [previous, anchor] = priorCommitSnapshots(history, current.commitSha);
|
|
1059
1938
|
if (!previous) {
|
|
1060
1939
|
return {
|
|
1061
1940
|
events: [],
|
|
1062
|
-
confidence
|
|
1941
|
+
confidence,
|
|
1063
1942
|
snapshotsAvailable,
|
|
1943
|
+
distinctCommits,
|
|
1064
1944
|
stability: { stable: 0, transient: 0, unverified: 0 },
|
|
1065
|
-
hint
|
|
1945
|
+
hint
|
|
1066
1946
|
};
|
|
1067
1947
|
}
|
|
1068
1948
|
const currentByCommunityId = indexByCommunityId(current.communities);
|
|
@@ -1115,13 +1995,14 @@ var DriftDetector = class {
|
|
|
1115
1995
|
stability: classification
|
|
1116
1996
|
});
|
|
1117
1997
|
}
|
|
1118
|
-
const events = (this.suppressTransient ? rawEvents.filter((e) => e.stability !== "transient") : rawEvents).sort((a, b) => a.symbolId
|
|
1998
|
+
const events = (this.suppressTransient ? rawEvents.filter((e) => e.stability !== "transient") : rawEvents).sort((a, b) => compareCodepoint(a.symbolId, b.symbolId));
|
|
1119
1999
|
return {
|
|
1120
2000
|
events,
|
|
1121
|
-
confidence
|
|
2001
|
+
confidence,
|
|
1122
2002
|
snapshotsAvailable,
|
|
2003
|
+
distinctCommits,
|
|
1123
2004
|
stability,
|
|
1124
|
-
hint
|
|
2005
|
+
hint
|
|
1125
2006
|
};
|
|
1126
2007
|
}
|
|
1127
2008
|
mapCommunities(current, previous) {
|
|
@@ -1175,41 +2056,56 @@ function jaccard(a, b) {
|
|
|
1175
2056
|
const union = a.size + b.size - intersection;
|
|
1176
2057
|
return union === 0 ? 0 : intersection / union;
|
|
1177
2058
|
}
|
|
1178
|
-
function confidenceFor(
|
|
1179
|
-
if (
|
|
1180
|
-
if (
|
|
2059
|
+
function confidenceFor(distinctCommits) {
|
|
2060
|
+
if (distinctCommits >= 7) return "high";
|
|
2061
|
+
if (distinctCommits >= 3) return "medium";
|
|
1181
2062
|
return "low";
|
|
1182
2063
|
}
|
|
1183
|
-
function hintFor(
|
|
1184
|
-
if (
|
|
1185
|
-
return "Install post-commit hook or run `ctxo index`
|
|
2064
|
+
function hintFor(distinctCommits) {
|
|
2065
|
+
if (distinctCommits >= 3) return void 0;
|
|
2066
|
+
return "Install the post-commit hook (`ctxo init`) or run `ctxo index` after commits to build history; `ctxo index --check` does not record snapshots.";
|
|
1186
2067
|
}
|
|
1187
2068
|
|
|
1188
2069
|
// src/core/blast-radius/blast-radius-calculator.ts
|
|
1189
|
-
var
|
|
1190
|
-
|
|
2070
|
+
var EDGE_KIND_CONFIDENCE = {
|
|
2071
|
+
calls: "confirmed",
|
|
2072
|
+
extends: "confirmed",
|
|
2073
|
+
implements: "confirmed",
|
|
2074
|
+
reads: "confirmed",
|
|
2075
|
+
writes: "confirmed",
|
|
2076
|
+
executes: "confirmed",
|
|
2077
|
+
uses: "likely",
|
|
2078
|
+
imports: "potential"
|
|
2079
|
+
};
|
|
1191
2080
|
var CONFIDENCE_RANK = {
|
|
1192
2081
|
confirmed: 2,
|
|
1193
2082
|
likely: 1,
|
|
1194
2083
|
potential: 0
|
|
1195
2084
|
};
|
|
1196
2085
|
function edgeKindToConfidence(kind) {
|
|
1197
|
-
|
|
1198
|
-
if (LIKELY_KINDS.has(kind)) return "likely";
|
|
1199
|
-
return "potential";
|
|
2086
|
+
return EDGE_KINDS.includes(kind) ? EDGE_KIND_CONFIDENCE[kind] : "potential";
|
|
1200
2087
|
}
|
|
1201
2088
|
var BlastRadiusCalculator = class {
|
|
2089
|
+
/**
|
|
2090
|
+
* `symbolId` may be any id `SymbolGraph.getNode` resolves, a 2-part data id included. The root is the
|
|
2091
|
+
* node it names, and the root's own file is the one co-change is matched against. Dependents come from
|
|
2092
|
+
* `getDependentEdges`, so every copy of a table, view or procedure, and its 2-part id, has the dependents
|
|
2093
|
+
* of the resource, and no definition of the resource counts as a dependent of another.
|
|
2094
|
+
*/
|
|
1202
2095
|
calculate(graph, symbolId, coChangeMap) {
|
|
1203
|
-
|
|
2096
|
+
const root = graph.getNode(symbolId);
|
|
2097
|
+
if (!root) {
|
|
1204
2098
|
return { impactedSymbols: [], directDependentsCount: 0, confirmedCount: 0, likelyCount: 0, potentialCount: 0, overallRiskScore: 0 };
|
|
1205
2099
|
}
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
2100
|
+
const rootId = root.symbolId;
|
|
2101
|
+
const sourceFile = rootId.split("::")[0];
|
|
2102
|
+
const definitions = graph.dataResourceIdentity(rootId)?.definitions.map((definition) => definition.symbolId) ?? [];
|
|
2103
|
+
const visited = /* @__PURE__ */ new Set([rootId, ...definitions]);
|
|
1208
2104
|
const rawEntries = [];
|
|
1209
|
-
const queue = [{ id:
|
|
2105
|
+
const queue = [{ id: rootId, depth: 0 }];
|
|
1210
2106
|
while (queue.length > 0) {
|
|
1211
2107
|
const current = queue.shift();
|
|
1212
|
-
const reverseEdges = graph.
|
|
2108
|
+
const reverseEdges = graph.getDependentEdges(current.id);
|
|
1213
2109
|
const bestByNode = /* @__PURE__ */ new Map();
|
|
1214
2110
|
for (const edge of reverseEdges) {
|
|
1215
2111
|
if (visited.has(edge.from)) continue;
|
|
@@ -1234,7 +2130,7 @@ var BlastRadiusCalculator = class {
|
|
|
1234
2130
|
const nodeFile = nodeId.split("::")[0];
|
|
1235
2131
|
const coEntries = coChangeMap.get(sourceFile);
|
|
1236
2132
|
if (coEntries) {
|
|
1237
|
-
const match = coEntries.find((e) => e.file1 ===
|
|
2133
|
+
const match = coEntries.find((e) => (e.file1 === sourceFile ? e.file2 : e.file1) === nodeFile);
|
|
1238
2134
|
if (match) {
|
|
1239
2135
|
coChangeFrequency = match.frequency;
|
|
1240
2136
|
if (confidence === "potential" && match.frequency > 0.5) {
|
|
@@ -1254,11 +2150,10 @@ var BlastRadiusCalculator = class {
|
|
|
1254
2150
|
queue.push({ id: nodeId, depth });
|
|
1255
2151
|
}
|
|
1256
2152
|
}
|
|
1257
|
-
const blastSet = new Set(rawEntries.map((e) => e.symbolId));
|
|
1258
|
-
blastSet.add(symbolId);
|
|
2153
|
+
const blastSet = /* @__PURE__ */ new Set([rootId, ...definitions, ...rawEntries.map((e) => e.symbolId)]);
|
|
1259
2154
|
const entries = rawEntries.map((e) => ({
|
|
1260
2155
|
...e,
|
|
1261
|
-
dependentCount: graph.
|
|
2156
|
+
dependentCount: graph.getDependentEdges(e.symbolId).filter((re) => blastSet.has(re.from)).length
|
|
1262
2157
|
}));
|
|
1263
2158
|
entries.sort((a, b) => a.depth - b.depth);
|
|
1264
2159
|
const directDependentsCount = entries.filter((e) => e.depth === 1).length;
|
|
@@ -1271,6 +2166,36 @@ var BlastRadiusCalculator = class {
|
|
|
1271
2166
|
}
|
|
1272
2167
|
};
|
|
1273
2168
|
|
|
2169
|
+
// src/core/blast-radius/co-changed-files.ts
|
|
2170
|
+
var DEFAULT_LIMIT = 10;
|
|
2171
|
+
var DEFAULT_MIN_FREQUENCY = 0.5;
|
|
2172
|
+
function findCoChangedFiles(coChangeMap, rootFile, impactedFiles, options = {}) {
|
|
2173
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
2174
|
+
const minFrequency = options.minFrequency ?? DEFAULT_MIN_FREQUENCY;
|
|
2175
|
+
const strongestByFile = /* @__PURE__ */ new Map();
|
|
2176
|
+
for (const entry of coChangeMap.get(rootFile) ?? []) {
|
|
2177
|
+
const file = entry.file1 === rootFile ? entry.file2 : entry.file1;
|
|
2178
|
+
if (file === rootFile || impactedFiles.has(file) || entry.frequency < minFrequency) continue;
|
|
2179
|
+
const candidate = { file, frequency: entry.frequency, sharedCommits: entry.sharedCommits };
|
|
2180
|
+
const known = strongestByFile.get(file);
|
|
2181
|
+
if (!known || compareStrongestFirst(candidate, known) < 0) strongestByFile.set(file, candidate);
|
|
2182
|
+
}
|
|
2183
|
+
return [...strongestByFile.values()].sort(compareStrongestFirst).slice(0, Math.max(0, limit));
|
|
2184
|
+
}
|
|
2185
|
+
function compareStrongestFirst(a, b) {
|
|
2186
|
+
return b.frequency - a.frequency || b.sharedCommits - a.sharedCommits || compareCodepoint(a.file, b.file);
|
|
2187
|
+
}
|
|
2188
|
+
function findCoChangedFilesForBlastRadius(coChangeMap, rootSymbolId, impactedSymbols) {
|
|
2189
|
+
if (!coChangeMap) return [];
|
|
2190
|
+
const rootFile = fileOf(rootSymbolId);
|
|
2191
|
+
const impactedFiles = /* @__PURE__ */ new Set([rootFile]);
|
|
2192
|
+
for (const { symbolId } of impactedSymbols) impactedFiles.add(fileOf(symbolId));
|
|
2193
|
+
return findCoChangedFiles(coChangeMap, rootFile, impactedFiles);
|
|
2194
|
+
}
|
|
2195
|
+
function fileOf(symbolId) {
|
|
2196
|
+
return symbolId.split("::")[0];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1274
2199
|
// src/core/overlay/architectural-overlay.ts
|
|
1275
2200
|
var DEFAULT_RULES = [
|
|
1276
2201
|
// Test layer (matched first — __tests__, .test.ts, tests/, fixtures)
|
|
@@ -1324,6 +2249,7 @@ var ArchitecturalOverlay = class {
|
|
|
1324
2249
|
// src/core/dead-code/dead-code-detector.ts
|
|
1325
2250
|
var TEST_PATTERNS = [/__tests__/, /\.test\.ts$/, /\.spec\.ts$/, /\btests\//, /\bfixtures?\//];
|
|
1326
2251
|
var CONFIG_PATTERNS = [/\.(config|rc)\.(ts|js|json)$/, /tsconfig/, /eslint/, /\.d\.ts$/];
|
|
2252
|
+
var SQL_PATTERN = /\.sql$/i;
|
|
1327
2253
|
var FRAMEWORK_PATTERNS = [
|
|
1328
2254
|
// Vitest
|
|
1329
2255
|
/^(describe|it|expect|beforeAll|beforeEach|afterAll|afterEach|vi)$/,
|
|
@@ -1347,8 +2273,10 @@ var SCAFFOLDING_PATTERNS = [
|
|
|
1347
2273
|
var DeadCodeDetector = class {
|
|
1348
2274
|
detect(graph, options = {}) {
|
|
1349
2275
|
const allNodes = graph.allNodes();
|
|
1350
|
-
const candidates =
|
|
2276
|
+
const candidates = allNodes.filter((n) => {
|
|
1351
2277
|
const file = n.symbolId.split("::")[0] ?? "";
|
|
2278
|
+
if (SQL_PATTERN.test(file)) return false;
|
|
2279
|
+
if (options.includeTests) return true;
|
|
1352
2280
|
return !this.matchesAny(file, TEST_PATTERNS) && !this.matchesAny(file, CONFIG_PATTERNS);
|
|
1353
2281
|
});
|
|
1354
2282
|
if (candidates.length === 0) {
|
|
@@ -1365,7 +2293,7 @@ var DeadCodeDetector = class {
|
|
|
1365
2293
|
const forwardEdges = graph.getForwardEdges(node.symbolId);
|
|
1366
2294
|
const incomingFromCandidates = reverseEdges.filter((e) => candidateIds.has(e.from) && e.from !== node.symbolId);
|
|
1367
2295
|
if (incomingFromCandidates.length === 0) {
|
|
1368
|
-
const hasOutgoing = forwardEdges.some((e) => candidateIds.has(e.to));
|
|
2296
|
+
const hasOutgoing = forwardEdges.some((e) => candidateIds.has(e.to) || DATA_EDGE_KINDS.has(e.kind));
|
|
1369
2297
|
if (hasOutgoing || reverseEdges.length > 0) {
|
|
1370
2298
|
entryPoints.add(node.symbolId);
|
|
1371
2299
|
}
|
|
@@ -1531,13 +2459,13 @@ var DeadCodeDetector = class {
|
|
|
1531
2459
|
var DEFAULT_DAMPING = 0.85;
|
|
1532
2460
|
var DEFAULT_MAX_ITERATIONS = 100;
|
|
1533
2461
|
var DEFAULT_TOLERANCE = 1e-6;
|
|
1534
|
-
var
|
|
2462
|
+
var DEFAULT_LIMIT2 = 25;
|
|
1535
2463
|
var PageRankCalculator = class {
|
|
1536
2464
|
calculate(graph, options = {}) {
|
|
1537
2465
|
const damping = options.damping ?? DEFAULT_DAMPING;
|
|
1538
2466
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
1539
2467
|
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
1540
|
-
const limit = options.limit ??
|
|
2468
|
+
const limit = options.limit ?? DEFAULT_LIMIT2;
|
|
1541
2469
|
const nodes = graph.allNodes();
|
|
1542
2470
|
const n = nodes.length;
|
|
1543
2471
|
if (n === 0) {
|
|
@@ -1613,13 +2541,15 @@ var PageRankCalculator = class {
|
|
|
1613
2541
|
// src/core/overlay/boundary-violation-detector.ts
|
|
1614
2542
|
var HIGH_SEVERITY_THRESHOLD = 0;
|
|
1615
2543
|
var MEDIUM_SEVERITY_THRESHOLD = 2;
|
|
1616
|
-
var
|
|
2544
|
+
var MIN_DISTINCT_COMMITS_FOR_CONFIDENCE = 2;
|
|
1617
2545
|
var BoundaryViolationDetector = class {
|
|
1618
2546
|
detect(graph, current, history) {
|
|
1619
2547
|
const snapshotsAvailable = history.length;
|
|
2548
|
+
const distinctCommits = distinctCommitCount(history, current.commitSha);
|
|
1620
2549
|
const currentAssignments = assignmentMap2(current.communities);
|
|
1621
2550
|
const currentLabels = labelMap2(current.communities);
|
|
1622
|
-
const
|
|
2551
|
+
const earlierCommits = priorCommitSnapshots(history, current.commitSha);
|
|
2552
|
+
const historicalAssignments = earlierCommits.map((snapshot) => ({
|
|
1623
2553
|
snapshot,
|
|
1624
2554
|
assignments: assignmentMap2(snapshot.communities),
|
|
1625
2555
|
labels: labelMap2(snapshot.communities)
|
|
@@ -1627,13 +2557,14 @@ var BoundaryViolationDetector = class {
|
|
|
1627
2557
|
const violations = [];
|
|
1628
2558
|
for (const edge of graph.allEdges()) {
|
|
1629
2559
|
if (edge.from === edge.to) continue;
|
|
2560
|
+
if (DATA_EDGE_KINDS.has(edge.kind)) continue;
|
|
1630
2561
|
const fromCommunity = currentAssignments.get(edge.from);
|
|
1631
2562
|
const toCommunity = currentAssignments.get(edge.to);
|
|
1632
2563
|
if (fromCommunity === void 0 || toCommunity === void 0) continue;
|
|
1633
2564
|
if (fromCommunity === toCommunity) continue;
|
|
1634
2565
|
const fromLabel = currentLabels.get(fromCommunity) ?? `community-${fromCommunity}`;
|
|
1635
2566
|
const toLabel = currentLabels.get(toCommunity) ?? `community-${toCommunity}`;
|
|
1636
|
-
const historicalCount =
|
|
2567
|
+
const historicalCount = distinctCommits < MIN_DISTINCT_COMMITS_FOR_CONFIDENCE ? -1 : countHistoricalCrossCluster(historicalAssignments, edge.from, edge.to);
|
|
1637
2568
|
if (historicalCount === -1 || historicalCount <= MEDIUM_SEVERITY_THRESHOLD) {
|
|
1638
2569
|
violations.push({
|
|
1639
2570
|
from: {
|
|
@@ -1654,26 +2585,27 @@ var BoundaryViolationDetector = class {
|
|
|
1654
2585
|
}
|
|
1655
2586
|
violations.sort((a, b) => {
|
|
1656
2587
|
if (a.severity !== b.severity) return a.severity === "high" ? -1 : 1;
|
|
1657
|
-
const cmp = a.from.symbolId
|
|
1658
|
-
return cmp !== 0 ? cmp : a.to.symbolId
|
|
2588
|
+
const cmp = compareCodepoint(a.from.symbolId, b.from.symbolId);
|
|
2589
|
+
return cmp !== 0 ? cmp : compareCodepoint(a.to.symbolId, b.to.symbolId);
|
|
1659
2590
|
});
|
|
1660
2591
|
return {
|
|
1661
2592
|
violations,
|
|
1662
|
-
confidence: confidenceFor2(
|
|
2593
|
+
confidence: confidenceFor2(distinctCommits),
|
|
1663
2594
|
snapshotsAvailable,
|
|
1664
|
-
|
|
2595
|
+
distinctCommits,
|
|
2596
|
+
hint: hintFor2(distinctCommits)
|
|
1665
2597
|
};
|
|
1666
2598
|
}
|
|
1667
2599
|
};
|
|
1668
2600
|
function countHistoricalCrossCluster(history, fromSymbol, toSymbol) {
|
|
1669
|
-
let
|
|
2601
|
+
let count2 = 0;
|
|
1670
2602
|
for (const snap of history) {
|
|
1671
2603
|
const fromId = snap.assignments.get(fromSymbol);
|
|
1672
2604
|
const toId = snap.assignments.get(toSymbol);
|
|
1673
2605
|
if (fromId === void 0 || toId === void 0) continue;
|
|
1674
|
-
if (fromId !== toId)
|
|
2606
|
+
if (fromId !== toId) count2++;
|
|
1675
2607
|
}
|
|
1676
|
-
return
|
|
2608
|
+
return count2;
|
|
1677
2609
|
}
|
|
1678
2610
|
function assignmentMap2(entries) {
|
|
1679
2611
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1691,109 +2623,227 @@ function labelMap2(entries) {
|
|
|
1691
2623
|
}
|
|
1692
2624
|
return labels;
|
|
1693
2625
|
}
|
|
1694
|
-
function confidenceFor2(
|
|
1695
|
-
if (
|
|
1696
|
-
if (
|
|
2626
|
+
function confidenceFor2(distinctCommits) {
|
|
2627
|
+
if (distinctCommits >= 7) return "high";
|
|
2628
|
+
if (distinctCommits >= 3) return "medium";
|
|
1697
2629
|
return "low";
|
|
1698
2630
|
}
|
|
1699
|
-
function hintFor2(
|
|
1700
|
-
if (
|
|
1701
|
-
return "
|
|
2631
|
+
function hintFor2(distinctCommits) {
|
|
2632
|
+
if (distinctCommits >= MIN_DISTINCT_COMMITS_FOR_CONFIDENCE) return void 0;
|
|
2633
|
+
return "Install the post-commit hook (`ctxo init`) or run `ctxo index` after commits to build history; `ctxo index --check` does not record snapshots.";
|
|
1702
2634
|
}
|
|
1703
2635
|
|
|
1704
|
-
// src/core/
|
|
1705
|
-
var
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2636
|
+
// src/core/search/symbol-tokenizer.ts
|
|
2637
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2638
|
+
"get",
|
|
2639
|
+
"set",
|
|
2640
|
+
"is",
|
|
2641
|
+
"has",
|
|
2642
|
+
"can",
|
|
2643
|
+
"do",
|
|
2644
|
+
"the",
|
|
2645
|
+
"a",
|
|
2646
|
+
"an",
|
|
2647
|
+
"of",
|
|
2648
|
+
"to",
|
|
2649
|
+
"in",
|
|
2650
|
+
"for",
|
|
2651
|
+
"on",
|
|
2652
|
+
"with",
|
|
2653
|
+
"at",
|
|
2654
|
+
"by",
|
|
2655
|
+
"from",
|
|
2656
|
+
"as",
|
|
2657
|
+
"into",
|
|
2658
|
+
"not",
|
|
2659
|
+
"no",
|
|
2660
|
+
"or"
|
|
2661
|
+
]);
|
|
2662
|
+
var DEFAULT_OPTIONS = {
|
|
2663
|
+
includeStopWords: false,
|
|
2664
|
+
includeOriginal: true,
|
|
2665
|
+
includeFilePath: false
|
|
2666
|
+
};
|
|
2667
|
+
var SymbolTokenizer = class _SymbolTokenizer {
|
|
2668
|
+
options;
|
|
2669
|
+
constructor(options) {
|
|
2670
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* Tokenize a symbol name into searchable tokens.
|
|
2674
|
+
*
|
|
2675
|
+
* Examples:
|
|
2676
|
+
* "getCoChangeMetrics" → ["get", "co", "change", "metrics"] (or without stop words: ["co", "change", "metrics"])
|
|
2677
|
+
* "SqliteStorageAdapter" → ["sqlite", "storage", "adapter"]
|
|
2678
|
+
* "i_storage_port" → ["i", "storage", "port"]
|
|
2679
|
+
* "BM25Scorer" → ["bm", "25", "scorer"]
|
|
2680
|
+
* "DB_PASSWORD" → ["db", "password"]
|
|
2681
|
+
*/
|
|
2682
|
+
tokenize(symbolName, filePath) {
|
|
2683
|
+
const splitTokens = _SymbolTokenizer.splitName(symbolName);
|
|
2684
|
+
const lowered = splitTokens.map((t) => t.toLowerCase());
|
|
2685
|
+
const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
|
|
2686
|
+
if (this.options.includeOriginal) {
|
|
2687
|
+
const original = symbolName.toLowerCase();
|
|
2688
|
+
if (!tokens.includes(original)) {
|
|
2689
|
+
tokens.unshift(original);
|
|
1719
2690
|
}
|
|
1720
|
-
files.add(file);
|
|
1721
2691
|
}
|
|
1722
|
-
if (
|
|
1723
|
-
|
|
2692
|
+
if (this.options.includeFilePath && filePath) {
|
|
2693
|
+
const pathTokens = _SymbolTokenizer.tokenizeFilePath(filePath);
|
|
2694
|
+
for (const pt of pathTokens) {
|
|
2695
|
+
if (!tokens.includes(pt)) {
|
|
2696
|
+
tokens.push(pt);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
1724
2699
|
}
|
|
2700
|
+
return tokens;
|
|
1725
2701
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2702
|
+
/**
|
|
2703
|
+
* Tokenize a search query. Queries use stop words and don't include originals.
|
|
2704
|
+
*/
|
|
2705
|
+
tokenizeQuery(query) {
|
|
2706
|
+
const words = query.trim().split(/\s+/);
|
|
2707
|
+
const tokens = [];
|
|
2708
|
+
for (const word of words) {
|
|
2709
|
+
const split = _SymbolTokenizer.splitName(word);
|
|
2710
|
+
for (const t of split) {
|
|
2711
|
+
const lowered = t.toLowerCase();
|
|
2712
|
+
if (lowered.length > 0 && !tokens.includes(lowered)) {
|
|
2713
|
+
tokens.push(lowered);
|
|
2714
|
+
}
|
|
1734
2715
|
}
|
|
1735
2716
|
}
|
|
2717
|
+
return tokens;
|
|
1736
2718
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
1753
|
-
entries
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
function loadCoChangeMap(matrix) {
|
|
1757
|
-
const map = /* @__PURE__ */ new Map();
|
|
1758
|
-
for (const entry of matrix.entries) {
|
|
1759
|
-
let list1 = map.get(entry.file1);
|
|
1760
|
-
if (!list1) {
|
|
1761
|
-
list1 = [];
|
|
1762
|
-
map.set(entry.file1, list1);
|
|
2719
|
+
/**
|
|
2720
|
+
* Split a name into constituent words based on naming convention boundaries.
|
|
2721
|
+
*
|
|
2722
|
+
* Rules:
|
|
2723
|
+
* 1. Split on underscores and hyphens (snake_case, kebab-case)
|
|
2724
|
+
* 2. Split on camelCase/PascalCase boundaries (uppercase after lowercase)
|
|
2725
|
+
* 3. Split acronym boundaries (e.g., "HTMLParser" → ["HTML", "Parser"])
|
|
2726
|
+
* 4. Split on digit ↔ letter boundaries
|
|
2727
|
+
*/
|
|
2728
|
+
static splitName(name) {
|
|
2729
|
+
if (!name || name.length === 0) return [];
|
|
2730
|
+
const segments = name.split(/[_\-./\\]+/).filter((s) => s.length > 0);
|
|
2731
|
+
const tokens = [];
|
|
2732
|
+
for (const segment of segments) {
|
|
2733
|
+
tokens.push(..._SymbolTokenizer.splitCamelCase(segment));
|
|
1763
2734
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2735
|
+
return tokens.filter((t) => t.length > 0);
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Split a single segment on camelCase/PascalCase/digit boundaries.
|
|
2739
|
+
*/
|
|
2740
|
+
static splitCamelCase(segment) {
|
|
2741
|
+
const tokens = [];
|
|
2742
|
+
let current = "";
|
|
2743
|
+
for (let i = 0; i < segment.length; i++) {
|
|
2744
|
+
const ch = segment[i];
|
|
2745
|
+
const prev = i > 0 ? segment[i - 1] : "";
|
|
2746
|
+
if (current.length > 0) {
|
|
2747
|
+
const prevIsDigit = isDigit(prev);
|
|
2748
|
+
const currIsDigit = isDigit(ch);
|
|
2749
|
+
if (prevIsDigit !== currIsDigit) {
|
|
2750
|
+
tokens.push(current);
|
|
2751
|
+
current = ch;
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (current.length > 0 && isLower(prev) && isUpper(ch)) {
|
|
2756
|
+
tokens.push(current);
|
|
2757
|
+
current = ch;
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
|
|
2761
|
+
const lastChar = current[current.length - 1];
|
|
2762
|
+
if (isUpper(lastChar)) {
|
|
2763
|
+
tokens.push(current.slice(0, -1));
|
|
2764
|
+
current = lastChar + ch;
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
current += ch;
|
|
1769
2769
|
}
|
|
1770
|
-
|
|
2770
|
+
if (current.length > 0) {
|
|
2771
|
+
tokens.push(current);
|
|
2772
|
+
}
|
|
2773
|
+
return tokens;
|
|
1771
2774
|
}
|
|
1772
|
-
|
|
2775
|
+
/**
|
|
2776
|
+
* Extract searchable tokens from a file path.
|
|
2777
|
+
* "src/core/search/symbol-tokenizer.ts" → ["src", "core", "search", "symbol", "tokenizer", "ts"]
|
|
2778
|
+
*/
|
|
2779
|
+
static tokenizeFilePath(filePath) {
|
|
2780
|
+
const withoutExt = filePath.replace(/\.[^.]+$/, "");
|
|
2781
|
+
const segments = withoutExt.split(/[/\\]+/);
|
|
2782
|
+
const tokens = [];
|
|
2783
|
+
for (const seg of segments) {
|
|
2784
|
+
const parts = seg.split(/[_\-.]+/).filter((s) => s.length > 0);
|
|
2785
|
+
for (const p of parts) {
|
|
2786
|
+
const lowered = p.toLowerCase();
|
|
2787
|
+
if (lowered.length > 0 && !tokens.includes(lowered)) {
|
|
2788
|
+
tokens.push(lowered);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return tokens;
|
|
2793
|
+
}
|
|
2794
|
+
/** Check if a token is a stop word */
|
|
2795
|
+
static isStopWord(token) {
|
|
2796
|
+
return STOP_WORDS.has(token.toLowerCase());
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
function isUpper(ch) {
|
|
2800
|
+
return ch >= "A" && ch <= "Z";
|
|
2801
|
+
}
|
|
2802
|
+
function isLower(ch) {
|
|
2803
|
+
return ch >= "a" && ch <= "z";
|
|
2804
|
+
}
|
|
2805
|
+
function isDigit(ch) {
|
|
2806
|
+
return ch >= "0" && ch <= "9";
|
|
1773
2807
|
}
|
|
1774
2808
|
|
|
1775
2809
|
export {
|
|
2810
|
+
compareCodepoint,
|
|
1776
2811
|
CommunitySnapshotWriter,
|
|
1777
2812
|
SqliteStorageAdapter,
|
|
2813
|
+
buildCoChangeMatrix,
|
|
2814
|
+
CoChangeReader,
|
|
2815
|
+
DATA_EDGE_KINDS,
|
|
2816
|
+
isDataResourceKind,
|
|
2817
|
+
parseCoChangeSince,
|
|
2818
|
+
sinceCutoff,
|
|
2819
|
+
selectWindow,
|
|
2820
|
+
loadConfig,
|
|
2821
|
+
statsEnabled,
|
|
2822
|
+
indexIgnorePatterns,
|
|
2823
|
+
indexIgnoreProjectPatterns,
|
|
2824
|
+
factsRequested,
|
|
2825
|
+
indexTestPatterns,
|
|
2826
|
+
communityDetectorOptions,
|
|
2827
|
+
maskingClusterLabelPatterns,
|
|
2828
|
+
watchSnapshotMinFileChanges,
|
|
2829
|
+
resolveCoChangeConfig,
|
|
2830
|
+
resolveContextsViewConfig,
|
|
2831
|
+
makeGlobMatcher,
|
|
2832
|
+
filesWithTestAnnotations,
|
|
2833
|
+
createTestFileClassifier,
|
|
1778
2834
|
SymbolGraph,
|
|
1779
2835
|
RevertDetector,
|
|
1780
2836
|
DriftDetector,
|
|
1781
2837
|
SimpleGitAdapter,
|
|
1782
2838
|
BlastRadiusCalculator,
|
|
2839
|
+
findCoChangedFilesForBlastRadius,
|
|
1783
2840
|
ArchitecturalOverlay,
|
|
2841
|
+
ContextsViewReader,
|
|
1784
2842
|
DeadCodeDetector,
|
|
2843
|
+
SymbolTokenizer,
|
|
1785
2844
|
PageRankCalculator,
|
|
1786
2845
|
BoundaryViolationDetector,
|
|
1787
|
-
aggregateCoChanges,
|
|
1788
|
-
loadCoChangeMap,
|
|
1789
2846
|
SessionRecorderAdapter,
|
|
1790
|
-
detectWorkspace
|
|
1791
|
-
loadConfig,
|
|
1792
|
-
statsEnabled,
|
|
1793
|
-
indexIgnorePatterns,
|
|
1794
|
-
indexIgnoreProjectPatterns,
|
|
1795
|
-
maskingClusterLabelPatterns,
|
|
1796
|
-
watchSnapshotMinFileChanges,
|
|
1797
|
-
makeGlobMatcher
|
|
2847
|
+
detectWorkspace
|
|
1798
2848
|
};
|
|
1799
|
-
//# sourceMappingURL=chunk-
|
|
2849
|
+
//# sourceMappingURL=chunk-BYX3UFBJ.js.map
|