weavatrix 0.2.12 → 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.
Files changed (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -1,480 +1,11 @@
1
- import {createHash} from 'node:crypto'
2
1
  import {readFileSync, statSync} from 'node:fs'
3
2
  import {createRepoBoundary} from '../repo-path.js'
4
- import {CAPS, STATE, compareText, safeToken} from './evidence-snapshot.common.mjs'
3
+ import {STATE} from './evidence-snapshot.common.mjs'
4
+ import {parseBunLock, parseJsonc} from './evidence/bun-lock-graph.mjs'
5
+ import {MAX_LOCKFILE_BYTES, emptyGraph} from './evidence/package-graph-common.mjs'
6
+ import {parsePackageLock} from './evidence/package-lock-graph.mjs'
5
7
 
6
8
  const LOCKFILES = ['npm-shrinkwrap.json', 'package-lock.json', 'bun.lock']
7
- const MAX_LOCKFILE_BYTES = 64 * 1024 * 1024
8
- const MAX_PACKAGE_RECORDS = 50_000
9
- const MAX_DEPENDENCY_DECLARATIONS = 200_000
10
- const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/i
11
- const PACKAGE_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+~-]*$/
12
-
13
- const packageVersion = (value) => {
14
- const version = safeToken(value, 128)
15
- return version && PACKAGE_VERSION.test(version) ? version : null
16
- }
17
-
18
- const emptyCount = () => ({total: 0, returned: 0, truncated: false})
19
- const emptyDeclarations = () => ({total: 0, resolved: 0, unresolved: 0, local: 0, optionalMissing: 0})
20
-
21
- function emptyGraph(state, reason, extras = {}) {
22
- return {
23
- state,
24
- ecosystem: 'npm',
25
- root: '(root)',
26
- ...extras,
27
- completeness: {
28
- nodes: emptyCount(),
29
- edges: emptyCount(),
30
- declarations: emptyDeclarations(),
31
- reasons: reason ? [reason] : [],
32
- },
33
- nodes: [],
34
- edges: [],
35
- }
36
- }
37
-
38
- function normalizeLockPath(value) {
39
- if (typeof value !== 'string' || value.length > 4096 || /[\u0000-\u001f\u007f]/.test(value)) return null
40
- const raw = value.replace(/\\/g, '/')
41
- if (/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(raw)) return null
42
- const normalized = raw.replace(/\/$/, '')
43
- if (normalized === '') return ''
44
- const parts = normalized.split('/')
45
- return parts.every((part) => part && part !== '.' && part !== '..') ? normalized : null
46
- }
47
-
48
- function packageNameFromPath(packagePath) {
49
- const match = packagePath.match(/(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)$/)
50
- const name = match?.[1]
51
- return name && name.length <= 256 && PACKAGE_NAME.test(name) ? name : null
52
- }
53
-
54
- function packageId(name, version, packagePath) {
55
- const location = createHash('sha256').update(packagePath).digest('hex').slice(0, 12)
56
- return `npm:${name}@${version}:${location}`
57
- }
58
-
59
- function parentInstallPath(packagePath) {
60
- const marker = packagePath.lastIndexOf('/node_modules/')
61
- return marker >= 0 ? packagePath.slice(0, marker) : ''
62
- }
63
-
64
- function dependencyDeclarations(record) {
65
- const declarations = new Map()
66
- const add = (values, kind, replace = false) => {
67
- if (!values || typeof values !== 'object' || Array.isArray(values)) return
68
- for (const name of Object.keys(values)) {
69
- if (name.length > 256 || !PACKAGE_NAME.test(name)) continue
70
- if (replace || !declarations.has(name)) declarations.set(name, kind)
71
- }
72
- }
73
- add(record?.dependencies, 'runtime')
74
- add(record?.devDependencies, 'dev')
75
- add(record?.optionalDependencies, 'optional', true)
76
- if (record?.peerDependencies && typeof record.peerDependencies === 'object' && !Array.isArray(record.peerDependencies)) {
77
- for (const name of Object.keys(record.peerDependencies)) {
78
- if (name.length > 256 || !PACKAGE_NAME.test(name) || declarations.has(name)) continue
79
- const optional = record?.peerDependenciesMeta?.[name]?.optional === true
80
- declarations.set(name, optional ? 'optional-peer' : 'peer')
81
- }
82
- }
83
- return [...declarations].map(([name, kind]) => ({name, kind}))
84
- .sort((a, b) => compareText(a.name, b.name) || compareText(a.kind, b.kind))
85
- }
86
-
87
- function resolveDependency(sourcePath, name, records, externalNodes) {
88
- let cursor = sourcePath
89
- const seen = new Set()
90
- while (!seen.has(cursor)) {
91
- seen.add(cursor)
92
- const candidate = cursor ? `${cursor}/node_modules/${name}` : `node_modules/${name}`
93
- const record = records.get(candidate)
94
- if (record) {
95
- if (record.link === true) return {kind: 'local'}
96
- const node = externalNodes.get(candidate)
97
- return node ? {kind: 'external', node} : {kind: 'unresolved'}
98
- }
99
- if (!cursor) break
100
- cursor = parentInstallPath(cursor)
101
- }
102
- return {kind: 'unresolved'}
103
- }
104
-
105
- function parseLock(lock, lockfile) {
106
- const lockfileVersion = Number(lock?.lockfileVersion || 0)
107
- const extras = {lockfile, lockfileVersion: Number.isFinite(lockfileVersion) ? Math.trunc(lockfileVersion) : 0}
108
- if (![2, 3].includes(lockfileVersion) || !lock?.packages || typeof lock.packages !== 'object' || Array.isArray(lock.packages)) {
109
- return emptyGraph(STATE.PARTIAL, 'PACKAGE_LOCK_V2_V3_REQUIRED', extras)
110
- }
111
-
112
- const allKeys = Object.keys(lock.packages).sort(compareText)
113
- const selectedKeys = allKeys.slice(0, MAX_PACKAGE_RECORDS)
114
- const reasons = []
115
- if (selectedKeys.length < allKeys.length) reasons.push('LOCKFILE_PACKAGE_RECORD_LIMIT_REACHED')
116
-
117
- const records = new Map()
118
- let invalidRecords = 0
119
- for (const rawPath of selectedKeys) {
120
- const packagePath = normalizeLockPath(rawPath)
121
- const record = lock.packages[rawPath]
122
- if (packagePath == null || !record || typeof record !== 'object' || Array.isArray(record)) {
123
- invalidRecords++
124
- continue
125
- }
126
- const insideNodeModules = packagePath.startsWith('node_modules/') || packagePath.includes('/node_modules/')
127
- if (insideNodeModules && !packageNameFromPath(packagePath)) {
128
- invalidRecords++
129
- continue
130
- }
131
- if (records.has(packagePath)) {
132
- invalidRecords++
133
- continue
134
- }
135
- records.set(packagePath, record)
136
- }
137
- if (invalidRecords > 0) reasons.push('INVALID_LOCKFILE_PACKAGE_RECORDS')
138
-
139
- const externalNodes = new Map()
140
- let invalidPackageVersions = 0
141
- for (const [packagePath, record] of records) {
142
- const name = packageNameFromPath(packagePath)
143
- const version = packageVersion(record.version)
144
- if (!name || record.link === true) continue
145
- if (!version) { invalidPackageVersions++; continue }
146
- externalNodes.set(packagePath, {
147
- id: packageId(name, version, packagePath),
148
- name,
149
- version,
150
- direct: false,
151
- dev: record.dev === true || record.devOptional === true,
152
- optional: record.optional === true || record.devOptional === true,
153
- peer: record.peer === true,
154
- })
155
- }
156
- if (invalidPackageVersions > 0) reasons.push('INVALID_LOCKFILE_PACKAGE_VERSIONS')
157
-
158
- const edgeMap = new Map()
159
- const declarations = emptyDeclarations()
160
- let declarationLimitReached = false
161
- const sources = [...records].filter(([packagePath]) =>
162
- packageNameFromPath(packagePath) == null || externalNodes.has(packagePath))
163
- .sort(([a], [b]) => compareText(a, b))
164
-
165
- outer: for (const [sourcePath, record] of sources) {
166
- const sourceNode = externalNodes.get(sourcePath)
167
- const sourceId = sourceNode?.id || '(root)'
168
- for (const declaration of dependencyDeclarations(record)) {
169
- if (declarations.total >= MAX_DEPENDENCY_DECLARATIONS) {
170
- declarationLimitReached = true
171
- break outer
172
- }
173
- declarations.total++
174
- const resolved = resolveDependency(sourcePath, declaration.name, records, externalNodes)
175
- if (resolved.kind === 'local') {
176
- declarations.local++
177
- continue
178
- }
179
- if (resolved.kind !== 'external') {
180
- if (declaration.kind === 'optional' || declaration.kind === 'optional-peer') declarations.optionalMissing++
181
- else declarations.unresolved++
182
- continue
183
- }
184
- declarations.resolved++
185
- if (sourceId === '(root)') resolved.node.direct = true
186
- if (sourceId === resolved.node.id) continue
187
- const edge = {from: sourceId, to: resolved.node.id, kind: declaration.kind}
188
- edgeMap.set(`${edge.from}\0${edge.to}\0${edge.kind}`, edge)
189
- }
190
- }
191
- if (declarationLimitReached) reasons.push('LOCKFILE_DEPENDENCY_DECLARATION_LIMIT_REACHED')
192
- if (declarations.unresolved > 0) reasons.push('UNRESOLVED_LOCKFILE_DEPENDENCIES')
193
-
194
- return finalizeGraph({externalNodes, edgeMap, declarations, reasons, lockfile, lockfileVersion})
195
- }
196
-
197
- function finalizeGraph({externalNodes, edgeMap, declarations, reasons, lockfile, lockfileVersion}) {
198
- const allNodes = [...externalNodes.values()].sort((a, b) =>
199
- Number(b.direct) - Number(a.direct) || compareText(a.name, b.name) ||
200
- compareText(a.version, b.version) || compareText(a.id, b.id))
201
- const nodes = allNodes.slice(0, CAPS.packageGraphNodes)
202
- const nodeIds = new Set(nodes.map((node) => node.id))
203
- const allEdges = [...edgeMap.values()].sort((a, b) =>
204
- compareText(a.from, b.from) || compareText(a.to, b.to) || compareText(a.kind, b.kind))
205
- const eligibleEdges = allEdges.filter((edge) =>
206
- (edge.from === '(root)' || nodeIds.has(edge.from)) && nodeIds.has(edge.to))
207
- const edges = eligibleEdges.slice(0, CAPS.packageGraphEdges)
208
- const nodesTruncated = nodes.length < allNodes.length
209
- const edgesTruncated = edges.length < allEdges.length
210
- if (nodesTruncated) reasons.push('PACKAGE_NODE_LIMIT_REACHED')
211
- if (edgesTruncated) reasons.push('PACKAGE_EDGE_LIMIT_REACHED')
212
-
213
- return {
214
- state: reasons.length ? STATE.PARTIAL : STATE.COMPLETE,
215
- ecosystem: 'npm',
216
- lockfile,
217
- lockfileVersion,
218
- root: '(root)',
219
- completeness: {
220
- nodes: {total: allNodes.length, returned: nodes.length, truncated: nodesTruncated},
221
- edges: {total: allEdges.length, returned: edges.length, truncated: edgesTruncated},
222
- declarations,
223
- reasons: [...new Set(reasons)].sort(compareText),
224
- },
225
- nodes,
226
- edges,
227
- }
228
- }
229
-
230
- // Bun >= 1.2 writes a text lockfile (bun.lock) as JSONC: JSON plus comments and
231
- // trailing commas. parseJsonc strips both (outside of strings) with no new deps.
232
- function parseJsonc(text) {
233
- const source = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text
234
- const out = []
235
- let i = 0
236
- const length = source.length
237
- const skipComment = (index) => {
238
- if (source[index] !== '/') return index
239
- if (source[index + 1] === '/') {
240
- let cursor = index + 2
241
- while (cursor < length && source[cursor] !== '\n') cursor++
242
- return cursor
243
- }
244
- if (source[index + 1] === '*') {
245
- let cursor = index + 2
246
- while (cursor < length && !(source[cursor] === '*' && source[cursor + 1] === '/')) cursor++
247
- return Math.min(cursor + 2, length)
248
- }
249
- return index
250
- }
251
- while (i < length) {
252
- const char = source[i]
253
- if (char === '"') {
254
- const start = i
255
- i++
256
- while (i < length && source[i] !== '"') i += source[i] === '\\' ? 2 : 1
257
- i = Math.min(i + 1, length)
258
- out.push(source.slice(start, i))
259
- continue
260
- }
261
- if (char === '/') {
262
- const next = skipComment(i)
263
- if (next !== i) { i = next; continue }
264
- }
265
- if (char === ',') {
266
- let ahead = i + 1
267
- while (ahead < length) {
268
- if (/\s/.test(source[ahead])) { ahead++; continue }
269
- const next = skipComment(ahead)
270
- if (next !== ahead) { ahead = next; continue }
271
- break
272
- }
273
- if (ahead < length && (source[ahead] === '}' || source[ahead] === ']')) { i++; continue }
274
- }
275
- out.push(char)
276
- i++
277
- }
278
- return JSON.parse(out.join(''))
279
- }
280
-
281
- // bun.lock package keys are slash-joined chains of package names describing the
282
- // hoisted install position, e.g. "make-dir/semver" (semver as resolved for
283
- // make-dir). Scoped names keep their own slash: "@scope/pkg/dep".
284
- function bunKeySegments(key) {
285
- if (typeof key !== 'string' || key.length === 0 || key.length > 4096 || /[\u0000-\u001f\u007f]/.test(key)) return null
286
- const parts = key.split('/')
287
- const segments = []
288
- for (let index = 0; index < parts.length; index++) {
289
- let segment = parts[index]
290
- if (segment.startsWith('@')) {
291
- if (index + 1 >= parts.length) return null
292
- segment = `${segment}/${parts[++index]}`
293
- }
294
- if (segment.length > 256 || !PACKAGE_NAME.test(segment)) return null
295
- segments.push(segment)
296
- }
297
- return segments
298
- }
299
-
300
- // Entry shape: "key": ["name@version", registry, {dependencies, peerDependencies,
301
- // optionalPeers, ...}, integrity]. Workspace members resolve to "name@workspace:path".
302
- function bunPackageRecord(entry) {
303
- if (!Array.isArray(entry) || typeof entry[0] !== 'string' || entry[0].length > 1024) return null
304
- const at = entry[0].lastIndexOf('@')
305
- if (at <= 0) return null
306
- const name = entry[0].slice(0, at)
307
- if (name.length > 256 || !PACKAGE_NAME.test(name)) return null
308
- const rawVersion = entry[0].slice(at + 1)
309
- const meta = entry.slice(1).find((value) => value && typeof value === 'object' && !Array.isArray(value)) || {}
310
- return {name, rawVersion, workspace: rawVersion.startsWith('workspace:'), meta}
311
- }
312
-
313
- // Adapts a bun dependency map ({dependencies, devDependencies, optionalDependencies,
314
- // peerDependencies, optionalPeers: [names]}) to the npm record shape consumed by
315
- // dependencyDeclarations, so declaration kinds and precedence stay identical.
316
- function bunDeclarationRecord(meta) {
317
- if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {}
318
- const peerDependenciesMeta = {}
319
- if (Array.isArray(meta.optionalPeers)) {
320
- for (const name of meta.optionalPeers) {
321
- if (typeof name === 'string') peerDependenciesMeta[name] = {optional: true}
322
- }
323
- }
324
- return {
325
- dependencies: meta.dependencies,
326
- devDependencies: meta.devDependencies,
327
- optionalDependencies: meta.optionalDependencies,
328
- peerDependencies: meta.peerDependencies,
329
- peerDependenciesMeta,
330
- }
331
- }
332
-
333
- // Mirrors bun resolution: the most specific override key wins ("a/b/name"),
334
- // falling back segment by segment to the hoisted top-level key ("name").
335
- function resolveBunDependency(sourceSegments, name, records) {
336
- for (let depth = sourceSegments.length; depth >= 0; depth--) {
337
- const key = [...sourceSegments.slice(0, depth), name].join('/')
338
- const record = records.get(key)
339
- if (record) return {key, record}
340
- }
341
- return null
342
- }
343
-
344
- // bun.lock does not persist npm's per-package dev/optional/peer flags, so they are
345
- // approximated from graph reachability the same way npm derives them: a package is
346
- // dev when unreachable from the root without dev edges, optional (or peer) when
347
- // unreachable without optional (or peer) edges.
348
- function applyBunReachabilityFlags(nodes, edges) {
349
- const adjacency = new Map()
350
- for (const edge of edges) {
351
- if (!adjacency.has(edge.from)) adjacency.set(edge.from, [])
352
- adjacency.get(edge.from).push(edge)
353
- }
354
- const reach = (excluded) => {
355
- const seen = new Set(['(root)'])
356
- const queue = ['(root)']
357
- while (queue.length) {
358
- for (const edge of adjacency.get(queue.pop()) || []) {
359
- if (excluded.has(edge.kind) || seen.has(edge.to)) continue
360
- seen.add(edge.to)
361
- queue.push(edge.to)
362
- }
363
- }
364
- return seen
365
- }
366
- const withoutDev = reach(new Set(['dev']))
367
- const withoutOptional = reach(new Set(['optional', 'optional-peer']))
368
- const withoutPeer = reach(new Set(['peer', 'optional-peer']))
369
- for (const node of nodes) {
370
- node.dev = !withoutDev.has(node.id)
371
- node.optional = !withoutOptional.has(node.id)
372
- node.peer = !withoutPeer.has(node.id)
373
- }
374
- }
375
-
376
- function parseBunLock(lock, lockfile) {
377
- const lockfileVersion = Number(lock?.lockfileVersion)
378
- const extras = {lockfile, lockfileVersion: Number.isFinite(lockfileVersion) ? Math.trunc(lockfileVersion) : 0}
379
- if (!lock?.packages || typeof lock.packages !== 'object' || Array.isArray(lock.packages)) {
380
- return emptyGraph(STATE.PARTIAL, 'BUN_LOCK_PACKAGES_REQUIRED', extras)
381
- }
382
-
383
- const allKeys = Object.keys(lock.packages).sort(compareText)
384
- const selectedKeys = allKeys.slice(0, MAX_PACKAGE_RECORDS)
385
- const reasons = []
386
- if (selectedKeys.length < allKeys.length) reasons.push('LOCKFILE_PACKAGE_RECORD_LIMIT_REACHED')
387
-
388
- const records = new Map()
389
- let invalidRecords = 0
390
- for (const key of selectedKeys) {
391
- const segments = bunKeySegments(key)
392
- const record = segments ? bunPackageRecord(lock.packages[key]) : null
393
- if (!record) {
394
- invalidRecords++
395
- continue
396
- }
397
- records.set(key, {...record, segments})
398
- }
399
- if (invalidRecords > 0) reasons.push('INVALID_LOCKFILE_PACKAGE_RECORDS')
400
-
401
- // Node ids reuse packageId over the bun install-position key, so ids keep the
402
- // exact "npm:name@version:hash12" shape the npm parser and sync sanitizer use,
403
- // and node.name stays the bare package name that hosted BFS grounding matches.
404
- const externalNodes = new Map()
405
- let invalidPackageVersions = 0
406
- for (const [key, record] of records) {
407
- if (record.workspace) continue
408
- const version = packageVersion(record.rawVersion)
409
- if (!version) { invalidPackageVersions++; continue }
410
- externalNodes.set(key, {
411
- id: packageId(record.name, version, key),
412
- name: record.name,
413
- version,
414
- direct: false,
415
- dev: false,
416
- optional: false,
417
- peer: false,
418
- })
419
- }
420
- if (invalidPackageVersions > 0) reasons.push('INVALID_LOCKFILE_PACKAGE_VERSIONS')
421
-
422
- const workspaces = lock?.workspaces && typeof lock.workspaces === 'object' && !Array.isArray(lock.workspaces)
423
- ? lock.workspaces
424
- : null
425
- if (!workspaces) reasons.push('BUN_LOCK_WORKSPACES_MISSING')
426
-
427
- // Sources: every workspace member acts as '(root)' (matching how the npm parser
428
- // treats workspace-local records), then every resolved external package.
429
- const sources = []
430
- for (const path of Object.keys(workspaces || {}).sort(compareText)) {
431
- const meta = workspaces[path]
432
- if (!meta || typeof meta !== 'object' || Array.isArray(meta)) continue
433
- const workspaceName = path === '' ? null : safeToken(meta.name)
434
- const segments = workspaceName && records.has(workspaceName) ? [workspaceName] : []
435
- sources.push({sourceId: '(root)', segments, declarationRecord: bunDeclarationRecord(meta)})
436
- }
437
- for (const [key, record] of [...records].sort(([a], [b]) => compareText(a, b))) {
438
- const node = externalNodes.get(key)
439
- if (!node) continue
440
- sources.push({sourceId: node.id, segments: record.segments, declarationRecord: bunDeclarationRecord(record.meta)})
441
- }
442
-
443
- const edgeMap = new Map()
444
- const declarations = emptyDeclarations()
445
- let declarationLimitReached = false
446
- outer: for (const source of sources) {
447
- for (const declaration of dependencyDeclarations(source.declarationRecord)) {
448
- if (declarations.total >= MAX_DEPENDENCY_DECLARATIONS) {
449
- declarationLimitReached = true
450
- break outer
451
- }
452
- declarations.total++
453
- const resolved = resolveBunDependency(source.segments, declaration.name, records)
454
- if (resolved?.record.workspace) {
455
- declarations.local++
456
- continue
457
- }
458
- const node = resolved ? externalNodes.get(resolved.key) : null
459
- if (!node) {
460
- if (declaration.kind === 'optional' || declaration.kind === 'optional-peer') declarations.optionalMissing++
461
- else declarations.unresolved++
462
- continue
463
- }
464
- declarations.resolved++
465
- if (source.sourceId === '(root)') node.direct = true
466
- if (source.sourceId === node.id) continue
467
- const edge = {from: source.sourceId, to: node.id, kind: declaration.kind}
468
- edgeMap.set(`${edge.from}\0${edge.to}\0${edge.kind}`, edge)
469
- }
470
- }
471
- if (declarationLimitReached) reasons.push('LOCKFILE_DEPENDENCY_DECLARATION_LIMIT_REACHED')
472
- if (declarations.unresolved > 0) reasons.push('UNRESOLVED_LOCKFILE_DEPENDENCIES')
473
-
474
- applyBunReachabilityFlags(externalNodes.values(), edgeMap.values())
475
-
476
- return finalizeGraph({externalNodes, edgeMap, declarations, reasons, lockfile, lockfileVersion: extras.lockfileVersion})
477
- }
478
9
 
479
10
  export function buildPackageDependencyGraph(repoRoot) {
480
11
  const boundary = createRepoBoundary(repoRoot)
@@ -496,7 +27,7 @@ export function buildPackageDependencyGraph(repoRoot) {
496
27
  }
497
28
  const raw = readFileSync(selected.path, 'utf8')
498
29
  if (selected.lockfile === 'bun.lock') return parseBunLock(parseJsonc(raw), selected.lockfile)
499
- return parseLock(JSON.parse(raw), selected.lockfile)
30
+ return parsePackageLock(JSON.parse(raw), selected.lockfile)
500
31
  } catch {
501
32
  return emptyGraph(STATE.ERROR, 'PACKAGE_LOCK_READ_ERROR', {lockfile: selected.lockfile})
502
33
  }
@@ -0,0 +1,111 @@
1
+ import {readFileSync} from 'node:fs'
2
+ import {isStructuralRelation} from '../../graph/relations.js'
3
+ import {edgeProvenance} from '../../graph/edge-provenance.js'
4
+ import {mergePrecisionOverlay, precisionSemanticInputsMatch, readPrecisionOverlay} from '../../precision/lsp-overlay.js'
5
+
6
+ export function loadGraph(path, {repoRoot = null} = {}) {
7
+ const saved = JSON.parse(readFileSync(path, 'utf8'))
8
+ const overlay = readPrecisionOverlay(path, saved)
9
+ const safeOverlay = repoRoot && typeof overlay?.semanticInputFingerprint === 'string'
10
+ && !precisionSemanticInputsMatch(overlay, repoRoot, saved) ? null : overlay
11
+ const raw = mergePrecisionOverlay(saved, safeOverlay)
12
+ const nodes = Array.isArray(raw.nodes) ? raw.nodes : []
13
+ const links = Array.isArray(raw.links) ? raw.links : []
14
+ const byId = new Map()
15
+ const byLabel = new Map()
16
+ for (const n of nodes) {
17
+ if (!n || n.id == null) continue
18
+ byId.set(String(n.id), n)
19
+ const key = String(n.label ?? n.id).toLowerCase()
20
+ if (!byLabel.has(key)) byLabel.set(key, [])
21
+ byLabel.get(key).push(n)
22
+ }
23
+ const out = new Map()
24
+ const inn = new Map()
25
+ const push = (map, k, v) => {
26
+ if (!map.has(k)) map.set(k, [])
27
+ map.get(k).push(v)
28
+ }
29
+ for (const e of links) {
30
+ if (!e || e.source == null || e.target == null) continue
31
+ const s = String(e.source), t = String(e.target)
32
+ const metadata = {
33
+ relation: e.relation,
34
+ confidence: e.confidence,
35
+ provenance: edgeProvenance(e),
36
+ ...(e.typeOnly === true ? {typeOnly: true} : {}),
37
+ ...(e.compileOnly === true ? {compileOnly: true} : {}),
38
+ ...(Number.isInteger(e.line) ? {line: e.line} : {}),
39
+ ...(typeof e.specifier === 'string' ? {specifier: e.specifier} : {}),
40
+ ...(e.barrelProxy === true ? {barrelProxy: true} : {}),
41
+ ...(e.semanticOrigin === true ? {semanticOrigin: true} : {}),
42
+ ...(typeof e.viaBarrel === 'string' ? {viaBarrel: e.viaBarrel} : {}),
43
+ }
44
+ push(out, s, {id: t, ...metadata})
45
+ push(inn, t, {id: s, ...metadata})
46
+ }
47
+ return {
48
+ nodes, links, byId, byLabel, out, inn,
49
+ repoBoundaryV: Number(raw.repoBoundaryV) || 0,
50
+ edgeTypesV: Number(raw.edgeTypesV) || 0,
51
+ edgeProvenanceV: Number(raw.edgeProvenanceV) || 0,
52
+ barrelResolutionV: Number(raw.barrelResolutionV) || 0,
53
+ reExportOccurrencesV: Number(raw.reExportOccurrencesV) || 0,
54
+ symbolSpacesV: Number(raw.symbolSpacesV) || 0,
55
+ reExportOccurrences: Array.isArray(raw.reExportOccurrences) ? raw.reExportOccurrences : [],
56
+ jsExportRecords: raw.jsExportRecords && typeof raw.jsExportRecords === 'object' ? raw.jsExportRecords : {},
57
+ extractorSchemaV: Number(raw.extractorSchemaV) || 0,
58
+ extImportsV: Number(raw.extImportsV) || 0,
59
+ complexityV: Number(raw.complexityV) || 0,
60
+ physicalFileLocV: Number(raw.physicalFileLocV) || 0,
61
+ graphBuildMode: ['full', 'no-tests', 'tests-only'].includes(raw.graphBuildMode) ? raw.graphBuildMode : 'full',
62
+ graphBuildScope: typeof raw.graphBuildScope === 'string' ? raw.graphBuildScope : null,
63
+ graphRevision: typeof raw.graphRevision === 'string' ? raw.graphRevision : null,
64
+ repositoryFreshnessProbeV: Number(raw.repositoryFreshnessProbeV) || 0,
65
+ repositoryFreshnessBuilderSchemaV: Number(raw.repositoryFreshnessBuilderSchemaV) || 0,
66
+ repositoryFreshnessBuilderVersion: typeof raw.repositoryFreshnessBuilderVersion === 'string' ? raw.repositoryFreshnessBuilderVersion : null,
67
+ repositoryFreshnessProbe: typeof raw.repositoryFreshnessProbe === 'string' ? raw.repositoryFreshnessProbe : null,
68
+ repositoryFreshnessMode: typeof raw.repositoryFreshnessMode === 'string' ? raw.repositoryFreshnessMode : null,
69
+ graphPrecisionMode: raw.graphPrecisionMode === 'off' ? 'off' : 'lsp',
70
+ precisionOverlayV: Number(raw.precisionOverlayV) || 0,
71
+ precision: raw.precision || null,
72
+ }
73
+ }
74
+
75
+ export const isSymbol = (id) => String(id).includes('#')
76
+ export const labelOf = (g, id) => {
77
+ const n = g.byId.get(String(id))
78
+ return n ? String(n.label ?? n.id) : String(id)
79
+ }
80
+ export const connList = (list) => (list || []).filter((e) => !isStructuralRelation(e.relation) && e.barrelProxy !== true)
81
+ export const degreeOf = (g, id) => connList(g.out.get(id)).length + connList(g.inn.get(id)).length
82
+ export const uniqueConnCount = (list) => new Set(connList(list).map((e) => String(e.id))).size
83
+
84
+ export function resolveNodeInfo(g, query) {
85
+ const q = String(query ?? '').trim()
86
+ if (!q) return {node: null, matches: 0, alternates: []}
87
+ if (g.byId.has(q)) return {node: g.byId.get(q), matches: 1, alternates: []}
88
+ const exactLabel = g.byLabel.get(q.toLowerCase())
89
+ if (exactLabel?.length) return pickBest(g, exactLabel)
90
+ const hits = []
91
+ for (const n of g.nodes) {
92
+ if (String(n.id).toLowerCase().includes(q.toLowerCase()) || String(n.label ?? '').toLowerCase().includes(q.toLowerCase())) hits.push(n)
93
+ if (hits.length > 500) break
94
+ }
95
+ return hits.length ? pickBest(g, hits) : {node: null, matches: 0, alternates: []}
96
+ }
97
+
98
+ export const resolveNode = (g, query) => resolveNodeInfo(g, query).node
99
+ const bestByDegree = (g, list) => list.reduce((best, n) => (degreeOf(g, n.id) > degreeOf(g, best.id) ? n : best), list[0])
100
+ function pickBest(g, list) {
101
+ const node = bestByDegree(g, list)
102
+ const alternates = list.filter((n) => n !== node).sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id))
103
+ .slice(0, 4).map((n) => `${n.label ?? n.id} [${n.id}]`)
104
+ return {node, matches: list.length, alternates}
105
+ }
106
+
107
+ export function ambiguityNote(query, info) {
108
+ if (!info.node || info.matches <= 1) return null
109
+ const more = info.matches - 1 - info.alternates.length
110
+ return `Note: "${query}" matched ${info.matches} nodes; using the best-connected. Others: ${info.alternates.join(', ')}${more > 0 ? ` (+${more} more)` : ''}`
111
+ }