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
@@ -1,414 +1,18 @@
1
- // Action tools: rebuild the graph, the explicit 'retarget' group, and the explicit 'online' group
2
- // (the ONLY tools that ever touch the network).
3
- // Hot-reloadable (re-imported by catalog.mjs on change).
4
- import {readFileSync, writeFileSync, existsSync, statSync, realpathSync} from 'node:fs'
5
- import {dirname, join, isAbsolute} from 'node:path'
6
- import {createHash} from 'node:crypto'
7
- import {prevGraphPathFor, diffGraphs, formatGraphDiff, graphStaleness} from './graph-context.mjs'
8
- import {buildGraphForRepo, defaultPrecisionMode} from '../build-graph.js'
9
- import {graphHomeDir, graphOutDirForModule, graphOutDirForRepo} from '../graph/layout.js'
10
- import {liveRepositoryRecords, registerRepository, repositoryRecord} from '../graph/repo-registry.js'
11
- import {refreshAdvisories, storeMeta, DEFAULT_STORE} from '../security/advisory-store.js'
12
- import {collectInstalled} from '../security/installed.js'
13
- import {createSyncPayload, createSyncPayloadV3, MAX_SYNC_BODY_BYTES} from './sync-payload.mjs'
14
- import {createEvidenceSnapshot} from './evidence-snapshot.mjs'
15
- import {writeCachedArchitectureContract} from '../analysis/architecture-contract.js'
16
- import {precisionSemanticInputsMatch, readPrecisionOverlay} from '../precision/lsp-overlay.js'
17
- import {toolResult} from './tool-result.mjs'
18
-
19
- const MAX_SYNC_GRAPH_FILE_BYTES = 64 * 1024 * 1024
20
- const SYNC_PREVIEW_TTL_MS = 5 * 60 * 1000
21
- const MAX_SYNC_PREVIEWS = 4
22
- const syncPreviews = new Map()
23
-
24
- function syncRepoLabel(repoRoot) {
25
- const basename = String(repoRoot || '').replace(/[\\/]+$/, '').split(/[\\/]/).pop() || 'repo'
26
- const safe = basename.normalize('NFKC').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '')
27
- return (safe || 'repo').slice(0, 128)
28
- }
29
-
30
- function syncDestination(raw) {
31
- let url
32
- try { url = new URL(raw) } catch { throw new Error('WEAVATRIX_SYNC_URL is invalid') }
33
- if (!['http:', 'https:'].includes(url.protocol)) throw new Error('WEAVATRIX_SYNC_URL must use HTTPS (or HTTP for loopback development)')
34
- if (url.username || url.password) throw new Error('WEAVATRIX_SYNC_URL must not contain embedded credentials; use WEAVATRIX_SYNC_TOKEN')
35
- if (url.hash) throw new Error('WEAVATRIX_SYNC_URL must not contain a fragment')
36
- const loopback = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname.toLowerCase())
37
- if (url.protocol !== 'https:' && !loopback) throw new Error('WEAVATRIX_SYNC_URL must use HTTPS unless the destination is loopback')
38
- const display = `${url.origin}${url.pathname}${url.search ? ' (query redacted)' : ''}`
39
- return {url: url.toString(), display}
40
- }
41
-
42
- function pruneSyncPreviews(now = Date.now()) {
43
- for (const [token, preview] of syncPreviews) if (preview.expiresAt <= now) syncPreviews.delete(token)
44
- while (syncPreviews.size >= MAX_SYNC_PREVIEWS) syncPreviews.delete(syncPreviews.keys().next().value)
45
- }
46
-
47
- function confirmationToken({url, repositoryId, payloadVersion, bodyHash}) {
48
- return createHash('sha256')
49
- .update(`weavatrix-sync-preview-v1\0${url}\0${repositoryId}\0${payloadVersion}\0${bodyHash}`)
50
- .digest('hex').slice(0, 24)
51
- }
52
-
53
- function syncSectionSummary(payload) {
54
- if (payload.syncPayloadV !== 3) return 'graph topology only (explicit V2 compatibility mode)'
55
- const sections = payload.evidence?.sections || {}
56
- const names = Object.entries(sections).map(([name, section]) => `${name}:${section?.state || section?.verdict || 'included'}`)
57
- return names.join(', ') || 'bounded architecture/health/stack/package/duplicate evidence'
58
- }
59
-
60
- function syncPreviewText(preview, {expired = false} = {}) {
61
- return [
62
- `SYNC PREVIEW${expired ? ' (the supplied confirmation was missing, expired, or did not match)' : ''} — no network request was made.`,
63
- `Destination: ${preview.destinationDisplay}.`,
64
- `Repository: ${preview.repoName}; opaque repository UUID: ${preview.repositoryId}.`,
65
- `Payload V${preview.payload.syncPayloadV}: ${preview.payload.nodes.length} nodes / ${preview.payload.links.length} edges, ${Math.round(preview.bodyBytes / 1024)} KB; body SHA-256 ${preview.bodyHash.slice(0, 12)}.`,
66
- `Payload fields: ${Object.keys(preview.payload).sort().join(', ')}.`,
67
- `Included sections: ${syncSectionSummary(preview.payload)}.`,
68
- 'Excluded by the wire allowlist: source bodies, snippets, absolute host paths, environment values, credentials, Git remotes, and unknown fields.',
69
- `After the user approves this exact destination and summary, call sync_graph again within 5 minutes with dry_run:false and confirm_token: "${preview.token}".`,
70
- ].join('\n')
71
- }
72
-
73
- async function sendSyncPreview(preview, timeoutMs) {
74
- try {
75
- const res = await fetch(preview.url, {
76
- method: 'POST',
77
- headers: {
78
- 'content-type': 'application/json',
79
- 'x-weavatrix-payload-version': String(preview.payload.syncPayloadV),
80
- 'x-weavatrix-repo': preview.repoName,
81
- 'x-weavatrix-repository-id': preview.repositoryId,
82
- ...(process.env.WEAVATRIX_SYNC_TOKEN ? {authorization: `Bearer ${process.env.WEAVATRIX_SYNC_TOKEN}`} : {}),
83
- },
84
- body: preview.body,
85
- signal: AbortSignal.timeout(timeoutMs),
86
- })
87
- if (!res.ok) {
88
- const accepted = res.headers?.get?.('x-weavatrix-accept-payload-versions')
89
- const compatibility = (res.status === 415 || res.status === 422) && accepted
90
- ? ` Endpoint accepts payload version(s) ${accepted}; create and approve a new V2 preview only if graph-only sync is intentional.`
91
- : ''
92
- return `Sync endpoint ${preview.destinationDisplay} answered HTTP ${res.status} — graph NOT accepted.${compatibility}`
93
- }
94
- syncPreviews.delete(preview.token)
95
- const evidenceNote = preview.payload.syncPayloadV === 3
96
- ? ` + evidence ${preview.payload.evidence?.snapshotHash?.slice(0, 12) || 'unknown'}`
97
- : ''
98
- return `Graph for ${preview.repoName} (${preview.payload.nodes.length} nodes / ${preview.payload.links.length} edges${evidenceNote}, ${Math.round(preview.bodyBytes / 1024)} KB) pushed to approved destination ${preview.destinationDisplay}.`
99
- } catch (error) {
100
- return `Sync failed: ${error.message} — the graph stays local; the approved preview remains retryable until it expires.`
101
- }
102
- }
103
-
104
- export async function tRebuildGraph(g, args, ctx) {
105
- if (!ctx.repoRoot) return 'Rebuild needs the repo root (not provided to this server).'
106
- const mode = ['no-tests', 'tests-only', 'full'].includes(args.mode)
107
- ? args.mode
108
- : ['no-tests', 'tests-only', 'full'].includes(g?.graphBuildMode) ? g.graphBuildMode : 'full'
109
- const precision = args.precision === 'off' ? 'off' : args.precision === 'lsp' ? 'lsp' : (g?.graphPrecisionMode || defaultPrecisionMode())
110
- const scope = String(args.scope || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
111
- if (scope) {
112
- const scopedDir = graphOutDirForModule(ctx.repoRoot, scope)
113
- const scoped = await buildGraphForRepo(ctx.repoRoot, {mode, scope, precision, outDir: scopedDir})
114
- if (!scoped || !scoped.ok) return `Scoped graph build failed: ${(scoped && scoped.error) || 'unknown error'}`
115
- return [
116
- `Built an isolated scoped graph for ${scope} (${mode}). ${scoped.log || ''}.`,
117
- `The active full-repository graph was not replaced, so this operation cannot report a false full→scope structural delta.`,
118
- `Scoped graph: ${join(scopedDir, 'graph.json')}`,
119
- ].join('\n')
120
- }
121
- // snapshot the outgoing state: bytes → graph.prev.json (for graph_diff later), struct → inline delta
122
- let prevBytes = null
123
- try { prevBytes = readFileSync(ctx.graphPath) } catch { /* first build — nothing to diff against */ }
124
- let before = null
125
- try { before = prevBytes ? JSON.parse(prevBytes.toString('utf8')) : null } catch { before = null }
126
- const res = await buildGraphForRepo(ctx.repoRoot, {mode, scope: '', precision, outDir: ctx.graphPath ? dirname(ctx.graphPath) : undefined})
127
- if (!res || !res.ok) return `Graph rebuild failed: ${(res && res.error) || 'unknown error'}`
128
- if (prevBytes) { try { writeFileSync(prevGraphPathFor(ctx.graphPath), prevBytes) } catch { /* snapshot is best-effort */ } }
129
- const fresh = ctx.reload() // refresh THIS server's in-memory graph so subsequent tool calls see the new graph
130
- let afterStatic = null
131
- try { afterStatic = JSON.parse(readFileSync(ctx.graphPath, 'utf8')) } catch { /* reload already reports the failure */ }
132
- const beforeMode = ['full', 'no-tests', 'tests-only'].includes(before?.graphBuildMode) ? before.graphBuildMode : 'full'
133
- const afterMode = ['full', 'no-tests', 'tests-only'].includes(afterStatic?.graphBuildMode) ? afterStatic.graphBuildMode : 'full'
134
- const delta = before && afterStatic
135
- ? beforeMode === afterMode
136
- ? formatGraphDiff(diffGraphs(before, afterStatic))
137
- : `Structural delta not computed: build mode changed from ${beforeMode} to ${afterMode}, so the node/edge universes are not comparable.`
138
- : null
139
- return [
140
- `Rebuilt the graph (${mode}). ${res.log || ''}. In-memory graph reloaded — graph tools now reflect it.`,
141
- delta
142
- ].filter(Boolean).join('\n\n')
143
- }
144
-
145
- // Retarget this server at ANOTHER local repository at runtime — one weavatrix registration serves any
146
- // repo. Loads <parent>/weavatrix-graphs/<name>/graph.json (the central layout graphs
147
- // always live in), building it first when missing. On a failed load the previous repo stays active.
148
- export async function tOpenRepo(g, args, ctx) {
149
- const requestedPath = String(args.path || '').trim()
150
- if (!requestedPath) return 'Provide "path" — an absolute path to a local repository folder.'
151
- if (!isAbsolute(requestedPath)) return 'open_repo requires an absolute repository path.'
152
- let repoPath
153
- try { repoPath = realpathSync.native(requestedPath) } catch { return `Path not found: ${requestedPath}` }
154
- try { if (!statSync(repoPath).isDirectory()) return `Not a directory: ${requestedPath}` } catch { return `Path not found: ${requestedPath}` }
155
- if (!existsSync(join(repoPath, '.git'))) return `Not a Git repository: ${requestedPath}`
156
- const graphPath = join(graphOutDirForRepo(repoPath), 'graph.json')
157
- const graphExists = existsSync(graphPath)
158
- const requestedMode = ['no-tests', 'tests-only', 'full'].includes(args.mode) ? args.mode : null
159
- const requestedPrecision = ['lsp', 'off'].includes(args.precision) ? args.precision : null
160
- let built = false
161
- let upgrade = false
162
- let schemaUpgrade = false
163
- let precisionUpgrade = false
164
- let savedMode = 'full'
165
- let savedPrecision = defaultPrecisionMode()
166
- if (graphExists) {
167
- try {
168
- const saved = JSON.parse(readFileSync(graphPath, 'utf8'))
169
- savedMode = ['no-tests', 'tests-only', 'full'].includes(saved.graphBuildMode) ? saved.graphBuildMode : 'full'
170
- savedPrecision = ['lsp', 'off'].includes(saved.graphPrecisionMode) ? saved.graphPrecisionMode : defaultPrecisionMode()
171
- schemaUpgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
172
- || !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
173
- || !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 5
174
- || !Number.isInteger(saved.reExportOccurrencesV) || saved.reExportOccurrencesV < 1
175
- || !Number.isInteger(saved.symbolSpacesV) || saved.symbolSpacesV < 1
176
- if (savedPrecision === 'lsp') {
177
- const overlay = readPrecisionOverlay(graphPath, saved)
178
- precisionUpgrade = !overlay || (typeof overlay.semanticInputFingerprint === 'string'
179
- && !precisionSemanticInputsMatch(overlay, repoPath, saved))
180
- }
181
- upgrade = schemaUpgrade || precisionUpgrade
182
- } catch {
183
- upgrade = true
184
- }
185
- }
186
- const modeMismatch = graphExists && requestedMode != null && requestedMode !== savedMode
187
- const precisionMismatch = graphExists && requestedPrecision != null && requestedPrecision !== savedPrecision
188
- if (!graphExists || upgrade || modeMismatch || precisionMismatch) {
189
- if (args.build === false) {
190
- if (modeMismatch && !upgrade) {
191
- return `The existing graph for ${repoPath} was built in ${savedMode}, but ${requestedMode} was requested. Re-call without build:false to rebuild it before switching.`
192
- }
193
- if (precisionMismatch && !upgrade) {
194
- return `The existing graph for ${repoPath} uses semantic precision ${savedPrecision}, but ${requestedPrecision} was requested. Re-call without build:false to rebuild it before switching.`
195
- }
196
- return upgrade
197
- ? precisionUpgrade && !schemaUpgrade
198
- ? `The existing graph for ${repoPath} lacks current revision-matched semantic precision evidence. Re-call without build:false to refresh it before switching.`
199
- : `The existing graph for ${repoPath} predates current typed-edge/provenance metadata. Re-call without build:false to upgrade it before switching.`
200
- : `No graph yet for ${repoPath} (expected at ${graphPath}). Re-call without build:false to build one — large repos can take minutes.`
201
- }
202
- const mode = requestedMode || (graphExists ? savedMode : 'full')
203
- const precision = requestedPrecision || (graphExists ? savedPrecision : defaultPrecisionMode())
204
- const res = await buildGraphForRepo(repoPath, {mode, scope: '', precision})
205
- if (!res || !res.ok) return `Graph build failed for ${repoPath}: ${(res && res.error) || 'unknown error'}`
206
- built = true
207
- }
208
- const prev = {graphPath: ctx.graphPath, repoRoot: ctx.repoRoot}
209
- ctx.graphPath = graphPath
210
- ctx.repoRoot = repoPath
211
- const loaded = ctx.reload()
212
- if (!loaded) {
213
- ctx.graphPath = prev.graphPath
214
- ctx.repoRoot = prev.repoRoot
215
- ctx.reload()
216
- return `Failed to load ${graphPath} — still targeting the previous repo (${prev.repoRoot || 'none'}).`
217
- }
218
- registerRepository({repoPath, graphDir: graphOutDirForRepo(repoPath), graphHome: graphHomeDir()})
219
- const buildNote = built ? (upgrade ? ' (graph upgraded to current edge/precision metadata)' : modeMismatch ? ' (graph rebuilt in the requested mode)' : precisionMismatch ? ' (semantic precision mode updated)' : ' (graph built fresh)') : ''
220
- return [
221
- `Opened ${repoPath}${buildNote}: ${loaded.nodes.length} nodes / ${loaded.links.length} edges. All tools now target this repo.`,
222
- `Graph: ${graphPath}`,
223
- `Build mode: ${loaded.graphBuildMode || 'full'}`,
224
- `Semantic precision: ${loaded.precision?.state || 'UNAVAILABLE'} (${loaded.precision?.verifiedEdges || 0} EXACT_LSP edge(s))`,
225
- ].join('\n')
226
- }
227
-
228
- // Sibling repos that already have a built graph in the central weavatrix-graphs folder — open_repo candidates.
229
- export function tListKnownRepos(g, args, ctx) {
230
- const root = graphHomeDir()
231
- const norm = (p) => String(p).replace(/[\\/]+/g, '/').toLowerCase()
232
- const rows = liveRepositoryRecords(root).map((record) => ({
233
- ...record,
234
- builtAt: statSync(join(record.graphDir, 'graph.json')).mtime.toISOString(),
235
- }))
236
- if (!rows.length) return `No registered graphs under ${root}. Build a repository once with open_repo or rebuild_graph.`
237
- return [
238
- `Known repositories (${rows.length}) in ${root}:`,
239
- ...rows.map((r) => ` ${norm(r.repoPath) === norm(ctx.repoRoot) ? '»' : ' '} ${r.label} [${r.repositoryId}] — graph built ${r.builtAt} (${r.repoPath})`),
240
- ].join('\n')
241
- }
242
-
243
- // ---- online tools ('online' capability group) -----------------------------------------------------
244
- // Scans and graph queries stay 100% offline. These two run a network call ONLY when explicitly
245
- // invoked; registering the server with a caps list that omits 'online' removes them entirely.
246
-
247
- // Refresh the local OSV advisory store for the current repo's lockfile-pinned packages. What leaves
248
- // the machine: package names + versions (that is what an OSV query IS) — never source code.
249
- export async function tRefreshAdvisories(g, args, ctx) {
250
- if (!ctx.repoRoot) return 'No repo root — cannot collect installed packages.'
251
- const {installed} = collectInstalled(ctx.repoRoot)
252
- if (!installed.length) return 'No pinned packages found in lockfiles (npm/yarn/pip/poetry/uv/go) — nothing to query.'
253
- const res = await refreshAdvisories({installed, repoKey: ctx.repoRoot, timeoutMs: Number(args.timeout_ms) || undefined})
254
- if (res.ok === false) return `Advisory refresh failed: ${res.error}`
255
- const meta = storeMeta()
256
- return [
257
- `Advisory store ${res.status === 'PARTIAL' ? 'partially refreshed' : 'refreshed'} from OSV.dev: ${res.queriedOk ?? res.queried}/${res.queried} package versions queried successfully, ${res.vulnerable} with known advisories (${res.fetched} advisory records fetched).`,
258
- res.unsupported ? `${res.unsupported} packages skipped (ecosystem not OSV-queryable — npm/PyPI/Go only).` : null,
259
- res.errors?.length ? `Partial: ${res.errors.length} request error(s), first: ${res.errors[0]}` : null,
260
- `Store: ${DEFAULT_STORE} (${meta.advisoryCount} advisories, fetched ${meta.fetchedAt}). run_audit now reflects it — offline.`,
261
- ].filter(Boolean).join('\n')
262
- }
263
-
264
- export async function tPullArchitectureContract(g, args, ctx) {
265
- if (!ctx.repoRoot || !ctx.graphPath) return 'No active repository graph — open_repo first.'
266
- const syncUrl = process.env.WEAVATRIX_SYNC_URL
267
- const token = process.env.WEAVATRIX_SYNC_TOKEN
268
- if (!syncUrl || !token) return 'Hosted architecture pull is not configured. Use the hosted profile with WEAVATRIX_SYNC_URL and WEAVATRIX_SYNC_TOKEN, or keep .weavatrix/architecture.json locally.'
269
- let url
270
- try {
271
- const configured = process.env.WEAVATRIX_ARCHITECTURE_URL || new URL('/api/v1/architecture-contract', syncUrl).toString()
272
- url = syncDestination(configured).url
273
- } catch (error) { return `Hosted architecture pull is not configured safely: ${error.message}.` }
274
- const registry = repositoryRecord(ctx.repoRoot, graphHomeDir())
275
- || registerRepository({repoPath: ctx.repoRoot, graphDir: graphOutDirForRepo(ctx.repoRoot), graphHome: graphHomeDir()})
276
- const timeoutMs = Math.min(120000, Math.max(1000, Number(args.timeout_ms) || 30000))
277
- try {
278
- const res = await fetch(url, {
279
- headers: {authorization: `Bearer ${token}`, 'x-weavatrix-repository-id': registry.repositoryId},
280
- signal: AbortSignal.timeout(timeoutMs),
281
- })
282
- const body = await res.json().catch(() => null)
283
- if (!res.ok) {
284
- const serverCode = String(body?.error?.code || body?.state || '').toUpperCase()
285
- const state = res.status === 401 ? 'AUTH_REQUIRED'
286
- : res.status === 403 ? 'FORBIDDEN'
287
- : res.status === 404 && ['REPOSITORY_NOT_FOUND', 'NOT_FOUND'].includes(serverCode) ? 'REPOSITORY_NOT_REGISTERED'
288
- : res.status === 404 ? 'ENDPOINT_NOT_FOUND'
289
- : res.status === 409 ? 'REPOSITORY_NOT_READY'
290
- : 'HTTP_ERROR'
291
- const next = state === 'REPOSITORY_NOT_REGISTERED'
292
- ? 'The Hosted endpoint is reachable, but this UUID has not completed a preview-confirmed repository sync.'
293
- : state === 'ENDPOINT_NOT_FOUND'
294
- ? 'The configured architecture endpoint does not exist; verify WEAVATRIX_ARCHITECTURE_URL or the URL derived from WEAVATRIX_SYNC_URL.'
295
- : res.status === 401 || res.status === 403
296
- ? 'Check the hosted token and repository access; no local cache entry was changed.'
297
- : res.status === 409
298
- ? 'Sync/register this repository first, then create or pull its target contract.'
299
- : 'Check the hosted service status and configured endpoint before retrying.'
300
- return toolResult(`Hosted architecture pull: ${state} (HTTP ${res.status}). ${next} The previous local contract cache remains unchanged.`, {
301
- state, httpStatus: res.status, serverCode: serverCode || null, cacheChanged: false,
302
- })
303
- }
304
- if (body?.state === 'NOT_CONFIGURED' || !body?.contract) return toolResult(
305
- 'Hosted target architecture is NOT_CONFIGURED. Repository sync and authentication succeeded; define and save a target in the Architecture editor first.',
306
- {state: 'NOT_CONFIGURED', repositoryId: registry.repositoryId, cacheChanged: false},
307
- )
308
- const stored = writeCachedArchitectureContract(ctx.graphPath, body.contract)
309
- return `Pulled target architecture ${stored.contract.name} (${stored.contract.style}, ${stored.contract.enforcement}) into the local graph cache. get_architecture_contract and verify_architecture now use it.`
310
- } catch (error) {
311
- return `Hosted architecture pull failed: ${error.message}; the previous local contract, if any, remains active.`
312
- }
313
- }
314
-
315
- async function buildSyncPreview(g, args, ctx) {
316
- const configuredUrl = process.env.WEAVATRIX_SYNC_URL
317
- if (!configuredUrl) {
318
- return 'Graph sync is not configured (optional feature). Set WEAVATRIX_SYNC_URL to the upload endpoint'
319
- + ' (and WEAVATRIX_SYNC_TOKEN for bearer auth) in the MCP registration env, then call again.'
320
- }
321
- let destination
322
- try { destination = syncDestination(configuredUrl) }
323
- catch (error) { return `Graph sync is not configured safely: ${error.message}.` }
324
- if (!g) return 'No graph loaded — build one first (open_repo / rebuild_graph).'
325
- let raw
326
- try {
327
- const size = statSync(ctx.graphPath).size
328
- if (size > MAX_SYNC_GRAPH_FILE_BYTES) {
329
- return `Cannot sync: graph.json is ${Math.ceil(size / 1024 / 1024)} MB; the local safety limit is ${MAX_SYNC_GRAPH_FILE_BYTES / 1024 / 1024} MB.`
330
- }
331
- raw = JSON.parse(readFileSync(ctx.graphPath, 'utf8'))
332
- } catch (e) { return `Cannot read ${ctx.graphPath}: ${e.message}` }
333
- const requestedVersion = Number(args.payload_version) === 2 ? 2 : 3
334
- let payload
335
- try {
336
- if (requestedVersion === 2) {
337
- payload = createSyncPayload(raw)
338
- } else {
339
- if (!ctx.repoRoot) return 'Cannot build evidence: no repository root is active.'
340
- if (graphStaleness(ctx).stale) {
341
- return 'Cannot sync evidence from a stale graph. Run rebuild_graph, then call sync_graph again.'
342
- }
343
- const evidence = await createEvidenceSnapshot({repoRoot: ctx.repoRoot, graph: raw})
344
- payload = createSyncPayloadV3(raw, evidence)
345
- }
346
- } catch (e) {
347
- return `Cannot sync: ${e.message}. Run rebuild_graph once before sync_graph.`
348
- }
349
- const body = JSON.stringify(payload)
350
- const bodyBytes = Buffer.byteLength(body)
351
- if (bodyBytes > MAX_SYNC_BODY_BYTES) {
352
- return `Cannot sync: payload is ${Math.ceil(bodyBytes / 1024)} KB; the hosted safety limit is ${MAX_SYNC_BODY_BYTES / 1024} KB. Narrow the graph scope and rebuild before retrying.`
353
- }
354
- const repoName = syncRepoLabel(ctx.repoRoot)
355
- const registry = repositoryRecord(ctx.repoRoot, graphHomeDir())
356
- || registerRepository({repoPath: ctx.repoRoot, graphDir: graphOutDirForRepo(ctx.repoRoot), graphHome: graphHomeDir()})
357
- const bodyHash = createHash('sha256').update(body).digest('hex')
358
- const token = confirmationToken({url: destination.url, repositoryId: registry.repositoryId, payloadVersion: payload.syncPayloadV, bodyHash})
359
- const preview = {
360
- token, url: destination.url, destinationDisplay: destination.display,
361
- graphPath: ctx.graphPath, repoName, repositoryId: registry.repositoryId,
362
- payload, body, bodyBytes, bodyHash, expiresAt: Date.now() + SYNC_PREVIEW_TTL_MS,
363
- }
364
- syncPreviews.set(token, preview)
365
- return preview
366
- }
367
-
368
- function previewResult(preview, {expired = false} = {}) {
369
- if (typeof preview === 'string') return preview
370
- return toolResult(syncPreviewText(preview, {expired}), {
371
- status: 'PREVIEW_READY', networkRequestMade: false,
372
- destination: preview.destinationDisplay, repository: preview.repoName,
373
- repositoryId: preview.repositoryId, payloadVersion: preview.payload.syncPayloadV,
374
- nodes: preview.payload.nodes.length, links: preview.payload.links.length,
375
- bodyBytes: preview.bodyBytes, bodyHash: preview.bodyHash,
376
- payloadFields: Object.keys(preview.payload).sort(), sections: syncSectionSummary(preview.payload),
377
- expiresAt: new Date(preview.expiresAt).toISOString(), confirmToken: preview.token,
378
- }, {completeness: {status: 'COMPLETE', reason: 'exact allowlisted payload serialized locally; no network request made'}})
379
- }
380
-
381
- // Build the exact upload body and approval token without any network request. Kept separate from the
382
- // mutating tool so safety layers and humans can approve a local preview without authorizing egress.
383
- export async function tPreviewSyncGraph(g, args, ctx) {
384
- pruneSyncPreviews()
385
- return previewResult(await buildSyncPreview(g, args, ctx))
386
- }
387
-
388
- // Push the exact payload previously approved through preview_sync. The old dry_run form remains a
389
- // compatibility alias for one release, but can never send unless dry_run:false and the token matches.
390
- export async function tSyncGraph(g, args, ctx) {
391
- if (args.dry_run !== false) {
392
- pruneSyncPreviews()
393
- const preview = await buildSyncPreview(g, args, ctx)
394
- if (typeof preview === 'string') return preview
395
- const suppliedToken = String(args.confirm_token || '').trim()
396
- const exact = suppliedToken && suppliedToken === preview.token
397
- return `${syncPreviewText(preview, {expired: !!suppliedToken && !exact})}${exact ? '\nConfirmation token recognized, but dry_run is still true; no network request was made.' : ''}`
398
- }
399
- const configuredUrl = process.env.WEAVATRIX_SYNC_URL
400
- if (!configuredUrl) return 'Graph sync is not configured (optional feature). Set WEAVATRIX_SYNC_URL first.'
401
- let destination
402
- try { destination = syncDestination(configuredUrl) }
403
- catch (error) { return `Graph sync is not configured safely: ${error.message}.` }
404
- pruneSyncPreviews()
405
- const suppliedToken = String(args.confirm_token || '').trim()
406
- const approved = suppliedToken ? syncPreviews.get(suppliedToken) : null
407
- if (approved && approved.expiresAt > Date.now()
408
- && approved.url === destination.url && approved.graphPath === ctx.graphPath) {
409
- const timeoutMs = Math.min(120000, Math.max(1000, Number(args.timeout_ms) || 30000))
410
- return sendSyncPreview(approved, timeoutMs)
411
- }
412
- const preview = await buildSyncPreview(g, args, ctx)
413
- return typeof preview === 'string' ? preview : syncPreviewText(preview, {expired: true})
414
- }
1
+ // Propagate catalog.mjs's cache-busting query to owner modules so development hot reload remains
2
+ // behaviorally identical to the former single-file implementation.
3
+ const version = new URL(import.meta.url).search
4
+ const load = (path) => import(new URL(`${path}${version}`, import.meta.url).href)
5
+ const [lifecycle, advisories, architecture, sync] = await Promise.all([
6
+ load('./actions/graph-lifecycle.mjs'),
7
+ load('./actions/advisories.mjs'),
8
+ load('./actions/hosted-architecture.mjs'),
9
+ load('./actions/graph-sync.mjs'),
10
+ ])
11
+
12
+ export const tOpenRepo = lifecycle.tOpenRepo
13
+ export const tListKnownRepos = lifecycle.tListKnownRepos
14
+ export const tRebuildGraph = lifecycle.tRebuildGraph
15
+ export const tRefreshAdvisories = advisories.tRefreshAdvisories
16
+ export const tPullArchitectureContract = architecture.tPullArchitectureContract
17
+ export const tPreviewSyncGraph = sync.tPreviewSyncGraph
18
+ export const tSyncGraph = sync.tSyncGraph
@@ -5,21 +5,14 @@ import {createPathClassifier, hasPathClass} from '../path-classification.js'
5
5
  import {detectRepoStack} from '../scan/discover.js'
6
6
  import {toolResult} from './tool-result.mjs'
7
7
 
8
- const PROVISIONAL_BUDGETS = Object.freeze({
9
- runtimeCycles: 0,
10
- maxFileLoc: 300,
11
- maxFunctionLoc: 120,
12
- maxCyclomatic: 15,
13
- maxModuleFiles: 80,
14
- minModuleCohesion: .5,
15
- maxModuleBoundaryRatio: .65,
16
- })
17
-
18
- const SOURCE_ROOTS = new Set(['src', 'app', 'lib', 'packages', 'services'])
19
- const PRODUCT_CODE_EXTENSIONS = new Set([
20
- '.cjs', '.cs', '.css', '.go', '.htm', '.html', '.java', '.js', '.jsx', '.less', '.mjs', '.py', '.pyi',
21
- '.rs', '.scss', '.ts', '.tsx',
8
+ const version = new URL(import.meta.url).search
9
+ const load = (path) => import(new URL(`${path}${version}`, import.meta.url).href)
10
+ const [starter, bootstrap] = await Promise.all([
11
+ load('./architecture-starter.mjs'),
12
+ load('./architecture-bootstrap.mjs'),
22
13
  ])
14
+ const {createArchitectureStarter, PROVISIONAL_BUDGETS} = starter
15
+ export const tBootstrapArchitecture = bootstrap.tBootstrapArchitecture
23
16
 
24
17
  const remediation = () => ({
25
18
  offlinePath: '.weavatrix/architecture.json',
@@ -41,62 +34,9 @@ function activeContract(ctx) {
41
34
  return loadArchitectureContract(ctx.repoRoot, ctx.graphPath)
42
35
  }
43
36
 
44
- const codeExtension = (file) => {
45
- const name = String(file || '').split('/').at(-1) || ''
46
- const dot = name.lastIndexOf('.')
47
- return dot >= 0 ? name.slice(dot).toLowerCase() : ''
48
- }
49
-
50
- function starterContract(g, repoRoot) {
51
- const classifier = createPathClassifier(repoRoot)
52
- const files = [...new Set((g?.nodes || [])
53
- .filter((node) => !String(node.id).includes('#'))
54
- .map((node) => String(node.source_file || node.id || '').replace(/\\/g, '/'))
55
- .filter((file) => file && PRODUCT_CODE_EXTENSIONS.has(codeExtension(file))))]
56
- .filter((file) => {
57
- const explanation = classifier.explain(file)
58
- return !explanation.excluded && !hasPathClass(
59
- explanation, 'test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp',
60
- )
61
- })
62
- const groups = new Map()
63
- for (const file of files) {
64
- const parts = file.split('/').filter(Boolean)
65
- let key, name, path
66
- if (parts.length === 1) {
67
- key = 'root-code'
68
- name = 'root code'
69
- path = file
70
- } else if (SOURCE_ROOTS.has(parts[0]) && parts.length === 2) {
71
- key = `${parts[0]}-root`
72
- name = `${parts[0]} (root files)`
73
- path = file
74
- } else {
75
- path = parts.slice(0, SOURCE_ROOTS.has(parts[0]) ? 2 : 1).join('/')
76
- key = path
77
- name = path
78
- }
79
- const group = groups.get(key) || {name, paths: new Set(), files: 0}
80
- group.paths.add(path)
81
- group.files += 1
82
- groups.set(key, group)
83
- }
84
- const components = [...groups].sort((a, b) => b[1].files - a[1].files || a[0].localeCompare(b[0])).slice(0, 80)
85
- .map(([key, group], index) => ({
86
- id: key.toLowerCase().replace(/[^a-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100) || `component-${index + 1}`,
87
- name: group.name,
88
- paths: [...group.paths].sort((a, b) => a.localeCompare(b)),
89
- }))
90
- return normalizeArchitectureContract({
91
- name: 'Proposed no-regressions baseline', style: 'custom', enforcement: 'ratchet', components,
92
- dependencyRules: [],
93
- budgets: PROVISIONAL_BUDGETS,
94
- technologies: {required: [], forbidden: []}, exceptions: [], ratchet: {baseline: {fingerprints: [], metrics: {}}},
95
- })
96
- }
97
-
98
37
  function notConfiguredResult(g, action, {includeStarter = false, repoRoot = null} = {}) {
99
- const starter = includeStarter ? starterContract(g, repoRoot) : null
38
+ const proposal = includeStarter ? createArchitectureStarter(g, repoRoot) : null
39
+ const starter = proposal?.contract || null
100
40
  const starterText = starter
101
41
  ? ` A source-free starter with ${starter.components.length} product-code territories is available in JSON output from this lookup.`
102
42
  : ''
@@ -107,8 +47,15 @@ function notConfiguredResult(g, action, {includeStarter = false, repoRoot = null
107
47
  state: 'NOT_CONFIGURED',
108
48
  remediation: remediation(),
109
49
  ...(starter ? {
110
- starterSummary: {components: starter.components.length, budgets: starter.budgets},
50
+ starterSummary: {
51
+ components: starter.components.length, budgets: starter.budgets,
52
+ observedDependencyDirections: proposal.observedDependencyProposals.length,
53
+ candidateBudgetsNotEnforced: proposal.budgetProposals.length,
54
+ },
111
55
  starterContract: starter,
56
+ observedDependencyProposals: proposal.observedDependencyProposals,
57
+ budgetProposals: proposal.budgetProposals,
58
+ starterMethodology: proposal.methodology,
112
59
  } : {}),
113
60
  })
114
61
  }
@@ -150,6 +97,7 @@ function provisionalPreflight(g, args, ctx) {
150
97
  }
151
98
 
152
99
  export function tGetArchitectureContract(g, args, ctx) {
100
+ if (args?.action === 'preview' || args?.action === 'approve') return tBootstrapArchitecture(g, args, ctx)
153
101
  const loaded = activeContract(ctx)
154
102
  if (!loaded.contract) {
155
103
  const text = loaded.error