weavatrix 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -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 +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- 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.run.js +113 -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/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/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -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 +100 -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-server.mjs +35 -146
- 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/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
2
|
+
import {degreeOf, isSymbol, uniqueConnCount} from './context-core.mjs'
|
|
3
|
+
|
|
4
|
+
const QUERY_STOP = new Set('a an and are around architecture best code do does exact explain find focus focused for from how identify in inspect inspection is logic me of or path production project repository request requests rest show symbol symbols the through to trace what where which with'.split(' '))
|
|
5
|
+
const QUERY_INTENTS = [
|
|
6
|
+
['bootstrap', ['bootstrap', 'startup', 'entrypoint', 'entry', 'main', 'root', 'app', 'application', 'applications', 'index', 'server', 'cli']],
|
|
7
|
+
['tool-execution', ['tool', 'tools', 'tooling', 'mcp', 'execution', 'execute', 'invocation', 'invoke', 'dispatch', 'dispatcher', 'handler', 'catalog', 'registry']],
|
|
8
|
+
['auth', ['auth', 'authentication', 'authorization', 'login', 'session', 'authgate']],
|
|
9
|
+
['routing', ['routing', 'router', 'routes', 'route', 'navigation']],
|
|
10
|
+
['layout', ['layout', 'layouts', 'shell']],
|
|
11
|
+
['api', ['api', 'apis', 'endpoint', 'endpoints', 'client']],
|
|
12
|
+
['state', ['state', 'store', 'stores', 'reducer', 'context']],
|
|
13
|
+
]
|
|
14
|
+
const INTENT_BY_TERM = new Map(QUERY_INTENTS.filter(([id]) => id !== 'tool-execution')
|
|
15
|
+
.flatMap(([id, terms]) => terms.map((term) => [term, {id, terms}])))
|
|
16
|
+
const TOOL_EXECUTION_TERMS = QUERY_INTENTS.find(([id]) => id === 'tool-execution')[1]
|
|
17
|
+
const TOOL_EXECUTION_TRIGGERS = new Set(['tool', 'tools', 'tooling', 'mcp'])
|
|
18
|
+
const wordsOf = (value) => String(value ?? '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean)
|
|
19
|
+
const normPath = (value) => String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
20
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
21
|
+
const CLASS_QUERY_TERMS = Object.freeze({
|
|
22
|
+
test: ['test', 'tests', 'testing', 'unit'],
|
|
23
|
+
e2e: ['e2e', 'playwright', 'cypress'],
|
|
24
|
+
generated: ['generated', 'autogenerated', 'dist'],
|
|
25
|
+
mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
|
|
26
|
+
story: ['story', 'stories', 'storybook'],
|
|
27
|
+
docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
|
|
28
|
+
benchmark: ['benchmark', 'benchmarks', 'bench'],
|
|
29
|
+
temp: ['temp', 'temporary', 'tmp'],
|
|
30
|
+
})
|
|
31
|
+
const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
|
|
32
|
+
const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
|
|
33
|
+
const LANGUAGE_EXTENSIONS = Object.freeze({
|
|
34
|
+
rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
|
|
35
|
+
javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function requestedLanguages(query) {
|
|
39
|
+
const raw = String(query || ''), words = new Set(wordsOf(query)), requested = new Set()
|
|
40
|
+
if (words.has('rust')) requested.add('rust')
|
|
41
|
+
if (words.has('python') || words.has('py')) requested.add('python')
|
|
42
|
+
if (words.has('typescript') || words.has('ts') || /typescript/i.test(raw)) requested.add('typescript')
|
|
43
|
+
if (words.has('javascript') || words.has('js') || words.has('nodejs') || /javascript|node\.?js/i.test(raw)) requested.add('javascript')
|
|
44
|
+
if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
|
|
45
|
+
if (words.has('java')) requested.add('java')
|
|
46
|
+
if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
|
|
47
|
+
return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sourceFileOf = (node) => normPath(node?.source_file || String(node?.id ?? '').split('#', 1)[0])
|
|
51
|
+
const matchesLanguage = (node, extensions) => {
|
|
52
|
+
if (!extensions.size) return true
|
|
53
|
+
const match = /\.([^.\/]+)$/.exec(sourceFileOf(node))
|
|
54
|
+
return !!match && extensions.has(match[1].toLowerCase())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function requestedPathClasses(query) {
|
|
58
|
+
const words = new Set(wordsOf(query)), requested = new Set()
|
|
59
|
+
for (const [category, terms] of Object.entries(CLASS_QUERY_TERMS)) if (terms.some((term) => words.has(term))) requested.add(category)
|
|
60
|
+
if (requested.has('test')) requested.add('e2e')
|
|
61
|
+
if (requested.has('e2e')) requested.add('test')
|
|
62
|
+
return requested
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isQueryEligible(node, requestedClasses, classificationCache, classifier) {
|
|
66
|
+
const source = sourceFileOf(node)
|
|
67
|
+
if (!source) return true
|
|
68
|
+
if (!classificationCache.has(source)) classificationCache.set(source, classifier.explain(source, {content: ''}))
|
|
69
|
+
const info = classificationCache.get(source)
|
|
70
|
+
const classified = NON_PRODUCT_CLASSES.filter((category) => hasPathClass(info, category))
|
|
71
|
+
return classified.length === 0 || classified.some((category) => requestedClasses.has(category))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function entrypointSignal(g, node, source, stem) {
|
|
75
|
+
if (isSymbol(node.id) || !CODE_FILE_RE.test(source)) return 0
|
|
76
|
+
const depth = source.split('/').length
|
|
77
|
+
let score = 0
|
|
78
|
+
if (node.entrypoint === true || node.is_entrypoint === true || node.declared_entry === true) score = 72
|
|
79
|
+
if (/^bin\//.test(source) || /\/(?:bin|cmd)\//.test(source)) score = Math.max(score, 62)
|
|
80
|
+
if (depth <= 2 && /^(?:index|main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 60)
|
|
81
|
+
if (depth <= 2 && /(?:^|[-_.])(?:main|server|cli|bootstrap|entry)(?:$|[-_.])/.test(stem)) score = Math.max(score, 57)
|
|
82
|
+
if (depth <= 3 && /^(?:main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 52)
|
|
83
|
+
const incoming = uniqueConnCount(g.inn.get(String(node.id))), outgoing = uniqueConnCount(g.out.get(String(node.id)))
|
|
84
|
+
if (score && incoming === 0 && outgoing > 0) score += Math.min(7, 2 + outgoing)
|
|
85
|
+
return score
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toolExecutionSignal(node, source, words, stem) {
|
|
89
|
+
if (!CODE_FILE_RE.test(source)) return 0
|
|
90
|
+
let score = 0
|
|
91
|
+
if (/(^|\/)(?:mcp(?:[-_.][^/]*)?|tools?)(?:\/|[-_.])/.test(source)) score = Math.max(score, 51)
|
|
92
|
+
if (/^(?:catalog|dispatch(?:er)?|registry)$/.test(stem)) score = Math.max(score, 68)
|
|
93
|
+
if (/^(?:tool[-_.]?(?:handler|runner|executor)|tools?[-_.])/.test(stem)) score = Math.max(score, 55)
|
|
94
|
+
if ([...words].some((word) => /^(?:dispatch|dispatcher|toolcall|toolhandler|executetool|invoketool|calltool)$/.test(word))) score = Math.max(score, 64)
|
|
95
|
+
if (source.includes('/mcp/') || stem.includes('mcp-server')) score = Math.max(score, 48)
|
|
96
|
+
return score
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function queryConcepts(query) {
|
|
100
|
+
const tokens = wordsOf(query)
|
|
101
|
+
const toolExecution = tokens.some((token) => TOOL_EXECUTION_TRIGGERS.has(token))
|
|
102
|
+
const explanatoryWork = tokens.some((token) => token === 'how' || token === 'explain')
|
|
103
|
+
const seen = new Set(), concepts = []
|
|
104
|
+
for (const raw of tokens) {
|
|
105
|
+
if (raw.length < 2 || QUERY_STOP.has(raw)) continue
|
|
106
|
+
if (explanatoryWork && (raw === 'work' || raw === 'works' || raw === 'working')) continue
|
|
107
|
+
if (toolExecution && TOOL_EXECUTION_TERMS.includes(raw)) {
|
|
108
|
+
if (seen.has('tool-execution')) continue
|
|
109
|
+
const trigger = tokens.find((token) => TOOL_EXECUTION_TRIGGERS.has(token)) || raw
|
|
110
|
+
seen.add('tool-execution')
|
|
111
|
+
concepts.push({id: 'tool-execution', raw: trigger, terms: [trigger, ...TOOL_EXECUTION_TERMS.filter((term) => term !== trigger)]})
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const intent = INTENT_BY_TERM.get(raw), id = intent?.id || raw
|
|
115
|
+
if (seen.has(id)) continue
|
|
116
|
+
seen.add(id)
|
|
117
|
+
concepts.push({id, raw, terms: intent ? [raw, ...intent.terms.filter((term) => term !== raw)] : [raw]})
|
|
118
|
+
}
|
|
119
|
+
return concepts
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function exactIdentifierSeeds(g, query, limit, {repoRoot = null} = {}) {
|
|
123
|
+
const identifiers = [...new Set((String(query || '').match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [])
|
|
124
|
+
.filter((item) => /(?:[a-z0-9][A-Z]|_)/.test(item)).map((item) => item.toLowerCase()))]
|
|
125
|
+
if (!identifiers.length) return []
|
|
126
|
+
const requestedClasses = requestedPathClasses(query), languageExtensions = requestedLanguages(query)
|
|
127
|
+
const classifier = createPathClassifier(repoRoot), classificationCache = new Map(), matches = []
|
|
128
|
+
for (const identifier of identifiers) {
|
|
129
|
+
const candidates = g.nodes.filter((node) => String(node.label || '').replace(/\(\)$/, '').toLowerCase() === identifier
|
|
130
|
+
&& matchesLanguage(node, languageExtensions) && isQueryEligible(node, requestedClasses, classificationCache, classifier))
|
|
131
|
+
.sort((left, right) => degreeOf(g, right.id) - degreeOf(g, left.id) || String(left.id).localeCompare(String(right.id)))
|
|
132
|
+
for (const node of candidates) {
|
|
133
|
+
if (!matches.some((existing) => String(existing.id) === String(node.id))) matches.push(node)
|
|
134
|
+
if (matches.length >= limit) return matches
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return matches
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function conceptScore(g, node, concept, queryContext) {
|
|
141
|
+
const id = normPath(node.id), label = String(node.label ?? '').toLowerCase(), source = sourceFileOf(node)
|
|
142
|
+
const stem = (label.split('/').pop() || '').replace(/\.[^.]+$/, '')
|
|
143
|
+
const words = new Set(wordsOf(`${node.id} ${node.label ?? ''} ${node.source_file ?? ''}`))
|
|
144
|
+
const segments = new Set(source.split('/').flatMap((part) => wordsOf(part.replace(/\.[^.]+$/, ''))))
|
|
145
|
+
let match = 0
|
|
146
|
+
concept.terms.forEach((term, index) => {
|
|
147
|
+
const primary = index === 0
|
|
148
|
+
if (label === term || stem === term) match = Math.max(match, primary ? 60 : 42)
|
|
149
|
+
else if (segments.has(term)) match = Math.max(match, primary ? 48 : 36)
|
|
150
|
+
else if (words.has(term)) match = Math.max(match, primary ? 36 : 25)
|
|
151
|
+
else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
|
|
152
|
+
})
|
|
153
|
+
const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
|
|
154
|
+
const languageConcept = {rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'], javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'], golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs']}[concept.raw]
|
|
155
|
+
if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
|
|
156
|
+
const fileNode = !isSymbol(node.id), depth = source ? source.split('/').length : 9
|
|
157
|
+
if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))
|
|
158
|
+
if (concept.id === 'tool-execution') match = Math.max(match, toolExecutionSignal(node, source, words, stem))
|
|
159
|
+
if (!match) return 0
|
|
160
|
+
let score = match + (fileNode ? 7 : 0) + Math.max(0, 4 - depth) + Math.min(2, degreeOf(g, node.id) / 40)
|
|
161
|
+
if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && DATA_OR_PROSE_RE.test(source)) score -= 34
|
|
162
|
+
if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && !fileNode) score -= 18
|
|
163
|
+
if (queryContext.runtimeIntent && !queryContext.maintenanceIntent && /^(?:scripts?|tools?\/scripts?)\//.test(source)) score -= 32
|
|
164
|
+
if (queryContext.runtimeIntent && /^(?:site|website|public|static|assets)\//.test(source)) score -= 28
|
|
165
|
+
return Math.max(0, score)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function findSeeds(g, query, limit = 8, {repoRoot = null} = {}) {
|
|
169
|
+
const exact = exactIdentifierSeeds(g, query, limit, {repoRoot})
|
|
170
|
+
if (exact.length) return exact
|
|
171
|
+
const concepts = queryConcepts(query)
|
|
172
|
+
if (!concepts.length || limit <= 0) return []
|
|
173
|
+
const requestedClasses = requestedPathClasses(query), languageExtensions = requestedLanguages(query)
|
|
174
|
+
const queryContext = {runtimeIntent: concepts.some((concept) => concept.id === 'bootstrap' || concept.id === 'tool-execution'), maintenanceIntent: wordsOf(query).some((word) => ['script', 'scripts', 'build', 'release', 'publish', 'packaging'].includes(word)), languageExtensions}
|
|
175
|
+
const classifier = createPathClassifier(repoRoot), classificationCache = new Map()
|
|
176
|
+
const rows = g.nodes.filter((node) => matchesLanguage(node, languageExtensions) && isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
|
|
177
|
+
const scores = concepts.map((concept) => conceptScore(g, node, concept, queryContext))
|
|
178
|
+
return {node, scores, total: Math.max(...scores) + scores.reduce((sum, score) => sum + score, 0) / 10}
|
|
179
|
+
})
|
|
180
|
+
const chosen = [], used = new Set()
|
|
181
|
+
for (let index = 0; index < concepts.length && chosen.length < limit; index++) {
|
|
182
|
+
const best = rows.filter((row) => !used.has(String(row.node.id)) && row.scores[index] > 0)
|
|
183
|
+
.sort((a, b) => b.scores[index] - a.scores[index] || String(a.node.id).localeCompare(String(b.node.id)))[0]
|
|
184
|
+
if (best) { chosen.push(best.node); used.add(String(best.node.id)) }
|
|
185
|
+
}
|
|
186
|
+
rows.filter((row) => row.total > 0 && !used.has(String(row.node.id)))
|
|
187
|
+
.sort((a, b) => b.total - a.total || String(a.node.id).localeCompare(String(b.node.id)))
|
|
188
|
+
.slice(0, Math.max(0, limit - chosen.length)).forEach((row) => chosen.push(row.node))
|
|
189
|
+
return chosen
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function resolveSeedFiles(g, requested, limit = 12) {
|
|
193
|
+
const files = Array.isArray(requested) ? requested.slice(0, limit) : [], seeds = [], missing = []
|
|
194
|
+
for (const raw of files) {
|
|
195
|
+
const wanted = normPath(raw)
|
|
196
|
+
const node = g.nodes.find((candidate) => !isSymbol(candidate.id) && (normPath(candidate.id) === wanted || normPath(candidate.source_file) === wanted))
|
|
197
|
+
if (!node) missing.push(String(raw))
|
|
198
|
+
else if (!seeds.some((seed) => String(seed.id) === String(node.id))) seeds.push(node)
|
|
199
|
+
}
|
|
200
|
+
return {seeds, missing}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function undirectedNeighbors(g, id) {
|
|
204
|
+
const seen = new Map()
|
|
205
|
+
for (const e of g.out.get(id) || []) if (e.barrelProxy !== true) seen.set(e.id, e.relation)
|
|
206
|
+
for (const e of g.inn.get(id) || []) if (e.barrelProxy !== true && !seen.has(e.id)) seen.set(e.id, e.relation)
|
|
207
|
+
return seen
|
|
208
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {readFileSync, statSync} from 'node:fs'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
import {spawnSync} from 'node:child_process'
|
|
4
|
+
import {childProcessEnv} from '../../child-env.js'
|
|
5
|
+
import {resolveRepoPath} from '../../repo-path.js'
|
|
6
|
+
import {mergePrecisionOverlay, precisionSemanticInputsMatch, readPrecisionOverlay} from '../../precision/lsp-overlay.js'
|
|
7
|
+
|
|
8
|
+
let stalenessCache = {key: '', checkedAt: 0, info: null}
|
|
9
|
+
|
|
10
|
+
export function graphStaleness(ctx) {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
if (stalenessCache.info && stalenessCache.key === ctx.graphPath && now - stalenessCache.checkedAt < 60_000) return stalenessCache.info
|
|
13
|
+
const info = {builtAt: null, headAt: null, stale: false, behind: null}
|
|
14
|
+
try { info.builtAt = statSync(ctx.graphPath).mtime } catch { /* no graph file */ }
|
|
15
|
+
if (ctx.repoRoot && info.builtAt) {
|
|
16
|
+
try {
|
|
17
|
+
const head = spawnSync('git', ['-C', ctx.repoRoot, 'log', '-1', '--format=%cI'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
18
|
+
const iso = (head.stdout || '').trim()
|
|
19
|
+
if (head.status === 0 && iso) {
|
|
20
|
+
info.headAt = new Date(iso)
|
|
21
|
+
if (info.headAt > info.builtAt) {
|
|
22
|
+
info.stale = true
|
|
23
|
+
const count = spawnSync('git', ['-C', ctx.repoRoot, 'rev-list', '--count', `--since=${info.builtAt.toISOString()}`, 'HEAD'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
24
|
+
if (count.status === 0) info.behind = Number(count.stdout.trim()) || null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch { /* git unavailable */ }
|
|
28
|
+
try {
|
|
29
|
+
const status = spawnSync('git', ['-C', ctx.repoRoot, 'status', '--porcelain'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
30
|
+
if (status.status === 0) {
|
|
31
|
+
let newer = 0
|
|
32
|
+
for (const line of String(status.stdout || '').split(/\r?\n/).filter(Boolean).slice(0, 200)) {
|
|
33
|
+
const path = line.slice(3).trim().replace(/^"|"$/g, '')
|
|
34
|
+
try { if (statSync(join(ctx.repoRoot, path)).mtime > info.builtAt) newer++ } catch { newer++ }
|
|
35
|
+
}
|
|
36
|
+
info.dirtyNewer = newer
|
|
37
|
+
if (newer > 0) info.stale = true
|
|
38
|
+
}
|
|
39
|
+
} catch { /* git unavailable */ }
|
|
40
|
+
}
|
|
41
|
+
stalenessCache = {key: ctx.graphPath, checkedAt: now, info}
|
|
42
|
+
return info
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const resetStalenessCache = () => { stalenessCache = {key: '', checkedAt: 0, info: null} }
|
|
46
|
+
|
|
47
|
+
export function stalenessLine(ctx) {
|
|
48
|
+
const state = graphStaleness(ctx)
|
|
49
|
+
if (!state.stale) return null
|
|
50
|
+
const bits = []
|
|
51
|
+
if (state.headAt && state.headAt > state.builtAt) bits.push(`${state.behind != null ? `${state.behind} commit${state.behind === 1 ? '' : 's'}` : 'commits'} newer than the graph`)
|
|
52
|
+
if (state.dirtyNewer) bits.push(`${state.dirtyNewer} uncommitted file(s) edited after the build`)
|
|
53
|
+
return `Warning: graph may be stale — the repo has ${bits.join(' and ')} (built ${state.builtAt.toISOString()}). Line numbers may have drifted; call rebuild_graph.`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function fileStalenessNote(ctx, sourceFile) {
|
|
57
|
+
if (!ctx?.repoRoot || !sourceFile) return null
|
|
58
|
+
const state = graphStaleness(ctx)
|
|
59
|
+
if (!state.builtAt) return null
|
|
60
|
+
try {
|
|
61
|
+
const resolved = resolveRepoPath(ctx.repoRoot, String(sourceFile))
|
|
62
|
+
if (resolved.ok && statSync(resolved.path).mtime > state.builtAt) {
|
|
63
|
+
return `Note: ${sourceFile} changed after the graph was built — line numbers above may have drifted (rebuild_graph refreshes them).`
|
|
64
|
+
}
|
|
65
|
+
} catch { /* missing file */ }
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let rawGraphCache = {path: '', mtimeMs: 0, data: null}
|
|
70
|
+
|
|
71
|
+
export function rawGraph(ctx) {
|
|
72
|
+
const mtimeMs = statSync(ctx.graphPath).mtimeMs
|
|
73
|
+
if (!rawGraphCache.data || rawGraphCache.path !== ctx.graphPath || rawGraphCache.mtimeMs !== mtimeMs) {
|
|
74
|
+
rawGraphCache = {path: ctx.graphPath, mtimeMs, data: JSON.parse(readFileSync(ctx.graphPath, 'utf8'))}
|
|
75
|
+
}
|
|
76
|
+
return rawGraphCache.data
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function effectiveRawGraph(ctx) {
|
|
80
|
+
const raw = rawGraph(ctx)
|
|
81
|
+
const overlay = readPrecisionOverlay(ctx.graphPath, raw)
|
|
82
|
+
const safeOverlay = ctx?.repoRoot && typeof overlay?.semanticInputFingerprint === 'string'
|
|
83
|
+
&& !precisionSemanticInputsMatch(overlay, ctx.repoRoot, raw)
|
|
84
|
+
? null : overlay
|
|
85
|
+
return mergePrecisionOverlay(raw, safeOverlay)
|
|
86
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSymbol, degreeOf, labelOf, connList, resolveNodeInfo, ambiguityNote,
|
|
3
|
+
graphStaleness, fileStalenessNote,
|
|
4
|
+
} from '../graph-context.mjs'
|
|
5
|
+
import {summarizeEdgeProvenance} from '../../graph/edge-provenance.js'
|
|
6
|
+
|
|
7
|
+
const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
|
|
8
|
+
|
|
9
|
+
export function tGraphStats(g, ctx) {
|
|
10
|
+
const files = g.nodes.filter((node) => !isSymbol(node.id)).length
|
|
11
|
+
const symbols = g.nodes.length - files
|
|
12
|
+
const relCount = {}
|
|
13
|
+
const confCount = {}
|
|
14
|
+
let typeOnlyEdges = 0
|
|
15
|
+
let compileOnlyEdges = 0
|
|
16
|
+
for (const edge of g.links) {
|
|
17
|
+
relCount[edge.relation ?? '?'] = (relCount[edge.relation ?? '?'] || 0) + 1
|
|
18
|
+
if (edge.confidence != null) confCount[edge.confidence] = (confCount[edge.confidence] || 0) + 1
|
|
19
|
+
if (edge.typeOnly === true) typeOnlyEdges++
|
|
20
|
+
if (edge.compileOnly === true) compileOnlyEdges++
|
|
21
|
+
}
|
|
22
|
+
const communities = new Map()
|
|
23
|
+
for (const node of g.nodes) {
|
|
24
|
+
const community = node.community ?? 'none'
|
|
25
|
+
communities.set(community, (communities.get(community) || 0) + 1)
|
|
26
|
+
}
|
|
27
|
+
const topCommunities = [...communities.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
28
|
+
const formatCounts = (counts) => Object.entries(counts)
|
|
29
|
+
.sort((a, b) => b[1] - a[1])
|
|
30
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
31
|
+
.join(', ')
|
|
32
|
+
const freshness = ctx ? graphStaleness(ctx) : null
|
|
33
|
+
const provenance = summarizeEdgeProvenance(g.links)
|
|
34
|
+
const precision = g.precision || {state: 'UNAVAILABLE', verifiedEdges: 0, candidates: 0, queried: 0, reason: 'no revision-matched precision overlay'}
|
|
35
|
+
return [
|
|
36
|
+
'Graph summary',
|
|
37
|
+
ctx?.runtime ? `- Weavatrix runtime: v${ctx.runtime.version}; disk v${ctx.runtime.diskVersion || 'unavailable'}; stale ${ctx.runtime.staleRuntime ? 'YES' : 'no'}; profile ${ctx.runtime.profile}; ${ctx.runtime.toolCount} registered tools; capabilities: ${ctx.runtime.capabilities.join(',') || '(none)'}` : null,
|
|
38
|
+
ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
|
|
39
|
+
ctx?.graphPath ? `- Graph: ${ctx.graphPath}` : null,
|
|
40
|
+
`- Build mode: ${g.graphBuildMode || 'full'}`,
|
|
41
|
+
`- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
|
|
42
|
+
`- Edges: ${g.links.length}`,
|
|
43
|
+
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : '- Typed-edge metadata: unavailable (rebuild_graph required)',
|
|
44
|
+
g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${formatCounts(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : '- Edge provenance: unavailable (rebuild_graph required)',
|
|
45
|
+
`- 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}` : ''}`,
|
|
46
|
+
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)',
|
|
47
|
+
g.reExportOccurrencesV ? `- Re-export occurrences: v${g.reExportOccurrencesV} (${g.reExportOccurrences.length} exact site(s))` : '- Re-export occurrences: unavailable (rebuild_graph required)',
|
|
48
|
+
g.symbolSpacesV ? `- TypeScript symbol spaces: v${g.symbolSpacesV} (type/value identities separated)` : '- TypeScript symbol spaces: unavailable (rebuild_graph required)',
|
|
49
|
+
`- Relations: ${formatCounts(relCount)}`,
|
|
50
|
+
Object.keys(confCount).length ? `- Legacy confidence: ${formatCounts(confCount)}` : null,
|
|
51
|
+
`- Communities: ${communities.size} (top by size: ${topCommunities.map(([community, count]) => `#${community}=${count}`).join(', ')})`,
|
|
52
|
+
freshness?.builtAt ? `- Built: ${freshness.builtAt.toISOString()}${freshness.headAt ? ` (repo HEAD committed ${freshness.headAt.toISOString()})` : ''}` : null,
|
|
53
|
+
].filter(Boolean).join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function tGetNode(g, {label} = {}, ctx) {
|
|
57
|
+
const info = resolveNodeInfo(g, label)
|
|
58
|
+
const node = info.node
|
|
59
|
+
if (!node) return `No node found matching "${label}".`
|
|
60
|
+
const note = ambiguityNote(label, info)
|
|
61
|
+
const id = String(node.id)
|
|
62
|
+
const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
63
|
+
const outgoing = g.out.get(id) || []
|
|
64
|
+
const incoming = g.inn.get(id) || []
|
|
65
|
+
const semanticOutgoing = connList(outgoing)
|
|
66
|
+
const semanticIncoming = connList(incoming)
|
|
67
|
+
const sample = (edges, direction) => edges.slice(0, 12)
|
|
68
|
+
.map((edge) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${edge.provenance || 'UNKNOWN'}] ${labelOf(g, edge.id)} [${edge.id}]`)
|
|
69
|
+
.join('\n') || ' (none)'
|
|
70
|
+
return [
|
|
71
|
+
note,
|
|
72
|
+
`Node: ${node.label ?? id}`,
|
|
73
|
+
`- id: ${id}`,
|
|
74
|
+
`- kind: ${isSymbol(id) ? 'symbol' : 'file'}${node.file_type ? ` (${node.file_type})` : ''}`,
|
|
75
|
+
node.source_file ? `- source: ${node.source_file}${node.source_location ? ` ${node.source_location}` : ''}` : null,
|
|
76
|
+
node.community != null ? `- community: ${node.community}` : null,
|
|
77
|
+
`- semantic degree: ${semanticOutgoing.length + semanticIncoming.length} (out ${semanticOutgoing.length}, in ${semanticIncoming.length})${outgoing.length + incoming.length !== semanticOutgoing.length + semanticIncoming.length ? `; ${outgoing.length + incoming.length} physical/structural edges retained` : ''}`,
|
|
78
|
+
`Outgoing:\n${sample(outgoing, 'out')}`,
|
|
79
|
+
`Incoming:\n${sample(incoming, 'in')}`,
|
|
80
|
+
drift,
|
|
81
|
+
].filter(Boolean).join('\n')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dedupeEdges(edges) {
|
|
85
|
+
const grouped = new Map()
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
const key = `${edge.relation || 'rel'}|${compileKind(edge) || 'runtime'}|${edge.id}`
|
|
88
|
+
const current = grouped.get(key)
|
|
89
|
+
if (current) {
|
|
90
|
+
current.count += 1
|
|
91
|
+
current.provenance.add(edge.provenance || 'UNKNOWN')
|
|
92
|
+
} else grouped.set(key, {
|
|
93
|
+
id: edge.id, relation: edge.relation, typeOnly: edge.typeOnly === true,
|
|
94
|
+
compileOnly: edge.compileOnly === true, provenance: new Set([edge.provenance || 'UNKNOWN']), count: 1,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
return [...grouped.values()]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
101
|
+
const info = resolveNodeInfo(g, label)
|
|
102
|
+
const node = info.node
|
|
103
|
+
if (!node) return `No node found matching "${label}".`
|
|
104
|
+
const note = ambiguityNote(label, info)
|
|
105
|
+
const id = String(node.id)
|
|
106
|
+
const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
107
|
+
const filter = relation_filter ? String(relation_filter).toLowerCase() : null
|
|
108
|
+
const matches = (edge) => !filter || String(edge.relation ?? '').toLowerCase() === filter
|
|
109
|
+
const outgoingRaw = (g.out.get(id) || []).filter(matches)
|
|
110
|
+
const incomingRaw = (g.inn.get(id) || []).filter(matches)
|
|
111
|
+
const outgoing = dedupeEdges(outgoingRaw)
|
|
112
|
+
const incoming = dedupeEdges(incomingRaw)
|
|
113
|
+
const line = (edge, direction) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${[...edge.provenance].sort().join('+')}] ${labelOf(g, edge.id)} [${edge.id}]${edge.count > 1 ? ` (${edge.count} sites)` : ''}`
|
|
114
|
+
return [
|
|
115
|
+
note,
|
|
116
|
+
`Neighbors of ${node.label ?? id}${filter ? ` (relation=${filter})` : ''}: ${outgoing.length + incoming.length} unique (${outgoingRaw.length + incomingRaw.length} edges)`,
|
|
117
|
+
`Outgoing (${outgoing.length}):`,
|
|
118
|
+
...outgoing.slice(0, 60).map((edge) => line(edge, 'out')),
|
|
119
|
+
`Incoming (${incoming.length}):`,
|
|
120
|
+
...incoming.slice(0, 60).map((edge) => line(edge, 'in')),
|
|
121
|
+
drift,
|
|
122
|
+
].filter(Boolean).join('\n')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function tGetCommunity(g, {community_id} = {}) {
|
|
126
|
+
const groups = new Map()
|
|
127
|
+
for (const node of g.nodes) {
|
|
128
|
+
if (node.community == null) continue
|
|
129
|
+
if (!groups.has(node.community)) groups.set(node.community, [])
|
|
130
|
+
groups.get(node.community).push(node)
|
|
131
|
+
}
|
|
132
|
+
const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length)
|
|
133
|
+
const index = Number(community_id)
|
|
134
|
+
if (!Number.isInteger(index) || index < 0 || index >= ranked.length) return `Invalid community_id ${community_id}. Valid range 0..${ranked.length - 1} (0 = largest).`
|
|
135
|
+
const [rawId, members] = ranked[index]
|
|
136
|
+
const files = members.filter((member) => !isSymbol(member.id))
|
|
137
|
+
return [
|
|
138
|
+
`Community #${index} (raw id ${rawId}) — ${members.length} nodes, ${files.length} files:`,
|
|
139
|
+
...members.slice().sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id)).slice(0, 80)
|
|
140
|
+
.map((member) => ` ${member.label ?? member.id} [${member.id}]`),
|
|
141
|
+
members.length > 80 ? ` … +${members.length - 80} more` : null,
|
|
142
|
+
].filter(Boolean).join('\n')
|
|
143
|
+
}
|