weavatrix 0.2.13 → 0.2.15
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 +99 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +14 -71
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +45 -0
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +114 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +197 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +97 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/mcp-server.mjs +35 -146
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -3,15 +3,19 @@ import {boundedInteger} from '../bounds.js'
|
|
|
3
3
|
import {isSymbol, labelOf} from './graph-context.mjs'
|
|
4
4
|
import {toolResult} from './tool-result.mjs'
|
|
5
5
|
import {sourceExcerpt} from './tools-source.mjs'
|
|
6
|
+
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
6
7
|
|
|
7
8
|
const MAX_LINE_SAMPLES = 5
|
|
9
|
+
const CONTEXT_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
8
10
|
|
|
9
11
|
const fileOf = (g, id) => {
|
|
10
12
|
const node = g.byId.get(String(id))
|
|
11
13
|
return String(node?.source_file || (isSymbol(id) ? String(id).split('#')[0] : id))
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function aggregateEdges(g, edges, cap, {
|
|
16
|
+
function aggregateEdges(g, edges, cap, {
|
|
17
|
+
callsiteFile = null, classifier = null, includeClassified = true, productionFirst = false,
|
|
18
|
+
} = {}) {
|
|
15
19
|
const groups = new Map()
|
|
16
20
|
for (const edge of edges || []) {
|
|
17
21
|
if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
|
|
@@ -33,9 +37,23 @@ function aggregateEdges(g, edges, cap, {callsiteFile = null} = {}) {
|
|
|
33
37
|
if (edge.typeOnly === true) group.typeOnly = true
|
|
34
38
|
if (edge.compileOnly === true) group.compileOnly = true
|
|
35
39
|
}
|
|
36
|
-
const all = [...groups.values()]
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const all = [...groups.values()]
|
|
41
|
+
if (classifier) for (const group of all) {
|
|
42
|
+
const info = classifier.explain(group.file, {content: ''})
|
|
43
|
+
const classes = CONTEXT_NON_PRODUCT.filter((name) => hasPathClass(info, name))
|
|
44
|
+
group.classified = classes.length > 0 || info?.excluded === true
|
|
45
|
+
if (classes.length) group.pathClasses = classes
|
|
46
|
+
}
|
|
47
|
+
const eligible = includeClassified ? all : all.filter((group) => !group.classified)
|
|
48
|
+
eligible.sort((left, right) => (productionFirst ? Number(left.classified) - Number(right.classified) : 0)
|
|
49
|
+
|| right.count - left.count || left.file.localeCompare(right.file) || left.id.localeCompare(right.id))
|
|
50
|
+
return {
|
|
51
|
+
total: eligible.length,
|
|
52
|
+
available: all.length,
|
|
53
|
+
suppressed: all.length - eligible.length,
|
|
54
|
+
shown: eligible.slice(0, cap),
|
|
55
|
+
capped: eligible.length > cap,
|
|
56
|
+
}
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
const sameOrigin = (occurrence, definition) => occurrence.originId === definition.id
|
|
@@ -97,12 +115,14 @@ function exactReExportSites(g, definition, cap) {
|
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
function linesForGroups(title, groups) {
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const suppression = groups.suppressed ? `; ${groups.suppressed} classified container(s) suppressed` : ''
|
|
119
|
+
if (!groups.shown.length) return [`${title}: none${suppression}`]
|
|
120
|
+
const lines = [`${title}: ${groups.total} container(s)${groups.capped ? ` (${groups.shown.length} shown)` : ''}${suppression}`]
|
|
102
121
|
for (const group of groups.shown) {
|
|
103
122
|
const sites = group.lines.length ? `:${group.lines.join(',')}` : ''
|
|
104
123
|
const destination = group.targetFile ? ` → ${group.targetFile}` : ''
|
|
105
|
-
|
|
124
|
+
const classified = group.classified ? ` [classified${group.pathClasses?.length ? `:${group.pathClasses.join('+')}` : ''}]` : ''
|
|
125
|
+
lines.push(` ${group.count}× ${group.relation} ${group.label} [call site ${group.file}${sites}${destination}]${classified}`)
|
|
106
126
|
}
|
|
107
127
|
return lines
|
|
108
128
|
}
|
|
@@ -139,26 +159,47 @@ export async function tContextBundle(g, args = {}, ctx = {}, inspectSymbol) {
|
|
|
139
159
|
...inspection.definition,
|
|
140
160
|
name: String(g.byId.get(inspection.definition.id)?.label || '').replace(/\(\)$/, ''),
|
|
141
161
|
}
|
|
142
|
-
const
|
|
162
|
+
const classifier = createPathClassifier(ctx.repoRoot || null)
|
|
163
|
+
const includeClassified = args.include_classified === true
|
|
164
|
+
const inbound = aggregateEdges(g, g.inn.get(definition.id), maxRelated, {
|
|
165
|
+
classifier, includeClassified, productionFirst: true,
|
|
166
|
+
})
|
|
143
167
|
const outbound = aggregateEdges(g, g.out.get(definition.id), maxRelated, {callsiteFile: definition.file})
|
|
144
168
|
const reExports = exactReExportSites(g, definition, maxReExports)
|
|
145
169
|
const source = []
|
|
170
|
+
const overlaps = (left, right) => left.file === right.file
|
|
171
|
+
&& left.startLine <= right.endLine && right.startLine <= left.endLine
|
|
146
172
|
const append = (role, excerpt) => {
|
|
147
173
|
if (!excerpt || source.length >= maxSourceFiles) return
|
|
148
|
-
if (source.some((item) => item
|
|
174
|
+
if (source.some((item) => overlaps(item, excerpt))) return
|
|
149
175
|
source.push({role, ...excerpt})
|
|
150
176
|
}
|
|
151
177
|
append('Definition', inspection.source.definition)
|
|
152
178
|
const contextLines = boundedInteger(args.context_lines, 4, 0, 12)
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
const excerptCandidates = []
|
|
180
|
+
const groupExcerpts = (groups, role) => {
|
|
181
|
+
const primary = []
|
|
182
|
+
const secondary = []
|
|
183
|
+
for (const group of groups.shown) for (let index = 0; index < group.lines.length; index++) {
|
|
184
|
+
const candidate = {role, excerpt: sourceExcerpt(ctx.repoRoot, group.file, group.lines[index], contextLines)}
|
|
185
|
+
;(index === 0 ? primary : secondary).push(candidate)
|
|
186
|
+
}
|
|
187
|
+
return [...primary, ...secondary]
|
|
155
188
|
}
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
const outboundExcerpts = groupExcerpts(outbound, 'Outbound call site')
|
|
190
|
+
const inboundExcerpts = groupExcerpts(inbound, 'Inbound call site')
|
|
191
|
+
for (let index = 0; index < Math.max(outboundExcerpts.length, inboundExcerpts.length); index++) {
|
|
192
|
+
if (outboundExcerpts[index]) excerptCandidates.push(outboundExcerpts[index])
|
|
193
|
+
if (inboundExcerpts[index]) excerptCandidates.push(inboundExcerpts[index])
|
|
158
194
|
}
|
|
159
|
-
for (const excerpt of inspection.source.callers || []) {
|
|
160
|
-
|
|
195
|
+
for (const excerpt of inspection.source.callers || []) excerptCandidates.push({role: 'Reference', excerpt})
|
|
196
|
+
const deferred = []
|
|
197
|
+
for (const candidate of excerptCandidates) {
|
|
198
|
+
const knownFile = source.some((item) => item.file === candidate.excerpt?.file)
|
|
199
|
+
if (knownFile && candidate.role !== 'Outbound call site') deferred.push(candidate)
|
|
200
|
+
else append(candidate.role, candidate.excerpt)
|
|
161
201
|
}
|
|
202
|
+
for (const candidate of deferred) append(candidate.role, candidate.excerpt)
|
|
162
203
|
const result = {
|
|
163
204
|
status: 'OK', definition, evidence: inspection.evidence,
|
|
164
205
|
references: {
|
|
@@ -105,7 +105,10 @@ function endpointLine(endpoint) {
|
|
|
105
105
|
const mount = endpoint.mountChain?.length
|
|
106
106
|
? endpoint.mountChain.map((item) => `${item.file}:${item.line} ${item.path}`).join(' → ')
|
|
107
107
|
: 'no static router mount chain'
|
|
108
|
-
|
|
108
|
+
const activation = endpoint.conditional
|
|
109
|
+
? `; conditional default ${endpoint.defaultActive === false ? 'inactive' : endpoint.defaultActive === true ? 'active' : 'unknown'}`
|
|
110
|
+
: ''
|
|
111
|
+
return `${endpoint.method} ${endpoint.path} → ${endpoint.handler || 'inline/unknown'} (${endpoint.file}:${endpoint.line}; ${endpoint.mountState}/${endpoint.confidence}${activation}; declared ${endpoint.declaredPath}; ${mount})`
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
export function tTraceEndpoint(graph, args, ctx) {
|
package/src/mcp/tools-graph.mjs
CHANGED
|
@@ -1,331 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const confCount = {}
|
|
19
|
-
let typeOnlyEdges = 0
|
|
20
|
-
let compileOnlyEdges = 0
|
|
21
|
-
for (const e of g.links) {
|
|
22
|
-
relCount[e.relation ?? '?'] = (relCount[e.relation ?? '?'] || 0) + 1
|
|
23
|
-
if (e.confidence != null) confCount[e.confidence] = (confCount[e.confidence] || 0) + 1
|
|
24
|
-
if (e.typeOnly === true) typeOnlyEdges++
|
|
25
|
-
if (e.compileOnly === true) compileOnlyEdges++
|
|
26
|
-
}
|
|
27
|
-
const comm = new Map()
|
|
28
|
-
for (const n of g.nodes) {
|
|
29
|
-
const c = n.community ?? 'none'
|
|
30
|
-
comm.set(c, (comm.get(c) || 0) + 1)
|
|
31
|
-
}
|
|
32
|
-
const topComm = [...comm.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
33
|
-
const fmt = (o) =>
|
|
34
|
-
Object.entries(o)
|
|
35
|
-
.sort((a, b) => b[1] - a[1])
|
|
36
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
37
|
-
.join(', ')
|
|
38
|
-
const freshness = ctx ? graphStaleness(ctx) : null
|
|
39
|
-
const provenance = summarizeEdgeProvenance(g.links)
|
|
40
|
-
const precision = g.precision || {state: 'UNAVAILABLE', verifiedEdges: 0, candidates: 0, queried: 0, reason: 'no revision-matched precision overlay'}
|
|
41
|
-
return [
|
|
42
|
-
`Graph summary`,
|
|
43
|
-
ctx?.runtime ? `- Weavatrix runtime: v${ctx.runtime.version}; profile ${ctx.runtime.profile}; ${ctx.runtime.toolCount} registered tools; capabilities: ${ctx.runtime.capabilities.join(',') || '(none)'}` : null,
|
|
44
|
-
ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
|
|
45
|
-
ctx?.graphPath ? `- Graph: ${ctx.graphPath}` : null,
|
|
46
|
-
`- Build mode: ${g.graphBuildMode || 'full'}`,
|
|
47
|
-
`- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
|
|
48
|
-
`- Edges: ${g.links.length}`,
|
|
49
|
-
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : `- Typed-edge metadata: unavailable (rebuild_graph required)`,
|
|
50
|
-
g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${fmt(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : `- Edge provenance: unavailable (rebuild_graph required)`,
|
|
51
|
-
`- Semantic precision: ${precision.state}${precision.provider ? ` via ${precision.provider}${precision.providerVersion ? ` ${precision.providerVersion}` : ''}${precision.typescriptVersion ? ` (TypeScript ${precision.typescriptVersion})` : ''}` : ''}; ${precision.verifiedEdges || 0} EXACT_LSP edge(s), ${precision.queried || 0}/${precision.candidates || 0} bounded target(s) queried${precision.truncated ? ' (partial/truncated)' : ''}${precision.reason ? `; ${precision.reason}` : ''}`,
|
|
52
|
-
g.barrelResolutionV ? `- Barrel resolution: v${g.barrelResolutionV} (semantic tools look through JS/TS re-export facades)` : `- Barrel resolution: unavailable (rebuild_graph required for JS/TS barrel transparency)`,
|
|
53
|
-
g.reExportOccurrencesV ? `- Re-export occurrences: v${g.reExportOccurrencesV} (${g.reExportOccurrences.length} exact site(s))` : `- Re-export occurrences: unavailable (rebuild_graph required)`,
|
|
54
|
-
g.symbolSpacesV ? `- TypeScript symbol spaces: v${g.symbolSpacesV} (type/value identities separated)` : `- TypeScript symbol spaces: unavailable (rebuild_graph required)`,
|
|
55
|
-
`- Relations: ${fmt(relCount)}`,
|
|
56
|
-
Object.keys(confCount).length ? `- Legacy confidence: ${fmt(confCount)}` : null,
|
|
57
|
-
`- Communities: ${comm.size} (top by size: ${topComm.map(([c, n]) => `#${c}=${n}`).join(', ')})`,
|
|
58
|
-
freshness?.builtAt ? `- Built: ${freshness.builtAt.toISOString()}${freshness.headAt ? ` (repo HEAD committed ${freshness.headAt.toISOString()})` : ''}` : null,
|
|
59
|
-
]
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join('\n')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function tGetNode(g, {label} = {}, ctx) {
|
|
65
|
-
const info = resolveNodeInfo(g, label)
|
|
66
|
-
const n = info.node
|
|
67
|
-
if (!n) return `No node found matching "${label}".`
|
|
68
|
-
const note = ambiguityNote(label, info)
|
|
69
|
-
const id = String(n.id)
|
|
70
|
-
const drift = ctx ? fileStalenessNote(ctx, n.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
71
|
-
const outs = g.out.get(id) || []
|
|
72
|
-
const ins = g.inn.get(id) || []
|
|
73
|
-
const semanticOuts = connList(outs)
|
|
74
|
-
const semanticIns = connList(ins)
|
|
75
|
-
const sample = (list, dir) =>
|
|
76
|
-
list
|
|
77
|
-
.slice(0, 12)
|
|
78
|
-
.map((e) => ` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} [${e.provenance || 'UNKNOWN'}] ${labelOf(g, e.id)} [${e.id}]`)
|
|
79
|
-
.join('\n') || ' (none)'
|
|
80
|
-
return [
|
|
81
|
-
note,
|
|
82
|
-
`Node: ${n.label ?? id}`,
|
|
83
|
-
`- id: ${id}`,
|
|
84
|
-
`- kind: ${isSymbol(id) ? 'symbol' : 'file'}${n.file_type ? ` (${n.file_type})` : ''}`,
|
|
85
|
-
n.source_file ? `- source: ${n.source_file}${n.source_location ? ` ${n.source_location}` : ''}` : null,
|
|
86
|
-
n.community != null ? `- community: ${n.community}` : null,
|
|
87
|
-
`- semantic degree: ${semanticOuts.length + semanticIns.length} (out ${semanticOuts.length}, in ${semanticIns.length})${outs.length + ins.length !== semanticOuts.length + semanticIns.length ? `; ${outs.length + ins.length} physical/structural edges retained` : ''}`,
|
|
88
|
-
`Outgoing:\n${sample(outs, 'out')}`,
|
|
89
|
-
`Incoming:\n${sample(ins, 'in')}`,
|
|
90
|
-
drift,
|
|
91
|
-
]
|
|
92
|
-
.filter(Boolean)
|
|
93
|
-
.join('\n')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Collapse repeated edges to the same neighbor (one per call site in the graph) into `(N sites)` —
|
|
97
|
-
// a hub function's caller list shrinks ~2-3x with no information loss.
|
|
98
|
-
function dedupeEdges(list) {
|
|
99
|
-
const grouped = new Map()
|
|
100
|
-
for (const e of list) {
|
|
101
|
-
const key = `${e.relation || 'rel'}|${compileKind(e) || 'runtime'}|${e.id}`
|
|
102
|
-
const cur = grouped.get(key)
|
|
103
|
-
if (cur) {
|
|
104
|
-
cur.count += 1
|
|
105
|
-
cur.provenance.add(e.provenance || 'UNKNOWN')
|
|
106
|
-
} else grouped.set(key, {id: e.id, relation: e.relation, typeOnly: e.typeOnly === true, compileOnly: e.compileOnly === true, provenance: new Set([e.provenance || 'UNKNOWN']), count: 1})
|
|
107
|
-
}
|
|
108
|
-
return [...grouped.values()]
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
112
|
-
const info = resolveNodeInfo(g, label)
|
|
113
|
-
const n = info.node
|
|
114
|
-
if (!n) return `No node found matching "${label}".`
|
|
115
|
-
const note = ambiguityNote(label, info)
|
|
116
|
-
const id = String(n.id)
|
|
117
|
-
const drift = ctx ? fileStalenessNote(ctx, n.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
118
|
-
const rf = relation_filter ? String(relation_filter).toLowerCase() : null
|
|
119
|
-
const match = (e) => !rf || String(e.relation ?? '').toLowerCase() === rf
|
|
120
|
-
const outsRaw = (g.out.get(id) || []).filter(match)
|
|
121
|
-
const insRaw = (g.inn.get(id) || []).filter(match)
|
|
122
|
-
const outs = dedupeEdges(outsRaw)
|
|
123
|
-
const ins = dedupeEdges(insRaw)
|
|
124
|
-
const line = (e, dir) =>
|
|
125
|
-
` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} [${[...e.provenance].sort().join('+')}] ${labelOf(g, e.id)} [${e.id}]${e.count > 1 ? ` (${e.count} sites)` : ''}`
|
|
126
|
-
return [
|
|
127
|
-
note,
|
|
128
|
-
`Neighbors of ${n.label ?? id}${rf ? ` (relation=${rf})` : ''}: ${outs.length + ins.length} unique (${outsRaw.length + insRaw.length} edges)`,
|
|
129
|
-
`Outgoing (${outs.length}):`,
|
|
130
|
-
...outs.slice(0, 60).map((e) => line(e, 'out')),
|
|
131
|
-
`Incoming (${ins.length}):`,
|
|
132
|
-
...ins.slice(0, 60).map((e) => line(e, 'in')),
|
|
133
|
-
drift,
|
|
134
|
-
].filter(Boolean).join('\n')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export {tGodNodes} from './tools-graph-hubs.mjs'
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
export function tGetCommunity(g, {community_id} = {}) {
|
|
141
|
-
const groups = new Map()
|
|
142
|
-
for (const node of g.nodes) {
|
|
143
|
-
const c = node.community
|
|
144
|
-
if (c == null) continue
|
|
145
|
-
if (!groups.has(c)) groups.set(c, [])
|
|
146
|
-
groups.get(c).push(node)
|
|
147
|
-
}
|
|
148
|
-
const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length) // 0-indexed by size
|
|
149
|
-
const idx = Number(community_id)
|
|
150
|
-
if (!Number.isInteger(idx) || idx < 0 || idx >= ranked.length)
|
|
151
|
-
return `Invalid community_id ${community_id}. Valid range 0..${ranked.length - 1} (0 = largest).`
|
|
152
|
-
const [rawId, members] = ranked[idx]
|
|
153
|
-
const files = members.filter((m) => !isSymbol(m.id))
|
|
154
|
-
return [
|
|
155
|
-
`Community #${idx} (raw id ${rawId}) — ${members.length} nodes, ${files.length} files:`,
|
|
156
|
-
...members
|
|
157
|
-
.slice()
|
|
158
|
-
.sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id))
|
|
159
|
-
.slice(0, 80)
|
|
160
|
-
.map((m) => ` ${m.label ?? m.id} [${m.id}]`),
|
|
161
|
-
members.length > 80 ? ` … +${members.length - 80} more` : null,
|
|
162
|
-
]
|
|
163
|
-
.filter(Boolean)
|
|
164
|
-
.join('\n')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// A plain BFS/DFS flood dumps every reached node (thousands on a real graph) at near-zero signal.
|
|
168
|
-
// Instead: traverse to record reach + distance-from-seed, then show only the closest, most-connected
|
|
169
|
-
// slice as a coherent subgraph (edges kept only among shown nodes). Honest about what was trimmed.
|
|
170
|
-
const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
171
|
-
const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
|
|
172
|
-
const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
173
|
-
const queryWords = (value) => new Set(String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean))
|
|
174
|
-
|
|
175
|
-
export function tQueryGraph(g, {
|
|
176
|
-
question, mode = 'bfs', depth = 3, context_filter, seed_files, augment_seeds = false,
|
|
177
|
-
include_classified = false, include_low_signal = false, token_budget = 2000,
|
|
178
|
-
} = {}, toolCtx = {}) {
|
|
179
|
-
const pinned = resolveSeedFiles(g, seed_files)
|
|
180
|
-
// Exact seed files are a control surface, not a hint: by default they disable fuzzy keyword seeds.
|
|
181
|
-
// Callers can opt back into augmentation when they explicitly want both behaviors.
|
|
182
|
-
const automatic = pinned.seeds.length && augment_seeds !== true
|
|
183
|
-
? []
|
|
184
|
-
: findSeeds(g, question, Math.max(0, 6 - pinned.seeds.length), {repoRoot: toolCtx.repoRoot || null})
|
|
185
|
-
const seeds = [...pinned.seeds, ...automatic.filter((node) => !pinned.seeds.some((seed) => String(seed.id) === String(node.id)))]
|
|
186
|
-
if (!seeds.length) return `No nodes matched "${question}".`
|
|
187
|
-
const maxDepth = Math.max(1, Math.min(6, Number(depth) || 3))
|
|
188
|
-
const ctx = Array.isArray(context_filter) && context_filter.length ? new Set(context_filter.map((c) => String(c).toLowerCase())) : null
|
|
189
|
-
const relOk = (rel) => !ctx || ctx.has(String(rel ?? '').toLowerCase())
|
|
190
|
-
const requestedClasses = requestedPathClasses(question)
|
|
191
|
-
const classifier = createPathClassifier(toolCtx.repoRoot || null)
|
|
192
|
-
const classificationCache = new Map()
|
|
193
|
-
const pinnedFiles = new Set(pinned.seeds.map(querySourceFile))
|
|
194
|
-
const classifiedSuppressed = new Set()
|
|
195
|
-
const pathPolicy = (id) => {
|
|
196
|
-
const node = g.byId.get(String(id))
|
|
197
|
-
const file = querySourceFile(node)
|
|
198
|
-
if (!file || pinnedFiles.has(file) || include_classified === true) return {ok: true}
|
|
199
|
-
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
|
|
200
|
-
const info = classificationCache.get(file)
|
|
201
|
-
const classes = QUERY_NON_PRODUCT.filter((name) => hasPathClass(info, name))
|
|
202
|
-
if (!classes.length && !info?.excluded) return {ok: true}
|
|
203
|
-
if (classes.some((name) => requestedClasses.has(name))) return {ok: true}
|
|
204
|
-
classifiedSuppressed.add(String(id))
|
|
205
|
-
return {ok: false, bucket: 'classified'}
|
|
206
|
-
}
|
|
207
|
-
const questionTerms = queryWords(question)
|
|
208
|
-
const isLowSignal = (id) => {
|
|
209
|
-
if (include_low_signal === true || start.includes(String(id))) return false
|
|
210
|
-
const node = g.byId.get(String(id))
|
|
211
|
-
if (!node || !isSymbol(node.id) || !LOW_SIGNAL_SYMBOL_RE.test(String(node.symbol_kind || ''))) return false
|
|
212
|
-
const labelTerms = queryWords(node.label || String(node.id || '').split('#').pop() || '')
|
|
213
|
-
if ([...questionTerms].some((term) => labelTerms.has(term))) return false
|
|
214
|
-
return degreeOf(g, id) === 0
|
|
215
|
-
}
|
|
216
|
-
const charBudget = Math.max(400, (Number(token_budget) || 2000) * 4)
|
|
217
|
-
// node budget scales gently with the token budget; edges follow the surviving nodes.
|
|
218
|
-
const nodeBudget = Math.max(20, Math.min(120, Math.round((Number(token_budget) || 2000) / 40)))
|
|
219
|
-
const depthOf = new Map() // id -> shortest distance from any seed
|
|
220
|
-
const start = seeds.map((s) => String(s.id))
|
|
221
|
-
if (mode === 'dfs') {
|
|
222
|
-
const stack = start.map((id) => ({id, d: 0}))
|
|
223
|
-
const seen = new Set()
|
|
224
|
-
while (stack.length) {
|
|
225
|
-
const {id, d} = stack.pop()
|
|
226
|
-
if (!depthOf.has(id) || d < depthOf.get(id)) depthOf.set(id, d)
|
|
227
|
-
if (seen.has(id)) continue
|
|
228
|
-
seen.add(id)
|
|
229
|
-
if (d >= maxDepth) continue
|
|
230
|
-
for (const [nid, rel] of undirectedNeighbors(g, id)) {
|
|
231
|
-
if (!relOk(rel)) continue
|
|
232
|
-
if (!pathPolicy(nid).ok) continue
|
|
233
|
-
if (!seen.has(nid)) stack.push({id: nid, d: d + 1})
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
let frontier = start.slice()
|
|
238
|
-
start.forEach((id) => depthOf.set(id, 0))
|
|
239
|
-
for (let d = 0; d < maxDepth && frontier.length; d++) {
|
|
240
|
-
const next = []
|
|
241
|
-
for (const id of frontier)
|
|
242
|
-
for (const [nid, rel] of undirectedNeighbors(g, id)) {
|
|
243
|
-
if (!relOk(rel)) continue
|
|
244
|
-
if (!pathPolicy(nid).ok) continue
|
|
245
|
-
if (!depthOf.has(nid)) {
|
|
246
|
-
depthOf.set(nid, d + 1)
|
|
247
|
-
next.push(nid)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
frontier = next
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// rank reached nodes: seeds first, then by proximity (depth asc), then connectivity (degree desc)
|
|
254
|
-
const reachedBeforeSignalFilter = depthOf.size
|
|
255
|
-
const lowSignalSuppressed = [...depthOf.keys()].filter(isLowSignal).length
|
|
256
|
-
const ranked = [...depthOf.entries()]
|
|
257
|
-
.filter(([id]) => !isLowSignal(id))
|
|
258
|
-
.map(([id, d]) => ({id, d, deg: degreeOf(g, id)}))
|
|
259
|
-
.sort((a, b) => a.d - b.d || b.deg - a.deg)
|
|
260
|
-
const shown = ranked.slice(0, nodeBudget)
|
|
261
|
-
const shownIds = new Set(shown.map((n) => n.id))
|
|
262
|
-
const edgeSeen = new Set()
|
|
263
|
-
const shownEdges = []
|
|
264
|
-
for (const source of shownIds) {
|
|
265
|
-
for (const edge of g.out.get(source) || []) {
|
|
266
|
-
const target = String(edge.id)
|
|
267
|
-
if (edge.barrelProxy === true || !shownIds.has(target) || !relOk(edge.relation)) continue
|
|
268
|
-
const key = `${source}|${edge.relation}|${target}`
|
|
269
|
-
if (edgeSeen.has(key)) continue
|
|
270
|
-
edgeSeen.add(key)
|
|
271
|
-
shownEdges.push([source, edge.relation, target])
|
|
272
|
-
if (shownEdges.length >= 160) break
|
|
273
|
-
}
|
|
274
|
-
if (shownEdges.length >= 160) break
|
|
275
|
-
}
|
|
276
|
-
const head = [
|
|
277
|
-
`Query: "${question}" (${mode}, depth ${maxDepth}${ctx ? `, context ${[...ctx].join('/')}` : ''})`,
|
|
278
|
-
`Seeds: ${seeds.map((s) => s.label ?? s.id).join(', ')}`,
|
|
279
|
-
pinned.missing.length ? `Unresolved pinned seed files: ${pinned.missing.join(', ')}` : null,
|
|
280
|
-
`Reached ${reachedBeforeSignalFilter} policy-eligible nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
|
|
281
|
-
classifiedSuppressed.size ? `Suppressed ${classifiedSuppressed.size} classified/non-product traversal node(s); ask for that class or pass include_classified:true.` : null,
|
|
282
|
-
lowSignalSuppressed ? `Suppressed ${lowSignalSuppressed} unreferenced constant/field node(s) with no query-term match; pass include_low_signal:true to inspect them.` : null,
|
|
283
|
-
include_classified === true ? 'Path policy: classified/non-product traversal explicitly enabled.' : `Path policy: production-first${requestedClasses.size ? `; explicit question classes enabled: ${[...requestedClasses].join(', ')}` : ''}.`,
|
|
284
|
-
``,
|
|
285
|
-
`Nodes:`,
|
|
286
|
-
]
|
|
287
|
-
const nodeLines = shown.map((n) => ` [d${n.d}] ${labelOf(g, n.id)} (deg ${n.deg}) [${n.id}]`)
|
|
288
|
-
const edgeLines = ['', 'Edges:', ...shownEdges.map(([s, r, t]) => ` ${labelOf(g, s)} --${r || 'rel'}--> ${labelOf(g, t)}`)]
|
|
289
|
-
let text = [...head.filter(Boolean), ...nodeLines, ...edgeLines].join('\n')
|
|
290
|
-
if (text.length > charBudget) text = text.slice(0, charBudget) + `\n... (truncated to ~${token_budget} tokens)`
|
|
291
|
-
return text
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function tShortestPath(g, {source, target, max_hops = 8} = {}) {
|
|
295
|
-
const s = resolveNode(g, source)
|
|
296
|
-
const t = resolveNode(g, target)
|
|
297
|
-
if (!s) return `Source "${source}" not found.`
|
|
298
|
-
if (!t) return `Target "${target}" not found.`
|
|
299
|
-
const sid = String(s.id)
|
|
300
|
-
const tid = String(t.id)
|
|
301
|
-
if (sid === tid) return `Source and target are the same node: ${s.label ?? sid}.`
|
|
302
|
-
const cap = Math.max(1, Math.min(20, Number(max_hops) || 8))
|
|
303
|
-
const prev = new Map([[sid, null]])
|
|
304
|
-
const relTo = new Map()
|
|
305
|
-
let frontier = [sid]
|
|
306
|
-
let hops = 0
|
|
307
|
-
let found = false
|
|
308
|
-
while (frontier.length && hops < cap && !found) {
|
|
309
|
-
const next = []
|
|
310
|
-
for (const id of frontier) {
|
|
311
|
-
for (const [nid, rel] of undirectedNeighbors(g, id)) {
|
|
312
|
-
if (prev.has(nid)) continue
|
|
313
|
-
prev.set(nid, id)
|
|
314
|
-
relTo.set(nid, rel)
|
|
315
|
-
if (nid === tid) {
|
|
316
|
-
found = true
|
|
317
|
-
break
|
|
318
|
-
}
|
|
319
|
-
next.push(nid)
|
|
320
|
-
}
|
|
321
|
-
if (found) break
|
|
322
|
-
}
|
|
323
|
-
frontier = next
|
|
324
|
-
hops++
|
|
325
|
-
}
|
|
326
|
-
if (!prev.has(tid)) return `No path found between "${s.label ?? sid}" and "${t.label ?? tid}" within ${cap} hops.`
|
|
327
|
-
const path = []
|
|
328
|
-
for (let cur = tid; cur != null; cur = prev.get(cur)) path.unshift(cur)
|
|
329
|
-
const lines = path.map((id, i) => (i === 0 ? ` ${labelOf(g, id)}` : ` --${relTo.get(id) || 'rel'}--> ${labelOf(g, id)}`))
|
|
330
|
-
return [`Shortest path (${path.length - 1} hops): ${s.label ?? sid} → ${t.label ?? tid}`, ...lines].join('\n')
|
|
331
|
-
}
|
|
1
|
+
// Stable public facade for graph inspection and traversal tools. Propagate the catalog's
|
|
2
|
+
// cache-busting query into owner modules so edits go live without an MCP reconnect.
|
|
3
|
+
const version = new URL(import.meta.url).search
|
|
4
|
+
const load = (path) => import(new URL(`${path}${version}`, import.meta.url).href)
|
|
5
|
+
const [core, query, hubs] = await Promise.all([
|
|
6
|
+
load('./graph/tools-core.mjs'),
|
|
7
|
+
load('./graph/tools-query.mjs'),
|
|
8
|
+
load('./tools-graph-hubs.mjs'),
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
export const tGraphStats = core.tGraphStats
|
|
12
|
+
export const tGetNode = core.tGetNode
|
|
13
|
+
export const tGetNeighbors = core.tGetNeighbors
|
|
14
|
+
export const tGetCommunity = core.tGetCommunity
|
|
15
|
+
export const tQueryGraph = query.tQueryGraph
|
|
16
|
+
export const tShortestPath = query.tShortestPath
|
|
17
|
+
export const tGodNodes = hubs.tGodNodes
|