weavatrix 0.3.14 → 1.1.0
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/LICENSE +1 -0
- package/README.md +132 -200
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +10 -67
- package/server.json +38 -0
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.14.md +0 -37
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports-tarpaulin.js +0 -112
- package/src/analysis/coverage-reports.js +0 -246
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -79
- package/src/analysis/endpoints/extract.js +0 -120
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -296
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -202
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -201
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -199
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -121
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -220
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -226
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import {changeLimits, normalizeChangePath} from './options.js'
|
|
2
|
-
|
|
3
|
-
function decodeGitQuoted(value) {
|
|
4
|
-
const input = String(value || '').trim()
|
|
5
|
-
if (!input.startsWith('"')) return input.split('\t', 1)[0]
|
|
6
|
-
try { return JSON.parse(input) } catch { /* Git octal escapes are not JSON. */ }
|
|
7
|
-
const bytes = []
|
|
8
|
-
for (let index = 1; index < input.length - 1; index++) {
|
|
9
|
-
const char = input[index]
|
|
10
|
-
if (char !== '\\') { bytes.push(...Buffer.from(char)); continue }
|
|
11
|
-
const next = input[++index] || ''
|
|
12
|
-
if (/[0-7]/.test(next)) {
|
|
13
|
-
let octal = next
|
|
14
|
-
while (octal.length < 3 && /[0-7]/.test(input[index + 1] || '')) octal += input[++index]
|
|
15
|
-
bytes.push(parseInt(octal, 8))
|
|
16
|
-
} else {
|
|
17
|
-
const escapes = {n: 10, r: 13, t: 9, b: 8, f: 12, v: 11, '\\': 92, '"': 34}
|
|
18
|
-
bytes.push(escapes[next] ?? next.charCodeAt(0))
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return Buffer.from(bytes).toString('utf8')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function diffPath(raw, prefix) {
|
|
25
|
-
const decoded = decodeGitQuoted(raw)
|
|
26
|
-
if (!decoded || decoded === '/dev/null') return null
|
|
27
|
-
return normalizeChangePath(decoded.startsWith(`${prefix}/`) ? decoded.slice(2) : decoded)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function headerPaths(line) {
|
|
31
|
-
const match = /^diff --git ("(?:\\.|[^"])*"|\S+) ("(?:\\.|[^"])*"|\S+)$/.exec(line)
|
|
32
|
-
return match
|
|
33
|
-
? {oldPath: diffPath(match[1], 'a'), newPath: diffPath(match[2], 'b')}
|
|
34
|
-
: {oldPath: null, newPath: null}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const emptyFile = (paths = {}) => ({
|
|
38
|
-
oldPath: paths.oldPath || null,
|
|
39
|
-
newPath: paths.newPath || null,
|
|
40
|
-
newFile: false,
|
|
41
|
-
deletedFile: false,
|
|
42
|
-
renamed: false,
|
|
43
|
-
binary: false,
|
|
44
|
-
hunks: [],
|
|
45
|
-
additions: [],
|
|
46
|
-
removals: [],
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
export function parseZeroContextDiff(diffText, options = {}) {
|
|
50
|
-
const limits = changeLimits(options)
|
|
51
|
-
const original = String(diffText ?? '')
|
|
52
|
-
const byteLength = Buffer.byteLength(original)
|
|
53
|
-
const oversized = byteLength > limits.maxDiffBytes
|
|
54
|
-
const text = oversized ? original.slice(0, limits.maxDiffBytes) : original
|
|
55
|
-
const files = []
|
|
56
|
-
let file = null, hunk = null, changedLines = 0, truncated = oversized
|
|
57
|
-
const finish = () => {
|
|
58
|
-
if (!file) return
|
|
59
|
-
if ((file.oldPath || file.newPath) && files.length < limits.maxFiles) files.push(file)
|
|
60
|
-
else if (files.length >= limits.maxFiles) truncated = true
|
|
61
|
-
file = null; hunk = null
|
|
62
|
-
}
|
|
63
|
-
const addChange = (kind, line, oldLine, newLine, mappedNewLine) => {
|
|
64
|
-
changedLines++
|
|
65
|
-
if (changedLines > limits.maxChangedLines) { truncated = true; return }
|
|
66
|
-
const change = {
|
|
67
|
-
kind,
|
|
68
|
-
text: String(line).slice(0, limits.maxLineLength),
|
|
69
|
-
...(oldLine != null ? {oldLine} : {}),
|
|
70
|
-
...(newLine != null ? {newLine} : {}),
|
|
71
|
-
mappedNewLine,
|
|
72
|
-
}
|
|
73
|
-
;(kind === 'added' ? file.additions : file.removals).push(change)
|
|
74
|
-
}
|
|
75
|
-
for (const line of text.split(/\r?\n/)) {
|
|
76
|
-
if (line.startsWith('diff --git ')) { finish(); file = emptyFile(headerPaths(line)); continue }
|
|
77
|
-
if (!file && line.startsWith('--- ')) file = emptyFile()
|
|
78
|
-
if (!file) continue
|
|
79
|
-
if (line.startsWith('new file mode ')) { file.newFile = true; continue }
|
|
80
|
-
if (line.startsWith('deleted file mode ')) { file.deletedFile = true; continue }
|
|
81
|
-
if (line.startsWith('rename from ')) { file.oldPath = normalizeChangePath(decodeGitQuoted(line.slice(12))); file.renamed = true; continue }
|
|
82
|
-
if (line.startsWith('rename to ')) { file.newPath = normalizeChangePath(decodeGitQuoted(line.slice(10))); file.renamed = true; continue }
|
|
83
|
-
if (line.startsWith('Binary files ') || line === 'GIT binary patch') { file.binary = true; continue }
|
|
84
|
-
if (!hunk && line.startsWith('--- ')) { file.oldPath = diffPath(line.slice(4), 'a'); if (!file.oldPath) file.newFile = true; continue }
|
|
85
|
-
if (!hunk && line.startsWith('+++ ')) { file.newPath = diffPath(line.slice(4), 'b'); if (!file.newPath) file.deletedFile = true; continue }
|
|
86
|
-
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line)
|
|
87
|
-
if (match) {
|
|
88
|
-
hunk = {
|
|
89
|
-
oldStart: Number(match[1]), oldCount: match[2] == null ? 1 : Number(match[2]),
|
|
90
|
-
newStart: Number(match[3]), newCount: match[4] == null ? 1 : Number(match[4]),
|
|
91
|
-
oldCursor: Number(match[1]), newCursor: Number(match[3]),
|
|
92
|
-
}
|
|
93
|
-
file.hunks.push({oldStart: hunk.oldStart, oldCount: hunk.oldCount, newStart: hunk.newStart, newCount: hunk.newCount})
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
if (!hunk || line.startsWith('\\ No newline')) continue
|
|
97
|
-
if (line.startsWith('+')) { addChange('added', line.slice(1), null, hunk.newCursor, hunk.newCursor); hunk.newCursor++ }
|
|
98
|
-
else if (line.startsWith('-')) { addChange('removed', line.slice(1), hunk.oldCursor, null, hunk.newCursor); hunk.oldCursor++ }
|
|
99
|
-
else { hunk.oldCursor++; hunk.newCursor++ }
|
|
100
|
-
}
|
|
101
|
-
finish()
|
|
102
|
-
return {files, byteLength, changedLines: Math.min(changedLines, limits.maxChangedLines), truncated, oversized, limits}
|
|
103
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export const CHANGE_CLASSIFICATION_LIMITS = Object.freeze({
|
|
2
|
-
maxDiffBytes: 2 * 1024 * 1024,
|
|
3
|
-
maxFiles: 500,
|
|
4
|
-
maxChangedLines: 20_000,
|
|
5
|
-
maxLineLength: 4_000,
|
|
6
|
-
maxSymbolsPerFile: 250,
|
|
7
|
-
maxSeeds: 1_000,
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export const CHANGE_CLASS_RANK = Object.freeze({
|
|
11
|
-
'metadata-only': 0,
|
|
12
|
-
'test-only': 0,
|
|
13
|
-
added: 1,
|
|
14
|
-
'body-changed': 2,
|
|
15
|
-
'signature-changed': 3,
|
|
16
|
-
removed: 4,
|
|
17
|
-
unknown: 5,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export const normalizeChangePath = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
21
|
-
export const changeLineNumber = (value) => Number((String(value || '').match(/(?:^L|@)(\d+)$/) || [])[1] || 0)
|
|
22
|
-
export const bareGraphLabel = (value) => String(value || '').replace(/\(.*$/, '').replace(/[^A-Za-z0-9_$].*$/, '').trim()
|
|
23
|
-
|
|
24
|
-
export function changeLimits(value = {}) {
|
|
25
|
-
const bounded = (item, fallback, min, max) => Math.max(min, Math.min(max, Number(item) || fallback))
|
|
26
|
-
const defaults = CHANGE_CLASSIFICATION_LIMITS
|
|
27
|
-
return {
|
|
28
|
-
maxDiffBytes: bounded(value.maxDiffBytes, defaults.maxDiffBytes, 1_024, defaults.maxDiffBytes),
|
|
29
|
-
maxFiles: bounded(value.maxFiles, defaults.maxFiles, 1, defaults.maxFiles),
|
|
30
|
-
maxChangedLines: bounded(value.maxChangedLines, defaults.maxChangedLines, 10, defaults.maxChangedLines),
|
|
31
|
-
maxLineLength: bounded(value.maxLineLength, defaults.maxLineLength, 80, defaults.maxLineLength),
|
|
32
|
-
maxSymbolsPerFile: bounded(value.maxSymbolsPerFile, defaults.maxSymbolsPerFile, 1, defaults.maxSymbolsPerFile),
|
|
33
|
-
maxSeeds: bounded(value.maxSeeds, defaults.maxSeeds, 1, defaults.maxSeeds),
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function uniqueChangeSeeds(values, limit) {
|
|
38
|
-
const all = [...new Set(values.filter(Boolean).map(String))].sort((a, b) => a.localeCompare(b))
|
|
39
|
-
return {items: all.slice(0, limit), truncated: all.length > limit, total: all.length}
|
|
40
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import {hasPathClass} from '../../path-classification.js'
|
|
2
|
-
import {bareGraphLabel, CHANGE_CLASS_RANK, changeLineNumber, normalizeChangePath} from './options.js'
|
|
3
|
-
|
|
4
|
-
export function indexChangeGraph(graph, limits) {
|
|
5
|
-
const byFile = new Map()
|
|
6
|
-
for (const node of graph?.nodes || []) {
|
|
7
|
-
const file = normalizeChangePath(node?.source_file || (!String(node?.id || '').includes('#') ? node?.id : ''))
|
|
8
|
-
if (!file) continue
|
|
9
|
-
if (!byFile.has(file)) byFile.set(file, {path: file, fileNodeId: null, symbols: []})
|
|
10
|
-
const record = byFile.get(file)
|
|
11
|
-
if (!String(node.id).includes('#')) record.fileNodeId = String(node.id)
|
|
12
|
-
else if (record.symbols.length < limits.maxSymbolsPerFile) {
|
|
13
|
-
const start = changeLineNumber(node.source_location) || changeLineNumber(node.id)
|
|
14
|
-
if (!start) continue
|
|
15
|
-
record.symbols.push({
|
|
16
|
-
id: String(node.id), label: String(node.label || node.id), start,
|
|
17
|
-
end: changeLineNumber(node.source_end), exported: node.exported === true,
|
|
18
|
-
symbolKind: node.symbol_kind || null,
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
for (const record of byFile.values()) {
|
|
23
|
-
record.symbols.sort((a, b) => a.start - b.start || a.id.localeCompare(b.id))
|
|
24
|
-
for (let index = 0; index < record.symbols.length; index++) {
|
|
25
|
-
const symbol = record.symbols[index]
|
|
26
|
-
if (!symbol.end || symbol.end < symbol.start) {
|
|
27
|
-
const next = record.symbols[index + 1]?.start
|
|
28
|
-
symbol.end = next ? Math.max(symbol.start, next - 1) : symbol.start + 400
|
|
29
|
-
}
|
|
30
|
-
symbol.end = Math.min(symbol.end, symbol.start + 2_000)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return byFile
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const isMetadataLine = (text) => {
|
|
37
|
-
const value = String(text || '').trim()
|
|
38
|
-
return !value || /^(?:\/\/|\/\*|\*|\*\/|#(?!include\b)|<!--|-->|"""|''')/.test(value)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function signatureText(text, symbol) {
|
|
42
|
-
const value = String(text || '').trim(), name = bareGraphLabel(symbol.label)
|
|
43
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
44
|
-
const hasName = name && new RegExp(`\\b${escaped}\\b`, 'i').test(value)
|
|
45
|
-
return hasName && /\b(?:export|default|declare|abstract|async|function|class|interface|type|enum|const|let|var|def|func|fn|struct|trait|impl|public|private|protected|static)\b/.test(value)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function signaturePosition(change, symbol) {
|
|
49
|
-
const candidates = change.kind === 'removed' ? [change.mappedNewLine, change.oldLine] : [change.newLine]
|
|
50
|
-
if (candidates.some((line) => line === symbol.start)) return true
|
|
51
|
-
if (!candidates.some((line) => Number.isFinite(line) && line > symbol.start && line <= symbol.start + 4)) return false
|
|
52
|
-
const value = String(change.text || '').trim()
|
|
53
|
-
if (/^(?:return|throw|yield|if|for|while|switch|match|const|let|var|this\.|self\.|[A-Za-z_$][\w$]*\s*=)/.test(value)) return false
|
|
54
|
-
return /^(?:[A-Za-z_$][\w$]*\??\s*:\s*[^;]+[,)]?|[A-Za-z_$][\w$<>,.?\[\] :*&]+\s+[A-Za-z_$][\w$]*\s*[,)]|[),:<>{}\[\]|&?]+)$/.test(value)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const moduleSignatureText = (text) => /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+\w+\s*=\s*require\b|using\b|package\b|#include\b|mod\b|pub\s+use\b)/.test(String(text || ''))
|
|
58
|
-
|
|
59
|
-
function chooseSymbol(record, change) {
|
|
60
|
-
if (!record?.symbols?.length) return null
|
|
61
|
-
const lines = change.kind === 'removed'
|
|
62
|
-
? [...new Set([change.mappedNewLine, change.oldLine].filter((line) => Number.isFinite(line) && line > 0))]
|
|
63
|
-
: [change.newLine]
|
|
64
|
-
const candidates = []
|
|
65
|
-
for (const symbol of record.symbols) {
|
|
66
|
-
if (!lines.some((line) => line >= symbol.start && line <= symbol.end)) continue
|
|
67
|
-
const label = bareGraphLabel(symbol.label)
|
|
68
|
-
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
69
|
-
candidates.push({symbol, mentions: label && new RegExp(`\\b${escaped}\\b`).test(change.text), span: symbol.end - symbol.start})
|
|
70
|
-
}
|
|
71
|
-
return candidates.sort((a, b) => Number(b.mentions) - Number(a.mentions) || a.span - b.span || a.symbol.id.localeCompare(b.symbol.id))[0]?.symbol || null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const strongest = (classes) => [...classes]
|
|
75
|
-
.sort((a, b) => (CHANGE_CLASS_RANK[b] ?? 5) - (CHANGE_CLASS_RANK[a] ?? 5) || a.localeCompare(b))[0] || 'metadata-only'
|
|
76
|
-
|
|
77
|
-
function symbolChanges(parsed, record, includeAddedSeeds) {
|
|
78
|
-
const grouped = new Map(), unmapped = []
|
|
79
|
-
for (const change of [...parsed.additions, ...parsed.removals]) {
|
|
80
|
-
const symbol = chooseSymbol(record, change)
|
|
81
|
-
if (!symbol) { unmapped.push(change); continue }
|
|
82
|
-
if (!grouped.has(symbol.id)) grouped.set(symbol.id, {symbol, additions: [], removals: []})
|
|
83
|
-
grouped.get(symbol.id)[change.kind === 'added' ? 'additions' : 'removals'].push(change)
|
|
84
|
-
}
|
|
85
|
-
const symbols = [], seedIds = []
|
|
86
|
-
for (const group of [...grouped.values()].sort((a, b) => a.symbol.start - b.symbol.start || a.symbol.id.localeCompare(b.symbol.id))) {
|
|
87
|
-
const addedCode = group.additions.filter((change) => !isMetadataLine(change.text))
|
|
88
|
-
const removedCode = group.removals.filter((change) => !isMetadataLine(change.text))
|
|
89
|
-
const addedDeclaration = addedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol))
|
|
90
|
-
const removedDeclaration = removedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol))
|
|
91
|
-
let classification
|
|
92
|
-
if (parsed.newFile || (addedDeclaration && !removedCode.length)) classification = 'added'
|
|
93
|
-
else if (parsed.deletedFile || (removedDeclaration && !addedCode.length)) classification = 'removed'
|
|
94
|
-
else if (addedDeclaration || removedDeclaration) classification = 'signature-changed'
|
|
95
|
-
else if (addedCode.length || removedCode.length) classification = 'body-changed'
|
|
96
|
-
else classification = 'metadata-only'
|
|
97
|
-
const reasons = {
|
|
98
|
-
added: 'new declaration; existing callers cannot depend on it yet',
|
|
99
|
-
removed: 'declaration removed; existing callers may break',
|
|
100
|
-
'signature-changed': 'declaration/signature line changed',
|
|
101
|
-
'body-changed': 'executable lines changed inside the symbol body',
|
|
102
|
-
'metadata-only': 'only comment/blank lines changed in this symbol',
|
|
103
|
-
}
|
|
104
|
-
const symbolSeeds = []
|
|
105
|
-
if (classification === 'added') { if (includeAddedSeeds) symbolSeeds.push(group.symbol.id) }
|
|
106
|
-
else if (classification !== 'metadata-only') {
|
|
107
|
-
symbolSeeds.push(group.symbol.id)
|
|
108
|
-
if (['removed', 'signature-changed'].includes(classification) && group.symbol.exported && record?.fileNodeId) symbolSeeds.push(record.fileNodeId)
|
|
109
|
-
}
|
|
110
|
-
seedIds.push(...symbolSeeds)
|
|
111
|
-
symbols.push({
|
|
112
|
-
id: group.symbol.id, label: group.symbol.label, start: group.symbol.start, end: group.symbol.end,
|
|
113
|
-
exported: group.symbol.exported, classification, reason: reasons[classification],
|
|
114
|
-
addedLines: group.additions.map((change) => change.newLine).filter(Boolean),
|
|
115
|
-
removedLines: group.removals.map((change) => change.oldLine).filter(Boolean),
|
|
116
|
-
seedIds: [...new Set(symbolSeeds)].sort(),
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
return {symbols, seedIds, unmapped}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function analyzeParsedFile(parsed, indexed, {includeAddedSeeds}) {
|
|
123
|
-
const path = parsed.newPath || parsed.oldPath || '(unknown)'
|
|
124
|
-
const record = indexed.get(parsed.newPath) || indexed.get(parsed.oldPath) || null
|
|
125
|
-
const grouped = symbolChanges(parsed, record, includeAddedSeeds)
|
|
126
|
-
const unmappedCode = grouped.unmapped.filter((change) => !isMetadataLine(change.text))
|
|
127
|
-
const unmappedMetadata = grouped.unmapped.length > 0 && !unmappedCode.length
|
|
128
|
-
let classification, reason
|
|
129
|
-
if (parsed.binary) { classification = 'unknown'; reason = 'binary diff has no line-level evidence' }
|
|
130
|
-
else if (parsed.deletedFile) { classification = 'removed'; reason = 'file removed' }
|
|
131
|
-
else if (parsed.renamed) { classification = 'signature-changed'; reason = 'file rename changes module identity' }
|
|
132
|
-
else if (parsed.newFile) { classification = 'added'; reason = 'new file; no existing dependent can target it yet' }
|
|
133
|
-
else if (unmappedCode.some((change) => moduleSignatureText(change.text))) { classification = 'signature-changed'; reason = 'module import/export surface changed outside a mapped symbol' }
|
|
134
|
-
else if (unmappedCode.length) { classification = 'unknown'; reason = 'executable diff lines could not be mapped to a graph symbol' }
|
|
135
|
-
else if (grouped.symbols.length) {
|
|
136
|
-
classification = strongest(grouped.symbols.map((symbol) => symbol.classification))
|
|
137
|
-
reason = grouped.symbols.length === 1 ? grouped.symbols[0].reason : `${grouped.symbols.length} mapped symbols; strongest change is ${classification}`
|
|
138
|
-
} else if (unmappedMetadata || parsed.hunks.length || parsed.additions.length || parsed.removals.length) {
|
|
139
|
-
classification = 'metadata-only'; reason = 'only comment/blank metadata changed outside symbols'
|
|
140
|
-
} else { classification = 'metadata-only'; reason = 'file metadata changed without textual hunks' }
|
|
141
|
-
|
|
142
|
-
const seedIds = grouped.seedIds
|
|
143
|
-
if (parsed.binary || parsed.deletedFile || parsed.renamed || classification === 'unknown') {
|
|
144
|
-
if (record?.fileNodeId) seedIds.push(record.fileNodeId)
|
|
145
|
-
if (parsed.binary || parsed.deletedFile || classification === 'unknown') seedIds.push(...(record?.symbols || []).map((symbol) => symbol.id))
|
|
146
|
-
} else if (classification === 'signature-changed' && !grouped.symbols.length && record?.fileNodeId) seedIds.push(record.fileNodeId)
|
|
147
|
-
else if (classification === 'added' && includeAddedSeeds) {
|
|
148
|
-
if (record?.fileNodeId) seedIds.push(record.fileNodeId)
|
|
149
|
-
seedIds.push(...grouped.symbols.map((symbol) => symbol.id))
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
path, oldPath: parsed.oldPath, newPath: parsed.newPath, classification, reason,
|
|
153
|
-
binary: parsed.binary, renamed: parsed.renamed,
|
|
154
|
-
addedLines: parsed.additions.length, removedLines: parsed.removals.length,
|
|
155
|
-
symbols: grouped.symbols, seedIds: [...new Set(seedIds)].sort(),
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function unknownChangedFile(path, indexed, reason) {
|
|
160
|
-
const normalized = normalizeChangePath(path), record = indexed.get(normalized)
|
|
161
|
-
return {
|
|
162
|
-
path: normalized, oldPath: normalized, newPath: normalized, classification: 'unknown', reason,
|
|
163
|
-
binary: false, renamed: false, addedLines: 0, removedLines: 0, symbols: [],
|
|
164
|
-
seedIds: [record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id)].filter(Boolean).sort(),
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function classifyTestSurface(file, pathClassifier) {
|
|
169
|
-
const explanation = pathClassifier.explain(file.path)
|
|
170
|
-
if (!hasPathClass(explanation, 'test', 'e2e')) return file
|
|
171
|
-
const surface = explanation.classes.includes('e2e') ? 'e2e' : 'test'
|
|
172
|
-
return {
|
|
173
|
-
...file, classification: 'test-only', changeClassification: file.classification,
|
|
174
|
-
reason: `${surface} path; excluded from the product blast-radius seed set`,
|
|
175
|
-
pathClasses: explanation.classes, seedIds: [],
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
// Symbol-aware git-diff classification for change_impact.
|
|
2
|
-
import {runGit} from '../git-exec.js'
|
|
3
|
-
import {createPathClassifier} from '../path-classification.js'
|
|
4
|
-
import {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
5
|
-
import {
|
|
6
|
-
CHANGE_CLASSIFICATION_LIMITS,
|
|
7
|
-
CHANGE_CLASS_RANK,
|
|
8
|
-
changeLimits,
|
|
9
|
-
normalizeChangePath,
|
|
10
|
-
uniqueChangeSeeds,
|
|
11
|
-
} from './change-classification/options.js'
|
|
12
|
-
import {
|
|
13
|
-
analyzeParsedFile,
|
|
14
|
-
classifyTestSurface,
|
|
15
|
-
indexChangeGraph,
|
|
16
|
-
unknownChangedFile,
|
|
17
|
-
} from './change-classification/symbol-classifier.js'
|
|
18
|
-
|
|
19
|
-
export {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
20
|
-
export {CHANGE_CLASSIFICATION_LIMITS} from './change-classification/options.js'
|
|
21
|
-
|
|
22
|
-
const VERDICT_RANK = Object.freeze({LOW: 0, MEDIUM: 1, HIGH: 2})
|
|
23
|
-
|
|
24
|
-
function recoverBoundedDiff(repoRoot, base, files, limits) {
|
|
25
|
-
const deadline = Date.now() + 12_000
|
|
26
|
-
let text = ''
|
|
27
|
-
const fallbackFiles = []
|
|
28
|
-
for (let index = 0; index < files.length; index++) {
|
|
29
|
-
const remainingMs = deadline - Date.now()
|
|
30
|
-
const remainingBytes = limits.maxDiffBytes - Buffer.byteLength(text)
|
|
31
|
-
if (remainingMs <= 0 || remainingBytes < 1_024) {
|
|
32
|
-
fallbackFiles.push(...files.slice(index))
|
|
33
|
-
break
|
|
34
|
-
}
|
|
35
|
-
const result = runGit(repoRoot, [
|
|
36
|
-
'diff', '--no-ext-diff', '--find-renames', '--no-color',
|
|
37
|
-
'--unified=0', String(base), '--', files[index],
|
|
38
|
-
], {timeout: Math.min(2_000, remainingMs), maxBuffer: remainingBytes + 1})
|
|
39
|
-
const chunk = String(result.stdout || '')
|
|
40
|
-
if (result.status !== 0 || !chunk || Buffer.byteLength(chunk) > remainingBytes) {
|
|
41
|
-
fallbackFiles.push(files[index])
|
|
42
|
-
continue
|
|
43
|
-
}
|
|
44
|
-
text += chunk
|
|
45
|
-
}
|
|
46
|
-
return {text, fallbackFiles}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function runGitDiff(repoRoot, base, limits) {
|
|
50
|
-
const args = ['diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
|
|
51
|
-
const result = runGit(repoRoot, args, {timeout: 12_000, maxBuffer: limits.maxDiffBytes + 1})
|
|
52
|
-
const output = String(result.stdout || '')
|
|
53
|
-
const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(output) > limits.maxDiffBytes
|
|
54
|
-
if (result.status === 0 && !oversized) return {available: true, text: output, error: null}
|
|
55
|
-
let fallbackFiles = []
|
|
56
|
-
let fallbackTruncated = false
|
|
57
|
-
let recoveredText = ''
|
|
58
|
-
if (oversized) {
|
|
59
|
-
const names = runGit(repoRoot, [
|
|
60
|
-
'diff', '--name-only', '-z', '--no-ext-diff', '--find-renames', String(base), '--',
|
|
61
|
-
], {timeout: 12_000, maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096)})
|
|
62
|
-
if (names.status === 0) {
|
|
63
|
-
const all = String(names.stdout || '').split('\0').map(normalizeChangePath).filter(Boolean)
|
|
64
|
-
const boundedFiles = [...new Set(all)].sort((a, b) => a.localeCompare(b)).slice(0, limits.maxFiles)
|
|
65
|
-
fallbackTruncated = all.length > limits.maxFiles
|
|
66
|
-
const recovered = recoverBoundedDiff(repoRoot, base, boundedFiles, limits)
|
|
67
|
-
recoveredText = recovered.text
|
|
68
|
-
fallbackFiles = recovered.fallbackFiles
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
available: recoveredText.length > 0,
|
|
73
|
-
text: recoveredText,
|
|
74
|
-
oversized,
|
|
75
|
-
partialFallback: recoveredText.length > 0,
|
|
76
|
-
fallbackFiles,
|
|
77
|
-
fallbackTruncated,
|
|
78
|
-
error: oversized
|
|
79
|
-
? 'git diff exceeded the byte limit'
|
|
80
|
-
: String(result.stderr || result.error?.message || 'git diff unavailable').trim(),
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const validExplicitFiles = (files) => [...new Set((Array.isArray(files) ? files : [])
|
|
85
|
-
.map(normalizeChangePath)
|
|
86
|
-
.filter((file) => file && !file.startsWith('../') && !file.includes('/../') && !file.startsWith('-')))]
|
|
87
|
-
.sort((a, b) => a.localeCompare(b))
|
|
88
|
-
|
|
89
|
-
function collectDiffInput({diffText, repoRoot, base, explicitFiles, limits}) {
|
|
90
|
-
if (typeof diffText === 'string') return {source: 'provided-diff', available: true, text: diffText, reason: '', oversized: false}
|
|
91
|
-
if (repoRoot && base) {
|
|
92
|
-
const result = runGitDiff(repoRoot, base, limits)
|
|
93
|
-
return {
|
|
94
|
-
source: 'git-diff', available: result.available, text: result.text,
|
|
95
|
-
reason: result.error || '', oversized: result.oversized === true,
|
|
96
|
-
fallbackFiles: result.fallbackFiles || [], fallbackTruncated: result.fallbackTruncated === true,
|
|
97
|
-
partialFallback: result.partialFallback === true,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
source: 'files-only', available: false, text: '', oversized: false,
|
|
102
|
-
reason: 'no unified diff was provided and no repoRoot/base pair was available',
|
|
103
|
-
fallbackFiles: [], fallbackTruncated: false, partialFallback: false,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function classificationReasons(counts, {available, unavailableReason, truncated, includeAddedSeeds}) {
|
|
108
|
-
const reasons = []
|
|
109
|
-
if (!available) reasons.push(`Diff unavailable: ${unavailableReason || 'unknown error'}; using conservative file/symbol seeds.`)
|
|
110
|
-
if (truncated) reasons.push('Diff exceeded a safety bound; incomplete evidence is classified HIGH/unknown.')
|
|
111
|
-
if (counts.removed || counts['signature-changed']) reasons.push(`${counts.removed} removed and ${counts['signature-changed']} signature/module-surface file change(s) can break existing callers.`)
|
|
112
|
-
if (counts['body-changed']) reasons.push(`${counts['body-changed']} file(s) contain mapped executable body changes.`)
|
|
113
|
-
if (counts.added && !includeAddedSeeds) reasons.push(`${counts.added} purely additive file change(s) create no dependent seeds by default.`)
|
|
114
|
-
if (counts['metadata-only']) reasons.push(`${counts['metadata-only']} metadata-only file change(s) create no dependent seeds.`)
|
|
115
|
-
if (counts['test-only']) reasons.push(`${counts['test-only']} test-only file change(s) are labelled explicitly and create no product blast-radius seeds.`)
|
|
116
|
-
if (counts.unknown) reasons.push(`${counts.unknown} file change(s) remain unknown and are seeded conservatively.`)
|
|
117
|
-
if (!reasons.length) reasons.push('No changed files were present in the supplied diff.')
|
|
118
|
-
return reasons
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function classifyChangeImpact({
|
|
122
|
-
repoRoot = '', graph = {}, base = '', diffText, files = [], includeAddedSeeds = false,
|
|
123
|
-
limits: requestedLimits = {},
|
|
124
|
-
} = {}) {
|
|
125
|
-
const limits = changeLimits(requestedLimits)
|
|
126
|
-
const explicitFiles = validExplicitFiles(files)
|
|
127
|
-
const input = collectDiffInput({diffText, repoRoot, base, explicitFiles, limits})
|
|
128
|
-
const fallbackFiles = validExplicitFiles([...explicitFiles, ...(input.fallbackFiles || [])]).slice(0, limits.maxFiles)
|
|
129
|
-
const indexed = indexChangeGraph(graph, limits)
|
|
130
|
-
const pathClassifier = createPathClassifier(repoRoot)
|
|
131
|
-
const parsed = input.available
|
|
132
|
-
? parseZeroContextDiff(input.text, limits)
|
|
133
|
-
: {files: [], changedLines: 0, byteLength: 0, truncated: input.oversized, oversized: input.oversized, limits}
|
|
134
|
-
const analyzed = parsed.files.map((file) => classifyTestSurface(analyzeParsedFile(file, indexed, {includeAddedSeeds}), pathClassifier))
|
|
135
|
-
const represented = new Set(analyzed.flatMap((file) => [file.oldPath, file.newPath].filter(Boolean)))
|
|
136
|
-
for (const file of fallbackFiles) if (!represented.has(file)) analyzed.push(classifyTestSurface(
|
|
137
|
-
unknownChangedFile(file, indexed, input.available ? 'explicitly changed file had no textual hunk' : input.reason),
|
|
138
|
-
pathClassifier,
|
|
139
|
-
))
|
|
140
|
-
if (!input.available && !fallbackFiles.length) analyzed.push(unknownChangedFile('(diff unavailable)', indexed, input.reason))
|
|
141
|
-
analyzed.sort((a, b) => a.path.localeCompare(b.path))
|
|
142
|
-
|
|
143
|
-
const incomplete = parsed.truncated || parsed.oversized || input.oversized || input.fallbackTruncated
|
|
144
|
-
const parsedEvidenceIncomplete = parsed.truncated || parsed.oversized || (input.oversized && !input.partialFallback)
|
|
145
|
-
if (parsedEvidenceIncomplete) for (const file of analyzed) {
|
|
146
|
-
if (file.classification === 'test-only') continue
|
|
147
|
-
file.classification = 'unknown'
|
|
148
|
-
file.reason = 'diff was truncated/oversized; symbol-level classification is incomplete'
|
|
149
|
-
const record = indexed.get(file.newPath) || indexed.get(file.oldPath)
|
|
150
|
-
file.seedIds = [...new Set([record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id), ...file.seedIds].filter(Boolean))].sort()
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const seeds = uniqueChangeSeeds(analyzed.flatMap((file) => file.seedIds), limits.maxSeeds)
|
|
154
|
-
let verdict = 'LOW'
|
|
155
|
-
for (const file of analyzed) {
|
|
156
|
-
const next = ['removed', 'signature-changed', 'unknown'].includes(file.classification)
|
|
157
|
-
? 'HIGH' : file.classification === 'body-changed' ? 'MEDIUM' : 'LOW'
|
|
158
|
-
if (VERDICT_RANK[next] > VERDICT_RANK[verdict]) verdict = next
|
|
159
|
-
}
|
|
160
|
-
if (!input.available || input.oversized || parsed.truncated || seeds.truncated) verdict = 'HIGH'
|
|
161
|
-
const counts = Object.fromEntries(Object.keys(CHANGE_CLASS_RANK)
|
|
162
|
-
.map((name) => [name, analyzed.filter((file) => file.classification === name).length]))
|
|
163
|
-
return {
|
|
164
|
-
ok: input.available && !input.oversized && !parsed.truncated && !seeds.truncated,
|
|
165
|
-
source: input.source,
|
|
166
|
-
verdict,
|
|
167
|
-
reasons: classificationReasons(counts, {
|
|
168
|
-
available: input.available,
|
|
169
|
-
unavailableReason: input.reason,
|
|
170
|
-
truncated: incomplete,
|
|
171
|
-
includeAddedSeeds,
|
|
172
|
-
}),
|
|
173
|
-
seedIds: seeds.items,
|
|
174
|
-
files: analyzed,
|
|
175
|
-
summary: {
|
|
176
|
-
files: analyzed.length,
|
|
177
|
-
symbols: analyzed.reduce((sum, file) => sum + file.symbols.length, 0),
|
|
178
|
-
counts,
|
|
179
|
-
seeds: seeds.items.length,
|
|
180
|
-
totalSeedsBeforeCap: seeds.total,
|
|
181
|
-
},
|
|
182
|
-
bounds: {
|
|
183
|
-
...limits, diffBytes: parsed.byteLength, changedLines: parsed.changedLines,
|
|
184
|
-
fallbackFiles: input.fallbackFiles?.length || 0,
|
|
185
|
-
truncated: input.oversized === true || !!parsed.truncated || seeds.truncated || input.fallbackTruncated === true,
|
|
186
|
-
},
|
|
187
|
-
}
|
|
188
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
function pct01(value) {
|
|
4
|
-
const n = Number(value);
|
|
5
|
-
if (!Number.isFinite(n)) return null;
|
|
6
|
-
return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function pctFromCounts(covered, total) {
|
|
10
|
-
const c = Number(covered);
|
|
11
|
-
const t = Number(total);
|
|
12
|
-
if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
|
|
13
|
-
return Math.max(0, Math.min(1, c / t));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function addLineHit(lines, line, hit) {
|
|
17
|
-
const value = Number(line);
|
|
18
|
-
if (!Number.isFinite(value) || value <= 0) return;
|
|
19
|
-
const n = Math.round(value);
|
|
20
|
-
if (n < 1) return;
|
|
21
|
-
const current = lines.get(n);
|
|
22
|
-
lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function numericLineList(value) {
|
|
26
|
-
const input = Array.isArray(value) ? value : [];
|
|
27
|
-
return input
|
|
28
|
-
.map((line) => Number(line))
|
|
29
|
-
.filter((line) => Number.isFinite(line) && line > 0)
|
|
30
|
-
.map((line) => Math.round(line));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function legacyLineGroups(record) {
|
|
34
|
-
if (!record || typeof record !== "object") return { covered: [], uncovered: [] };
|
|
35
|
-
const lines = record.lines && typeof record.lines === "object" ? record.lines : {};
|
|
36
|
-
return {
|
|
37
|
-
covered: [
|
|
38
|
-
...numericLineList(record.covered),
|
|
39
|
-
...numericLineList(record.covered_lines),
|
|
40
|
-
...numericLineList(record.coveredLines),
|
|
41
|
-
...numericLineList(lines.covered),
|
|
42
|
-
...numericLineList(lines.covered_lines),
|
|
43
|
-
],
|
|
44
|
-
uncovered: [
|
|
45
|
-
...numericLineList(record.uncovered),
|
|
46
|
-
...numericLineList(record.uncovered_lines),
|
|
47
|
-
...numericLineList(record.uncoveredLines),
|
|
48
|
-
...numericLineList(record.missing),
|
|
49
|
-
...numericLineList(record.missing_lines),
|
|
50
|
-
...numericLineList(lines.uncovered),
|
|
51
|
-
...numericLineList(lines.uncovered_lines),
|
|
52
|
-
...numericLineList(lines.missing),
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function recordPath(record) {
|
|
58
|
-
const raw = record?.path || record?.file || record?.filename || record?.name;
|
|
59
|
-
if (Array.isArray(raw)) return raw.map(String).filter(Boolean).join("/");
|
|
60
|
-
return raw;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function reportEntries(json) {
|
|
64
|
-
const files = json?.files || json?.coverage || json;
|
|
65
|
-
if (Array.isArray(files)) {
|
|
66
|
-
return files.map((record) => [recordPath(record), record]).filter(([raw]) => raw);
|
|
67
|
-
}
|
|
68
|
-
if (files && typeof files === "object") return Object.entries(files);
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function coveragePct(record, covered, total) {
|
|
73
|
-
const summary = record?.summary && typeof record.summary === "object" ? record.summary : {};
|
|
74
|
-
return pct01(
|
|
75
|
-
record?.coverage
|
|
76
|
-
?? record?.coverage_percentage
|
|
77
|
-
?? record?.line_coverage
|
|
78
|
-
?? summary.coverage
|
|
79
|
-
?? summary.coverage_percentage
|
|
80
|
-
?? summary.percent
|
|
81
|
-
)
|
|
82
|
-
?? pctFromCounts(record?.covered, record?.coverable)
|
|
83
|
-
?? pctFromCounts(covered, total);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function readTarpaulinCoverage(filePath) {
|
|
87
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
88
|
-
return reportEntries(json).map(([path, record]) => {
|
|
89
|
-
const lines = new Map();
|
|
90
|
-
for (const trace of Array.isArray(record?.traces) ? record.traces : []) {
|
|
91
|
-
if (!trace?.stats || typeof trace.stats !== "object" || !("Line" in trace.stats)) continue;
|
|
92
|
-
addLineHit(lines, trace.line, trace.stats.Line);
|
|
93
|
-
}
|
|
94
|
-
const legacy = legacyLineGroups(record);
|
|
95
|
-
for (const line of legacy.uncovered) addLineHit(lines, line, 0);
|
|
96
|
-
for (const line of legacy.covered) addLineHit(lines, line, 1);
|
|
97
|
-
const measuredTotal = lines.size;
|
|
98
|
-
const measuredCovered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
99
|
-
const reportedTotal = Number(record?.coverable);
|
|
100
|
-
const reportedCovered = Number(record?.covered);
|
|
101
|
-
return {
|
|
102
|
-
path,
|
|
103
|
-
entry: {
|
|
104
|
-
pct: coveragePct(record, measuredCovered, measuredTotal),
|
|
105
|
-
total: Number.isFinite(reportedTotal) ? reportedTotal : measuredTotal,
|
|
106
|
-
covered: Number.isFinite(reportedCovered) ? reportedCovered : measuredCovered,
|
|
107
|
-
lines,
|
|
108
|
-
source: "tarpaulin-report.json",
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
}
|