@wrongstack/tools 1.0.1 → 1.0.3

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 (98) hide show
  1. package/dist/_win32-resolve.d.ts +1 -6
  2. package/dist/audit.js +5 -5
  3. package/dist/bash.js +2 -2
  4. package/dist/builtin.js +30 -30
  5. package/dist/{chunk-7LQCT3LT.js → chunk-32XPUSY3.js} +3 -3
  6. package/dist/{chunk-N3K2SFEI.js → chunk-3JVSOBHU.js} +1408 -200
  7. package/dist/{chunk-7N5U2RAU.js → chunk-3LS3FYEF.js} +2 -2
  8. package/dist/{chunk-FBUP524S.js → chunk-4YKVHCQC.js} +13 -2
  9. package/dist/{chunk-KE7A76VC.js → chunk-5U5ULQX2.js} +4 -14
  10. package/dist/{chunk-NU2IZ72J.js → chunk-6LCKF5KH.js} +8 -6
  11. package/dist/{chunk-GDDXX27P.js → chunk-6ZLRRC74.js} +2 -2
  12. package/dist/{chunk-IYH4OHBP.js → chunk-77MBK46T.js} +3 -3
  13. package/dist/{chunk-DK4J2WMH.js → chunk-AMURQAJX.js} +2 -2
  14. package/dist/{chunk-ZTCJCYLN.js → chunk-CVNCSMVU.js} +49 -6
  15. package/dist/{chunk-MJSJWXBF.js → chunk-DX5KZEMK.js} +14 -2
  16. package/dist/{chunk-CGXTR34H.js → chunk-E2ZD4ERD.js} +2 -2
  17. package/dist/{chunk-DRRY6SCM.js → chunk-EMA2XENR.js} +2 -2
  18. package/dist/{chunk-3D5DJWOE.js → chunk-FC3X7GGJ.js} +11 -3
  19. package/dist/{chunk-T3SMFF5Q.js → chunk-GU7QJ37F.js} +2 -2
  20. package/dist/{chunk-4WPBS45A.js → chunk-IRUEHGCR.js} +3 -3
  21. package/dist/{chunk-WEEVSWQB.js → chunk-KIQTABBJ.js} +3 -3
  22. package/dist/{chunk-6H4ERJP7.js → chunk-KYVOTHG7.js} +2 -2
  23. package/dist/{chunk-O6BY43ZB.js → chunk-MIV2RU5B.js} +3 -3
  24. package/dist/{chunk-5KS24LTS.js → chunk-NOCU555K.js} +2 -2
  25. package/dist/{chunk-6GOZTS6F.js → chunk-OUEH4I4X.js} +3 -3
  26. package/dist/{chunk-YEISRT4S.js → chunk-PNLDTRC5.js} +1429 -242
  27. package/dist/{chunk-GKSG5CPH.js → chunk-QU2VRCMM.js} +2 -2
  28. package/dist/{chunk-VVZPQPTY.js → chunk-REWOZIA4.js} +5 -3
  29. package/dist/{chunk-MGPR7VG3.js → chunk-T7YVY2BV.js} +2 -2
  30. package/dist/{chunk-IAYMTTR2.js → chunk-TYRVYBHW.js} +27 -23
  31. package/dist/{chunk-LQSOU63Z.js → chunk-VKAJMHVG.js} +2 -2
  32. package/dist/{chunk-E7JE3UEX.js → chunk-VO6M2FVG.js} +2 -2
  33. package/dist/{chunk-NJX2RXKJ.js → chunk-YDTRKEYY.js} +11 -3
  34. package/dist/{chunk-MK2JK35J.js → chunk-YECHJJJF.js} +3 -3
  35. package/dist/{chunk-QA6IASYX.js → chunk-Z34CUZCZ.js} +3 -3
  36. package/dist/{chunk-UCYQTZ7F.js → chunk-ZKC5UNGD.js} +56 -2
  37. package/dist/codebase-index/atlas-brief.d.ts +90 -0
  38. package/dist/codebase-index/atlas-export.d.ts +38 -0
  39. package/dist/codebase-index/atlas-projection.d.ts +114 -0
  40. package/dist/codebase-index/atlas-types.d.ts +90 -0
  41. package/dist/codebase-index/background-indexer.d.ts +15 -4
  42. package/dist/codebase-index/codebase-context-tool.d.ts +54 -0
  43. package/dist/codebase-index/concept-enrichment.d.ts +154 -0
  44. package/dist/codebase-index/context-retrieval.d.ts +91 -0
  45. package/dist/codebase-index/embedding-pass.d.ts +95 -0
  46. package/dist/codebase-index/graph-adjacency-cache.d.ts +36 -0
  47. package/dist/codebase-index/graph-rank-pass.d.ts +46 -0
  48. package/dist/codebase-index/graph-rank.d.ts +198 -0
  49. package/dist/codebase-index/index-service.d.ts +15 -1
  50. package/dist/codebase-index/index.d.ts +34 -19
  51. package/dist/codebase-index/index.js +128 -11
  52. package/dist/codebase-index/parser-worker-script.js +4 -4
  53. package/dist/codebase-index/perf-metrics.d.ts +19 -0
  54. package/dist/codebase-index/project-server-protocol.d.ts +2 -0
  55. package/dist/codebase-index/project-server-query-cache.d.ts +4 -0
  56. package/dist/codebase-index/project-server.js +55 -8
  57. package/dist/codebase-index/repo-map-fallback.d.ts +18 -0
  58. package/dist/codebase-index/repo-map-render.d.ts +42 -0
  59. package/dist/codebase-index/repo-map-types.d.ts +21 -0
  60. package/dist/codebase-index/repo-map.d.ts +28 -16
  61. package/dist/codebase-index/schema.d.ts +20 -0
  62. package/dist/codebase-index/worker-protocol.d.ts +47 -1
  63. package/dist/codebase-index/worker.js +12 -6
  64. package/dist/codebase-index/writer-concepts.d.ts +84 -0
  65. package/dist/codebase-index/writer-graph-decorate.d.ts +30 -0
  66. package/dist/codebase-index/writer-graph-reader.d.ts +22 -0
  67. package/dist/codebase-index/writer-rank.d.ts +78 -0
  68. package/dist/codebase-index/writer-schema.d.ts +51 -0
  69. package/dist/codebase-index/writer-vectors.d.ts +53 -0
  70. package/dist/codebase-index/writer.d.ts +59 -0
  71. package/dist/edit.js +8 -8
  72. package/dist/exec.js +2 -2
  73. package/dist/fetch.js +1 -1
  74. package/dist/format.js +5 -5
  75. package/dist/{go-parser-D7ELWQ7F.js → go-parser-EMKTAAD3.js} +5 -3
  76. package/dist/index.d.ts +48 -48
  77. package/dist/index.js +57 -33
  78. package/dist/install.js +5 -5
  79. package/dist/languages/index.js +3 -3
  80. package/dist/{legacy-bridge-YXANP5QB.js → legacy-bridge-WVQ4BHDI.js} +5 -5
  81. package/dist/lint.js +5 -5
  82. package/dist/outdated.js +2 -2
  83. package/dist/pack.js +31 -31
  84. package/dist/parser-dispatch-UQAB4E6V.js +14 -0
  85. package/dist/patch.js +8 -8
  86. package/dist/plan.js +2 -2
  87. package/dist/{py-parser-QC6MR4GB.js → py-parser-DDG53RXI.js} +4 -4
  88. package/dist/read.js +8 -8
  89. package/dist/replace.js +8 -8
  90. package/dist/task.js +2 -2
  91. package/dist/test.js +5 -5
  92. package/dist/todo.js +1 -1
  93. package/dist/tool-tier.js +32 -32
  94. package/dist/typecheck.js +5 -5
  95. package/dist/win32.js +1 -1
  96. package/dist/write.js +8 -8
  97. package/package.json +5 -5
  98. package/dist/parser-dispatch-BDVCJXTV.js +0 -14
@@ -3,9 +3,11 @@ import {
3
3
  } from "./chunk-XRNN44QY.js";
4
4
  import {
5
5
  spawnStream
6
- } from "./chunk-DRRY6SCM.js";
6
+ } from "./chunk-EMA2XENR.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-CVNCSMVU.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-3JVSOBHU.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,15 @@ 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 { ToolValidationError } from "@wrongstack/core/types";
879
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
954
880
  var codebaseIncomingCallsTool = {
955
881
  name: "codebase-incoming-calls",
956
882
  category: "Project",
@@ -987,6 +913,12 @@ var codebaseIncomingCallsTool = {
987
913
  required: ["symbol"]
988
914
  },
989
915
  async execute(input, ctx) {
916
+ if (!input?.symbol || typeof input.symbol !== "string" || !input.symbol.trim()) {
917
+ throw new ToolValidationError({
918
+ message: "codebase-incoming-calls: symbol is required and cannot be empty",
919
+ field: "symbol"
920
+ });
921
+ }
990
922
  const state = getIndexState();
991
923
  if (state.lastError) {
992
924
  const circuit = state.circuit;
@@ -1024,7 +956,7 @@ var codebaseIncomingCallsTool = {
1024
956
  symbol: input.symbol,
1025
957
  calls: [],
1026
958
  total: 0,
1027
- indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
959
+ indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep for this lookup.`
1028
960
  };
1029
961
  }
1030
962
  const { calls, symbolFound, ambiguous, totalMatches, stale } = serviced;
@@ -1082,11 +1014,11 @@ var codebaseIncomingCallsTool = {
1082
1014
  };
1083
1015
 
1084
1016
  // src/codebase-index/codebase-index-tool.ts
1085
- import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
1017
+ import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
1086
1018
 
1087
1019
  // src/codebase-index/codebase-search-tool.ts
1088
- import { ToolValidationError } from "@wrongstack/core/types";
1089
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
1020
+ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
1021
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
1090
1022
  var INDEXABLE_LANG_IDS = [
1091
1023
  ...new Set(Object.values(EXT_TO_LANG))
1092
1024
  ].sort();
@@ -1163,13 +1095,13 @@ var codebaseSearchTool = {
1163
1095
  },
1164
1096
  async execute(input, ctx, execOpts) {
1165
1097
  if (!input?.query || typeof input.query !== "string" || !input.query.trim()) {
1166
- throw new ToolValidationError({
1098
+ throw new ToolValidationError2({
1167
1099
  message: "codebase-search: query is required and cannot be empty",
1168
1100
  field: "query"
1169
1101
  });
1170
1102
  }
1171
1103
  if (input.lspKind !== void 0 && (!Number.isInteger(input.lspKind) || input.lspKind < 1 || input.lspKind > 26)) {
1172
- throw new ToolValidationError({
1104
+ throw new ToolValidationError2({
1173
1105
  message: "codebase-search: lspKind must be an integer between 1 and 26",
1174
1106
  field: "lspKind"
1175
1107
  });
@@ -1219,7 +1151,7 @@ var codebaseSearchTool = {
1219
1151
  results: [],
1220
1152
  total: 0,
1221
1153
  query: input.query,
1222
- indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep/glob for this lookup.`
1154
+ indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep/glob for this lookup.`
1223
1155
  };
1224
1156
  }
1225
1157
  const { results, total, stale, indexSummary } = searched;
@@ -1287,7 +1219,7 @@ var codebaseIndexTool = {
1287
1219
  signal?.throwIfAborted();
1288
1220
  if (input.langs) {
1289
1221
  if (!Array.isArray(input.langs) || input.langs.length === 0) {
1290
- throw new ToolValidationError2({
1222
+ throw new ToolValidationError3({
1291
1223
  message: "codebase-index: langs cannot be an empty array. Pass at least one valid language or omit langs.",
1292
1224
  field: "langs"
1293
1225
  });
@@ -1296,7 +1228,7 @@ var codebaseIndexTool = {
1296
1228
  (lang) => !INDEXABLE_LANG_IDS.includes(lang)
1297
1229
  );
1298
1230
  if (unknown.length > 0) {
1299
- throw new ToolValidationError2({
1231
+ throw new ToolValidationError3({
1300
1232
  message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
1301
1233
  field: "langs"
1302
1234
  });
@@ -1345,7 +1277,87 @@ var codebaseIndexTool = {
1345
1277
  }
1346
1278
  };
1347
1279
 
1280
+ // src/codebase-index/codebase-invariant-check-tool.ts
1281
+ import * as fs2 from "node:fs/promises";
1282
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
1283
+ var codebaseInvariantCheckTool = {
1284
+ name: "codebase-invariant-check",
1285
+ category: "Inspect",
1286
+ icon: "index",
1287
+ permission: "auto",
1288
+ subjectKey: "file",
1289
+ mutating: false,
1290
+ capabilities: ["fs.read"],
1291
+ 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.",
1292
+ 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.",
1293
+ inputSchema: {
1294
+ type: "object",
1295
+ properties: {
1296
+ file: {
1297
+ type: "string",
1298
+ description: "Path to existing source file to compare against candidate modifications."
1299
+ },
1300
+ originalCode: {
1301
+ type: "string",
1302
+ description: "Original source code string (if not reading from an existing file)."
1303
+ },
1304
+ modifiedCode: {
1305
+ type: "string",
1306
+ description: "The candidate modified code string to validate."
1307
+ },
1308
+ lang: {
1309
+ type: "string",
1310
+ description: "Language hint (ts, py, go, rs, etc.). Inferred from file path if omitted."
1311
+ }
1312
+ },
1313
+ required: ["modifiedCode"],
1314
+ additionalProperties: false
1315
+ },
1316
+ async execute(input, ctx) {
1317
+ try {
1318
+ let originalCode = input.originalCode;
1319
+ let lang = input.lang;
1320
+ if (!originalCode && input.file) {
1321
+ const resolved = await safeResolveProjectPath(input.file, ctx);
1322
+ originalCode = await fs2.readFile(resolved, "utf8");
1323
+ if (!lang) {
1324
+ lang = detectLang(resolved) ?? "ts";
1325
+ }
1326
+ }
1327
+ if (originalCode === void 0) {
1328
+ return {
1329
+ valid: false,
1330
+ violations: [],
1331
+ summary: "Neither originalCode nor file was provided for invariant comparison.",
1332
+ error: "Missing originalCode or file path."
1333
+ };
1334
+ }
1335
+ const res = await polyglotInvariantEngine.evaluate({
1336
+ originalCode,
1337
+ modifiedCode: input.modifiedCode,
1338
+ lang,
1339
+ filePath: input.file
1340
+ });
1341
+ const summary = res.valid ? "AST Invariants PASSED. Mutation is 100% backward compatible." : `AST Invariants FAILED with ${res.violations.length} violation(s):
1342
+ ` + res.violations.map((v) => ` - [${v.ruleId}] ${v.symbolName}: ${v.message}`).join("\n");
1343
+ return {
1344
+ valid: res.valid,
1345
+ violations: res.violations,
1346
+ summary
1347
+ };
1348
+ } catch (err) {
1349
+ return {
1350
+ valid: false,
1351
+ violations: [],
1352
+ summary: `Error during invariant evaluation: ${toErrorMessage5(err)}`,
1353
+ error: toErrorMessage5(err)
1354
+ };
1355
+ }
1356
+ }
1357
+ };
1358
+
1348
1359
  // src/codebase-index/codebase-outgoing-calls-tool.ts
1360
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
1349
1361
  import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
1350
1362
  var codebaseOutgoingCallsTool = {
1351
1363
  name: "codebase-outgoing-calls",
@@ -1383,6 +1395,12 @@ var codebaseOutgoingCallsTool = {
1383
1395
  required: ["symbol"]
1384
1396
  },
1385
1397
  async execute(input, ctx) {
1398
+ if (!input?.symbol || typeof input.symbol !== "string" || !input.symbol.trim()) {
1399
+ throw new ToolValidationError4({
1400
+ message: "codebase-outgoing-calls: symbol is required and cannot be empty",
1401
+ field: "symbol"
1402
+ });
1403
+ }
1386
1404
  const state = getIndexState();
1387
1405
  if (state.lastError) {
1388
1406
  const circuit = state.circuit;
@@ -1481,9 +1499,16 @@ var codebaseOutgoingCallsTool = {
1481
1499
  import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
1482
1500
 
1483
1501
  // src/codebase-index/repo-map.ts
1484
- import * as fs4 from "node:fs/promises";
1502
+ import { existsSync } from "node:fs";
1503
+ import * as path6 from "node:path";
1504
+
1505
+ // src/codebase-index/repo-map-fallback.ts
1506
+ import * as fs5 from "node:fs/promises";
1485
1507
  import * as path5 from "node:path";
1486
1508
 
1509
+ // src/codebase-index/repo-map-render.ts
1510
+ import * as fs4 from "node:fs/promises";
1511
+
1487
1512
  // src/codebase-index/skeleton-extractor.ts
1488
1513
  import * as fs3 from "node:fs/promises";
1489
1514
  import * as path4 from "node:path";
@@ -1870,40 +1895,102 @@ ${res.skeleton}`
1870
1895
  };
1871
1896
  }
1872
1897
 
1873
- // src/codebase-index/repo-map.ts
1898
+ // src/codebase-index/repo-map-render.ts
1899
+ var TRUNCATION_NOTE = "\n // \u2026 signatures truncated to fit the token budget";
1900
+ function truncateAtLine(text, limit) {
1901
+ if (text.length <= limit) return text;
1902
+ const cut = text.slice(0, limit);
1903
+ const lastNewline = cut.lastIndexOf("\n");
1904
+ return lastNewline > 0 ? cut.slice(0, lastNewline) : cut;
1905
+ }
1906
+ var COMMENT_ONLY_LINE = /^\s*(?:\/\/|\/\*|\*\/|\*|#|--|<!--|-->)/;
1907
+ function stripCommentLines(body) {
1908
+ const kept = body.split("\n").filter((line) => !COMMENT_ONLY_LINE.test(line));
1909
+ const joined = kept.join("\n").trim();
1910
+ return joined.length > 0 ? joined : body;
1911
+ }
1912
+ async function renderSkeletonSections(candidates, charBudget, options = {}) {
1913
+ const sections = [];
1914
+ const files = [];
1915
+ const maxSectionChars = options.maxSectionChars ?? Number.POSITIVE_INFINITY;
1916
+ let chars = 0;
1917
+ for (const candidate of candidates) {
1918
+ if (chars >= charBudget) break;
1919
+ let content;
1920
+ try {
1921
+ content = await fs4.readFile(candidate.absolute, "utf8");
1922
+ } catch {
1923
+ continue;
1924
+ }
1925
+ if (content.length === 0) continue;
1926
+ let skeleton;
1927
+ try {
1928
+ skeleton = await extractFileSkeleton({
1929
+ file: candidate.absolute,
1930
+ content,
1931
+ options: {
1932
+ exportsOnly: true,
1933
+ collapseImports: true,
1934
+ includeDocs: false,
1935
+ includeLineNumbers: true,
1936
+ ...options.skeleton ?? {}
1937
+ }
1938
+ });
1939
+ } catch {
1940
+ continue;
1941
+ }
1942
+ const body = skeleton.skeleton.trim();
1943
+ if (!body) continue;
1944
+ const indented = (options.signaturesOnly === true ? stripCommentLines(body) : body).split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n");
1945
+ const header = `${candidate.relative} (${skeleton.lang}):
1946
+ `;
1947
+ const room = Math.min(maxSectionChars, charBudget - chars) - header.length - TRUNCATION_NOTE.length - 2;
1948
+ let section;
1949
+ if (indented.length <= room) {
1950
+ section = header + indented;
1951
+ } else {
1952
+ if (room <= 0) break;
1953
+ section = `${header}${truncateAtLine(indented, room)}${TRUNCATION_NOTE}`;
1954
+ }
1955
+ const sectionChars = section.length + 2;
1956
+ if (chars + sectionChars > charBudget && files.length > 0) break;
1957
+ sections.push(section);
1958
+ files.push(candidate.relative);
1959
+ chars += sectionChars;
1960
+ }
1961
+ return { sections, files, chars };
1962
+ }
1963
+
1964
+ // src/codebase-index/repo-map-fallback.ts
1874
1965
  var ENTRY_POINT_REGEX = /(?:^|[/\\])(?:index|main|app|server|cli|lib|api|types|routes|mod)\.[a-zA-Z0-9]+$/i;
1875
1966
  var TEST_OR_MOCK_REGEX = /(?:test|spec|mock|fixture|bench|example)[s]?[/\\._]/i;
1967
+ var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage"]);
1876
1968
  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
- }
1969
+ if (TEST_OR_MOCK_REGEX.test(relPath)) return -20;
1970
+ if (ENTRY_POINT_REGEX.test(relPath)) return 25;
1883
1971
  const depth = relPath.split(/[/\\]/).length;
1884
1972
  if (depth <= 2) return 10;
1885
1973
  if (depth <= 3) return 5;
1886
1974
  return 0;
1887
1975
  }
1888
- async function generateRepoMap(opts) {
1976
+ async function generateFallbackRepoMap(opts) {
1889
1977
  const projectRoot = opts.projectRoot;
1890
- const maxTokens = opts.maxTokens ?? 1200;
1891
- const charBudget = maxTokens * 4;
1978
+ const charBudget = (opts.maxTokens ?? 1200) * 4;
1892
1979
  const focusSet = new Set((opts.focusFiles ?? []).map((f) => path5.resolve(projectRoot, f)));
1980
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
1893
1981
  const allFiles = [];
1894
1982
  async function scan(dir) {
1895
- if (allFiles.length > 500) return;
1896
1983
  let entries;
1897
1984
  try {
1898
- entries = await fs4.readdir(dir, { withFileTypes: true });
1985
+ entries = await fs5.readdir(dir, { withFileTypes: true });
1899
1986
  } catch {
1900
1987
  return;
1901
1988
  }
1902
1989
  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
- }
1990
+ if (entry.name.startsWith(".") || ALWAYS_SKIP.has(entry.name)) continue;
1906
1991
  const fullPath = path5.join(dir, entry.name);
1992
+ const rel = path5.relative(projectRoot, fullPath).replace(/\\/g, "/");
1993
+ if (isGitIgnored(rel, entry.isDirectory())) continue;
1907
1994
  if (entry.isDirectory()) {
1908
1995
  await scan(fullPath);
1909
1996
  } else if (entry.isFile() && isIndexablePath(fullPath)) {
@@ -1921,58 +2008,141 @@ async function generateRepoMap(opts) {
1921
2008
  rankedFiles: []
1922
2009
  };
1923
2010
  }
1924
- const scoredFiles = [];
1925
- for (const file of allFiles) {
2011
+ const scored = allFiles.map((file) => {
1926
2012
  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
- }
2013
+ const score = 10 + (focusSet.has(file) ? 100 : 0) + getEntryPointBonus(relPath);
2014
+ return { file, relPath, score };
2015
+ });
2016
+ scored.sort((a, b) => b.score - a.score || a.relPath.localeCompare(b.relPath));
2017
+ const { sections, files } = await renderSkeletonSections(
2018
+ scored.map((s) => ({ absolute: s.file, relative: s.relPath })),
2019
+ charBudget,
2020
+ { skeleton: opts.options }
2021
+ );
2022
+ const map = sections.join("\n\n");
2023
+ return {
2024
+ map,
2025
+ filesCount: files.length,
2026
+ totalFilesScanned: allFiles.length,
2027
+ estimatedTokens: Math.ceil(map.length / 4),
2028
+ rankedFiles: files
2029
+ };
2030
+ }
2031
+
2032
+ // src/codebase-index/repo-map.ts
2033
+ var RANKED_FETCH_LIMIT = 400;
2034
+ var INDEX_DB_FILE = "index.db";
2035
+ var MAX_CLUSTERS = 16;
2036
+ var MAX_HOTSPOTS = 12;
2037
+ var HEADER_BUDGET_SHARE = 0.4;
2038
+ var TARGET_BODY_FILES = 6;
2039
+ var MIN_SECTION_CHARS = 300;
2040
+ function buildClusters(rows, relativeOf, packageCounts) {
2041
+ const byLabel = /* @__PURE__ */ new Map();
2042
+ for (const row of rows) {
2043
+ const relative12 = relativeOf(row.file);
2044
+ const label = row.package || relative12.split("/")[0] || "(root)";
2045
+ const existing = byLabel.get(label);
2046
+ if (existing === void 0) {
2047
+ byLabel.set(label, {
2048
+ label,
2049
+ fileCount: packageCounts.get(row.package) ?? 0,
2050
+ rankedCount: 1,
2051
+ hub: relative12,
2052
+ hubRank: row.rank
1953
2053
  });
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 {
2054
+ continue;
2055
+ }
2056
+ existing.rankedCount += 1;
2057
+ if (row.rank > existing.hubRank) {
2058
+ existing.hub = relative12;
2059
+ existing.hubRank = row.rank;
2060
+ }
2061
+ }
2062
+ return [...byLabel.values()].sort(
2063
+ (a, b) => b.hubRank - a.hubRank || a.label.localeCompare(b.label)
2064
+ );
2065
+ }
2066
+ function renderHeader(clusters, hotspots, relativeOf, budget) {
2067
+ const lines = ["// Repo map \u2014 ranked by graph centrality (1.00 = most central)."];
2068
+ if (clusters.length > 0) {
2069
+ lines.push("", "// Clusters (hub = most central file):");
2070
+ for (const cluster of clusters.slice(0, MAX_CLUSTERS)) {
2071
+ const size = cluster.fileCount > 0 ? `${cluster.fileCount} files` : "unlabelled";
2072
+ lines.push(`// ${cluster.label} (${size}) hub: ${cluster.hub}`);
2073
+ }
2074
+ }
2075
+ if (hotspots.length > 0) {
2076
+ lines.push("", "// Hotspots:");
2077
+ for (const row of hotspots.slice(0, MAX_HOTSPOTS)) {
2078
+ lines.push(
2079
+ `// ${row.rank.toFixed(2)} in=${row.inDeg} out=${row.outDeg} ${relativeOf(row.file)}`
2080
+ );
2081
+ }
2082
+ }
2083
+ while (lines.length > 1 && lines.join("\n").length + 2 > budget) lines.pop();
2084
+ return lines.join("\n");
2085
+ }
2086
+ async function generateRepoMap(opts) {
2087
+ const projectRoot = opts.projectRoot;
2088
+ const charBudget = (opts.maxTokens ?? 1200) * 4;
2089
+ let ranked = [];
2090
+ let packageCounts = /* @__PURE__ */ new Map();
2091
+ let totalRankedFiles = 0;
2092
+ let store;
2093
+ try {
2094
+ if (!existsSync(path6.join(resolveIndexDir(projectRoot, opts.indexDir), INDEX_DB_FILE))) {
2095
+ return generateFallbackRepoMap(opts);
1966
2096
  }
2097
+ store = indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
2098
+ totalRankedFiles = store.getRankCounts().files;
2099
+ if (totalRankedFiles > 0) {
2100
+ ranked = store.getRankedFiles(RANKED_FETCH_LIMIT);
2101
+ packageCounts = store.getPackageFileCounts();
2102
+ }
2103
+ } catch {
2104
+ ranked = [];
2105
+ } finally {
2106
+ if (store !== void 0) indexStorePool.release(store);
1967
2107
  }
1968
- const map = includedSections.join("\n\n");
1969
- const estimatedTokens = Math.ceil(map.length / 4);
2108
+ if (ranked.length === 0) return generateFallbackRepoMap(opts);
2109
+ const relativeOf = (file) => {
2110
+ const relative12 = path6.relative(projectRoot, file);
2111
+ if (!relative12 || relative12.startsWith("..")) return posixIndexPath(file);
2112
+ return posixIndexPath(relative12);
2113
+ };
2114
+ const clusters = buildClusters(ranked, relativeOf, packageCounts);
2115
+ const header = renderHeader(clusters, ranked, relativeOf, charBudget * HEADER_BUDGET_SHARE);
2116
+ const focus = (opts.focusFiles ?? []).map((f) => path6.resolve(projectRoot, f));
2117
+ const focusIndex = new Map(focus.map((file, i) => [file, i]));
2118
+ const candidates = ranked.map((row) => ({
2119
+ row,
2120
+ absolute: path6.resolve(projectRoot, row.file)
2121
+ })).sort((a, b) => {
2122
+ const fa = focusIndex.get(a.absolute) ?? Number.POSITIVE_INFINITY;
2123
+ const fb = focusIndex.get(b.absolute) ?? Number.POSITIVE_INFINITY;
2124
+ if (fa !== fb) return fa - fb;
2125
+ return b.row.rank - a.row.rank;
2126
+ }).map(({ row, absolute }) => ({ absolute, relative: relativeOf(row.file) }));
2127
+ const known = new Set(candidates.map((c) => c.absolute));
2128
+ const missingFocus = focus.filter((file) => !known.has(file)).map((file) => ({ absolute: file, relative: relativeOf(file) }));
2129
+ const bodyBudget = Math.max(0, charBudget - header.length - 2);
2130
+ const { sections, files } = await renderSkeletonSections(
2131
+ [...missingFocus, ...candidates],
2132
+ bodyBudget,
2133
+ {
2134
+ skeleton: opts.options,
2135
+ maxSectionChars: Math.max(MIN_SECTION_CHARS, bodyBudget / TARGET_BODY_FILES),
2136
+ signaturesOnly: true
2137
+ }
2138
+ );
2139
+ const map = [header, ...sections].join("\n\n");
1970
2140
  return {
1971
2141
  map,
1972
- filesCount: includedFiles.length,
1973
- totalFilesScanned: allFiles.length,
1974
- estimatedTokens,
1975
- rankedFiles: includedFiles
2142
+ filesCount: files.length,
2143
+ totalFilesScanned: totalRankedFiles,
2144
+ estimatedTokens: Math.ceil(map.length / 4),
2145
+ rankedFiles: files
1976
2146
  };
1977
2147
  }
1978
2148
 
@@ -1984,8 +2154,8 @@ var codebaseRepoMapTool = {
1984
2154
  permission: "auto",
1985
2155
  mutating: false,
1986
2156
  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.',
2157
+ 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.",
2158
+ 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
2159
  inputSchema: {
1990
2160
  type: "object",
1991
2161
  properties: {
@@ -2007,7 +2177,10 @@ var codebaseRepoMapTool = {
2007
2177
  const result = await generateRepoMap({
2008
2178
  projectRoot,
2009
2179
  maxTokens: input.maxTokens,
2010
- focusFiles: input.focusFiles
2180
+ focusFiles: input.focusFiles,
2181
+ // Honour a caller-supplied index location so the map reads the same
2182
+ // index the other codebase-* tools do.
2183
+ indexDir: codebaseIndexDirOverride(ctx)
2011
2184
  });
2012
2185
  return {
2013
2186
  status: "ok",
@@ -2028,7 +2201,7 @@ var codebaseRepoMapTool = {
2028
2201
  };
2029
2202
 
2030
2203
  // src/codebase-index/codebase-skeleton-tool.ts
2031
- import * as fs5 from "node:fs/promises";
2204
+ import * as fs6 from "node:fs/promises";
2032
2205
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
2033
2206
  var codebaseSkeletonTool = {
2034
2207
  name: "codebase-skeleton",
@@ -2073,7 +2246,7 @@ var codebaseSkeletonTool = {
2073
2246
  const targetPath = await safeResolveProjectPath(input.path, ctx);
2074
2247
  let stat3;
2075
2248
  try {
2076
- stat3 = await fs5.stat(targetPath);
2249
+ stat3 = await fs6.stat(targetPath);
2077
2250
  } catch (err) {
2078
2251
  throw new Error(
2079
2252
  `codebase-skeleton: file or directory not found at "${input.path}": ${toErrorMessage8(err)}`
@@ -2109,7 +2282,7 @@ var codebaseSkeletonTool = {
2109
2282
  files: dirResult.files
2110
2283
  };
2111
2284
  }
2112
- const content = await fs5.readFile(targetPath, "utf8");
2285
+ const content = await fs6.readFile(targetPath, "utf8");
2113
2286
  const fileResult = await extractFileSkeleton({
2114
2287
  file: targetPath,
2115
2288
  content,
@@ -2215,28 +2388,28 @@ var codebaseStatsTool = {
2215
2388
  };
2216
2389
 
2217
2390
  // src/codebase-index/codebase-targeted-test-tool.ts
2218
- import * as fs6 from "node:fs/promises";
2219
- import * as path6 from "node:path";
2391
+ import * as fs7 from "node:fs/promises";
2392
+ import * as path7 from "node:path";
2220
2393
  import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
2221
2394
  var TEST_FILE_REGEX2 = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
2222
2395
  async function findConventionTestFiles(projectRoot, sourceRelPath) {
2223
- const parsed = path6.parse(sourceRelPath);
2396
+ const parsed = path7.parse(sourceRelPath);
2224
2397
  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}`)
2398
+ path7.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
2399
+ path7.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
2400
+ path7.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
2401
+ path7.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
2402
+ path7.join("tests", `${parsed.name}.test${parsed.ext}`),
2403
+ path7.join("tests", `${parsed.name}.spec${parsed.ext}`),
2404
+ path7.join("test", `${parsed.name}.test${parsed.ext}`),
2405
+ path7.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
2406
+ path7.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
2234
2407
  ];
2235
2408
  const found = [];
2236
2409
  for (const cand of candidates) {
2237
- const absPath = path6.resolve(projectRoot, cand);
2410
+ const absPath = path7.resolve(projectRoot, cand);
2238
2411
  try {
2239
- const st = await fs6.stat(absPath);
2412
+ const st = await fs7.stat(absPath);
2240
2413
  if (st.isFile()) {
2241
2414
  found.push(cand.replace(/\\/g, "/"));
2242
2415
  }
@@ -2293,7 +2466,7 @@ var codebaseTargetedTestTool = {
2293
2466
  transitive: true
2294
2467
  });
2295
2468
  for (const site of serviced.calls) {
2296
- const relPath = path6.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
2469
+ const relPath = path7.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
2297
2470
  if (TEST_FILE_REGEX2.test(relPath)) {
2298
2471
  suitesSet.add(relPath);
2299
2472
  }
@@ -2377,8 +2550,8 @@ ${streamRes.stderr}`.trim(),
2377
2550
  };
2378
2551
 
2379
2552
  // src/codebase-index/dead-code-scan.ts
2380
- import * as fs7 from "node:fs";
2381
- import * as path7 from "node:path";
2553
+ import * as fs8 from "node:fs";
2554
+ import * as path8 from "node:path";
2382
2555
  var deadCodeScanTool = {
2383
2556
  name: "dead-code-scan",
2384
2557
  category: "Project",
@@ -2423,25 +2596,25 @@ var deadCodeScanTool = {
2423
2596
  };
2424
2597
  function tryReadJson(filePath) {
2425
2598
  try {
2426
- const raw = fs7.readFileSync(filePath, "utf8");
2599
+ const raw = fs8.readFileSync(filePath, "utf8");
2427
2600
  return JSON.parse(raw);
2428
2601
  } catch {
2429
2602
  return null;
2430
2603
  }
2431
2604
  }
2432
- function resolveAgainst(base, relative7) {
2433
- if (path7.isAbsolute(relative7)) return relative7;
2434
- return path7.resolve(base, relative7);
2605
+ function resolveAgainst(base, relative12) {
2606
+ if (path8.isAbsolute(relative12)) return relative12;
2607
+ return path8.resolve(base, relative12);
2435
2608
  }
2436
2609
  function discoverEntryPoints(projectRoot, userEntryPoints) {
2437
2610
  const entries = /* @__PURE__ */ new Set();
2438
2611
  if (userEntryPoints) {
2439
2612
  for (const ep of userEntryPoints) {
2440
2613
  const resolved = resolveAgainst(projectRoot, ep);
2441
- if (fs7.existsSync(resolved)) entries.add(resolved);
2614
+ if (fs8.existsSync(resolved)) entries.add(resolved);
2442
2615
  }
2443
2616
  }
2444
- const rootPkg = tryReadJson(path7.join(projectRoot, "package.json"));
2617
+ const rootPkg = tryReadJson(path8.join(projectRoot, "package.json"));
2445
2618
  if (rootPkg) {
2446
2619
  addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
2447
2620
  }
@@ -2455,45 +2628,45 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
2455
2628
  workspaces = [];
2456
2629
  }
2457
2630
  for (const wsDir of workspaces) {
2458
- const pkgJsonPath = path7.join(wsDir, "package.json");
2631
+ const pkgJsonPath = path8.join(wsDir, "package.json");
2459
2632
  const pkg = tryReadJson(pkgJsonPath);
2460
2633
  if (pkg) {
2461
2634
  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);
2635
+ const convention1 = path8.join(wsDir, "src", "index.ts");
2636
+ if (fs8.existsSync(convention1)) entries.add(convention1);
2637
+ const convention2 = path8.join(wsDir, "src", "main.ts");
2638
+ if (fs8.existsSync(convention2)) entries.add(convention2);
2639
+ const convention3 = path8.join(wsDir, "index.ts");
2640
+ if (fs8.existsSync(convention3)) entries.add(convention3);
2468
2641
  }
2469
2642
  }
2470
2643
  if (!rootPkg || !workspaces.length) {
2471
2644
  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);
2645
+ const convention = path8.join(projectRoot, name);
2646
+ if (fs8.existsSync(convention)) entries.add(convention);
2474
2647
  }
2475
2648
  }
2476
2649
  return [...entries];
2477
2650
  }
2478
2651
  var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
2479
- var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path7.sep}${d}${path7.sep}`);
2652
+ var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path8.sep}${d}${path8.sep}`);
2480
2653
  function trySourceEquivalent(resolved) {
2481
- resolved = resolved.replace(/[/\\]/g, path7.sep);
2654
+ resolved = resolved.replace(/[/\\]/g, path8.sep);
2482
2655
  for (const marker of BUILD_OUTPUT_DIR_NAMES) {
2483
2656
  const idx = resolved.indexOf(marker);
2484
2657
  if (idx === -1) continue;
2485
- const base = resolved.replace(marker, `${path7.sep}src${path7.sep}`);
2658
+ const base = resolved.replace(marker, `${path8.sep}src${path8.sep}`);
2486
2659
  const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
2487
- if (candidate !== base && fs7.existsSync(candidate)) {
2660
+ if (candidate !== base && fs8.existsSync(candidate)) {
2488
2661
  return candidate;
2489
2662
  }
2490
2663
  const dtsStripped = base.replace(/\.d\.ts$/, "");
2491
2664
  const candidateDts = dtsStripped + ".ts";
2492
- if (candidateDts !== base && candidateDts !== candidate && fs7.existsSync(candidateDts)) {
2665
+ if (candidateDts !== base && candidateDts !== candidate && fs8.existsSync(candidateDts)) {
2493
2666
  return candidateDts;
2494
2667
  }
2495
2668
  const candidateNoExt = base + ".ts";
2496
- if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs7.existsSync(candidateNoExt)) {
2669
+ if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs8.existsSync(candidateNoExt)) {
2497
2670
  return candidateNoExt;
2498
2671
  }
2499
2672
  }
@@ -2501,9 +2674,9 @@ function trySourceEquivalent(resolved) {
2501
2674
  }
2502
2675
  function tryAddEntryPath(pkgDir, rawPath, entries) {
2503
2676
  const resolved = resolveAgainst(pkgDir, rawPath);
2504
- if (fs7.existsSync(resolved)) entries.add(resolved);
2677
+ if (fs8.existsSync(resolved)) entries.add(resolved);
2505
2678
  const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
2506
- if (tsResolved !== resolved && fs7.existsSync(tsResolved)) {
2679
+ if (tsResolved !== resolved && fs8.existsSync(tsResolved)) {
2507
2680
  entries.add(tsResolved);
2508
2681
  }
2509
2682
  const srcAlt = trySourceEquivalent(resolved);
@@ -2547,18 +2720,18 @@ function expandGlobPattern(entry, projectRoot) {
2547
2720
  const dirs = [];
2548
2721
  if (entry.includes("*")) {
2549
2722
  const base = entry.replace(/\/\*+$/, "");
2550
- const baseDir = path7.resolve(projectRoot, base);
2723
+ const baseDir = path8.resolve(projectRoot, base);
2551
2724
  try {
2552
- const children = fs7.readdirSync(baseDir, { withFileTypes: true });
2725
+ const children = fs8.readdirSync(baseDir, { withFileTypes: true });
2553
2726
  for (const child of children) {
2554
2727
  if (child.isDirectory()) {
2555
- dirs.push(path7.join(baseDir, child.name));
2728
+ dirs.push(path8.join(baseDir, child.name));
2556
2729
  }
2557
2730
  }
2558
2731
  } catch {
2559
2732
  }
2560
2733
  } else {
2561
- dirs.push(path7.resolve(projectRoot, entry));
2734
+ dirs.push(path8.resolve(projectRoot, entry));
2562
2735
  }
2563
2736
  return dirs;
2564
2737
  }
@@ -2575,10 +2748,10 @@ function extractWorkspaceGlobs(pkg, projectRoot) {
2575
2748
  return dirs;
2576
2749
  }
2577
2750
  function extractPnpmWorkspaceDirs(projectRoot) {
2578
- const yamlPath = path7.join(projectRoot, "pnpm-workspace.yaml");
2579
- if (!fs7.existsSync(yamlPath)) return [];
2751
+ const yamlPath = path8.join(projectRoot, "pnpm-workspace.yaml");
2752
+ if (!fs8.existsSync(yamlPath)) return [];
2580
2753
  try {
2581
- const content = fs7.readFileSync(yamlPath, "utf8");
2754
+ const content = fs8.readFileSync(yamlPath, "utf8");
2582
2755
  const dirs = [];
2583
2756
  let inPackages = false;
2584
2757
  const lines = content.split("\n");
@@ -2612,8 +2785,8 @@ function extractPnpmWorkspaceDirs(projectRoot) {
2612
2785
  }
2613
2786
  function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
2614
2787
  if (!moduleSpecifier.startsWith(".")) return [];
2615
- const dir = path7.dirname(importerPath);
2616
- const base = path7.resolve(dir, moduleSpecifier);
2788
+ const dir = path8.dirname(importerPath);
2789
+ const base = path8.resolve(dir, moduleSpecifier);
2617
2790
  const results = [];
2618
2791
  const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
2619
2792
  const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
@@ -2625,18 +2798,18 @@ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
2625
2798
  if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
2626
2799
  if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
2627
2800
  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"));
2801
+ if (indexedFiles.has(path8.join(candidate, "index.ts")))
2802
+ results.push(path8.join(candidate, "index.ts"));
2803
+ if (indexedFiles.has(path8.join(candidate, "index.tsx")))
2804
+ results.push(path8.join(candidate, "index.tsx"));
2805
+ if (indexedFiles.has(path8.join(candidate, "index.js")))
2806
+ results.push(path8.join(candidate, "index.js"));
2807
+ if (indexedFiles.has(path8.join(candidate, "index.jsx")))
2808
+ results.push(path8.join(candidate, "index.jsx"));
2809
+ if (indexedFiles.has(path8.join(candidate, "index.mjs")))
2810
+ results.push(path8.join(candidate, "index.mjs"));
2811
+ if (indexedFiles.has(path8.join(candidate, "index.cjs")))
2812
+ results.push(path8.join(candidate, "index.cjs"));
2640
2813
  }
2641
2814
  return [...new Set(results)];
2642
2815
  }
@@ -2668,7 +2841,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
2668
2841
  symbolById.set(s.id, s);
2669
2842
  }
2670
2843
  const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
2671
- const entryFileSet = new Set(discoveredFiles.map((f) => path7.resolve(f)));
2844
+ const entryFileSet = new Set(discoveredFiles.map((f) => path8.resolve(f)));
2672
2845
  const indexedFiles = /* @__PURE__ */ new Set();
2673
2846
  for (const s of allSymbols) indexedFiles.add(s.file);
2674
2847
  for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
@@ -2694,7 +2867,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
2694
2867
  if (scannedBarrels.has(epFile)) continue;
2695
2868
  scannedBarrels.add(epFile);
2696
2869
  try {
2697
- const content = fs7.readFileSync(epFile, "utf8");
2870
+ const content = fs8.readFileSync(epFile, "utf8");
2698
2871
  const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
2699
2872
  const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
2700
2873
  for (; ; ) {
@@ -2784,7 +2957,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
2784
2957
  const deadPackages = [];
2785
2958
  const pkgEntries = findPackageEntries(projectRoot);
2786
2959
  for (const [pkgName, pkgDir] of pkgEntries) {
2787
- const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path7.sep));
2960
+ const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path8.sep));
2788
2961
  if (pkgFiles.length === 0) continue;
2789
2962
  const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
2790
2963
  if (pkgUsed.length === 0) {
@@ -2820,7 +2993,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
2820
2993
  }
2821
2994
  function findPackageEntries(projectRoot) {
2822
2995
  const pkgMap = /* @__PURE__ */ new Map();
2823
- const rootPkg = tryReadJson(path7.join(projectRoot, "package.json"));
2996
+ const rootPkg = tryReadJson(path8.join(projectRoot, "package.json"));
2824
2997
  if (rootPkg && typeof rootPkg.name === "string") {
2825
2998
  pkgMap.set(rootPkg.name, projectRoot);
2826
2999
  }
@@ -2830,7 +3003,7 @@ function findPackageEntries(projectRoot) {
2830
3003
  wsDirs = extractPnpmWorkspaceDirs(projectRoot);
2831
3004
  }
2832
3005
  for (const wsDir of wsDirs) {
2833
- const wsPkg = tryReadJson(path7.join(wsDir, "package.json"));
3006
+ const wsPkg = tryReadJson(path8.join(wsDir, "package.json"));
2834
3007
  if (wsPkg && typeof wsPkg.name === "string") {
2835
3008
  pkgMap.set(wsPkg.name, wsDir);
2836
3009
  }
@@ -2839,16 +3012,997 @@ function findPackageEntries(projectRoot) {
2839
3012
  return pkgMap;
2840
3013
  }
2841
3014
 
3015
+ // src/codebase-index/atlas-brief.ts
3016
+ import { existsSync as existsSync4 } from "node:fs";
3017
+ import * as path10 from "node:path";
3018
+
3019
+ // src/codebase-index/atlas-projection.ts
3020
+ import { createHash } from "node:crypto";
3021
+ import { existsSync as existsSync3 } from "node:fs";
3022
+ import * as fs9 from "node:fs/promises";
3023
+ import * as path9 from "node:path";
3024
+
3025
+ // src/codebase-index/atlas-export.ts
3026
+ var EXPORT_PACKAGE_LIMIT = 40;
3027
+ var EXPORT_FILE_LIMIT = 120;
3028
+ var CANVAS = { width: 1e3, height: 700 };
3029
+ var RADIUS = { min: 14, max: 46 };
3030
+ var ESCAPES = {
3031
+ "&": "&amp;",
3032
+ "<": "&lt;",
3033
+ ">": "&gt;",
3034
+ '"': "&quot;",
3035
+ "'": "&#39;"
3036
+ };
3037
+ function escapeHtml(value) {
3038
+ return value.replace(/[&<>"']/g, (character) => ESCAPES[character] ?? character);
3039
+ }
3040
+ function inlineJson(value) {
3041
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
3042
+ }
3043
+ function placePackages(packages) {
3044
+ const shown = packages.slice(0, EXPORT_PACKAGE_LIMIT);
3045
+ if (shown.length === 0) return [];
3046
+ const maxRank = shown.reduce((max, pkg) => Math.max(max, pkg.rank), 0) || 1;
3047
+ const centreX = CANVAS.width / 2;
3048
+ const centreY = CANVAS.height / 2;
3049
+ const placed = [];
3050
+ let index = 0;
3051
+ let ring = 0;
3052
+ while (index < shown.length) {
3053
+ const capacity = ring === 0 ? 1 : Math.min(ring * 6, shown.length - index);
3054
+ const radius = ring === 0 ? 0 : ring * 115;
3055
+ for (let slot = 0; slot < capacity && index < shown.length; slot++, index++) {
3056
+ const pkg = shown[index];
3057
+ const angle = slot / capacity * Math.PI * 2 - Math.PI / 2;
3058
+ placed.push({
3059
+ package: pkg,
3060
+ x: centreX + Math.cos(angle) * radius,
3061
+ y: centreY + Math.sin(angle) * radius * 0.68,
3062
+ radius: RADIUS.min + pkg.rank / maxRank * (RADIUS.max - RADIUS.min)
3063
+ });
3064
+ }
3065
+ ring++;
3066
+ }
3067
+ return placed;
3068
+ }
3069
+ var unit = (value) => value.toFixed(1);
3070
+ function renderSvg(document) {
3071
+ const placed = placePackages(document.packages);
3072
+ if (placed.length === 0) {
3073
+ return '<p class="empty">No ranked packages to draw.</p>';
3074
+ }
3075
+ const at = new Map(placed.map((entry) => [entry.package.name, entry]));
3076
+ const maxWeight = document.edges.reduce((max, edge) => Math.max(max, edge.weight), 0) || 1;
3077
+ const lines = [
3078
+ `<svg viewBox="0 0 ${CANVAS.width} ${CANVAS.height}" role="img" aria-label="Package dependency map">`
3079
+ ];
3080
+ for (const edge of document.edges) {
3081
+ const from = at.get(edge.from);
3082
+ const to = at.get(edge.to);
3083
+ if (from === void 0 || to === void 0 || from === to) continue;
3084
+ const strength = Math.min(1, edge.weight / maxWeight);
3085
+ lines.push(
3086
+ `<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)}" />`
3087
+ );
3088
+ }
3089
+ for (const entry of placed) {
3090
+ const name = escapeHtml(entry.package.name);
3091
+ const summary = entry.package.summary ?? "";
3092
+ const title = escapeHtml(
3093
+ `${entry.package.name} \u2014 ${entry.package.files} files, rank ${entry.package.rank.toFixed(4)}${summary === "" ? "" : `
3094
+ ${summary}`}`
3095
+ );
3096
+ lines.push(
3097
+ `<g class="pkg" data-package="${name}" tabindex="0">`,
3098
+ `<title>${title}</title>`,
3099
+ `<circle cx="${unit(entry.x)}" cy="${unit(entry.y)}" r="${unit(entry.radius)}" />`,
3100
+ `<text x="${unit(entry.x)}" y="${unit(entry.y + entry.radius + 13)}">${name}</text>`,
3101
+ "</g>"
3102
+ );
3103
+ }
3104
+ lines.push("</svg>");
3105
+ return lines.join("\n");
3106
+ }
3107
+ function renderFileRows(document) {
3108
+ return document.files.slice(0, EXPORT_FILE_LIMIT).map((file) => {
3109
+ const declarations = file.symbols.slice(0, 4).map((symbol) => escapeHtml(symbol.name)).join(", ");
3110
+ const description = file.concept ?? declarations;
3111
+ 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>`;
3112
+ }).join("\n");
3113
+ }
3114
+ function renderAtlasHtml(document, options = {}) {
3115
+ const name = options.projectName ?? "Codebase";
3116
+ const described = document.packages.filter((pkg) => pkg.summary !== void 0);
3117
+ return `<!doctype html>
3118
+ <html lang="en">
3119
+ <head>
3120
+ <meta charset="utf-8" />
3121
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3122
+ <title>${escapeHtml(name)} \u2014 Codebase Atlas</title>
3123
+ <style>
3124
+ :root {
3125
+ color-scheme: light dark;
3126
+ --bg: #ffffff; --fg: #16181d; --muted: #61656e; --line: #d9dce2;
3127
+ --accent: #3b5bdb; --panel: #f6f7f9;
3128
+ }
3129
+ @media (prefers-color-scheme: dark) {
3130
+ :root { --bg: #101216; --fg: #e6e8ec; --muted: #9aa0ab; --line: #2a2e36;
3131
+ --accent: #7c93f5; --panel: #171a20; }
3132
+ }
3133
+ * { box-sizing: border-box; }
3134
+ body { margin: 0; background: var(--bg); color: var(--fg);
3135
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
3136
+ header { padding: 24px 28px 12px; border-bottom: 1px solid var(--line); }
3137
+ h1 { margin: 0 0 4px; font-size: 20px; }
3138
+ h2 { margin: 28px 0 10px; font-size: 15px; text-transform: uppercase;
3139
+ letter-spacing: .12em; color: var(--muted); }
3140
+ main { padding: 8px 28px 48px; max-width: 1180px; margin: 0 auto; }
3141
+ .counts { color: var(--muted); font-size: 12px; }
3142
+ figure { margin: 16px 0 0; border: 1px solid var(--line); background: var(--panel); }
3143
+ svg { display: block; width: 100%; height: auto; }
3144
+ svg line { stroke: var(--accent); }
3145
+ svg circle { fill: var(--accent); fill-opacity: .22; stroke: var(--accent); stroke-width: 1.5; }
3146
+ svg text { fill: var(--fg); font-size: 10px; text-anchor: middle; font-family: ui-monospace, monospace; }
3147
+ svg .pkg { cursor: pointer; }
3148
+ svg .pkg:hover circle, svg .pkg:focus circle { fill-opacity: .5; outline: none; }
3149
+ svg .pkg.muted { opacity: .25; }
3150
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
3151
+ th, td { padding: 5px 8px; border-bottom: 1px solid var(--line); text-align: left;
3152
+ vertical-align: top; }
3153
+ th { color: var(--muted); font-weight: 600; font-size: 11px; text-transform: uppercase;
3154
+ letter-spacing: .08em; }
3155
+ td.num { text-align: right; font-family: ui-monospace, monospace; white-space: nowrap; }
3156
+ td.path { font-family: ui-monospace, monospace; }
3157
+ td.desc { color: var(--muted); }
3158
+ tr[hidden] { display: none; }
3159
+ ul.subsystems { list-style: none; padding: 0; margin: 0; }
3160
+ ul.subsystems li { padding: 6px 0; border-bottom: 1px solid var(--line); }
3161
+ ul.subsystems b { font-family: ui-monospace, monospace; }
3162
+ .filter { margin: 10px 0 0; font-size: 12px; color: var(--muted); }
3163
+ .filter button { font: inherit; color: var(--accent); background: none; border: 0;
3164
+ cursor: pointer; padding: 0; text-decoration: underline; }
3165
+ .empty { color: var(--muted); }
3166
+ .wrap { overflow-x: auto; }
3167
+ footer { color: var(--muted); font-size: 11px; padding: 0 28px 32px; max-width: 1180px;
3168
+ margin: 0 auto; }
3169
+ </style>
3170
+ </head>
3171
+ <body>
3172
+ <header>
3173
+ <h1>${escapeHtml(name)} \u2014 Codebase Atlas</h1>
3174
+ <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>
3175
+ </header>
3176
+ <main>
3177
+ <h2>Package map</h2>
3178
+ <figure>${renderSvg(document)}</figure>
3179
+ <p class="filter" id="filter">Click a package to filter the table. <button type="button" id="clear" hidden>Clear filter</button></p>
3180
+ ${described.length === 0 ? "" : ` <h2>Subsystems</h2>
3181
+ <ul class="subsystems">
3182
+ ${described.map((pkg) => ` <li><b>${escapeHtml(pkg.name)}</b> \u2014 ${escapeHtml(pkg.summary ?? "")}</li>`).join("\n")}
3183
+ </ul>
3184
+ `} <h2>Most central files</h2>
3185
+ <div class="wrap">
3186
+ <table>
3187
+ <thead><tr><th>Rank</th><th>File</th><th>In</th><th>Out</th><th>What it is</th></tr></thead>
3188
+ <tbody id="files">
3189
+ ${renderFileRows(document)}
3190
+ </tbody>
3191
+ </table>
3192
+ </div>
3193
+ </main>
3194
+ <footer>Generated from the codebase index by <code>/codebase-map --export</code>. Ranks are relative to this repository and mean nothing across repositories.</footer>
3195
+ <script id="atlas" type="application/json">${inlineJson({
3196
+ counts: document.counts,
3197
+ packages: document.packages,
3198
+ edges: document.edges
3199
+ })}</script>
3200
+ <script>
3201
+ (function () {
3202
+ var rows = Array.prototype.slice.call(document.querySelectorAll('#files tr'));
3203
+ var groups = Array.prototype.slice.call(document.querySelectorAll('svg .pkg'));
3204
+ var clear = document.getElementById('clear');
3205
+ var active = null;
3206
+
3207
+ function apply(name) {
3208
+ active = name;
3209
+ rows.forEach(function (row) {
3210
+ row.hidden = name !== null && row.getAttribute('data-package') !== name;
3211
+ });
3212
+ groups.forEach(function (group) {
3213
+ group.classList.toggle('muted', name !== null && group.getAttribute('data-package') !== name);
3214
+ });
3215
+ clear.hidden = name === null;
3216
+ }
3217
+
3218
+ groups.forEach(function (group) {
3219
+ var name = group.getAttribute('data-package');
3220
+ function toggle() { apply(active === name ? null : name); }
3221
+ group.addEventListener('click', toggle);
3222
+ group.addEventListener('keydown', function (event) {
3223
+ if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggle(); }
3224
+ });
3225
+ });
3226
+ clear.addEventListener('click', function () { apply(null); });
3227
+ })();
3228
+ </script>
3229
+ </body>
3230
+ </html>
3231
+ `;
3232
+ }
3233
+
3234
+ // src/codebase-index/atlas-projection.ts
3235
+ var ATLAS_DIR = path9.join(".wrongstack", "atlas");
3236
+ var ATLAS_JSON = "atlas.json";
3237
+ var ATLAS_MARKDOWN = "ATLAS.md";
3238
+ var ATLAS_MANIFEST = "manifest.json";
3239
+ var ATLAS_SCHEMA = 2;
3240
+ var ATLAS_FILE_LIMIT = 300;
3241
+ var SYMBOLS_PER_FILE = 6;
3242
+ var MARKDOWN_FILE_LIMIT = 60;
3243
+ var MARKDOWN_SYMBOLS_PER_FILE = 3;
3244
+ var RANK_PRECISION = 4;
3245
+ function round(value) {
3246
+ return Number(value.toFixed(RANK_PRECISION));
3247
+ }
3248
+ function relativeFactory(projectRoot) {
3249
+ return (file) => {
3250
+ const relative12 = path9.relative(projectRoot, file);
3251
+ if (!relative12 || relative12.startsWith("..")) return posixIndexPath(file);
3252
+ return posixIndexPath(relative12);
3253
+ };
3254
+ }
3255
+ function digestOf(entries) {
3256
+ const hash = createHash("sha256");
3257
+ for (const [file, contentHash] of [...entries].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
3258
+ hash.update(file);
3259
+ hash.update("\0");
3260
+ hash.update(contentHash);
3261
+ hash.update("\n");
3262
+ }
3263
+ return hash.digest("hex");
3264
+ }
3265
+ function renderMarkdown(document) {
3266
+ const lines = [
3267
+ "# Codebase Atlas",
3268
+ "",
3269
+ "Generated from the codebase index. Do not edit by hand \u2014 run `/codebase-map --write`.",
3270
+ "",
3271
+ `- Indexed files: ${document.counts.files}`,
3272
+ `- Indexed symbols: ${document.counts.symbols}`,
3273
+ `- Packages: ${document.counts.packages}`,
3274
+ "",
3275
+ "Ranking is PageRank over the reference graph (calls, imports, type references,",
3276
+ "inheritance). `1.0000` is the most central file in this repository; the scores",
3277
+ "are relative to this repository only.",
3278
+ "",
3279
+ "## Packages",
3280
+ "",
3281
+ "| Package | Files | Hub | Rank |",
3282
+ "| --- | ---: | --- | ---: |"
3283
+ ];
3284
+ for (const pkg of document.packages) {
3285
+ lines.push(`| ${pkg.name} | ${pkg.files} | \`${pkg.hub}\` | ${pkg.rank.toFixed(4)} |`);
3286
+ }
3287
+ const described = document.packages.filter((pkg) => pkg.summary !== void 0);
3288
+ if (described.length > 0) {
3289
+ lines.push("", "## Subsystems", "");
3290
+ for (const pkg of described) {
3291
+ lines.push(`- **${pkg.name}** \u2014 ${pkg.summary}`);
3292
+ }
3293
+ }
3294
+ lines.push(
3295
+ "",
3296
+ `## Most central files (top ${Math.min(MARKDOWN_FILE_LIMIT, document.files.length)} of ${document.files.length})`,
3297
+ "",
3298
+ `The full ranking, with every declaration, is in \`${ATLAS_JSON}\`.`,
3299
+ "",
3300
+ "| Rank | File | In | Out | Declarations |",
3301
+ "| ---: | --- | ---: | ---: | --- |"
3302
+ );
3303
+ for (const file of document.files.slice(0, MARKDOWN_FILE_LIMIT)) {
3304
+ const declarations = file.symbols.slice(0, MARKDOWN_SYMBOLS_PER_FILE).map((symbol) => `\`${symbol.name}\``).join(", ");
3305
+ lines.push(
3306
+ `| ${file.rank.toFixed(4)} | \`${file.path}\` | ${file.inDeg} | ${file.outDeg} | ${declarations} |`
3307
+ );
3308
+ }
3309
+ return `${lines.join("\n").trimEnd()}
3310
+ `;
3311
+ }
3312
+ function buildAtlas(store, projectRoot) {
3313
+ const relativeOf = relativeFactory(projectRoot);
3314
+ const stats = store.getStats();
3315
+ const packageCounts = store.getPackageFileCounts();
3316
+ const ranked = store.getRankedFiles(ATLAS_FILE_LIMIT);
3317
+ const concepts = store.getReadyConceptSummaries();
3318
+ const subsystemSummaries = new Map(
3319
+ store.getSubsystems().map((subsystem) => [subsystem.id, subsystem.summary])
3320
+ );
3321
+ const packages = /* @__PURE__ */ new Map();
3322
+ const packageNameOf = (row) => row.package || relativeOf(row.file).split("/")[0] || "(root)";
3323
+ for (const row of ranked) {
3324
+ const relative12 = relativeOf(row.file);
3325
+ const name = packageNameOf(row);
3326
+ const existing = packages.get(name);
3327
+ if (existing === void 0) {
3328
+ const summary = subsystemSummaries.get(name);
3329
+ packages.set(name, {
3330
+ name,
3331
+ files: packageCounts.get(row.package) ?? 0,
3332
+ hub: relative12,
3333
+ rank: round(row.rank),
3334
+ // Absent rather than empty: an un-enriched repository must project the
3335
+ // same bytes it projected before the concept layer existed.
3336
+ ...summary !== void 0 && summary !== "" ? { summary } : {}
3337
+ });
3338
+ continue;
3339
+ }
3340
+ if (row.rank > existing.rank) {
3341
+ existing.hub = relative12;
3342
+ existing.rank = round(row.rank);
3343
+ }
3344
+ }
3345
+ const files = ranked.map((row) => {
3346
+ const symbols = store.getFileSymbols(row.file, SYMBOLS_PER_FILE).map((symbol) => ({ name: symbol.name, kind: symbol.kind, line: symbol.line }));
3347
+ const concept = concepts.get(row.file);
3348
+ return {
3349
+ path: relativeOf(row.file),
3350
+ rank: round(row.rank),
3351
+ inDeg: row.inDeg,
3352
+ outDeg: row.outDeg,
3353
+ package: packageNameOf(row),
3354
+ symbols,
3355
+ ...concept !== void 0 && concept !== "" ? { concept } : {}
3356
+ };
3357
+ });
3358
+ const known = new Set(packages.keys());
3359
+ const edges = store.getPackageGraph().edges.map((edge) => ({
3360
+ from: edge.source.replace(/^pkg:/, ""),
3361
+ to: edge.target.replace(/^pkg:/, ""),
3362
+ weight: edge.weight,
3363
+ refType: edge.refType
3364
+ })).filter((edge) => known.has(edge.from) && known.has(edge.to)).sort(
3365
+ (a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.refType.localeCompare(b.refType)
3366
+ );
3367
+ const document = {
3368
+ schema: ATLAS_SCHEMA,
3369
+ counts: { files: stats.totalFiles, symbols: stats.totalSymbols, packages: packages.size },
3370
+ packages: [...packages.values()].sort(
3371
+ (a, b) => b.rank - a.rank || a.name.localeCompare(b.name)
3372
+ ),
3373
+ edges,
3374
+ files
3375
+ };
3376
+ const allHashes = store.getAllFileMetas().map((meta) => [relativeOf(meta.file), meta.contentHash ?? ""]);
3377
+ const atlasPaths = new Set(files.map((file) => file.path));
3378
+ const manifestFiles = {};
3379
+ for (const [file, hash] of [...allHashes].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
3380
+ if (atlasPaths.has(file)) manifestFiles[file] = hash;
3381
+ }
3382
+ const manifest = {
3383
+ schema: ATLAS_SCHEMA,
3384
+ counts: {
3385
+ files: document.counts.files,
3386
+ symbols: document.counts.symbols,
3387
+ tracked: allHashes.length
3388
+ },
3389
+ digest: digestOf(allHashes),
3390
+ files: manifestFiles
3391
+ };
3392
+ return { document, manifest, markdown: renderMarkdown(document) };
3393
+ }
3394
+ function stableJson(value) {
3395
+ return `${JSON.stringify(value, null, 2)}
3396
+ `;
3397
+ }
3398
+ async function writeAtlas(store, projectRoot) {
3399
+ const projection = buildAtlas(store, projectRoot);
3400
+ const dir = path9.join(projectRoot, ATLAS_DIR);
3401
+ await fs9.mkdir(dir, { recursive: true });
3402
+ const written = [
3403
+ [ATLAS_JSON, stableJson(projection.document)],
3404
+ [ATLAS_MANIFEST, stableJson(projection.manifest)],
3405
+ [ATLAS_MARKDOWN, projection.markdown]
3406
+ ];
3407
+ for (const [name, body] of written) {
3408
+ await fs9.writeFile(path9.join(dir, name), body, "utf8");
3409
+ }
3410
+ return {
3411
+ dir: posixIndexPath(ATLAS_DIR),
3412
+ files: written.map(([name]) => name),
3413
+ fileCount: projection.document.files.length,
3414
+ packageCount: projection.document.packages.length
3415
+ };
3416
+ }
3417
+ var MAX_REPORTED_DRIFT = 20;
3418
+ async function checkAtlasFreshness(store, projectRoot) {
3419
+ const stale = (reason) => ({
3420
+ fresh: false,
3421
+ reason,
3422
+ changed: [],
3423
+ removed: [],
3424
+ added: 0,
3425
+ digestMatches: false
3426
+ });
3427
+ let raw;
3428
+ try {
3429
+ raw = await fs9.readFile(path9.join(projectRoot, ATLAS_DIR, ATLAS_MANIFEST), "utf8");
3430
+ } catch {
3431
+ return stale("missing");
3432
+ }
3433
+ let manifest;
3434
+ try {
3435
+ manifest = JSON.parse(raw);
3436
+ } catch {
3437
+ return stale("unreadable");
3438
+ }
3439
+ if (manifest?.schema !== ATLAS_SCHEMA || typeof manifest.files !== "object") {
3440
+ return stale("schema");
3441
+ }
3442
+ const relativeOf = relativeFactory(projectRoot);
3443
+ const live = /* @__PURE__ */ new Map();
3444
+ const allHashes = [];
3445
+ for (const meta of store.getAllFileMetas()) {
3446
+ const relative12 = relativeOf(meta.file);
3447
+ const hash = meta.contentHash ?? "";
3448
+ live.set(relative12, hash);
3449
+ allHashes.push([relative12, hash]);
3450
+ }
3451
+ const changed = [];
3452
+ const removed = [];
3453
+ for (const [file, hash] of Object.entries(manifest.files)) {
3454
+ const current = live.get(file);
3455
+ if (current === void 0) {
3456
+ removed.push(file);
3457
+ continue;
3458
+ }
3459
+ if (hash !== "" && current !== "" && hash !== current) changed.push(file);
3460
+ }
3461
+ const digestMatches = digestOf(allHashes) === manifest.digest;
3462
+ const tracked = typeof manifest.counts?.tracked === "number" ? manifest.counts.tracked : live.size;
3463
+ const added = Math.max(0, live.size - tracked);
3464
+ const fresh = digestMatches && changed.length === 0 && removed.length === 0;
3465
+ return {
3466
+ fresh,
3467
+ ...fresh ? {} : { reason: "drift" },
3468
+ changed: changed.sort().slice(0, MAX_REPORTED_DRIFT),
3469
+ removed: removed.sort().slice(0, MAX_REPORTED_DRIFT),
3470
+ added,
3471
+ digestMatches
3472
+ };
3473
+ }
3474
+ function hasIndex(projectRoot, indexDir) {
3475
+ return existsSync3(path9.join(resolveIndexDir(projectRoot, indexDir), "index.db"));
3476
+ }
3477
+ async function withStore(projectRoot, indexDir, job) {
3478
+ if (!hasIndex(projectRoot, indexDir)) return { indexed: false };
3479
+ const store = indexStorePool.acquire(projectRoot, { indexDir });
3480
+ try {
3481
+ return await job(store);
3482
+ } finally {
3483
+ indexStorePool.release(store);
3484
+ }
3485
+ }
3486
+ function writeProjectAtlas(projectRoot, opts = {}) {
3487
+ return withStore(projectRoot, opts.indexDir, (store) => writeAtlas(store, projectRoot));
3488
+ }
3489
+ function checkProjectAtlasFreshness(projectRoot, opts = {}) {
3490
+ return withStore(projectRoot, opts.indexDir, (store) => checkAtlasFreshness(store, projectRoot));
3491
+ }
3492
+ function exportProjectAtlasHtml(projectRoot, opts = {}) {
3493
+ return withStore(
3494
+ projectRoot,
3495
+ opts.indexDir,
3496
+ (store) => renderAtlasHtml(buildAtlas(store, projectRoot).document, {
3497
+ projectName: opts.projectName ?? path9.basename(projectRoot)
3498
+ })
3499
+ );
3500
+ }
3501
+
3502
+ // src/codebase-index/atlas-brief.ts
3503
+ var BRIEF_HUB_LIMIT = 20;
3504
+ var BRIEF_PACKAGE_LIMIT = 12;
3505
+ var BRIEF_SUBSYSTEM_LIMIT = 10;
3506
+ var BRIEF_SUMMARY_CHARS = 160;
3507
+ function trim(text) {
3508
+ const clean = text.replace(/\s+/g, " ").trim();
3509
+ return clean.length > BRIEF_SUMMARY_CHARS ? `${clean.slice(0, BRIEF_SUMMARY_CHARS - 1).trimEnd()}\u2026` : clean;
3510
+ }
3511
+ async function buildAtlasBrief(store, projectRoot) {
3512
+ const relativeOf = (file) => {
3513
+ const relative12 = path10.relative(projectRoot, file);
3514
+ return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
3515
+ };
3516
+ const stats = store.getStats();
3517
+ const concepts = store.getReadyConceptSummaries();
3518
+ const ranked = store.getRankedFiles(BRIEF_HUB_LIMIT);
3519
+ const hubs = ranked.map((row) => {
3520
+ const concept = concepts.get(row.file);
3521
+ return {
3522
+ path: relativeOf(row.file),
3523
+ rank: row.rank,
3524
+ ...concept !== void 0 && concept !== "" ? { concept: trim(concept) } : {}
3525
+ };
3526
+ });
3527
+ const counts = store.getPackageFileCounts();
3528
+ const hubOf = /* @__PURE__ */ new Map();
3529
+ for (const row of store.getRankedFiles(Math.max(counts.size * 4, BRIEF_HUB_LIMIT))) {
3530
+ const name = row.package || relativeOf(row.file).split("/")[0] || "(root)";
3531
+ const current = hubOf.get(name);
3532
+ if (current === void 0 || row.rank > current.rank) {
3533
+ hubOf.set(name, { file: relativeOf(row.file), rank: row.rank });
3534
+ }
3535
+ }
3536
+ 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 }));
3537
+ const subsystems = store.getSubsystems().filter((subsystem) => subsystem.summary !== "").slice(0, BRIEF_SUBSYSTEM_LIMIT).map((subsystem) => ({ name: subsystem.name, summary: trim(subsystem.summary) }));
3538
+ const brief = {
3539
+ ranked: hubs.length > 0,
3540
+ counts: {
3541
+ files: stats.totalFiles,
3542
+ symbols: stats.totalSymbols,
3543
+ packages: counts.size
3544
+ },
3545
+ hubs,
3546
+ packages,
3547
+ subsystems
3548
+ };
3549
+ try {
3550
+ const freshness = await checkAtlasFreshness(store, projectRoot);
3551
+ if (freshness.reason !== "missing") {
3552
+ brief.staleFiles = freshness.changed.length + freshness.removed.length + freshness.added;
3553
+ }
3554
+ } catch {
3555
+ }
3556
+ return brief;
3557
+ }
3558
+ async function buildProjectAtlasBrief(projectRoot, opts = {}) {
3559
+ if (!existsSync4(path10.join(resolveIndexDir(projectRoot, opts.indexDir), "index.db"))) {
3560
+ return { indexed: false };
3561
+ }
3562
+ const store = indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
3563
+ try {
3564
+ return await buildAtlasBrief(store, projectRoot);
3565
+ } finally {
3566
+ indexStorePool.release(store);
3567
+ }
3568
+ }
3569
+
3570
+ // src/codebase-index/codebase-context-tool.ts
3571
+ import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
3572
+ import { toErrorMessage as toErrorMessage11 } from "@wrongstack/core/utils";
3573
+ var TOOL_TIMEOUT_MS = 35e3;
3574
+ var VECTOR_HIT_LIMIT = 12;
3575
+ var VECTOR_MIN_SCORE = 0.25;
3576
+ var queryEmbedder;
3577
+ function setContextQueryEmbedder(port) {
3578
+ queryEmbedder = port;
3579
+ }
3580
+ async function semanticSeeds(projectRoot, indexDir, query) {
3581
+ const port = queryEmbedder;
3582
+ if (port === void 0) return [];
3583
+ try {
3584
+ const [vector] = await port.embed([query]);
3585
+ if (vector === void 0 || vector.length !== port.dimensions) return [];
3586
+ const result = await codebaseVectorSearch({
3587
+ projectRoot,
3588
+ indexDir,
3589
+ vector: Array.from(vector),
3590
+ limit: VECTOR_HIT_LIMIT,
3591
+ minScore: VECTOR_MIN_SCORE
3592
+ });
3593
+ return result.hits;
3594
+ } catch {
3595
+ return [];
3596
+ }
3597
+ }
3598
+ var codebaseContextTool = {
3599
+ name: "codebase-context",
3600
+ category: "Project",
3601
+ icon: "index",
3602
+ permission: "auto",
3603
+ mutating: false,
3604
+ capabilities: ["fs.read"],
3605
+ timeoutMs: TOOL_TIMEOUT_MS,
3606
+ 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.",
3607
+ 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.',
3608
+ inputSchema: {
3609
+ type: "object",
3610
+ properties: {
3611
+ query: {
3612
+ type: "string",
3613
+ description: "What you are trying to do or understand, in plain words."
3614
+ },
3615
+ limit: {
3616
+ type: "number",
3617
+ description: "Number of files to return (default 12, max 50)."
3618
+ },
3619
+ symbolsPerFile: {
3620
+ type: "number",
3621
+ description: "Declarations shown per file (default 4, max 20)."
3622
+ },
3623
+ pathPrefix: {
3624
+ type: "string",
3625
+ description: "Restrict results to this project-relative path prefix."
3626
+ }
3627
+ },
3628
+ required: ["query"],
3629
+ additionalProperties: false
3630
+ },
3631
+ async execute(input, ctx, execOpts) {
3632
+ const query = typeof input.query === "string" ? input.query.trim() : "";
3633
+ if (query.length === 0) {
3634
+ throw new ToolValidationError5({
3635
+ message: "codebase-context requires a non-empty `query`.",
3636
+ field: "query"
3637
+ });
3638
+ }
3639
+ const signal = execOpts?.signal ?? ctx.signal;
3640
+ const projectRoot = ctx.projectRoot ?? ctx.cwd ?? process.cwd();
3641
+ try {
3642
+ signal?.throwIfAborted();
3643
+ const indexDir = codebaseIndexDirOverride(ctx);
3644
+ const vectorFiles = await semanticSeeds(projectRoot, indexDir, query);
3645
+ signal?.throwIfAborted();
3646
+ const result = await codebaseContext({
3647
+ projectRoot,
3648
+ indexDir,
3649
+ query,
3650
+ ...vectorFiles.length > 0 ? { vectorFiles } : {},
3651
+ limit: input.limit,
3652
+ symbolsPerFile: input.symbolsPerFile,
3653
+ pathPrefix: input.pathPrefix
3654
+ });
3655
+ return {
3656
+ query: result.query,
3657
+ entries: result.entries,
3658
+ seedCount: result.seedCount,
3659
+ semanticSeedCount: result.semanticSeedCount,
3660
+ totalCandidates: result.totalCandidates,
3661
+ indexStatus: result.indexStatus,
3662
+ ..."stale" in result && result.stale === true ? { stale: true } : {}
3663
+ };
3664
+ } catch (err) {
3665
+ if (signal?.aborted) throw err;
3666
+ const refreshing = err instanceof Error && err.name === "IndexRefreshInProgressError";
3667
+ return {
3668
+ query,
3669
+ entries: [],
3670
+ seedCount: 0,
3671
+ semanticSeedCount: 0,
3672
+ totalCandidates: 0,
3673
+ indexStatus: "error",
3674
+ error: refreshing ? "Codebase index is refreshing; retry once the current generation is published." : toErrorMessage11(err)
3675
+ };
3676
+ }
3677
+ }
3678
+ };
3679
+
3680
+ // src/codebase-index/concept-enrichment.ts
3681
+ import { existsSync as existsSync5 } from "node:fs";
3682
+ import * as fs10 from "node:fs/promises";
3683
+ import * as path11 from "node:path";
3684
+ var MAX_SOURCE_CHARS = 12e3;
3685
+ var MAX_CRUX_LINES = 12;
3686
+ var MAX_DECLARATIONS = 24;
3687
+ var DEFAULT_CONCURRENCY = 5;
3688
+ var MAX_SUMMARY_CHARS = 400;
3689
+ function normaliseCrux(result, lineCount) {
3690
+ const rawStart = result.cruxStart;
3691
+ const rawEnd = result.cruxEnd;
3692
+ if (typeof rawStart !== "number" || !Number.isFinite(rawStart)) return { start: null, end: null };
3693
+ const start = Math.max(1, Math.min(Math.trunc(rawStart), lineCount));
3694
+ const proposedEnd = typeof rawEnd === "number" && Number.isFinite(rawEnd) ? Math.trunc(rawEnd) : start;
3695
+ const end = Math.max(start, Math.min(proposedEnd, lineCount, start + MAX_CRUX_LINES - 1));
3696
+ return { start, end };
3697
+ }
3698
+ async function enrichConcepts(store, port, relativeOf, options = {}) {
3699
+ const startedAt = Date.now();
3700
+ const errors = [];
3701
+ const signal = options.signal;
3702
+ const concurrency = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, 16));
3703
+ const pruned = store.pruneOrphanConcepts();
3704
+ const markedStale = store.markStaleConcepts();
3705
+ const hashByFile = /* @__PURE__ */ new Map();
3706
+ for (const meta of store.getAllFileMetas()) hashByFile.set(meta.file, meta.contentHash ?? "");
3707
+ const existing = /* @__PURE__ */ new Map();
3708
+ for (const concept of store.getAllFileConcepts()) existing.set(concept.file, concept);
3709
+ const rankOf = /* @__PURE__ */ new Map();
3710
+ for (const row of store.getRankedFiles(hashByFile.size || 1)) rankOf.set(row.file, row.rank);
3711
+ const ordered = [...hashByFile.keys()].sort(
3712
+ (a, b) => (rankOf.get(b) ?? 0) - (rankOf.get(a) ?? 0) || a.localeCompare(b)
3713
+ );
3714
+ const queue = [];
3715
+ let cached = 0;
3716
+ for (const file of ordered) {
3717
+ const hash = hashByFile.get(file);
3718
+ if (hash === void 0) continue;
3719
+ const concept = existing.get(file);
3720
+ const current = concept !== void 0 && concept.state === "ready" && concept.contentHash === hash;
3721
+ if (current && options.force !== true) {
3722
+ cached++;
3723
+ continue;
3724
+ }
3725
+ queue.push(file);
3726
+ if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
3727
+ }
3728
+ let summarised = 0;
3729
+ let failed = 0;
3730
+ let done = 0;
3731
+ let cursor = 0;
3732
+ const worker = async () => {
3733
+ for (; ; ) {
3734
+ if (signal?.aborted) return;
3735
+ const index = cursor++;
3736
+ const file = queue[index];
3737
+ if (file === void 0) return;
3738
+ try {
3739
+ const result = await summariseOne(
3740
+ store,
3741
+ port,
3742
+ relativeOf,
3743
+ file,
3744
+ hashByFile,
3745
+ existing,
3746
+ signal
3747
+ );
3748
+ if (result === "skipped") failed++;
3749
+ else summarised++;
3750
+ } catch (error) {
3751
+ failed++;
3752
+ if (errors.length < 50) {
3753
+ errors.push(
3754
+ `${relativeOf(file)}: ${error instanceof Error ? error.message : String(error)}`
3755
+ );
3756
+ }
3757
+ } finally {
3758
+ done++;
3759
+ options.onProgress?.(done, queue.length);
3760
+ }
3761
+ }
3762
+ };
3763
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, () => worker()));
3764
+ let subsystems = 0;
3765
+ if (options.subsystems === true && port.describeSubsystem !== void 0 && signal?.aborted !== true) {
3766
+ try {
3767
+ subsystems = await deriveSubsystems(store, port, relativeOf, signal);
3768
+ } catch (error) {
3769
+ errors.push(`subsystems: ${error instanceof Error ? error.message : String(error)}`);
3770
+ }
3771
+ }
3772
+ return {
3773
+ summarised,
3774
+ cached,
3775
+ failed,
3776
+ markedStale,
3777
+ pruned,
3778
+ subsystems,
3779
+ durationMs: Date.now() - startedAt,
3780
+ errors
3781
+ };
3782
+ }
3783
+ async function summariseOne(store, port, relativeOf, file, hashByFile, existing, signal) {
3784
+ let source;
3785
+ try {
3786
+ source = await fs10.readFile(file, "utf8");
3787
+ } catch {
3788
+ return "skipped";
3789
+ }
3790
+ if (source.trim().length === 0) return "skipped";
3791
+ const truncated = source.length > MAX_SOURCE_CHARS;
3792
+ const declarations = store.getFileSymbols(file, MAX_DECLARATIONS).map((symbol) => ({ name: symbol.name, kind: symbol.kind, line: symbol.line }));
3793
+ const stale = existing.get(file);
3794
+ const result = await port.describeFile({
3795
+ file: relativeOf(file),
3796
+ absolutePath: file,
3797
+ language: "",
3798
+ source: truncated ? source.slice(0, MAX_SOURCE_CHARS) : source,
3799
+ truncated,
3800
+ declarations,
3801
+ ...stale !== void 0 && stale.summary !== "" ? { staleSummary: stale.summary } : {},
3802
+ signal
3803
+ });
3804
+ const summary = result?.summary?.trim() ?? "";
3805
+ if (summary.length === 0) return "skipped";
3806
+ const lineCount = source.split("\n").length;
3807
+ const crux = normaliseCrux(result, lineCount);
3808
+ store.upsertFileConcept({
3809
+ file,
3810
+ contentHash: hashByFile.get(file) ?? "",
3811
+ summary: summary.slice(0, MAX_SUMMARY_CHARS),
3812
+ cruxStart: crux.start,
3813
+ cruxEnd: crux.end,
3814
+ state: "ready",
3815
+ model: result?.model ?? "",
3816
+ updatedAt: Date.now()
3817
+ });
3818
+ return "stored";
3819
+ }
3820
+ var SUBSYSTEM_FILE_SAMPLE = 12;
3821
+ async function deriveSubsystems(store, port, relativeOf, signal) {
3822
+ const describe = port.describeSubsystem;
3823
+ if (describe === void 0) return 0;
3824
+ const summaries = store.getReadyConceptSummaries();
3825
+ if (summaries.size === 0) return 0;
3826
+ const byPackage = /* @__PURE__ */ new Map();
3827
+ for (const row of store.getRankedFiles(summaries.size * 2)) {
3828
+ const summary = summaries.get(row.file);
3829
+ if (summary === void 0) continue;
3830
+ const name = row.package || relativeOf(row.file).split("/")[0] || "(root)";
3831
+ const bucket = byPackage.get(name);
3832
+ const entry = { file: relativeOf(row.file), summary, rank: row.rank };
3833
+ if (bucket === void 0) byPackage.set(name, [entry]);
3834
+ else if (bucket.length < SUBSYSTEM_FILE_SAMPLE) bucket.push(entry);
3835
+ }
3836
+ const names = [...byPackage.keys()].sort();
3837
+ const known = new Set(names);
3838
+ const subsystems = [];
3839
+ const edges = [];
3840
+ for (const name of names) {
3841
+ if (signal?.aborted) break;
3842
+ const files = byPackage.get(name) ?? [];
3843
+ const result = await describe({ name, files, signal });
3844
+ const summary = result?.summary?.trim() ?? "";
3845
+ if (summary.length === 0) continue;
3846
+ subsystems.push({
3847
+ id: name,
3848
+ name,
3849
+ summary: summary.slice(0, MAX_SUMMARY_CHARS),
3850
+ memberFiles: files.map((f) => f.file),
3851
+ model: result?.model ?? "",
3852
+ updatedAt: Date.now()
3853
+ });
3854
+ for (const relation of result?.relations ?? []) {
3855
+ if (!known.has(relation.to) || relation.to === name) continue;
3856
+ if (!isConceptRelation(relation.relation)) continue;
3857
+ edges.push({ fromId: name, toId: relation.to, relation: relation.relation });
3858
+ }
3859
+ }
3860
+ store.replaceSubsystems(subsystems, edges);
3861
+ return subsystems.length;
3862
+ }
3863
+ async function enrichProjectConcepts(projectRoot, port, options = {}) {
3864
+ const { indexDir, ...rest } = options;
3865
+ if (!existsSync5(path11.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
3866
+ return { indexed: false };
3867
+ }
3868
+ const store = indexStorePool.acquire(projectRoot, { indexDir });
3869
+ try {
3870
+ return await enrichConcepts(
3871
+ store,
3872
+ port,
3873
+ (file) => {
3874
+ const relative12 = path11.relative(projectRoot, file);
3875
+ const chosen = relative12 && !relative12.startsWith("..") ? relative12 : file;
3876
+ return posixIndexPath(chosen);
3877
+ },
3878
+ rest
3879
+ );
3880
+ } finally {
3881
+ indexStorePool.release(store);
3882
+ }
3883
+ }
3884
+
3885
+ // src/codebase-index/embedding-pass.ts
3886
+ import { existsSync as existsSync6 } from "node:fs";
3887
+ import * as path12 from "node:path";
3888
+ var DEFAULT_BATCH_SIZE = 16;
3889
+ var MAX_EMBED_CHARS = 2e3;
3890
+ var FALLBACK_DECLARATIONS = 12;
3891
+ function embedTextFor(relativePath, summary, declarations) {
3892
+ const described = summary?.trim() ?? "";
3893
+ if (described.length > 0) {
3894
+ return {
3895
+ text: `${relativePath}
3896
+ ${described}`.slice(0, MAX_EMBED_CHARS),
3897
+ fromDeclarations: false
3898
+ };
3899
+ }
3900
+ const names = declarations.slice(0, FALLBACK_DECLARATIONS).join(", ");
3901
+ return {
3902
+ text: `${relativePath}${names.length > 0 ? `
3903
+ ${names}` : ""}`.slice(0, MAX_EMBED_CHARS),
3904
+ fromDeclarations: true
3905
+ };
3906
+ }
3907
+ async function embedFiles(store, port, relativeOf, options = {}) {
3908
+ const startedAt = Date.now();
3909
+ const errors = [];
3910
+ const signal = options.signal;
3911
+ const batchSize = Math.max(1, Math.min(options.batchSize ?? DEFAULT_BATCH_SIZE, 128));
3912
+ const providerChanged = store.reconcileVectorProvider(port.id);
3913
+ const pruned = store.pruneOrphanFileVectors();
3914
+ const stored = store.getFileVectorStates(port.id);
3915
+ const summaries = store.getReadyConceptSummaries();
3916
+ const rankOf = /* @__PURE__ */ new Map();
3917
+ const metas = store.getAllFileMetas();
3918
+ for (const row of store.getRankedFiles(metas.length || 1)) rankOf.set(row.file, row.rank);
3919
+ const ordered = metas.map((meta) => meta.file).sort((a, b) => (rankOf.get(b) ?? 0) - (rankOf.get(a) ?? 0) || a.localeCompare(b));
3920
+ const queue = [];
3921
+ let cached = 0;
3922
+ for (const file of ordered) {
3923
+ const declarations = summaries.has(file) ? [] : store.getFileSymbols(file, FALLBACK_DECLARATIONS).map((symbol) => symbol.name);
3924
+ const { text, fromDeclarations: fromDeclarations2 } = embedTextFor(
3925
+ relativeOf(file),
3926
+ summaries.get(file),
3927
+ declarations
3928
+ );
3929
+ if (text.trim().length === 0) continue;
3930
+ const hash = xxhash64String(text);
3931
+ if (options.force !== true && stored.get(file) === hash) {
3932
+ cached++;
3933
+ continue;
3934
+ }
3935
+ queue.push({ file, text, hash, fromDeclarations: fromDeclarations2 });
3936
+ if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
3937
+ }
3938
+ let embedded = 0;
3939
+ let fromDeclarations = 0;
3940
+ let done = 0;
3941
+ for (let start = 0; start < queue.length; start += batchSize) {
3942
+ if (signal?.aborted) break;
3943
+ const batch = queue.slice(start, start + batchSize);
3944
+ try {
3945
+ const vectors = await port.embed(batch.map((entry) => entry.text));
3946
+ const rows = [];
3947
+ for (let i = 0; i < batch.length; i++) {
3948
+ const entry = batch[i];
3949
+ const vector = vectors[i];
3950
+ if (vector === void 0 || vector.length !== port.dimensions) continue;
3951
+ rows.push({ file: entry.file, vector, sourceHash: entry.hash, provider: port.id });
3952
+ if (entry.fromDeclarations) fromDeclarations++;
3953
+ }
3954
+ store.upsertFileVectors(rows);
3955
+ embedded += rows.length;
3956
+ } catch (error) {
3957
+ if (errors.length < 20) {
3958
+ errors.push(`batch at ${start}: ${error instanceof Error ? error.message : String(error)}`);
3959
+ }
3960
+ } finally {
3961
+ done += batch.length;
3962
+ options.onProgress?.(done, queue.length);
3963
+ }
3964
+ }
3965
+ return {
3966
+ embedded,
3967
+ cached,
3968
+ pruned,
3969
+ providerChanged,
3970
+ fromDeclarations,
3971
+ durationMs: Date.now() - startedAt,
3972
+ errors
3973
+ };
3974
+ }
3975
+ async function embedProjectFiles(projectRoot, port, options = {}) {
3976
+ const { indexDir, ...rest } = options;
3977
+ if (!existsSync6(path12.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
3978
+ return { indexed: false };
3979
+ }
3980
+ const store = indexStorePool.acquire(projectRoot, { indexDir });
3981
+ try {
3982
+ return await embedFiles(
3983
+ store,
3984
+ port,
3985
+ (file) => {
3986
+ const relative12 = path12.relative(projectRoot, file);
3987
+ return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
3988
+ },
3989
+ rest
3990
+ );
3991
+ } finally {
3992
+ indexStorePool.release(store);
3993
+ }
3994
+ }
3995
+
2842
3996
  export {
2843
3997
  PolyglotInvariantEngine,
2844
3998
  polyglotInvariantEngine,
2845
3999
  replaceSymbolInFile,
2846
4000
  codebaseAstReplaceTool,
2847
- codebaseInvariantCheckTool,
2848
4001
  codebaseImpactAnalysisTool,
2849
4002
  codebaseIncomingCallsTool,
2850
4003
  codebaseSearchTool,
2851
4004
  codebaseIndexTool,
4005
+ codebaseInvariantCheckTool,
2852
4006
  codebaseOutgoingCallsTool,
2853
4007
  extractFileSkeleton,
2854
4008
  extractDirectorySkeleton,
@@ -2858,6 +4012,39 @@ export {
2858
4012
  codebaseStatsTool,
2859
4013
  codebaseTargetedTestTool,
2860
4014
  deadCodeScanTool,
2861
- runDeadCodeScan
4015
+ runDeadCodeScan,
4016
+ EXPORT_PACKAGE_LIMIT,
4017
+ EXPORT_FILE_LIMIT,
4018
+ renderAtlasHtml,
4019
+ ATLAS_DIR,
4020
+ ATLAS_JSON,
4021
+ ATLAS_MARKDOWN,
4022
+ ATLAS_MANIFEST,
4023
+ ATLAS_SCHEMA,
4024
+ ATLAS_FILE_LIMIT,
4025
+ buildAtlas,
4026
+ writeAtlas,
4027
+ MAX_REPORTED_DRIFT,
4028
+ checkAtlasFreshness,
4029
+ writeProjectAtlas,
4030
+ checkProjectAtlasFreshness,
4031
+ exportProjectAtlasHtml,
4032
+ BRIEF_HUB_LIMIT,
4033
+ BRIEF_PACKAGE_LIMIT,
4034
+ BRIEF_SUBSYSTEM_LIMIT,
4035
+ buildAtlasBrief,
4036
+ buildProjectAtlasBrief,
4037
+ setContextQueryEmbedder,
4038
+ codebaseContextTool,
4039
+ MAX_SOURCE_CHARS,
4040
+ MAX_CRUX_LINES,
4041
+ DEFAULT_CONCURRENCY,
4042
+ MAX_SUMMARY_CHARS,
4043
+ enrichConcepts,
4044
+ enrichProjectConcepts,
4045
+ DEFAULT_BATCH_SIZE,
4046
+ MAX_EMBED_CHARS,
4047
+ embedFiles,
4048
+ embedProjectFiles
2862
4049
  };
2863
- //# sourceMappingURL=chunk-YEISRT4S.js.map
4050
+ //# sourceMappingURL=chunk-PNLDTRC5.js.map