@wrongstack/tools 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-proceed-loop-guard.d.ts +1 -1
- package/dist/auto-proceed-loop-guard.js +1 -1
- package/dist/bash.js +1 -1
- package/dist/builtin.js +15 -15
- package/dist/{chunk-VVZPQPTY.js → chunk-3AU6CZWL.js} +4 -2
- package/dist/{chunk-N3K2SFEI.js → chunk-52UXXQMP.js} +1408 -200
- package/dist/{chunk-NN2SNP2R.js → chunk-6H4ERJP7.js} +137 -21
- package/dist/{chunk-DK4J2WMH.js → chunk-7XUEPBNT.js} +2 -2
- package/dist/{chunk-IYH4OHBP.js → chunk-DKHMPKDG.js} +2 -2
- package/dist/{chunk-NU2IZ72J.js → chunk-DL2ARI4S.js} +7 -5
- package/dist/{chunk-M6UV67BH.js → chunk-EL2NJXSK.js} +2 -2
- package/dist/{chunk-YEISRT4S.js → chunk-HWXIRVKH.js} +1408 -235
- package/dist/{chunk-GKSG5CPH.js → chunk-KVTEIUND.js} +2 -2
- package/dist/{chunk-GDDXX27P.js → chunk-MBR5VAB5.js} +2 -2
- package/dist/{chunk-KDEXUQY7.js → chunk-MGPR7VG3.js} +261 -258
- package/dist/{chunk-YULVUTK5.js → chunk-NFX2IUQA.js} +3 -3
- package/dist/{chunk-37QKUQ2Z.js → chunk-NL2S2CXD.js} +1 -1
- package/dist/{chunk-ZTCJCYLN.js → chunk-TI5COGLY.js} +49 -6
- package/dist/{chunk-CGXTR34H.js → chunk-TU5MTW2S.js} +2 -2
- package/dist/{chunk-AB2CAUOP.js → chunk-UCXFTCAI.js} +186 -36
- package/dist/{chunk-MK2JK35J.js → chunk-UOYRAS4J.js} +3 -3
- package/dist/{chunk-UCYQTZ7F.js → chunk-ZKC5UNGD.js} +56 -2
- package/dist/codebase-index/atlas-brief.d.ts +90 -0
- package/dist/codebase-index/atlas-export.d.ts +38 -0
- package/dist/codebase-index/atlas-projection.d.ts +114 -0
- package/dist/codebase-index/atlas-types.d.ts +90 -0
- package/dist/codebase-index/background-indexer.d.ts +15 -4
- package/dist/codebase-index/codebase-context-tool.d.ts +54 -0
- package/dist/codebase-index/concept-enrichment.d.ts +154 -0
- package/dist/codebase-index/context-retrieval.d.ts +91 -0
- package/dist/codebase-index/embedding-pass.d.ts +95 -0
- package/dist/codebase-index/graph-adjacency-cache.d.ts +36 -0
- package/dist/codebase-index/graph-rank-pass.d.ts +46 -0
- package/dist/codebase-index/graph-rank.d.ts +198 -0
- package/dist/codebase-index/index-service.d.ts +15 -1
- package/dist/codebase-index/index.d.ts +34 -19
- package/dist/codebase-index/index.js +126 -9
- package/dist/codebase-index/parser-worker-script.js +3 -3
- package/dist/codebase-index/perf-metrics.d.ts +19 -0
- package/dist/codebase-index/project-server-protocol.d.ts +2 -0
- package/dist/codebase-index/project-server-query-cache.d.ts +4 -0
- package/dist/codebase-index/project-server.js +54 -7
- package/dist/codebase-index/repo-map-fallback.d.ts +18 -0
- package/dist/codebase-index/repo-map-render.d.ts +42 -0
- package/dist/codebase-index/repo-map-types.d.ts +21 -0
- package/dist/codebase-index/repo-map.d.ts +28 -16
- package/dist/codebase-index/schema.d.ts +20 -0
- package/dist/codebase-index/worker-protocol.d.ts +47 -1
- package/dist/codebase-index/worker.js +11 -5
- package/dist/codebase-index/writer-concepts.d.ts +84 -0
- package/dist/codebase-index/writer-graph-decorate.d.ts +30 -0
- package/dist/codebase-index/writer-graph-reader.d.ts +22 -0
- package/dist/codebase-index/writer-rank.d.ts +78 -0
- package/dist/codebase-index/writer-schema.d.ts +51 -0
- package/dist/codebase-index/writer-vectors.d.ts +53 -0
- package/dist/codebase-index/writer.d.ts +59 -0
- package/dist/edit.js +7 -7
- package/dist/exec.js +1 -1
- package/dist/{go-parser-D7ELWQ7F.js → go-parser-QIEXU3P2.js} +4 -2
- package/dist/index.d.ts +48 -48
- package/dist/index.js +43 -19
- package/dist/pack.js +16 -16
- package/dist/{parser-dispatch-BDVCJXTV.js → parser-dispatch-37FT3EXQ.js} +4 -4
- package/dist/patch.js +7 -7
- package/dist/{py-parser-QC6MR4GB.js → py-parser-6GJTGUXK.js} +3 -3
- package/dist/read.js +7 -7
- package/dist/replace.js +7 -7
- package/dist/tool-tier.js +17 -17
- package/dist/write.js +7 -7
- package/package.json +5 -5
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
spawnStream
|
|
6
6
|
} from "./chunk-DRRY6SCM.js";
|
|
7
7
|
import {
|
|
8
|
+
codebaseContext,
|
|
8
9
|
codebaseIndexStats,
|
|
10
|
+
codebaseVectorSearch,
|
|
9
11
|
enqueueReindex,
|
|
10
12
|
getIndexState,
|
|
11
13
|
incomingCallsService,
|
|
@@ -13,34 +15,31 @@ import {
|
|
|
13
15
|
outgoingCallsService,
|
|
14
16
|
runStartupIndex,
|
|
15
17
|
searchCodebaseIndex
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-TI5COGLY.js";
|
|
17
19
|
import {
|
|
18
20
|
IndexTimeoutError,
|
|
19
21
|
SCHEMA_VERSION,
|
|
20
22
|
codebaseIndexDirOverride,
|
|
21
23
|
indexCircuitBreaker,
|
|
22
24
|
indexStorePool,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
isConceptRelation,
|
|
26
|
+
posixIndexPath,
|
|
27
|
+
resolveIndexDir,
|
|
28
|
+
xxhash64String
|
|
29
|
+
} from "./chunk-52UXXQMP.js";
|
|
25
30
|
import {
|
|
26
31
|
EXT_TO_LANG,
|
|
27
32
|
detectLang,
|
|
28
33
|
isIndexablePath
|
|
29
34
|
} from "./chunk-YOHO4DVT.js";
|
|
35
|
+
import {
|
|
36
|
+
loadGitignoreMatcher
|
|
37
|
+
} from "./chunk-2CTZW6JQ.js";
|
|
30
38
|
import {
|
|
31
39
|
normalizeCommandOutput,
|
|
32
40
|
safeResolveProjectPath
|
|
33
41
|
} from "./chunk-ZUFHAHTE.js";
|
|
34
42
|
|
|
35
|
-
// src/codebase-index/codebase-ast-replace-tool.ts
|
|
36
|
-
import * as path2 from "node:path";
|
|
37
|
-
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
38
|
-
|
|
39
|
-
// src/codebase-index/ast-symbol-mutator.ts
|
|
40
|
-
import * as fs from "node:fs/promises";
|
|
41
|
-
import * as path from "node:path";
|
|
42
|
-
import { atomicWrite } from "@wrongstack/core/utils";
|
|
43
|
-
|
|
44
43
|
// src/codebase-index/ast-invariant-engine.ts
|
|
45
44
|
var tsModule = null;
|
|
46
45
|
async function loadTs() {
|
|
@@ -467,6 +466,9 @@ var PolyglotInvariantEngine = class {
|
|
|
467
466
|
var polyglotInvariantEngine = new PolyglotInvariantEngine();
|
|
468
467
|
|
|
469
468
|
// src/codebase-index/ast-symbol-mutator.ts
|
|
469
|
+
import * as fs from "node:fs/promises";
|
|
470
|
+
import * as path from "node:path";
|
|
471
|
+
import { atomicWrite } from "@wrongstack/core/utils";
|
|
470
472
|
var tsModule2 = null;
|
|
471
473
|
async function loadTs2() {
|
|
472
474
|
if (!tsModule2) {
|
|
@@ -639,6 +641,8 @@ Pass allowBreakingChanges: true if this breaking change is intended.`
|
|
|
639
641
|
}
|
|
640
642
|
|
|
641
643
|
// src/codebase-index/codebase-ast-replace-tool.ts
|
|
644
|
+
import * as path2 from "node:path";
|
|
645
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
642
646
|
var codebaseAstReplaceTool = {
|
|
643
647
|
name: "codebase-ast-replace",
|
|
644
648
|
category: "Edit",
|
|
@@ -705,88 +709,9 @@ var codebaseAstReplaceTool = {
|
|
|
705
709
|
}
|
|
706
710
|
};
|
|
707
711
|
|
|
708
|
-
// src/codebase-index/codebase-invariant-check-tool.ts
|
|
709
|
-
import * as fs2 from "node:fs/promises";
|
|
710
|
-
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
711
|
-
var codebaseInvariantCheckTool = {
|
|
712
|
-
name: "codebase-invariant-check",
|
|
713
|
-
category: "Inspect",
|
|
714
|
-
icon: "index",
|
|
715
|
-
permission: "auto",
|
|
716
|
-
subjectKey: "file",
|
|
717
|
-
mutating: false,
|
|
718
|
-
capabilities: ["fs.read"],
|
|
719
|
-
description: "Mathematically verify AST backward-compatibility invariants between original code and candidate mutations across TypeScript, Python, Go, Rust, Java, etc. Detects missing exports (INV-001), mandatory parameter additions (INV-002), interface breaking expansions (INV-003), and return type changes (INV-004) with zero hallucinations.",
|
|
720
|
-
usageHint: "PRE-FLIGHT AST INVARIANT AUDIT:\n\n- Provide `file` and `modifiedCode` (or both `originalCode` and `modifiedCode`).\n- Returns `valid: true` if changes are strictly backward compatible.\n- Returns structured `violations[]` with exact rule IDs, offending symbol names, and fix instructions.",
|
|
721
|
-
inputSchema: {
|
|
722
|
-
type: "object",
|
|
723
|
-
properties: {
|
|
724
|
-
file: {
|
|
725
|
-
type: "string",
|
|
726
|
-
description: "Path to existing source file to compare against candidate modifications."
|
|
727
|
-
},
|
|
728
|
-
originalCode: {
|
|
729
|
-
type: "string",
|
|
730
|
-
description: "Original source code string (if not reading from an existing file)."
|
|
731
|
-
},
|
|
732
|
-
modifiedCode: {
|
|
733
|
-
type: "string",
|
|
734
|
-
description: "The candidate modified code string to validate."
|
|
735
|
-
},
|
|
736
|
-
lang: {
|
|
737
|
-
type: "string",
|
|
738
|
-
description: "Language hint (ts, py, go, rs, etc.). Inferred from file path if omitted."
|
|
739
|
-
}
|
|
740
|
-
},
|
|
741
|
-
required: ["modifiedCode"],
|
|
742
|
-
additionalProperties: false
|
|
743
|
-
},
|
|
744
|
-
async execute(input, ctx) {
|
|
745
|
-
try {
|
|
746
|
-
let originalCode = input.originalCode;
|
|
747
|
-
let lang = input.lang;
|
|
748
|
-
if (!originalCode && input.file) {
|
|
749
|
-
const resolved = await safeResolveProjectPath(input.file, ctx);
|
|
750
|
-
originalCode = await fs2.readFile(resolved, "utf8");
|
|
751
|
-
if (!lang) {
|
|
752
|
-
lang = detectLang(resolved) ?? "ts";
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (originalCode === void 0) {
|
|
756
|
-
return {
|
|
757
|
-
valid: false,
|
|
758
|
-
violations: [],
|
|
759
|
-
summary: "Neither originalCode nor file was provided for invariant comparison.",
|
|
760
|
-
error: "Missing originalCode or file path."
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
const res = await polyglotInvariantEngine.evaluate({
|
|
764
|
-
originalCode,
|
|
765
|
-
modifiedCode: input.modifiedCode,
|
|
766
|
-
lang,
|
|
767
|
-
filePath: input.file
|
|
768
|
-
});
|
|
769
|
-
const summary = res.valid ? "AST Invariants PASSED. Mutation is 100% backward compatible." : `AST Invariants FAILED with ${res.violations.length} violation(s):
|
|
770
|
-
` + res.violations.map((v) => ` - [${v.ruleId}] ${v.symbolName}: ${v.message}`).join("\n");
|
|
771
|
-
return {
|
|
772
|
-
valid: res.valid,
|
|
773
|
-
violations: res.violations,
|
|
774
|
-
summary
|
|
775
|
-
};
|
|
776
|
-
} catch (err) {
|
|
777
|
-
return {
|
|
778
|
-
valid: false,
|
|
779
|
-
violations: [],
|
|
780
|
-
summary: `Error during invariant evaluation: ${toErrorMessage2(err)}`,
|
|
781
|
-
error: toErrorMessage2(err)
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
|
|
787
712
|
// src/codebase-index/codebase-impact-analysis-tool.ts
|
|
788
713
|
import * as path3 from "node:path";
|
|
789
|
-
import { toErrorMessage as
|
|
714
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
790
715
|
var TEST_FILE_REGEX = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
|
|
791
716
|
var codebaseImpactAnalysisTool = {
|
|
792
717
|
name: "codebase-impact-analysis",
|
|
@@ -840,7 +765,7 @@ var codebaseImpactAnalysisTool = {
|
|
|
840
765
|
} catch (err) {
|
|
841
766
|
symbolFound = false;
|
|
842
767
|
indexUnavailable = true;
|
|
843
|
-
indexError =
|
|
768
|
+
indexError = toErrorMessage2(err);
|
|
844
769
|
}
|
|
845
770
|
if (indexUnavailable) {
|
|
846
771
|
const detail = indexError ? ` (${indexError})` : "";
|
|
@@ -943,14 +868,14 @@ var codebaseImpactAnalysisTool = {
|
|
|
943
868
|
callSites: [],
|
|
944
869
|
recommendedActionPlan: [],
|
|
945
870
|
symbolFound: false,
|
|
946
|
-
error:
|
|
871
|
+
error: toErrorMessage2(err)
|
|
947
872
|
};
|
|
948
873
|
}
|
|
949
874
|
}
|
|
950
875
|
};
|
|
951
876
|
|
|
952
877
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
953
|
-
import { toErrorMessage as
|
|
878
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
954
879
|
var codebaseIncomingCallsTool = {
|
|
955
880
|
name: "codebase-incoming-calls",
|
|
956
881
|
category: "Project",
|
|
@@ -1024,7 +949,7 @@ var codebaseIncomingCallsTool = {
|
|
|
1024
949
|
symbol: input.symbol,
|
|
1025
950
|
calls: [],
|
|
1026
951
|
total: 0,
|
|
1027
|
-
indexStatus: `Index query failed: ${
|
|
952
|
+
indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep for this lookup.`
|
|
1028
953
|
};
|
|
1029
954
|
}
|
|
1030
955
|
const { calls, symbolFound, ambiguous, totalMatches, stale } = serviced;
|
|
@@ -1086,7 +1011,7 @@ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/ty
|
|
|
1086
1011
|
|
|
1087
1012
|
// src/codebase-index/codebase-search-tool.ts
|
|
1088
1013
|
import { ToolValidationError } from "@wrongstack/core/types";
|
|
1089
|
-
import { toErrorMessage as
|
|
1014
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
1090
1015
|
var INDEXABLE_LANG_IDS = [
|
|
1091
1016
|
...new Set(Object.values(EXT_TO_LANG))
|
|
1092
1017
|
].sort();
|
|
@@ -1219,7 +1144,7 @@ var codebaseSearchTool = {
|
|
|
1219
1144
|
results: [],
|
|
1220
1145
|
total: 0,
|
|
1221
1146
|
query: input.query,
|
|
1222
|
-
indexStatus: `Index query failed: ${
|
|
1147
|
+
indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep/glob for this lookup.`
|
|
1223
1148
|
};
|
|
1224
1149
|
}
|
|
1225
1150
|
const { results, total, stale, indexSummary } = searched;
|
|
@@ -1345,6 +1270,85 @@ var codebaseIndexTool = {
|
|
|
1345
1270
|
}
|
|
1346
1271
|
};
|
|
1347
1272
|
|
|
1273
|
+
// src/codebase-index/codebase-invariant-check-tool.ts
|
|
1274
|
+
import * as fs2 from "node:fs/promises";
|
|
1275
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
1276
|
+
var codebaseInvariantCheckTool = {
|
|
1277
|
+
name: "codebase-invariant-check",
|
|
1278
|
+
category: "Inspect",
|
|
1279
|
+
icon: "index",
|
|
1280
|
+
permission: "auto",
|
|
1281
|
+
subjectKey: "file",
|
|
1282
|
+
mutating: false,
|
|
1283
|
+
capabilities: ["fs.read"],
|
|
1284
|
+
description: "Mathematically verify AST backward-compatibility invariants between original code and candidate mutations across TypeScript, Python, Go, Rust, Java, etc. Detects missing exports (INV-001), mandatory parameter additions (INV-002), interface breaking expansions (INV-003), and return type changes (INV-004) with zero hallucinations.",
|
|
1285
|
+
usageHint: "PRE-FLIGHT AST INVARIANT AUDIT:\n\n- Provide `file` and `modifiedCode` (or both `originalCode` and `modifiedCode`).\n- Returns `valid: true` if changes are strictly backward compatible.\n- Returns structured `violations[]` with exact rule IDs, offending symbol names, and fix instructions.",
|
|
1286
|
+
inputSchema: {
|
|
1287
|
+
type: "object",
|
|
1288
|
+
properties: {
|
|
1289
|
+
file: {
|
|
1290
|
+
type: "string",
|
|
1291
|
+
description: "Path to existing source file to compare against candidate modifications."
|
|
1292
|
+
},
|
|
1293
|
+
originalCode: {
|
|
1294
|
+
type: "string",
|
|
1295
|
+
description: "Original source code string (if not reading from an existing file)."
|
|
1296
|
+
},
|
|
1297
|
+
modifiedCode: {
|
|
1298
|
+
type: "string",
|
|
1299
|
+
description: "The candidate modified code string to validate."
|
|
1300
|
+
},
|
|
1301
|
+
lang: {
|
|
1302
|
+
type: "string",
|
|
1303
|
+
description: "Language hint (ts, py, go, rs, etc.). Inferred from file path if omitted."
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
required: ["modifiedCode"],
|
|
1307
|
+
additionalProperties: false
|
|
1308
|
+
},
|
|
1309
|
+
async execute(input, ctx) {
|
|
1310
|
+
try {
|
|
1311
|
+
let originalCode = input.originalCode;
|
|
1312
|
+
let lang = input.lang;
|
|
1313
|
+
if (!originalCode && input.file) {
|
|
1314
|
+
const resolved = await safeResolveProjectPath(input.file, ctx);
|
|
1315
|
+
originalCode = await fs2.readFile(resolved, "utf8");
|
|
1316
|
+
if (!lang) {
|
|
1317
|
+
lang = detectLang(resolved) ?? "ts";
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (originalCode === void 0) {
|
|
1321
|
+
return {
|
|
1322
|
+
valid: false,
|
|
1323
|
+
violations: [],
|
|
1324
|
+
summary: "Neither originalCode nor file was provided for invariant comparison.",
|
|
1325
|
+
error: "Missing originalCode or file path."
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const res = await polyglotInvariantEngine.evaluate({
|
|
1329
|
+
originalCode,
|
|
1330
|
+
modifiedCode: input.modifiedCode,
|
|
1331
|
+
lang,
|
|
1332
|
+
filePath: input.file
|
|
1333
|
+
});
|
|
1334
|
+
const summary = res.valid ? "AST Invariants PASSED. Mutation is 100% backward compatible." : `AST Invariants FAILED with ${res.violations.length} violation(s):
|
|
1335
|
+
` + res.violations.map((v) => ` - [${v.ruleId}] ${v.symbolName}: ${v.message}`).join("\n");
|
|
1336
|
+
return {
|
|
1337
|
+
valid: res.valid,
|
|
1338
|
+
violations: res.violations,
|
|
1339
|
+
summary
|
|
1340
|
+
};
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
return {
|
|
1343
|
+
valid: false,
|
|
1344
|
+
violations: [],
|
|
1345
|
+
summary: `Error during invariant evaluation: ${toErrorMessage5(err)}`,
|
|
1346
|
+
error: toErrorMessage5(err)
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1348
1352
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
1349
1353
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
1350
1354
|
var codebaseOutgoingCallsTool = {
|
|
@@ -1481,9 +1485,16 @@ var codebaseOutgoingCallsTool = {
|
|
|
1481
1485
|
import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
1482
1486
|
|
|
1483
1487
|
// src/codebase-index/repo-map.ts
|
|
1484
|
-
import
|
|
1488
|
+
import { existsSync } from "node:fs";
|
|
1489
|
+
import * as path6 from "node:path";
|
|
1490
|
+
|
|
1491
|
+
// src/codebase-index/repo-map-fallback.ts
|
|
1492
|
+
import * as fs5 from "node:fs/promises";
|
|
1485
1493
|
import * as path5 from "node:path";
|
|
1486
1494
|
|
|
1495
|
+
// src/codebase-index/repo-map-render.ts
|
|
1496
|
+
import * as fs4 from "node:fs/promises";
|
|
1497
|
+
|
|
1487
1498
|
// src/codebase-index/skeleton-extractor.ts
|
|
1488
1499
|
import * as fs3 from "node:fs/promises";
|
|
1489
1500
|
import * as path4 from "node:path";
|
|
@@ -1870,40 +1881,102 @@ ${res.skeleton}`
|
|
|
1870
1881
|
};
|
|
1871
1882
|
}
|
|
1872
1883
|
|
|
1873
|
-
// src/codebase-index/repo-map.ts
|
|
1884
|
+
// src/codebase-index/repo-map-render.ts
|
|
1885
|
+
var TRUNCATION_NOTE = "\n // \u2026 signatures truncated to fit the token budget";
|
|
1886
|
+
function truncateAtLine(text, limit) {
|
|
1887
|
+
if (text.length <= limit) return text;
|
|
1888
|
+
const cut = text.slice(0, limit);
|
|
1889
|
+
const lastNewline = cut.lastIndexOf("\n");
|
|
1890
|
+
return lastNewline > 0 ? cut.slice(0, lastNewline) : cut;
|
|
1891
|
+
}
|
|
1892
|
+
var COMMENT_ONLY_LINE = /^\s*(?:\/\/|\/\*|\*\/|\*|#|--|<!--|-->)/;
|
|
1893
|
+
function stripCommentLines(body) {
|
|
1894
|
+
const kept = body.split("\n").filter((line) => !COMMENT_ONLY_LINE.test(line));
|
|
1895
|
+
const joined = kept.join("\n").trim();
|
|
1896
|
+
return joined.length > 0 ? joined : body;
|
|
1897
|
+
}
|
|
1898
|
+
async function renderSkeletonSections(candidates, charBudget, options = {}) {
|
|
1899
|
+
const sections = [];
|
|
1900
|
+
const files = [];
|
|
1901
|
+
const maxSectionChars = options.maxSectionChars ?? Number.POSITIVE_INFINITY;
|
|
1902
|
+
let chars = 0;
|
|
1903
|
+
for (const candidate of candidates) {
|
|
1904
|
+
if (chars >= charBudget) break;
|
|
1905
|
+
let content;
|
|
1906
|
+
try {
|
|
1907
|
+
content = await fs4.readFile(candidate.absolute, "utf8");
|
|
1908
|
+
} catch {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (content.length === 0) continue;
|
|
1912
|
+
let skeleton;
|
|
1913
|
+
try {
|
|
1914
|
+
skeleton = await extractFileSkeleton({
|
|
1915
|
+
file: candidate.absolute,
|
|
1916
|
+
content,
|
|
1917
|
+
options: {
|
|
1918
|
+
exportsOnly: true,
|
|
1919
|
+
collapseImports: true,
|
|
1920
|
+
includeDocs: false,
|
|
1921
|
+
includeLineNumbers: true,
|
|
1922
|
+
...options.skeleton ?? {}
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
} catch {
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const body = skeleton.skeleton.trim();
|
|
1929
|
+
if (!body) continue;
|
|
1930
|
+
const indented = (options.signaturesOnly === true ? stripCommentLines(body) : body).split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n");
|
|
1931
|
+
const header = `${candidate.relative} (${skeleton.lang}):
|
|
1932
|
+
`;
|
|
1933
|
+
const room = Math.min(maxSectionChars, charBudget - chars) - header.length - TRUNCATION_NOTE.length - 2;
|
|
1934
|
+
let section;
|
|
1935
|
+
if (indented.length <= room) {
|
|
1936
|
+
section = header + indented;
|
|
1937
|
+
} else {
|
|
1938
|
+
if (room <= 0) break;
|
|
1939
|
+
section = `${header}${truncateAtLine(indented, room)}${TRUNCATION_NOTE}`;
|
|
1940
|
+
}
|
|
1941
|
+
const sectionChars = section.length + 2;
|
|
1942
|
+
if (chars + sectionChars > charBudget && files.length > 0) break;
|
|
1943
|
+
sections.push(section);
|
|
1944
|
+
files.push(candidate.relative);
|
|
1945
|
+
chars += sectionChars;
|
|
1946
|
+
}
|
|
1947
|
+
return { sections, files, chars };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/codebase-index/repo-map-fallback.ts
|
|
1874
1951
|
var ENTRY_POINT_REGEX = /(?:^|[/\\])(?:index|main|app|server|cli|lib|api|types|routes|mod)\.[a-zA-Z0-9]+$/i;
|
|
1875
1952
|
var TEST_OR_MOCK_REGEX = /(?:test|spec|mock|fixture|bench|example)[s]?[/\\._]/i;
|
|
1953
|
+
var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage"]);
|
|
1876
1954
|
function getEntryPointBonus(relPath) {
|
|
1877
|
-
if (TEST_OR_MOCK_REGEX.test(relPath))
|
|
1878
|
-
|
|
1879
|
-
}
|
|
1880
|
-
if (ENTRY_POINT_REGEX.test(relPath)) {
|
|
1881
|
-
return 25;
|
|
1882
|
-
}
|
|
1955
|
+
if (TEST_OR_MOCK_REGEX.test(relPath)) return -20;
|
|
1956
|
+
if (ENTRY_POINT_REGEX.test(relPath)) return 25;
|
|
1883
1957
|
const depth = relPath.split(/[/\\]/).length;
|
|
1884
1958
|
if (depth <= 2) return 10;
|
|
1885
1959
|
if (depth <= 3) return 5;
|
|
1886
1960
|
return 0;
|
|
1887
1961
|
}
|
|
1888
|
-
async function
|
|
1962
|
+
async function generateFallbackRepoMap(opts) {
|
|
1889
1963
|
const projectRoot = opts.projectRoot;
|
|
1890
|
-
const
|
|
1891
|
-
const charBudget = maxTokens * 4;
|
|
1964
|
+
const charBudget = (opts.maxTokens ?? 1200) * 4;
|
|
1892
1965
|
const focusSet = new Set((opts.focusFiles ?? []).map((f) => path5.resolve(projectRoot, f)));
|
|
1966
|
+
const isGitIgnored = await loadGitignoreMatcher(projectRoot);
|
|
1893
1967
|
const allFiles = [];
|
|
1894
1968
|
async function scan(dir) {
|
|
1895
|
-
if (allFiles.length > 500) return;
|
|
1896
1969
|
let entries;
|
|
1897
1970
|
try {
|
|
1898
|
-
entries = await
|
|
1971
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1899
1972
|
} catch {
|
|
1900
1973
|
return;
|
|
1901
1974
|
}
|
|
1902
1975
|
for (const entry of entries) {
|
|
1903
|
-
if (entry.name.startsWith(".") ||
|
|
1904
|
-
continue;
|
|
1905
|
-
}
|
|
1976
|
+
if (entry.name.startsWith(".") || ALWAYS_SKIP.has(entry.name)) continue;
|
|
1906
1977
|
const fullPath = path5.join(dir, entry.name);
|
|
1978
|
+
const rel = path5.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
1979
|
+
if (isGitIgnored(rel, entry.isDirectory())) continue;
|
|
1907
1980
|
if (entry.isDirectory()) {
|
|
1908
1981
|
await scan(fullPath);
|
|
1909
1982
|
} else if (entry.isFile() && isIndexablePath(fullPath)) {
|
|
@@ -1921,58 +1994,141 @@ async function generateRepoMap(opts) {
|
|
|
1921
1994
|
rankedFiles: []
|
|
1922
1995
|
};
|
|
1923
1996
|
}
|
|
1924
|
-
const
|
|
1925
|
-
for (const file of allFiles) {
|
|
1997
|
+
const scored = allFiles.map((file) => {
|
|
1926
1998
|
const relPath = path5.relative(projectRoot, file).replace(/\\/g, "/");
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
const
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1999
|
+
const score = 10 + (focusSet.has(file) ? 100 : 0) + getEntryPointBonus(relPath);
|
|
2000
|
+
return { file, relPath, score };
|
|
2001
|
+
});
|
|
2002
|
+
scored.sort((a, b) => b.score - a.score || a.relPath.localeCompare(b.relPath));
|
|
2003
|
+
const { sections, files } = await renderSkeletonSections(
|
|
2004
|
+
scored.map((s) => ({ absolute: s.file, relative: s.relPath })),
|
|
2005
|
+
charBudget,
|
|
2006
|
+
{ skeleton: opts.options }
|
|
2007
|
+
);
|
|
2008
|
+
const map = sections.join("\n\n");
|
|
2009
|
+
return {
|
|
2010
|
+
map,
|
|
2011
|
+
filesCount: files.length,
|
|
2012
|
+
totalFilesScanned: allFiles.length,
|
|
2013
|
+
estimatedTokens: Math.ceil(map.length / 4),
|
|
2014
|
+
rankedFiles: files
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/codebase-index/repo-map.ts
|
|
2019
|
+
var RANKED_FETCH_LIMIT = 400;
|
|
2020
|
+
var INDEX_DB_FILE = "index.db";
|
|
2021
|
+
var MAX_CLUSTERS = 16;
|
|
2022
|
+
var MAX_HOTSPOTS = 12;
|
|
2023
|
+
var HEADER_BUDGET_SHARE = 0.4;
|
|
2024
|
+
var TARGET_BODY_FILES = 6;
|
|
2025
|
+
var MIN_SECTION_CHARS = 300;
|
|
2026
|
+
function buildClusters(rows, relativeOf, packageCounts) {
|
|
2027
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2028
|
+
for (const row of rows) {
|
|
2029
|
+
const relative12 = relativeOf(row.file);
|
|
2030
|
+
const label = row.package || relative12.split("/")[0] || "(root)";
|
|
2031
|
+
const existing = byLabel.get(label);
|
|
2032
|
+
if (existing === void 0) {
|
|
2033
|
+
byLabel.set(label, {
|
|
2034
|
+
label,
|
|
2035
|
+
fileCount: packageCounts.get(row.package) ?? 0,
|
|
2036
|
+
rankedCount: 1,
|
|
2037
|
+
hub: relative12,
|
|
2038
|
+
hubRank: row.rank
|
|
1953
2039
|
});
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
existing.rankedCount += 1;
|
|
2043
|
+
if (row.rank > existing.hubRank) {
|
|
2044
|
+
existing.hub = relative12;
|
|
2045
|
+
existing.hubRank = row.rank;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return [...byLabel.values()].sort(
|
|
2049
|
+
(a, b) => b.hubRank - a.hubRank || a.label.localeCompare(b.label)
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
function renderHeader(clusters, hotspots, relativeOf, budget) {
|
|
2053
|
+
const lines = ["// Repo map \u2014 ranked by graph centrality (1.00 = most central)."];
|
|
2054
|
+
if (clusters.length > 0) {
|
|
2055
|
+
lines.push("", "// Clusters (hub = most central file):");
|
|
2056
|
+
for (const cluster of clusters.slice(0, MAX_CLUSTERS)) {
|
|
2057
|
+
const size = cluster.fileCount > 0 ? `${cluster.fileCount} files` : "unlabelled";
|
|
2058
|
+
lines.push(`// ${cluster.label} (${size}) hub: ${cluster.hub}`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (hotspots.length > 0) {
|
|
2062
|
+
lines.push("", "// Hotspots:");
|
|
2063
|
+
for (const row of hotspots.slice(0, MAX_HOTSPOTS)) {
|
|
2064
|
+
lines.push(
|
|
2065
|
+
`// ${row.rank.toFixed(2)} in=${row.inDeg} out=${row.outDeg} ${relativeOf(row.file)}`
|
|
2066
|
+
);
|
|
1966
2067
|
}
|
|
1967
2068
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
2069
|
+
while (lines.length > 1 && lines.join("\n").length + 2 > budget) lines.pop();
|
|
2070
|
+
return lines.join("\n");
|
|
2071
|
+
}
|
|
2072
|
+
async function generateRepoMap(opts) {
|
|
2073
|
+
const projectRoot = opts.projectRoot;
|
|
2074
|
+
const charBudget = (opts.maxTokens ?? 1200) * 4;
|
|
2075
|
+
let ranked = [];
|
|
2076
|
+
let packageCounts = /* @__PURE__ */ new Map();
|
|
2077
|
+
let totalRankedFiles = 0;
|
|
2078
|
+
let store;
|
|
2079
|
+
try {
|
|
2080
|
+
if (!existsSync(path6.join(resolveIndexDir(projectRoot, opts.indexDir), INDEX_DB_FILE))) {
|
|
2081
|
+
return generateFallbackRepoMap(opts);
|
|
2082
|
+
}
|
|
2083
|
+
store = indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
|
|
2084
|
+
totalRankedFiles = store.getRankCounts().files;
|
|
2085
|
+
if (totalRankedFiles > 0) {
|
|
2086
|
+
ranked = store.getRankedFiles(RANKED_FETCH_LIMIT);
|
|
2087
|
+
packageCounts = store.getPackageFileCounts();
|
|
2088
|
+
}
|
|
2089
|
+
} catch {
|
|
2090
|
+
ranked = [];
|
|
2091
|
+
} finally {
|
|
2092
|
+
if (store !== void 0) indexStorePool.release(store);
|
|
2093
|
+
}
|
|
2094
|
+
if (ranked.length === 0) return generateFallbackRepoMap(opts);
|
|
2095
|
+
const relativeOf = (file) => {
|
|
2096
|
+
const relative12 = path6.relative(projectRoot, file);
|
|
2097
|
+
if (!relative12 || relative12.startsWith("..")) return posixIndexPath(file);
|
|
2098
|
+
return posixIndexPath(relative12);
|
|
2099
|
+
};
|
|
2100
|
+
const clusters = buildClusters(ranked, relativeOf, packageCounts);
|
|
2101
|
+
const header = renderHeader(clusters, ranked, relativeOf, charBudget * HEADER_BUDGET_SHARE);
|
|
2102
|
+
const focus = (opts.focusFiles ?? []).map((f) => path6.resolve(projectRoot, f));
|
|
2103
|
+
const focusIndex = new Map(focus.map((file, i) => [file, i]));
|
|
2104
|
+
const candidates = ranked.map((row) => ({
|
|
2105
|
+
row,
|
|
2106
|
+
absolute: path6.resolve(projectRoot, row.file)
|
|
2107
|
+
})).sort((a, b) => {
|
|
2108
|
+
const fa = focusIndex.get(a.absolute) ?? Number.POSITIVE_INFINITY;
|
|
2109
|
+
const fb = focusIndex.get(b.absolute) ?? Number.POSITIVE_INFINITY;
|
|
2110
|
+
if (fa !== fb) return fa - fb;
|
|
2111
|
+
return b.row.rank - a.row.rank;
|
|
2112
|
+
}).map(({ row, absolute }) => ({ absolute, relative: relativeOf(row.file) }));
|
|
2113
|
+
const known = new Set(candidates.map((c) => c.absolute));
|
|
2114
|
+
const missingFocus = focus.filter((file) => !known.has(file)).map((file) => ({ absolute: file, relative: relativeOf(file) }));
|
|
2115
|
+
const bodyBudget = Math.max(0, charBudget - header.length - 2);
|
|
2116
|
+
const { sections, files } = await renderSkeletonSections(
|
|
2117
|
+
[...missingFocus, ...candidates],
|
|
2118
|
+
bodyBudget,
|
|
2119
|
+
{
|
|
2120
|
+
skeleton: opts.options,
|
|
2121
|
+
maxSectionChars: Math.max(MIN_SECTION_CHARS, bodyBudget / TARGET_BODY_FILES),
|
|
2122
|
+
signaturesOnly: true
|
|
2123
|
+
}
|
|
2124
|
+
);
|
|
2125
|
+
const map = [header, ...sections].join("\n\n");
|
|
1970
2126
|
return {
|
|
1971
2127
|
map,
|
|
1972
|
-
filesCount:
|
|
1973
|
-
totalFilesScanned:
|
|
1974
|
-
estimatedTokens,
|
|
1975
|
-
rankedFiles:
|
|
2128
|
+
filesCount: files.length,
|
|
2129
|
+
totalFilesScanned: totalRankedFiles,
|
|
2130
|
+
estimatedTokens: Math.ceil(map.length / 4),
|
|
2131
|
+
rankedFiles: files
|
|
1976
2132
|
};
|
|
1977
2133
|
}
|
|
1978
2134
|
|
|
@@ -1984,8 +2140,8 @@ var codebaseRepoMapTool = {
|
|
|
1984
2140
|
permission: "auto",
|
|
1985
2141
|
mutating: false,
|
|
1986
2142
|
capabilities: ["fs.read"],
|
|
1987
|
-
description: "Generate a
|
|
1988
|
-
usageHint: 'USE AT THE START OF COMPLEX OR REPOSITORY-WIDE TASKS:\n\n- Call with default parameters to get a global architecture map within ~1200 tokens.\n- Pass `focusFiles: ["src/auth.ts"]` to
|
|
2143
|
+
description: "Generate a centrality-ranked, token-budgeted Repository Map of the codebase (~1200 token default). Use it for orientation before a cross-file change. Ranking comes from the index's reference graph (PageRank over calls, imports, type references and inheritance), not from filenames, so the map opens with the package clusters and their hub files, then the repository-wide hotspots, then the signatures of the most central files. Use this at the beginning of tasks or when navigating unfamiliar repositories to get a bird-eye view of the architecture.",
|
|
2144
|
+
usageHint: 'USE AT THE START OF COMPLEX OR REPOSITORY-WIDE TASKS:\n\n- Call with default parameters to get a global architecture map within ~1200 tokens.\n- The `1.00 = most central` scores are relative to this repository only; never compare them across projects.\n- Pass `focusFiles: ["src/auth.ts"]` to put specific files at the head of the map alongside the central ones.\n- Use the returned line numbers (e.g. `/* L32-L45 */`) to navigate or partially read only the functions you need.\n- Falls back to a filename heuristic when the index has not been built yet; run `/codebase-reindex` to get the ranked map.',
|
|
1989
2145
|
inputSchema: {
|
|
1990
2146
|
type: "object",
|
|
1991
2147
|
properties: {
|
|
@@ -2007,7 +2163,10 @@ var codebaseRepoMapTool = {
|
|
|
2007
2163
|
const result = await generateRepoMap({
|
|
2008
2164
|
projectRoot,
|
|
2009
2165
|
maxTokens: input.maxTokens,
|
|
2010
|
-
focusFiles: input.focusFiles
|
|
2166
|
+
focusFiles: input.focusFiles,
|
|
2167
|
+
// Honour a caller-supplied index location so the map reads the same
|
|
2168
|
+
// index the other codebase-* tools do.
|
|
2169
|
+
indexDir: codebaseIndexDirOverride(ctx)
|
|
2011
2170
|
});
|
|
2012
2171
|
return {
|
|
2013
2172
|
status: "ok",
|
|
@@ -2028,7 +2187,7 @@ var codebaseRepoMapTool = {
|
|
|
2028
2187
|
};
|
|
2029
2188
|
|
|
2030
2189
|
// src/codebase-index/codebase-skeleton-tool.ts
|
|
2031
|
-
import * as
|
|
2190
|
+
import * as fs6 from "node:fs/promises";
|
|
2032
2191
|
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
2033
2192
|
var codebaseSkeletonTool = {
|
|
2034
2193
|
name: "codebase-skeleton",
|
|
@@ -2073,7 +2232,7 @@ var codebaseSkeletonTool = {
|
|
|
2073
2232
|
const targetPath = await safeResolveProjectPath(input.path, ctx);
|
|
2074
2233
|
let stat3;
|
|
2075
2234
|
try {
|
|
2076
|
-
stat3 = await
|
|
2235
|
+
stat3 = await fs6.stat(targetPath);
|
|
2077
2236
|
} catch (err) {
|
|
2078
2237
|
throw new Error(
|
|
2079
2238
|
`codebase-skeleton: file or directory not found at "${input.path}": ${toErrorMessage8(err)}`
|
|
@@ -2109,7 +2268,7 @@ var codebaseSkeletonTool = {
|
|
|
2109
2268
|
files: dirResult.files
|
|
2110
2269
|
};
|
|
2111
2270
|
}
|
|
2112
|
-
const content = await
|
|
2271
|
+
const content = await fs6.readFile(targetPath, "utf8");
|
|
2113
2272
|
const fileResult = await extractFileSkeleton({
|
|
2114
2273
|
file: targetPath,
|
|
2115
2274
|
content,
|
|
@@ -2215,28 +2374,28 @@ var codebaseStatsTool = {
|
|
|
2215
2374
|
};
|
|
2216
2375
|
|
|
2217
2376
|
// src/codebase-index/codebase-targeted-test-tool.ts
|
|
2218
|
-
import * as
|
|
2219
|
-
import * as
|
|
2377
|
+
import * as fs7 from "node:fs/promises";
|
|
2378
|
+
import * as path7 from "node:path";
|
|
2220
2379
|
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
2221
2380
|
var TEST_FILE_REGEX2 = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
|
|
2222
2381
|
async function findConventionTestFiles(projectRoot, sourceRelPath) {
|
|
2223
|
-
const parsed =
|
|
2382
|
+
const parsed = path7.parse(sourceRelPath);
|
|
2224
2383
|
const candidates = [
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2384
|
+
path7.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
|
|
2385
|
+
path7.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
|
|
2386
|
+
path7.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
|
|
2387
|
+
path7.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
|
|
2388
|
+
path7.join("tests", `${parsed.name}.test${parsed.ext}`),
|
|
2389
|
+
path7.join("tests", `${parsed.name}.spec${parsed.ext}`),
|
|
2390
|
+
path7.join("test", `${parsed.name}.test${parsed.ext}`),
|
|
2391
|
+
path7.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
|
|
2392
|
+
path7.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
|
|
2234
2393
|
];
|
|
2235
2394
|
const found = [];
|
|
2236
2395
|
for (const cand of candidates) {
|
|
2237
|
-
const absPath =
|
|
2396
|
+
const absPath = path7.resolve(projectRoot, cand);
|
|
2238
2397
|
try {
|
|
2239
|
-
const st = await
|
|
2398
|
+
const st = await fs7.stat(absPath);
|
|
2240
2399
|
if (st.isFile()) {
|
|
2241
2400
|
found.push(cand.replace(/\\/g, "/"));
|
|
2242
2401
|
}
|
|
@@ -2293,7 +2452,7 @@ var codebaseTargetedTestTool = {
|
|
|
2293
2452
|
transitive: true
|
|
2294
2453
|
});
|
|
2295
2454
|
for (const site of serviced.calls) {
|
|
2296
|
-
const relPath =
|
|
2455
|
+
const relPath = path7.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
|
|
2297
2456
|
if (TEST_FILE_REGEX2.test(relPath)) {
|
|
2298
2457
|
suitesSet.add(relPath);
|
|
2299
2458
|
}
|
|
@@ -2377,8 +2536,8 @@ ${streamRes.stderr}`.trim(),
|
|
|
2377
2536
|
};
|
|
2378
2537
|
|
|
2379
2538
|
// src/codebase-index/dead-code-scan.ts
|
|
2380
|
-
import * as
|
|
2381
|
-
import * as
|
|
2539
|
+
import * as fs8 from "node:fs";
|
|
2540
|
+
import * as path8 from "node:path";
|
|
2382
2541
|
var deadCodeScanTool = {
|
|
2383
2542
|
name: "dead-code-scan",
|
|
2384
2543
|
category: "Project",
|
|
@@ -2423,25 +2582,25 @@ var deadCodeScanTool = {
|
|
|
2423
2582
|
};
|
|
2424
2583
|
function tryReadJson(filePath) {
|
|
2425
2584
|
try {
|
|
2426
|
-
const raw =
|
|
2585
|
+
const raw = fs8.readFileSync(filePath, "utf8");
|
|
2427
2586
|
return JSON.parse(raw);
|
|
2428
2587
|
} catch {
|
|
2429
2588
|
return null;
|
|
2430
2589
|
}
|
|
2431
2590
|
}
|
|
2432
|
-
function resolveAgainst(base,
|
|
2433
|
-
if (
|
|
2434
|
-
return
|
|
2591
|
+
function resolveAgainst(base, relative12) {
|
|
2592
|
+
if (path8.isAbsolute(relative12)) return relative12;
|
|
2593
|
+
return path8.resolve(base, relative12);
|
|
2435
2594
|
}
|
|
2436
2595
|
function discoverEntryPoints(projectRoot, userEntryPoints) {
|
|
2437
2596
|
const entries = /* @__PURE__ */ new Set();
|
|
2438
2597
|
if (userEntryPoints) {
|
|
2439
2598
|
for (const ep of userEntryPoints) {
|
|
2440
2599
|
const resolved = resolveAgainst(projectRoot, ep);
|
|
2441
|
-
if (
|
|
2600
|
+
if (fs8.existsSync(resolved)) entries.add(resolved);
|
|
2442
2601
|
}
|
|
2443
2602
|
}
|
|
2444
|
-
const rootPkg = tryReadJson(
|
|
2603
|
+
const rootPkg = tryReadJson(path8.join(projectRoot, "package.json"));
|
|
2445
2604
|
if (rootPkg) {
|
|
2446
2605
|
addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
|
|
2447
2606
|
}
|
|
@@ -2455,45 +2614,45 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
|
|
|
2455
2614
|
workspaces = [];
|
|
2456
2615
|
}
|
|
2457
2616
|
for (const wsDir of workspaces) {
|
|
2458
|
-
const pkgJsonPath =
|
|
2617
|
+
const pkgJsonPath = path8.join(wsDir, "package.json");
|
|
2459
2618
|
const pkg = tryReadJson(pkgJsonPath);
|
|
2460
2619
|
if (pkg) {
|
|
2461
2620
|
addPkgJsonEntryPoints(wsDir, pkg, entries);
|
|
2462
|
-
const convention1 =
|
|
2463
|
-
if (
|
|
2464
|
-
const convention2 =
|
|
2465
|
-
if (
|
|
2466
|
-
const convention3 =
|
|
2467
|
-
if (
|
|
2621
|
+
const convention1 = path8.join(wsDir, "src", "index.ts");
|
|
2622
|
+
if (fs8.existsSync(convention1)) entries.add(convention1);
|
|
2623
|
+
const convention2 = path8.join(wsDir, "src", "main.ts");
|
|
2624
|
+
if (fs8.existsSync(convention2)) entries.add(convention2);
|
|
2625
|
+
const convention3 = path8.join(wsDir, "index.ts");
|
|
2626
|
+
if (fs8.existsSync(convention3)) entries.add(convention3);
|
|
2468
2627
|
}
|
|
2469
2628
|
}
|
|
2470
2629
|
if (!rootPkg || !workspaces.length) {
|
|
2471
2630
|
for (const name of ["src/index.ts", "src/main.ts", "index.ts"]) {
|
|
2472
|
-
const convention =
|
|
2473
|
-
if (
|
|
2631
|
+
const convention = path8.join(projectRoot, name);
|
|
2632
|
+
if (fs8.existsSync(convention)) entries.add(convention);
|
|
2474
2633
|
}
|
|
2475
2634
|
}
|
|
2476
2635
|
return [...entries];
|
|
2477
2636
|
}
|
|
2478
2637
|
var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
|
|
2479
|
-
var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${
|
|
2638
|
+
var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path8.sep}${d}${path8.sep}`);
|
|
2480
2639
|
function trySourceEquivalent(resolved) {
|
|
2481
|
-
resolved = resolved.replace(/[/\\]/g,
|
|
2640
|
+
resolved = resolved.replace(/[/\\]/g, path8.sep);
|
|
2482
2641
|
for (const marker of BUILD_OUTPUT_DIR_NAMES) {
|
|
2483
2642
|
const idx = resolved.indexOf(marker);
|
|
2484
2643
|
if (idx === -1) continue;
|
|
2485
|
-
const base = resolved.replace(marker, `${
|
|
2644
|
+
const base = resolved.replace(marker, `${path8.sep}src${path8.sep}`);
|
|
2486
2645
|
const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
|
|
2487
|
-
if (candidate !== base &&
|
|
2646
|
+
if (candidate !== base && fs8.existsSync(candidate)) {
|
|
2488
2647
|
return candidate;
|
|
2489
2648
|
}
|
|
2490
2649
|
const dtsStripped = base.replace(/\.d\.ts$/, "");
|
|
2491
2650
|
const candidateDts = dtsStripped + ".ts";
|
|
2492
|
-
if (candidateDts !== base && candidateDts !== candidate &&
|
|
2651
|
+
if (candidateDts !== base && candidateDts !== candidate && fs8.existsSync(candidateDts)) {
|
|
2493
2652
|
return candidateDts;
|
|
2494
2653
|
}
|
|
2495
2654
|
const candidateNoExt = base + ".ts";
|
|
2496
|
-
if (candidate !== candidateNoExt && candidateNoExt !== candidateDts &&
|
|
2655
|
+
if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs8.existsSync(candidateNoExt)) {
|
|
2497
2656
|
return candidateNoExt;
|
|
2498
2657
|
}
|
|
2499
2658
|
}
|
|
@@ -2501,9 +2660,9 @@ function trySourceEquivalent(resolved) {
|
|
|
2501
2660
|
}
|
|
2502
2661
|
function tryAddEntryPath(pkgDir, rawPath, entries) {
|
|
2503
2662
|
const resolved = resolveAgainst(pkgDir, rawPath);
|
|
2504
|
-
if (
|
|
2663
|
+
if (fs8.existsSync(resolved)) entries.add(resolved);
|
|
2505
2664
|
const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
|
|
2506
|
-
if (tsResolved !== resolved &&
|
|
2665
|
+
if (tsResolved !== resolved && fs8.existsSync(tsResolved)) {
|
|
2507
2666
|
entries.add(tsResolved);
|
|
2508
2667
|
}
|
|
2509
2668
|
const srcAlt = trySourceEquivalent(resolved);
|
|
@@ -2547,18 +2706,18 @@ function expandGlobPattern(entry, projectRoot) {
|
|
|
2547
2706
|
const dirs = [];
|
|
2548
2707
|
if (entry.includes("*")) {
|
|
2549
2708
|
const base = entry.replace(/\/\*+$/, "");
|
|
2550
|
-
const baseDir =
|
|
2709
|
+
const baseDir = path8.resolve(projectRoot, base);
|
|
2551
2710
|
try {
|
|
2552
|
-
const children =
|
|
2711
|
+
const children = fs8.readdirSync(baseDir, { withFileTypes: true });
|
|
2553
2712
|
for (const child of children) {
|
|
2554
2713
|
if (child.isDirectory()) {
|
|
2555
|
-
dirs.push(
|
|
2714
|
+
dirs.push(path8.join(baseDir, child.name));
|
|
2556
2715
|
}
|
|
2557
2716
|
}
|
|
2558
2717
|
} catch {
|
|
2559
2718
|
}
|
|
2560
2719
|
} else {
|
|
2561
|
-
dirs.push(
|
|
2720
|
+
dirs.push(path8.resolve(projectRoot, entry));
|
|
2562
2721
|
}
|
|
2563
2722
|
return dirs;
|
|
2564
2723
|
}
|
|
@@ -2575,10 +2734,10 @@ function extractWorkspaceGlobs(pkg, projectRoot) {
|
|
|
2575
2734
|
return dirs;
|
|
2576
2735
|
}
|
|
2577
2736
|
function extractPnpmWorkspaceDirs(projectRoot) {
|
|
2578
|
-
const yamlPath =
|
|
2579
|
-
if (!
|
|
2737
|
+
const yamlPath = path8.join(projectRoot, "pnpm-workspace.yaml");
|
|
2738
|
+
if (!fs8.existsSync(yamlPath)) return [];
|
|
2580
2739
|
try {
|
|
2581
|
-
const content =
|
|
2740
|
+
const content = fs8.readFileSync(yamlPath, "utf8");
|
|
2582
2741
|
const dirs = [];
|
|
2583
2742
|
let inPackages = false;
|
|
2584
2743
|
const lines = content.split("\n");
|
|
@@ -2612,8 +2771,8 @@ function extractPnpmWorkspaceDirs(projectRoot) {
|
|
|
2612
2771
|
}
|
|
2613
2772
|
function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
|
|
2614
2773
|
if (!moduleSpecifier.startsWith(".")) return [];
|
|
2615
|
-
const dir =
|
|
2616
|
-
const base =
|
|
2774
|
+
const dir = path8.dirname(importerPath);
|
|
2775
|
+
const base = path8.resolve(dir, moduleSpecifier);
|
|
2617
2776
|
const results = [];
|
|
2618
2777
|
const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
2619
2778
|
const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
|
|
@@ -2625,18 +2784,18 @@ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
|
|
|
2625
2784
|
if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
|
|
2626
2785
|
if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
|
|
2627
2786
|
if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
|
|
2628
|
-
if (indexedFiles.has(
|
|
2629
|
-
results.push(
|
|
2630
|
-
if (indexedFiles.has(
|
|
2631
|
-
results.push(
|
|
2632
|
-
if (indexedFiles.has(
|
|
2633
|
-
results.push(
|
|
2634
|
-
if (indexedFiles.has(
|
|
2635
|
-
results.push(
|
|
2636
|
-
if (indexedFiles.has(
|
|
2637
|
-
results.push(
|
|
2638
|
-
if (indexedFiles.has(
|
|
2639
|
-
results.push(
|
|
2787
|
+
if (indexedFiles.has(path8.join(candidate, "index.ts")))
|
|
2788
|
+
results.push(path8.join(candidate, "index.ts"));
|
|
2789
|
+
if (indexedFiles.has(path8.join(candidate, "index.tsx")))
|
|
2790
|
+
results.push(path8.join(candidate, "index.tsx"));
|
|
2791
|
+
if (indexedFiles.has(path8.join(candidate, "index.js")))
|
|
2792
|
+
results.push(path8.join(candidate, "index.js"));
|
|
2793
|
+
if (indexedFiles.has(path8.join(candidate, "index.jsx")))
|
|
2794
|
+
results.push(path8.join(candidate, "index.jsx"));
|
|
2795
|
+
if (indexedFiles.has(path8.join(candidate, "index.mjs")))
|
|
2796
|
+
results.push(path8.join(candidate, "index.mjs"));
|
|
2797
|
+
if (indexedFiles.has(path8.join(candidate, "index.cjs")))
|
|
2798
|
+
results.push(path8.join(candidate, "index.cjs"));
|
|
2640
2799
|
}
|
|
2641
2800
|
return [...new Set(results)];
|
|
2642
2801
|
}
|
|
@@ -2668,7 +2827,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2668
2827
|
symbolById.set(s.id, s);
|
|
2669
2828
|
}
|
|
2670
2829
|
const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
|
|
2671
|
-
const entryFileSet = new Set(discoveredFiles.map((f) =>
|
|
2830
|
+
const entryFileSet = new Set(discoveredFiles.map((f) => path8.resolve(f)));
|
|
2672
2831
|
const indexedFiles = /* @__PURE__ */ new Set();
|
|
2673
2832
|
for (const s of allSymbols) indexedFiles.add(s.file);
|
|
2674
2833
|
for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
|
|
@@ -2694,7 +2853,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2694
2853
|
if (scannedBarrels.has(epFile)) continue;
|
|
2695
2854
|
scannedBarrels.add(epFile);
|
|
2696
2855
|
try {
|
|
2697
|
-
const content =
|
|
2856
|
+
const content = fs8.readFileSync(epFile, "utf8");
|
|
2698
2857
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
2699
2858
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
2700
2859
|
for (; ; ) {
|
|
@@ -2784,7 +2943,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2784
2943
|
const deadPackages = [];
|
|
2785
2944
|
const pkgEntries = findPackageEntries(projectRoot);
|
|
2786
2945
|
for (const [pkgName, pkgDir] of pkgEntries) {
|
|
2787
|
-
const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir +
|
|
2946
|
+
const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path8.sep));
|
|
2788
2947
|
if (pkgFiles.length === 0) continue;
|
|
2789
2948
|
const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
|
|
2790
2949
|
if (pkgUsed.length === 0) {
|
|
@@ -2820,7 +2979,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2820
2979
|
}
|
|
2821
2980
|
function findPackageEntries(projectRoot) {
|
|
2822
2981
|
const pkgMap = /* @__PURE__ */ new Map();
|
|
2823
|
-
const rootPkg = tryReadJson(
|
|
2982
|
+
const rootPkg = tryReadJson(path8.join(projectRoot, "package.json"));
|
|
2824
2983
|
if (rootPkg && typeof rootPkg.name === "string") {
|
|
2825
2984
|
pkgMap.set(rootPkg.name, projectRoot);
|
|
2826
2985
|
}
|
|
@@ -2830,7 +2989,7 @@ function findPackageEntries(projectRoot) {
|
|
|
2830
2989
|
wsDirs = extractPnpmWorkspaceDirs(projectRoot);
|
|
2831
2990
|
}
|
|
2832
2991
|
for (const wsDir of wsDirs) {
|
|
2833
|
-
const wsPkg = tryReadJson(
|
|
2992
|
+
const wsPkg = tryReadJson(path8.join(wsDir, "package.json"));
|
|
2834
2993
|
if (wsPkg && typeof wsPkg.name === "string") {
|
|
2835
2994
|
pkgMap.set(wsPkg.name, wsDir);
|
|
2836
2995
|
}
|
|
@@ -2839,16 +2998,997 @@ function findPackageEntries(projectRoot) {
|
|
|
2839
2998
|
return pkgMap;
|
|
2840
2999
|
}
|
|
2841
3000
|
|
|
3001
|
+
// src/codebase-index/atlas-brief.ts
|
|
3002
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
3003
|
+
import * as path10 from "node:path";
|
|
3004
|
+
|
|
3005
|
+
// src/codebase-index/atlas-projection.ts
|
|
3006
|
+
import { createHash } from "node:crypto";
|
|
3007
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
3008
|
+
import * as fs9 from "node:fs/promises";
|
|
3009
|
+
import * as path9 from "node:path";
|
|
3010
|
+
|
|
3011
|
+
// src/codebase-index/atlas-export.ts
|
|
3012
|
+
var EXPORT_PACKAGE_LIMIT = 40;
|
|
3013
|
+
var EXPORT_FILE_LIMIT = 120;
|
|
3014
|
+
var CANVAS = { width: 1e3, height: 700 };
|
|
3015
|
+
var RADIUS = { min: 14, max: 46 };
|
|
3016
|
+
var ESCAPES = {
|
|
3017
|
+
"&": "&",
|
|
3018
|
+
"<": "<",
|
|
3019
|
+
">": ">",
|
|
3020
|
+
'"': """,
|
|
3021
|
+
"'": "'"
|
|
3022
|
+
};
|
|
3023
|
+
function escapeHtml(value) {
|
|
3024
|
+
return value.replace(/[&<>"']/g, (character) => ESCAPES[character] ?? character);
|
|
3025
|
+
}
|
|
3026
|
+
function inlineJson(value) {
|
|
3027
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
3028
|
+
}
|
|
3029
|
+
function placePackages(packages) {
|
|
3030
|
+
const shown = packages.slice(0, EXPORT_PACKAGE_LIMIT);
|
|
3031
|
+
if (shown.length === 0) return [];
|
|
3032
|
+
const maxRank = shown.reduce((max, pkg) => Math.max(max, pkg.rank), 0) || 1;
|
|
3033
|
+
const centreX = CANVAS.width / 2;
|
|
3034
|
+
const centreY = CANVAS.height / 2;
|
|
3035
|
+
const placed = [];
|
|
3036
|
+
let index = 0;
|
|
3037
|
+
let ring = 0;
|
|
3038
|
+
while (index < shown.length) {
|
|
3039
|
+
const capacity = ring === 0 ? 1 : Math.min(ring * 6, shown.length - index);
|
|
3040
|
+
const radius = ring === 0 ? 0 : ring * 115;
|
|
3041
|
+
for (let slot = 0; slot < capacity && index < shown.length; slot++, index++) {
|
|
3042
|
+
const pkg = shown[index];
|
|
3043
|
+
const angle = slot / capacity * Math.PI * 2 - Math.PI / 2;
|
|
3044
|
+
placed.push({
|
|
3045
|
+
package: pkg,
|
|
3046
|
+
x: centreX + Math.cos(angle) * radius,
|
|
3047
|
+
y: centreY + Math.sin(angle) * radius * 0.68,
|
|
3048
|
+
radius: RADIUS.min + pkg.rank / maxRank * (RADIUS.max - RADIUS.min)
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
ring++;
|
|
3052
|
+
}
|
|
3053
|
+
return placed;
|
|
3054
|
+
}
|
|
3055
|
+
var unit = (value) => value.toFixed(1);
|
|
3056
|
+
function renderSvg(document) {
|
|
3057
|
+
const placed = placePackages(document.packages);
|
|
3058
|
+
if (placed.length === 0) {
|
|
3059
|
+
return '<p class="empty">No ranked packages to draw.</p>';
|
|
3060
|
+
}
|
|
3061
|
+
const at = new Map(placed.map((entry) => [entry.package.name, entry]));
|
|
3062
|
+
const maxWeight = document.edges.reduce((max, edge) => Math.max(max, edge.weight), 0) || 1;
|
|
3063
|
+
const lines = [
|
|
3064
|
+
`<svg viewBox="0 0 ${CANVAS.width} ${CANVAS.height}" role="img" aria-label="Package dependency map">`
|
|
3065
|
+
];
|
|
3066
|
+
for (const edge of document.edges) {
|
|
3067
|
+
const from = at.get(edge.from);
|
|
3068
|
+
const to = at.get(edge.to);
|
|
3069
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
3070
|
+
const strength = Math.min(1, edge.weight / maxWeight);
|
|
3071
|
+
lines.push(
|
|
3072
|
+
`<line x1="${unit(from.x)}" y1="${unit(from.y)}" x2="${unit(to.x)}" y2="${unit(to.y)}" stroke-width="${(0.5 + strength * 2.5).toFixed(2)}" stroke-opacity="${(0.12 + strength * 0.4).toFixed(2)}" />`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
for (const entry of placed) {
|
|
3076
|
+
const name = escapeHtml(entry.package.name);
|
|
3077
|
+
const summary = entry.package.summary ?? "";
|
|
3078
|
+
const title = escapeHtml(
|
|
3079
|
+
`${entry.package.name} \u2014 ${entry.package.files} files, rank ${entry.package.rank.toFixed(4)}${summary === "" ? "" : `
|
|
3080
|
+
${summary}`}`
|
|
3081
|
+
);
|
|
3082
|
+
lines.push(
|
|
3083
|
+
`<g class="pkg" data-package="${name}" tabindex="0">`,
|
|
3084
|
+
`<title>${title}</title>`,
|
|
3085
|
+
`<circle cx="${unit(entry.x)}" cy="${unit(entry.y)}" r="${unit(entry.radius)}" />`,
|
|
3086
|
+
`<text x="${unit(entry.x)}" y="${unit(entry.y + entry.radius + 13)}">${name}</text>`,
|
|
3087
|
+
"</g>"
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
lines.push("</svg>");
|
|
3091
|
+
return lines.join("\n");
|
|
3092
|
+
}
|
|
3093
|
+
function renderFileRows(document) {
|
|
3094
|
+
return document.files.slice(0, EXPORT_FILE_LIMIT).map((file) => {
|
|
3095
|
+
const declarations = file.symbols.slice(0, 4).map((symbol) => escapeHtml(symbol.name)).join(", ");
|
|
3096
|
+
const description = file.concept ?? declarations;
|
|
3097
|
+
return `<tr data-package="${escapeHtml(file.package)}"><td class="num">${file.rank.toFixed(4)}</td><td class="path">${escapeHtml(file.path)}</td><td class="num">${file.inDeg}</td><td class="num">${file.outDeg}</td><td class="desc">${escapeHtml(description)}</td></tr>`;
|
|
3098
|
+
}).join("\n");
|
|
3099
|
+
}
|
|
3100
|
+
function renderAtlasHtml(document, options = {}) {
|
|
3101
|
+
const name = options.projectName ?? "Codebase";
|
|
3102
|
+
const described = document.packages.filter((pkg) => pkg.summary !== void 0);
|
|
3103
|
+
return `<!doctype html>
|
|
3104
|
+
<html lang="en">
|
|
3105
|
+
<head>
|
|
3106
|
+
<meta charset="utf-8" />
|
|
3107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3108
|
+
<title>${escapeHtml(name)} \u2014 Codebase Atlas</title>
|
|
3109
|
+
<style>
|
|
3110
|
+
:root {
|
|
3111
|
+
color-scheme: light dark;
|
|
3112
|
+
--bg: #ffffff; --fg: #16181d; --muted: #61656e; --line: #d9dce2;
|
|
3113
|
+
--accent: #3b5bdb; --panel: #f6f7f9;
|
|
3114
|
+
}
|
|
3115
|
+
@media (prefers-color-scheme: dark) {
|
|
3116
|
+
:root { --bg: #101216; --fg: #e6e8ec; --muted: #9aa0ab; --line: #2a2e36;
|
|
3117
|
+
--accent: #7c93f5; --panel: #171a20; }
|
|
3118
|
+
}
|
|
3119
|
+
* { box-sizing: border-box; }
|
|
3120
|
+
body { margin: 0; background: var(--bg); color: var(--fg);
|
|
3121
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
|
3122
|
+
header { padding: 24px 28px 12px; border-bottom: 1px solid var(--line); }
|
|
3123
|
+
h1 { margin: 0 0 4px; font-size: 20px; }
|
|
3124
|
+
h2 { margin: 28px 0 10px; font-size: 15px; text-transform: uppercase;
|
|
3125
|
+
letter-spacing: .12em; color: var(--muted); }
|
|
3126
|
+
main { padding: 8px 28px 48px; max-width: 1180px; margin: 0 auto; }
|
|
3127
|
+
.counts { color: var(--muted); font-size: 12px; }
|
|
3128
|
+
figure { margin: 16px 0 0; border: 1px solid var(--line); background: var(--panel); }
|
|
3129
|
+
svg { display: block; width: 100%; height: auto; }
|
|
3130
|
+
svg line { stroke: var(--accent); }
|
|
3131
|
+
svg circle { fill: var(--accent); fill-opacity: .22; stroke: var(--accent); stroke-width: 1.5; }
|
|
3132
|
+
svg text { fill: var(--fg); font-size: 10px; text-anchor: middle; font-family: ui-monospace, monospace; }
|
|
3133
|
+
svg .pkg { cursor: pointer; }
|
|
3134
|
+
svg .pkg:hover circle, svg .pkg:focus circle { fill-opacity: .5; outline: none; }
|
|
3135
|
+
svg .pkg.muted { opacity: .25; }
|
|
3136
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
3137
|
+
th, td { padding: 5px 8px; border-bottom: 1px solid var(--line); text-align: left;
|
|
3138
|
+
vertical-align: top; }
|
|
3139
|
+
th { color: var(--muted); font-weight: 600; font-size: 11px; text-transform: uppercase;
|
|
3140
|
+
letter-spacing: .08em; }
|
|
3141
|
+
td.num { text-align: right; font-family: ui-monospace, monospace; white-space: nowrap; }
|
|
3142
|
+
td.path { font-family: ui-monospace, monospace; }
|
|
3143
|
+
td.desc { color: var(--muted); }
|
|
3144
|
+
tr[hidden] { display: none; }
|
|
3145
|
+
ul.subsystems { list-style: none; padding: 0; margin: 0; }
|
|
3146
|
+
ul.subsystems li { padding: 6px 0; border-bottom: 1px solid var(--line); }
|
|
3147
|
+
ul.subsystems b { font-family: ui-monospace, monospace; }
|
|
3148
|
+
.filter { margin: 10px 0 0; font-size: 12px; color: var(--muted); }
|
|
3149
|
+
.filter button { font: inherit; color: var(--accent); background: none; border: 0;
|
|
3150
|
+
cursor: pointer; padding: 0; text-decoration: underline; }
|
|
3151
|
+
.empty { color: var(--muted); }
|
|
3152
|
+
.wrap { overflow-x: auto; }
|
|
3153
|
+
footer { color: var(--muted); font-size: 11px; padding: 0 28px 32px; max-width: 1180px;
|
|
3154
|
+
margin: 0 auto; }
|
|
3155
|
+
</style>
|
|
3156
|
+
</head>
|
|
3157
|
+
<body>
|
|
3158
|
+
<header>
|
|
3159
|
+
<h1>${escapeHtml(name)} \u2014 Codebase Atlas</h1>
|
|
3160
|
+
<p class="counts">${document.counts.files} files \xB7 ${document.counts.symbols} symbols \xB7 ${document.counts.packages} packages \xB7 ranking is PageRank over the reference graph, relative to this repository</p>
|
|
3161
|
+
</header>
|
|
3162
|
+
<main>
|
|
3163
|
+
<h2>Package map</h2>
|
|
3164
|
+
<figure>${renderSvg(document)}</figure>
|
|
3165
|
+
<p class="filter" id="filter">Click a package to filter the table. <button type="button" id="clear" hidden>Clear filter</button></p>
|
|
3166
|
+
${described.length === 0 ? "" : ` <h2>Subsystems</h2>
|
|
3167
|
+
<ul class="subsystems">
|
|
3168
|
+
${described.map((pkg) => ` <li><b>${escapeHtml(pkg.name)}</b> \u2014 ${escapeHtml(pkg.summary ?? "")}</li>`).join("\n")}
|
|
3169
|
+
</ul>
|
|
3170
|
+
`} <h2>Most central files</h2>
|
|
3171
|
+
<div class="wrap">
|
|
3172
|
+
<table>
|
|
3173
|
+
<thead><tr><th>Rank</th><th>File</th><th>In</th><th>Out</th><th>What it is</th></tr></thead>
|
|
3174
|
+
<tbody id="files">
|
|
3175
|
+
${renderFileRows(document)}
|
|
3176
|
+
</tbody>
|
|
3177
|
+
</table>
|
|
3178
|
+
</div>
|
|
3179
|
+
</main>
|
|
3180
|
+
<footer>Generated from the codebase index by <code>/codebase-map --export</code>. Ranks are relative to this repository and mean nothing across repositories.</footer>
|
|
3181
|
+
<script id="atlas" type="application/json">${inlineJson({
|
|
3182
|
+
counts: document.counts,
|
|
3183
|
+
packages: document.packages,
|
|
3184
|
+
edges: document.edges
|
|
3185
|
+
})}</script>
|
|
3186
|
+
<script>
|
|
3187
|
+
(function () {
|
|
3188
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('#files tr'));
|
|
3189
|
+
var groups = Array.prototype.slice.call(document.querySelectorAll('svg .pkg'));
|
|
3190
|
+
var clear = document.getElementById('clear');
|
|
3191
|
+
var active = null;
|
|
3192
|
+
|
|
3193
|
+
function apply(name) {
|
|
3194
|
+
active = name;
|
|
3195
|
+
rows.forEach(function (row) {
|
|
3196
|
+
row.hidden = name !== null && row.getAttribute('data-package') !== name;
|
|
3197
|
+
});
|
|
3198
|
+
groups.forEach(function (group) {
|
|
3199
|
+
group.classList.toggle('muted', name !== null && group.getAttribute('data-package') !== name);
|
|
3200
|
+
});
|
|
3201
|
+
clear.hidden = name === null;
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
groups.forEach(function (group) {
|
|
3205
|
+
var name = group.getAttribute('data-package');
|
|
3206
|
+
function toggle() { apply(active === name ? null : name); }
|
|
3207
|
+
group.addEventListener('click', toggle);
|
|
3208
|
+
group.addEventListener('keydown', function (event) {
|
|
3209
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); }
|
|
3210
|
+
});
|
|
3211
|
+
});
|
|
3212
|
+
clear.addEventListener('click', function () { apply(null); });
|
|
3213
|
+
})();
|
|
3214
|
+
</script>
|
|
3215
|
+
</body>
|
|
3216
|
+
</html>
|
|
3217
|
+
`;
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
// src/codebase-index/atlas-projection.ts
|
|
3221
|
+
var ATLAS_DIR = path9.join(".wrongstack", "atlas");
|
|
3222
|
+
var ATLAS_JSON = "atlas.json";
|
|
3223
|
+
var ATLAS_MARKDOWN = "ATLAS.md";
|
|
3224
|
+
var ATLAS_MANIFEST = "manifest.json";
|
|
3225
|
+
var ATLAS_SCHEMA = 2;
|
|
3226
|
+
var ATLAS_FILE_LIMIT = 300;
|
|
3227
|
+
var SYMBOLS_PER_FILE = 6;
|
|
3228
|
+
var MARKDOWN_FILE_LIMIT = 60;
|
|
3229
|
+
var MARKDOWN_SYMBOLS_PER_FILE = 3;
|
|
3230
|
+
var RANK_PRECISION = 4;
|
|
3231
|
+
function round(value) {
|
|
3232
|
+
return Number(value.toFixed(RANK_PRECISION));
|
|
3233
|
+
}
|
|
3234
|
+
function relativeFactory(projectRoot) {
|
|
3235
|
+
return (file) => {
|
|
3236
|
+
const relative12 = path9.relative(projectRoot, file);
|
|
3237
|
+
if (!relative12 || relative12.startsWith("..")) return posixIndexPath(file);
|
|
3238
|
+
return posixIndexPath(relative12);
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
function digestOf(entries) {
|
|
3242
|
+
const hash = createHash("sha256");
|
|
3243
|
+
for (const [file, contentHash] of [...entries].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
3244
|
+
hash.update(file);
|
|
3245
|
+
hash.update("\0");
|
|
3246
|
+
hash.update(contentHash);
|
|
3247
|
+
hash.update("\n");
|
|
3248
|
+
}
|
|
3249
|
+
return hash.digest("hex");
|
|
3250
|
+
}
|
|
3251
|
+
function renderMarkdown(document) {
|
|
3252
|
+
const lines = [
|
|
3253
|
+
"# Codebase Atlas",
|
|
3254
|
+
"",
|
|
3255
|
+
"Generated from the codebase index. Do not edit by hand \u2014 run `/codebase-map --write`.",
|
|
3256
|
+
"",
|
|
3257
|
+
`- Indexed files: ${document.counts.files}`,
|
|
3258
|
+
`- Indexed symbols: ${document.counts.symbols}`,
|
|
3259
|
+
`- Packages: ${document.counts.packages}`,
|
|
3260
|
+
"",
|
|
3261
|
+
"Ranking is PageRank over the reference graph (calls, imports, type references,",
|
|
3262
|
+
"inheritance). `1.0000` is the most central file in this repository; the scores",
|
|
3263
|
+
"are relative to this repository only.",
|
|
3264
|
+
"",
|
|
3265
|
+
"## Packages",
|
|
3266
|
+
"",
|
|
3267
|
+
"| Package | Files | Hub | Rank |",
|
|
3268
|
+
"| --- | ---: | --- | ---: |"
|
|
3269
|
+
];
|
|
3270
|
+
for (const pkg of document.packages) {
|
|
3271
|
+
lines.push(`| ${pkg.name} | ${pkg.files} | \`${pkg.hub}\` | ${pkg.rank.toFixed(4)} |`);
|
|
3272
|
+
}
|
|
3273
|
+
const described = document.packages.filter((pkg) => pkg.summary !== void 0);
|
|
3274
|
+
if (described.length > 0) {
|
|
3275
|
+
lines.push("", "## Subsystems", "");
|
|
3276
|
+
for (const pkg of described) {
|
|
3277
|
+
lines.push(`- **${pkg.name}** \u2014 ${pkg.summary}`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
lines.push(
|
|
3281
|
+
"",
|
|
3282
|
+
`## Most central files (top ${Math.min(MARKDOWN_FILE_LIMIT, document.files.length)} of ${document.files.length})`,
|
|
3283
|
+
"",
|
|
3284
|
+
`The full ranking, with every declaration, is in \`${ATLAS_JSON}\`.`,
|
|
3285
|
+
"",
|
|
3286
|
+
"| Rank | File | In | Out | Declarations |",
|
|
3287
|
+
"| ---: | --- | ---: | ---: | --- |"
|
|
3288
|
+
);
|
|
3289
|
+
for (const file of document.files.slice(0, MARKDOWN_FILE_LIMIT)) {
|
|
3290
|
+
const declarations = file.symbols.slice(0, MARKDOWN_SYMBOLS_PER_FILE).map((symbol) => `\`${symbol.name}\``).join(", ");
|
|
3291
|
+
lines.push(
|
|
3292
|
+
`| ${file.rank.toFixed(4)} | \`${file.path}\` | ${file.inDeg} | ${file.outDeg} | ${declarations} |`
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
return `${lines.join("\n").trimEnd()}
|
|
3296
|
+
`;
|
|
3297
|
+
}
|
|
3298
|
+
function buildAtlas(store, projectRoot) {
|
|
3299
|
+
const relativeOf = relativeFactory(projectRoot);
|
|
3300
|
+
const stats = store.getStats();
|
|
3301
|
+
const packageCounts = store.getPackageFileCounts();
|
|
3302
|
+
const ranked = store.getRankedFiles(ATLAS_FILE_LIMIT);
|
|
3303
|
+
const concepts = store.getReadyConceptSummaries();
|
|
3304
|
+
const subsystemSummaries = new Map(
|
|
3305
|
+
store.getSubsystems().map((subsystem) => [subsystem.id, subsystem.summary])
|
|
3306
|
+
);
|
|
3307
|
+
const packages = /* @__PURE__ */ new Map();
|
|
3308
|
+
const packageNameOf = (row) => row.package || relativeOf(row.file).split("/")[0] || "(root)";
|
|
3309
|
+
for (const row of ranked) {
|
|
3310
|
+
const relative12 = relativeOf(row.file);
|
|
3311
|
+
const name = packageNameOf(row);
|
|
3312
|
+
const existing = packages.get(name);
|
|
3313
|
+
if (existing === void 0) {
|
|
3314
|
+
const summary = subsystemSummaries.get(name);
|
|
3315
|
+
packages.set(name, {
|
|
3316
|
+
name,
|
|
3317
|
+
files: packageCounts.get(row.package) ?? 0,
|
|
3318
|
+
hub: relative12,
|
|
3319
|
+
rank: round(row.rank),
|
|
3320
|
+
// Absent rather than empty: an un-enriched repository must project the
|
|
3321
|
+
// same bytes it projected before the concept layer existed.
|
|
3322
|
+
...summary !== void 0 && summary !== "" ? { summary } : {}
|
|
3323
|
+
});
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
if (row.rank > existing.rank) {
|
|
3327
|
+
existing.hub = relative12;
|
|
3328
|
+
existing.rank = round(row.rank);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
const files = ranked.map((row) => {
|
|
3332
|
+
const symbols = store.getFileSymbols(row.file, SYMBOLS_PER_FILE).map((symbol) => ({ name: symbol.name, kind: symbol.kind, line: symbol.line }));
|
|
3333
|
+
const concept = concepts.get(row.file);
|
|
3334
|
+
return {
|
|
3335
|
+
path: relativeOf(row.file),
|
|
3336
|
+
rank: round(row.rank),
|
|
3337
|
+
inDeg: row.inDeg,
|
|
3338
|
+
outDeg: row.outDeg,
|
|
3339
|
+
package: packageNameOf(row),
|
|
3340
|
+
symbols,
|
|
3341
|
+
...concept !== void 0 && concept !== "" ? { concept } : {}
|
|
3342
|
+
};
|
|
3343
|
+
});
|
|
3344
|
+
const known = new Set(packages.keys());
|
|
3345
|
+
const edges = store.getPackageGraph().edges.map((edge) => ({
|
|
3346
|
+
from: edge.source.replace(/^pkg:/, ""),
|
|
3347
|
+
to: edge.target.replace(/^pkg:/, ""),
|
|
3348
|
+
weight: edge.weight,
|
|
3349
|
+
refType: edge.refType
|
|
3350
|
+
})).filter((edge) => known.has(edge.from) && known.has(edge.to)).sort(
|
|
3351
|
+
(a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.refType.localeCompare(b.refType)
|
|
3352
|
+
);
|
|
3353
|
+
const document = {
|
|
3354
|
+
schema: ATLAS_SCHEMA,
|
|
3355
|
+
counts: { files: stats.totalFiles, symbols: stats.totalSymbols, packages: packages.size },
|
|
3356
|
+
packages: [...packages.values()].sort(
|
|
3357
|
+
(a, b) => b.rank - a.rank || a.name.localeCompare(b.name)
|
|
3358
|
+
),
|
|
3359
|
+
edges,
|
|
3360
|
+
files
|
|
3361
|
+
};
|
|
3362
|
+
const allHashes = store.getAllFileMetas().map((meta) => [relativeOf(meta.file), meta.contentHash ?? ""]);
|
|
3363
|
+
const atlasPaths = new Set(files.map((file) => file.path));
|
|
3364
|
+
const manifestFiles = {};
|
|
3365
|
+
for (const [file, hash] of [...allHashes].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
|
|
3366
|
+
if (atlasPaths.has(file)) manifestFiles[file] = hash;
|
|
3367
|
+
}
|
|
3368
|
+
const manifest = {
|
|
3369
|
+
schema: ATLAS_SCHEMA,
|
|
3370
|
+
counts: {
|
|
3371
|
+
files: document.counts.files,
|
|
3372
|
+
symbols: document.counts.symbols,
|
|
3373
|
+
tracked: allHashes.length
|
|
3374
|
+
},
|
|
3375
|
+
digest: digestOf(allHashes),
|
|
3376
|
+
files: manifestFiles
|
|
3377
|
+
};
|
|
3378
|
+
return { document, manifest, markdown: renderMarkdown(document) };
|
|
3379
|
+
}
|
|
3380
|
+
function stableJson(value) {
|
|
3381
|
+
return `${JSON.stringify(value, null, 2)}
|
|
3382
|
+
`;
|
|
3383
|
+
}
|
|
3384
|
+
async function writeAtlas(store, projectRoot) {
|
|
3385
|
+
const projection = buildAtlas(store, projectRoot);
|
|
3386
|
+
const dir = path9.join(projectRoot, ATLAS_DIR);
|
|
3387
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
3388
|
+
const written = [
|
|
3389
|
+
[ATLAS_JSON, stableJson(projection.document)],
|
|
3390
|
+
[ATLAS_MANIFEST, stableJson(projection.manifest)],
|
|
3391
|
+
[ATLAS_MARKDOWN, projection.markdown]
|
|
3392
|
+
];
|
|
3393
|
+
for (const [name, body] of written) {
|
|
3394
|
+
await fs9.writeFile(path9.join(dir, name), body, "utf8");
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
dir: posixIndexPath(ATLAS_DIR),
|
|
3398
|
+
files: written.map(([name]) => name),
|
|
3399
|
+
fileCount: projection.document.files.length,
|
|
3400
|
+
packageCount: projection.document.packages.length
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
var MAX_REPORTED_DRIFT = 20;
|
|
3404
|
+
async function checkAtlasFreshness(store, projectRoot) {
|
|
3405
|
+
const stale = (reason) => ({
|
|
3406
|
+
fresh: false,
|
|
3407
|
+
reason,
|
|
3408
|
+
changed: [],
|
|
3409
|
+
removed: [],
|
|
3410
|
+
added: 0,
|
|
3411
|
+
digestMatches: false
|
|
3412
|
+
});
|
|
3413
|
+
let raw;
|
|
3414
|
+
try {
|
|
3415
|
+
raw = await fs9.readFile(path9.join(projectRoot, ATLAS_DIR, ATLAS_MANIFEST), "utf8");
|
|
3416
|
+
} catch {
|
|
3417
|
+
return stale("missing");
|
|
3418
|
+
}
|
|
3419
|
+
let manifest;
|
|
3420
|
+
try {
|
|
3421
|
+
manifest = JSON.parse(raw);
|
|
3422
|
+
} catch {
|
|
3423
|
+
return stale("unreadable");
|
|
3424
|
+
}
|
|
3425
|
+
if (manifest?.schema !== ATLAS_SCHEMA || typeof manifest.files !== "object") {
|
|
3426
|
+
return stale("schema");
|
|
3427
|
+
}
|
|
3428
|
+
const relativeOf = relativeFactory(projectRoot);
|
|
3429
|
+
const live = /* @__PURE__ */ new Map();
|
|
3430
|
+
const allHashes = [];
|
|
3431
|
+
for (const meta of store.getAllFileMetas()) {
|
|
3432
|
+
const relative12 = relativeOf(meta.file);
|
|
3433
|
+
const hash = meta.contentHash ?? "";
|
|
3434
|
+
live.set(relative12, hash);
|
|
3435
|
+
allHashes.push([relative12, hash]);
|
|
3436
|
+
}
|
|
3437
|
+
const changed = [];
|
|
3438
|
+
const removed = [];
|
|
3439
|
+
for (const [file, hash] of Object.entries(manifest.files)) {
|
|
3440
|
+
const current = live.get(file);
|
|
3441
|
+
if (current === void 0) {
|
|
3442
|
+
removed.push(file);
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (hash !== "" && current !== "" && hash !== current) changed.push(file);
|
|
3446
|
+
}
|
|
3447
|
+
const digestMatches = digestOf(allHashes) === manifest.digest;
|
|
3448
|
+
const tracked = typeof manifest.counts?.tracked === "number" ? manifest.counts.tracked : live.size;
|
|
3449
|
+
const added = Math.max(0, live.size - tracked);
|
|
3450
|
+
const fresh = digestMatches && changed.length === 0 && removed.length === 0;
|
|
3451
|
+
return {
|
|
3452
|
+
fresh,
|
|
3453
|
+
...fresh ? {} : { reason: "drift" },
|
|
3454
|
+
changed: changed.sort().slice(0, MAX_REPORTED_DRIFT),
|
|
3455
|
+
removed: removed.sort().slice(0, MAX_REPORTED_DRIFT),
|
|
3456
|
+
added,
|
|
3457
|
+
digestMatches
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
function hasIndex(projectRoot, indexDir) {
|
|
3461
|
+
return existsSync3(path9.join(resolveIndexDir(projectRoot, indexDir), "index.db"));
|
|
3462
|
+
}
|
|
3463
|
+
async function withStore(projectRoot, indexDir, job) {
|
|
3464
|
+
if (!hasIndex(projectRoot, indexDir)) return { indexed: false };
|
|
3465
|
+
const store = indexStorePool.acquire(projectRoot, { indexDir });
|
|
3466
|
+
try {
|
|
3467
|
+
return await job(store);
|
|
3468
|
+
} finally {
|
|
3469
|
+
indexStorePool.release(store);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
function writeProjectAtlas(projectRoot, opts = {}) {
|
|
3473
|
+
return withStore(projectRoot, opts.indexDir, (store) => writeAtlas(store, projectRoot));
|
|
3474
|
+
}
|
|
3475
|
+
function checkProjectAtlasFreshness(projectRoot, opts = {}) {
|
|
3476
|
+
return withStore(projectRoot, opts.indexDir, (store) => checkAtlasFreshness(store, projectRoot));
|
|
3477
|
+
}
|
|
3478
|
+
function exportProjectAtlasHtml(projectRoot, opts = {}) {
|
|
3479
|
+
return withStore(
|
|
3480
|
+
projectRoot,
|
|
3481
|
+
opts.indexDir,
|
|
3482
|
+
(store) => renderAtlasHtml(buildAtlas(store, projectRoot).document, {
|
|
3483
|
+
projectName: opts.projectName ?? path9.basename(projectRoot)
|
|
3484
|
+
})
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
// src/codebase-index/atlas-brief.ts
|
|
3489
|
+
var BRIEF_HUB_LIMIT = 20;
|
|
3490
|
+
var BRIEF_PACKAGE_LIMIT = 12;
|
|
3491
|
+
var BRIEF_SUBSYSTEM_LIMIT = 10;
|
|
3492
|
+
var BRIEF_SUMMARY_CHARS = 160;
|
|
3493
|
+
function trim(text) {
|
|
3494
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
3495
|
+
return clean.length > BRIEF_SUMMARY_CHARS ? `${clean.slice(0, BRIEF_SUMMARY_CHARS - 1).trimEnd()}\u2026` : clean;
|
|
3496
|
+
}
|
|
3497
|
+
async function buildAtlasBrief(store, projectRoot) {
|
|
3498
|
+
const relativeOf = (file) => {
|
|
3499
|
+
const relative12 = path10.relative(projectRoot, file);
|
|
3500
|
+
return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
|
|
3501
|
+
};
|
|
3502
|
+
const stats = store.getStats();
|
|
3503
|
+
const concepts = store.getReadyConceptSummaries();
|
|
3504
|
+
const ranked = store.getRankedFiles(BRIEF_HUB_LIMIT);
|
|
3505
|
+
const hubs = ranked.map((row) => {
|
|
3506
|
+
const concept = concepts.get(row.file);
|
|
3507
|
+
return {
|
|
3508
|
+
path: relativeOf(row.file),
|
|
3509
|
+
rank: row.rank,
|
|
3510
|
+
...concept !== void 0 && concept !== "" ? { concept: trim(concept) } : {}
|
|
3511
|
+
};
|
|
3512
|
+
});
|
|
3513
|
+
const counts = store.getPackageFileCounts();
|
|
3514
|
+
const hubOf = /* @__PURE__ */ new Map();
|
|
3515
|
+
for (const row of store.getRankedFiles(Math.max(counts.size * 4, BRIEF_HUB_LIMIT))) {
|
|
3516
|
+
const name = row.package || relativeOf(row.file).split("/")[0] || "(root)";
|
|
3517
|
+
const current = hubOf.get(name);
|
|
3518
|
+
if (current === void 0 || row.rank > current.rank) {
|
|
3519
|
+
hubOf.set(name, { file: relativeOf(row.file), rank: row.rank });
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
const packages = [...hubOf.entries()].sort((a, b) => b[1].rank - a[1].rank || a[0].localeCompare(b[0])).slice(0, BRIEF_PACKAGE_LIMIT).map(([name, hub]) => ({ name, files: counts.get(name) ?? 0, hub: hub.file }));
|
|
3523
|
+
const subsystems = store.getSubsystems().filter((subsystem) => subsystem.summary !== "").slice(0, BRIEF_SUBSYSTEM_LIMIT).map((subsystem) => ({ name: subsystem.name, summary: trim(subsystem.summary) }));
|
|
3524
|
+
const brief = {
|
|
3525
|
+
ranked: hubs.length > 0,
|
|
3526
|
+
counts: {
|
|
3527
|
+
files: stats.totalFiles,
|
|
3528
|
+
symbols: stats.totalSymbols,
|
|
3529
|
+
packages: counts.size
|
|
3530
|
+
},
|
|
3531
|
+
hubs,
|
|
3532
|
+
packages,
|
|
3533
|
+
subsystems
|
|
3534
|
+
};
|
|
3535
|
+
try {
|
|
3536
|
+
const freshness = await checkAtlasFreshness(store, projectRoot);
|
|
3537
|
+
if (freshness.reason !== "missing") {
|
|
3538
|
+
brief.staleFiles = freshness.changed.length + freshness.removed.length + freshness.added;
|
|
3539
|
+
}
|
|
3540
|
+
} catch {
|
|
3541
|
+
}
|
|
3542
|
+
return brief;
|
|
3543
|
+
}
|
|
3544
|
+
async function buildProjectAtlasBrief(projectRoot, opts = {}) {
|
|
3545
|
+
if (!existsSync4(path10.join(resolveIndexDir(projectRoot, opts.indexDir), "index.db"))) {
|
|
3546
|
+
return { indexed: false };
|
|
3547
|
+
}
|
|
3548
|
+
const store = indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
|
|
3549
|
+
try {
|
|
3550
|
+
return await buildAtlasBrief(store, projectRoot);
|
|
3551
|
+
} finally {
|
|
3552
|
+
indexStorePool.release(store);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/codebase-index/codebase-context-tool.ts
|
|
3557
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
3558
|
+
import { toErrorMessage as toErrorMessage11 } from "@wrongstack/core/utils";
|
|
3559
|
+
var TOOL_TIMEOUT_MS = 35e3;
|
|
3560
|
+
var VECTOR_HIT_LIMIT = 12;
|
|
3561
|
+
var VECTOR_MIN_SCORE = 0.25;
|
|
3562
|
+
var queryEmbedder;
|
|
3563
|
+
function setContextQueryEmbedder(port) {
|
|
3564
|
+
queryEmbedder = port;
|
|
3565
|
+
}
|
|
3566
|
+
async function semanticSeeds(projectRoot, indexDir, query) {
|
|
3567
|
+
const port = queryEmbedder;
|
|
3568
|
+
if (port === void 0) return [];
|
|
3569
|
+
try {
|
|
3570
|
+
const [vector] = await port.embed([query]);
|
|
3571
|
+
if (vector === void 0 || vector.length !== port.dimensions) return [];
|
|
3572
|
+
const result = await codebaseVectorSearch({
|
|
3573
|
+
projectRoot,
|
|
3574
|
+
indexDir,
|
|
3575
|
+
vector: Array.from(vector),
|
|
3576
|
+
limit: VECTOR_HIT_LIMIT,
|
|
3577
|
+
minScore: VECTOR_MIN_SCORE
|
|
3578
|
+
});
|
|
3579
|
+
return result.hits;
|
|
3580
|
+
} catch {
|
|
3581
|
+
return [];
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
var codebaseContextTool = {
|
|
3585
|
+
name: "codebase-context",
|
|
3586
|
+
category: "Project",
|
|
3587
|
+
icon: "index",
|
|
3588
|
+
permission: "auto",
|
|
3589
|
+
mutating: false,
|
|
3590
|
+
capabilities: ["fs.read"],
|
|
3591
|
+
timeoutMs: TOOL_TIMEOUT_MS,
|
|
3592
|
+
description: "Find the files and declarations a task touches, from a plain-language description. Combines ranked symbol search with a personalised walk over the reference graph, so results include not just what matched by name but what those matches are structurally attached to \u2014 the interface behind a handler, the store behind a route, the module every match imports. Returns ranked files with the relevant declarations, their signatures and line numbers.",
|
|
3593
|
+
usageHint: 'START HERE FOR ANY TASK THAT SPANS MORE THAN ONE FILE:\n\n- Describe the task, not a symbol name: `codebase-context({ query: "retry a provider request after a 429" })`.\n- Prefer this over `codebase-search` when you do not already know the symbol you want; prefer `codebase-search` when you do.\n- Use the returned `file` + `line` to `read` only the parts you need, instead of reading whole files.\n- `matched: true` means the file matched the query directly; the rest were reached through the graph.\n- `relevance` is relative to this answer only (top result is always 1.0) \u2014 never compare it across queries.\n- Narrow with `pathPrefix: "packages/core"` when you already know the area.\n- `indexStatus: "no-index"` means run `/codebase-reindex` first.',
|
|
3594
|
+
inputSchema: {
|
|
3595
|
+
type: "object",
|
|
3596
|
+
properties: {
|
|
3597
|
+
query: {
|
|
3598
|
+
type: "string",
|
|
3599
|
+
description: "What you are trying to do or understand, in plain words."
|
|
3600
|
+
},
|
|
3601
|
+
limit: {
|
|
3602
|
+
type: "number",
|
|
3603
|
+
description: "Number of files to return (default 12, max 50)."
|
|
3604
|
+
},
|
|
3605
|
+
symbolsPerFile: {
|
|
3606
|
+
type: "number",
|
|
3607
|
+
description: "Declarations shown per file (default 4, max 20)."
|
|
3608
|
+
},
|
|
3609
|
+
pathPrefix: {
|
|
3610
|
+
type: "string",
|
|
3611
|
+
description: "Restrict results to this project-relative path prefix."
|
|
3612
|
+
}
|
|
3613
|
+
},
|
|
3614
|
+
required: ["query"],
|
|
3615
|
+
additionalProperties: false
|
|
3616
|
+
},
|
|
3617
|
+
async execute(input, ctx, execOpts) {
|
|
3618
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
3619
|
+
if (query.length === 0) {
|
|
3620
|
+
throw new ToolValidationError3({
|
|
3621
|
+
message: "codebase-context requires a non-empty `query`.",
|
|
3622
|
+
field: "query"
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
const signal = execOpts?.signal ?? ctx.signal;
|
|
3626
|
+
const projectRoot = ctx.projectRoot ?? ctx.cwd ?? process.cwd();
|
|
3627
|
+
try {
|
|
3628
|
+
signal?.throwIfAborted();
|
|
3629
|
+
const indexDir = codebaseIndexDirOverride(ctx);
|
|
3630
|
+
const vectorFiles = await semanticSeeds(projectRoot, indexDir, query);
|
|
3631
|
+
signal?.throwIfAborted();
|
|
3632
|
+
const result = await codebaseContext({
|
|
3633
|
+
projectRoot,
|
|
3634
|
+
indexDir,
|
|
3635
|
+
query,
|
|
3636
|
+
...vectorFiles.length > 0 ? { vectorFiles } : {},
|
|
3637
|
+
limit: input.limit,
|
|
3638
|
+
symbolsPerFile: input.symbolsPerFile,
|
|
3639
|
+
pathPrefix: input.pathPrefix
|
|
3640
|
+
});
|
|
3641
|
+
return {
|
|
3642
|
+
query: result.query,
|
|
3643
|
+
entries: result.entries,
|
|
3644
|
+
seedCount: result.seedCount,
|
|
3645
|
+
semanticSeedCount: result.semanticSeedCount,
|
|
3646
|
+
totalCandidates: result.totalCandidates,
|
|
3647
|
+
indexStatus: result.indexStatus,
|
|
3648
|
+
..."stale" in result && result.stale === true ? { stale: true } : {}
|
|
3649
|
+
};
|
|
3650
|
+
} catch (err) {
|
|
3651
|
+
if (signal?.aborted) throw err;
|
|
3652
|
+
const refreshing = err instanceof Error && err.name === "IndexRefreshInProgressError";
|
|
3653
|
+
return {
|
|
3654
|
+
query,
|
|
3655
|
+
entries: [],
|
|
3656
|
+
seedCount: 0,
|
|
3657
|
+
semanticSeedCount: 0,
|
|
3658
|
+
totalCandidates: 0,
|
|
3659
|
+
indexStatus: "error",
|
|
3660
|
+
error: refreshing ? "Codebase index is refreshing; retry once the current generation is published." : toErrorMessage11(err)
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
// src/codebase-index/concept-enrichment.ts
|
|
3667
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
3668
|
+
import * as fs10 from "node:fs/promises";
|
|
3669
|
+
import * as path11 from "node:path";
|
|
3670
|
+
var MAX_SOURCE_CHARS = 12e3;
|
|
3671
|
+
var MAX_CRUX_LINES = 12;
|
|
3672
|
+
var MAX_DECLARATIONS = 24;
|
|
3673
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
3674
|
+
var MAX_SUMMARY_CHARS = 400;
|
|
3675
|
+
function normaliseCrux(result, lineCount) {
|
|
3676
|
+
const rawStart = result.cruxStart;
|
|
3677
|
+
const rawEnd = result.cruxEnd;
|
|
3678
|
+
if (typeof rawStart !== "number" || !Number.isFinite(rawStart)) return { start: null, end: null };
|
|
3679
|
+
const start = Math.max(1, Math.min(Math.trunc(rawStart), lineCount));
|
|
3680
|
+
const proposedEnd = typeof rawEnd === "number" && Number.isFinite(rawEnd) ? Math.trunc(rawEnd) : start;
|
|
3681
|
+
const end = Math.max(start, Math.min(proposedEnd, lineCount, start + MAX_CRUX_LINES - 1));
|
|
3682
|
+
return { start, end };
|
|
3683
|
+
}
|
|
3684
|
+
async function enrichConcepts(store, port, relativeOf, options = {}) {
|
|
3685
|
+
const startedAt = Date.now();
|
|
3686
|
+
const errors = [];
|
|
3687
|
+
const signal = options.signal;
|
|
3688
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, 16));
|
|
3689
|
+
const pruned = store.pruneOrphanConcepts();
|
|
3690
|
+
const markedStale = store.markStaleConcepts();
|
|
3691
|
+
const hashByFile = /* @__PURE__ */ new Map();
|
|
3692
|
+
for (const meta of store.getAllFileMetas()) hashByFile.set(meta.file, meta.contentHash ?? "");
|
|
3693
|
+
const existing = /* @__PURE__ */ new Map();
|
|
3694
|
+
for (const concept of store.getAllFileConcepts()) existing.set(concept.file, concept);
|
|
3695
|
+
const rankOf = /* @__PURE__ */ new Map();
|
|
3696
|
+
for (const row of store.getRankedFiles(hashByFile.size || 1)) rankOf.set(row.file, row.rank);
|
|
3697
|
+
const ordered = [...hashByFile.keys()].sort(
|
|
3698
|
+
(a, b) => (rankOf.get(b) ?? 0) - (rankOf.get(a) ?? 0) || a.localeCompare(b)
|
|
3699
|
+
);
|
|
3700
|
+
const queue = [];
|
|
3701
|
+
let cached = 0;
|
|
3702
|
+
for (const file of ordered) {
|
|
3703
|
+
const hash = hashByFile.get(file);
|
|
3704
|
+
if (hash === void 0) continue;
|
|
3705
|
+
const concept = existing.get(file);
|
|
3706
|
+
const current = concept !== void 0 && concept.state === "ready" && concept.contentHash === hash;
|
|
3707
|
+
if (current && options.force !== true) {
|
|
3708
|
+
cached++;
|
|
3709
|
+
continue;
|
|
3710
|
+
}
|
|
3711
|
+
queue.push(file);
|
|
3712
|
+
if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
|
|
3713
|
+
}
|
|
3714
|
+
let summarised = 0;
|
|
3715
|
+
let failed = 0;
|
|
3716
|
+
let done = 0;
|
|
3717
|
+
let cursor = 0;
|
|
3718
|
+
const worker = async () => {
|
|
3719
|
+
for (; ; ) {
|
|
3720
|
+
if (signal?.aborted) return;
|
|
3721
|
+
const index = cursor++;
|
|
3722
|
+
const file = queue[index];
|
|
3723
|
+
if (file === void 0) return;
|
|
3724
|
+
try {
|
|
3725
|
+
const result = await summariseOne(
|
|
3726
|
+
store,
|
|
3727
|
+
port,
|
|
3728
|
+
relativeOf,
|
|
3729
|
+
file,
|
|
3730
|
+
hashByFile,
|
|
3731
|
+
existing,
|
|
3732
|
+
signal
|
|
3733
|
+
);
|
|
3734
|
+
if (result === "skipped") failed++;
|
|
3735
|
+
else summarised++;
|
|
3736
|
+
} catch (error) {
|
|
3737
|
+
failed++;
|
|
3738
|
+
if (errors.length < 50) {
|
|
3739
|
+
errors.push(
|
|
3740
|
+
`${relativeOf(file)}: ${error instanceof Error ? error.message : String(error)}`
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
} finally {
|
|
3744
|
+
done++;
|
|
3745
|
+
options.onProgress?.(done, queue.length);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
};
|
|
3749
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, () => worker()));
|
|
3750
|
+
let subsystems = 0;
|
|
3751
|
+
if (options.subsystems === true && port.describeSubsystem !== void 0 && signal?.aborted !== true) {
|
|
3752
|
+
try {
|
|
3753
|
+
subsystems = await deriveSubsystems(store, port, relativeOf, signal);
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
errors.push(`subsystems: ${error instanceof Error ? error.message : String(error)}`);
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
return {
|
|
3759
|
+
summarised,
|
|
3760
|
+
cached,
|
|
3761
|
+
failed,
|
|
3762
|
+
markedStale,
|
|
3763
|
+
pruned,
|
|
3764
|
+
subsystems,
|
|
3765
|
+
durationMs: Date.now() - startedAt,
|
|
3766
|
+
errors
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
async function summariseOne(store, port, relativeOf, file, hashByFile, existing, signal) {
|
|
3770
|
+
let source;
|
|
3771
|
+
try {
|
|
3772
|
+
source = await fs10.readFile(file, "utf8");
|
|
3773
|
+
} catch {
|
|
3774
|
+
return "skipped";
|
|
3775
|
+
}
|
|
3776
|
+
if (source.trim().length === 0) return "skipped";
|
|
3777
|
+
const truncated = source.length > MAX_SOURCE_CHARS;
|
|
3778
|
+
const declarations = store.getFileSymbols(file, MAX_DECLARATIONS).map((symbol) => ({ name: symbol.name, kind: symbol.kind, line: symbol.line }));
|
|
3779
|
+
const stale = existing.get(file);
|
|
3780
|
+
const result = await port.describeFile({
|
|
3781
|
+
file: relativeOf(file),
|
|
3782
|
+
absolutePath: file,
|
|
3783
|
+
language: "",
|
|
3784
|
+
source: truncated ? source.slice(0, MAX_SOURCE_CHARS) : source,
|
|
3785
|
+
truncated,
|
|
3786
|
+
declarations,
|
|
3787
|
+
...stale !== void 0 && stale.summary !== "" ? { staleSummary: stale.summary } : {},
|
|
3788
|
+
signal
|
|
3789
|
+
});
|
|
3790
|
+
const summary = result?.summary?.trim() ?? "";
|
|
3791
|
+
if (summary.length === 0) return "skipped";
|
|
3792
|
+
const lineCount = source.split("\n").length;
|
|
3793
|
+
const crux = normaliseCrux(result, lineCount);
|
|
3794
|
+
store.upsertFileConcept({
|
|
3795
|
+
file,
|
|
3796
|
+
contentHash: hashByFile.get(file) ?? "",
|
|
3797
|
+
summary: summary.slice(0, MAX_SUMMARY_CHARS),
|
|
3798
|
+
cruxStart: crux.start,
|
|
3799
|
+
cruxEnd: crux.end,
|
|
3800
|
+
state: "ready",
|
|
3801
|
+
model: result?.model ?? "",
|
|
3802
|
+
updatedAt: Date.now()
|
|
3803
|
+
});
|
|
3804
|
+
return "stored";
|
|
3805
|
+
}
|
|
3806
|
+
var SUBSYSTEM_FILE_SAMPLE = 12;
|
|
3807
|
+
async function deriveSubsystems(store, port, relativeOf, signal) {
|
|
3808
|
+
const describe = port.describeSubsystem;
|
|
3809
|
+
if (describe === void 0) return 0;
|
|
3810
|
+
const summaries = store.getReadyConceptSummaries();
|
|
3811
|
+
if (summaries.size === 0) return 0;
|
|
3812
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
3813
|
+
for (const row of store.getRankedFiles(summaries.size * 2)) {
|
|
3814
|
+
const summary = summaries.get(row.file);
|
|
3815
|
+
if (summary === void 0) continue;
|
|
3816
|
+
const name = row.package || relativeOf(row.file).split("/")[0] || "(root)";
|
|
3817
|
+
const bucket = byPackage.get(name);
|
|
3818
|
+
const entry = { file: relativeOf(row.file), summary, rank: row.rank };
|
|
3819
|
+
if (bucket === void 0) byPackage.set(name, [entry]);
|
|
3820
|
+
else if (bucket.length < SUBSYSTEM_FILE_SAMPLE) bucket.push(entry);
|
|
3821
|
+
}
|
|
3822
|
+
const names = [...byPackage.keys()].sort();
|
|
3823
|
+
const known = new Set(names);
|
|
3824
|
+
const subsystems = [];
|
|
3825
|
+
const edges = [];
|
|
3826
|
+
for (const name of names) {
|
|
3827
|
+
if (signal?.aborted) break;
|
|
3828
|
+
const files = byPackage.get(name) ?? [];
|
|
3829
|
+
const result = await describe({ name, files, signal });
|
|
3830
|
+
const summary = result?.summary?.trim() ?? "";
|
|
3831
|
+
if (summary.length === 0) continue;
|
|
3832
|
+
subsystems.push({
|
|
3833
|
+
id: name,
|
|
3834
|
+
name,
|
|
3835
|
+
summary: summary.slice(0, MAX_SUMMARY_CHARS),
|
|
3836
|
+
memberFiles: files.map((f) => f.file),
|
|
3837
|
+
model: result?.model ?? "",
|
|
3838
|
+
updatedAt: Date.now()
|
|
3839
|
+
});
|
|
3840
|
+
for (const relation of result?.relations ?? []) {
|
|
3841
|
+
if (!known.has(relation.to) || relation.to === name) continue;
|
|
3842
|
+
if (!isConceptRelation(relation.relation)) continue;
|
|
3843
|
+
edges.push({ fromId: name, toId: relation.to, relation: relation.relation });
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
store.replaceSubsystems(subsystems, edges);
|
|
3847
|
+
return subsystems.length;
|
|
3848
|
+
}
|
|
3849
|
+
async function enrichProjectConcepts(projectRoot, port, options = {}) {
|
|
3850
|
+
const { indexDir, ...rest } = options;
|
|
3851
|
+
if (!existsSync5(path11.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
|
|
3852
|
+
return { indexed: false };
|
|
3853
|
+
}
|
|
3854
|
+
const store = indexStorePool.acquire(projectRoot, { indexDir });
|
|
3855
|
+
try {
|
|
3856
|
+
return await enrichConcepts(
|
|
3857
|
+
store,
|
|
3858
|
+
port,
|
|
3859
|
+
(file) => {
|
|
3860
|
+
const relative12 = path11.relative(projectRoot, file);
|
|
3861
|
+
const chosen = relative12 && !relative12.startsWith("..") ? relative12 : file;
|
|
3862
|
+
return posixIndexPath(chosen);
|
|
3863
|
+
},
|
|
3864
|
+
rest
|
|
3865
|
+
);
|
|
3866
|
+
} finally {
|
|
3867
|
+
indexStorePool.release(store);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// src/codebase-index/embedding-pass.ts
|
|
3872
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
3873
|
+
import * as path12 from "node:path";
|
|
3874
|
+
var DEFAULT_BATCH_SIZE = 16;
|
|
3875
|
+
var MAX_EMBED_CHARS = 2e3;
|
|
3876
|
+
var FALLBACK_DECLARATIONS = 12;
|
|
3877
|
+
function embedTextFor(relativePath, summary, declarations) {
|
|
3878
|
+
const described = summary?.trim() ?? "";
|
|
3879
|
+
if (described.length > 0) {
|
|
3880
|
+
return {
|
|
3881
|
+
text: `${relativePath}
|
|
3882
|
+
${described}`.slice(0, MAX_EMBED_CHARS),
|
|
3883
|
+
fromDeclarations: false
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
const names = declarations.slice(0, FALLBACK_DECLARATIONS).join(", ");
|
|
3887
|
+
return {
|
|
3888
|
+
text: `${relativePath}${names.length > 0 ? `
|
|
3889
|
+
${names}` : ""}`.slice(0, MAX_EMBED_CHARS),
|
|
3890
|
+
fromDeclarations: true
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
async function embedFiles(store, port, relativeOf, options = {}) {
|
|
3894
|
+
const startedAt = Date.now();
|
|
3895
|
+
const errors = [];
|
|
3896
|
+
const signal = options.signal;
|
|
3897
|
+
const batchSize = Math.max(1, Math.min(options.batchSize ?? DEFAULT_BATCH_SIZE, 128));
|
|
3898
|
+
const providerChanged = store.reconcileVectorProvider(port.id);
|
|
3899
|
+
const pruned = store.pruneOrphanFileVectors();
|
|
3900
|
+
const stored = store.getFileVectorStates(port.id);
|
|
3901
|
+
const summaries = store.getReadyConceptSummaries();
|
|
3902
|
+
const rankOf = /* @__PURE__ */ new Map();
|
|
3903
|
+
const metas = store.getAllFileMetas();
|
|
3904
|
+
for (const row of store.getRankedFiles(metas.length || 1)) rankOf.set(row.file, row.rank);
|
|
3905
|
+
const ordered = metas.map((meta) => meta.file).sort((a, b) => (rankOf.get(b) ?? 0) - (rankOf.get(a) ?? 0) || a.localeCompare(b));
|
|
3906
|
+
const queue = [];
|
|
3907
|
+
let cached = 0;
|
|
3908
|
+
for (const file of ordered) {
|
|
3909
|
+
const declarations = summaries.has(file) ? [] : store.getFileSymbols(file, FALLBACK_DECLARATIONS).map((symbol) => symbol.name);
|
|
3910
|
+
const { text, fromDeclarations: fromDeclarations2 } = embedTextFor(
|
|
3911
|
+
relativeOf(file),
|
|
3912
|
+
summaries.get(file),
|
|
3913
|
+
declarations
|
|
3914
|
+
);
|
|
3915
|
+
if (text.trim().length === 0) continue;
|
|
3916
|
+
const hash = xxhash64String(text);
|
|
3917
|
+
if (options.force !== true && stored.get(file) === hash) {
|
|
3918
|
+
cached++;
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
queue.push({ file, text, hash, fromDeclarations: fromDeclarations2 });
|
|
3922
|
+
if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
|
|
3923
|
+
}
|
|
3924
|
+
let embedded = 0;
|
|
3925
|
+
let fromDeclarations = 0;
|
|
3926
|
+
let done = 0;
|
|
3927
|
+
for (let start = 0; start < queue.length; start += batchSize) {
|
|
3928
|
+
if (signal?.aborted) break;
|
|
3929
|
+
const batch = queue.slice(start, start + batchSize);
|
|
3930
|
+
try {
|
|
3931
|
+
const vectors = await port.embed(batch.map((entry) => entry.text));
|
|
3932
|
+
const rows = [];
|
|
3933
|
+
for (let i = 0; i < batch.length; i++) {
|
|
3934
|
+
const entry = batch[i];
|
|
3935
|
+
const vector = vectors[i];
|
|
3936
|
+
if (vector === void 0 || vector.length !== port.dimensions) continue;
|
|
3937
|
+
rows.push({ file: entry.file, vector, sourceHash: entry.hash, provider: port.id });
|
|
3938
|
+
if (entry.fromDeclarations) fromDeclarations++;
|
|
3939
|
+
}
|
|
3940
|
+
store.upsertFileVectors(rows);
|
|
3941
|
+
embedded += rows.length;
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
if (errors.length < 20) {
|
|
3944
|
+
errors.push(`batch at ${start}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3945
|
+
}
|
|
3946
|
+
} finally {
|
|
3947
|
+
done += batch.length;
|
|
3948
|
+
options.onProgress?.(done, queue.length);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return {
|
|
3952
|
+
embedded,
|
|
3953
|
+
cached,
|
|
3954
|
+
pruned,
|
|
3955
|
+
providerChanged,
|
|
3956
|
+
fromDeclarations,
|
|
3957
|
+
durationMs: Date.now() - startedAt,
|
|
3958
|
+
errors
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
async function embedProjectFiles(projectRoot, port, options = {}) {
|
|
3962
|
+
const { indexDir, ...rest } = options;
|
|
3963
|
+
if (!existsSync6(path12.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
|
|
3964
|
+
return { indexed: false };
|
|
3965
|
+
}
|
|
3966
|
+
const store = indexStorePool.acquire(projectRoot, { indexDir });
|
|
3967
|
+
try {
|
|
3968
|
+
return await embedFiles(
|
|
3969
|
+
store,
|
|
3970
|
+
port,
|
|
3971
|
+
(file) => {
|
|
3972
|
+
const relative12 = path12.relative(projectRoot, file);
|
|
3973
|
+
return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
|
|
3974
|
+
},
|
|
3975
|
+
rest
|
|
3976
|
+
);
|
|
3977
|
+
} finally {
|
|
3978
|
+
indexStorePool.release(store);
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
2842
3982
|
export {
|
|
2843
3983
|
PolyglotInvariantEngine,
|
|
2844
3984
|
polyglotInvariantEngine,
|
|
2845
3985
|
replaceSymbolInFile,
|
|
2846
3986
|
codebaseAstReplaceTool,
|
|
2847
|
-
codebaseInvariantCheckTool,
|
|
2848
3987
|
codebaseImpactAnalysisTool,
|
|
2849
3988
|
codebaseIncomingCallsTool,
|
|
2850
3989
|
codebaseSearchTool,
|
|
2851
3990
|
codebaseIndexTool,
|
|
3991
|
+
codebaseInvariantCheckTool,
|
|
2852
3992
|
codebaseOutgoingCallsTool,
|
|
2853
3993
|
extractFileSkeleton,
|
|
2854
3994
|
extractDirectorySkeleton,
|
|
@@ -2858,6 +3998,39 @@ export {
|
|
|
2858
3998
|
codebaseStatsTool,
|
|
2859
3999
|
codebaseTargetedTestTool,
|
|
2860
4000
|
deadCodeScanTool,
|
|
2861
|
-
runDeadCodeScan
|
|
4001
|
+
runDeadCodeScan,
|
|
4002
|
+
EXPORT_PACKAGE_LIMIT,
|
|
4003
|
+
EXPORT_FILE_LIMIT,
|
|
4004
|
+
renderAtlasHtml,
|
|
4005
|
+
ATLAS_DIR,
|
|
4006
|
+
ATLAS_JSON,
|
|
4007
|
+
ATLAS_MARKDOWN,
|
|
4008
|
+
ATLAS_MANIFEST,
|
|
4009
|
+
ATLAS_SCHEMA,
|
|
4010
|
+
ATLAS_FILE_LIMIT,
|
|
4011
|
+
buildAtlas,
|
|
4012
|
+
writeAtlas,
|
|
4013
|
+
MAX_REPORTED_DRIFT,
|
|
4014
|
+
checkAtlasFreshness,
|
|
4015
|
+
writeProjectAtlas,
|
|
4016
|
+
checkProjectAtlasFreshness,
|
|
4017
|
+
exportProjectAtlasHtml,
|
|
4018
|
+
BRIEF_HUB_LIMIT,
|
|
4019
|
+
BRIEF_PACKAGE_LIMIT,
|
|
4020
|
+
BRIEF_SUBSYSTEM_LIMIT,
|
|
4021
|
+
buildAtlasBrief,
|
|
4022
|
+
buildProjectAtlasBrief,
|
|
4023
|
+
setContextQueryEmbedder,
|
|
4024
|
+
codebaseContextTool,
|
|
4025
|
+
MAX_SOURCE_CHARS,
|
|
4026
|
+
MAX_CRUX_LINES,
|
|
4027
|
+
DEFAULT_CONCURRENCY,
|
|
4028
|
+
MAX_SUMMARY_CHARS,
|
|
4029
|
+
enrichConcepts,
|
|
4030
|
+
enrichProjectConcepts,
|
|
4031
|
+
DEFAULT_BATCH_SIZE,
|
|
4032
|
+
MAX_EMBED_CHARS,
|
|
4033
|
+
embedFiles,
|
|
4034
|
+
embedProjectFiles
|
|
2862
4035
|
};
|
|
2863
|
-
//# sourceMappingURL=chunk-
|
|
4036
|
+
//# sourceMappingURL=chunk-HWXIRVKH.js.map
|