weavatrix 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -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
+ }