weavatrix 0.2.13 → 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 +83 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- 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 +20 -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 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp-server.mjs +35 -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.13.md +0 -48
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAPS, CATEGORIES, CONFIDENCE, SEVERITIES, checks, compare, count, graphId,
|
|
3
|
+
int, list, numericRecord, packageName, path, privacySafeText, reasons, set,
|
|
4
|
+
state, text, token, verdict,
|
|
5
|
+
} from './evidence-common.mjs'
|
|
6
|
+
|
|
7
|
+
function finding(value) {
|
|
8
|
+
if (!value || typeof value !== 'object') return null
|
|
9
|
+
const id = text(value.id, 64), category = token(value.category, 32), rule = token(value.rule, 64), severity = token(value.severity, 16)
|
|
10
|
+
if (!id || !/^[a-f0-9]{8,64}$/i.test(id) || !CATEGORIES.has(category) || !rule || !SEVERITIES.has(severity)) return null
|
|
11
|
+
const out = {id, category, rule, severity}
|
|
12
|
+
if (CONFIDENCE.has(value.confidence)) out.confidence = value.confidence
|
|
13
|
+
set(out, 'file', path(value.file)); set(out, 'line', Number.isFinite(value.line) && value.line >= 0 ? Math.trunc(value.line) : undefined)
|
|
14
|
+
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))
|
|
15
|
+
return out
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hotspot(value) {
|
|
19
|
+
const id = graphId(value?.id), file = path(value?.file), severity = token(value?.severity, 16)
|
|
20
|
+
if (!id || !file || !['medium', 'high'].includes(severity)) return null
|
|
21
|
+
const out = {id, file, severity, breaches: [...new Set((value.breaches || []).map((item) => token(item, 48)).filter(Boolean))].sort(compare).slice(0, 12)}
|
|
22
|
+
set(out, 'symbol', privacySafeText(value.symbol))
|
|
23
|
+
for (const key of ['startLine', 'endLine', 'loc', 'cyclomatic', 'params']) if (Number.isFinite(value[key]) && value[key] >= 0) out[key] = Math.trunc(value[key])
|
|
24
|
+
return out
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function sanitizeHealth(value) {
|
|
28
|
+
const findings = list(value?.findings, CAPS.findings, finding, (a, b) => compare(a.severity, b.severity) || compare(a.id, b.id))
|
|
29
|
+
const hotspots = list(value?.complexity?.hotspots, CAPS.hotspots, hotspot, (a, b) => compare(a.severity, b.severity) || compare(a.id, b.id))
|
|
30
|
+
const truncated = findings.truncated || hotspots.truncated
|
|
31
|
+
const outState = state(value?.state)
|
|
32
|
+
return {
|
|
33
|
+
state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState, verdict: verdict(value?.verdict),
|
|
34
|
+
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)},
|
|
35
|
+
summary: {
|
|
36
|
+
bySeverity: numericRecord(value?.summary?.bySeverity, ['critical', 'high', 'medium', 'low', 'info']),
|
|
37
|
+
byCategory: numericRecord(value?.summary?.byCategory, ['unused', 'structure', 'vulnerability', 'malware']),
|
|
38
|
+
dead: numericRecord(value?.summary?.dead, ['deadSymbols', 'deadFiles', 'unusedExports']),
|
|
39
|
+
structure: numericRecord(value?.summary?.structure, ['runtimeImportEdges', 'typeOnlyImportEdges', 'compileOnlyImportEdges', 'runtimeCycles', 'compileTimeCouplings', 'largestCycle', 'largestCompileTimeCoupling', 'orphans', 'boundaryViolations']),
|
|
40
|
+
},
|
|
41
|
+
checks: checks(value?.checks), findings: findings.items,
|
|
42
|
+
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},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function badge(value) {
|
|
47
|
+
const category = token(value?.category, 32), id = token(value?.id, 128)
|
|
48
|
+
if (!['languages', 'runtimes', 'tests', 'infra', 'deploy'].includes(category) || !id) return null
|
|
49
|
+
const out = {category, id}; set(out, 'kind', token(value.kind, 64)); set(out, 'version', token(value.version, 64)); return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sanitizeTechnologies(value) {
|
|
53
|
+
const badges = list(value?.badges, CAPS.badges, badge, (a, b) => compare(a.category, b.category) || compare(a.id, b.id))
|
|
54
|
+
const outState = state(value?.state)
|
|
55
|
+
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}
|
|
56
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAPS, PACKAGE_DEPENDENCY_KINDS, bool, checks, compare, count, int, list,
|
|
3
|
+
numericRecord, packageName, packageVersion, path, reasons, set, state, text,
|
|
4
|
+
token, verdict,
|
|
5
|
+
} from './evidence-common.mjs'
|
|
6
|
+
|
|
7
|
+
const PACKAGE_SOURCES = new Set(['package-lock', 'yarn-lock', 'requirements', 'venv', 'poetry-lock', 'uv-lock', 'pipfile-lock', 'go-sum', 'go-mod', 'node_modules'])
|
|
8
|
+
|
|
9
|
+
function packageSource(value) {
|
|
10
|
+
const source = text(value, 512)
|
|
11
|
+
if (PACKAGE_SOURCES.has(source)) return source
|
|
12
|
+
const relative = path(source, 512)
|
|
13
|
+
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
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function packageFact(value) {
|
|
17
|
+
const name = packageName(value?.name), version = token(value?.version, 128), ecosystem = token(value?.ecosystem, 64), source = packageSource(value?.source)
|
|
18
|
+
return name && version && ecosystem && source ? {name, version, ecosystem, dev: bool(value.dev), source} : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usage(value) {
|
|
22
|
+
const name = packageName(value?.name), ecosystem = token(value?.ecosystem, 64)
|
|
23
|
+
if (!name || !ecosystem) return null
|
|
24
|
+
const files = [...new Set((value.files || []).map((item) => path(item)).filter(Boolean))].sort(compare)
|
|
25
|
+
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)}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function packageGraphNode(value) {
|
|
29
|
+
const name = packageName(value?.name), version = packageVersion(value?.version), id = text(value?.id, 512)
|
|
30
|
+
if (!name || !version || !id) return null
|
|
31
|
+
const prefix = `npm:${name}@${version}:`
|
|
32
|
+
if (!id.startsWith(prefix) || !/^[a-f0-9]{12}$/i.test(id.slice(prefix.length))) return null
|
|
33
|
+
return {id, name, version, direct: bool(value.direct), dev: bool(value.dev), optional: bool(value.optional), peer: bool(value.peer)}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sanitizePackageDependencyGraph(value) {
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
38
|
+
return {
|
|
39
|
+
state: 'NOT_CHECKED', ecosystem: 'npm', root: '(root)',
|
|
40
|
+
completeness: {nodes: count(null, 0, 0), edges: count(null, 0, 0), declarations: numericRecord(null, ['total', 'resolved', 'unresolved', 'local', 'optionalMissing']), reasons: ['DEPENDENCY_GRAPH_NOT_PROVIDED']},
|
|
41
|
+
nodes: [], edges: [],
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const nodes = list(value.nodes, CAPS.packageGraphNodes, packageGraphNode,
|
|
45
|
+
(a, b) => Number(b.direct) - Number(a.direct) || compare(a.name, b.name) || compare(a.version, b.version) || compare(a.id, b.id))
|
|
46
|
+
const nodeIds = new Set(nodes.items.map((node) => node.id))
|
|
47
|
+
const edge = (candidate) => {
|
|
48
|
+
const from = candidate?.from === '(root)' ? '(root)' : text(candidate?.from, 512)
|
|
49
|
+
const to = text(candidate?.to, 512), kind = token(candidate?.kind, 32)
|
|
50
|
+
if (!from || !to || !PACKAGE_DEPENDENCY_KINDS.has(kind) || from === to ||
|
|
51
|
+
(from !== '(root)' && !nodeIds.has(from)) || !nodeIds.has(to)) return null
|
|
52
|
+
return {from, to, kind}
|
|
53
|
+
}
|
|
54
|
+
const edges = list(value.edges, CAPS.packageGraphEdges, edge,
|
|
55
|
+
(a, b) => compare(a.from, b.from) || compare(a.to, b.to) || compare(a.kind, b.kind))
|
|
56
|
+
const lockfile = path(value.lockfile, 512)
|
|
57
|
+
const safeLockfile = lockfile && /(^|\/)(?:package-lock\.json|npm-shrinkwrap\.json)$/i.test(lockfile) ? lockfile : undefined
|
|
58
|
+
const graphState = state(value.state)
|
|
59
|
+
const truncated = nodes.truncated || edges.truncated
|
|
60
|
+
const out = {
|
|
61
|
+
state: truncated && graphState === 'COMPLETE' ? 'PARTIAL' : graphState,
|
|
62
|
+
ecosystem: 'npm', root: '(root)',
|
|
63
|
+
completeness: {
|
|
64
|
+
nodes: count(value?.completeness?.nodes, nodes.total, nodes.items.length),
|
|
65
|
+
edges: count(value?.completeness?.edges, edges.total, edges.items.length),
|
|
66
|
+
declarations: numericRecord(value?.completeness?.declarations, ['total', 'resolved', 'unresolved', 'local', 'optionalMissing']),
|
|
67
|
+
reasons: reasons(value?.completeness?.reasons),
|
|
68
|
+
},
|
|
69
|
+
nodes: nodes.items, edges: edges.items,
|
|
70
|
+
}
|
|
71
|
+
set(out, 'lockfile', safeLockfile)
|
|
72
|
+
const lockfileVersion = int(value.lockfileVersion)
|
|
73
|
+
if (lockfileVersion > 0) out.lockfileVersion = lockfileVersion
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function sanitizePackages(value) {
|
|
78
|
+
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))
|
|
79
|
+
const directUsage = list(value?.directUsage, CAPS.usage, usage, (a, b) => compare(a.ecosystem, b.ecosystem) || compare(a.name, b.name))
|
|
80
|
+
const dependencyGraph = sanitizePackageDependencyGraph(value?.dependencyGraph)
|
|
81
|
+
const outState = state(value?.state)
|
|
82
|
+
const truncated = inventory.truncated || directUsage.truncated || (dependencyGraph.state === 'PARTIAL' && value?.dependencyGraph?.state === 'COMPLETE')
|
|
83
|
+
return {
|
|
84
|
+
state: truncated && outState === 'COMPLETE' ? 'PARTIAL' : outState,
|
|
85
|
+
verdict: verdict(value?.verdict),
|
|
86
|
+
completeness: {
|
|
87
|
+
inventory: count(value?.completeness?.inventory, inventory.total, inventory.items.length),
|
|
88
|
+
directUsage: count(value?.completeness?.directUsage, directUsage.total, directUsage.items.length),
|
|
89
|
+
dependencyGraphNodes: count(value?.completeness?.dependencyGraphNodes, dependencyGraph.completeness.nodes.total, dependencyGraph.nodes.length),
|
|
90
|
+
dependencyGraphEdges: count(value?.completeness?.dependencyGraphEdges, dependencyGraph.completeness.edges.total, dependencyGraph.edges.length),
|
|
91
|
+
reasons: reasons(value?.completeness?.reasons),
|
|
92
|
+
},
|
|
93
|
+
checks: checks(value?.checks), inventory: inventory.items, directUsage: directUsage.items, dependencyGraph,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
2
|
+
const ABSOLUTE_PATH_FRAGMENT = /(?:^|[\/\s"'`(=])[a-z]:[\\/]|(?:^|[\s"'`(=])(?:\\\\[^\\/\s]+(?:[\\/]|$)|file:(?:\/\/)?[\\/]|\/(?!\/)[^\s])/i;
|
|
3
|
+
|
|
4
|
+
export const MAX_SYNC_BODY_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
export const MAX_SYNC_NODES = 25_000;
|
|
6
|
+
export const MAX_SYNC_LINKS = 100_000;
|
|
7
|
+
export const MAX_SYNC_EXTERNAL_IMPORTS = 50_000;
|
|
8
|
+
|
|
9
|
+
export function metadataString(value, max = 4096) {
|
|
10
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max && !CONTROL_CHARS.test(value)
|
|
11
|
+
? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function repoRelativePathString(value, max = 4096) {
|
|
15
|
+
const path = metadataString(value, max);
|
|
16
|
+
if (!path || /^(?:[a-z][a-z0-9+.-]*:|[\\/])/i.test(path)) return undefined;
|
|
17
|
+
const segments = path.split(/[\\/]/);
|
|
18
|
+
if (segments.some((segment) => segment === '.' || segment === '..')) return undefined;
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function graphIdString(value) {
|
|
23
|
+
const id = metadataString(value);
|
|
24
|
+
if (!id) return undefined;
|
|
25
|
+
const hash = id.indexOf('#');
|
|
26
|
+
const file = hash < 0 ? id : id.slice(0, hash);
|
|
27
|
+
return repoRelativePathString(file) ? id : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function privacySafeText(value, max = 4096) {
|
|
31
|
+
const text = metadataString(value, max);
|
|
32
|
+
return text && !ABSOLUTE_PATH_FRAGMENT.test(text) ? text : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function repoPathV3(value, max = 4096) {
|
|
36
|
+
const path = repoRelativePathString(value, max);
|
|
37
|
+
return path ? path.replace(/\\/g, '/') : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function graphIdV3(value) {
|
|
41
|
+
const id = metadataString(value);
|
|
42
|
+
if (!id) return undefined;
|
|
43
|
+
const hash = id.indexOf('#');
|
|
44
|
+
const file = hash < 0 ? id : id.slice(0, hash);
|
|
45
|
+
const safeFile = repoPathV3(file);
|
|
46
|
+
if (!safeFile) return undefined;
|
|
47
|
+
if (hash < 0) return safeFile;
|
|
48
|
+
const suffix = id.slice(hash);
|
|
49
|
+
if (suffix.length > 512 || !/^#[^\\/\s\u0000-\u001f\u007f]{1,511}$/u.test(suffix)) return undefined;
|
|
50
|
+
return `${safeFile}${suffix}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function safeToken(value, max = 256) {
|
|
54
|
+
const token = metadataString(value, max);
|
|
55
|
+
return token && /^[\p{L}\p{N}_.:@+\-#$<>()\[\],]+$/u.test(token) ? token : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function packageName(value) {
|
|
59
|
+
const name = metadataString(value, 256);
|
|
60
|
+
return name && /^(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/i.test(name) ? name : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function externalSpecifier(value) {
|
|
64
|
+
const spec = metadataString(value, 512);
|
|
65
|
+
if (!spec || ABSOLUTE_PATH_FRAGMENT.test(spec) || /^(?:[a-z]:[\\/]|[\\/]|\.\.?[\\/])/i.test(spec)) return undefined;
|
|
66
|
+
return /^[a-z0-9@][a-z0-9@._:/+\-]*$/i.test(spec) ? spec : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function finiteNumber(value) {
|
|
70
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setIf(out, key, value) {
|
|
74
|
+
if (value !== undefined) out[key] = value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const COMPLEXITY_NUMBERS = [
|
|
78
|
+
'startLine', 'endLine', 'loc', 'params', 'objectFields', 'branches', 'cyclomatic',
|
|
79
|
+
'loops', 'maxLoopDepth', 'returns', 'awaits', 'callCount', 'externalCalls',
|
|
80
|
+
'asyncBoundaries', 'allocations', 'objectLiterals', 'spreadCopies', 'sorts',
|
|
81
|
+
'linearOps', 'allocationsInLoops', 'copiesInLoops', 'linearOpsInLoops',
|
|
82
|
+
'sortsInLoops', 'recursionInLoops', 'timeRank', 'timeScore', 'memoryRank', 'memoryScore',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
export function sanitizeComplexity(value) {
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const key of COMPLEXITY_NUMBERS) setIf(out, key, finiteNumber(value[key]));
|
|
89
|
+
if (typeof value.recursion === 'boolean') out.recursion = value.recursion;
|
|
90
|
+
for (const key of ['family', 'scope', 'complexityScope', 'confidence']) setIf(out, key, metadataString(value[key], 32));
|
|
91
|
+
return Object.keys(out).length ? out : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function assertArrayLimit(raw, key, limit) {
|
|
95
|
+
const count = Array.isArray(raw[key]) ? raw[key].length : 0;
|
|
96
|
+
if (count > limit) throw new Error(`${key} has ${count} entries; maximum is ${limit}`);
|
|
97
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
finiteNumber, graphIdString, metadataString, repoRelativePathString,
|
|
3
|
+
sanitizeComplexity, setIf,
|
|
4
|
+
} from './payload-common.mjs';
|
|
5
|
+
|
|
6
|
+
export function sanitizeNode(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
8
|
+
const id = graphIdString(value.id);
|
|
9
|
+
if (!id) return null;
|
|
10
|
+
const out = {id};
|
|
11
|
+
setIf(out, 'label', metadataString(value.label, 1024));
|
|
12
|
+
setIf(out, 'file_type', metadataString(value.file_type, 32));
|
|
13
|
+
setIf(out, 'source_file', repoRelativePathString(value.source_file));
|
|
14
|
+
const sourceLocation = metadataString(value.source_location, 32);
|
|
15
|
+
const sourceEnd = metadataString(value.source_end, 32);
|
|
16
|
+
if (sourceLocation && /^L\d+$/.test(sourceLocation)) out.source_location = sourceLocation;
|
|
17
|
+
if (sourceEnd && /^L\d+$/.test(sourceEnd)) out.source_end = sourceEnd;
|
|
18
|
+
const community = finiteNumber(value.community);
|
|
19
|
+
if (community !== undefined && Number.isInteger(community) && community >= 0) out.community = community;
|
|
20
|
+
if (typeof value.exported === 'boolean') out.exported = value.exported;
|
|
21
|
+
if (typeof value.decorated === 'boolean') out.decorated = value.decorated;
|
|
22
|
+
setIf(out, 'complexity', sanitizeComplexity(value.complexity));
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeLink(value) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
28
|
+
const source = graphIdString(value.source);
|
|
29
|
+
const target = graphIdString(value.target);
|
|
30
|
+
if (!source || !target) return null;
|
|
31
|
+
const out = {source, target};
|
|
32
|
+
setIf(out, 'relation', metadataString(value.relation, 32));
|
|
33
|
+
setIf(out, 'confidence', metadataString(value.confidence, 32));
|
|
34
|
+
if (value.typeOnly === true) out.typeOnly = true;
|
|
35
|
+
if (value.compileOnly === true) out.compileOnly = true;
|
|
36
|
+
const line = finiteNumber(value.line);
|
|
37
|
+
if (line !== undefined && Number.isInteger(line) && line >= 0) out.line = line;
|
|
38
|
+
setIf(out, 'specifier', metadataString(value.specifier));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function sanitizeExternalImport(value) {
|
|
43
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
44
|
+
const file = repoRelativePathString(value.file);
|
|
45
|
+
if (!file) return null;
|
|
46
|
+
const out = {file};
|
|
47
|
+
for (const key of ['spec', 'target']) setIf(out, key, metadataString(value[key]));
|
|
48
|
+
for (const key of ['pkg', 'kind', 'ecosystem']) setIf(out, key, metadataString(value[key], 256));
|
|
49
|
+
const line = finiteNumber(value.line);
|
|
50
|
+
if (line !== undefined && Number.isInteger(line) && line >= 0) out.line = line;
|
|
51
|
+
for (const key of ['builtin', 'dynamic', 'unresolved']) if (typeof value[key] === 'boolean') out[key] = value[key];
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {edgeProvenance} from '../../graph/edge-provenance.js';
|
|
2
|
+
import {
|
|
3
|
+
externalSpecifier, graphIdV3, packageName, privacySafeText, repoPathV3,
|
|
4
|
+
safeToken, setIf,
|
|
5
|
+
} from './payload-common.mjs';
|
|
6
|
+
import {sanitizeExternalImport, sanitizeLink, sanitizeNode} from './payload-v2.mjs';
|
|
7
|
+
|
|
8
|
+
export function sanitizeNodeV3(value) {
|
|
9
|
+
const out = sanitizeNode(value);
|
|
10
|
+
if (!out) return null;
|
|
11
|
+
const id = graphIdV3(value.id);
|
|
12
|
+
if (!id) return null;
|
|
13
|
+
const sourceFile = value.source_file == null ? undefined : repoPathV3(value.source_file);
|
|
14
|
+
if (value.source_file != null && !sourceFile) return null;
|
|
15
|
+
const hash = id.indexOf('#');
|
|
16
|
+
const idFile = hash < 0 ? id : id.slice(0, hash);
|
|
17
|
+
if (sourceFile && idFile !== sourceFile) return null;
|
|
18
|
+
out.id = id;
|
|
19
|
+
const label = privacySafeText(value.label, 1024);
|
|
20
|
+
if (label) out.label = label;
|
|
21
|
+
else delete out.label;
|
|
22
|
+
const fileType = safeToken(value.file_type, 32);
|
|
23
|
+
if (fileType) out.file_type = fileType;
|
|
24
|
+
else delete out.file_type;
|
|
25
|
+
if (sourceFile) out.source_file = sourceFile;
|
|
26
|
+
else delete out.source_file;
|
|
27
|
+
for (const key of ['symbol_kind', 'symbol_space', 'member_of', 'visibility']) setIf(out, key, safeToken(value[key], 128));
|
|
28
|
+
if (out.complexity) for (const key of ['family', 'scope', 'complexityScope', 'confidence']) {
|
|
29
|
+
const safe = safeToken(value.complexity?.[key], 32);
|
|
30
|
+
if (safe) out.complexity[key] = safe;
|
|
31
|
+
else delete out.complexity[key];
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sanitizeLinkV3(value) {
|
|
37
|
+
const out = sanitizeLink(value);
|
|
38
|
+
if (!out) return null;
|
|
39
|
+
const source = graphIdV3(value.source);
|
|
40
|
+
const target = graphIdV3(value.target);
|
|
41
|
+
if (!source || !target) return null;
|
|
42
|
+
out.source = source;
|
|
43
|
+
out.target = target;
|
|
44
|
+
for (const key of ['relation', 'confidence']) {
|
|
45
|
+
const safe = safeToken(value[key], 32);
|
|
46
|
+
if (safe) out[key] = safe;
|
|
47
|
+
else delete out[key];
|
|
48
|
+
}
|
|
49
|
+
const provenance = edgeProvenance(value);
|
|
50
|
+
if (provenance !== 'UNKNOWN') out.provenance = provenance;
|
|
51
|
+
const specifier = privacySafeText(value.specifier, 1024);
|
|
52
|
+
if (specifier) out.specifier = specifier;
|
|
53
|
+
else delete out.specifier;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sanitizeExternalImportV3(value) {
|
|
58
|
+
const out = sanitizeExternalImport(value);
|
|
59
|
+
if (!out) return null;
|
|
60
|
+
const file = repoPathV3(value.file);
|
|
61
|
+
if (!file) return null;
|
|
62
|
+
out.file = file;
|
|
63
|
+
setIf(out, 'spec', externalSpecifier(value.spec));
|
|
64
|
+
if (!externalSpecifier(value.spec)) delete out.spec;
|
|
65
|
+
setIf(out, 'pkg', packageName(value.pkg));
|
|
66
|
+
if (!packageName(value.pkg)) delete out.pkg;
|
|
67
|
+
for (const key of ['kind', 'ecosystem']) {
|
|
68
|
+
const safe = safeToken(value[key], 64);
|
|
69
|
+
if (safe) out[key] = safe;
|
|
70
|
+
else delete out[key];
|
|
71
|
+
}
|
|
72
|
+
if (value.target != null) {
|
|
73
|
+
const target = repoPathV3(value.target);
|
|
74
|
+
if (target) out.target = target;
|
|
75
|
+
else delete out.target;
|
|
76
|
+
}
|
|
77
|
+
if (value.typeOnly === true) out.typeOnly = true;
|
|
78
|
+
return out;
|
|
79
|
+
}
|