@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.
Files changed (70) hide show
  1. package/dist/auto-proceed-loop-guard.d.ts +1 -1
  2. package/dist/auto-proceed-loop-guard.js +1 -1
  3. package/dist/bash.js +1 -1
  4. package/dist/builtin.js +15 -15
  5. package/dist/{chunk-VVZPQPTY.js → chunk-3AU6CZWL.js} +4 -2
  6. package/dist/{chunk-N3K2SFEI.js → chunk-52UXXQMP.js} +1408 -200
  7. package/dist/{chunk-NN2SNP2R.js → chunk-6H4ERJP7.js} +137 -21
  8. package/dist/{chunk-DK4J2WMH.js → chunk-7XUEPBNT.js} +2 -2
  9. package/dist/{chunk-IYH4OHBP.js → chunk-DKHMPKDG.js} +2 -2
  10. package/dist/{chunk-NU2IZ72J.js → chunk-DL2ARI4S.js} +7 -5
  11. package/dist/{chunk-M6UV67BH.js → chunk-EL2NJXSK.js} +2 -2
  12. package/dist/{chunk-YEISRT4S.js → chunk-HWXIRVKH.js} +1408 -235
  13. package/dist/{chunk-GKSG5CPH.js → chunk-KVTEIUND.js} +2 -2
  14. package/dist/{chunk-GDDXX27P.js → chunk-MBR5VAB5.js} +2 -2
  15. package/dist/{chunk-KDEXUQY7.js → chunk-MGPR7VG3.js} +261 -258
  16. package/dist/{chunk-YULVUTK5.js → chunk-NFX2IUQA.js} +3 -3
  17. package/dist/{chunk-37QKUQ2Z.js → chunk-NL2S2CXD.js} +1 -1
  18. package/dist/{chunk-ZTCJCYLN.js → chunk-TI5COGLY.js} +49 -6
  19. package/dist/{chunk-CGXTR34H.js → chunk-TU5MTW2S.js} +2 -2
  20. package/dist/{chunk-AB2CAUOP.js → chunk-UCXFTCAI.js} +186 -36
  21. package/dist/{chunk-MK2JK35J.js → chunk-UOYRAS4J.js} +3 -3
  22. package/dist/{chunk-UCYQTZ7F.js → chunk-ZKC5UNGD.js} +56 -2
  23. package/dist/codebase-index/atlas-brief.d.ts +90 -0
  24. package/dist/codebase-index/atlas-export.d.ts +38 -0
  25. package/dist/codebase-index/atlas-projection.d.ts +114 -0
  26. package/dist/codebase-index/atlas-types.d.ts +90 -0
  27. package/dist/codebase-index/background-indexer.d.ts +15 -4
  28. package/dist/codebase-index/codebase-context-tool.d.ts +54 -0
  29. package/dist/codebase-index/concept-enrichment.d.ts +154 -0
  30. package/dist/codebase-index/context-retrieval.d.ts +91 -0
  31. package/dist/codebase-index/embedding-pass.d.ts +95 -0
  32. package/dist/codebase-index/graph-adjacency-cache.d.ts +36 -0
  33. package/dist/codebase-index/graph-rank-pass.d.ts +46 -0
  34. package/dist/codebase-index/graph-rank.d.ts +198 -0
  35. package/dist/codebase-index/index-service.d.ts +15 -1
  36. package/dist/codebase-index/index.d.ts +34 -19
  37. package/dist/codebase-index/index.js +126 -9
  38. package/dist/codebase-index/parser-worker-script.js +3 -3
  39. package/dist/codebase-index/perf-metrics.d.ts +19 -0
  40. package/dist/codebase-index/project-server-protocol.d.ts +2 -0
  41. package/dist/codebase-index/project-server-query-cache.d.ts +4 -0
  42. package/dist/codebase-index/project-server.js +54 -7
  43. package/dist/codebase-index/repo-map-fallback.d.ts +18 -0
  44. package/dist/codebase-index/repo-map-render.d.ts +42 -0
  45. package/dist/codebase-index/repo-map-types.d.ts +21 -0
  46. package/dist/codebase-index/repo-map.d.ts +28 -16
  47. package/dist/codebase-index/schema.d.ts +20 -0
  48. package/dist/codebase-index/worker-protocol.d.ts +47 -1
  49. package/dist/codebase-index/worker.js +11 -5
  50. package/dist/codebase-index/writer-concepts.d.ts +84 -0
  51. package/dist/codebase-index/writer-graph-decorate.d.ts +30 -0
  52. package/dist/codebase-index/writer-graph-reader.d.ts +22 -0
  53. package/dist/codebase-index/writer-rank.d.ts +78 -0
  54. package/dist/codebase-index/writer-schema.d.ts +51 -0
  55. package/dist/codebase-index/writer-vectors.d.ts +53 -0
  56. package/dist/codebase-index/writer.d.ts +59 -0
  57. package/dist/edit.js +7 -7
  58. package/dist/exec.js +1 -1
  59. package/dist/{go-parser-D7ELWQ7F.js → go-parser-QIEXU3P2.js} +4 -2
  60. package/dist/index.d.ts +48 -48
  61. package/dist/index.js +43 -19
  62. package/dist/pack.js +16 -16
  63. package/dist/{parser-dispatch-BDVCJXTV.js → parser-dispatch-37FT3EXQ.js} +4 -4
  64. package/dist/patch.js +7 -7
  65. package/dist/{py-parser-QC6MR4GB.js → py-parser-6GJTGUXK.js} +3 -3
  66. package/dist/read.js +7 -7
  67. package/dist/replace.js +7 -7
  68. package/dist/tool-tier.js +17 -17
  69. package/dist/write.js +7 -7
  70. 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-ZTCJCYLN.js";
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
- resolveIndexDir
24
- } from "./chunk-N3K2SFEI.js";
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 toErrorMessage3 } from "@wrongstack/core/utils";
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 = toErrorMessage3(err);
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: toErrorMessage3(err)
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 toErrorMessage4 } from "@wrongstack/core/utils";
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: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
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 toErrorMessage5 } from "@wrongstack/core/utils";
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: ${toErrorMessage5(err)}. Fall back to grep/glob for this lookup.`
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 * as fs4 from "node:fs/promises";
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
- return -20;
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 generateRepoMap(opts) {
1962
+ async function generateFallbackRepoMap(opts) {
1889
1963
  const projectRoot = opts.projectRoot;
1890
- const maxTokens = opts.maxTokens ?? 1200;
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 fs4.readdir(dir, { withFileTypes: true });
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(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "coverage") {
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 scoredFiles = [];
1925
- for (const file of allFiles) {
1997
+ const scored = allFiles.map((file) => {
1926
1998
  const relPath = path5.relative(projectRoot, file).replace(/\\/g, "/");
1927
- let score = 10;
1928
- if (focusSet.has(file)) {
1929
- score += 100;
1930
- }
1931
- score += getEntryPointBonus(relPath);
1932
- scoredFiles.push({ file, score, relPath });
1933
- }
1934
- scoredFiles.sort((a, b) => b.score - a.score);
1935
- const includedSections = [];
1936
- const includedFiles = [];
1937
- let currentChars = 0;
1938
- for (const candidate of scoredFiles) {
1939
- if (currentChars >= charBudget) break;
1940
- try {
1941
- const content = await fs4.readFile(candidate.file, "utf8");
1942
- if (content.length === 0) continue;
1943
- const skeletonResult = await extractFileSkeleton({
1944
- file: candidate.file,
1945
- content,
1946
- options: {
1947
- exportsOnly: true,
1948
- collapseImports: true,
1949
- includeDocs: false,
1950
- includeLineNumbers: true,
1951
- ...opts.options ?? {}
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
- const cleanSkeleton = skeletonResult.skeleton.trim();
1955
- if (!cleanSkeleton) continue;
1956
- const section = `${candidate.relPath} (${skeletonResult.lang}):
1957
- ${cleanSkeleton.split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n")}`;
1958
- const sectionChars = section.length + 2;
1959
- if (currentChars + sectionChars > charBudget && includedFiles.length > 0) {
1960
- break;
1961
- }
1962
- includedSections.push(section);
1963
- includedFiles.push(candidate.relPath);
1964
- currentChars += sectionChars;
1965
- } catch {
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
- const map = includedSections.join("\n\n");
1969
- const estimatedTokens = Math.ceil(map.length / 4);
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: includedFiles.length,
1973
- totalFilesScanned: allFiles.length,
1974
- estimatedTokens,
1975
- rankedFiles: includedFiles
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 reference-weighted, token-budgeted Repository Map of the codebase (~1200 token default). Use it for orientation before a cross-file change. Extracts the most architecturally central types, interfaces, classes, and function signatures across the repository into a compact outline (~800-1500 tokens). Use this at the beginning of tasks or when navigating unfamiliar repositories to get a bird-eye view of the architecture.",
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 ensure candidate files are guaranteed to be mapped alongside related entry-points.\n- Use the returned line numbers (e.g. `/* L32-L45 */`) to navigate or partially read only the functions you need.',
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 fs5 from "node:fs/promises";
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 fs5.stat(targetPath);
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 fs5.readFile(targetPath, "utf8");
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 fs6 from "node:fs/promises";
2219
- import * as path6 from "node:path";
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 = path6.parse(sourceRelPath);
2382
+ const parsed = path7.parse(sourceRelPath);
2224
2383
  const candidates = [
2225
- path6.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
2226
- path6.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
2227
- path6.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
2228
- path6.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
2229
- path6.join("tests", `${parsed.name}.test${parsed.ext}`),
2230
- path6.join("tests", `${parsed.name}.spec${parsed.ext}`),
2231
- path6.join("test", `${parsed.name}.test${parsed.ext}`),
2232
- path6.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
2233
- path6.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
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 = path6.resolve(projectRoot, cand);
2396
+ const absPath = path7.resolve(projectRoot, cand);
2238
2397
  try {
2239
- const st = await fs6.stat(absPath);
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 = path6.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
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 fs7 from "node:fs";
2381
- import * as path7 from "node:path";
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 = fs7.readFileSync(filePath, "utf8");
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, relative7) {
2433
- if (path7.isAbsolute(relative7)) return relative7;
2434
- return path7.resolve(base, relative7);
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 (fs7.existsSync(resolved)) entries.add(resolved);
2600
+ if (fs8.existsSync(resolved)) entries.add(resolved);
2442
2601
  }
2443
2602
  }
2444
- const rootPkg = tryReadJson(path7.join(projectRoot, "package.json"));
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 = path7.join(wsDir, "package.json");
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 = path7.join(wsDir, "src", "index.ts");
2463
- if (fs7.existsSync(convention1)) entries.add(convention1);
2464
- const convention2 = path7.join(wsDir, "src", "main.ts");
2465
- if (fs7.existsSync(convention2)) entries.add(convention2);
2466
- const convention3 = path7.join(wsDir, "index.ts");
2467
- if (fs7.existsSync(convention3)) entries.add(convention3);
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 = path7.join(projectRoot, name);
2473
- if (fs7.existsSync(convention)) entries.add(convention);
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) => `${path7.sep}${d}${path7.sep}`);
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, path7.sep);
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, `${path7.sep}src${path7.sep}`);
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 && fs7.existsSync(candidate)) {
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 && fs7.existsSync(candidateDts)) {
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 && fs7.existsSync(candidateNoExt)) {
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 (fs7.existsSync(resolved)) entries.add(resolved);
2663
+ if (fs8.existsSync(resolved)) entries.add(resolved);
2505
2664
  const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
2506
- if (tsResolved !== resolved && fs7.existsSync(tsResolved)) {
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 = path7.resolve(projectRoot, base);
2709
+ const baseDir = path8.resolve(projectRoot, base);
2551
2710
  try {
2552
- const children = fs7.readdirSync(baseDir, { withFileTypes: true });
2711
+ const children = fs8.readdirSync(baseDir, { withFileTypes: true });
2553
2712
  for (const child of children) {
2554
2713
  if (child.isDirectory()) {
2555
- dirs.push(path7.join(baseDir, child.name));
2714
+ dirs.push(path8.join(baseDir, child.name));
2556
2715
  }
2557
2716
  }
2558
2717
  } catch {
2559
2718
  }
2560
2719
  } else {
2561
- dirs.push(path7.resolve(projectRoot, entry));
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 = path7.join(projectRoot, "pnpm-workspace.yaml");
2579
- if (!fs7.existsSync(yamlPath)) return [];
2737
+ const yamlPath = path8.join(projectRoot, "pnpm-workspace.yaml");
2738
+ if (!fs8.existsSync(yamlPath)) return [];
2580
2739
  try {
2581
- const content = fs7.readFileSync(yamlPath, "utf8");
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 = path7.dirname(importerPath);
2616
- const base = path7.resolve(dir, moduleSpecifier);
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(path7.join(candidate, "index.ts")))
2629
- results.push(path7.join(candidate, "index.ts"));
2630
- if (indexedFiles.has(path7.join(candidate, "index.tsx")))
2631
- results.push(path7.join(candidate, "index.tsx"));
2632
- if (indexedFiles.has(path7.join(candidate, "index.js")))
2633
- results.push(path7.join(candidate, "index.js"));
2634
- if (indexedFiles.has(path7.join(candidate, "index.jsx")))
2635
- results.push(path7.join(candidate, "index.jsx"));
2636
- if (indexedFiles.has(path7.join(candidate, "index.mjs")))
2637
- results.push(path7.join(candidate, "index.mjs"));
2638
- if (indexedFiles.has(path7.join(candidate, "index.cjs")))
2639
- results.push(path7.join(candidate, "index.cjs"));
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) => path7.resolve(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 = fs7.readFileSync(epFile, "utf8");
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 + path7.sep));
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(path7.join(projectRoot, "package.json"));
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(path7.join(wsDir, "package.json"));
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
+ "&": "&amp;",
3018
+ "<": "&lt;",
3019
+ ">": "&gt;",
3020
+ '"': "&quot;",
3021
+ "'": "&#39;"
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-YEISRT4S.js.map
4036
+ //# sourceMappingURL=chunk-HWXIRVKH.js.map