weavatrix 0.2.13 → 0.2.15

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