@nxuss/lemma 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +24 -4
  2. package/dist/cjs/mcp/index.js +14 -0
  3. package/dist/cjs/mcp/index.js.map +1 -1
  4. package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
  5. package/dist/cjs/mcp/tool-profiles.js +3 -0
  6. package/dist/cjs/mcp/tool-profiles.js.map +1 -1
  7. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  8. package/dist/cjs/mcp/tools.js +290 -138
  9. package/dist/cjs/mcp/tools.js.map +1 -1
  10. package/dist/cjs/subconscious/TheBrainV2.d.ts +27 -1
  11. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
  12. package/dist/cjs/subconscious/TheBrainV2.js +53 -1
  13. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
  14. package/dist/cjs/utils/AffectedTests.d.ts +54 -0
  15. package/dist/cjs/utils/AffectedTests.d.ts.map +1 -0
  16. package/dist/cjs/utils/AffectedTests.js +195 -0
  17. package/dist/cjs/utils/AffectedTests.js.map +1 -0
  18. package/dist/cjs/utils/DepGraph.d.ts +28 -0
  19. package/dist/cjs/utils/DepGraph.d.ts.map +1 -0
  20. package/dist/cjs/utils/DepGraph.js +210 -0
  21. package/dist/cjs/utils/DepGraph.js.map +1 -0
  22. package/dist/cjs/utils/LocalPrefilter.d.ts +61 -0
  23. package/dist/cjs/utils/LocalPrefilter.d.ts.map +1 -0
  24. package/dist/cjs/utils/LocalPrefilter.js +169 -0
  25. package/dist/cjs/utils/LocalPrefilter.js.map +1 -0
  26. package/dist/cjs/utils/OutputDelta.d.ts +60 -0
  27. package/dist/cjs/utils/OutputDelta.d.ts.map +1 -0
  28. package/dist/cjs/utils/OutputDelta.js +226 -0
  29. package/dist/cjs/utils/OutputDelta.js.map +1 -0
  30. package/dist/cjs/utils/OutputLedger.d.ts +60 -0
  31. package/dist/cjs/utils/OutputLedger.d.ts.map +1 -0
  32. package/dist/cjs/utils/OutputLedger.js +116 -0
  33. package/dist/cjs/utils/OutputLedger.js.map +1 -0
  34. package/dist/cjs/utils/ReadWorkspaceCache.d.ts +8 -2
  35. package/dist/cjs/utils/ReadWorkspaceCache.d.ts.map +1 -1
  36. package/dist/cjs/utils/ReadWorkspaceCache.js +58 -3
  37. package/dist/cjs/utils/ReadWorkspaceCache.js.map +1 -1
  38. package/dist/esm/mcp/index.js +14 -0
  39. package/dist/esm/mcp/index.js.map +1 -1
  40. package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
  41. package/dist/esm/mcp/tool-profiles.js +3 -0
  42. package/dist/esm/mcp/tool-profiles.js.map +1 -1
  43. package/dist/esm/mcp/tools.d.ts.map +1 -1
  44. package/dist/esm/mcp/tools.js +288 -136
  45. package/dist/esm/mcp/tools.js.map +1 -1
  46. package/dist/esm/subconscious/TheBrainV2.d.ts +27 -1
  47. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
  48. package/dist/esm/subconscious/TheBrainV2.js +53 -1
  49. package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
  50. package/dist/esm/utils/AffectedTests.d.ts +54 -0
  51. package/dist/esm/utils/AffectedTests.d.ts.map +1 -0
  52. package/dist/esm/utils/AffectedTests.js +186 -0
  53. package/dist/esm/utils/AffectedTests.js.map +1 -0
  54. package/dist/esm/utils/DepGraph.d.ts +28 -0
  55. package/dist/esm/utils/DepGraph.d.ts.map +1 -0
  56. package/dist/esm/utils/DepGraph.js +169 -0
  57. package/dist/esm/utils/DepGraph.js.map +1 -0
  58. package/dist/esm/utils/LocalPrefilter.d.ts +61 -0
  59. package/dist/esm/utils/LocalPrefilter.d.ts.map +1 -0
  60. package/dist/esm/utils/LocalPrefilter.js +159 -0
  61. package/dist/esm/utils/LocalPrefilter.js.map +1 -0
  62. package/dist/esm/utils/OutputDelta.d.ts +60 -0
  63. package/dist/esm/utils/OutputDelta.d.ts.map +1 -0
  64. package/dist/esm/utils/OutputDelta.js +221 -0
  65. package/dist/esm/utils/OutputDelta.js.map +1 -0
  66. package/dist/esm/utils/OutputLedger.d.ts +60 -0
  67. package/dist/esm/utils/OutputLedger.d.ts.map +1 -0
  68. package/dist/esm/utils/OutputLedger.js +107 -0
  69. package/dist/esm/utils/OutputLedger.js.map +1 -0
  70. package/dist/esm/utils/ReadWorkspaceCache.d.ts +8 -2
  71. package/dist/esm/utils/ReadWorkspaceCache.d.ts.map +1 -1
  72. package/dist/esm/utils/ReadWorkspaceCache.js +57 -3
  73. package/dist/esm/utils/ReadWorkspaceCache.js.map +1 -1
  74. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"tool-profiles.js","sourceRoot":"","sources":["../../../src/mcp/tool-profiles.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-profiles.js","sourceRoot":"","sources":["../../../src/mcp/tool-profiles.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AA0kCnE,oFAAoF;AACpF,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAQA;AAqHD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CAwEN"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAknCnE,oFAAoF;AACpF,wBAAgB,kBAAkB,IAAI,MAAM,CAM3C;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAQA;AAsHD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqCD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,IAAI,CAwEN"}
@@ -37,7 +37,12 @@ import { recordReceiptEvent, getReceiptSummary, getLedgerLength } from "../utils
37
37
  import { distillCommandOutput, buildDistillFooter, readRegion } from "../utils/CommandOutputDistiller.js";
38
38
  import { findMatch, reindentReplacement } from "../utils/PatchMatcher.js";
39
39
  import { searchWorkspace, groupSearchResults, parseExtensionFilter } from "../utils/WorkspaceSearch.js";
40
- import { checkAlreadySent, recordSent } from "../utils/ReadWorkspaceCache.js";
40
+ import { rankCandidates } from "../utils/LocalPrefilter.js";
41
+ import { checkAlreadySent, recordSent, getLastEmitted } from "../utils/ReadWorkspaceCache.js";
42
+ import { shouldEmitDelta } from "../utils/OutputDelta.js";
43
+ import { ledgerKey, checkOutput, recordOutput as recordLedgerOutput } from "../utils/OutputLedger.js";
44
+ import { buildDepGraphMap, bfsDepGraph } from "../utils/DepGraph.js";
45
+ import { findAffectedTests, formatAffectedTests } from "../utils/AffectedTests.js";
41
46
  import { mapSqueezedToOriginal, withLineNumbers, sequentialNumbers } from "../utils/LineNumberMap.js";
42
47
  import { fileOutline, formatOutline } from "../utils/FileOutline.js";
43
48
  import { getCachedSearch, setCachedSearch } from "../utils/SearchCache.js";
@@ -137,6 +142,30 @@ const toolDefinitions = [
137
142
  description: "Symbols (not whole files) this answer depends on. Freshness is then judged by that symbol's own source, so edits elsewhere in the same file don't stale this memory out.",
138
143
  },
139
144
  outcome: { type: "string", enum: ["confirmed", "failed"], description: "Default 'confirmed' (a verified working solution). Set 'failed' to record a dead end — an approach ruled out, not one to recommend." },
145
+ claims: {
146
+ type: "array",
147
+ items: {
148
+ type: "object",
149
+ properties: {
150
+ text: { type: "string", description: "One sub-statement from `response`, verifiable on its own (e.g. one sentence about one function)." },
151
+ filePaths: { type: "array", items: { type: "string" }, description: "Paths this specific claim depends on." },
152
+ symbols: {
153
+ type: "array",
154
+ items: {
155
+ type: "object",
156
+ properties: {
157
+ filePath: { type: "string" },
158
+ symbolName: { type: "string" },
159
+ },
160
+ required: ["filePath", "symbolName"],
161
+ },
162
+ description: "Symbols this specific claim depends on.",
163
+ },
164
+ },
165
+ required: ["text"],
166
+ },
167
+ description: "Split `response` into independently-verifiable sub-claims when it makes more than one assertion about different parts of the code. Each claim tracks its own filePaths/symbols, so one claim going stale (e.g. one function changed) doesn't discard the others that are still true. Skip this for a single-fact response — plain filePaths/symbols above already covers that case.",
168
+ },
140
169
  },
141
170
  required: ["query", "response"],
142
171
  },
@@ -165,7 +194,7 @@ const toolDefinitions = [
165
194
  },
166
195
  {
167
196
  name: "read_workspace_file",
168
- description: "Read a file inside the workspace. Compresses comments/whitespace and scrubs API keys. Every line carries its REAL line number in the original file (compression removes lines, it never renumbers them), so you can cite file:line and patch precisely without re-reading. The header lists which imports were compressed away. Use offset/limit to read a line range of a large file. If this exact view was already read this session and hasn't changed on disk, returns an UNCHANGED marker plus a symbol outline reuse your copy, or pass force:true if it's no longer in your context.",
197
+ description: "Read a file inside the workspace. Compresses comments/whitespace and scrubs API keys. Every line carries its REAL line number in the original file (compression removes lines, it never renumbers them), so you can cite file:line and patch precisely without re-reading. The header lists which imports were compressed away. Use offset/limit to read a line range of a large file. If this exact view was already read this session and hasn't changed on disk, returns an UNCHANGED marker plus a symbol outline. If it changed only slightly, returns a CHANGED marker with a unified diff to apply to the copy you already hold. Either way, pass force:true to get the full file when it is no longer in your context.",
169
198
  inputSchema: {
170
199
  type: "object",
171
200
  properties: {
@@ -707,6 +736,17 @@ const toolDefinitions = [
707
736
  required: ["filePath"],
708
737
  },
709
738
  },
739
+ {
740
+ name: "affected_tests",
741
+ description: "Given the current diff (or an explicit file list), returns only the test files that transitively import the changed code, plus the exact command to run just those. Uses the TypeScript import graph — zero LLM calls. Run this instead of the full suite after an edit: fewer minutes, and far less test output in context. Reports honestly when the full suite is the right answer (unbounded change, runtime-only coupling, most of the suite affected).",
742
+ inputSchema: {
743
+ type: "object",
744
+ properties: {
745
+ files: { type: "array", items: { type: "string" }, description: "Explicit changed files, relative to workspace root. Omit to read the diff from git (working tree + untracked)." },
746
+ baseRef: { type: "string", description: "Additional git ref to diff against, e.g. 'main'. The working tree is always included." },
747
+ },
748
+ },
749
+ },
710
750
  {
711
751
  name: "refactor",
712
752
  description: "Declarative multi-file codemod engine. Rename symbols across the entire workspace or move files updating all imports. Uses TypeScript Compiler API — zero LLM calls. Supports dry-run diff preview and post-refactor tsc verification.",
@@ -1139,6 +1179,7 @@ const toolHandlers = {
1139
1179
  pr_status: handlePRStatus,
1140
1180
  generate_pr_workflow: handleGeneratePRWorkflow,
1141
1181
  depgraph: handleDepgraph,
1182
+ affected_tests: handleAffectedTests,
1142
1183
  refactor: handleRefactor,
1143
1184
  smart_file_slice: handleSmartFileSlice,
1144
1185
  test_oracle: handleTestOracle,
@@ -1348,7 +1389,16 @@ async function handleSearchMemory(args) {
1348
1389
  }
1349
1390
  const fresh = results.filter((r) => r.fresh);
1350
1391
  const stale = results.filter((r) => !r.fresh);
1351
- const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)${r.outcome === "failed" ? "\n⚠️ TAGGED AS A FAILED ATTEMPT — this was tried before and did NOT work. Do not repeat it; treat this as a warning, not a suggestion." : ""}\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
1392
+ const formatClaims = (r) => {
1393
+ if (!Array.isArray(r.claims) || r.claims.length === 0)
1394
+ return "";
1395
+ const lines = r.claims.map((c) => c.fresh
1396
+ ? ` ✓ fresh: ${c.text}`
1397
+ : ` ✗ STALE (${(c.staleFiles || []).join(", ")}): ${c.text}`);
1398
+ const staleCount = r.claims.filter((c) => !c.fresh).length;
1399
+ return `\nClaims (${r.claims.length - staleCount}/${r.claims.length} still fresh — trust only the ✓ ones):\n${lines.join("\n")}`;
1400
+ };
1401
+ const formatResult = (r, i) => `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)${r.outcome === "failed" ? "\n⚠️ TAGGED AS A FAILED ATTEMPT — this was tried before and did NOT work. Do not repeat it; treat this as a warning, not a suggestion." : ""}\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}${formatClaims(r)}`;
1352
1402
  if (fresh.length > 0) {
1353
1403
  // Only a fresh hit (or an untracked, purely conceptual entry) counts as a real
1354
1404
  // avoided re-investigation — this is the only case worth crediting as savings.
@@ -1393,11 +1443,14 @@ async function handleStoreMemory(args) {
1393
1443
  const symbols = Array.isArray(args?.symbols)
1394
1444
  ? args.symbols.filter((s) => s?.filePath && s?.symbolName)
1395
1445
  : undefined;
1446
+ const claims = Array.isArray(args?.claims)
1447
+ ? args.claims.filter((c) => c?.text)
1448
+ : undefined;
1396
1449
  if (!query || !responseText)
1397
1450
  throw new Error("Query and response are required");
1398
1451
  try {
1399
1452
  const brain = getBrain();
1400
- const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols);
1453
+ const storeRes = brain.store(query, responseText, provider, 0.92, filePaths, undefined, outcome, symbols, claims);
1401
1454
  // Storing a memory saves nothing — it only creates the chance of a saving later.
1402
1455
  // Crediting tokens here inflated the ledger on write and then credited the same
1403
1456
  // answer again on every read. The saving is booked by search_memory on a fresh hit.
@@ -1409,7 +1462,8 @@ async function handleStoreMemory(args) {
1409
1462
  const trackingNote = filePaths && filePaths.length > 0 ? ` Tracking freshness against ${filePaths.length} file(s) — this memory auto-invalidates if they change.` : "";
1410
1463
  const symbolNote = symbols && symbols.length > 0 ? ` Tracking freshness against ${symbols.length} symbol(s) specifically — unrelated edits elsewhere in those files won't stale this out.` : "";
1411
1464
  const outcomeNote = outcome === "failed" ? " Tagged as a FAILED attempt — future searches will surface it as a warning, not a suggestion." : "";
1412
- return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}` }] };
1465
+ const claimsNote = claims && claims.length > 0 ? ` Split into ${claims.length} independently-verifiable claim(s) — a future search can trust the ones still fresh even if another goes stale.` : "";
1466
+ return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}${trackingNote}${symbolNote}${outcomeNote}${claimsNote}` }] };
1413
1467
  }
1414
1468
  catch (e) {
1415
1469
  logError("store_memory", e);
@@ -1539,7 +1593,42 @@ async function handleReadWorkspaceFile(args) {
1539
1593
  }
1540
1594
  if (numbers)
1541
1595
  content = withLineNumbers(content, numbers);
1542
- recordSent(resolved, view, dedup.hash);
1596
+ // The file changed, but usually only slightly — the common case in an edit loop is a
1597
+ // few lines moving in a file the model already holds in full. Re-sending everything
1598
+ // charges for the whole file (and keeps charging, since tool results stay in the
1599
+ // transcript). A diff against the exact text we last emitted covers the same ground
1600
+ // for a fraction of the tokens; `shouldEmitDelta` declines whenever it wouldn't.
1601
+ if (!force) {
1602
+ const previousEmitted = getLastEmitted(resolved, view);
1603
+ if (previousEmitted !== null) {
1604
+ const delta = shouldEmitDelta(previousEmitted, content);
1605
+ if (delta.emit) {
1606
+ reportSavings({
1607
+ source: "contextSqueeze",
1608
+ charsBefore: content.length,
1609
+ charsAfter: delta.body.length,
1610
+ toolName: "read_workspace_file",
1611
+ filePath,
1612
+ });
1613
+ recordSent(resolved, view, dedup.hash, content);
1614
+ return {
1615
+ content: [
1616
+ {
1617
+ type: "text",
1618
+ text: [
1619
+ `CHANGED: ${filePath} — ${delta.hunkCount} hunk(s), +${delta.added}/-${delta.removed} since you last read it.`,
1620
+ `Apply this diff to the copy you already have. Line numbers are real positions in the file on disk.`,
1621
+ `If you no longer hold that copy, re-fetch with force:true.`,
1622
+ ``,
1623
+ delta.body,
1624
+ ].join("\n"),
1625
+ },
1626
+ ],
1627
+ };
1628
+ }
1629
+ }
1630
+ }
1631
+ recordSent(resolved, view, dedup.hash, content);
1543
1632
  return { content: [{ type: "text", text: rangeHeader + content }] };
1544
1633
  }
1545
1634
  catch (err) {
@@ -1758,9 +1847,35 @@ async function handleRunWorkspaceCommand(args) {
1758
1847
  handle: distilled.handle,
1759
1848
  });
1760
1849
  }
1850
+ // A debug loop runs the same command repeatedly and the output barely moves between
1851
+ // runs. The header is part of the compared text on purpose: a pass/fail flip must never
1852
+ // be reported as "identical", even in the rare case where the body alone matches.
1853
+ // The footer is deliberately excluded and always re-appended — it carries this run's
1854
+ // `output_region` handle, which is different every time and must stay usable.
1855
+ const body = `${header}\n\nOutput:\n${distilled.text}`;
1856
+ const footer = buildDistillFooter(distilled);
1857
+ const key = ledgerKey("run_workspace_command", { command, cwd: workspaceRoot, raw: args?.raw === true });
1858
+ const ledger = checkOutput(key, body, {
1859
+ label: `\`${command}\``,
1860
+ rerunHint: "The command did run again — this reports its result, it does not skip execution.",
1861
+ });
1862
+ recordLedgerOutput(key, body);
1863
+ if (ledger.replacement) {
1864
+ reportSavings({
1865
+ source: "contextSqueeze",
1866
+ charsBefore: body.length,
1867
+ charsAfter: ledger.replacement.length,
1868
+ toolName: "run_workspace_command",
1869
+ });
1870
+ recordReceiptEvent("tool_call", `run_workspace_command ${ledger.status} vs. earlier run`, {
1871
+ command,
1872
+ savedChars: ledger.savedChars,
1873
+ });
1874
+ return { content: [{ type: "text", text: ledger.replacement + footer }] };
1875
+ }
1761
1876
  return {
1762
1877
  content: [
1763
- { type: "text", text: `${header}\n\nOutput:\n${distilled.text}${buildDistillFooter(distilled)}` },
1878
+ { type: "text", text: body + footer },
1764
1879
  ],
1765
1880
  };
1766
1881
  }
@@ -1851,6 +1966,67 @@ async function handleListWorkspaceDir(args) {
1851
1966
  return { content: [{ type: "text", text: `Error listing directory: ${err.message}` }] };
1852
1967
  }
1853
1968
  }
1969
+ /** Files shown after a successful local ranking; the rest are named, not silently dropped. */
1970
+ const PREFILTER_FILES_SHOWN = 12;
1971
+ /**
1972
+ * Rank matching files with the local model and show only the top ones.
1973
+ *
1974
+ * Ranking happens per file, not per match: the model is choosing which files are worth
1975
+ * reading, and ranking 200 individual lines would ask it a question it cannot answer as
1976
+ * well. On any failure — disabled, no Ollama, unparseable reply — this returns the input
1977
+ * untouched, which is the behaviour with the feature off.
1978
+ */
1979
+ async function prefilterSearchMatches(query, matches) {
1980
+ const byFile = new Map();
1981
+ for (const m of matches) {
1982
+ const list = byFile.get(m.filePath);
1983
+ if (list)
1984
+ list.push(m);
1985
+ else
1986
+ byFile.set(m.filePath, [m]);
1987
+ }
1988
+ const totalFiles = byFile.size;
1989
+ const untouched = { matches, totalFiles, note: "", applied: false };
1990
+ if (totalFiles <= PREFILTER_FILES_SHOWN)
1991
+ return untouched;
1992
+ try {
1993
+ const candidates = [...byFile.entries()].map(([filePath, fileMatches]) => ({
1994
+ id: filePath,
1995
+ summary: `${filePath} (${fileMatches.length} match${fileMatches.length === 1 ? "" : "es"}): ${fileMatches[0].text.trim().slice(0, 120)}`,
1996
+ }));
1997
+ const ranked = await rankCandidates(query, candidates);
1998
+ if (!ranked)
1999
+ return untouched;
2000
+ const shown = ranked.order.slice(0, PREFILTER_FILES_SHOWN);
2001
+ const withheld = ranked.order.slice(PREFILTER_FILES_SHOWN);
2002
+ if (withheld.length === 0)
2003
+ return untouched;
2004
+ const shownSet = new Set(shown);
2005
+ const kept = matches.filter((m) => shownSet.has(m.filePath));
2006
+ const note = [
2007
+ ``,
2008
+ `── ${withheld.length} more file(s) matched but were ranked lower by a local model (${ranked.model}) running on this machine:`,
2009
+ ...withheld.slice(0, 25).map((f) => ` ${f}`),
2010
+ ...(withheld.length > 25 ? [` … and ${withheld.length - 25} more`] : []),
2011
+ `This ranking is a local model's judgement, not a filter on relevance — if what you need`,
2012
+ `is not above, re-run this search with maxResults raised or a narrower query to see the`,
2013
+ `full results, or read one of the files listed here directly.`,
2014
+ ].join("\n");
2015
+ reportSavings({
2016
+ source: "contextSqueeze",
2017
+ charsBefore: matches.reduce((n, m) => n + m.text.length, 0),
2018
+ charsAfter: kept.reduce((n, m) => n + m.text.length, 0),
2019
+ toolName: "search_workspace",
2020
+ query: query.substring(0, 100),
2021
+ });
2022
+ return { matches: kept, totalFiles, note, applied: true };
2023
+ }
2024
+ catch (err) {
2025
+ // The prefilter is an optimization. It must never be the reason a search returns less.
2026
+ logWarn("search_workspace", "local prefilter failed, returning unranked results");
2027
+ return untouched;
2028
+ }
2029
+ }
1854
2030
  async function handleSearchWorkspace(args) {
1855
2031
  const query = args?.query;
1856
2032
  const extension = args?.extension;
@@ -1861,29 +2037,62 @@ async function handleSearchWorkspace(args) {
1861
2037
  try {
1862
2038
  const extensions = parseExtensionFilter(extension);
1863
2039
  const cached = getCachedSearch(workspaceRoot, query, extensions, maxResults);
2040
+ // `SearchCache` spares the filesystem walk, but it still hands back the full result
2041
+ // text — and re-emitting an identical wall of matches costs exactly as many tokens as
2042
+ // producing it did. The ledger below is what turns that into a saving, so both the
2043
+ // cached and freshly-computed paths converge on one `resultText` first.
2044
+ let resultText;
1864
2045
  if (cached.text !== null) {
1865
2046
  reportSavings({ source: "cache", tokens: Math.floor(cached.text.length / 4), toolName: "search_workspace", query: query.substring(0, 100) });
1866
- return { content: [{ type: "text", text: cached.text }] };
1867
- }
1868
- const { matches, truncated } = searchWorkspace(workspaceRoot, query, extensions, maxResults);
1869
- if (matches.length === 0) {
1870
- const emptyText = `No matches found in the workspace for "${query}".`;
1871
- setCachedSearch(query, extensions, maxResults, cached.fingerprint, emptyText);
1872
- return { content: [{ type: "text", text: emptyText }] };
2047
+ resultText = cached.text;
1873
2048
  }
1874
- const grouped = groupSearchResults(matches, truncated, undefined, workspaceRoot);
1875
- let formatted = grouped.formatted;
1876
- try {
1877
- const scrubber = new SemanticScrubber();
1878
- const { maskedPrompt } = scrubber.mask(formatted);
1879
- formatted = maskedPrompt;
2049
+ else {
2050
+ const { matches, truncated } = searchWorkspace(workspaceRoot, query, extensions, maxResults);
2051
+ if (matches.length === 0) {
2052
+ resultText = `No matches found in the workspace for "${query}".`;
2053
+ setCachedSearch(query, extensions, maxResults, cached.fingerprint, resultText);
2054
+ }
2055
+ else {
2056
+ // Opt-in: a local model ranks the matching files so the expensive model only reads
2057
+ // the ones likely to matter. It only ever reorders and truncates with disclosure —
2058
+ // and returns null on any doubt, in which case `matches` is used untouched.
2059
+ const prefilter = await prefilterSearchMatches(query, matches);
2060
+ const grouped = groupSearchResults(prefilter.matches, truncated, undefined, workspaceRoot);
2061
+ let formatted = grouped.formatted + prefilter.note;
2062
+ try {
2063
+ const scrubber = new SemanticScrubber();
2064
+ const { maskedPrompt } = scrubber.mask(formatted);
2065
+ formatted = maskedPrompt;
2066
+ }
2067
+ catch (err) {
2068
+ logWarn("search_workspace", "SemanticScrubber failed, returning raw results");
2069
+ }
2070
+ const truncationNote = grouped.truncated ? ` (truncated at ${maxResults} matches — narrow your query or extension filter for the rest)` : "";
2071
+ // The header reports what the search actually found, not what survived the
2072
+ // prefilter — otherwise a ranked-down file would vanish from the count as well as
2073
+ // from the list, and nothing would tell the reader it ever existed.
2074
+ resultText = `Found ${matches.length} matches across ${prefilter.totalFiles} files${truncationNote}:\n\n${formatted}`;
2075
+ // A ranked-down result is never cached: it would then be replayed to a later call
2076
+ // whose local model was unavailable, silently serving a subset as if it were the
2077
+ // whole search. Repeats are handled by the output ledger below instead.
2078
+ if (!prefilter.applied) {
2079
+ setCachedSearch(query, extensions, maxResults, cached.fingerprint, resultText);
2080
+ }
2081
+ }
1880
2082
  }
1881
- catch (err) {
1882
- logWarn("search_workspace", "SemanticScrubber failed, returning raw results");
2083
+ const key = ledgerKey("search_workspace", { query, extension: extension ?? null, maxResults, cwd: workspaceRoot });
2084
+ const ledger = checkOutput(key, resultText, { label: `search for "${query}"` });
2085
+ recordLedgerOutput(key, resultText);
2086
+ if (ledger.replacement) {
2087
+ reportSavings({
2088
+ source: "contextSqueeze",
2089
+ charsBefore: resultText.length,
2090
+ charsAfter: ledger.replacement.length,
2091
+ toolName: "search_workspace",
2092
+ query: query.substring(0, 100),
2093
+ });
2094
+ return { content: [{ type: "text", text: ledger.replacement }] };
1883
2095
  }
1884
- const truncationNote = grouped.truncated ? ` (truncated at ${maxResults} matches — narrow your query or extension filter for the rest)` : "";
1885
- const resultText = `Found ${grouped.totalMatches} matches across ${grouped.filesMatched} files${truncationNote}:\n\n${formatted}`;
1886
- setCachedSearch(query, extensions, maxResults, cached.fingerprint, resultText);
1887
2096
  return {
1888
2097
  content: [
1889
2098
  {
@@ -3769,113 +3978,9 @@ function generateAzurePipelineYaml(threshold) {
3769
3978
  ` AZURE_DEVOPS_TOKEN: \$(AZURE_DEVOPS_TOKEN)`,
3770
3979
  ].join("\n");
3771
3980
  }
3772
- function buildDepGraphMap(workspaceRoot) {
3773
- const map = new Map();
3774
- const IGNORE_DG = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
3775
- function walkDG(dir) {
3776
- let entries = [];
3777
- try {
3778
- entries = fs.readdirSync(dir, { withFileTypes: true });
3779
- }
3780
- catch {
3781
- return;
3782
- }
3783
- for (const e of entries) {
3784
- if (IGNORE_DG.has(e.name))
3785
- continue;
3786
- const abs = path.join(dir, e.name);
3787
- if (e.isDirectory()) {
3788
- walkDG(abs);
3789
- }
3790
- else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
3791
- const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
3792
- map.set(rel, { rel, abs, imports: [], importedBy: [], externalImports: [], exports: [] });
3793
- }
3794
- }
3795
- }
3796
- walkDG(workspaceRoot);
3797
- for (const [, node] of map) {
3798
- try {
3799
- const src = fs.readFileSync(node.abs, "utf8");
3800
- const sf = ts.createSourceFile(node.abs, src, ts.ScriptTarget.Latest, true);
3801
- ts.forEachChild(sf, (n) => {
3802
- if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
3803
- const spec = n.moduleSpecifier.text;
3804
- if (spec.startsWith(".")) {
3805
- const dir = path.dirname(node.abs);
3806
- let resolved = path.resolve(dir, spec);
3807
- for (const ext of [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"]) {
3808
- if (fs.existsSync(resolved + ext)) {
3809
- resolved += ext;
3810
- break;
3811
- }
3812
- }
3813
- const rel = path.relative(workspaceRoot, resolved).replace(/\\/g, "/");
3814
- if (!node.imports.includes(rel))
3815
- node.imports.push(rel);
3816
- }
3817
- else {
3818
- const pkg = spec.split("/")[0];
3819
- if (!node.externalImports.includes(pkg))
3820
- node.externalImports.push(pkg);
3821
- }
3822
- }
3823
- const hasExportMod = (nd) => ts.canHaveModifiers(nd) &&
3824
- (ts.getModifiers(nd) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
3825
- if (hasExportMod(n)) {
3826
- if (ts.isFunctionDeclaration(n) && n.name)
3827
- node.exports.push(n.name.text);
3828
- else if (ts.isClassDeclaration(n) && n.name)
3829
- node.exports.push(n.name.text);
3830
- else if (ts.isVariableStatement(n)) {
3831
- for (const decl of n.declarationList.declarations) {
3832
- if (ts.isIdentifier(decl.name))
3833
- node.exports.push(decl.name.text);
3834
- }
3835
- }
3836
- else if (ts.isInterfaceDeclaration(n))
3837
- node.exports.push(n.name.text);
3838
- else if (ts.isTypeAliasDeclaration(n))
3839
- node.exports.push(n.name.text);
3840
- else if (ts.isEnumDeclaration(n))
3841
- node.exports.push(n.name.text);
3842
- }
3843
- if (ts.isExportDeclaration(n) && n.exportClause && ts.isNamedExports(n.exportClause)) {
3844
- for (const el of n.exportClause.elements)
3845
- node.exports.push(el.name.text);
3846
- }
3847
- });
3848
- }
3849
- catch { /* skip unparseable */ }
3850
- }
3851
- for (const [, node] of map) {
3852
- for (const imp of node.imports) {
3853
- const target = map.get(imp);
3854
- if (target && !target.importedBy.includes(node.rel))
3855
- target.importedBy.push(node.rel);
3856
- }
3857
- }
3858
- return map;
3859
- }
3860
- function bfsDepGraph(map, startRel, direction, maxDepth) {
3861
- const visited = new Map();
3862
- const queue = [{ rel: startRel, depth: 0 }];
3863
- while (queue.length > 0) {
3864
- const item = queue.shift();
3865
- if (visited.has(item.rel) || item.depth > maxDepth)
3866
- continue;
3867
- const node = map.get(item.rel);
3868
- if (!node)
3869
- continue;
3870
- visited.set(item.rel, { node, depth: item.depth });
3871
- if (item.depth < maxDepth) {
3872
- const neighbors = direction === "forward" ? node.imports : node.importedBy;
3873
- for (const n of neighbors)
3874
- queue.push({ rel: n, depth: item.depth + 1 });
3875
- }
3876
- }
3877
- return visited;
3878
- }
3981
+ // ═══════════════════════════════════════════════════════════════════════════════
3982
+ // ─── depgraph ─────────────────────────────────────────────────────────────────
3983
+ // ═══════════════════════════════════════════════════════════════════════════════
3879
3984
  function detectDepCycles(map, subset) {
3880
3985
  const cycles = [];
3881
3986
  const visited = new Set();
@@ -4080,13 +4185,46 @@ async function handleDepgraph(args) {
4080
4185
  output = formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
4081
4186
  }
4082
4187
  const stats = `\n\n📊 Stats: ${forwardVisited.size} nodos forward, ${backwardVisited.size} nodos reverse, depth=${depth}`;
4083
- return { content: [{ type: "text", text: output + (format === "json" ? "" : stats) }] };
4188
+ const finalText = output + (format === "json" ? "" : stats);
4189
+ const key = ledgerKey("depgraph", { file: relTarget, depth, format, showReverse, includeExternal, detectCircular, cwd: workspaceRoot });
4190
+ const ledger = checkOutput(key, finalText, { label: `depgraph for ${relTarget}` });
4191
+ recordLedgerOutput(key, finalText);
4192
+ if (ledger.replacement) {
4193
+ reportSavings({ source: "contextSqueeze", charsBefore: finalText.length, charsAfter: ledger.replacement.length, toolName: "depgraph", filePath: relTarget });
4194
+ return { content: [{ type: "text", text: ledger.replacement }] };
4195
+ }
4196
+ return { content: [{ type: "text", text: finalText }] };
4084
4197
  }
4085
4198
  catch (err) {
4086
4199
  logError("depgraph", err);
4087
4200
  return { content: [{ type: "text", text: `depgraph failed: ${err.message}` }] };
4088
4201
  }
4089
4202
  }
4203
+ async function handleAffectedTests(args) {
4204
+ const workspaceRoot = process.cwd();
4205
+ try {
4206
+ const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : undefined;
4207
+ const result = findAffectedTests({ workspaceRoot, files, baseRef: args?.baseRef });
4208
+ const text = formatAffectedTests(result);
4209
+ // Worth recording even when it narrows nothing: the receipt is the record of what this
4210
+ // tool actually did, and "recommended the full suite" is as much a result as a subset.
4211
+ recordReceiptEvent("tool_call", "affected_tests", {
4212
+ changed: result.changed.length,
4213
+ affected: result.affected.length,
4214
+ totalTests: result.totalTests,
4215
+ narrowed: result.command !== null,
4216
+ });
4217
+ return { content: [{ type: "text", text }] };
4218
+ }
4219
+ catch (err) {
4220
+ logError("affected_tests", err);
4221
+ // Failing here must never look like "nothing is affected" — that would talk a caller
4222
+ // out of running tests at all.
4223
+ return {
4224
+ content: [{ type: "text", text: `affected_tests failed: ${err.message}\nFall back to running the full suite.` }],
4225
+ };
4226
+ }
4227
+ }
4090
4228
  // ═══════════════════════════════════════════════════════════════════════════════
4091
4229
  // ─── refactor ─────────────────────────────────────────────────────────────────
4092
4230
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -4584,10 +4722,13 @@ async function handleSmartFileSlice(args) {
4584
4722
  async function handleTestOracle(args) {
4585
4723
  const command = args.command || "npm test";
4586
4724
  const workspaceRoot = process.cwd();
4587
- // Run tests, check errors
4725
+ // A failing test suite is re-run more than any other command in a session, and the
4726
+ // failure list barely moves between runs. Both branches funnel into one `resultText` so
4727
+ // the ledger can answer a repeat with a diff instead of the same stack traces again.
4728
+ let resultText;
4588
4729
  try {
4589
4730
  const out = execSync(command, { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
4590
- return { content: [{ type: "text", text: `All tests passed!\n\nOutput:\n${out.substring(0, 1000)}` }] };
4731
+ resultText = `All tests passed!\n\nOutput:\n${out.substring(0, 1000)}`;
4591
4732
  }
4592
4733
  catch (e) {
4593
4734
  const stdout = e.stdout || "";
@@ -4597,8 +4738,19 @@ async function handleTestOracle(args) {
4597
4738
  .split("\n")
4598
4739
  .filter((l) => l.includes("fail") || l.includes("Error") || l.includes("at ") || l.includes("✗") || l.includes("Stack"));
4599
4740
  const summary = failureLines.slice(0, 80).join("\n") || `Test execution failed:\n${stdout.substring(0, 800)}\n${stderr.substring(0, 800)}`;
4600
- return { content: [{ type: "text", text: `Tests Failed. Compressed failures:\n\n${summary}` }] };
4741
+ resultText = `Tests Failed. Compressed failures:\n\n${summary}`;
4742
+ }
4743
+ const key = ledgerKey("test_oracle", { command, cwd: workspaceRoot });
4744
+ const ledger = checkOutput(key, resultText, {
4745
+ label: `\`${command}\``,
4746
+ rerunHint: "The suite did run again — this reports its result, it does not skip execution.",
4747
+ });
4748
+ recordLedgerOutput(key, resultText);
4749
+ if (ledger.replacement) {
4750
+ reportSavings({ source: "contextSqueeze", charsBefore: resultText.length, charsAfter: ledger.replacement.length, toolName: "test_oracle" });
4751
+ return { content: [{ type: "text", text: ledger.replacement }] };
4601
4752
  }
4753
+ return { content: [{ type: "text", text: resultText }] };
4602
4754
  }
4603
4755
  // ── Schema Extract ───────────────────────────────────────────────────
4604
4756
  async function handleSchemaExtract(args) {