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,207 +0,0 @@
|
|
|
1
|
-
import {posix} from 'node:path'
|
|
2
|
-
import {analyzeEndpointInventory} from '../analysis/endpoints.js'
|
|
3
|
-
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
4
|
-
import {sourceExcerpt} from './tools-source.mjs'
|
|
5
|
-
import {toolResult} from './tool-result.mjs'
|
|
6
|
-
|
|
7
|
-
const NON_PRODUCT = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
8
|
-
|
|
9
|
-
const symbolLine = (node) => Number(String(node?.source_location || '').match(/L(\d+)/)?.[1]
|
|
10
|
-
|| String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
|
|
11
|
-
|
|
12
|
-
const symbolName = (node) => String(node?.label || node?.id || '')
|
|
13
|
-
.replace(/\([^)]*\).*$/, '')
|
|
14
|
-
.split(/[.#]/).at(-1)
|
|
15
|
-
.trim()
|
|
16
|
-
|
|
17
|
-
function codeFiles(graph) {
|
|
18
|
-
return [...new Set((graph?.nodes || [])
|
|
19
|
-
.filter((node) => !String(node.id).includes('#') && node.source_file && node.file_type === 'code')
|
|
20
|
-
.map((node) => node.source_file))]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function endpointCandidates(inventory, args) {
|
|
24
|
-
const path = String(args.path || '').trim()
|
|
25
|
-
const method = args.method ? String(args.method).toUpperCase() : null
|
|
26
|
-
if (!path) return []
|
|
27
|
-
const eligible = inventory.endpoints.filter((endpoint) => !method || endpoint.method === method)
|
|
28
|
-
const exact = eligible.filter((endpoint) => endpoint.path === path)
|
|
29
|
-
return exact.length ? exact : eligible.filter((endpoint) => endpoint.path.endsWith(path))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// A handler_file hint narrows the FULL same-named pool before any scoring, so it can rescue a
|
|
33
|
-
// candidate the ranking would otherwise collapse away. An exact repo-relative match beats suffix
|
|
34
|
-
// matches; comparison is case-insensitive (Windows paths and human-typed hints disagree on casing).
|
|
35
|
-
function matchesHandlerHint(file, hint) {
|
|
36
|
-
if (!hint) return true
|
|
37
|
-
const lower = file.toLowerCase()
|
|
38
|
-
return lower === hint || lower.endsWith(`/${hint}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function handlerCandidates(graph, endpoint, hint = '') {
|
|
42
|
-
if (!endpoint.handler) return []
|
|
43
|
-
const wanted = endpoint.handler.toLowerCase()
|
|
44
|
-
const qualifier = String(endpoint.handlerRef || '').split('.').slice(-2, -1)[0]?.toLowerCase() || ''
|
|
45
|
-
const routeDir = posix.dirname(endpoint.file)
|
|
46
|
-
// File-level import/re-export bindings at the route file break same-name ties between otherwise
|
|
47
|
-
// unranked candidates. The bonus deliberately sits BELOW same-directory proximity (+40): a
|
|
48
|
-
// file-level edge proves coupling, not symbol identity, and must never outrank the conventional
|
|
49
|
-
// same-package/same-dir handler (Go same-package files have no import edge at all).
|
|
50
|
-
const importBound = new Set()
|
|
51
|
-
for (const edge of graph.out.get(endpoint.file) || []) {
|
|
52
|
-
if (edge.relation !== 'imports' && edge.relation !== 're_exports') continue
|
|
53
|
-
if (edge.typeOnly === true || edge.barrelProxy === true) continue
|
|
54
|
-
const targetId = String(edge.id)
|
|
55
|
-
if (targetId.includes('#')) continue
|
|
56
|
-
const target = graph.byId.get(targetId)
|
|
57
|
-
importBound.add(String(target?.source_file || targetId).replace(/\\/g, '/'))
|
|
58
|
-
}
|
|
59
|
-
let pool = (graph.nodes || [])
|
|
60
|
-
.filter((node) => String(node.id).includes('#') && node.source_file && symbolName(node).toLowerCase() === wanted)
|
|
61
|
-
if (hint) {
|
|
62
|
-
pool = pool.filter((node) => matchesHandlerHint(String(node.source_file).replace(/\\/g, '/').toLowerCase(), hint))
|
|
63
|
-
const exact = pool.filter((node) => String(node.source_file).replace(/\\/g, '/').toLowerCase() === hint)
|
|
64
|
-
if (exact.length) pool = exact
|
|
65
|
-
}
|
|
66
|
-
const scored = pool
|
|
67
|
-
.map((node) => {
|
|
68
|
-
const file = String(node.source_file).replace(/\\/g, '/')
|
|
69
|
-
let score = 0
|
|
70
|
-
const importBoundAtRoute = importBound.has(file)
|
|
71
|
-
if (file === endpoint.file) score += 100
|
|
72
|
-
if (importBoundAtRoute) score += 35
|
|
73
|
-
if (posix.dirname(file) === routeDir) score += 40
|
|
74
|
-
if (file.startsWith(`${routeDir}/`)) score += 15
|
|
75
|
-
if (qualifier) {
|
|
76
|
-
const basename = posix.basename(file).replace(/\.[^.]+$/, '').toLowerCase()
|
|
77
|
-
if (basename === qualifier || basename.endsWith(`.${qualifier}`) || basename.endsWith(`-${qualifier}`)) score += 60
|
|
78
|
-
}
|
|
79
|
-
const line = symbolLine(node)
|
|
80
|
-
if (file === endpoint.file && line >= endpoint.line && line - endpoint.line <= 250) score += 20
|
|
81
|
-
return {node, score, importBoundAtRoute}
|
|
82
|
-
})
|
|
83
|
-
.sort((a, b) => b.score - a.score || String(a.node.id).localeCompare(String(b.node.id)))
|
|
84
|
-
if (!scored.length) return []
|
|
85
|
-
const best = scored[0].score
|
|
86
|
-
return scored.filter((item) => item.score === best).map(({node, importBoundAtRoute}) => ({node, importBoundAtRoute}))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function traceCalls(graph, start, {maxDepth, maxNodes, includeClassified, classifier}) {
|
|
90
|
-
const allowed = (node) => {
|
|
91
|
-
if (!node?.source_file) return false
|
|
92
|
-
if (includeClassified) return true
|
|
93
|
-
const info = classifier.explain(node.source_file, {content: ''})
|
|
94
|
-
return !info?.excluded && !NON_PRODUCT.some((name) => hasPathClass(info, name))
|
|
95
|
-
}
|
|
96
|
-
const queue = [{id: String(start.id), depth: 0}]
|
|
97
|
-
const seen = new Set([String(start.id)])
|
|
98
|
-
const edges = []
|
|
99
|
-
let truncated = false
|
|
100
|
-
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
101
|
-
const current = queue[cursor]
|
|
102
|
-
if (current.depth >= maxDepth) continue
|
|
103
|
-
const outgoing = (graph.out.get(current.id) || [])
|
|
104
|
-
.filter((edge) => edge.relation === 'calls' && edge.typeOnly !== true && edge.compileOnly !== true && edge.barrelProxy !== true)
|
|
105
|
-
.map((edge) => ({edge, target: graph.byId.get(String(edge.id))}))
|
|
106
|
-
.filter(({target}) => target && String(target.id).includes('#') && allowed(target))
|
|
107
|
-
.sort((a, b) => (Number(a.edge.line) || 0) - (Number(b.edge.line) || 0) || String(a.target.id).localeCompare(String(b.target.id)))
|
|
108
|
-
const unique = new Set()
|
|
109
|
-
for (const {edge, target} of outgoing) {
|
|
110
|
-
const targetId = String(target.id)
|
|
111
|
-
if (unique.has(targetId)) continue
|
|
112
|
-
unique.add(targetId)
|
|
113
|
-
if (edges.length >= maxNodes - 1) { truncated = true; break }
|
|
114
|
-
edges.push({
|
|
115
|
-
from: current.id,
|
|
116
|
-
to: targetId,
|
|
117
|
-
depth: current.depth + 1,
|
|
118
|
-
relation: edge.relation,
|
|
119
|
-
provenance: edge.provenance || 'UNKNOWN',
|
|
120
|
-
line: Number(edge.line) || symbolLine(graph.byId.get(current.id)) || 1,
|
|
121
|
-
})
|
|
122
|
-
if (!seen.has(targetId)) {
|
|
123
|
-
seen.add(targetId)
|
|
124
|
-
queue.push({id: targetId, depth: current.depth + 1})
|
|
125
|
-
}
|
|
126
|
-
if (unique.size >= 6) { truncated = outgoing.length > unique.size; break }
|
|
127
|
-
}
|
|
128
|
-
if (truncated && edges.length >= maxNodes - 1) break
|
|
129
|
-
}
|
|
130
|
-
return {edges, truncated, nodeCount: seen.size}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function endpointLine(endpoint) {
|
|
134
|
-
const mount = endpoint.mountChain?.length
|
|
135
|
-
? endpoint.mountChain.map((item) => `${item.file}:${item.line} ${item.path}`).join(' → ')
|
|
136
|
-
: 'no static router mount chain'
|
|
137
|
-
const activation = endpoint.conditional
|
|
138
|
-
? `; conditional default ${endpoint.defaultActive === false ? 'inactive' : endpoint.defaultActive === true ? 'active' : 'unknown'}`
|
|
139
|
-
: ''
|
|
140
|
-
return `${endpoint.method} ${endpoint.path} → ${endpoint.handler || 'inline/unknown'} (${endpoint.file}:${endpoint.line}; ${endpoint.mountState}/${endpoint.confidence}${activation}; declared ${endpoint.declaredPath}; ${mount})`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function tTraceEndpoint(graph, args, ctx) {
|
|
144
|
-
if (!ctx?.repoRoot) return 'Endpoint tracing needs the repo root (not provided to this server).'
|
|
145
|
-
const inventory = analyzeEndpointInventory(ctx.repoRoot, codeFiles(graph))
|
|
146
|
-
const candidates = endpointCandidates(inventory, args)
|
|
147
|
-
if (!candidates.length) return toolResult(`No endpoint matched ${args.method ? `${String(args.method).toUpperCase()} ` : ''}${args.path || '(missing path)'}.`, {
|
|
148
|
-
status: 'NOT_FOUND', query: {path: args.path || null, method: args.method || null}, inventory: inventory.stats,
|
|
149
|
-
})
|
|
150
|
-
if (candidates.length > 1) return toolResult([
|
|
151
|
-
`Endpoint query is ambiguous (${candidates.length} matches). Pass the exact composed path and method:`,
|
|
152
|
-
...candidates.slice(0, 20).map((endpoint) => ` ${endpointLine(endpoint)}`),
|
|
153
|
-
].join('\n'), {status: 'AMBIGUOUS', candidates: candidates.slice(0, 20), inventory: inventory.stats}, {
|
|
154
|
-
completeness: {status: 'PARTIAL', reason: 'ambiguous endpoint'},
|
|
155
|
-
})
|
|
156
|
-
const endpoint = candidates[0]
|
|
157
|
-
const classifier = createPathClassifier(ctx.repoRoot)
|
|
158
|
-
const classify = (file) => {
|
|
159
|
-
const info = classifier.explain(file, {content: ''})
|
|
160
|
-
return {classified: info.excluded || NON_PRODUCT.some((name) => hasPathClass(info, name)), pathClasses: info.classes}
|
|
161
|
-
}
|
|
162
|
-
const hint = String(args.handler_file || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
163
|
-
let handlers = handlerCandidates(graph, endpoint, hint)
|
|
164
|
-
.map((item) => ({...item, ...classify(String(item.node.source_file).replace(/\\/g, '/'))}))
|
|
165
|
-
// Production-first: a classified twin (tests/mocks/…) never ties with a production candidate,
|
|
166
|
-
// but an all-classified candidate set is kept rather than emptied.
|
|
167
|
-
if (args.include_classified !== true && handlers.some((item) => !item.classified)) {
|
|
168
|
-
handlers = handlers.filter((item) => !item.classified)
|
|
169
|
-
}
|
|
170
|
-
if (handlers.length !== 1) return toolResult([
|
|
171
|
-
`Endpoint resolved, but its handler symbol is ${handlers.length ? 'ambiguous' : `not present in the graph${hint ? ` (no candidate matched handler_file "${hint}")` : ''}`}:`,
|
|
172
|
-
` ${endpointLine(endpoint)}`,
|
|
173
|
-
...handlers.slice(0, 12).map(({node, importBoundAtRoute, pathClasses}) => ` candidate ${node.label || node.id} (${node.source_file}:${symbolLine(node) || '?'}; import-bound at route: ${importBoundAtRoute ? 'yes' : 'no'}${pathClasses.length ? `; classified:${pathClasses.join('+')}` : ''}) [${node.id}]`),
|
|
174
|
-
...(handlers.length > 1 ? [' Next: pass handler_file with the repo-relative path (or an unambiguous path suffix) of the file that declares the intended handler.'] : []),
|
|
175
|
-
].join('\n'), {status: handlers.length ? 'AMBIGUOUS_HANDLER' : 'HANDLER_NOT_FOUND', endpoint, handlers: handlers.map(({node, importBoundAtRoute, pathClasses}) => ({...node, importBoundAtRoute, pathClasses}))}, {
|
|
176
|
-
completeness: {status: 'PARTIAL', reason: handlers.length ? 'ambiguous handler symbol' : 'handler symbol not found'},
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
const maxDepth = Math.max(1, Math.min(4, Number(args.max_depth) || 3))
|
|
180
|
-
const maxNodes = Math.max(2, Math.min(40, Number(args.max_nodes) || 20))
|
|
181
|
-
const contextLines = Math.max(0, Math.min(6, Number(args.context_lines) || 2))
|
|
182
|
-
const maxExcerpts = Math.max(0, Math.min(12, Number(args.max_excerpts) || 6))
|
|
183
|
-
const handler = handlers[0].node
|
|
184
|
-
const traced = traceCalls(graph, handler, {
|
|
185
|
-
maxDepth, maxNodes, includeClassified: args.include_classified === true, classifier,
|
|
186
|
-
})
|
|
187
|
-
const excerpts = traced.edges.slice(0, maxExcerpts).map((edge) => {
|
|
188
|
-
const source = graph.byId.get(edge.from)
|
|
189
|
-
return sourceExcerpt(ctx.repoRoot, source?.source_file, edge.line, contextLines)
|
|
190
|
-
}).filter(Boolean)
|
|
191
|
-
const lines = [
|
|
192
|
-
`Endpoint trace (${traced.truncated ? 'PARTIAL' : 'COMPLETE'}; depth ≤${maxDepth}, nodes ${traced.nodeCount}/${maxNodes}):`,
|
|
193
|
-
` ${endpointLine(endpoint)}`,
|
|
194
|
-
` handler ${handler.label || handler.id} (${handler.source_file}:${symbolLine(handler) || '?'}) [${handler.id}]`,
|
|
195
|
-
traced.edges.length ? 'Call graph:' : 'Call graph: no outgoing call edges from the resolved handler.',
|
|
196
|
-
...traced.edges.map((edge) => {
|
|
197
|
-
const from = graph.byId.get(edge.from), to = graph.byId.get(edge.to)
|
|
198
|
-
return ` ${' '.repeat(Math.max(0, edge.depth - 1))}${from?.label || edge.from} → ${to?.label || edge.to} (${from?.source_file}:${edge.line}; ${edge.provenance})`
|
|
199
|
-
}),
|
|
200
|
-
...excerpts.flatMap((excerpt) => ['', `Call-site excerpt (${excerpt.file}:${excerpt.startLine}-${excerpt.endLine}, focus ${excerpt.focusLine}):`, excerpt.text]),
|
|
201
|
-
]
|
|
202
|
-
return toolResult(lines.join('\n'), {
|
|
203
|
-
status: traced.truncated ? 'PARTIAL' : 'COMPLETE', endpoint, handler: {
|
|
204
|
-
id: String(handler.id), label: handler.label || String(handler.id), file: handler.source_file, line: symbolLine(handler),
|
|
205
|
-
}, trace: traced, excerpts, inventory: inventory.stats,
|
|
206
|
-
}, {completeness: {status: traced.truncated ? 'PARTIAL' : 'COMPLETE', reason: traced.truncated ? 'bounded trace cap reached' : 'bounded outgoing call graph exhausted'}})
|
|
207
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// Connectivity-hub reporting, split from the general graph query tools.
|
|
2
|
-
import {connList} from './graph-context.mjs'
|
|
3
|
-
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
4
|
-
|
|
5
|
-
const isCompileTimeEdge = (edge) => edge?.typeOnly === true || edge?.compileOnly === true
|
|
6
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
|
-
const sourceFileOf = (node) => String(node?.source_file || (node?.file_type === 'code' ? node?.id : '') || '').replace(/\\/g, '/')
|
|
8
|
-
|
|
9
|
-
export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx = {}) {
|
|
10
|
-
const n = Math.max(1, Math.min(100, Number(top_n) || 10))
|
|
11
|
-
const classifier = createPathClassifier(ctx.repoRoot || null)
|
|
12
|
-
const classificationByFile = new Map()
|
|
13
|
-
const isNonProduct = (node) => {
|
|
14
|
-
if (include_classified === true) return false
|
|
15
|
-
if (node?.test_surface === true) return true
|
|
16
|
-
const file = sourceFileOf(node)
|
|
17
|
-
if (!file) return false
|
|
18
|
-
if (!classificationByFile.has(file)) classificationByFile.set(file, classifier.explain(file))
|
|
19
|
-
const info = classificationByFile.get(file)
|
|
20
|
-
return info.excluded || hasPathClass(info, ...NON_PRODUCT_CLASSES)
|
|
21
|
-
}
|
|
22
|
-
const excludedIds = new Set(g.nodes.filter(isNonProduct).map((node) => String(node.id)))
|
|
23
|
-
const scored = g.nodes
|
|
24
|
-
.filter((node) => !excludedIds.has(String(node.id)))
|
|
25
|
-
.map((node) => {
|
|
26
|
-
// Coupling from a suppressed artifact must not inflate an otherwise valid product hub.
|
|
27
|
-
const rawOuts = (g.out.get(String(node.id)) || []).filter((edge) => !excludedIds.has(String(edge.id)))
|
|
28
|
-
const rawIns = (g.inn.get(String(node.id)) || []).filter((edge) => !excludedIds.has(String(edge.id)))
|
|
29
|
-
const outs = connList(rawOuts)
|
|
30
|
-
const ins = connList(rawIns)
|
|
31
|
-
const ownedMethods = new Set(rawOuts.filter((e) => e.relation === 'method').map((e) => String(e.id)))
|
|
32
|
-
const outIds = new Set(outs.map((e) => String(e.id)))
|
|
33
|
-
const inIds = new Set(ins.map((e) => String(e.id)))
|
|
34
|
-
const allIds = new Set([...outIds, ...inIds])
|
|
35
|
-
const runtimeIds = new Set([...outs, ...ins].filter((e) => !isCompileTimeEdge(e)).map((e) => String(e.id)))
|
|
36
|
-
const compileOnlyIds = new Set([...outs, ...ins].filter(isCompileTimeEdge).map((e) => String(e.id)))
|
|
37
|
-
for (const id of runtimeIds) compileOnlyIds.delete(id)
|
|
38
|
-
const occurrences = outs.length + ins.length
|
|
39
|
-
return {
|
|
40
|
-
node,
|
|
41
|
-
deg: allIds.size,
|
|
42
|
-
runtime: runtimeIds.size,
|
|
43
|
-
compileOnly: compileOnlyIds.size,
|
|
44
|
-
out: outIds.size,
|
|
45
|
-
in: inIds.size,
|
|
46
|
-
occurrences,
|
|
47
|
-
ownedMethods: ownedMethods.size,
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
.filter((entry) => entry.deg > 0 || entry.ownedMethods > 0)
|
|
51
|
-
.sort((a, b) => b.runtime - a.runtime || b.deg - a.deg || b.ownedMethods - a.ownedMethods || b.occurrences - a.occurrences)
|
|
52
|
-
const ranked = scored.slice(0, n)
|
|
53
|
-
const rankedIds = new Set(ranked.map((entry) => String(entry.node.id)))
|
|
54
|
-
// Unique neighbors measure coupling, but a large component repeatedly calling the same helper (for
|
|
55
|
-
// example i18n) can still be a valuable complexity hotspot. Preserve that second lens explicitly
|
|
56
|
-
// instead of letting repeated sites inflate the coupling rank.
|
|
57
|
-
const occurrenceHotspots = scored
|
|
58
|
-
.filter((entry) => !rankedIds.has(String(entry.node.id)))
|
|
59
|
-
.filter((entry) => entry.occurrences >= 20 && entry.occurrences - entry.deg >= 10)
|
|
60
|
-
.sort((a, b) => (b.occurrences - b.deg) - (a.occurrences - a.deg) || b.occurrences - a.occurrences)
|
|
61
|
-
.slice(0, Math.min(5, n))
|
|
62
|
-
return [
|
|
63
|
-
`Top ${ranked.length} connectivity hubs (ranked by unique runtime call/import/reference neighbors; structural ownership shown separately):`,
|
|
64
|
-
...ranked.map(
|
|
65
|
-
(r, i) =>
|
|
66
|
-
`${String(i + 1).padStart(2)}. ${r.node.label ?? r.node.id} (${r.deg} unique: ${r.runtime} runtime, ${r.compileOnly} compile-only; out ${r.out}, in ${r.in}; ${r.occurrences} edge occurrence${r.occurrences === 1 ? '' : 's'}${r.ownedMethods ? `; owns ${r.ownedMethods} method${r.ownedMethods === 1 ? '' : 's'}` : ''}) [${r.node.id}]`
|
|
67
|
-
),
|
|
68
|
-
`Repeated call sites affect the occurrence count, not the connectivity rank; compile-only neighbors are secondary to runtime coupling.`,
|
|
69
|
-
occurrenceHotspots.length ? `` : null,
|
|
70
|
-
occurrenceHotspots.length ? `High occurrence hotspots outside the connectivity rank (repeated call/reference sites; complexity signal, not broader coupling):` : null,
|
|
71
|
-
...occurrenceHotspots.map((r) =>
|
|
72
|
-
` ${r.node.label ?? r.node.id} (${r.occurrences} occurrences across ${r.deg} unique neighbors; ${r.occurrences - r.deg} repeats) [${r.node.id}]`
|
|
73
|
-
),
|
|
74
|
-
excludedIds.size > 0 && include_classified !== true
|
|
75
|
-
? `${excludedIds.size} node(s) classified as tests/e2e/generated/build output/mocks/stories/docs/benchmarks/temp or explicitly excluded were omitted; pass include_classified:true to inspect them.`
|
|
76
|
-
: null,
|
|
77
|
-
].filter((line) => line != null).join('\n')
|
|
78
|
-
}
|
package/src/mcp/tools-graph.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Stable public facade for graph inspection and traversal tools. Propagate the catalog's
|
|
2
|
-
// cache-busting query into owner modules so edits go live without an MCP reconnect.
|
|
3
|
-
const version = new URL(import.meta.url).search
|
|
4
|
-
const load = (path) => import(new URL(`${path}${version}`, import.meta.url).href)
|
|
5
|
-
const [core, query, hubs] = await Promise.all([
|
|
6
|
-
load('./graph/tools-core.mjs'),
|
|
7
|
-
load('./graph/tools-query.mjs'),
|
|
8
|
-
load('./tools-graph-hubs.mjs'),
|
|
9
|
-
])
|
|
10
|
-
|
|
11
|
-
export const tGraphStats = core.tGraphStats
|
|
12
|
-
export const tGetNode = core.tGetNode
|
|
13
|
-
export const tGetNeighbors = core.tGetNeighbors
|
|
14
|
-
export const tGetCommunity = core.tGetCommunity
|
|
15
|
-
export const tQueryGraph = query.tQueryGraph
|
|
16
|
-
export const tShortestPath = query.tShortestPath
|
|
17
|
-
export const tGodNodes = hubs.tGodNodes
|
package/src/mcp/tools-health.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Propagate catalog.mjs's cache-busting query to owner modules so development hot reload remains
|
|
2
|
-
// behaviorally identical to the former single-file implementation.
|
|
3
|
-
const version = new URL(import.meta.url).search
|
|
4
|
-
const load = (path) => import(new URL(`${path}${version}`, import.meta.url).href)
|
|
5
|
-
const [duplicates, deadCode, auditFormat, audit, structure, endpoints] = await Promise.all([
|
|
6
|
-
load('./health/duplicates.mjs'),
|
|
7
|
-
load('./health/dead-code.mjs'),
|
|
8
|
-
load('./health/audit-format.mjs'),
|
|
9
|
-
load('./health/audit.mjs'),
|
|
10
|
-
load('./health/structure.mjs'),
|
|
11
|
-
load('./health/endpoints.mjs'),
|
|
12
|
-
])
|
|
13
|
-
|
|
14
|
-
export const tFindDuplicates = duplicates.tFindDuplicates
|
|
15
|
-
export const tFindDeadCode = deadCode.tFindDeadCode
|
|
16
|
-
export const auditFindingPathScope = auditFormat.auditFindingPathScope
|
|
17
|
-
export const formatAuditFinding = auditFormat.formatAuditFinding
|
|
18
|
-
export const isDependencyAuditFinding = auditFormat.isDependencyAuditFinding
|
|
19
|
-
export const tRunAudit = audit.tRunAudit
|
|
20
|
-
export const tCoverageMap = structure.tCoverageMap
|
|
21
|
-
export const tHotPathReview = structure.tHotPathReview
|
|
22
|
-
export const tListCommunities = structure.tListCommunities
|
|
23
|
-
export const tModuleMap = structure.tModuleMap
|
|
24
|
-
export const tListEndpoints = endpoints.tListEndpoints
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Behavioral architecture evidence from bounded, local git history.
|
|
2
|
-
import {analyzeGitHistory, boundGitHistoryAnalytics, formatGitHistoryAnalytics} from '../analysis/git-history.js'
|
|
3
|
-
import {toolResult} from './tool-result.mjs'
|
|
4
|
-
|
|
5
|
-
export function gitHistoryToolResult(result, args = {}) {
|
|
6
|
-
const bounded = boundGitHistoryAnalytics(result, {topN: args.top_n})
|
|
7
|
-
return toolResult(formatGitHistoryAnalytics(bounded.result, {topN: args.top_n}), bounded.result, {
|
|
8
|
-
page: bounded.page,
|
|
9
|
-
completeness: {
|
|
10
|
-
status: result.status,
|
|
11
|
-
complete: result.completeness?.complete === true,
|
|
12
|
-
reasons: result.completeness?.reasons || [],
|
|
13
|
-
},
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function tGitHistory(g, args = {}, ctx) {
|
|
18
|
-
if (!ctx?.repoRoot) return toolResult('Git history intelligence is unavailable: no repository root is active.', {status: 'unavailable'})
|
|
19
|
-
const result = await analyzeGitHistory({
|
|
20
|
-
repoRoot: ctx.repoRoot,
|
|
21
|
-
graph: g,
|
|
22
|
-
months: args.months,
|
|
23
|
-
maxCommits: args.max_commits,
|
|
24
|
-
maxPairs: args.max_pairs,
|
|
25
|
-
minPairCount: args.min_pair_count,
|
|
26
|
-
})
|
|
27
|
-
return gitHistoryToolResult(result, args)
|
|
28
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
// Symbol-aware change_impact implementation, isolated from the other hot impact tools so its
|
|
2
|
-
// classifier/evidence contract can be tested without perturbing get_dependents or graph_diff.
|
|
3
|
-
import {runGit} from '../git-exec.js'
|
|
4
|
-
import {fileOfId} from '../graph/node-id.js'
|
|
5
|
-
import {classifyChangeImpact} from '../analysis/change-classification.js'
|
|
6
|
-
import {readCoverageForRepo} from '../analysis/coverage-reports.js'
|
|
7
|
-
import {computeStaticTestReachability} from '../analysis/static-test-reachability.js'
|
|
8
|
-
import {degreeOf, labelOf, rawGraph} from './graph-context.mjs'
|
|
9
|
-
import {reverseReach} from './graph/reverse-reach.mjs'
|
|
10
|
-
import {gitLines} from './git-output.mjs'
|
|
11
|
-
import {toolResult} from './tool-result.mjs'
|
|
12
|
-
|
|
13
|
-
function gitValue(repoRoot, args) {
|
|
14
|
-
const result = runGit(repoRoot, args)
|
|
15
|
-
if (result.status !== 0) return null
|
|
16
|
-
return String(result.stdout || '').trim() || null
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function resolveImpactBase(repoRoot, requested) {
|
|
20
|
-
const candidates = requested ? [requested] : ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master']
|
|
21
|
-
for (const ref of candidates) {
|
|
22
|
-
const value = gitValue(repoRoot, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`])
|
|
23
|
-
if (value) return ref
|
|
24
|
-
}
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const impactKind = (entry) => entry?.runtimeDepth != null
|
|
29
|
-
? (entry.compileDepth != null ? `runtime + compile-time(d${entry.compileDepth})` : 'runtime')
|
|
30
|
-
: 'compile-time'
|
|
31
|
-
|
|
32
|
-
const partialResult = (text, reason, status = 'UNAVAILABLE') => toolResult(text, {
|
|
33
|
-
status,
|
|
34
|
-
verdict: 'HIGH',
|
|
35
|
-
reasons: [reason],
|
|
36
|
-
changes: [],
|
|
37
|
-
seeds: {ids: [], unmappedIds: []},
|
|
38
|
-
blastRadius: {impacted: 0, runtime: 0, compileTimeOnly: 0, nodes: []},
|
|
39
|
-
}, {completeness: {status: 'PARTIAL', reason}})
|
|
40
|
-
|
|
41
|
-
export function tChangeImpactV2(g, args = {}, ctx = {}) {
|
|
42
|
-
if (!ctx.repoRoot) return partialResult(
|
|
43
|
-
'HIGH — change impact unavailable\nNo repository root is active; no diff can be classified safely.',
|
|
44
|
-
'Repository root unavailable.'
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
const explicit = Array.isArray(args.files)
|
|
48
|
-
? [...new Set(args.files.map((file) => String(file).replace(/\\/g, '/').trim()).filter(Boolean))]
|
|
49
|
-
: null
|
|
50
|
-
if (explicit && !explicit.length) return partialResult(
|
|
51
|
-
'HIGH — invalid change set\nfiles was provided but empty; pass repo-relative paths, or omit it to diff local changes.',
|
|
52
|
-
'Explicit files list was empty.',
|
|
53
|
-
'INVALID'
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
const graph = rawGraph(ctx)
|
|
57
|
-
const providedDiff = typeof args.diff === 'string'
|
|
58
|
-
let sourceLabel
|
|
59
|
-
let classification
|
|
60
|
-
if (providedDiff) {
|
|
61
|
-
sourceLabel = explicit ? `from provided unified diff + ${explicit.length} file hint(s)` : 'from provided unified diff'
|
|
62
|
-
classification = classifyChangeImpact({repoRoot: ctx.repoRoot, graph, diffText: args.diff, files: explicit || []})
|
|
63
|
-
} else if (explicit) {
|
|
64
|
-
sourceLabel = `for ${explicit.length} provided file(s) (no diff; conservative fallback)`
|
|
65
|
-
classification = classifyChangeImpact({repoRoot: ctx.repoRoot, graph, files: explicit})
|
|
66
|
-
} else {
|
|
67
|
-
const base = resolveImpactBase(ctx.repoRoot, args.base ? String(args.base).trim() : '')
|
|
68
|
-
if (!base) return partialResult(
|
|
69
|
-
`HIGH — change impact unavailable\nCould not resolve base ref${args.base ? ` "${args.base}"` : ''}; pass base explicitly (for example origin/main or HEAD~1).`,
|
|
70
|
-
'Base ref could not be resolved.'
|
|
71
|
-
)
|
|
72
|
-
const mergeBase = gitValue(ctx.repoRoot, ['merge-base', base, 'HEAD']) || base
|
|
73
|
-
const untracked = gitLines(ctx.repoRoot, ['ls-files', '--others', '--exclude-standard']) || []
|
|
74
|
-
sourceLabel = `vs ${base}${mergeBase !== base ? ` (merge-base ${mergeBase.slice(0, 12)})` : ''}`
|
|
75
|
-
classification = classifyChangeImpact({repoRoot: ctx.repoRoot, graph, base: mergeBase, files: untracked})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!classification.files.length) return toolResult(`LOW — change impact ${sourceLabel}\nNo textual or untracked changes were found.`, {
|
|
79
|
-
status: 'COMPLETE',
|
|
80
|
-
verdict: 'LOW',
|
|
81
|
-
reasons: classification.reasons,
|
|
82
|
-
changes: [],
|
|
83
|
-
seeds: {ids: [], unmappedIds: []},
|
|
84
|
-
blastRadius: {impacted: 0, runtime: 0, compileTimeOnly: 0, nodes: []},
|
|
85
|
-
}, {completeness: {status: 'COMPLETE'}})
|
|
86
|
-
|
|
87
|
-
const changed = [...new Set(classification.files
|
|
88
|
-
.flatMap((file) => [file.oldPath, file.newPath])
|
|
89
|
-
.filter((file) => file && file !== '(diff unavailable)'))]
|
|
90
|
-
const changedSet = new Set(changed)
|
|
91
|
-
const seedIds = classification.seedIds.filter((id) => g.byId.has(String(id)))
|
|
92
|
-
const seeds = new Set(seedIds)
|
|
93
|
-
const unmappedSeedIds = classification.seedIds.filter((id) => !g.byId.has(String(id)))
|
|
94
|
-
const unmappedFiles = classification.files
|
|
95
|
-
.filter((file) => ![file.oldPath, file.newPath].some((path) => path && g.byId.has(String(path))))
|
|
96
|
-
.map((file) => file.path)
|
|
97
|
-
|
|
98
|
-
const maxDepth = Math.max(1, Math.min(4, Number(args.depth) || 2))
|
|
99
|
-
const cap = Math.max(5, Math.min(120, Number(args.max_nodes) || 40))
|
|
100
|
-
const reached = seeds.size ? reverseReach(g, seeds, maxDepth) : new Map()
|
|
101
|
-
const impacted = [...reached.entries()]
|
|
102
|
-
.filter(([id]) => !seeds.has(id) && !changedSet.has(fileOfId(id)))
|
|
103
|
-
.map(([id, entry]) => ({id, depth: entry.depth, entry, degree: degreeOf(g, id), file: fileOfId(id)}))
|
|
104
|
-
.sort((left, right) => left.depth - right.depth
|
|
105
|
-
|| Number(left.entry.compileOnly) - Number(right.entry.compileOnly)
|
|
106
|
-
|| right.degree - left.degree
|
|
107
|
-
|| left.id.localeCompare(right.id))
|
|
108
|
-
|
|
109
|
-
// Measured coverage wins. Static reachability stays separately labelled and never becomes a
|
|
110
|
-
// synthetic coverage percentage.
|
|
111
|
-
const knownFiles = (graph.nodes || []).filter((node) => !String(node.id).includes('#') && node.source_file).map((node) => node.source_file)
|
|
112
|
-
const coverage = readCoverageForRepo(ctx.repoRoot, knownFiles)
|
|
113
|
-
const coverageOf = (file) => coverage.get(String(file).replace(/\\/g, '/'))?.pct ?? null
|
|
114
|
-
const hasCoverage = coverage.size > 0
|
|
115
|
-
const staticTests = computeStaticTestReachability(graph, {repoRoot: ctx.repoRoot})
|
|
116
|
-
const staticByFile = new Map(staticTests.reachable.map((entry) => [entry.file, entry.nearestTests[0]]))
|
|
117
|
-
const staticUnreachable = new Set(staticTests.unreachable)
|
|
118
|
-
const testEvidenceFor = (file) => {
|
|
119
|
-
const normalized = String(file).replace(/\\/g, '/')
|
|
120
|
-
const actualCoverage = coverageOf(normalized)
|
|
121
|
-
const nearest = staticByFile.get(normalized)
|
|
122
|
-
return {
|
|
123
|
-
actualCoverage,
|
|
124
|
-
staticTestReachability: nearest ? {
|
|
125
|
-
status: 'REACHABLE',
|
|
126
|
-
test: nearest.test,
|
|
127
|
-
distance: nearest.distance,
|
|
128
|
-
confidence: nearest.confidence,
|
|
129
|
-
path: nearest.path,
|
|
130
|
-
} : staticUnreachable.has(normalized) ? {status: 'NO_PATH'} : {status: 'NOT_INDEXED'},
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const evidenceLabel = (file) => {
|
|
134
|
-
const evidence = testEvidenceFor(file)
|
|
135
|
-
if (evidence.actualCoverage != null) return `cov ${Math.round(evidence.actualCoverage * 100)}%`
|
|
136
|
-
const reachability = evidence.staticTestReachability
|
|
137
|
-
if (reachability.status === 'REACHABLE') return `static-test d${reachability.distance} ${reachability.confidence}`
|
|
138
|
-
return reachability.status === 'NO_PATH' ? 'no static test path' : 'test evidence n/a'
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const shown = impacted.slice(0, cap)
|
|
142
|
-
const runtimeImpacted = impacted.filter((entry) => !entry.entry.compileOnly).length
|
|
143
|
-
const compileImpacted = impacted.length - runtimeImpacted
|
|
144
|
-
const coverageHotspots = hasCoverage
|
|
145
|
-
? shown.filter((node) => { const value = coverageOf(node.file); return value != null && value < 0.5 && node.degree >= 5 })
|
|
146
|
-
: []
|
|
147
|
-
const nodes = shown.map((node) => ({
|
|
148
|
-
id: node.id,
|
|
149
|
-
label: labelOf(g, node.id),
|
|
150
|
-
file: node.file,
|
|
151
|
-
depth: node.depth,
|
|
152
|
-
kind: impactKind(node.entry),
|
|
153
|
-
relation: node.entry.relation || 'rel',
|
|
154
|
-
degree: node.degree,
|
|
155
|
-
testEvidence: testEvidenceFor(node.file),
|
|
156
|
-
}))
|
|
157
|
-
const changeLines = classification.files.slice(0, 30).flatMap((file) => [
|
|
158
|
-
` [${file.classification}] ${file.path} — ${file.reason} (${file.seedIds.length} seed${file.seedIds.length === 1 ? '' : 's'})`,
|
|
159
|
-
...file.symbols.slice(0, 8).map((symbol) => ` ↳ [${symbol.classification}] ${symbol.label} [${symbol.id}]`),
|
|
160
|
-
])
|
|
161
|
-
const warnings = []
|
|
162
|
-
if (!classification.ok) warnings.push({code: 'CHANGE_CLASSIFICATION_PARTIAL', message: classification.reasons.join(' ')})
|
|
163
|
-
if (unmappedFiles.length) warnings.push({code: 'CHANGED_FILES_UNMAPPED', message: `${unmappedFiles.length} changed file(s) are absent from the current graph.`})
|
|
164
|
-
if (unmappedSeedIds.length) warnings.push({code: 'CHANGE_SEEDS_UNMAPPED', message: `${unmappedSeedIds.length} classified seed(s) are absent from the current graph.`})
|
|
165
|
-
|
|
166
|
-
const text = [
|
|
167
|
-
`${classification.verdict} — symbol-aware change impact ${sourceLabel}`,
|
|
168
|
-
...classification.reasons.map((reason) => `Reason: ${reason}`),
|
|
169
|
-
`Changed evidence: ${classification.files.length} file(s), ${classification.summary.symbols} mapped symbol(s) → ${seeds.size} exact graph seed(s).`,
|
|
170
|
-
...changeLines,
|
|
171
|
-
classification.files.length > 30 ? ` … +${classification.files.length - 30} more changed files` : null,
|
|
172
|
-
'',
|
|
173
|
-
impacted.length
|
|
174
|
-
? `Blast radius: ${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
|
|
175
|
-
: seeds.size ? `Blast radius: nothing else depends on the exact changed symbols within ${maxDepth} hop(s).` : `Blast radius: 0 — additive, metadata-only, and test-only changes do not inherit the containing file's legacy importers.`,
|
|
176
|
-
hasCoverage
|
|
177
|
-
? 'Test evidence: measured coverage is shown where mapped; static reachability remains separate in structured output.'
|
|
178
|
-
: `Test evidence: actualCoverage NOT_AVAILABLE; static paths only (${staticTests.reachableFiles}/${staticTests.productFiles} product files reachable from indexed tests).`,
|
|
179
|
-
...shown.map((node) => ` [d${node.depth} ${impactKind(node.entry)}] ${node.entry.relation || 'rel'} ${labelOf(g, node.id)} (deg ${node.degree}, ${evidenceLabel(node.file)}) [${node.id}]`),
|
|
180
|
-
coverageHotspots.length ? '' : null,
|
|
181
|
-
coverageHotspots.length ? 'Measured-coverage hotspots in the blast radius (<50%, deg ≥5):' : null,
|
|
182
|
-
...coverageHotspots.slice(0, 10).map((node) => ` ${labelOf(g, node.id)} (cov ${Math.round(coverageOf(node.file) * 100)}%, deg ${node.degree}) ${node.file}`),
|
|
183
|
-
'',
|
|
184
|
-
'Drill into an impacted node with get_dependents/read_source; coverage_map distinguishes measured coverage from static reachability.',
|
|
185
|
-
].filter((line) => line != null).join('\n')
|
|
186
|
-
|
|
187
|
-
const complete = classification.ok && !unmappedFiles.length && !unmappedSeedIds.length
|
|
188
|
-
return toolResult(text, {
|
|
189
|
-
status: complete ? 'COMPLETE' : 'PARTIAL',
|
|
190
|
-
verdict: classification.verdict,
|
|
191
|
-
reasons: classification.reasons,
|
|
192
|
-
source: {label: sourceLabel, kind: classification.source},
|
|
193
|
-
changes: classification.files,
|
|
194
|
-
classification: {summary: classification.summary, bounds: classification.bounds},
|
|
195
|
-
seeds: {ids: [...seeds].sort(), unmappedIds: unmappedSeedIds},
|
|
196
|
-
blastRadius: {
|
|
197
|
-
depth: maxDepth,
|
|
198
|
-
impacted: impacted.length,
|
|
199
|
-
runtime: runtimeImpacted,
|
|
200
|
-
compileTimeOnly: compileImpacted,
|
|
201
|
-
nodes,
|
|
202
|
-
},
|
|
203
|
-
testEvidence: {
|
|
204
|
-
actualCoverage: hasCoverage ? 'AVAILABLE' : 'NOT_AVAILABLE',
|
|
205
|
-
changedFiles: changed.slice(0, 500).map((file) => ({file, ...testEvidenceFor(file)})),
|
|
206
|
-
staticTestReachability: {
|
|
207
|
-
kind: staticTests.kind,
|
|
208
|
-
testFiles: staticTests.testFiles,
|
|
209
|
-
productFiles: staticTests.productFiles,
|
|
210
|
-
reachableFiles: staticTests.reachableFiles,
|
|
211
|
-
bounds: staticTests.bounds,
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
unmappedFiles,
|
|
215
|
-
}, {
|
|
216
|
-
warnings,
|
|
217
|
-
page: {shown: shown.length, total: impacted.length, capped: shown.length < impacted.length},
|
|
218
|
-
completeness: {status: complete ? 'COMPLETE' : 'PARTIAL'},
|
|
219
|
-
})
|
|
220
|
-
}
|