weavatrix 0.2.6 → 0.2.8

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.
@@ -109,8 +109,9 @@ export function computeHotPathReview(graph, options = {}) {
109
109
  calls: boundedInt(options.callThreshold, 12, 1, 10000),
110
110
  loopDepth: boundedInt(options.loopDepthThreshold, 2, 1, 10),
111
111
  timeRank: boundedInt(options.timeRankThreshold, 2, 0, 5),
112
- minScore: boundedInt(options.minScore, 0, 0, 100),
112
+ minScore: boundedInt(options.minScore, 85, 0, 100),
113
113
  }
114
+ const defaultFocus = options.minScore == null
114
115
  const topN = boundedInt(options.topN, 20, 1, 100)
115
116
  const classifier = createPathClassifier(options.repoRoot || null)
116
117
  const knownFiles = [...new Set((graph?.nodes || []).map((node) => normalize(node?.source_file)).filter(Boolean))]
@@ -152,8 +153,19 @@ export function computeHotPathReview(graph, options = {}) {
152
153
  const nearest = staticByFile.get(file)?.nearestTests?.[0] || null
153
154
  const coverageRisk = actualCoverage == null ? 0 : (1 - actualCoverage) * 100
154
155
  const score = round(clamp(syntaxScore * 0.72 + graphScore * 0.2 + coverageRisk * 0.08))
155
- if (score < thresholds.minScore) continue
156
156
  const directHotEvidence = Array.isArray(complexity.hotEvidence) ? complexity.hotEvidence.slice(0, 12) : []
157
+ // The default queue is intentionally narrow. A small, locally expensive function can still be
158
+ // important even with little graph fan-in, so retain only a bounded strong-local fallback. An
159
+ // explicit minScore disables this fallback and gives the caller a strict numeric gate.
160
+ const bodyLines = startLine > 0 && endLine >= startLine ? endLine - startLine + 1 : Number.POSITIVE_INFINITY
161
+ const strongLocalEvidence = defaultFocus && directHotEvidence.length > 0 && bodyLines <= 80 && (
162
+ Number(complexity.recursionInLoops || 0) > 0
163
+ || (bodyLines <= 40 && (
164
+ Number(complexity.sortsInLoops || 0) > 0
165
+ || (Number(complexity.timeRank || 0) >= 4 && Number(complexity.maxLoopDepth || 0) >= 2)
166
+ ))
167
+ )
168
+ if (score < thresholds.minScore && !strongLocalEvidence) continue
157
169
  const confidence = directHotEvidence.length ? 'HIGH' : complexity.recursion ? 'LOW' : 'MEDIUM'
158
170
  candidates.push({
159
171
  id: String(node.id),
@@ -164,6 +176,7 @@ export function computeHotPathReview(graph, options = {}) {
164
176
  endLine,
165
177
  classification: entry.classification,
166
178
  score,
179
+ selection: score >= thresholds.minScore ? 'SCORE_THRESHOLD' : 'STRONG_LOCAL_EVIDENCE',
167
180
  confidence,
168
181
  localSyntax: {
169
182
  score: syntaxScore,
@@ -207,6 +220,11 @@ export function computeHotPathReview(graph, options = {}) {
207
220
  includeClassified: options.includeClassified === true,
208
221
  },
209
222
  thresholds,
223
+ selectionPolicy: {
224
+ mode: defaultFocus ? 'FOCUSED_DEFAULT' : 'EXPLICIT_SCORE_THRESHOLD',
225
+ strongLocalFallback: defaultFocus,
226
+ broadenWith: 'Set min_score lower (0 restores the full diagnostic candidate set).',
227
+ },
210
228
  analyzedSymbols: eligible.length,
211
229
  candidateSymbols: candidates.length,
212
230
  coverage: measuredCoverage.size
@@ -3,7 +3,7 @@
3
3
  import { readFileSync, readdirSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
- import { parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps } from "./manifests.js";
6
+ import { parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503 } from "./manifests.js";
7
7
  import { createRepoBoundary } from "../repo-path.js";
8
8
  import { childProcessEnv } from "../child-env.js";
9
9
  import { filterWeavatrixIgnored } from "../path-ignore.js";
@@ -254,28 +254,61 @@ export function workspacePkgNames(repoRoot, pkg) {
254
254
 
255
255
  export const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i;
256
256
 
257
- // Python declared deps: root requirements*.txt/.in + requirements/ dir + pyproject.toml + Pipfile.
257
+ // Python declared deps are owned by the nearest manifest root, matching nested service/monorepo
258
+ // layouts. `requirements/*.txt` belongs to the directory above `requirements`; a colocated
259
+ // requirements.txt, pyproject.toml or Pipfile owns its own directory.
258
260
  // present=false (no manifest at all) softens missing-dep findings instead of suppressing them.
259
261
  export function collectPyManifest(repoRoot) {
260
- const deps = [];
261
- let present = false;
262
- let names = [];
263
262
  const boundary = createRepoBoundary(repoRoot);
264
- try { names = readdirSync(boundary.root).filter((n) => /^requirements[\w.-]*\.(txt|in)$/i.test(n)); } catch { /* unreadable root */ }
265
- try {
266
- const requirements = boundary.resolve("requirements");
267
- if (requirements.ok) names.push(...readdirSync(requirements.path).filter((n) => /\.(txt|in)$/i.test(n)).map((n) => `requirements/${n}`));
268
- } catch { /* no requirements dir */ }
269
- for (const n of names) {
270
- const t = readRepoText(boundary, n);
263
+ const scopes = new Map();
264
+ const scopeFor = (root) => {
265
+ const normalized = normRoot(root);
266
+ if (!scopes.has(normalized)) scopes.set(normalized, { root: normalized, present: false, deps: [], manifests: [] });
267
+ return scopes.get(normalized);
268
+ };
269
+ const addManifest = (root, manifest, parsedDeps, present = true) => {
270
+ if (!present) return;
271
+ const scope = scopeFor(root);
272
+ scope.present = true;
273
+ scope.manifests.push(manifest);
274
+ scope.deps.push(...parsedDeps.map((dep) => ({ ...dep, manifest })));
275
+ };
276
+ const files = listRepoFiles(repoRoot);
277
+ for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
278
+ || /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
279
+ const t = readRepoText(boundary, file);
271
280
  if (t == null) continue;
272
- present = true;
273
- const dev = /dev|test|lint|doc|ci/i.test(n.replace(/^requirements[/\\]?/i, ""));
274
- for (const d of parseRequirementsNames(t)) deps.push({ ...d, dev });
281
+ const parent = normRoot(dirname(file));
282
+ const root = /(^|\/)requirements$/i.test(parent) ? normRoot(dirname(parent)) : parent;
283
+ const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf("/") + 1));
284
+ addManifest(root, file, parseRequirementsNames(t).map((dep) => ({ ...dep, dev })));
275
285
  }
276
- const pp = readRepoText(boundary, "pyproject.toml");
277
- if (pp != null) { const r = parsePyprojectDeps(pp); if (r.present) { present = true; deps.push(...r.deps); } }
278
- const pf = readRepoText(boundary, "Pipfile");
279
- if (pf != null) { const r = parsePipfileDeps(pf); if (r.present) { present = true; deps.push(...r.deps); } }
280
- return { present, deps };
286
+ for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
287
+ const parsed = parsePyprojectDeps(readRepoText(boundary, file));
288
+ addManifest(dirname(file), file, parsed.deps, parsed.present);
289
+ }
290
+ for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
291
+ const parsed = parsePipfileDeps(readRepoText(boundary, file));
292
+ addManifest(dirname(file), file, parsed.deps, parsed.present);
293
+ }
294
+ const normalizedScopes = [...scopes.values()]
295
+ .map((scope) => {
296
+ const seen = new Set();
297
+ return {
298
+ ...scope,
299
+ manifests: [...new Set(scope.manifests)].sort(),
300
+ deps: scope.deps.filter((dep) => {
301
+ const key = pep503(dep.name);
302
+ if (!key || seen.has(key)) return false;
303
+ seen.add(key);
304
+ return true;
305
+ }),
306
+ };
307
+ })
308
+ .sort((left, right) => right.root.length - left.root.length || left.root.localeCompare(right.root));
309
+ return {
310
+ present: normalizedScopes.some((scope) => scope.present),
311
+ deps: normalizedScopes.flatMap((scope) => scope.deps),
312
+ scopes: normalizedScopes,
313
+ };
281
314
  }
@@ -300,15 +300,20 @@ export async function runInternalAudit(repoPath, { graph, advisoryStorePath, ski
300
300
  findings: sorted,
301
301
  dependencyReport: {
302
302
  status: dependencyStatus,
303
+ evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE",
304
+ perFindingVerification: true,
305
+ verificationCoverage: { npm: "COMPLETE_FOR_GRAPH_SCOPE", go: "SUMMARY_ONLY", python: "SUMMARY_ONLY" },
303
306
  declared: dep.declared.size + goDep.declared.size + pyDep.declared.size,
304
307
  importedPackages: importedPackages.size,
305
308
  importRecords: externalImports.length,
306
309
  unused: dependencyFindings.filter((finding) => finding.rule === "unused-dep").length,
307
310
  missing: dependencyFindings.filter((finding) => finding.rule === "missing-dep").length,
308
311
  duplicateDeclarations: dependencyFindings.filter((finding) => finding.rule === "duplicate-dep").length,
312
+ unusedRequiringReview: dependencyFindings.filter((finding) => finding.rule === "unused-dep" && finding.verification?.decision === "REVIEW_REQUIRED").length,
313
+ missingWithSourceEvidence: dependencyFindings.filter((finding) => finding.rule === "missing-dep" && finding.verification?.indexedSourceImports?.status === "FOUND").length,
309
314
  packageScopes: packageScopes.length,
310
315
  reason: dependencyStatus === "COMPLETE"
311
- ? "All discovered dependency manifests were compared with the complete indexed import set."
316
+ ? "All discovered dependency manifests were compared with the complete indexed import set; every npm unused/missing/duplicate finding carries manifest and source/config verification state."
312
317
  : "The dependency result is scoped to a partial graph and is not a repository-wide clean bill.",
313
318
  },
314
319
  deadReport: {
@@ -0,0 +1,116 @@
1
+ import {createPathClassifier, hasPathClass} from '../path-classification.js'
2
+
3
+ const words = (value) => new Set(String(value || '').toLowerCase().match(/[\p{L}_$][\p{L}\p{N}_$-]{2,}/gu) || [])
4
+ const fileOf = (node) => String(node?.source_file || (String(node?.id || '').includes('#') ? String(node.id).split('#')[0] : node?.id || '')).replace(/\\/g, '/')
5
+ const isSymbol = (id) => String(id || '').includes('#')
6
+ const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
7
+ const CLASS_TERMS = Object.freeze({
8
+ test: ['test', 'tests', 'testing', 'spec', 'coverage', 'verify'],
9
+ e2e: ['e2e', 'playwright', 'cypress'],
10
+ generated: ['generated', 'autogenerated', 'dist'],
11
+ mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
12
+ story: ['story', 'stories', 'storybook'],
13
+ docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
14
+ benchmark: ['benchmark', 'benchmarks', 'bench'],
15
+ temp: ['temp', 'temporary', 'tmp'],
16
+ })
17
+
18
+ const INTENT_TRANSLATIONS = [
19
+ [/авториз|аутентиф|логин|сесси|токен/iu, 'auth authentication login session token'],
20
+ [/маршрут|роут|эндпоинт|апи|http/iu, 'route router endpoint api http'],
21
+ [/тест|покрыти/iu, 'test spec coverage verify'],
22
+ [/к[эе]ш|хранилищ/iu, 'cache store storage'],
23
+ [/баз[аы]|запрос|sql/iu, 'database query sql'],
24
+ [/конфиг|настройк/iu, 'config settings configuration'],
25
+ [/безопас|вредонос|секрет/iu, 'security malware secret'],
26
+ [/зависим|импорт/iu, 'dependency import module'],
27
+ [/дублик|клон/iu, 'duplicate clone'],
28
+ ]
29
+
30
+ export function expandTaskQuery(task) {
31
+ const text = String(task || '')
32
+ const expansions = INTENT_TRANSLATIONS.filter(([pattern]) => pattern.test(text)).map(([, value]) => value)
33
+ return [...new Set([text, ...expansions])].join(' ')
34
+ }
35
+
36
+ function overlapScore(taskWords, node) {
37
+ const haystack = words(`${node?.label || ''} ${node?.id || ''} ${node?.symbol_kind || ''}`)
38
+ let score = 0
39
+ for (const word of taskWords) if (haystack.has(word)) score += 8
40
+ return score
41
+ }
42
+
43
+ function addCandidate(map, node, score, reason) {
44
+ if (!node?.id) return
45
+ const id = String(node.id)
46
+ const current = map.get(id) || {node, score: 0, reasons: new Set()}
47
+ current.score += score
48
+ current.reasons.add(reason)
49
+ map.set(id, current)
50
+ }
51
+
52
+ function containedSymbols(g, node) {
53
+ if (isSymbol(node?.id)) return [node]
54
+ return (g.out.get(String(node?.id)) || [])
55
+ .filter((edge) => edge.relation === 'contains')
56
+ .map((edge) => g.byId.get(String(edge.id)))
57
+ .filter(Boolean)
58
+ }
59
+
60
+ // Combines intent-expanded search seeds with exact changed symbols. The result deliberately stays
61
+ // deterministic and source-free; exact LSP/source evidence is collected by context_bundle later.
62
+ export function retrieveTaskContext(g, {
63
+ task, semanticSeeds = [], changedSeedIds = [], maxSymbols = 3, repoRoot = null, includeClassified = false,
64
+ } = {}) {
65
+ const expandedTask = expandTaskQuery(task)
66
+ const taskWords = words(expandedTask)
67
+ const requestedClasses = new Set(Object.entries(CLASS_TERMS)
68
+ .filter(([, terms]) => terms.some((term) => taskWords.has(term)))
69
+ .map(([name]) => name))
70
+ if (requestedClasses.has('test')) requestedClasses.add('e2e')
71
+ if (requestedClasses.has('e2e')) requestedClasses.add('test')
72
+ const classifier = createPathClassifier(repoRoot)
73
+ const classificationCache = new Map()
74
+ const changedFiles = new Set((changedSeedIds || []).map((id) => fileOf(g.byId.get(String(id)) || {id})))
75
+ const pathAllowed = (node) => {
76
+ const file = fileOf(node)
77
+ if (!file || changedFiles.has(file) || includeClassified === true) return true
78
+ if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
79
+ const info = classificationCache.get(file)
80
+ const classes = NON_PRODUCT_CLASSES.filter((name) => hasPathClass(info, name))
81
+ if (!info?.excluded && !classes.length) return true
82
+ return classes.some((name) => requestedClasses.has(name))
83
+ }
84
+ const candidates = new Map()
85
+ for (const id of changedSeedIds || []) {
86
+ const node = g.byId.get(String(id))
87
+ if (node) addCandidate(candidates, node, 100 + overlapScore(taskWords, node), 'changed-symbol')
88
+ }
89
+ for (const seed of semanticSeeds || []) addCandidate(candidates, seed, 45 + overlapScore(taskWords, seed), 'task-intent')
90
+
91
+ for (const candidate of [...candidates.values()]) {
92
+ for (const symbol of containedSymbols(g, candidate.node)) {
93
+ const degree = (g.out.get(String(symbol.id)) || []).length + (g.inn.get(String(symbol.id)) || []).length
94
+ addCandidate(candidates, symbol, 22 + overlapScore(taskWords, symbol) + Math.min(12, degree), `symbol-in:${fileOf(candidate.node)}`)
95
+ }
96
+ }
97
+
98
+ const symbolCandidates = [...candidates.values()].filter((item) => isSymbol(item.node.id))
99
+ const ranked = symbolCandidates
100
+ .filter((item) => pathAllowed(item.node))
101
+ .sort((left, right) => right.score - left.score || String(left.node.id).localeCompare(String(right.node.id)))
102
+ const limit = Math.max(1, Math.min(5, Number(maxSymbols) || 3))
103
+ return {
104
+ method: 'intent-expanded graph retrieval + exact changed-symbol seeds',
105
+ status: ranked.length ? 'COMPLETE' : 'NO_SYMBOLS',
106
+ selected: ranked.slice(0, limit).map((item) => ({
107
+ id: String(item.node.id), label: item.node.label || String(item.node.id), file: fileOf(item.node),
108
+ kind: item.node.symbol_kind || null, score: item.score, reasons: [...item.reasons].sort(),
109
+ })),
110
+ candidateCount: ranked.length,
111
+ suppressedClassified: symbolCandidates.length - ranked.length,
112
+ pathPolicy: includeClassified === true
113
+ ? {mode: 'ALL_CLASSIFIED'}
114
+ : {mode: 'PRODUCTION_FIRST', requestedClasses: [...requestedClasses].sort(), changedFilesPinned: changedFiles.size},
115
+ }
116
+ }
@@ -4,23 +4,43 @@
4
4
  // `crate/self/super` paths resolve to repo-local .rs or */mod.rs files. External crates deliberately stay out
5
5
  // of this adapter; Cargo dependency analysis owns them.
6
6
  const SYMS_CORE = `
7
- (function_item name: (identifier) @method)
8
- (struct_item name: (type_identifier) @class)
9
- (enum_item name: (type_identifier) @class)
10
- (trait_item name: (type_identifier) @class)
11
- (type_item name: (type_identifier) @class)
12
- (mod_item name: (identifier) @class)
13
- (const_item name: (identifier) @field)
14
- (static_item name: (identifier) @field)`;
7
+ (function_item name: (identifier) @function)
8
+ (struct_item name: (type_identifier) @struct)
9
+ (enum_item name: (type_identifier) @enum)
10
+ (trait_item name: (type_identifier) @trait)
11
+ (type_item name: (type_identifier) @type)
12
+ (mod_item name: (identifier) @module)
13
+ (const_item name: (identifier) @constant)
14
+ (static_item name: (identifier) @static)`;
15
15
  // Grammar-version-dependent node types, compiled SEPARATELY (one unknown type voids its whole query).
16
16
  const SYMS_OPTIONAL = [
17
- `(macro_definition name: (identifier) @method)`,
18
- `(union_item name: (type_identifier) @class)`,
17
+ `(function_signature_item name: (identifier) @function)`,
18
+ `(macro_definition name: (identifier) @macro)`,
19
+ `(union_item name: (type_identifier) @union)`,
19
20
  ];
20
21
 
21
22
  const cleanSegment = (part) => String(part || "").trim().replace(/^r#/, "");
22
23
  const pathParts = (node) => String(node?.text || "").split("::").map(cleanSegment).filter(Boolean);
23
24
  const under = (node, type) => { for (let p = node?.parent; p; p = p.parent) if (p.type === type) return true; return false; };
25
+ const ancestor = (node, types) => {
26
+ for (let parent = node?.parent; parent; parent = parent.parent) if (types.has(parent.type)) return parent;
27
+ return null;
28
+ };
29
+ const memberOwners = new Set(["impl_item", "trait_item"]);
30
+ const publicVisibility = (declaration, owner) => {
31
+ if (owner?.type === "trait_item") return "public";
32
+ const source = String(declaration?.text || "").trimStart();
33
+ if (/^pub\s/.test(source)) return "public";
34
+ if (/^pub\s*\(/.test(source)) return "protected";
35
+ return "private";
36
+ };
37
+ const ownerName = (owner, field) => {
38
+ if (!owner) return "";
39
+ if (owner.type === "trait_item") return field(owner, "name")?.text || "";
40
+ const type = field(owner, "type")?.text || "";
41
+ const withoutGenerics = type.replace(/<[^<>]*>/g, "");
42
+ return (withoutGenerics.match(/[A-Za-z_]\w*/g) || []).at(-1) || "";
43
+ };
24
44
 
25
45
  function pathAttribute(modNode) {
26
46
  for (let prev = modNode?.previousNamedSibling; prev?.type === "attribute_item"; prev = prev.previousNamedSibling) {
@@ -93,12 +113,29 @@ export default {
93
113
  ],
94
114
 
95
115
  pass1(ctx) {
96
- const { grammar, tree, fileRel, caps, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath } = ctx;
116
+ const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath, links, nameToId } = ctx;
117
+ const owned = [];
97
118
  for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
98
119
  for (const cap of caps(grammar, src, tree.rootNode)) {
99
- addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
120
+ const declaration = cap.node.parent;
121
+ const owner = cap.name === "function" ? ancestor(cap.node, memberOwners) : null;
122
+ const memberOf = ownerName(owner, field);
123
+ const symbolKind = cap.name === "function" ? (memberOf ? "method" : "function") : cap.name;
124
+ const visibility = publicVisibility(declaration, owner);
125
+ const id = addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "function", {
126
+ sourceNode: declaration,
127
+ selectionNode: cap.node,
128
+ symbolKind,
129
+ ...(memberOf ? { memberOf, visibility } : { moduleDeclaration: true }),
130
+ ...(!memberOf && visibility === "public" ? { exported: true } : {}),
131
+ });
132
+ if (id && memberOf) owned.push({owner: memberOf, id});
100
133
  }
101
134
  }
135
+ for (const member of owned) {
136
+ const ownerId = nameToId.get(member.owner);
137
+ if (ownerId && ownerId !== member.id) links.push({source: ownerId, target: member.id, relation: "method", confidence: "EXTRACTED"});
138
+ }
102
139
 
103
140
  // File dependency edges are intentionally unique per source/target/relation. A qualified path can be
104
141
  // nested in another qualified path and often repeats an existing `mod`/`use`; counting occurrences would
@@ -26,19 +26,38 @@ const PROFILE_CAPS = Object.freeze({
26
26
 
27
27
  // The files whose mtime the stdio shell watches for hot reload — keep in sync with the imports in
28
28
  // loadHotApi below (catalog.mjs itself is last: a change here re-derives the whole table).
29
- export const HOT_FILES = ['tools-graph.mjs', 'tools-impact.mjs', 'tools-health.mjs', 'tools-source.mjs', 'tools-context.mjs', 'tools-actions.mjs', 'tools-architecture.mjs', 'tools-history.mjs', 'tools-company.mjs', 'catalog.mjs']
29
+ export const HOT_FILES = ['tools-graph.mjs', 'tools-impact.mjs', 'tools-health.mjs', 'tools-source.mjs', 'tools-context.mjs', 'tools-actions.mjs', 'tools-architecture.mjs', 'tools-history.mjs', 'tools-company.mjs', 'tools-verified-change.mjs', 'catalog.mjs']
30
30
 
31
- function buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc}) {
31
+ function buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc, tv, caps}) {
32
32
  const tools = [
33
33
  {cap: 'graph', name: 'graph_stats', description: 'Return summary statistics: node count, edge count, communities, versioned edge-provenance/legacy-confidence breakdowns, and graph build time vs repo HEAD (staleness).', inputSchema: {type: 'object', properties: {}}, run: (g, a, ctx) => tg.tGraphStats(g, ctx)},
34
34
  {cap: 'graph', name: 'get_node', description: 'Get full details for a specific node by label or ID.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Node label or ID to look up'}}, required: ['label']}, run: (g, a, ctx) => tg.tGetNode(g, a, ctx)},
35
35
  {cap: 'graph', name: 'get_neighbors', description: 'Get all direct neighbors of a node with edge details (1 hop, call sites deduped). For transitive impact use get_dependents; for the impact of your current branch changes use change_impact.', inputSchema: {type: 'object', properties: {label: {type: 'string'}, relation_filter: {type: 'string', description: 'Optional: filter by relation type'}}, required: ['label']}, run: (g, a, ctx) => tg.tGetNeighbors(g, a, ctx)},
36
- {cap: 'graph', name: 'query_graph', description: 'Explore the graph around a concept (BFS/DFS). Returns a focused, ranked subgraph the closest, most-connected nodes near the matched seeds, with edges among them not the full flood; states how many nodes were reached vs shown.', inputSchema: {type: 'object', properties: {question: {type: 'string', description: 'Natural language question or keyword search'}, mode: {type: 'string', enum: ['bfs', 'dfs'], default: 'bfs'}, depth: {type: 'integer', default: 3}, context_filter: {type: 'array', items: {type: 'string'}}, seed_files: {type: 'array', items: {type: 'string'}, maxItems: 12, description: 'Optional exact repo-relative file paths. When resolved, these are the only seeds unless augment_seeds is true'}, augment_seeds: {type: 'boolean', default: false, description: 'With seed_files, also add fuzzy question-derived seeds; false keeps traversal strictly pinned'}, token_budget: {type: 'integer', description: 'Higher budget shows more nodes/edges', default: 2000}}, required: ['question']}, run: (g, a, ctx) => tg.tQueryGraph(g, a, ctx)},
36
+ {cap: 'graph', name: 'query_graph', description: 'Explore a focused production-first graph around a concept (BFS/DFS). Exact seed files stay pinned; classified paths and unreferenced constant/field leaves are suppressed unless the question or explicit flags request them. Reports policy and suppression instead of silently flooding the result.', inputSchema: {type: 'object', properties: {question: {type: 'string', description: 'Natural language question or keyword search'}, mode: {type: 'string', enum: ['bfs', 'dfs'], default: 'bfs'}, depth: {type: 'integer', default: 3}, context_filter: {type: 'array', items: {type: 'string'}}, seed_files: {type: 'array', items: {type: 'string'}, maxItems: 12, description: 'Optional exact repo-relative file paths. When resolved, these are the only seeds unless augment_seeds is true'}, augment_seeds: {type: 'boolean', default: false, description: 'With seed_files, also add fuzzy question-derived seeds; false keeps traversal strictly pinned'}, include_classified: {type: 'boolean', default: false, description: 'Allow traversal through tests/e2e/generated/mocks/stories/docs/benchmarks/temp and explicitly excluded paths. An explicit class term in the question enables only that class.'}, include_low_signal: {type: 'boolean', default: false, description: 'Include unreferenced constant/field leaf symbols that do not match a query term'}, token_budget: {type: 'integer', description: 'Higher budget shows more nodes/edges', default: 2000}}, required: ['question']}, run: (g, a, ctx) => tg.tQueryGraph(g, a, ctx)},
37
37
  {cap: 'graph', name: 'god_nodes', description: 'Rank production-code connectivity hubs by unique call/import/reference neighbors, with class/method ownership reported separately from runtime connectivity. Repeated call sites do not inflate the rank; classified tests, generated/build output and other non-product paths are excluded by default.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', default: 10}, include_classified: {type: 'boolean', default: false, description: 'Include tests/e2e/generated/build output/mocks/stories/docs/benchmarks/temp and paths explicitly excluded by repository classification'}}}, run: (g, a, ctx) => tg.tGodNodes(g, a, ctx)},
38
38
  {cap: 'graph', name: 'shortest_path', description: 'Find the shortest path between two concepts in the knowledge graph.', inputSchema: {type: 'object', properties: {source: {type: 'string'}, target: {type: 'string'}, max_hops: {type: 'integer', default: 8}}, required: ['source', 'target']}, run: (g, a) => tg.tShortestPath(g, a)},
39
39
  {cap: 'graph', name: 'get_dependents', description: 'Transitive blast-radius of ONE node: everything that calls/imports/inherits it, directly or through intermediaries (reverse edges, ranked by proximity then connectivity). Symbol queries stay symbol-precise by default; set include_container_importers only for a conservative module-wide radius. Use before refactoring; get_neighbors shows only 1 hop, change_impact covers your whole current diff at once.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Node label or ID'}, depth: {type: 'integer', description: 'Max reverse hops, default 3', default: 3}, max_nodes: {type: 'integer', description: 'Max dependents to list, default 40', default: 40}, include_container_importers: {type: 'boolean', description: 'Also seed importers of the symbol containing file (broader, conservative; default false)', default: false}}, required: ['label']}, run: (g, a) => ti.tGetDependents(g, a)},
40
40
  {cap: 'graph', name: 'change_impact', description: 'Verdict-first, symbol-aware blast radius. Parses a zero-context git diff, distinguishes additive exports from signature/body/removal risk, and reverse-traverses only exact changed seeds so a new API does not inherit every legacy importer of its file. Measured coverage is used when present; otherwise static test reachability is labelled actualCoverage=NOT_AVAILABLE. Pass `diff` for a not-checked-out PR; `files` without a diff remains a conservative fallback.', inputSchema: {type: 'object', properties: {base: {type: 'string', description: 'Base ref, e.g. origin/main or HEAD~1 (default: first existing of origin/HEAD, origin/main, origin/master, main, master)'}, diff: {type: 'string', maxLength: 2097152, description: 'Optional unified diff (prefer --unified=0) for a PR/change that is not checked out; enables symbol-level classification'}, files: {type: 'array', items: {type: 'string'}, maxItems: 500, description: 'Optional repo-relative changed-file hints. Without diff evidence these are classified conservatively rather than guessed additive'}, depth: {type: 'integer', description: 'Max reverse hops, default 2'}, max_nodes: {type: 'integer', description: 'Max impacted nodes to list, default 40'}}}, run: (g, a, ctx) => ti.tChangeImpact(g, a, ctx)},
41
41
  {cap: 'graph', name: 'git_history', description: 'Behavioral architecture evidence from bounded local git history: churn × connectivity hotspots, hidden co-change coupling, and expected test/source coupling. Reads numstat only — never commit messages, authors, or source bodies.', inputSchema: {type: 'object', properties: {months: {type: 'integer', enum: [3, 6, 12], default: 6}, max_commits: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, min_pair_count: {type: 'integer', minimum: 2, maximum: 100, default: 3}, max_pairs: {type: 'integer', minimum: 1, maximum: 500, default: 100}, top_n: {type: 'integer', minimum: 1, maximum: 50, default: 10}}}, run: (g, a, ctx) => thi.tGitHistory(g, a, ctx)},
42
+ {
43
+ cap: 'graph', refreshGraph: true, name: 'verified_change',
44
+ description: 'Proof-carrying change workflow. Given a natural-language task and current diff/files, returns compact edit contexts, bounded call-argument data-flow, blast radius, graph/architecture/duplicate/API ratchets, affected tests, and one PASS/BLOCKED/UNKNOWN verdict. Package tests run only when explicitly requested and WEAVATRIX_ALLOW_TEST_RUNS=1.',
45
+ inputSchema: {type: 'object', additionalProperties: false, properties: {
46
+ task: {type: 'string', minLength: 1, maxLength: 4000}, phase: {type: 'string', enum: ['plan', 'verify'], default: 'plan'},
47
+ base_ref: {type: 'string', maxLength: 200, default: 'HEAD'}, diff: {type: 'string', maxLength: 2097152}, files: {type: 'array', items: {type: 'string'}, maxItems: 500},
48
+ precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_symbols: {type: 'integer', minimum: 1, maximum: 5, default: 3},
49
+ impact_depth: {type: 'integer', minimum: 1, maximum: 4, default: 2}, max_impact_nodes: {type: 'integer', minimum: 5, maximum: 120, default: 40},
50
+ data_flow_depth: {type: 'integer', minimum: 1, maximum: 3, default: 2}, max_data_flow_edges: {type: 'integer', minimum: 1, maximum: 60, default: 30},
51
+ duplicate_ratchet: {type: 'boolean', default: true},
52
+ api_contract: {type: 'object', additionalProperties: true, properties: {backend: {type: 'string'}, clients: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 20}, method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']}, path: {type: 'string', maxLength: 2048}, changed_files: {type: 'array', items: {type: 'string'}, maxItems: 500}}, required: ['backend', 'clients']},
53
+ tests: {type: 'array', maxItems: 5, items: {type: 'object', additionalProperties: false, properties: {script: {type: 'string', maxLength: 120}, args: {type: 'array', maxItems: 40, items: {type: 'string', maxLength: 300}}}, required: ['script']}},
54
+ run_tests: {type: 'boolean', default: false}, test_timeout_ms: {type: 'integer', minimum: 1000, maximum: 300000, default: 60000},
55
+ }, required: ['task']},
56
+ run: (g, a, ctx) => tv.tVerifiedChange(g, a, ctx, {
57
+ impact: ti.tChangeImpact, context: tb.tContextBundle, inspect: ts.tInspectSymbol,
58
+ prepareChange: tar.tPrepareChange, verifyArchitecture: tar.tVerifyArchitecture, traceApi: tc.tTraceApiContract,
59
+ }, {source: caps.has('source'), health: caps.has('health'), crossrepo: caps.has('crossrepo')}),
60
+ },
42
61
  {
43
62
  cap: 'crossrepo',
44
63
  name: 'trace_api_contract',
@@ -91,7 +110,7 @@ function buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc}) {
91
110
  {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)},
92
111
  {cap: 'health', name: 'run_audit', description: 'Full internal health audit. With base_ref, builds and audits an immutable Git checkout, derives changed files, 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: ['unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category'}, 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_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)},
93
112
  {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)},
94
- {cap: 'health', name: 'hot_path_review', description: 'Rank bounded production-symbol hot paths 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. Local syntax cost stays separate from graph risk; 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: 0}, 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)},
113
+ {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)},
95
114
  {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)},
96
115
  {cap: 'graph', name: 'module_map', description: '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)},
97
116
  {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): method, composed path, handler, and file:line.', inputSchema: {type: 'object', properties: {max_results: {type: 'integer', description: 'Max endpoints to list, default 100'}}}, run: (g, a, ctx) => th.tListEndpoints(g, a, ctx)},
@@ -134,7 +153,7 @@ function buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc}) {
134
153
  // present string (even '') is an explicit selection, so "select nothing" really exposes nothing.
135
154
  export async function loadHotApi(version, capsArg) {
136
155
  const v = version ? `?v=${version}` : ''
137
- const [tg, ti, th, ts, tb, ta, tar, thi, tc] = await Promise.all([
156
+ const [tg, ti, th, ts, tb, ta, tar, thi, tc, tv] = await Promise.all([
138
157
  import(new URL(`./tools-graph.mjs${v}`, import.meta.url).href),
139
158
  import(new URL(`./tools-impact.mjs${v}`, import.meta.url).href),
140
159
  import(new URL(`./tools-health.mjs${v}`, import.meta.url).href),
@@ -144,12 +163,13 @@ export async function loadHotApi(version, capsArg) {
144
163
  import(new URL(`./tools-architecture.mjs${v}`, import.meta.url).href),
145
164
  import(new URL(`./tools-history.mjs${v}`, import.meta.url).href),
146
165
  import(new URL(`./tools-company.mjs${v}`, import.meta.url).href),
166
+ import(new URL(`./tools-verified-change.mjs${v}`, import.meta.url).href),
147
167
  ])
148
- const all = buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc})
149
168
  const raw = capsArg == null ? 'offline' : String(capsArg).trim()
150
169
  const selected = PROFILE_CAPS[raw] || raw.split(',').map((s) => s.trim()).filter(Boolean)
151
170
  .flatMap((cap) => cap === 'online' ? ['advisories', 'hosted'] : [cap])
152
171
  const caps = new Set(selected)
172
+ const all = buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc, tv, caps})
153
173
  const tools = all.filter((t) => caps.has(t.cap))
154
174
  return {
155
175
  tools,
@@ -163,13 +163,37 @@ const CLASS_QUERY_TERMS = Object.freeze({
163
163
  })
164
164
  const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
165
165
  const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
166
+ const LANGUAGE_EXTENSIONS = Object.freeze({
167
+ rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
168
+ javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
169
+ })
170
+
171
+ function requestedLanguages(query) {
172
+ const raw = String(query || '')
173
+ const words = new Set(wordsOf(query))
174
+ const requested = new Set()
175
+ if (words.has('rust')) requested.add('rust')
176
+ if (words.has('python') || words.has('py')) requested.add('python')
177
+ if (words.has('typescript') || words.has('ts') || /typescript/i.test(raw)) requested.add('typescript')
178
+ if (words.has('javascript') || words.has('js') || words.has('nodejs') || /javascript|node\.?js/i.test(raw)) requested.add('javascript')
179
+ if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
180
+ if (words.has('java')) requested.add('java')
181
+ if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
182
+ return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
183
+ }
184
+
185
+ const matchesLanguage = (node, extensions) => {
186
+ if (!extensions.size) return true
187
+ const match = /\.([^.\/]+)$/.exec(sourceFileOf(node))
188
+ return !!match && extensions.has(match[1].toLowerCase())
189
+ }
166
190
 
167
191
  const sourceFileOf = (node) => {
168
192
  const source = node?.source_file || String(node?.id ?? '').split('#', 1)[0]
169
193
  return normPath(source)
170
194
  }
171
195
 
172
- function requestedPathClasses(query) {
196
+ export function requestedPathClasses(query) {
173
197
  const words = new Set(wordsOf(query))
174
198
  const requested = new Set()
175
199
  for (const [category, terms] of Object.entries(CLASS_QUERY_TERMS)) {
@@ -266,6 +290,14 @@ function conceptScore(g, node, concept, queryContext) {
266
290
  else if (words.has(term)) match = Math.max(match, primary ? 36 : 25)
267
291
  else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
268
292
  })
293
+ const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
294
+ const languageConcept = {
295
+ rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'],
296
+ typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'],
297
+ javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'],
298
+ golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs'],
299
+ }[concept.raw]
300
+ if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
269
301
  const fileNode = !isSymbol(node.id)
270
302
  const depth = source ? source.split('/').length : 9
271
303
  if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))
@@ -288,13 +320,16 @@ export function findSeeds(g, query, limit = 8, {repoRoot = null} = {}) {
288
320
  const concepts = queryConcepts(query)
289
321
  if (!concepts.length || limit <= 0) return []
290
322
  const requestedClasses = requestedPathClasses(query)
323
+ const languageExtensions = requestedLanguages(query)
291
324
  const queryContext = {
292
325
  runtimeIntent: concepts.some((concept) => concept.id === 'bootstrap' || concept.id === 'tool-execution'),
293
326
  maintenanceIntent: wordsOf(query).some((word) => ['script', 'scripts', 'build', 'release', 'publish', 'packaging'].includes(word)),
327
+ languageExtensions,
294
328
  }
295
329
  const classifier = createPathClassifier(repoRoot)
296
330
  const classificationCache = new Map()
297
- const rows = g.nodes.filter((node) => isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
331
+ const rows = g.nodes.filter((node) => matchesLanguage(node, languageExtensions)
332
+ && isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
298
333
  const scores = concepts.map((concept) => conceptScore(g, node, concept, queryContext))
299
334
  return {node, scores, total: Math.max(...scores) + scores.reduce((sum, score) => sum + score, 0) / 10}
300
335
  })