weavatrix 0.3.12 → 0.3.14

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 (34) hide show
  1. package/README.md +9 -4
  2. package/docs/releases/v0.3.13.md +34 -0
  3. package/docs/releases/v0.3.14.md +37 -0
  4. package/package.json +3 -1
  5. package/src/analysis/coverage-reports-tarpaulin.js +112 -0
  6. package/src/analysis/coverage-reports.js +13 -2
  7. package/src/analysis/dead-code-review/policy.js +1 -1
  8. package/src/analysis/duplicate-groups.js +1 -1
  9. package/src/analysis/endpoints/common.js +18 -1
  10. package/src/analysis/endpoints/extract.js +13 -0
  11. package/src/analysis/graph-analysis.aggregate.js +4 -3
  12. package/src/analysis/graph-analysis.refs.js +48 -0
  13. package/src/analysis/hot-path-review.js +1 -1
  14. package/src/analysis/internal-audit.reach.js +28 -0
  15. package/src/analysis/internal-audit.run.js +1 -1
  16. package/src/analysis/static-test-reachability.js +1 -1
  17. package/src/mcp/architecture-starter.mjs +1 -1
  18. package/src/mcp/catalog.mjs +9 -5
  19. package/src/mcp/company-contract-page.mjs +142 -0
  20. package/src/mcp/company-contract-reconcile.mjs +87 -0
  21. package/src/mcp/evidence-snapshot.duplicates.mjs +1 -1
  22. package/src/mcp/graph/context-seeds.mjs +0 -7
  23. package/src/mcp/graph/tools-query.mjs +34 -7
  24. package/src/mcp/health/audit-format.mjs +2 -2
  25. package/src/mcp/health/audit.mjs +29 -4
  26. package/src/mcp/health/duplicates.mjs +43 -7
  27. package/src/mcp/health/endpoints.mjs +1 -1
  28. package/src/mcp/health/structure.mjs +4 -3
  29. package/src/mcp/tools-company.mjs +52 -76
  30. package/src/mcp/tools-context.mjs +1 -1
  31. package/src/mcp/tools-endpoints.mjs +1 -1
  32. package/src/mcp/tools-graph-hubs.mjs +1 -1
  33. package/src/mcp/tools-verified-change.mjs +11 -4
  34. package/src/path-classification.js +7 -1
package/README.md CHANGED
@@ -12,9 +12,12 @@ questions. **34 network-free tools. No repository data leaves your machine.**
12
12
  - Source: [github.com/sergii-ziborov/weavatrix](https://github.com/sergii-ziborov/weavatrix)
13
13
  - npm: [`weavatrix`](https://www.npmjs.com/package/weavatrix) — `npx -y weavatrix <repoRoot>`
14
14
 
15
- This package is the complete offline engine under the MIT license and initiates no outbound HTTP.
16
- Every network capability lives in the separately versioned `weavatrix-online` superset, which depends
17
- on this core through a supported extension API. The split is documented in
15
+ This package is the complete offline engine under the MIT license: it reads and analyzes your code,
16
+ initiates no outbound HTTP, and never edits your source. It is the read-only base of a layered stack —
17
+ add [`weavatrix-refactor`](https://github.com/sergii-ziborov/weavatrix-refactor) (Apache-2.0) to apply
18
+ hash-verified, reversible edits, or [`weavatrix-online`](https://github.com/sergii-ziborov/weavatrix-online)
19
+ for authorized Cloud or self-hosted sync. Each is an optional superset that depends on this core through
20
+ a supported extension API; the offline/online split is documented in
18
21
  [docs/adr/0001-v0.3-offline-online-split.md](docs/adr/0001-v0.3-offline-online-split.md).
19
22
 
20
23
  ## Install
@@ -125,7 +128,9 @@ The 34 methods project the same reusable graph into the smallest view a task nee
125
128
  - **build** — `rebuild_graph` (reports the structural delta, keeps the prior state as `graph.prev.json`).
126
129
  - **retarget** *(in `offline`, absent from `pinned`)* — `open_repo`, `list_known_repos`.
127
130
  - **crossrepo** *(in `offline`, absent from `pinned`)* — `trace_api_contract` (joins routes to client
128
- call-sites across registered local graphs; reads no source).
131
+ call-sites across registered local graphs; reads no source). Results are compact and paginated by
132
+ default (`page_size`, opaque `cursor`, and bounded `per_item_limit` samples); `response_detail="full"`
133
+ is an explicit opt-in and remains paginated.
129
134
 
130
135
  Every finding is review evidence, never an auto-delete verdict: `find_dead_code` /
131
136
  `run_audit category=unused` always return `REVIEW_REQUIRED` with `autoDelete:false`. Typecheck, tests
@@ -0,0 +1,34 @@
1
+ # Weavatrix 0.3.13
2
+
3
+ 0.3.13 fixes two misleading verification and graph-path signals found while
4
+ dogfooding a real duplicate-removal refactor. It also bounds cross-repository
5
+ contract responses with revision-bound pagination.
6
+
7
+ ## Honest refactor verification
8
+
9
+ - `verified_change` no longer reports an enabled duplicate ratchet as
10
+ `SKIPPED: duplicate ratchet disabled` during the planning phase. An enabled
11
+ plan now reports `PLANNED`, `enabled: true`, and states that the ratchet runs
12
+ during `phase="verify"`. An explicit `duplicate_ratchet: false` remains the
13
+ only disabled state.
14
+ - `shortest_path` remains an undirected connectivity query, but every rendered
15
+ hop now preserves the stored graph-edge direction. Reverse traversal renders
16
+ `<--imports--` or `<--calls--` instead of inventing a forward dependency.
17
+ Repeated and generic file names are widened in both the header and hop lines.
18
+
19
+ ## Bounded cross-repository contract evidence
20
+
21
+ - `trace_api_contract` response schema v4 returns compact, paginated evidence
22
+ by default. `page_size`, `per_item_limit`, and a revision/filter-bound opaque
23
+ `cursor` keep responses bounded; `response_detail="full"` remains an explicit
24
+ opt-in and is still paginated.
25
+ - Invalid, stale, or cross-query cursors fail closed with `INVALID_CURSOR`.
26
+ - Repository graph reconciliation now reuses a persisted fresh graph before
27
+ invoking a rebuild, while stale or failed refreshes retain their explicit
28
+ completeness status.
29
+
30
+ ## Layer compatibility
31
+
32
+ The read-only core keeps its 34-tool surface and `weavatrix.edit-plan.v1`
33
+ contract unchanged. `weavatrix-refactor` 0.1.1 and `weavatrix-online` 0.2.0
34
+ are the paired upper-layer releases for this core version.
@@ -0,0 +1,37 @@
1
+ # Weavatrix 0.3.14
2
+
3
+ 0.3.14 is a Repo Lens dogfood hardening release. It keeps the 34-tool,
4
+ network-free Core surface while making large JavaScript graphs practical for
5
+ UI consumers and reducing production-analysis noise.
6
+
7
+ ## Structured results and performance
8
+
9
+ - `find_duplicates` and ordinary `run_audit` results now include bounded
10
+ structured groups/findings alongside their existing text summaries, so MCP
11
+ clients can render native views without parsing prose.
12
+ - Local symbol references are indexed once per file and declaration ranges are
13
+ subtracted with binary search instead of rescanning the whole file for every
14
+ symbol. On the 8,117-node Repo Lens graph, aggregation fell from more than
15
+ 64 seconds to about 0.93 seconds while preserving graph totals.
16
+
17
+ ## Production-first precision
18
+
19
+ - `vendor`, `vendored`, and `third-party` paths are classified explicitly and
20
+ suppressed consistently across duplicates, audit, endpoints, context,
21
+ architecture, dead-code, hot-path, and graph-query surfaces.
22
+ - Electron preload files are treated as entry points only when another source
23
+ contains a bounded `BrowserWindow` `preload:` option.
24
+ - Endpoint extraction masks JavaScript regex literals and comments correctly
25
+ and recognizes bounded `node:http` method plus `url.pathname` guards. Repo
26
+ Lens now reports its four production control routes without comment/test
27
+ false positives in the production view.
28
+
29
+ ## Coverage interoperability
30
+
31
+ - `coverage_map` reads Cargo Tarpaulin's official JSON report shape, including
32
+ component paths and `traces[].stats.Line`, and advertises both Tarpaulin JSON
33
+ and `cargo llvm-cov` LCOV workflows for Rust repositories.
34
+
35
+ This Core release is paired with `weavatrix-refactor` 0.1.2 and
36
+ `weavatrix-online` 0.2.0. Each layer keeps its own package, license, version,
37
+ and release provenance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weavatrix",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "Local repository intelligence MCP for AI coding agents: an always-fresh architecture graph with production-first evidence — blast radius, dead code, endpoints, clones, history, Health and architecture safeguards, with honestly labeled precision.",
6
6
  "author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
@@ -47,6 +47,8 @@
47
47
  "docs/releases/v0.3.10.md",
48
48
  "docs/releases/v0.3.11.md",
49
49
  "docs/releases/v0.3.12.md",
50
+ "docs/releases/v0.3.13.md",
51
+ "docs/releases/v0.3.14.md",
50
52
  "docs/releases/v0.2.19.md",
51
53
  "README.md",
52
54
  "SECURITY.md",
@@ -0,0 +1,112 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ function pct01(value) {
4
+ const n = Number(value);
5
+ if (!Number.isFinite(n)) return null;
6
+ return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
7
+ }
8
+
9
+ function pctFromCounts(covered, total) {
10
+ const c = Number(covered);
11
+ const t = Number(total);
12
+ if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
13
+ return Math.max(0, Math.min(1, c / t));
14
+ }
15
+
16
+ function addLineHit(lines, line, hit) {
17
+ const value = Number(line);
18
+ if (!Number.isFinite(value) || value <= 0) return;
19
+ const n = Math.round(value);
20
+ if (n < 1) return;
21
+ const current = lines.get(n);
22
+ lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
23
+ }
24
+
25
+ function numericLineList(value) {
26
+ const input = Array.isArray(value) ? value : [];
27
+ return input
28
+ .map((line) => Number(line))
29
+ .filter((line) => Number.isFinite(line) && line > 0)
30
+ .map((line) => Math.round(line));
31
+ }
32
+
33
+ function legacyLineGroups(record) {
34
+ if (!record || typeof record !== "object") return { covered: [], uncovered: [] };
35
+ const lines = record.lines && typeof record.lines === "object" ? record.lines : {};
36
+ return {
37
+ covered: [
38
+ ...numericLineList(record.covered),
39
+ ...numericLineList(record.covered_lines),
40
+ ...numericLineList(record.coveredLines),
41
+ ...numericLineList(lines.covered),
42
+ ...numericLineList(lines.covered_lines),
43
+ ],
44
+ uncovered: [
45
+ ...numericLineList(record.uncovered),
46
+ ...numericLineList(record.uncovered_lines),
47
+ ...numericLineList(record.uncoveredLines),
48
+ ...numericLineList(record.missing),
49
+ ...numericLineList(record.missing_lines),
50
+ ...numericLineList(lines.uncovered),
51
+ ...numericLineList(lines.uncovered_lines),
52
+ ...numericLineList(lines.missing),
53
+ ],
54
+ };
55
+ }
56
+
57
+ function recordPath(record) {
58
+ const raw = record?.path || record?.file || record?.filename || record?.name;
59
+ if (Array.isArray(raw)) return raw.map(String).filter(Boolean).join("/");
60
+ return raw;
61
+ }
62
+
63
+ function reportEntries(json) {
64
+ const files = json?.files || json?.coverage || json;
65
+ if (Array.isArray(files)) {
66
+ return files.map((record) => [recordPath(record), record]).filter(([raw]) => raw);
67
+ }
68
+ if (files && typeof files === "object") return Object.entries(files);
69
+ return [];
70
+ }
71
+
72
+ function coveragePct(record, covered, total) {
73
+ const summary = record?.summary && typeof record.summary === "object" ? record.summary : {};
74
+ return pct01(
75
+ record?.coverage
76
+ ?? record?.coverage_percentage
77
+ ?? record?.line_coverage
78
+ ?? summary.coverage
79
+ ?? summary.coverage_percentage
80
+ ?? summary.percent
81
+ )
82
+ ?? pctFromCounts(record?.covered, record?.coverable)
83
+ ?? pctFromCounts(covered, total);
84
+ }
85
+
86
+ export function readTarpaulinCoverage(filePath) {
87
+ const json = JSON.parse(readFileSync(filePath, "utf8"));
88
+ return reportEntries(json).map(([path, record]) => {
89
+ const lines = new Map();
90
+ for (const trace of Array.isArray(record?.traces) ? record.traces : []) {
91
+ if (!trace?.stats || typeof trace.stats !== "object" || !("Line" in trace.stats)) continue;
92
+ addLineHit(lines, trace.line, trace.stats.Line);
93
+ }
94
+ const legacy = legacyLineGroups(record);
95
+ for (const line of legacy.uncovered) addLineHit(lines, line, 0);
96
+ for (const line of legacy.covered) addLineHit(lines, line, 1);
97
+ const measuredTotal = lines.size;
98
+ const measuredCovered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
99
+ const reportedTotal = Number(record?.coverable);
100
+ const reportedCovered = Number(record?.covered);
101
+ return {
102
+ path,
103
+ entry: {
104
+ pct: coveragePct(record, measuredCovered, measuredTotal),
105
+ total: Number.isFinite(reportedTotal) ? reportedTotal : measuredTotal,
106
+ covered: Number.isFinite(reportedCovered) ? reportedCovered : measuredCovered,
107
+ lines,
108
+ source: "tarpaulin-report.json",
109
+ },
110
+ };
111
+ });
112
+ }
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { join, relative, resolve } from "node:path";
6
6
  import { createRepoBoundary } from "../repo-path.js";
7
+ import { readTarpaulinCoverage } from "./coverage-reports-tarpaulin.js";
7
8
 
8
9
  export function normRepoPath(value) {
9
10
  return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
@@ -84,8 +85,10 @@ function mergeCoverageEntry(map, rawPath, entry, knownFiles, repoRoot) {
84
85
  }
85
86
 
86
87
  function addLineHit(lines, line, hit) {
87
- const n = Math.max(1, Math.round(Number(line) || 0));
88
- if (!n) return;
88
+ const value = Number(line);
89
+ if (!Number.isFinite(value) || value <= 0) return;
90
+ const n = Math.round(value);
91
+ if (n < 1) return;
89
92
  const current = lines.get(n);
90
93
  lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
91
94
  }
@@ -208,6 +211,12 @@ function parseGoCoverage(map, filePath, knownFiles, repoRoot) {
208
211
  }
209
212
  }
210
213
 
214
+ function parseTarpaulinJson(map, filePath, knownFiles, repoRoot) {
215
+ for (const record of readTarpaulinCoverage(filePath)) {
216
+ mergeCoverageEntry(map, record.path, record.entry, knownFiles, repoRoot);
217
+ }
218
+ }
219
+
211
220
  export function readCoverageForRepo(repoRoot, knownFiles) {
212
221
  const out = new Map();
213
222
  if (!repoRoot) return out;
@@ -219,6 +228,8 @@ export function readCoverageForRepo(repoRoot, knownFiles) {
219
228
  ["lcov.info", parseLcov],
220
229
  ["coverage/coverage.json", parseCoveragePyJson],
221
230
  ["coverage.json", parseCoveragePyJson],
231
+ ["tarpaulin-report.json", parseTarpaulinJson],
232
+ ["coverage/tarpaulin-report.json", parseTarpaulinJson],
222
233
  ["coverage.out", parseGoCoverage],
223
234
  ["cover.out", parseGoCoverage]
224
235
  ];
@@ -12,7 +12,7 @@ export function hasDynamicCode(source, file = '') {
12
12
  return DYNAMIC_CODE_RE.test(text) || (JS_LIKE_PATH_RE.test(String(file)) && JS_DYNAMIC_IMPORT_RE.test(text))
13
13
  }
14
14
 
15
- const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
15
+ const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
16
16
 
17
17
  export function deadCodePathAllowed(info, {includeTests, includeClassified}) {
18
18
  if (!includeTests && hasPathClass(info, 'test', 'e2e')) return {ok: false, bucket: 'tests'}
@@ -6,7 +6,7 @@ import {withGitRefCheckout} from './git-ref-graph.js'
6
6
  import {buildInternalGraph} from '../graph/internal-builder.js'
7
7
  import {filterGraphForMode} from '../graph/graph-filter.js'
8
8
 
9
- const NON_PRODUCT = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
9
+ const NON_PRODUCT = new Set(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
10
10
  const eligible = (fragment, settings) => {
11
11
  const classes = new Set(fragment.classes || [])
12
12
  if (fragment.n < settings.tokMin) return false
@@ -11,7 +11,15 @@ export function lineAt(text, index) {
11
11
  // Preserve offsets and literals while hiding comments from regex extractors.
12
12
  export function maskComments(text, { hashComments = false } = {}) {
13
13
  const chars = String(text || "").split("");
14
- let quote = "", escaped = false, lineComment = false, blockComment = false;
14
+ let quote = "", escaped = false, lineComment = false, blockComment = false, regex = false, regexClass = false;
15
+ const startsRegex = (index) => {
16
+ let cursor = index - 1;
17
+ while (cursor >= 0 && /\s/.test(chars[cursor])) cursor--;
18
+ if (cursor < 0) return true;
19
+ if (/[=(:,!&|?{}[\];]/.test(chars[cursor])) return true;
20
+ const prefix = chars.slice(Math.max(0, cursor - 12), cursor + 1).join("");
21
+ return /(?:^|\b)(?:return|throw|case|yield|await)\s*$/i.test(prefix) || /=>\s*$/.test(prefix);
22
+ };
15
23
  for (let i = 0; i < chars.length; i++) {
16
24
  const ch = chars[i], next = chars[i + 1];
17
25
  if (lineComment) {
@@ -24,6 +32,14 @@ export function maskComments(text, { hashComments = false } = {}) {
24
32
  else if (ch !== "\n" && ch !== "\r") chars[i] = " ";
25
33
  continue;
26
34
  }
35
+ if (regex) {
36
+ if (escaped) escaped = false;
37
+ else if (ch === "\\") escaped = true;
38
+ else if (ch === "[") regexClass = true;
39
+ else if (ch === "]") regexClass = false;
40
+ else if (ch === "/" && !regexClass) regex = false;
41
+ continue;
42
+ }
27
43
  if (quote) {
28
44
  if (escaped) escaped = false;
29
45
  else if (ch === "\\") escaped = true;
@@ -33,6 +49,7 @@ export function maskComments(text, { hashComments = false } = {}) {
33
49
  if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; }
34
50
  if (ch === "/" && next === "/") { chars[i] = chars[i + 1] = " "; i++; lineComment = true; continue; }
35
51
  if (ch === "/" && next === "*") { chars[i] = chars[i + 1] = " "; i++; blockComment = true; continue; }
52
+ if (!hashComments && ch === "/" && startsRegex(i)) { regex = true; regexClass = false; escaped = false; continue; }
36
53
  if (hashComments && ch === "#") { chars[i] = " "; lineComment = true; }
37
54
  }
38
55
  return chars.join("");
@@ -92,6 +92,19 @@ export function extractEndpointsFromText(text, file) {
92
92
  if (!HTTP_CLIENT_CALLER.test(match[1]) && handler && handler[0] !== "{") add(match[2], match[4], match[5], match.index);
93
93
  }
94
94
 
95
+ // Bare node:http servers commonly dispatch through request-method and URL-pathname guards rather
96
+ // than a router API. Keep the extraction bounded to files that actually construct an HTTP server;
97
+ // test assertions and arbitrary `object.method` comparisons are not enough to create endpoints.
98
+ if (!py && /\bcreateServer\s*\(|\b(?:node:)?https?\b/.test(scanText)) {
99
+ const guardedRoute = /\bif\s*\(([\s\S]{0,600}?)\)\s*(?:\{|return\b)/g;
100
+ while ((match = guardedRoute.exec(scanText))) {
101
+ const condition = match[1];
102
+ const methodMatch = /\b[A-Za-z_$][\w$]*\.method\s*={2,3}\s*(["'`])(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|CONNECT)\1/i.exec(condition);
103
+ const pathMatch = /\b[A-Za-z_$][\w$]*\.pathname\s*={2,3}\s*(["'`])(\/[^"'`]*)\1/i.exec(condition);
104
+ if (methodMatch && pathMatch) add(methodMatch[2], pathMatch[2], "", match.index);
105
+ }
106
+ }
107
+
95
108
  const go = /\.\s*(?:HandleFunc|Handle)\s*\(\s*(["'`])(\/[^"'`]*)\1\s*,\s*([\s\S]{0,120}?)\)/g;
96
109
  while ((match = go.exec(scanText))) add("ANY", match[2], match[3], match.index);
97
110
 
@@ -4,7 +4,7 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { normalizeRepoParts, readCoverageForRepo, pctFromCounts } from "./coverage-reports.js";
7
- import { bareSymbolName, countLocalRefsOutsideOwnRange, computeSymbolExternalRefs } from "./graph-analysis.refs.js";
7
+ import { bareSymbolName, computeLocalSymbolRefs, computeSymbolExternalRefs } from "./graph-analysis.refs.js";
8
8
  import { createRepoBoundary } from "../repo-path.js";
9
9
  import { edgeList, folderModuleOf } from "./graph-analysis.edges.js";
10
10
  export { folderModuleOf } from "./graph-analysis.edges.js";
@@ -223,6 +223,7 @@ export function aggregateGraph(graph, repoRoot) {
223
223
  .map((s) => ({ id: s.id, label: s.label, start: parseInt(s.line, 10), end: parseInt(s.endLine, 10), ref: s }))
224
224
  .filter((s) => Number.isFinite(s.start) && s.start > 0)
225
225
  .sort((a, b) => a.start - b.start);
226
+ const ranges = [];
226
227
  for (let i = 0; i < sorted.length; i++) {
227
228
  const start = sorted[i].start;
228
229
  const next = i + 1 < sorted.length ? sorted[i + 1].start : total > start ? total + 1 : start + 1;
@@ -234,9 +235,9 @@ export function aggregateGraph(graph, repoRoot) {
234
235
  sorted[i].ref.endLine = end;
235
236
  const cov = coverageForRange(fid, start, end);
236
237
  if (cov != null) symbolCoverage.set(sorted[i].id, cov);
237
- const refs = countLocalRefsOutsideOwnRange(txt, bareSymbolName(sorted[i].label), start, end);
238
- if (refs > 0) symbolLocalRefs.set(sorted[i].id, refs);
238
+ ranges.push({id: sorted[i].id, name: bareSymbolName(sorted[i].label), startLine: start, endLine: end});
239
239
  }
240
+ for (const [id, refs] of computeLocalSymbolRefs(txt, ranges)) symbolLocalRefs.set(id, refs);
240
241
  }
241
242
  for (const s of symbols) {
242
243
  const fid = id2file.get(s.id);
@@ -41,6 +41,54 @@ export function countLocalRefsOutsideOwnRange(text, name, startLine, endLine) {
41
41
  return refs;
42
42
  }
43
43
 
44
+ const lowerBound = (values, target) => {
45
+ let low = 0, high = values.length;
46
+ while (low < high) {
47
+ const middle = (low + high) >>> 1;
48
+ if (values[middle] < target) low = middle + 1;
49
+ else high = middle;
50
+ }
51
+ return low;
52
+ };
53
+
54
+ // Count every wanted identifier in a file once, then subtract occurrences inside each declaration
55
+ // with binary searches over occurrence line numbers. The former per-symbol full-file scan became
56
+ // quadratic on large vendored files (thousands of symbols over tens of thousands of lines).
57
+ export function computeLocalSymbolRefs(text, symbols) {
58
+ const candidates = (symbols || []).map((symbol) => ({
59
+ id: symbol.id,
60
+ name: bareSymbolName(symbol.name ?? symbol.label),
61
+ start: Number(symbol.startLine ?? symbol.start),
62
+ end: Number(symbol.endLine ?? symbol.end),
63
+ })).filter((symbol) => symbol.id && isIdentifierName(symbol.name));
64
+ if (!text || !candidates.length) return new Map();
65
+ const wanted = new Set(candidates.map((symbol) => symbol.name));
66
+ const occurrenceLines = new Map();
67
+ const identifier = /[A-Za-z_$][\w$]*/g;
68
+ const lines = String(text).split(/\r?\n/);
69
+ for (let index = 0; index < lines.length; index++) {
70
+ identifier.lastIndex = 0;
71
+ let match;
72
+ while ((match = identifier.exec(lines[index]))) {
73
+ const name = match[0];
74
+ if (!wanted.has(name)) continue;
75
+ const positions = occurrenceLines.get(name) || [];
76
+ positions.push(index + 1);
77
+ occurrenceLines.set(name, positions);
78
+ }
79
+ }
80
+ const refs = new Map();
81
+ for (const symbol of candidates) {
82
+ const positions = occurrenceLines.get(symbol.name) || [];
83
+ const start = Number.isFinite(symbol.start) && symbol.start > 0 ? symbol.start : 0;
84
+ const end = Number.isFinite(symbol.end) && symbol.end >= start ? symbol.end : start;
85
+ const inside = start ? lowerBound(positions, end + 1) - lowerBound(positions, start) : 0;
86
+ const outside = Math.max(0, positions.length - inside);
87
+ if (outside > 0) refs.set(symbol.id, outside);
88
+ }
89
+ return refs;
90
+ }
91
+
44
92
  function importCandidates(fromFile, spec) {
45
93
  const raw = String(spec || "");
46
94
  if (!raw.startsWith(".")) return [];
@@ -7,7 +7,7 @@ import {createPathClassifier, hasPathClass} from '../path-classification.js'
7
7
  import {isStructuralRelation} from '../graph/relations.js'
8
8
  import {boundedInteger} from '../util.js'
9
9
 
10
- const NON_PRODUCT = ['generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
10
+ const NON_PRODUCT = ['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
11
11
  const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value || '')
12
12
  const normalize = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
13
13
  const boundedInt = boundedInteger
@@ -53,6 +53,30 @@ export function springConventionEntries(sources, fileSet = null) {
53
53
  return out.sort((a, b) => a.file.localeCompare(b.file));
54
54
  }
55
55
 
56
+ // Electron loads preload scripts from BrowserWindow.webPreferences rather than through a normal
57
+ // import edge. Require both a conventional preload filename and a bounded `preload:` option that
58
+ // names that exact file, so an unrelated helper called preload.js is not silently blessed.
59
+ export function electronPreloadEntries(sources, fileSet = null) {
60
+ const files = fileSet || new Set((sources || new Map()).keys());
61
+ const out = [];
62
+ for (const rawFile of files) {
63
+ const file = String(rawFile || "").replace(/\\/g, "/");
64
+ if (!/(^|\/)preload\.[cm]?[jt]s$/i.test(file)) continue;
65
+ const basename = posix.basename(file).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
+ const marker = new RegExp(`\\bpreload\\s*:\\s*[\\s\\S]{0,240}?["'\\x60]${basename}["'\\x60]`, "i");
67
+ const owner = [...(sources || [])].find(([other, text]) => String(other).replace(/\\/g, "/") !== file && marker.test(String(text || "")));
68
+ if (!owner) continue;
69
+ out.push({
70
+ file,
71
+ framework: "electron",
72
+ marker: `BrowserWindow preload option in ${String(owner[0]).replace(/\\/g, "/")}`,
73
+ confidence: "high",
74
+ reason: "Electron loads this exact preload script outside the static import graph",
75
+ });
76
+ }
77
+ return out.sort((a, b) => a.file.localeCompare(b.file));
78
+ }
79
+
56
80
  // Entry set for reachability: conventional entry names + package.json main/module/browser/bin/exports +
57
81
  // html pages (they root classic-script apps) + test files (the runner enters them) + root config files +
58
82
  // dynamic-import targets. Anything reachable from here is "used"; the rest corroborates unused-file.
@@ -136,6 +160,10 @@ export function entryFiles(graph, pkgOrScopes, dynamicTargets = new Set(), { dec
136
160
  entries.add(evidence.file);
137
161
  conventionEvidence.push(evidence);
138
162
  }
163
+ for (const evidence of electronPreloadEntries(sources, fileSet)) {
164
+ entries.add(evidence.file);
165
+ conventionEvidence.push(evidence);
166
+ }
139
167
  for (const raw of Array.isArray(declaredEntries) ? declaredEntries : [declaredEntries]) {
140
168
  const resolved = resolveEntry("", raw);
141
169
  if (resolved && fileSet.has(resolved)) entries.add(resolved);
@@ -61,7 +61,7 @@ export async function runInternalAudit(repoPath, {
61
61
  };
62
62
  const isNonProductPath = (file) => {
63
63
  const info = classifyPath(file);
64
- return info.excluded || hasPathClass(info, "test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp");
64
+ return info.excluded || hasPathClass(info, "test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp");
65
65
  };
66
66
 
67
67
  const conventionEvidence = [];
@@ -47,7 +47,7 @@ export function computeStaticTestReachability(graph, {
47
47
  const isTest = (file) => hasPathClass(classify(file), "test", "e2e");
48
48
  const isProduct = (file) => {
49
49
  const info = classify(file);
50
- return !info.excluded && !hasPathClass(info, "test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp");
50
+ return !info.excluded && !hasPathClass(info, "test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp");
51
51
  };
52
52
 
53
53
  const idToFile = new Map();
@@ -30,7 +30,7 @@ const CODE_EXTENSIONS = new Set([
30
30
  '.cjs', '.cs', '.css', '.go', '.htm', '.html', '.java', '.js', '.jsx', '.less', '.mjs', '.py', '.pyi',
31
31
  '.rs', '.scss', '.sol', '.sql', '.ts', '.tsx',
32
32
  ])
33
- const CLASSIFIED = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
33
+ const CLASSIFIED = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
34
34
  const EDGE_RELATIONS = new Set(['imports', 're_exports', 'calls', 'references'])
35
35
 
36
36
  const slash = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
@@ -111,6 +111,10 @@ function buildTools({tg, ti, th, ts, tb, te, ta, tar, thi, tc, tv, caps}) {
111
111
  max_matches: {type: 'integer', minimum: 1, maximum: 5000, default: 1000},
112
112
  max_affected_files: {type: 'integer', minimum: 1, maximum: 500, default: 100},
113
113
  top_n: {type: 'integer', minimum: 1, maximum: 50, default: 10},
114
+ response_detail: {type: 'string', enum: ['compact', 'full'], default: 'compact', description: 'Compact returns bounded samples and counts. Full is explicit opt-in and still paginated.'},
115
+ page_size: {type: 'integer', minimum: 1, maximum: 50, default: 10, description: 'Maximum HTTP/transport/uncertain evidence items returned on this page.'},
116
+ per_item_limit: {type: 'integer', minimum: 1, maximum: 25, default: 5, description: 'Compact-mode sample limit for callsites, affected files, screens and modules per item.'},
117
+ cursor: {type: 'string', maxLength: 256, description: 'Opaque nextCursor from the previous page; bound to repository revisions and filters.'},
114
118
  },
115
119
  required: ['backend', 'clients'],
116
120
  },
@@ -120,12 +124,12 @@ function buildTools({tg, ti, th, ts, tb, te, ta, tar, thi, tc, tv, caps}) {
120
124
  {cap: 'search', name: 'search_code', description: 'Full-text or regex search across the repo source (ripgrep-backed, Node fallback). The graph only stores structure — use this to find literal text/patterns, then get_node/get_neighbors for structure.', inputSchema: {type: 'object', properties: {query: {type: 'string', description: 'text or regex to search for'}, is_regex: {type: 'boolean', default: false}, glob: {type: 'string', description: 'optional path glob, e.g. "*.js" or "src/**"'}, max_results: {type: 'integer', default: 40}}, required: ['query']}, run: (g, a, ctx) => searchCode({repoRoot: ctx.repoRoot, resolveRg}, a)},
121
125
  {cap: 'source', name: 'read_source', description: "Read the actual source of a node (by label/ID) or a repo-relative file path — the symbol's lines with context. The graph stores only locations, not source text. For a path read, pass start_line to anchor the window anywhere in the file (otherwise it shows the head).", inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'node label or ID'}, path: {type: 'string', description: 'or a repo-relative file path'}, start_line: {type: 'integer', description: 'anchor line: window = start_line-before .. start_line+after'}, before: {type: 'integer', default: 3}, after: {type: 'integer', default: 40}}}, run: (g, a, ctx) => readSource({repoRoot: ctx.repoRoot, resolveNode, isSymbol}, g, a)},
122
126
  {cap: 'source', refreshGraph: true, name: 'inspect_symbol', description: 'Inspect one exact symbol with an on-demand TypeScript/JavaScript LSP reference query, grouped occurrence containers, graph blast radius, complexity facts and bounded local source context. Ambiguous labels fail closed; point queries never replace the broad precision overlay.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_containers: {type: 'integer', minimum: 1, maximum: 50, default: 15}, context_lines: {type: 'integer', minimum: 0, maximum: 40, default: 8}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => ts.tInspectSymbol(g, a, ctx)},
123
- {cap: 'source', refreshGraph: true, name: 'context_bundle', description: 'Return one compact, bounded source bundle for an exact symbol: definition, production-first inbound/outbound containers, exact re-export sites, on-demand TS/JS reference evidence and diverse excerpts around call sites. Use before an edit when query_graph would be too broad.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_related: {type: 'integer', minimum: 1, maximum: 30, default: 10}, max_reexports: {type: 'integer', minimum: 1, maximum: 100, default: 20}, max_source_files: {type: 'integer', minimum: 1, maximum: 8, default: 4}, context_lines: {type: 'integer', minimum: 0, maximum: 12, default: 4}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp callers after production callers'}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => tb.tContextBundle(g, a, ctx, ts.tInspectSymbol)},
124
- {cap: 'health', name: 'find_duplicates', description: "Content-based clone detection over production code (MOSS winnowing over method bodies). Supports high-confidence small clones down to 12 tokens when min_tokens is lowered. Tests, classified non-product paths, all-router framework boilerplate and immutable declarative catalogs are excluded by default; opt them in explicitly.", inputSchema: {type: 'object', properties: {min_similarity: {type: 'integer', description: '50-100, default 80 (ignored in semantic mode)'}, min_tokens: {type: 'integer', minimum: 12, maximum: 400, description: 'min fragment size, 12-400; default 50'}, mode: {type: 'string', enum: ['renamed', 'strict', 'semantic'], default: 'renamed'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, include_boilerplate: {type: 'boolean', default: false, description: 'Include clone groups made entirely of conventional *.router.js/ts router symbols'}, include_declarative: {type: 'boolean', default: false, description: 'Include repeated immutable array/object catalogs that contain no executable control flow'}, include_strings: {type: 'boolean', description: 'Also clone-check large multi-line string literals', default: false}, top_n: {type: 'integer', default: 15}}}, run: (g, a, ctx) => th.tFindDuplicates(g, a, ctx)},
125
- {cap: 'health', name: 'find_dead_code', description: 'Conservative review queue for statically unreferenced files, functions, methods and symbols. Returns confidence, reason, bounded evidence and explicit framework/dynamic/reflection/public-API caveats; never an auto-delete verdict. Tests, generated code, mocks, stories, docs, benchmarks and temporary roots are excluded by default.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repo-relative path prefix'}, kinds: {type: 'array', items: {type: 'string', enum: ['file', 'function', 'method', 'symbol']}, maxItems: 4, uniqueItems: true, description: 'Optional candidate kinds; defaults to all'}, min_confidence: {type: 'string', enum: ['high', 'medium', 'low'], default: 'medium', description: 'Minimum confidence to include. low explicitly includes public/framework/dynamic review candidates'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 30}}}, run: (g, a, ctx) => th.tFindDeadCode(g, a, ctx)},
126
- {cap: 'health', name: 'run_audit', description: 'Core production-first repository Health review with an explicit capability/completeness matrix for structure, dependencies, bounded runtime-correctness/concurrency patterns, advisories, malware and coverage. Unsupported Maven/Gradle import verification is NOT_SUPPORTED/PARTIAL, never a clean zero. Findings whose evidence is entirely test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded are suppressed by default; opt them in with include_classified. category=dependencies is a dedicated dependency-health projection across missing, unused and duplicate declarations while preserving each finding\'s native category. With base_ref, builds and audits an immutable Git checkout and compares stable deterministic finding IDs; debt defaults to genuinely new findings. Supply-chain checks remain explicitly uncomparable across the source-only baseline. changed_files without base_ref is only changed-scope, never a new-debt claim.', inputSchema: {type: 'object', properties: {category: {type: 'string', enum: ['dependencies', 'unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category; dependencies selects dependency manifest/import findings across native categories'}, min_severity: {type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity to include'}, max_findings: {type: 'integer', description: 'Max findings to list, default 30'}, include_classified: {type: 'boolean', default: false, description: 'Include findings whose evidence is entirely tests/e2e/generated/mocks/stories/docs/benchmarks/temp or explicitly excluded'}, include_malware_scan: {type: 'boolean', description: 'Also grep installed packages for malware heuristics (slow)', default: false}, base_ref: {type: 'string', maxLength: 200, description: 'Optional immutable Git baseline (for example HEAD~1 or origin/main). Enables honest new/existing/fixed debt comparison'}, changed_files: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 500, description: 'Optional explicit repo-relative scope. Without base_ref this is changed-scope only; when omitted with base_ref, files are derived from the Git diff'}, debt: {type: 'string', enum: ['new', 'existing', 'all'], default: 'new', description: 'Baseline comparison view. Defaults to genuinely new deterministic findings when base_ref is present'}}}, run: (g, a, ctx) => th.tRunAudit(g, a, ctx)},
127
+ {cap: 'source', refreshGraph: true, name: 'context_bundle', description: 'Return one compact, bounded source bundle for an exact symbol: definition, production-first inbound/outbound containers, exact re-export sites, on-demand TS/JS reference evidence and diverse excerpts around call sites. Use before an edit when query_graph would be too broad.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_related: {type: 'integer', minimum: 1, maximum: 30, default: 10}, max_reexports: {type: 'integer', minimum: 1, maximum: 100, default: 20}, max_source_files: {type: 'integer', minimum: 1, maximum: 8, default: 4}, context_lines: {type: 'integer', minimum: 0, maximum: 12, default: 4}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/vendored/mock/story/docs/benchmark/temp callers after production callers'}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => tb.tContextBundle(g, a, ctx, ts.tInspectSymbol)},
128
+ {cap: 'health', name: 'find_duplicates', description: "Content-based clone detection over production code (MOSS winnowing over method bodies). Supports high-confidence small clones down to 12 tokens when min_tokens is lowered. Tests, classified non-product paths, all-router framework boilerplate and immutable declarative catalogs are excluded by default; opt them in explicitly.", inputSchema: {type: 'object', properties: {min_similarity: {type: 'integer', description: '50-100, default 80 (ignored in semantic mode)'}, min_tokens: {type: 'integer', minimum: 12, maximum: 400, description: 'min fragment size, 12-400; default 50'}, mode: {type: 'string', enum: ['renamed', 'strict', 'semantic'], default: 'renamed'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, include_boilerplate: {type: 'boolean', default: false, description: 'Include clone groups made entirely of conventional *.router.js/ts router symbols'}, include_declarative: {type: 'boolean', default: false, description: 'Include repeated immutable array/object catalogs that contain no executable control flow'}, include_strings: {type: 'boolean', description: 'Also clone-check large multi-line string literals', default: false}, top_n: {type: 'integer', default: 15}}}, run: (g, a, ctx) => th.tFindDuplicates(g, a, ctx)},
129
+ {cap: 'health', name: 'find_dead_code', description: 'Conservative review queue for statically unreferenced files, functions, methods and symbols. Returns confidence, reason, bounded evidence and explicit framework/dynamic/reflection/public-API caveats; never an auto-delete verdict. Tests, generated/vendored code, mocks, stories, docs, benchmarks and temporary roots are excluded by default.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repo-relative path prefix'}, kinds: {type: 'array', items: {type: 'string', enum: ['file', 'function', 'method', 'symbol']}, maxItems: 4, uniqueItems: true, description: 'Optional candidate kinds; defaults to all'}, min_confidence: {type: 'string', enum: ['high', 'medium', 'low'], default: 'medium', description: 'Minimum confidence to include. low explicitly includes public/framework/dynamic review candidates'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 30}}}, run: (g, a, ctx) => th.tFindDeadCode(g, a, ctx)},
130
+ {cap: 'health', name: 'run_audit', description: 'Core production-first repository Health review with an explicit capability/completeness matrix for structure, dependencies, bounded runtime-correctness/concurrency patterns, advisories, malware and coverage. Unsupported Maven/Gradle import verification is NOT_SUPPORTED/PARTIAL, never a clean zero. Findings whose evidence is entirely test/e2e/generated/vendored/mock/story/docs/benchmark/temp or explicitly excluded are suppressed by default; opt them in with include_classified. category=dependencies is a dedicated dependency-health projection across missing, unused and duplicate declarations while preserving each finding\'s native category. With base_ref, builds and audits an immutable Git checkout and compares stable deterministic finding IDs; debt defaults to genuinely new findings. Supply-chain checks remain explicitly uncomparable across the source-only baseline. changed_files without base_ref is only changed-scope, never a new-debt claim.', inputSchema: {type: 'object', properties: {category: {type: 'string', enum: ['dependencies', 'unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category; dependencies selects dependency manifest/import findings across native categories'}, min_severity: {type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity to include'}, max_findings: {type: 'integer', description: 'Max findings to list, default 30'}, include_classified: {type: 'boolean', default: false, description: 'Include findings whose evidence is entirely tests/e2e/generated/vendored/mocks/stories/docs/benchmarks/temp or explicitly excluded'}, include_malware_scan: {type: 'boolean', description: 'Also grep installed packages for malware heuristics (slow)', default: false}, base_ref: {type: 'string', maxLength: 200, description: 'Optional immutable Git baseline (for example HEAD~1 or origin/main). Enables honest new/existing/fixed debt comparison'}, changed_files: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 500, description: 'Optional explicit repo-relative scope. Without base_ref this is changed-scope only; when omitted with base_ref, files are derived from the Git diff'}, debt: {type: 'string', enum: ['new', 'existing', 'all'], default: 'new', description: 'Baseline comparison view. Defaults to genuinely new deterministic findings when base_ref is present'}}}, run: (g, a, ctx) => th.tRunAudit(g, a, ctx)},
127
131
  {cap: 'health', name: 'coverage_map', description: 'Map a real existing coverage report onto the graph. If no report exists, return clearly labelled static test reachability (a test imports/reaches a source file) with actualCoverage=NOT_AVAILABLE; reachability is never presented as measured coverage.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max risk hotspots to list, default 15'}, path: {type: 'string', description: 'Optional repo-relative path prefix filter, e.g. src/query'}}}, run: (g, a, ctx) => th.tCoverageMap(g, a, ctx)},
128
- {cap: 'health', name: 'hot_path_review', description: 'Rank a focused production-symbol hot-path queue from parser-derived local complexity, inside-loop allocations/copies/scans/sorts/recursion, graph fan-in/fan-out, and measured coverage or clearly labelled static test reachability. The default score gate is 85 with a narrow strong-local fallback; set min_score=0 for the full diagnostic queue. This is not profiler data or interprocedural Big-O.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repository-relative path prefix'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 20}, min_score: {type: 'integer', minimum: 0, maximum: 100, default: 85, description: 'Focused default is 85; lower explicitly to broaden, or use 0 for every threshold-matching diagnostic candidate'}, cyclomatic_threshold: {type: 'integer', minimum: 2, maximum: 1000, default: 8}, call_threshold: {type: 'integer', minimum: 1, maximum: 10000, default: 12}, loop_depth_threshold: {type: 'integer', minimum: 1, maximum: 10, default: 2}, time_rank_threshold: {type: 'integer', minimum: 0, maximum: 5, default: 2}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and explicitly excluded paths; tests still require include_tests'}}}, run: (g, a, ctx) => th.tHotPathReview(g, a, ctx)},
132
+ {cap: 'health', name: 'hot_path_review', description: 'Rank a focused production-symbol hot-path queue from parser-derived local complexity, inside-loop allocations/copies/scans/sorts/recursion, graph fan-in/fan-out, and measured coverage or clearly labelled static test reachability. The default score gate is 85 with a narrow strong-local fallback; set min_score=0 for the full diagnostic queue. This is not profiler data or interprocedural Big-O.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repository-relative path prefix'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 20}, min_score: {type: 'integer', minimum: 0, maximum: 100, default: 85, description: 'Focused default is 85; lower explicitly to broaden, or use 0 for every threshold-matching diagnostic candidate'}, cyclomatic_threshold: {type: 'integer', minimum: 2, maximum: 1000, default: 8}, call_threshold: {type: 'integer', minimum: 1, maximum: 10000, default: 12}, loop_depth_threshold: {type: 'integer', minimum: 1, maximum: 10, default: 2}, time_rank_threshold: {type: 'integer', minimum: 0, maximum: 5, default: 2}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and explicitly excluded paths; tests still require include_tests'}}}, run: (g, a, ctx) => th.tHotPathReview(g, a, ctx)},
129
133
  {cap: 'graph', name: 'list_communities', description: 'List graph communities named by their dominant folder (largest first) with sample files — a readable module overview; feed the list position into get_community.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max communities to list, default 20'}}}, run: (g, a, ctx) => th.tListCommunities(g, a, ctx)},
130
134
  {cap: 'graph', name: 'module_map', description: 'First orientation view for understanding an unfamiliar application with little context: a production-first folder architecture map with file/symbol counts and strongest module dependencies, separating runtime, TypeScript type-only and language compile-only coupling.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max modules to list, default 25'}, include_non_product: {type: 'boolean', default: false, description: 'Include tests, fixtures, benchmarks, generated output, docs and other classified non-product files; false by default'}}}, run: (g, a, ctx) => th.tModuleMap(g, a, ctx)},
131
135
  {cap: 'source', refreshGraph: true, name: 'list_endpoints', description: 'Inventory of HTTP endpoints defined in the repo (Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web/Spring MVC and WebFlux): declared and reachable composed paths, static mount provenance, confidence, handler, file:line, and Spring conditional/default-active state.', inputSchema: {type: 'object', properties: {method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, path: {type: 'string', maxLength: 2048, description: 'Optional exact composed path or segment-aligned suffix'}, max_results: {type: 'integer', description: 'Max endpoints to list, default 100'}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp and explicitly excluded targets'}}}, run: (g, a, ctx) => th.tListEndpoints(g, a, ctx)},