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.
- package/README.md +90 -21
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +108 -43
- package/src/analysis/allowed-test-runner.js +20 -9
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +22 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -330
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-verified-change.mjs +12 -4
- package/src/mcp-server.mjs +49 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- package/docs/releases/v0.2.12.md +0 -45
|
@@ -1,406 +1,11 @@
|
|
|
1
|
-
// Defense-in-depth wire sanitizer for evidence snapshots.
|
|
2
|
-
//
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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')
|