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.
Files changed (145) hide show
  1. package/README.md +99 -39
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.15.md +37 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +57 -38
  6. package/src/analysis/architecture/contract-graph.js +119 -0
  7. package/src/analysis/architecture/contract-schema.js +110 -0
  8. package/src/analysis/architecture/contract-storage.js +35 -0
  9. package/src/analysis/architecture/contract-verification.js +168 -0
  10. package/src/analysis/architecture-contract.js +16 -343
  11. package/src/analysis/change-classification/diff-parser.js +103 -0
  12. package/src/analysis/change-classification/options.js +40 -0
  13. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  14. package/src/analysis/change-classification.js +120 -519
  15. package/src/analysis/dead-code-review/policy.js +23 -0
  16. package/src/analysis/dead-code-review.js +9 -19
  17. package/src/analysis/dep-check.js +14 -71
  18. package/src/analysis/dep-rules.js +10 -303
  19. package/src/analysis/dependency/conventions.js +16 -0
  20. package/src/analysis/dependency/scoped-dependencies.js +45 -0
  21. package/src/analysis/dependency/source-references.js +21 -0
  22. package/src/analysis/duplicates.tokenize.js +12 -2
  23. package/src/analysis/endpoints/common.js +62 -0
  24. package/src/analysis/endpoints/extract.js +107 -0
  25. package/src/analysis/endpoints/inventory.js +84 -0
  26. package/src/analysis/endpoints/mounts.js +129 -0
  27. package/src/analysis/endpoints-java.js +80 -3
  28. package/src/analysis/endpoints.js +3 -448
  29. package/src/analysis/git-history/analytics.js +177 -0
  30. package/src/analysis/git-history/collector.js +109 -0
  31. package/src/analysis/git-history/options.js +68 -0
  32. package/src/analysis/git-history.js +128 -577
  33. package/src/analysis/health-capabilities.js +82 -0
  34. package/src/analysis/http-contracts/analysis.js +169 -0
  35. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  36. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  37. package/src/analysis/http-contracts/graph-context.js +143 -0
  38. package/src/analysis/http-contracts/matching.js +61 -0
  39. package/src/analysis/http-contracts/shared.js +86 -0
  40. package/src/analysis/http-contracts.js +6 -822
  41. package/src/analysis/internal-audit/dependency-health.js +111 -0
  42. package/src/analysis/internal-audit/python-manifests.js +65 -0
  43. package/src/analysis/internal-audit/repo-files.js +55 -0
  44. package/src/analysis/internal-audit/supply-chain.js +132 -0
  45. package/src/analysis/internal-audit.collect.js +5 -106
  46. package/src/analysis/internal-audit.reach.js +20 -0
  47. package/src/analysis/internal-audit.run.js +114 -200
  48. package/src/analysis/jvm-dependency-evidence.js +69 -0
  49. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  50. package/src/analysis/source-correctness.js +225 -0
  51. package/src/analysis/structure/dependency-graph.js +156 -0
  52. package/src/analysis/structure/findings.js +142 -0
  53. package/src/analysis/task-retrieval.js +3 -14
  54. package/src/graph/builder/go-receiver-resolution.js +112 -0
  55. package/src/graph/builder/js/queries.js +48 -0
  56. package/src/graph/builder/lang-go.js +89 -4
  57. package/src/graph/builder/lang-js.js +4 -44
  58. package/src/graph/builder/pass2-resolution.js +68 -0
  59. package/src/graph/freshness-probe.js +2 -1
  60. package/src/graph/incremental-refresh.js +3 -2
  61. package/src/graph/internal-builder.build.js +22 -183
  62. package/src/graph/internal-builder.pass2.js +161 -0
  63. package/src/graph/internal-builder.resolvers.js +2 -105
  64. package/src/graph/resolvers/rust.js +117 -0
  65. package/src/mcp/actions/advisories.mjs +18 -0
  66. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  67. package/src/mcp/actions/graph-sync.mjs +195 -0
  68. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  69. package/src/mcp/architecture-bootstrap.mjs +168 -0
  70. package/src/mcp/architecture-starter.mjs +234 -0
  71. package/src/mcp/catalog.mjs +20 -6
  72. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  73. package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
  74. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  75. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  76. package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
  77. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  78. package/src/mcp/git-output.mjs +10 -0
  79. package/src/mcp/graph/context-core.mjs +111 -0
  80. package/src/mcp/graph/context-seeds.mjs +197 -0
  81. package/src/mcp/graph/context-state.mjs +86 -0
  82. package/src/mcp/graph/reverse-reach.mjs +42 -0
  83. package/src/mcp/graph/tools-core.mjs +143 -0
  84. package/src/mcp/graph/tools-query.mjs +223 -0
  85. package/src/mcp/graph-context.mjs +4 -496
  86. package/src/mcp/health/audit-format.mjs +172 -0
  87. package/src/mcp/health/audit.mjs +171 -0
  88. package/src/mcp/health/dead-code.mjs +110 -0
  89. package/src/mcp/health/duplicates.mjs +83 -0
  90. package/src/mcp/health/endpoints.mjs +42 -0
  91. package/src/mcp/health/structure.mjs +219 -0
  92. package/src/mcp/runtime-version.mjs +32 -0
  93. package/src/mcp/server/auto-refresh.mjs +66 -0
  94. package/src/mcp/server/runtime-config.mjs +43 -0
  95. package/src/mcp/server/shutdown.mjs +40 -0
  96. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  97. package/src/mcp/sync/evidence-common.mjs +88 -0
  98. package/src/mcp/sync/evidence-duplicates.mjs +97 -0
  99. package/src/mcp/sync/evidence-health.mjs +56 -0
  100. package/src/mcp/sync/evidence-packages.mjs +95 -0
  101. package/src/mcp/sync/payload-common.mjs +97 -0
  102. package/src/mcp/sync/payload-v2.mjs +53 -0
  103. package/src/mcp/sync/payload-v3.mjs +79 -0
  104. package/src/mcp/sync-evidence.mjs +7 -402
  105. package/src/mcp/sync-payload.mjs +10 -244
  106. package/src/mcp/tools-actions.mjs +18 -414
  107. package/src/mcp/tools-architecture.mjs +18 -70
  108. package/src/mcp/tools-context.mjs +56 -15
  109. package/src/mcp/tools-endpoints.mjs +4 -1
  110. package/src/mcp/tools-graph.mjs +17 -331
  111. package/src/mcp/tools-health.mjs +24 -705
  112. package/src/mcp/tools-impact-change.mjs +2 -38
  113. package/src/mcp/tools-impact.mjs +2 -43
  114. package/src/mcp-server.mjs +35 -146
  115. package/src/path-classification.js +14 -0
  116. package/src/precision/lsp-client/constants.js +12 -0
  117. package/src/precision/lsp-client/environment.js +20 -0
  118. package/src/precision/lsp-client/errors.js +15 -0
  119. package/src/precision/lsp-client/lifecycle.js +127 -0
  120. package/src/precision/lsp-client/message-parser.js +81 -0
  121. package/src/precision/lsp-client/protocol.js +212 -0
  122. package/src/precision/lsp-client/registry.js +58 -0
  123. package/src/precision/lsp-client/repo-uri.js +60 -0
  124. package/src/precision/lsp-client/stdio-client.js +151 -0
  125. package/src/precision/lsp-client.js +10 -682
  126. package/src/precision/lsp-overlay/build.js +238 -0
  127. package/src/precision/lsp-overlay/contract.js +61 -0
  128. package/src/precision/lsp-overlay/reference-results.js +108 -0
  129. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  130. package/src/precision/lsp-overlay/source-session.js +134 -0
  131. package/src/precision/lsp-overlay/store.js +150 -0
  132. package/src/precision/lsp-overlay/target-index.js +147 -0
  133. package/src/precision/lsp-overlay/target-query.js +55 -0
  134. package/src/precision/lsp-overlay.js +11 -868
  135. package/src/precision/typescript-lsp-provider.js +12 -682
  136. package/src/precision/typescript-provider/client.js +69 -0
  137. package/src/precision/typescript-provider/discovery.js +124 -0
  138. package/src/precision/typescript-provider/project-host.js +249 -0
  139. package/src/precision/typescript-provider/project-safety.js +161 -0
  140. package/src/precision/typescript-provider/reference-usage.js +53 -0
  141. package/src/security/malware-heuristics.sweep.js +1 -1
  142. package/src/security/registry-sig.classify.js +2 -1
  143. package/src/security/registry-sig.rules.js +3 -3
  144. package/src/version.js +7 -0
  145. package/docs/releases/v0.2.13.md +0 -48
@@ -0,0 +1,197 @@
1
+ import {PATH_CLASS_NAMES, PATH_CLASS_QUERY_TERMS, 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 CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
21
+ const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
22
+ const LANGUAGE_EXTENSIONS = Object.freeze({
23
+ rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
24
+ javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
25
+ })
26
+
27
+ function requestedLanguages(query) {
28
+ const raw = String(query || ''), words = new Set(wordsOf(query)), requested = new Set()
29
+ if (words.has('rust')) requested.add('rust')
30
+ if (words.has('python') || words.has('py')) requested.add('python')
31
+ if (words.has('typescript') || words.has('ts') || /typescript/i.test(raw)) requested.add('typescript')
32
+ if (words.has('javascript') || words.has('js') || words.has('nodejs') || /javascript|node\.?js/i.test(raw)) requested.add('javascript')
33
+ if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
34
+ if (words.has('java')) requested.add('java')
35
+ if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
36
+ return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
37
+ }
38
+
39
+ const sourceFileOf = (node) => normPath(node?.source_file || String(node?.id ?? '').split('#', 1)[0])
40
+ const matchesLanguage = (node, extensions) => {
41
+ if (!extensions.size) return true
42
+ const match = /\.([^.\/]+)$/.exec(sourceFileOf(node))
43
+ return !!match && extensions.has(match[1].toLowerCase())
44
+ }
45
+
46
+ export function requestedPathClasses(query) {
47
+ const words = new Set(wordsOf(query)), requested = new Set()
48
+ for (const [category, terms] of Object.entries(PATH_CLASS_QUERY_TERMS)) if (terms.some((term) => words.has(term))) requested.add(category)
49
+ if (requested.has('test')) requested.add('e2e')
50
+ if (requested.has('e2e')) requested.add('test')
51
+ return requested
52
+ }
53
+
54
+ function isQueryEligible(node, requestedClasses, classificationCache, classifier) {
55
+ const source = sourceFileOf(node)
56
+ if (!source) return true
57
+ if (!classificationCache.has(source)) classificationCache.set(source, classifier.explain(source, {content: ''}))
58
+ const info = classificationCache.get(source)
59
+ const classified = PATH_CLASS_NAMES.filter((category) => hasPathClass(info, category))
60
+ return classified.length === 0 || classified.some((category) => requestedClasses.has(category))
61
+ }
62
+
63
+ function entrypointSignal(g, node, source, stem) {
64
+ if (isSymbol(node.id) || !CODE_FILE_RE.test(source)) return 0
65
+ const depth = source.split('/').length
66
+ let score = 0
67
+ if (node.entrypoint === true || node.is_entrypoint === true || node.declared_entry === true) score = 72
68
+ if (/^bin\//.test(source) || /\/(?:bin|cmd)\//.test(source)) score = Math.max(score, 62)
69
+ if (depth <= 2 && /^(?:index|main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 60)
70
+ if (depth <= 2 && /(?:^|[-_.])(?:main|server|cli|bootstrap|entry)(?:$|[-_.])/.test(stem)) score = Math.max(score, 57)
71
+ if (depth <= 3 && /^(?:main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 52)
72
+ const incoming = uniqueConnCount(g.inn.get(String(node.id))), outgoing = uniqueConnCount(g.out.get(String(node.id)))
73
+ if (score && incoming === 0 && outgoing > 0) score += Math.min(7, 2 + outgoing)
74
+ return score
75
+ }
76
+
77
+ function toolExecutionSignal(node, source, words, stem) {
78
+ if (!CODE_FILE_RE.test(source)) return 0
79
+ let score = 0
80
+ if (/(^|\/)(?:mcp(?:[-_.][^/]*)?|tools?)(?:\/|[-_.])/.test(source)) score = Math.max(score, 51)
81
+ if (/^(?:catalog|dispatch(?:er)?|registry)$/.test(stem)) score = Math.max(score, 68)
82
+ if (/^(?:tool[-_.]?(?:handler|runner|executor)|tools?[-_.])/.test(stem)) score = Math.max(score, 55)
83
+ if ([...words].some((word) => /^(?:dispatch|dispatcher|toolcall|toolhandler|executetool|invoketool|calltool)$/.test(word))) score = Math.max(score, 64)
84
+ if (source.includes('/mcp/') || stem.includes('mcp-server')) score = Math.max(score, 48)
85
+ return score
86
+ }
87
+
88
+ function queryConcepts(query) {
89
+ const tokens = wordsOf(query)
90
+ const toolExecution = tokens.some((token) => TOOL_EXECUTION_TRIGGERS.has(token))
91
+ const explanatoryWork = tokens.some((token) => token === 'how' || token === 'explain')
92
+ const seen = new Set(), concepts = []
93
+ for (const raw of tokens) {
94
+ if (raw.length < 2 || QUERY_STOP.has(raw)) continue
95
+ if (explanatoryWork && (raw === 'work' || raw === 'works' || raw === 'working')) continue
96
+ if (toolExecution && TOOL_EXECUTION_TERMS.includes(raw)) {
97
+ if (seen.has('tool-execution')) continue
98
+ const trigger = tokens.find((token) => TOOL_EXECUTION_TRIGGERS.has(token)) || raw
99
+ seen.add('tool-execution')
100
+ concepts.push({id: 'tool-execution', raw: trigger, terms: [trigger, ...TOOL_EXECUTION_TERMS.filter((term) => term !== trigger)]})
101
+ continue
102
+ }
103
+ const intent = INTENT_BY_TERM.get(raw), id = intent?.id || raw
104
+ if (seen.has(id)) continue
105
+ seen.add(id)
106
+ concepts.push({id, raw, terms: intent ? [raw, ...intent.terms.filter((term) => term !== raw)] : [raw]})
107
+ }
108
+ return concepts
109
+ }
110
+
111
+ function exactIdentifierSeeds(g, query, limit, {repoRoot = null} = {}) {
112
+ const identifiers = [...new Set((String(query || '').match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [])
113
+ .filter((item) => /(?:[a-z0-9][A-Z]|_)/.test(item)).map((item) => item.toLowerCase()))]
114
+ if (!identifiers.length) return []
115
+ const requestedClasses = requestedPathClasses(query), languageExtensions = requestedLanguages(query)
116
+ const classifier = createPathClassifier(repoRoot), classificationCache = new Map(), matches = []
117
+ for (const identifier of identifiers) {
118
+ const candidates = g.nodes.filter((node) => String(node.label || '').replace(/\(\)$/, '').toLowerCase() === identifier
119
+ && matchesLanguage(node, languageExtensions) && isQueryEligible(node, requestedClasses, classificationCache, classifier))
120
+ .sort((left, right) => degreeOf(g, right.id) - degreeOf(g, left.id) || String(left.id).localeCompare(String(right.id)))
121
+ for (const node of candidates) {
122
+ if (!matches.some((existing) => String(existing.id) === String(node.id))) matches.push(node)
123
+ if (matches.length >= limit) return matches
124
+ }
125
+ }
126
+ return matches
127
+ }
128
+
129
+ function conceptScore(g, node, concept, queryContext) {
130
+ const id = normPath(node.id), label = String(node.label ?? '').toLowerCase(), source = sourceFileOf(node)
131
+ const stem = (label.split('/').pop() || '').replace(/\.[^.]+$/, '')
132
+ const words = new Set(wordsOf(`${node.id} ${node.label ?? ''} ${node.source_file ?? ''}`))
133
+ const segments = new Set(source.split('/').flatMap((part) => wordsOf(part.replace(/\.[^.]+$/, ''))))
134
+ let match = 0
135
+ concept.terms.forEach((term, index) => {
136
+ const primary = index === 0
137
+ if (label === term || stem === term) match = Math.max(match, primary ? 60 : 42)
138
+ else if (segments.has(term)) match = Math.max(match, primary ? 48 : 36)
139
+ else if (words.has(term)) match = Math.max(match, primary ? 36 : 25)
140
+ else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
141
+ })
142
+ const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
143
+ 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]
144
+ if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
145
+ const fileNode = !isSymbol(node.id), depth = source ? source.split('/').length : 9
146
+ if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))
147
+ if (concept.id === 'tool-execution') match = Math.max(match, toolExecutionSignal(node, source, words, stem))
148
+ if (!match) return 0
149
+ let score = match + (fileNode ? 7 : 0) + Math.max(0, 4 - depth) + Math.min(2, degreeOf(g, node.id) / 40)
150
+ if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && DATA_OR_PROSE_RE.test(source)) score -= 34
151
+ if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && !fileNode) score -= 18
152
+ if (queryContext.runtimeIntent && !queryContext.maintenanceIntent && /^(?:scripts?|tools?\/scripts?)\//.test(source)) score -= 32
153
+ if (queryContext.runtimeIntent && /^(?:site|website|public|static|assets)\//.test(source)) score -= 28
154
+ return Math.max(0, score)
155
+ }
156
+
157
+ export function findSeeds(g, query, limit = 8, {repoRoot = null} = {}) {
158
+ const exact = exactIdentifierSeeds(g, query, limit, {repoRoot})
159
+ if (exact.length) return exact
160
+ const concepts = queryConcepts(query)
161
+ if (!concepts.length || limit <= 0) return []
162
+ const requestedClasses = requestedPathClasses(query), languageExtensions = requestedLanguages(query)
163
+ 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}
164
+ const classifier = createPathClassifier(repoRoot), classificationCache = new Map()
165
+ const rows = g.nodes.filter((node) => matchesLanguage(node, languageExtensions) && isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
166
+ const scores = concepts.map((concept) => conceptScore(g, node, concept, queryContext))
167
+ return {node, scores, total: Math.max(...scores) + scores.reduce((sum, score) => sum + score, 0) / 10}
168
+ })
169
+ const chosen = [], used = new Set()
170
+ for (let index = 0; index < concepts.length && chosen.length < limit; index++) {
171
+ const best = rows.filter((row) => !used.has(String(row.node.id)) && row.scores[index] > 0)
172
+ .sort((a, b) => b.scores[index] - a.scores[index] || String(a.node.id).localeCompare(String(b.node.id)))[0]
173
+ if (best) { chosen.push(best.node); used.add(String(best.node.id)) }
174
+ }
175
+ rows.filter((row) => row.total > 0 && !used.has(String(row.node.id)))
176
+ .sort((a, b) => b.total - a.total || String(a.node.id).localeCompare(String(b.node.id)))
177
+ .slice(0, Math.max(0, limit - chosen.length)).forEach((row) => chosen.push(row.node))
178
+ return chosen
179
+ }
180
+
181
+ export function resolveSeedFiles(g, requested, limit = 12) {
182
+ const files = Array.isArray(requested) ? requested.slice(0, limit) : [], seeds = [], missing = []
183
+ for (const raw of files) {
184
+ const wanted = normPath(raw)
185
+ const node = g.nodes.find((candidate) => !isSymbol(candidate.id) && (normPath(candidate.id) === wanted || normPath(candidate.source_file) === wanted))
186
+ if (!node) missing.push(String(raw))
187
+ else if (!seeds.some((seed) => String(seed.id) === String(node.id))) seeds.push(node)
188
+ }
189
+ return {seeds, missing}
190
+ }
191
+
192
+ export function undirectedNeighbors(g, id) {
193
+ const seen = new Map()
194
+ for (const e of g.out.get(id) || []) if (e.barrelProxy !== true) seen.set(e.id, e.relation)
195
+ for (const e of g.inn.get(id) || []) if (e.barrelProxy !== true && !seen.has(e.id)) seen.set(e.id, e.relation)
196
+ return seen
197
+ }
@@ -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,42 @@
1
+ import {isStructuralRelation} from '../../graph/relations.js'
2
+
3
+ // Shared reverse dependency walk for get_dependents and change_impact. Keep one
4
+ // representation so runtime/compile-time depth and edge provenance cannot drift
5
+ // between the two public tools.
6
+ export function reverseReach(g, seeds, maxDepth) {
7
+ const states = new Map([...seeds].map((id) => [String(id), {
8
+ runtimeDepth: 0, runtimeRelation: null, runtimeProvenance: null,
9
+ compileDepth: null, compileRelation: null, compileProvenance: null,
10
+ }]))
11
+ const frontier = [...seeds].map((id) => ({id: String(id), depth: 0, compileOnly: false}))
12
+ for (let cursor = 0; cursor < frontier.length; cursor++) {
13
+ const current = frontier[cursor]
14
+ if (current.depth >= maxDepth) continue
15
+ for (const edge of g.inn.get(current.id) || []) {
16
+ if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
17
+ const id = String(edge.id)
18
+ const compileOnly = current.compileOnly || edge.typeOnly === true || edge.compileOnly === true
19
+ const depth = current.depth + 1
20
+ const entry = states.get(id) || {
21
+ runtimeDepth: null, runtimeRelation: null, runtimeProvenance: null,
22
+ compileDepth: null, compileRelation: null, compileProvenance: null,
23
+ }
24
+ const depthKey = compileOnly ? 'compileDepth' : 'runtimeDepth'
25
+ const relationKey = compileOnly ? 'compileRelation' : 'runtimeRelation'
26
+ const provenanceKey = compileOnly ? 'compileProvenance' : 'runtimeProvenance'
27
+ if (entry[depthKey] != null && entry[depthKey] <= depth) continue
28
+ entry[depthKey] = depth
29
+ entry[relationKey] = edge.relation || 'rel'
30
+ entry[provenanceKey] = edge.provenance || 'UNKNOWN'
31
+ states.set(id, entry)
32
+ frontier.push({id, depth, compileOnly})
33
+ }
34
+ }
35
+ return new Map([...states].map(([id, entry]) => [id, {
36
+ ...entry,
37
+ depth: entry.runtimeDepth ?? entry.compileDepth ?? 0,
38
+ compileOnly: entry.runtimeDepth == null,
39
+ relation: entry.runtimeDepth != null ? entry.runtimeRelation : entry.compileRelation,
40
+ provenance: entry.runtimeDepth != null ? entry.runtimeProvenance : entry.compileProvenance,
41
+ }]))
42
+ }
@@ -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
+ }