weavatrix 0.3.9 → 0.3.11
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.
- package/README.md +29 -4
- package/docs/releases/v0.3.10.md +76 -0
- package/docs/releases/v0.3.11.md +55 -0
- package/package.json +4 -2
- package/skill/SKILL.md +13 -6
- package/src/analysis/dead-check.js +10 -8
- package/src/analysis/dead-code-review.js +3 -0
- package/src/analysis/dep-rules.js +1 -1
- package/src/analysis/hot-path-review.js +1 -1
- package/src/analysis/http-contracts/analysis.js +9 -1
- package/src/analysis/structure/findings.js +21 -1
- package/src/analysis/task-retrieval.js +2 -0
- package/src/build-graph.js +9 -1
- package/src/graph/builder/lang-rust.js +36 -0
- package/src/graph/builder/lang-solidity.js +126 -0
- package/src/graph/builder/lang-sql.js +263 -0
- package/src/graph/builder/pass2-resolution.js +3 -1
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/graph-filter.js +5 -3
- package/src/graph/incremental-refresh.js +1 -1
- package/src/graph/internal-builder.build.js +18 -6
- package/src/graph/internal-builder.langs.js +3 -1
- package/src/graph/internal-builder.resolvers.js +31 -1
- package/src/graph/parser-artifact-boundary.js +1 -0
- package/src/mcp/actions/graph-lifecycle.mjs +3 -3
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/catalog.mjs +2 -2
- package/src/mcp/evidence-snapshot.structure.mjs +5 -2
- package/src/mcp/graph/context-seeds.mjs +6 -2
- package/src/mcp/graph/tools-query.mjs +28 -2
- package/src/mcp/health/endpoints.mjs +34 -5
- package/src/mcp/sync/payload-v2.mjs +1 -0
- package/src/mcp/tools-company.mjs +9 -0
- package/src/mcp/tools-endpoints.mjs +54 -12
- package/src/mcp/tools-graph-hubs.mjs +1 -0
- package/src/mcp/tools-impact.mjs +9 -1
- package/src/precision/lsp-overlay/build.js +9 -4
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/lsp-overlay/target-index.js +8 -3
- package/src/precision/symbol-query.js +2 -1
|
@@ -6,6 +6,7 @@ import {createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
|
6
6
|
|
|
7
7
|
const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
8
8
|
const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
|
|
9
|
+
const AMBIGUOUS_FILE_BASENAME_RE = /^(mod|lib|main)\.rs$|^index\.[a-z0-9]+$|^__init__\.py$/
|
|
9
10
|
const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
10
11
|
const queryWords = (value) => new Set(String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean))
|
|
11
12
|
const exactSymbolName = (node) => {
|
|
@@ -34,6 +35,23 @@ function resolveExactSeedSymbols(g, requested, limit = 12) {
|
|
|
34
35
|
return {seeds, missing, ambiguous}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Edge/hop lines print bare labels without the [id] suffix node lines carry, so repeated or
|
|
39
|
+
// conventionally-generic file basenames (mod.rs, index.ts, __init__.py) become indistinguishable —
|
|
40
|
+
// widen those, scoped to the shown ids, to the last two path segments.
|
|
41
|
+
const makeEdgeLabel = (g, shownIds) => {
|
|
42
|
+
const counts = new Map()
|
|
43
|
+
for (const id of shownIds) {
|
|
44
|
+
const label = labelOf(g, id)
|
|
45
|
+
counts.set(label, (counts.get(label) || 0) + 1)
|
|
46
|
+
}
|
|
47
|
+
return (id) => {
|
|
48
|
+
const label = labelOf(g, id)
|
|
49
|
+
const isFile = String(id).includes('/') && !String(id).includes('#')
|
|
50
|
+
if (isFile && ((counts.get(label) || 0) > 1 || AMBIGUOUS_FILE_BASENAME_RE.test(label))) return String(id).split('/').slice(-2).join('/')
|
|
51
|
+
return label
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
const relationSet = (relationFilter, legacyFilter) => {
|
|
38
56
|
const raw = relationFilter ?? legacyFilter
|
|
39
57
|
const values = Array.isArray(raw) ? raw : (raw == null ? [] : String(raw).split(','))
|
|
@@ -89,6 +107,12 @@ export function tQueryGraph(g, {
|
|
|
89
107
|
const node = g.byId.get(String(id))
|
|
90
108
|
const file = querySourceFile(node)
|
|
91
109
|
if (!file || pinnedFiles.has(file) || include_classified === true) return {ok: true}
|
|
110
|
+
// Extractor-proven test symbols (Rust #[cfg(test)]) live in production files; suppress them
|
|
111
|
+
// under the same policy as path-classified tests unless the question asks about tests.
|
|
112
|
+
if (node?.test_surface === true && !requestedClasses.has('test')) {
|
|
113
|
+
classifiedSuppressed.add(String(id))
|
|
114
|
+
return {ok: false, bucket: 'classified'}
|
|
115
|
+
}
|
|
92
116
|
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
|
|
93
117
|
const info = classificationCache.get(file)
|
|
94
118
|
const classes = QUERY_NON_PRODUCT.filter((name) => hasPathClass(info, name))
|
|
@@ -180,7 +204,8 @@ export function tQueryGraph(g, {
|
|
|
180
204
|
'Nodes:',
|
|
181
205
|
]
|
|
182
206
|
const nodeLines = shown.map((node) => ` [d${node.distance}] ${labelOf(g, node.id)} (deg ${node.degree}) [${node.id}]`)
|
|
183
|
-
const
|
|
207
|
+
const edgeLabel = makeEdgeLabel(g, shownIds)
|
|
208
|
+
const edgeLines = ['', 'Edges:', ...shownEdges.map(([source, relation, target]) => ` ${edgeLabel(source)} --${relation || 'rel'}--> ${edgeLabel(target)}`)]
|
|
184
209
|
let text = [...head.filter(Boolean), ...nodeLines, ...edgeLines].join('\n')
|
|
185
210
|
if (text.length > charBudget) text = text.slice(0, charBudget) + `\n... (truncated to ~${token_budget} tokens)`
|
|
186
211
|
return text
|
|
@@ -218,6 +243,7 @@ export function tShortestPath(g, {source, target, max_hops = 8} = {}) {
|
|
|
218
243
|
if (!previous.has(targetId)) return `No path found between "${sourceNode.label ?? sourceId}" and "${targetNode.label ?? targetId}" within ${limit} hops.`
|
|
219
244
|
const path = []
|
|
220
245
|
for (let current = targetId; current != null; current = previous.get(current)) path.unshift(current)
|
|
221
|
-
const
|
|
246
|
+
const edgeLabel = makeEdgeLabel(g, path)
|
|
247
|
+
const lines = path.map((id, index) => index === 0 ? ` ${edgeLabel(id)}` : ` --${relationTo.get(id) || 'rel'}--> ${edgeLabel(id)}`)
|
|
222
248
|
return [`Shortest path (${path.length - 1} hops): ${sourceNode.label ?? sourceId} → ${targetNode.label ?? targetId}`, ...lines].join('\n')
|
|
223
249
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {rawGraph} from '../graph-context.mjs'
|
|
2
2
|
import {analyzeEndpointInventory} from '../../analysis/endpoints.js'
|
|
3
|
+
import {PATH_CLASS_NAMES, createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
3
4
|
import {toolResult} from '../tool-result.mjs'
|
|
4
5
|
|
|
5
6
|
export function tListEndpoints(g, args, ctx) {
|
|
@@ -16,12 +17,34 @@ export function tListEndpoints(g, args, ctx) {
|
|
|
16
17
|
const path = args.path ? String(args.path) : null
|
|
17
18
|
if (method) eps = eps.filter((endpoint) => endpoint.method === method)
|
|
18
19
|
if (path) eps = eps.filter((endpoint) => endpoint.path === path || endpoint.path.endsWith(path))
|
|
19
|
-
|
|
20
|
+
// Production-first: classified endpoints are suppressed by default; a tests-only graph would
|
|
21
|
+
// suppress everything, so that build mode auto-includes them (same precedent as module_map).
|
|
22
|
+
const includeClassified = args.include_classified === true || graph.graphBuildMode === 'tests-only'
|
|
23
|
+
const classifier = createPathClassifier(ctx.repoRoot)
|
|
24
|
+
const byFile = new Map()
|
|
25
|
+
const classesOf = (file) => {
|
|
26
|
+
const key = String(file || '')
|
|
27
|
+
if (!byFile.has(key)) {
|
|
28
|
+
const info = classifier.explain(key, {content: ''})
|
|
29
|
+
byFile.set(key, {classified: info.excluded || hasPathClass(info, ...PATH_CLASS_NAMES), pathClasses: info.classes})
|
|
30
|
+
}
|
|
31
|
+
return byFile.get(key)
|
|
32
|
+
}
|
|
33
|
+
const production = eps.filter((endpoint) => !classesOf(endpoint.file).classified)
|
|
34
|
+
const classified = eps.filter((endpoint) => classesOf(endpoint.file).classified)
|
|
35
|
+
const suppressed = includeClassified ? 0 : classified.length
|
|
36
|
+
eps = includeClassified ? [...production, ...classified] : production
|
|
37
|
+
const suppressionNote = suppressed
|
|
38
|
+
? `${suppressed} endpoint(s) in classified test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded paths were suppressed; pass include_classified:true to inspect them.`
|
|
39
|
+
: ''
|
|
40
|
+
if (!eps.length) return suppressed
|
|
41
|
+
? `No production HTTP endpoints detected; ${suppressionNote}`
|
|
42
|
+
: 'No HTTP endpoints detected in the indexed code files.'
|
|
20
43
|
const max = Math.max(1, Math.min(300, Number(args.max_results) || 100))
|
|
21
44
|
const shown = eps.slice(0, max)
|
|
22
45
|
const stats = inventory.stats
|
|
23
46
|
const text = [
|
|
24
|
-
`${eps.length} endpoint(s) matched${eps.length > shown.length ? `, showing ${shown.length}` : ''}. Inventory: ${stats.declaredRoutes} declaration(s); ${stats.reachableStaticRoutes} statically reachable composed route(s); ${stats.localDeclarations} local/root declaration candidate(s); ${stats.staticMounts} static router mount(s)${stats.truncated ? `; TRUNCATED at ${stats.maxEndpoints}` : ''}
|
|
47
|
+
`${eps.length} endpoint(s) matched${eps.length > shown.length ? `, showing ${shown.length}` : ''}. Inventory: ${stats.declaredRoutes} declaration(s); ${stats.reachableStaticRoutes} statically reachable composed route(s); ${stats.localDeclarations} local/root declaration candidate(s); ${stats.staticMounts} static router mount(s)${stats.truncated ? `; TRUNCATED at ${stats.maxEndpoints}` : ''}.${suppressed ? ` ${suppressionNote}` : ''}`,
|
|
25
48
|
...shown.map((e) => {
|
|
26
49
|
const via = e.mountChain?.length
|
|
27
50
|
? `\n declared ${e.declaredPath} in ${e.file}${e.line ? `:${e.line}` : ''}; mount chain ${e.mountChain.map((mount) => `${mount.file}:${mount.line} ${mount.path}`).join(' → ')}`
|
|
@@ -29,14 +52,20 @@ export function tListEndpoints(g, args, ctx) {
|
|
|
29
52
|
const activation = e.conditional
|
|
30
53
|
? `; conditional default ${e.defaultActive === false ? 'inactive' : e.defaultActive === true ? 'active' : 'unknown'}`
|
|
31
54
|
: ''
|
|
32
|
-
|
|
55
|
+
const info = classesOf(e.file)
|
|
56
|
+
const tag = info.classified ? ` [classified${info.pathClasses.length ? `:${info.pathClasses.join('+')}` : ''}]` : ''
|
|
57
|
+
return ` ${e.method.toUpperCase().padEnd(6)} ${e.path}${e.handler ? ` → ${e.handler}` : ''} (${e.file}${e.line ? `:${e.line}` : ''}; ${e.mountState}/${e.confidence}${activation})${tag}${via}`
|
|
33
58
|
}),
|
|
34
59
|
].join('\n')
|
|
35
60
|
return toolResult(text, {
|
|
36
61
|
filters: {method, path},
|
|
37
62
|
stats,
|
|
38
|
-
|
|
63
|
+
pathPolicy: includeClassified ? 'all' : 'production-first',
|
|
64
|
+
suppressed,
|
|
65
|
+
endpoints: shown.map((e) => {
|
|
66
|
+
const info = classesOf(e.file)
|
|
67
|
+
return info.classified ? {...e, classified: true, pathClasses: info.pathClasses} : e
|
|
68
|
+
}),
|
|
39
69
|
page: {shown: shown.length, total: eps.length, truncated: eps.length > shown.length || stats.truncated},
|
|
40
70
|
}, {completeness: {status: stats.truncated ? 'PARTIAL' : 'COMPLETE', reason: stats.truncated ? `endpoint cap ${stats.maxEndpoints} reached` : 'all indexed code files scanned'}})
|
|
41
71
|
}
|
|
42
|
-
|
|
@@ -19,6 +19,7 @@ export function sanitizeNode(value) {
|
|
|
19
19
|
if (community !== undefined && Number.isInteger(community) && community >= 0) out.community = community;
|
|
20
20
|
if (typeof value.exported === 'boolean') out.exported = value.exported;
|
|
21
21
|
if (typeof value.decorated === 'boolean') out.decorated = value.decorated;
|
|
22
|
+
if (value.test_surface === true) out.test_surface = true;
|
|
22
23
|
setIf(out, 'complexity', sanitizeComplexity(value.complexity));
|
|
23
24
|
return out;
|
|
24
25
|
}
|
|
@@ -231,6 +231,15 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
231
231
|
...screens,
|
|
232
232
|
]
|
|
233
233
|
}),
|
|
234
|
+
// Mismatch-only endpoints rarely rank into top_n (zero callsites), yet they drive the
|
|
235
|
+
// *_METHOD_MISMATCH verdicts — always surface them, citing the verdict's own total.
|
|
236
|
+
...(analysis.totals.methodMismatches > 0 ? [
|
|
237
|
+
` Method mismatches (${verdict.methodMismatches} call(s)):`,
|
|
238
|
+
...analysis.endpoints.filter((endpoint) => endpoint.methodMismatches > 0).slice(0, 5).flatMap((endpoint) => [
|
|
239
|
+
` ${endpoint.method} ${endpoint.path} — ${endpoint.methodMismatches} call(s) use a different method`,
|
|
240
|
+
...(endpoint.methodMismatchSites || []).slice(0, 2).map((site) => ` caller ${site.clientRepo}:${site.file}:${site.line} uses ${site.method}`),
|
|
241
|
+
]),
|
|
242
|
+
] : []),
|
|
234
243
|
...transportAnalysis.contracts.filter((contract) => contract.callsites.length).slice(0, topN).map((contract) =>
|
|
235
244
|
` ${contract.transport.toUpperCase()} ${contract.service ? `${contract.service}.` : contract.operation ? `${contract.operation} ` : ''}${contract.name} (${contract.file}:${contract.line}) [${contract.liveness}] → ${contract.callsites.length} callsite(s), ${contract.affected.files.length} affected file(s)`),
|
|
236
245
|
completeness.complete
|
|
@@ -29,17 +29,47 @@ function endpointCandidates(inventory, args) {
|
|
|
29
29
|
return exact.length ? exact : eligible.filter((endpoint) => endpoint.path.endsWith(path))
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// A handler_file hint narrows the FULL same-named pool before any scoring, so it can rescue a
|
|
33
|
+
// candidate the ranking would otherwise collapse away. An exact repo-relative match beats suffix
|
|
34
|
+
// matches; comparison is case-insensitive (Windows paths and human-typed hints disagree on casing).
|
|
35
|
+
function matchesHandlerHint(file, hint) {
|
|
36
|
+
if (!hint) return true
|
|
37
|
+
const lower = file.toLowerCase()
|
|
38
|
+
return lower === hint || lower.endsWith(`/${hint}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handlerCandidates(graph, endpoint, hint = '') {
|
|
33
42
|
if (!endpoint.handler) return []
|
|
34
43
|
const wanted = endpoint.handler.toLowerCase()
|
|
35
44
|
const qualifier = String(endpoint.handlerRef || '').split('.').slice(-2, -1)[0]?.toLowerCase() || ''
|
|
36
45
|
const routeDir = posix.dirname(endpoint.file)
|
|
37
|
-
|
|
46
|
+
// File-level import/re-export bindings at the route file break same-name ties between otherwise
|
|
47
|
+
// unranked candidates. The bonus deliberately sits BELOW same-directory proximity (+40): a
|
|
48
|
+
// file-level edge proves coupling, not symbol identity, and must never outrank the conventional
|
|
49
|
+
// same-package/same-dir handler (Go same-package files have no import edge at all).
|
|
50
|
+
const importBound = new Set()
|
|
51
|
+
for (const edge of graph.out.get(endpoint.file) || []) {
|
|
52
|
+
if (edge.relation !== 'imports' && edge.relation !== 're_exports') continue
|
|
53
|
+
if (edge.typeOnly === true || edge.barrelProxy === true) continue
|
|
54
|
+
const targetId = String(edge.id)
|
|
55
|
+
if (targetId.includes('#')) continue
|
|
56
|
+
const target = graph.byId.get(targetId)
|
|
57
|
+
importBound.add(String(target?.source_file || targetId).replace(/\\/g, '/'))
|
|
58
|
+
}
|
|
59
|
+
let pool = (graph.nodes || [])
|
|
38
60
|
.filter((node) => String(node.id).includes('#') && node.source_file && symbolName(node).toLowerCase() === wanted)
|
|
61
|
+
if (hint) {
|
|
62
|
+
pool = pool.filter((node) => matchesHandlerHint(String(node.source_file).replace(/\\/g, '/').toLowerCase(), hint))
|
|
63
|
+
const exact = pool.filter((node) => String(node.source_file).replace(/\\/g, '/').toLowerCase() === hint)
|
|
64
|
+
if (exact.length) pool = exact
|
|
65
|
+
}
|
|
66
|
+
const scored = pool
|
|
39
67
|
.map((node) => {
|
|
40
68
|
const file = String(node.source_file).replace(/\\/g, '/')
|
|
41
69
|
let score = 0
|
|
70
|
+
const importBoundAtRoute = importBound.has(file)
|
|
42
71
|
if (file === endpoint.file) score += 100
|
|
72
|
+
if (importBoundAtRoute) score += 35
|
|
43
73
|
if (posix.dirname(file) === routeDir) score += 40
|
|
44
74
|
if (file.startsWith(`${routeDir}/`)) score += 15
|
|
45
75
|
if (qualifier) {
|
|
@@ -48,16 +78,15 @@ function handlerCandidates(graph, endpoint) {
|
|
|
48
78
|
}
|
|
49
79
|
const line = symbolLine(node)
|
|
50
80
|
if (file === endpoint.file && line >= endpoint.line && line - endpoint.line <= 250) score += 20
|
|
51
|
-
return {node, score}
|
|
81
|
+
return {node, score, importBoundAtRoute}
|
|
52
82
|
})
|
|
53
83
|
.sort((a, b) => b.score - a.score || String(a.node.id).localeCompare(String(b.node.id)))
|
|
54
84
|
if (!scored.length) return []
|
|
55
85
|
const best = scored[0].score
|
|
56
|
-
return scored.filter((item) => item.score === best).map((
|
|
86
|
+
return scored.filter((item) => item.score === best).map(({node, importBoundAtRoute}) => ({node, importBoundAtRoute}))
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
function traceCalls(graph, start, {maxDepth, maxNodes, includeClassified,
|
|
60
|
-
const classifier = createPathClassifier(repoRoot)
|
|
89
|
+
function traceCalls(graph, start, {maxDepth, maxNodes, includeClassified, classifier}) {
|
|
61
90
|
const allowed = (node) => {
|
|
62
91
|
if (!node?.source_file) return false
|
|
63
92
|
if (includeClassified) return true
|
|
@@ -125,12 +154,25 @@ export function tTraceEndpoint(graph, args, ctx) {
|
|
|
125
154
|
completeness: {status: 'PARTIAL', reason: 'ambiguous endpoint'},
|
|
126
155
|
})
|
|
127
156
|
const endpoint = candidates[0]
|
|
128
|
-
const
|
|
157
|
+
const classifier = createPathClassifier(ctx.repoRoot)
|
|
158
|
+
const classify = (file) => {
|
|
159
|
+
const info = classifier.explain(file, {content: ''})
|
|
160
|
+
return {classified: info.excluded || NON_PRODUCT.some((name) => hasPathClass(info, name)), pathClasses: info.classes}
|
|
161
|
+
}
|
|
162
|
+
const hint = String(args.handler_file || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
163
|
+
let handlers = handlerCandidates(graph, endpoint, hint)
|
|
164
|
+
.map((item) => ({...item, ...classify(String(item.node.source_file).replace(/\\/g, '/'))}))
|
|
165
|
+
// Production-first: a classified twin (tests/mocks/…) never ties with a production candidate,
|
|
166
|
+
// but an all-classified candidate set is kept rather than emptied.
|
|
167
|
+
if (args.include_classified !== true && handlers.some((item) => !item.classified)) {
|
|
168
|
+
handlers = handlers.filter((item) => !item.classified)
|
|
169
|
+
}
|
|
129
170
|
if (handlers.length !== 1) return toolResult([
|
|
130
|
-
`Endpoint resolved, but its handler symbol is ${handlers.length ? 'ambiguous' :
|
|
171
|
+
`Endpoint resolved, but its handler symbol is ${handlers.length ? 'ambiguous' : `not present in the graph${hint ? ` (no candidate matched handler_file "${hint}")` : ''}`}:`,
|
|
131
172
|
` ${endpointLine(endpoint)}`,
|
|
132
|
-
...handlers.slice(0, 12).map((node) => ` candidate ${node.label || node.id} (${node.source_file}:${symbolLine(node) || '?'}) [${node.id}]`),
|
|
133
|
-
|
|
173
|
+
...handlers.slice(0, 12).map(({node, importBoundAtRoute, pathClasses}) => ` candidate ${node.label || node.id} (${node.source_file}:${symbolLine(node) || '?'}; import-bound at route: ${importBoundAtRoute ? 'yes' : 'no'}${pathClasses.length ? `; classified:${pathClasses.join('+')}` : ''}) [${node.id}]`),
|
|
174
|
+
...(handlers.length > 1 ? [' Next: pass handler_file with the repo-relative path (or an unambiguous path suffix) of the file that declares the intended handler.'] : []),
|
|
175
|
+
].join('\n'), {status: handlers.length ? 'AMBIGUOUS_HANDLER' : 'HANDLER_NOT_FOUND', endpoint, handlers: handlers.map(({node, importBoundAtRoute, pathClasses}) => ({...node, importBoundAtRoute, pathClasses}))}, {
|
|
134
176
|
completeness: {status: 'PARTIAL', reason: handlers.length ? 'ambiguous handler symbol' : 'handler symbol not found'},
|
|
135
177
|
})
|
|
136
178
|
|
|
@@ -138,9 +180,9 @@ export function tTraceEndpoint(graph, args, ctx) {
|
|
|
138
180
|
const maxNodes = Math.max(2, Math.min(40, Number(args.max_nodes) || 20))
|
|
139
181
|
const contextLines = Math.max(0, Math.min(6, Number(args.context_lines) || 2))
|
|
140
182
|
const maxExcerpts = Math.max(0, Math.min(12, Number(args.max_excerpts) || 6))
|
|
141
|
-
const handler = handlers[0]
|
|
183
|
+
const handler = handlers[0].node
|
|
142
184
|
const traced = traceCalls(graph, handler, {
|
|
143
|
-
maxDepth, maxNodes, includeClassified: args.include_classified === true,
|
|
185
|
+
maxDepth, maxNodes, includeClassified: args.include_classified === true, classifier,
|
|
144
186
|
})
|
|
145
187
|
const excerpts = traced.edges.slice(0, maxExcerpts).map((edge) => {
|
|
146
188
|
const source = graph.byId.get(edge.from)
|
|
@@ -12,6 +12,7 @@ export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx
|
|
|
12
12
|
const classificationByFile = new Map()
|
|
13
13
|
const isNonProduct = (node) => {
|
|
14
14
|
if (include_classified === true) return false
|
|
15
|
+
if (node?.test_surface === true) return true
|
|
15
16
|
const file = sourceFileOf(node)
|
|
16
17
|
if (!file) return false
|
|
17
18
|
if (!classificationByFile.has(file)) classificationByFile.set(file, classifier.explain(file))
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -87,10 +87,18 @@ export function tGetDependents(g, args = {}, ctx = {}) {
|
|
|
87
87
|
`Semantic precision: EXACT_LSP point query${result.cached ? ' (cache hit)' : ''}; ${count} classified direct reference edge(s).`,
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
|
+
// Never echo COMPLETE here ('COMPLETE; exact absence was not proven' is a contradiction),
|
|
91
|
+
// but keep NONE/UNAVAILABLE truthful — PARTIAL implies probing actually began.
|
|
92
|
+
const fallbackState = result.overlay?.state === 'NONE' ? 'NONE'
|
|
93
|
+
: result.overlay?.state === 'UNAVAILABLE' ? 'UNAVAILABLE' : 'PARTIAL'
|
|
90
94
|
return getDependentsFromGraph(
|
|
91
95
|
g,
|
|
92
96
|
args,
|
|
93
|
-
|
|
97
|
+
fallbackState === 'NONE'
|
|
98
|
+
? `Semantic precision: NONE; exact references are not available for this language/target, so graph edges are retained (${result.overlay?.reason || 'no eligible JavaScript/TypeScript semantic targets'}).`
|
|
99
|
+
: fallbackState === 'UNAVAILABLE'
|
|
100
|
+
? `Semantic precision: UNAVAILABLE; graph evidence retained (${result.overlay?.reason || 'semantic point query unavailable'}).`
|
|
101
|
+
: `Semantic precision: PARTIAL; exact absence was not proven, so graph edges are retained (${result.overlay?.reason || 'incomplete project coverage'}).`,
|
|
94
102
|
)
|
|
95
103
|
} catch (error) {
|
|
96
104
|
return getDependentsFromGraph(
|
|
@@ -39,7 +39,9 @@ function coverage(session, verifiedEdges = session.links.length) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function completedOverlay(session) {
|
|
42
|
-
|
|
42
|
+
// Coherence guard: COMPLETE must mean every selected target was actually probed.
|
|
43
|
+
const underQueried = session.queried < session.targets.length
|
|
44
|
+
const state = session.errors || session.truncated || session.unclassifiedReferences || underQueried
|
|
43
45
|
? 'PARTIAL' : 'COMPLETE'
|
|
44
46
|
return baseOverlay(session.graph, state, {
|
|
45
47
|
request: session.request,
|
|
@@ -67,7 +69,9 @@ function completedOverlay(session) {
|
|
|
67
69
|
...(session.errors ? {reason: `${session.errors} semantic request(s) failed or were refused`}
|
|
68
70
|
: session.truncated ? {reason: 'semantic precision stopped at a configured safety limit'}
|
|
69
71
|
: session.unclassifiedReferences
|
|
70
|
-
? {reason: 'some exact references could not be classified as runtime or type-only'}
|
|
72
|
+
? {reason: 'some exact references could not be classified as runtime or type-only'}
|
|
73
|
+
: underQueried
|
|
74
|
+
? {reason: 'semantic precision stopped before probing every selected target'} : {}),
|
|
71
75
|
})
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -185,14 +189,15 @@ export async function buildLspPrecisionOverlay({
|
|
|
185
189
|
}
|
|
186
190
|
if (graphPath) {
|
|
187
191
|
const cached = readPrecisionOverlay(graphPath, graph)
|
|
188
|
-
|
|
192
|
+
// NONE (no eligible targets) is as stable as COMPLETE for an unchanged fingerprint.
|
|
193
|
+
if ((cached?.state === 'COMPLETE' || cached?.state === 'NONE')
|
|
189
194
|
&& precisionOverlayMatches(cached, graph, {request: session.request})
|
|
190
195
|
&& cached.semanticInputFingerprint === session.semanticInputs.fingerprint) return cached
|
|
191
196
|
}
|
|
192
197
|
session.eligible = eligibleTargets(graph, session.boundedMax, targetIds)
|
|
193
198
|
session.targets = session.eligible.targets
|
|
194
199
|
if (!session.targets.length) {
|
|
195
|
-
return persist(session, baseOverlay(graph, '
|
|
200
|
+
return persist(session, baseOverlay(graph, 'NONE', {
|
|
196
201
|
request: session.request,
|
|
197
202
|
semanticInputFingerprint: session.semanticInputs.fingerprint,
|
|
198
203
|
pluginPolicy: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {typeScriptLspContract} from '../typescript-lsp-provider.js'
|
|
2
2
|
|
|
3
|
-
export const PRECISION_OVERLAY_V =
|
|
3
|
+
export const PRECISION_OVERLAY_V = 5
|
|
4
4
|
export const PRECISION_FILE = 'precision.json'
|
|
5
5
|
export const JS_TS_FILE = /\.(?:[cm]?[jt]sx?)$/i
|
|
6
6
|
export const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value)
|
|
@@ -99,10 +99,15 @@ export function eligibleTargets(graph, limit, requestedIds = null) {
|
|
|
99
99
|
const byId = new Map((graph.nodes || []).map((node) => [String(node.id), node]))
|
|
100
100
|
if (Array.isArray(requestedIds) && requestedIds.length) {
|
|
101
101
|
const ids = [...new Set(requestedIds.map(String))]
|
|
102
|
-
const
|
|
102
|
+
const eligible = ids.map((id) => byId.get(id))
|
|
103
103
|
.filter((node) => node?.selection_start && JS_TS_FILE.test(String(node.source_file || '')))
|
|
104
|
-
|
|
105
|
-
return {
|
|
104
|
+
// total counts only eligible targets so coverage fractions stay honest when ids were dropped.
|
|
105
|
+
return {
|
|
106
|
+
targets: eligible.slice(0, limit),
|
|
107
|
+
total: eligible.length,
|
|
108
|
+
droppedRequested: ids.length - eligible.length,
|
|
109
|
+
orphanIds: new Set(),
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
const ranked = new Map()
|
|
108
113
|
const inbound = new Set()
|
|
@@ -75,7 +75,8 @@ async function storeEntry(path, entry) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function cacheable(overlay) {
|
|
78
|
-
|
|
78
|
+
// NONE is stable for the fingerprint, but only COMPLETE ever proves absence (see reader below).
|
|
79
|
+
if (overlay?.state === 'COMPLETE' || overlay?.state === 'NONE') return true
|
|
79
80
|
return overlay?.state === 'PARTIAL'
|
|
80
81
|
&& overlay?.reason === 'semantic precision stopped at a configured safety limit'
|
|
81
82
|
}
|