weavatrix 0.3.13 → 1.0.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 +231 -199
- 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 +8 -65
- 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.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.js +0 -235
- 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 -62
- package/src/analysis/endpoints/extract.js +0 -107
- 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 -295
- 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 -154
- 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 -173
- 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 -174
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -85
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -219
- 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 -220
- 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,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,235 +0,0 @@
|
|
|
1
|
-
// graph-builder-coverage.js — coverage-report readers for the graph analysis. Parses Istanbul (summary/final),
|
|
2
|
-
// lcov, Python coverage.json, and Go coverage.out into per-file line-hit maps, resolving report paths onto the
|
|
3
|
-
// graph's known files. Split out of graph-builder-analysis.js (pure except for reading the repo's coverage files).
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, relative, resolve } from "node:path";
|
|
6
|
-
import { createRepoBoundary } from "../repo-path.js";
|
|
7
|
-
|
|
8
|
-
export function normRepoPath(value) {
|
|
9
|
-
return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function dirOfRepoPath(value) {
|
|
13
|
-
const p = normRepoPath(value);
|
|
14
|
-
const i = p.lastIndexOf("/");
|
|
15
|
-
return i >= 0 ? p.slice(0, i) : "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function normalizeRepoParts(value) {
|
|
19
|
-
const out = [];
|
|
20
|
-
for (const part of normRepoPath(value).split("/")) {
|
|
21
|
-
if (!part || part === ".") continue;
|
|
22
|
-
if (part === "..") out.pop();
|
|
23
|
-
else out.push(part);
|
|
24
|
-
}
|
|
25
|
-
return out.join("/");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function pct01(value) {
|
|
29
|
-
const n = Number(value);
|
|
30
|
-
if (!Number.isFinite(n)) return null;
|
|
31
|
-
return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function pctFromCounts(covered, total) {
|
|
35
|
-
const c = Number(covered);
|
|
36
|
-
const t = Number(total);
|
|
37
|
-
if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
|
|
38
|
-
return Math.max(0, Math.min(1, c / t));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveCoverageFile(rawPath, knownFiles, repoRoot) {
|
|
42
|
-
const known = (knownFiles || []).map(normalizeRepoParts).filter(Boolean);
|
|
43
|
-
const knownSet = new Set(known);
|
|
44
|
-
const candidates = [];
|
|
45
|
-
const add = (value) => {
|
|
46
|
-
const p = normalizeRepoParts(value);
|
|
47
|
-
if (p && !candidates.includes(p)) candidates.push(p);
|
|
48
|
-
};
|
|
49
|
-
const raw = String(rawPath || "").replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
|
|
50
|
-
const normalizedRaw = normRepoPath(raw);
|
|
51
|
-
add(normalizedRaw);
|
|
52
|
-
add(normalizedRaw.replace(/^\.\//, ""));
|
|
53
|
-
if (repoRoot) {
|
|
54
|
-
try {
|
|
55
|
-
const rel = normRepoPath(relative(repoRoot, raw));
|
|
56
|
-
if (rel && !rel.startsWith("..")) add(rel);
|
|
57
|
-
} catch {
|
|
58
|
-
/* best effort */
|
|
59
|
-
}
|
|
60
|
-
const rootPrefix = `${normRepoPath(resolve(repoRoot)).toLowerCase()}/`;
|
|
61
|
-
const lowerRaw = normalizedRaw.toLowerCase();
|
|
62
|
-
if (lowerRaw.startsWith(rootPrefix)) add(normalizedRaw.slice(rootPrefix.length));
|
|
63
|
-
}
|
|
64
|
-
for (const candidate of candidates) if (knownSet.has(candidate)) return candidate;
|
|
65
|
-
const lower = normalizedRaw.toLowerCase();
|
|
66
|
-
const suffixHit = known
|
|
67
|
-
.slice()
|
|
68
|
-
.sort((a, b) => b.length - a.length)
|
|
69
|
-
.find((file) => lower.endsWith(`/${file.toLowerCase()}`) || lower === file.toLowerCase());
|
|
70
|
-
return suffixHit || candidates[0] || "";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function mergeCoverageEntry(map, rawPath, entry, knownFiles, repoRoot) {
|
|
74
|
-
const file = resolveCoverageFile(rawPath, knownFiles, repoRoot);
|
|
75
|
-
if (!file) return;
|
|
76
|
-
const current = map.get(file) || {};
|
|
77
|
-
map.set(file, {
|
|
78
|
-
pct: entry.pct != null ? entry.pct : current.pct ?? null,
|
|
79
|
-
total: entry.total != null ? entry.total : current.total ?? null,
|
|
80
|
-
covered: entry.covered != null ? entry.covered : current.covered ?? null,
|
|
81
|
-
lines: entry.lines || current.lines || null,
|
|
82
|
-
source: current.source && entry.source ? `${current.source}, ${entry.source}` : entry.source || current.source || ""
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function addLineHit(lines, line, hit) {
|
|
87
|
-
const n = Math.max(1, Math.round(Number(line) || 0));
|
|
88
|
-
if (!n) return;
|
|
89
|
-
const current = lines.get(n);
|
|
90
|
-
lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function parseIstanbulSummary(map, filePath, knownFiles, repoRoot) {
|
|
94
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
95
|
-
for (const [raw, value] of Object.entries(json || {})) {
|
|
96
|
-
if (raw === "total" || !value || typeof value !== "object") continue;
|
|
97
|
-
const lines = value.lines || value.statements || {};
|
|
98
|
-
mergeCoverageEntry(map, raw, {
|
|
99
|
-
pct: pct01(lines.pct),
|
|
100
|
-
total: Number.isFinite(Number(lines.total)) ? Number(lines.total) : null,
|
|
101
|
-
covered: Number.isFinite(Number(lines.covered)) ? Number(lines.covered) : null,
|
|
102
|
-
source: "coverage-summary.json"
|
|
103
|
-
}, knownFiles, repoRoot);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseIstanbulFinal(map, filePath, knownFiles, repoRoot) {
|
|
108
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
109
|
-
for (const [raw, record] of Object.entries(json || {})) {
|
|
110
|
-
if (!record || typeof record !== "object") continue;
|
|
111
|
-
const statementMap = record.statementMap || {};
|
|
112
|
-
const hits = record.s || {};
|
|
113
|
-
const lines = new Map();
|
|
114
|
-
for (const [id, loc] of Object.entries(statementMap)) {
|
|
115
|
-
const hit = Number(hits[id]) || 0;
|
|
116
|
-
const start = Math.max(1, Math.round(Number(loc?.start?.line) || 0));
|
|
117
|
-
const end = Math.max(start, Math.round(Number(loc?.end?.line) || start));
|
|
118
|
-
for (let line = start; line <= end; line++) addLineHit(lines, line, hit);
|
|
119
|
-
}
|
|
120
|
-
const total = lines.size;
|
|
121
|
-
const covered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
122
|
-
mergeCoverageEntry(map, record.path || raw, {
|
|
123
|
-
pct: pctFromCounts(covered, total),
|
|
124
|
-
total,
|
|
125
|
-
covered,
|
|
126
|
-
lines,
|
|
127
|
-
source: "coverage-final.json"
|
|
128
|
-
}, knownFiles, repoRoot);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function parseLcov(map, filePath, knownFiles, repoRoot) {
|
|
133
|
-
const flush = (record) => {
|
|
134
|
-
if (!record || !record.file) return;
|
|
135
|
-
const total = record.total ?? record.lines.size;
|
|
136
|
-
const covered = record.covered ?? [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
137
|
-
mergeCoverageEntry(map, record.file, {
|
|
138
|
-
pct: pctFromCounts(covered, total),
|
|
139
|
-
total,
|
|
140
|
-
covered,
|
|
141
|
-
lines: record.lines,
|
|
142
|
-
source: "lcov.info"
|
|
143
|
-
}, knownFiles, repoRoot);
|
|
144
|
-
};
|
|
145
|
-
let current = null;
|
|
146
|
-
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
147
|
-
const line = rawLine.trim();
|
|
148
|
-
if (line.startsWith("SF:")) {
|
|
149
|
-
flush(current);
|
|
150
|
-
current = { file: line.slice(3), lines: new Map(), total: null, covered: null };
|
|
151
|
-
} else if (current && line.startsWith("DA:")) {
|
|
152
|
-
const [lineNo, count] = line.slice(3).split(",");
|
|
153
|
-
addLineHit(current.lines, lineNo, count);
|
|
154
|
-
} else if (current && line.startsWith("LF:")) {
|
|
155
|
-
current.total = Number(line.slice(3)) || current.total;
|
|
156
|
-
} else if (current && line.startsWith("LH:")) {
|
|
157
|
-
current.covered = Number(line.slice(3)) || current.covered;
|
|
158
|
-
} else if (line === "end_of_record") {
|
|
159
|
-
flush(current);
|
|
160
|
-
current = null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
flush(current);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseCoveragePyJson(map, filePath, knownFiles, repoRoot) {
|
|
167
|
-
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
168
|
-
for (const [raw, file] of Object.entries(json?.files || {})) {
|
|
169
|
-
const lines = new Map();
|
|
170
|
-
for (const line of file.executed_lines || []) addLineHit(lines, line, 1);
|
|
171
|
-
for (const line of file.missing_lines || []) if (!lines.has(Number(line))) addLineHit(lines, line, 0);
|
|
172
|
-
const summary = file.summary || {};
|
|
173
|
-
const total = Number(summary.num_statements) || lines.size;
|
|
174
|
-
const covered = Number(summary.covered_lines) || [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
175
|
-
mergeCoverageEntry(map, raw, {
|
|
176
|
-
pct: pct01(summary.percent_covered ?? summary.percent_covered_display) ?? pctFromCounts(covered, total),
|
|
177
|
-
total,
|
|
178
|
-
covered,
|
|
179
|
-
lines,
|
|
180
|
-
source: "coverage.json"
|
|
181
|
-
}, knownFiles, repoRoot);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function parseGoCoverage(map, filePath, knownFiles, repoRoot) {
|
|
186
|
-
const byFile = new Map();
|
|
187
|
-
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
188
|
-
const line = rawLine.trim();
|
|
189
|
-
if (!line || line.startsWith("mode:")) continue;
|
|
190
|
-
const m = line.match(/^(.+):(\d+)\.\d+,(\d+)\.\d+\s+\d+\s+(\d+)$/);
|
|
191
|
-
if (!m) continue;
|
|
192
|
-
const record = byFile.get(m[1]) || { lines: new Map() };
|
|
193
|
-
const start = Math.max(1, Number(m[2]) || 1);
|
|
194
|
-
const end = Math.max(start, Number(m[3]) || start);
|
|
195
|
-
for (let n = start; n <= end; n++) addLineHit(record.lines, n, Number(m[4]) || 0);
|
|
196
|
-
byFile.set(m[1], record);
|
|
197
|
-
}
|
|
198
|
-
for (const [raw, record] of byFile) {
|
|
199
|
-
const total = record.lines.size;
|
|
200
|
-
const covered = [...record.lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
201
|
-
mergeCoverageEntry(map, raw, {
|
|
202
|
-
pct: pctFromCounts(covered, total),
|
|
203
|
-
total,
|
|
204
|
-
covered,
|
|
205
|
-
lines: record.lines,
|
|
206
|
-
source: "coverage.out"
|
|
207
|
-
}, knownFiles, repoRoot);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function readCoverageForRepo(repoRoot, knownFiles) {
|
|
212
|
-
const out = new Map();
|
|
213
|
-
if (!repoRoot) return out;
|
|
214
|
-
const boundary = createRepoBoundary(repoRoot);
|
|
215
|
-
const candidates = [
|
|
216
|
-
["coverage/coverage-summary.json", parseIstanbulSummary],
|
|
217
|
-
["coverage/coverage-final.json", parseIstanbulFinal],
|
|
218
|
-
["coverage/lcov.info", parseLcov],
|
|
219
|
-
["lcov.info", parseLcov],
|
|
220
|
-
["coverage/coverage.json", parseCoveragePyJson],
|
|
221
|
-
["coverage.json", parseCoveragePyJson],
|
|
222
|
-
["coverage.out", parseGoCoverage],
|
|
223
|
-
["cover.out", parseGoCoverage]
|
|
224
|
-
];
|
|
225
|
-
for (const [candidate, parser] of candidates) {
|
|
226
|
-
const resolved = boundary.resolve(candidate);
|
|
227
|
-
if (!resolved.ok || !existsSync(resolved.path)) continue;
|
|
228
|
-
try {
|
|
229
|
-
parser(out, resolved.path, knownFiles, repoRoot);
|
|
230
|
-
} catch {
|
|
231
|
-
/* malformed/foreign coverage report: ignore */
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const MAX_REPRESENTATIVE_CYCLE_FILES = 64;
|
|
2
|
-
|
|
3
|
-
// Keep the complete closed route for normal cycles. Very large SCCs still get a closed, deterministic
|
|
4
|
-
// head/tail route without letting one finding consume the whole audit response.
|
|
5
|
-
export function formatRepresentativeCycle(cycle, maxFiles = MAX_REPRESENTATIVE_CYCLE_FILES) {
|
|
6
|
-
const raw = Array.isArray(cycle) ? cycle.map(String).filter(Boolean) : [];
|
|
7
|
-
if (!raw.length) return "";
|
|
8
|
-
const closed = raw.length > 1 && raw[0] === raw[raw.length - 1] ? raw : [...raw, raw[0]];
|
|
9
|
-
const cap = Math.max(4, Number(maxFiles) || MAX_REPRESENTATIVE_CYCLE_FILES);
|
|
10
|
-
if (closed.length <= cap + 1) return closed.join(" → ");
|
|
11
|
-
const side = Math.max(2, Math.floor(cap / 2));
|
|
12
|
-
const omitted = closed.length - side * 2;
|
|
13
|
-
return `${closed.slice(0, side).join(" → ")} → … ${omitted} file(s) omitted … → ${closed.slice(-side).join(" → ")}`;
|
|
14
|
-
}
|