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,406 +1,11 @@
1
- // Defense-in-depth wire sanitizer for evidence snapshots. The local evidence builder is trusted code,
2
- // but its inputs (graph.json, manifests and cached advisory metadata) are not. Reconstruct every field
3
- // from a small schema here so source text, absolute paths and future analyzer fields cannot hitchhike.
1
+ // Defense-in-depth wire sanitizer for evidence snapshots. Each section owns its
2
+ // allowlist; this facade only assembles and hashes the versioned wire envelope.
4
3
  import {createHash} from 'node:crypto'
5
-
6
- const STATES = new Set(['COMPLETE', 'PARTIAL', 'NOT_CHECKED', 'NOT_APPLICABLE', 'ERROR'])
7
- const VERDICTS = new Set(['PASS', 'FAIL', 'UNKNOWN'])
8
- const SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'info'])
9
- const CONFIDENCE = new Set(['high', 'medium', 'low'])
10
- const CATEGORIES = new Set(['unused', 'structure', 'vulnerability', 'malware'])
11
- const CHECK_KEYS = ['osv', 'malware']
12
- const PACKAGE_DEPENDENCY_KINDS = new Set(['runtime', 'dev', 'optional', 'peer', 'optional-peer'])
13
- const CONTROL = /[\u0000-\u001f\u007f]/
14
- const ABSOLUTE_PATH_FRAGMENT = /(?:^|[\/\s"'`(=])[a-z]:[\\/]|(?:^|[\s"'`(=])(?:\\\\[^\\/\s]+(?:[\\/]|$)|file:(?:\/\/)?[\\/]|\/(?!\/)[^\s])/i
15
- const TOKEN = /^[\p{L}\p{N}_.:@+\-#$<>()\[\],]+$/u
16
- const PACKAGE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/i
17
- const CAPS = Object.freeze({
18
- modules: 500, dependencies: 2000, findings: 500, hotspots: 250, badges: 100,
19
- packages: 5000, usage: 1000, files: 20, packageGraphNodes: 5000, packageGraphEdges: 20000,
20
- duplicateGroups: 100, duplicateMembers: 12, divergenceCandidates: 100,
21
- })
22
- const DUPLICATE_THRESHOLDS = Object.freeze({
23
- clones: Object.freeze({mode: 'renamed', minSimilarityPercent: 80, minTokens: 50}),
24
- divergence: Object.freeze({sameName: true, maxSimilarityPercent: 45, minTokens: 50, maxImplementationsPerName: 12}),
25
- })
26
- const PACKAGE_SOURCES = new Set(['package-lock', 'yarn-lock', 'requirements', 'venv', 'poetry-lock', 'uv-lock', 'pipfile-lock', 'go-sum', 'go-mod', 'node_modules'])
27
-
28
- const int = (value) => Number.isFinite(value) && value >= 0 ? Math.trunc(value) : 0
29
- const bool = (value) => value === true
30
- const text = (value, max = 256) => typeof value === 'string' && value.length > 0 && value.length <= max && !CONTROL.test(value) ? value : undefined
31
- const token = (value, max = 256) => { const valueText = text(value, max); return valueText && TOKEN.test(valueText) ? valueText : undefined }
32
- const privacySafeText = (value, max = 256) => { const valueText = text(value, max); return valueText && !ABSOLUTE_PATH_FRAGMENT.test(valueText) ? valueText : undefined }
33
- const packageName = (value) => { const valueText = text(value, 256); return valueText && PACKAGE.test(valueText) ? valueText : undefined }
34
- const packageVersion = (value) => { const valueText = text(value, 128); return valueText && /^[A-Za-z0-9][A-Za-z0-9._+~-]*$/.test(valueText) ? valueText : undefined }
35
- const state = (value) => STATES.has(value) ? value : 'ERROR'
36
- const verdict = (value) => VERDICTS.has(value) ? value : 'UNKNOWN'
37
- const compare = (a, b) => String(a).localeCompare(String(b), 'en')
38
-
39
- function path(value, max = 4096) {
40
- const raw = text(value, max)
41
- if (!raw) return undefined
42
- const normalized = raw.replace(/\\/g, '/')
43
- if (/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(normalized)) return undefined
44
- const parts = normalized.split('/')
45
- return parts.length && parts.every((part) => part && part !== '.' && part !== '..') ? normalized : undefined
46
- }
47
-
48
- function graphId(value) {
49
- const id = text(value, 4096)
50
- if (!id) return undefined
51
- const hash = id.indexOf('#')
52
- const file = hash < 0 ? id : id.slice(0, hash)
53
- const safeFile = path(file)
54
- if (!safeFile) return undefined
55
- if (hash < 0) return safeFile
56
- const suffix = id.slice(hash)
57
- return suffix.length <= 512 && /^#[^\\/\s\u0000-\u001f\u007f]{1,511}$/u.test(suffix) ? `${safeFile}${suffix}` : undefined
58
- }
59
-
60
- function moduleId(value) { return value === '(root)' ? value : path(value) }
61
- function set(out, key, value) { if (value !== undefined) out[key] = value }
62
-
63
- function stableStringify(value) {
64
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`
65
- if (value && typeof value === 'object') return `{${Object.keys(value).sort(compare).map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`
66
- return JSON.stringify(value)
67
- }
68
-
69
- function list(values, cap, mapper, sorter) {
70
- const all = (Array.isArray(values) ? values : []).map(mapper).filter(Boolean).sort(sorter)
71
- return {items: all.slice(0, cap), total: all.length, truncated: all.length > cap}
72
- }
73
-
74
- function count(value, fallbackTotal, returned) {
75
- const total = Math.max(int(value?.total), fallbackTotal)
76
- return {
77
- total,
78
- returned,
79
- truncated: bool(value?.truncated) || total > returned,
80
- }
81
- }
82
-
83
- function reasons(values) {
84
- return [...new Set((Array.isArray(values) ? values : []).map((value) => token(value, 96)).filter(Boolean))].sort(compare).slice(0, 32)
85
- }
86
-
87
- function finding(value) {
88
- if (!value || typeof value !== 'object') return null
89
- const id = text(value.id, 64), category = token(value.category, 32), rule = token(value.rule, 64), severity = token(value.severity, 16)
90
- if (!id || !/^[a-f0-9]{8,64}$/i.test(id) || !CATEGORIES.has(category) || !rule || !SEVERITIES.has(severity)) return null
91
- const out = {id, category, rule, severity}
92
- if (CONFIDENCE.has(value.confidence)) out.confidence = value.confidence
93
- set(out, 'file', path(value.file)); set(out, 'line', Number.isFinite(value.line) && value.line >= 0 ? Math.trunc(value.line) : undefined)
94
- set(out, 'symbol', privacySafeText(value.symbol)); set(out, 'package', packageName(value.package)); set(out, 'version', token(value.version, 128)); set(out, 'graphNodeId', graphId(value.graphNodeId))
95
- return out
96
- }
97
-
98
- function moduleFact(value) {
99
- const id = moduleId(value?.id || value?.name)
100
- return id ? {id, fileCount: int(value.fileCount), nodeCount: int(value.nodeCount), symbolCount: int(value.symbolCount)} : null
101
- }
102
-
103
- function dependency(value) {
104
- const from = moduleId(value?.from), to = moduleId(value?.to)
105
- return from && to && from !== to ? {from, to, count: int(value.count)} : null
106
- }
107
-
108
- function cycleFact(value) {
109
- const id = text(value?.id, 64), kind = value?.kind
110
- if (!id || !/^[a-f0-9]{16,64}$/i.test(id) || !['runtime', 'compile-time'].includes(kind)) return null
111
- const members = [...new Set((value.members || []).map((item) => path(item)).filter(Boolean))].sort(compare).slice(0, 200)
112
- const representativePath = (value.representativePath || []).map((item) => path(item)).filter(Boolean).slice(0, 201)
113
- if (members.length < 2 || representativePath.length < 2) return null
114
- return {id, kind, size: Math.max(int(value.size), members.length), members, membersTruncated: bool(value.membersTruncated) || int(value.size) > members.length, representativePath}
115
- }
116
-
117
- function boundaryFact(value) {
118
- const ruleId = token(value?.ruleId, 96), from = path(value?.from), to = path(value?.to)
119
- if (!ruleId || !from || !to || !['forbidden', 'allowedOnly'].includes(value?.kind) || !SEVERITIES.has(value?.severity)) return null
120
- return {kind: value.kind, ruleId, severity: value.severity, from, to}
121
- }
122
-
123
- function sanitizeArchitecture(value) {
124
- const modules = list(value?.modules, CAPS.modules, moduleFact, (a, b) => compare(a.id, b.id))
125
- const dependencies = {}
126
- let truncated = modules.truncated
127
- const dependencyMeta = {}
128
- for (const kind of ['runtime', 'typeOnly', 'compileOnly']) {
129
- const result = list(value?.dependencies?.[kind], CAPS.dependencies, dependency, (a, b) => compare(a.from, b.from) || compare(a.to, b.to))
130
- dependencies[kind] = result.items
131
- const completenessKey = `${kind}Dependencies`
132
- dependencyMeta[completenessKey] = count(value?.completeness?.[completenessKey], result.total, result.items.length)
133
- truncated ||= result.truncated
134
- }
135
- const cycles = list(value?.cycles, CAPS.findings, cycleFact, (a, b) => compare(a.kind, b.kind) || b.size - a.size || compare(a.id, b.id))
136
- const boundaries = list(value?.boundaryViolations, CAPS.findings, boundaryFact, (a, b) => compare(a.ruleId, b.ruleId) || compare(a.from, b.from) || compare(a.to, b.to))
137
- truncated ||= cycles.truncated || boundaries.truncated
138
- const outState = state(value?.state)
139
- return {
140
- state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState,
141
- verdict: verdict(value?.verdict),
142
- completeness: {modules: count(value?.completeness?.modules, modules.total, modules.items.length), ...dependencyMeta, cycles: count(value?.completeness?.cycles, cycles.total, cycles.items.length), boundaryViolations: count(value?.completeness?.boundaryViolations, boundaries.total, boundaries.items.length), reasons: reasons(value?.completeness?.reasons)},
143
- modules: modules.items, dependencies, cycles: cycles.items, boundaryViolations: boundaries.items,
144
- boundaryRulesState: state(value?.boundaryRulesState),
145
- }
146
- }
147
-
148
- function duplicateEvidenceId(value) {
149
- const id = text(value, 64)
150
- return id && /^[a-f0-9]{24,64}$/i.test(id) ? id : undefined
151
- }
152
-
153
- function duplicateMember(value) {
154
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null
155
- const file = path(value.file)
156
- const startLine = int(value.startLine), endLine = int(value.endLine), tokens = int(value.tokens)
157
- if (!file || startLine < 1 || endLine < startLine || tokens < DUPLICATE_THRESHOLDS.clones.minTokens) return null
158
- const out = {file, startLine, endLine, tokens}
159
- const nodeId = graphId(value.graphNodeId)
160
- if (nodeId?.startsWith(`${file}#`)) out.graphNodeId = nodeId
161
- return out
162
- }
163
-
164
- function compareDuplicateMember(a, b) {
165
- return compare(a.file, b.file) || a.startLine - b.startLine || a.endLine - b.endLine ||
166
- compare(a.graphNodeId || '', b.graphNodeId || '')
167
- }
168
-
169
- function duplicateMembers(values, cap = CAPS.duplicateMembers) {
170
- const raw = Array.isArray(values) ? values : []
171
- const unique = new Map()
172
- for (const value of raw) {
173
- const item = duplicateMember(value)
174
- if (!item) continue
175
- const key = `${item.file}\0${item.startLine}\0${item.endLine}\0${item.graphNodeId || ''}`
176
- unique.set(key, item)
177
- }
178
- const all = [...unique.values()].sort(compareDuplicateMember)
179
- return {items: all.slice(0, cap), total: all.length, invalid: raw.length - all.length, truncated: all.length > cap}
180
- }
181
-
182
- function cloneGroup(value) {
183
- const id = duplicateEvidenceId(value?.id)
184
- const members = duplicateMembers(value?.members)
185
- const rawStrongestSimilarity = Number(value?.strongestSimilarity)
186
- const rawWeakestLinkedSimilarity = Number(value?.weakestLinkedSimilarity)
187
- const strongestSimilarity = Math.trunc(rawStrongestSimilarity)
188
- const weakestLinkedSimilarity = Math.trunc(rawWeakestLinkedSimilarity)
189
- if (!id || members.items.length < 2 || strongestSimilarity < DUPLICATE_THRESHOLDS.clones.minSimilarityPercent ||
190
- !Number.isFinite(rawStrongestSimilarity) || rawStrongestSimilarity > 100 ||
191
- !Number.isFinite(rawWeakestLinkedSimilarity) || weakestLinkedSimilarity < DUPLICATE_THRESHOLDS.clones.minSimilarityPercent ||
192
- rawWeakestLinkedSimilarity > 100 ||
193
- weakestLinkedSimilarity > strongestSimilarity) return null
194
- const memberCount = Math.max(int(value?.memberCount), members.total)
195
- const returnedTokens = members.items.reduce((sum, member) => sum + member.tokens, 0)
196
- return {
197
- id,
198
- memberCount,
199
- totalTokens: Math.max(int(value?.totalTokens), returnedTokens),
200
- strongestSimilarity,
201
- weakestLinkedSimilarity,
202
- membersTruncated: bool(value?.membersTruncated) || members.truncated || members.invalid > 0 || memberCount > members.items.length,
203
- members: members.items,
204
- }
205
- }
206
-
207
- function divergenceCandidate(value) {
208
- const id = duplicateEvidenceId(value?.id)
209
- const symbol = text(value?.symbol, 256)
210
- const rawSimilarity = Number(value?.similarity)
211
- const similarity = Math.trunc(rawSimilarity)
212
- const members = duplicateMembers(value?.members, 2)
213
- if (!id || !symbol || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(symbol) || members.total !== 2 || members.invalid > 0 ||
214
- !Number.isFinite(rawSimilarity) || rawSimilarity < 0 || rawSimilarity > DUPLICATE_THRESHOLDS.divergence.maxSimilarityPercent ||
215
- members.items[0].file === members.items[1].file) return null
216
- return {
217
- id,
218
- symbol,
219
- similarity,
220
- totalTokens: Math.max(int(value?.totalTokens), members.items[0].tokens + members.items[1].tokens),
221
- members: members.items,
222
- }
223
- }
224
-
225
- function sanitizeDuplicates(value) {
226
- const rawGroups = Array.isArray(value?.cloneGroups) ? value.cloneGroups : []
227
- const rawDivergence = Array.isArray(value?.divergenceCandidates) ? value.divergenceCandidates : []
228
- const cloneGroups = list(rawGroups, CAPS.duplicateGroups, cloneGroup,
229
- (a, b) => b.totalTokens - a.totalTokens || b.memberCount - a.memberCount || compare(a.id, b.id))
230
- const divergence = list(rawDivergence, CAPS.divergenceCandidates, divergenceCandidate,
231
- (a, b) => b.totalTokens - a.totalTokens || a.similarity - b.similarity || compare(a.symbol, b.symbol) || compare(a.id, b.id))
232
- const invalid = rawGroups.length - cloneGroups.total + rawDivergence.length - divergence.total
233
- const membersTruncated = cloneGroups.items.some((group) => group.membersTruncated)
234
- const groupCompleteness = count(value?.completeness?.cloneGroups, rawGroups.length, cloneGroups.items.length)
235
- const divergenceCompleteness = count(value?.completeness?.divergenceCandidates, rawDivergence.length, divergence.items.length)
236
- const truncated = cloneGroups.truncated || divergence.truncated || groupCompleteness.truncated ||
237
- divergenceCompleteness.truncated || membersTruncated || invalid > 0
238
- const outReasons = reasons([
239
- ...(Array.isArray(value?.completeness?.reasons) ? value.completeness.reasons : []),
240
- ...(invalid > 0 ? ['INVALID_DUPLICATE_EVIDENCE_DROPPED'] : []),
241
- ...(membersTruncated ? ['CLONE_MEMBERS_TRUNCATED'] : []),
242
- ...(cloneGroups.truncated ? ['CLONE_GROUPS_TRUNCATED'] : []),
243
- ...(divergence.truncated ? ['DIVERGENCE_CANDIDATES_TRUNCATED'] : []),
244
- ])
245
- const rawFragments = value?.completeness?.fragments
246
- const eligible = int(rawFragments?.eligible), filtered = int(rawFragments?.filtered)
247
- const total = Math.max(int(rawFragments?.total), eligible + filtered)
248
- const outState = state(value?.state)
249
- return {
250
- state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState,
251
- verdict: verdict(value?.verdict),
252
- thresholds: DUPLICATE_THRESHOLDS,
253
- completeness: {
254
- fragments: {total, eligible: Math.min(eligible, total), filtered: Math.max(filtered, total - eligible)},
255
- cloneGroups: groupCompleteness,
256
- divergenceCandidates: divergenceCompleteness,
257
- reasons: outReasons,
258
- },
259
- cloneGroups: cloneGroups.items,
260
- divergenceCandidates: divergence.items,
261
- }
262
- }
263
-
264
- function numericRecord(value, keys) { const out = {}; for (const key of keys) out[key] = int(value?.[key]); return out }
265
- function checks(value) { const out = {}; for (const key of CHECK_KEYS) out[key] = state(value?.[key]); return out }
266
-
267
- function hotspot(value) {
268
- const id = graphId(value?.id), file = path(value?.file), severity = token(value?.severity, 16)
269
- if (!id || !file || !['medium', 'high'].includes(severity)) return null
270
- const out = {id, file, severity, breaches: [...new Set((value.breaches || []).map((item) => token(item, 48)).filter(Boolean))].sort(compare).slice(0, 12)}
271
- set(out, 'symbol', privacySafeText(value.symbol)); for (const key of ['startLine', 'endLine', 'loc', 'cyclomatic', 'params']) if (Number.isFinite(value[key]) && value[key] >= 0) out[key] = Math.trunc(value[key])
272
- return out
273
- }
274
-
275
- function sanitizeHealth(value) {
276
- const findings = list(value?.findings, CAPS.findings, finding, (a, b) => compare(a.severity, b.severity) || compare(a.id, b.id))
277
- const hotspots = list(value?.complexity?.hotspots, CAPS.hotspots, hotspot, (a, b) => compare(a.severity, b.severity) || compare(a.id, b.id))
278
- const truncated = findings.truncated || hotspots.truncated
279
- const outState = state(value?.state)
280
- return {
281
- state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState, verdict: verdict(value?.verdict),
282
- completeness: {findings: count(value?.completeness?.findings, findings.total, findings.items.length), hotspots: count(value?.completeness?.hotspots, hotspots.total, hotspots.items.length), reasons: reasons(value?.completeness?.reasons)},
283
- summary: {
284
- bySeverity: numericRecord(value?.summary?.bySeverity, ['critical', 'high', 'medium', 'low', 'info']),
285
- byCategory: numericRecord(value?.summary?.byCategory, ['unused', 'structure', 'vulnerability', 'malware']),
286
- dead: numericRecord(value?.summary?.dead, ['deadSymbols', 'deadFiles', 'unusedExports']),
287
- structure: numericRecord(value?.summary?.structure, ['runtimeImportEdges', 'typeOnlyImportEdges', 'compileOnlyImportEdges', 'runtimeCycles', 'compileTimeCouplings', 'largestCycle', 'largestCompileTimeCoupling', 'orphans', 'boundaryViolations']),
288
- },
289
- checks: checks(value?.checks), findings: findings.items,
290
- complexity: {thresholds: {loc: {warning: int(value?.complexity?.thresholds?.loc?.warning), high: int(value?.complexity?.thresholds?.loc?.high)}, cyclomatic: {warning: int(value?.complexity?.thresholds?.cyclomatic?.warning), high: int(value?.complexity?.thresholds?.cyclomatic?.high)}, params: {warning: int(value?.complexity?.thresholds?.params?.warning), high: int(value?.complexity?.thresholds?.params?.high)}}, analyzed: int(value?.complexity?.analyzed), hotspots: hotspots.items},
291
- }
292
- }
293
-
294
- function badge(value) {
295
- const category = token(value?.category, 32), id = token(value?.id, 128)
296
- if (!['languages', 'runtimes', 'tests', 'infra', 'deploy'].includes(category) || !id) return null
297
- const out = {category, id}; set(out, 'kind', token(value.kind, 64)); set(out, 'version', token(value.version, 64)); return out
298
- }
299
-
300
- function sanitizeTechnologies(value) {
301
- const badges = list(value?.badges, CAPS.badges, badge, (a, b) => compare(a.category, b.category) || compare(a.id, b.id))
302
- const outState = state(value?.state)
303
- return {state: badges.truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState, verdict: verdict(value?.verdict), completeness: {badges: count(value?.completeness?.badges, badges.total, badges.items.length), reasons: reasons(value?.completeness?.reasons)}, badges: badges.items}
304
- }
305
-
306
- function packageSource(value) {
307
- const source = text(value, 512)
308
- if (PACKAGE_SOURCES.has(source)) return source
309
- const relative = path(source, 512)
310
- return relative && /(^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.ya?ml|requirements[\w.-]*\.(?:txt|in)|poetry\.lock|uv\.lock|Pipfile\.lock|go\.(?:mod|sum))$/i.test(relative) ? relative : undefined
311
- }
312
-
313
- function packageFact(value) {
314
- const name = packageName(value?.name), version = token(value?.version, 128), ecosystem = token(value?.ecosystem, 64), source = packageSource(value?.source)
315
- return name && version && ecosystem && source ? {name, version, ecosystem, dev: bool(value.dev), source} : null
316
- }
317
-
318
- function usage(value) {
319
- const name = packageName(value?.name), ecosystem = token(value?.ecosystem, 64)
320
- if (!name || !ecosystem) return null
321
- const files = [...new Set((value.files || []).map((item) => path(item)).filter(Boolean))].sort(compare)
322
- return {name, ecosystem, importCount: int(value.importCount), fileCount: int(value.fileCount), files: files.slice(0, CAPS.files), filesTruncated: bool(value.filesTruncated) || files.length > CAPS.files, kinds: [...new Set((value.kinds || []).map((item) => token(item, 64)).filter(Boolean))].sort(compare).slice(0, 32)}
323
- }
324
-
325
- function packageGraphNode(value) {
326
- const name = packageName(value?.name), version = packageVersion(value?.version), id = text(value?.id, 512)
327
- if (!name || !version || !id) return null
328
- const prefix = `npm:${name}@${version}:`
329
- if (!id.startsWith(prefix) || !/^[a-f0-9]{12}$/i.test(id.slice(prefix.length))) return null
330
- return {
331
- id, name, version,
332
- direct: bool(value.direct), dev: bool(value.dev), optional: bool(value.optional), peer: bool(value.peer),
333
- }
334
- }
335
-
336
- function sanitizePackageDependencyGraph(value) {
337
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
338
- return {
339
- state: 'NOT_CHECKED', ecosystem: 'npm', root: '(root)',
340
- completeness: {
341
- nodes: count(null, 0, 0), edges: count(null, 0, 0),
342
- declarations: numericRecord(null, ['total', 'resolved', 'unresolved', 'local', 'optionalMissing']),
343
- reasons: ['DEPENDENCY_GRAPH_NOT_PROVIDED'],
344
- },
345
- nodes: [], edges: [],
346
- }
347
- }
348
-
349
- const nodes = list(value.nodes, CAPS.packageGraphNodes, packageGraphNode,
350
- (a, b) => Number(b.direct) - Number(a.direct) || compare(a.name, b.name) || compare(a.version, b.version) || compare(a.id, b.id))
351
- const nodeIds = new Set(nodes.items.map((node) => node.id))
352
- const edge = (candidate) => {
353
- const from = candidate?.from === '(root)' ? '(root)' : text(candidate?.from, 512)
354
- const to = text(candidate?.to, 512), kind = token(candidate?.kind, 32)
355
- if (!from || !to || !PACKAGE_DEPENDENCY_KINDS.has(kind) || from === to ||
356
- (from !== '(root)' && !nodeIds.has(from)) || !nodeIds.has(to)) return null
357
- return {from, to, kind}
358
- }
359
- const edges = list(value.edges, CAPS.packageGraphEdges, edge,
360
- (a, b) => compare(a.from, b.from) || compare(a.to, b.to) || compare(a.kind, b.kind))
361
- const lockfile = path(value.lockfile, 512)
362
- const safeLockfile = lockfile && /(^|\/)(?:package-lock\.json|npm-shrinkwrap\.json)$/i.test(lockfile) ? lockfile : undefined
363
- const graphState = state(value.state)
364
- const truncated = nodes.truncated || edges.truncated
365
- const out = {
366
- state: truncated && graphState === 'COMPLETE' ? 'PARTIAL' : graphState,
367
- ecosystem: 'npm',
368
- root: '(root)',
369
- completeness: {
370
- nodes: count(value?.completeness?.nodes, nodes.total, nodes.items.length),
371
- edges: count(value?.completeness?.edges, edges.total, edges.items.length),
372
- declarations: numericRecord(value?.completeness?.declarations, ['total', 'resolved', 'unresolved', 'local', 'optionalMissing']),
373
- reasons: reasons(value?.completeness?.reasons),
374
- },
375
- nodes: nodes.items,
376
- edges: edges.items,
377
- }
378
- set(out, 'lockfile', safeLockfile)
379
- const lockfileVersion = int(value.lockfileVersion)
380
- if (lockfileVersion > 0) out.lockfileVersion = lockfileVersion
381
- return out
382
- }
383
-
384
- function sanitizePackages(value) {
385
- const inventory = list(value?.inventory, CAPS.packages, packageFact, (a, b) => compare(a.ecosystem, b.ecosystem) || compare(a.name, b.name) || compare(a.version, b.version))
386
- const directUsage = list(value?.directUsage, CAPS.usage, usage, (a, b) => compare(a.ecosystem, b.ecosystem) || compare(a.name, b.name))
387
- const dependencyGraph = sanitizePackageDependencyGraph(value?.dependencyGraph)
388
- const outState = state(value?.state)
389
- const truncated = inventory.truncated || directUsage.truncated ||
390
- (dependencyGraph.state === 'PARTIAL' && value?.dependencyGraph?.state === 'COMPLETE')
391
- return {
392
- state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState,
393
- verdict: verdict(value?.verdict),
394
- completeness: {
395
- inventory: count(value?.completeness?.inventory, inventory.total, inventory.items.length),
396
- directUsage: count(value?.completeness?.directUsage, directUsage.total, directUsage.items.length),
397
- dependencyGraphNodes: count(value?.completeness?.dependencyGraphNodes, dependencyGraph.completeness.nodes.total, dependencyGraph.nodes.length),
398
- dependencyGraphEdges: count(value?.completeness?.dependencyGraphEdges, dependencyGraph.completeness.edges.total, dependencyGraph.edges.length),
399
- reasons: reasons(value?.completeness?.reasons),
400
- },
401
- checks: checks(value?.checks), inventory: inventory.items, directUsage: directUsage.items, dependencyGraph,
402
- }
403
- }
4
+ import {sanitizeArchitecture} from './sync/evidence-architecture.mjs'
5
+ import {stableStringify} from './sync/evidence-common.mjs'
6
+ import {sanitizeDuplicates} from './sync/evidence-duplicates.mjs'
7
+ import {sanitizeHealth, sanitizeTechnologies} from './sync/evidence-health.mjs'
8
+ import {sanitizePackages} from './sync/evidence-packages.mjs'
404
9
 
405
10
  export function sanitizeEvidenceSnapshot(value) {
406
11
  if (!value || typeof value !== 'object' || Array.isArray(value) || value.evidenceSnapshotV !== 1) throw new Error('invalid evidence snapshot')