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,143 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isSymbol, degreeOf, labelOf, connList, resolveNodeInfo, ambiguityNote,
|
|
3
|
-
graphStaleness, fileStalenessNote,
|
|
4
|
-
} from '../graph-context.mjs'
|
|
5
|
-
import {summarizeEdgeProvenance} from '../../graph/edge-provenance.js'
|
|
6
|
-
|
|
7
|
-
const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
|
|
8
|
-
|
|
9
|
-
export function tGraphStats(g, ctx) {
|
|
10
|
-
const files = g.nodes.filter((node) => !isSymbol(node.id)).length
|
|
11
|
-
const symbols = g.nodes.length - files
|
|
12
|
-
const relCount = {}
|
|
13
|
-
const confCount = {}
|
|
14
|
-
let typeOnlyEdges = 0
|
|
15
|
-
let compileOnlyEdges = 0
|
|
16
|
-
for (const edge of g.links) {
|
|
17
|
-
relCount[edge.relation ?? '?'] = (relCount[edge.relation ?? '?'] || 0) + 1
|
|
18
|
-
if (edge.confidence != null) confCount[edge.confidence] = (confCount[edge.confidence] || 0) + 1
|
|
19
|
-
if (edge.typeOnly === true) typeOnlyEdges++
|
|
20
|
-
if (edge.compileOnly === true) compileOnlyEdges++
|
|
21
|
-
}
|
|
22
|
-
const communities = new Map()
|
|
23
|
-
for (const node of g.nodes) {
|
|
24
|
-
const community = node.community ?? 'none'
|
|
25
|
-
communities.set(community, (communities.get(community) || 0) + 1)
|
|
26
|
-
}
|
|
27
|
-
const topCommunities = [...communities.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
28
|
-
const formatCounts = (counts) => Object.entries(counts)
|
|
29
|
-
.sort((a, b) => b[1] - a[1])
|
|
30
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
31
|
-
.join(', ')
|
|
32
|
-
const freshness = ctx ? graphStaleness(ctx) : null
|
|
33
|
-
const provenance = summarizeEdgeProvenance(g.links)
|
|
34
|
-
const precision = g.precision || {state: 'UNAVAILABLE', verifiedEdges: 0, candidates: 0, queried: 0, reason: 'no revision-matched precision overlay'}
|
|
35
|
-
return [
|
|
36
|
-
'Graph summary',
|
|
37
|
-
ctx?.runtime ? `- Weavatrix runtime: v${ctx.runtime.version}; disk v${ctx.runtime.diskVersion || 'unavailable'}; stale ${ctx.runtime.staleRuntime ? 'YES' : 'no'}; profile ${ctx.runtime.profile}; ${ctx.runtime.toolCount} registered tools; capabilities: ${ctx.runtime.capabilities.join(',') || '(none)'}` : null,
|
|
38
|
-
ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
|
|
39
|
-
ctx?.graphPath ? `- Graph: ${ctx.graphPath}` : null,
|
|
40
|
-
`- Build mode: ${g.graphBuildMode || 'full'}`,
|
|
41
|
-
`- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
|
|
42
|
-
`- Edges: ${g.links.length}`,
|
|
43
|
-
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : '- Typed-edge metadata: unavailable (rebuild_graph required)',
|
|
44
|
-
g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${formatCounts(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : '- Edge provenance: unavailable (rebuild_graph required)',
|
|
45
|
-
`- Semantic precision: ${precision.state}${precision.provider ? ` via ${precision.provider}${precision.providerVersion ? ` ${precision.providerVersion}` : ''}${precision.typescriptVersion ? ` (TypeScript ${precision.typescriptVersion})` : ''}` : ''}; ${precision.verifiedEdges || 0} EXACT_LSP edge(s), ${precision.queried || 0}/${precision.candidates || 0} bounded target(s) queried${precision.truncated ? ' (partial/truncated)' : ''}${precision.reason ? `; ${precision.reason}` : ''}`,
|
|
46
|
-
g.barrelResolutionV ? `- Barrel resolution: v${g.barrelResolutionV} (semantic tools look through JS/TS re-export facades)` : '- Barrel resolution: unavailable (rebuild_graph required for JS/TS barrel transparency)',
|
|
47
|
-
g.reExportOccurrencesV ? `- Re-export occurrences: v${g.reExportOccurrencesV} (${g.reExportOccurrences.length} exact site(s))` : '- Re-export occurrences: unavailable (rebuild_graph required)',
|
|
48
|
-
g.symbolSpacesV ? `- TypeScript symbol spaces: v${g.symbolSpacesV} (type/value identities separated)` : '- TypeScript symbol spaces: unavailable (rebuild_graph required)',
|
|
49
|
-
`- Relations: ${formatCounts(relCount)}`,
|
|
50
|
-
Object.keys(confCount).length ? `- Legacy confidence: ${formatCounts(confCount)}` : null,
|
|
51
|
-
`- Communities: ${communities.size} (top by size: ${topCommunities.map(([community, count]) => `#${community}=${count}`).join(', ')})`,
|
|
52
|
-
freshness?.builtAt ? `- Built: ${freshness.builtAt.toISOString()}${freshness.headAt ? ` (repo HEAD committed ${freshness.headAt.toISOString()})` : ''}` : null,
|
|
53
|
-
].filter(Boolean).join('\n')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function tGetNode(g, {label} = {}, ctx) {
|
|
57
|
-
const info = resolveNodeInfo(g, label)
|
|
58
|
-
const node = info.node
|
|
59
|
-
if (!node) return `No node found matching "${label}".`
|
|
60
|
-
const note = ambiguityNote(label, info)
|
|
61
|
-
const id = String(node.id)
|
|
62
|
-
const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
63
|
-
const outgoing = g.out.get(id) || []
|
|
64
|
-
const incoming = g.inn.get(id) || []
|
|
65
|
-
const semanticOutgoing = connList(outgoing)
|
|
66
|
-
const semanticIncoming = connList(incoming)
|
|
67
|
-
const sample = (edges, direction) => edges.slice(0, 12)
|
|
68
|
-
.map((edge) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${edge.provenance || 'UNKNOWN'}] ${labelOf(g, edge.id)} [${edge.id}]`)
|
|
69
|
-
.join('\n') || ' (none)'
|
|
70
|
-
return [
|
|
71
|
-
note,
|
|
72
|
-
`Node: ${node.label ?? id}`,
|
|
73
|
-
`- id: ${id}`,
|
|
74
|
-
`- kind: ${isSymbol(id) ? 'symbol' : 'file'}${node.file_type ? ` (${node.file_type})` : ''}`,
|
|
75
|
-
node.source_file ? `- source: ${node.source_file}${node.source_location ? ` ${node.source_location}` : ''}` : null,
|
|
76
|
-
node.community != null ? `- community: ${node.community}` : null,
|
|
77
|
-
`- semantic degree: ${semanticOutgoing.length + semanticIncoming.length} (out ${semanticOutgoing.length}, in ${semanticIncoming.length})${outgoing.length + incoming.length !== semanticOutgoing.length + semanticIncoming.length ? `; ${outgoing.length + incoming.length} physical/structural edges retained` : ''}`,
|
|
78
|
-
`Outgoing:\n${sample(outgoing, 'out')}`,
|
|
79
|
-
`Incoming:\n${sample(incoming, 'in')}`,
|
|
80
|
-
drift,
|
|
81
|
-
].filter(Boolean).join('\n')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function dedupeEdges(edges) {
|
|
85
|
-
const grouped = new Map()
|
|
86
|
-
for (const edge of edges) {
|
|
87
|
-
const key = `${edge.relation || 'rel'}|${compileKind(edge) || 'runtime'}|${edge.id}`
|
|
88
|
-
const current = grouped.get(key)
|
|
89
|
-
if (current) {
|
|
90
|
-
current.count += 1
|
|
91
|
-
current.provenance.add(edge.provenance || 'UNKNOWN')
|
|
92
|
-
} else grouped.set(key, {
|
|
93
|
-
id: edge.id, relation: edge.relation, typeOnly: edge.typeOnly === true,
|
|
94
|
-
compileOnly: edge.compileOnly === true, provenance: new Set([edge.provenance || 'UNKNOWN']), count: 1,
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
return [...grouped.values()]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
101
|
-
const info = resolveNodeInfo(g, label)
|
|
102
|
-
const node = info.node
|
|
103
|
-
if (!node) return `No node found matching "${label}".`
|
|
104
|
-
const note = ambiguityNote(label, info)
|
|
105
|
-
const id = String(node.id)
|
|
106
|
-
const drift = ctx ? fileStalenessNote(ctx, node.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
107
|
-
const filter = relation_filter ? String(relation_filter).toLowerCase() : null
|
|
108
|
-
const matches = (edge) => !filter || String(edge.relation ?? '').toLowerCase() === filter
|
|
109
|
-
const outgoingRaw = (g.out.get(id) || []).filter(matches)
|
|
110
|
-
const incomingRaw = (g.inn.get(id) || []).filter(matches)
|
|
111
|
-
const outgoing = dedupeEdges(outgoingRaw)
|
|
112
|
-
const incoming = dedupeEdges(incomingRaw)
|
|
113
|
-
const line = (edge, direction) => ` ${direction === 'out' ? '→' : '←'} ${compileKind(edge) ? `${compileKind(edge)} ` : ''}${edge.relation || 'rel'} [${[...edge.provenance].sort().join('+')}] ${labelOf(g, edge.id)} [${edge.id}]${edge.count > 1 ? ` (${edge.count} sites)` : ''}`
|
|
114
|
-
return [
|
|
115
|
-
note,
|
|
116
|
-
`Neighbors of ${node.label ?? id}${filter ? ` (relation=${filter})` : ''}: ${outgoing.length + incoming.length} unique (${outgoingRaw.length + incomingRaw.length} edges)`,
|
|
117
|
-
`Outgoing (${outgoing.length}):`,
|
|
118
|
-
...outgoing.slice(0, 60).map((edge) => line(edge, 'out')),
|
|
119
|
-
`Incoming (${incoming.length}):`,
|
|
120
|
-
...incoming.slice(0, 60).map((edge) => line(edge, 'in')),
|
|
121
|
-
drift,
|
|
122
|
-
].filter(Boolean).join('\n')
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function tGetCommunity(g, {community_id} = {}) {
|
|
126
|
-
const groups = new Map()
|
|
127
|
-
for (const node of g.nodes) {
|
|
128
|
-
if (node.community == null) continue
|
|
129
|
-
if (!groups.has(node.community)) groups.set(node.community, [])
|
|
130
|
-
groups.get(node.community).push(node)
|
|
131
|
-
}
|
|
132
|
-
const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length)
|
|
133
|
-
const index = Number(community_id)
|
|
134
|
-
if (!Number.isInteger(index) || index < 0 || index >= ranked.length) return `Invalid community_id ${community_id}. Valid range 0..${ranked.length - 1} (0 = largest).`
|
|
135
|
-
const [rawId, members] = ranked[index]
|
|
136
|
-
const files = members.filter((member) => !isSymbol(member.id))
|
|
137
|
-
return [
|
|
138
|
-
`Community #${index} (raw id ${rawId}) — ${members.length} nodes, ${files.length} files:`,
|
|
139
|
-
...members.slice().sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id)).slice(0, 80)
|
|
140
|
-
.map((member) => ` ${member.label ?? member.id} [${member.id}]`),
|
|
141
|
-
members.length > 80 ? ` … +${members.length - 80} more` : null,
|
|
142
|
-
].filter(Boolean).join('\n')
|
|
143
|
-
}
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isSymbol, degreeOf, labelOf, resolveNode, findSeeds, resolveSeedFiles,
|
|
3
|
-
requestedPathClasses,
|
|
4
|
-
} from '../graph-context.mjs'
|
|
5
|
-
import {createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
6
|
-
|
|
7
|
-
const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
8
|
-
const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
|
|
9
|
-
const AMBIGUOUS_FILE_BASENAME_RE = /^(mod|lib|main)\.rs$|^index\.[a-z0-9]+$|^__init__\.py$/
|
|
10
|
-
const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
11
|
-
const queryWords = (value) => new Set(String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean))
|
|
12
|
-
const exactSymbolName = (node) => {
|
|
13
|
-
const match = String(node?.id || '').match(/#([^@]+)@/)
|
|
14
|
-
return match ? match[1] : String(node?.label || '').replace(/\(\)$/, '')
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveExactSeedSymbols(g, requested, limit = 12) {
|
|
18
|
-
const values = Array.isArray(requested) ? requested.slice(0, limit) : []
|
|
19
|
-
const seeds = []
|
|
20
|
-
const missing = []
|
|
21
|
-
const ambiguous = []
|
|
22
|
-
for (const raw of values) {
|
|
23
|
-
const wanted = String(raw || '').trim()
|
|
24
|
-
if (!wanted) continue
|
|
25
|
-
let matches = []
|
|
26
|
-
const byId = g.byId.get(wanted)
|
|
27
|
-
if (byId && isSymbol(byId.id)) matches = [byId]
|
|
28
|
-
else matches = g.nodes.filter((node) => isSymbol(node.id)
|
|
29
|
-
&& (String(node.label || '') === wanted || exactSymbolName(node) === wanted))
|
|
30
|
-
if (matches.length === 1) {
|
|
31
|
-
if (!seeds.some((seed) => String(seed.id) === String(matches[0].id))) seeds.push(matches[0])
|
|
32
|
-
} else if (matches.length > 1) ambiguous.push(`${wanted} (${matches.length} exact matches; pass a symbol id)`)
|
|
33
|
-
else missing.push(wanted)
|
|
34
|
-
}
|
|
35
|
-
return {seeds, missing, ambiguous}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Edge/hop lines print bare labels without the [id] suffix node lines carry, so repeated or
|
|
39
|
-
// conventionally-generic file basenames (mod.rs, index.ts, __init__.py) become indistinguishable —
|
|
40
|
-
// widen those, scoped to the shown ids, to the last two path segments.
|
|
41
|
-
const makeEdgeLabel = (g, shownIds) => {
|
|
42
|
-
const counts = new Map()
|
|
43
|
-
for (const id of shownIds) {
|
|
44
|
-
const label = labelOf(g, id)
|
|
45
|
-
counts.set(label, (counts.get(label) || 0) + 1)
|
|
46
|
-
}
|
|
47
|
-
return (id) => {
|
|
48
|
-
const label = labelOf(g, id)
|
|
49
|
-
const isFile = String(id).includes('/') && !String(id).includes('#')
|
|
50
|
-
if (isFile && ((counts.get(label) || 0) > 1 || AMBIGUOUS_FILE_BASENAME_RE.test(label))) return String(id).split('/').slice(-2).join('/')
|
|
51
|
-
return label
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// shortest_path is intentionally a connectivity query, so it may walk an edge in either
|
|
56
|
-
// direction. Keep the stored direction on every hop so the rendered path never turns a reverse
|
|
57
|
-
// traversal into a false caller/importer direction.
|
|
58
|
-
const connectivityNeighbors = (g, id) => {
|
|
59
|
-
const seen = new Map()
|
|
60
|
-
for (const edge of g.out.get(id) || []) {
|
|
61
|
-
if (edge.barrelProxy !== true) seen.set(String(edge.id), {relation: edge.relation, direction: 'forward'})
|
|
62
|
-
}
|
|
63
|
-
for (const edge of g.inn.get(id) || []) {
|
|
64
|
-
if (edge.barrelProxy !== true && !seen.has(String(edge.id))) {
|
|
65
|
-
seen.set(String(edge.id), {relation: edge.relation, direction: 'backward'})
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return seen
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const relationSet = (relationFilter, legacyFilter) => {
|
|
72
|
-
const raw = relationFilter ?? legacyFilter
|
|
73
|
-
const values = Array.isArray(raw) ? raw : (raw == null ? [] : String(raw).split(','))
|
|
74
|
-
const normalized = values.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
|
|
75
|
-
return normalized.length ? new Set(normalized) : null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function tQueryGraph(g, {
|
|
79
|
-
question, mode = 'bfs', depth = 3, context_filter, relation_filter, seed_files, seed_symbols, augment_seeds = false,
|
|
80
|
-
flow_direction = 'both', include_classified = false, include_low_signal = false, token_budget = 2000,
|
|
81
|
-
} = {}, toolCtx = {}) {
|
|
82
|
-
const pinned = resolveSeedFiles(g, seed_files)
|
|
83
|
-
const exactSymbols = resolveExactSeedSymbols(g, seed_symbols)
|
|
84
|
-
const pinnedSeeds = [...pinned.seeds, ...exactSymbols.seeds.filter((node) => !pinned.seeds.some((seed) => String(seed.id) === String(node.id)))]
|
|
85
|
-
const automatic = pinnedSeeds.length && augment_seeds !== true
|
|
86
|
-
? []
|
|
87
|
-
: findSeeds(g, question, Math.max(0, 6 - pinnedSeeds.length), {repoRoot: toolCtx.repoRoot || null})
|
|
88
|
-
const seeds = [...pinnedSeeds, ...automatic.filter((node) => !pinnedSeeds.some((seed) => String(seed.id) === String(node.id)))]
|
|
89
|
-
if (!seeds.length) {
|
|
90
|
-
const details = [
|
|
91
|
-
pinned.missing.length ? `Unresolved seed files: ${pinned.missing.join(', ')}` : null,
|
|
92
|
-
exactSymbols.missing.length ? `Unresolved exact symbols: ${exactSymbols.missing.join(', ')}` : null,
|
|
93
|
-
exactSymbols.ambiguous.length ? `Ambiguous exact symbols: ${exactSymbols.ambiguous.join(', ')}` : null,
|
|
94
|
-
].filter(Boolean)
|
|
95
|
-
return [`No nodes matched "${question || ''}".`, ...details].join('\n')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const maxDepth = Math.max(1, Math.min(6, Number(depth) || 3))
|
|
99
|
-
const relations = relationSet(relation_filter, context_filter)
|
|
100
|
-
const relationAllowed = (relation) => !relations || relations.has(String(relation ?? '').toLowerCase())
|
|
101
|
-
const direction = ['forward', 'backward', 'both'].includes(String(flow_direction).toLowerCase())
|
|
102
|
-
? String(flow_direction).toLowerCase() : 'both'
|
|
103
|
-
const neighbors = (id) => {
|
|
104
|
-
const found = new Map()
|
|
105
|
-
const add = (edges) => {
|
|
106
|
-
for (const edge of edges || []) {
|
|
107
|
-
const neighbor = String(edge.id)
|
|
108
|
-
if (edge.barrelProxy === true || !relationAllowed(edge.relation) || found.has(neighbor)) continue
|
|
109
|
-
found.set(neighbor, edge.relation)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (direction !== 'backward') add(g.out.get(id))
|
|
113
|
-
if (direction !== 'forward') add(g.inn.get(id))
|
|
114
|
-
return found
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const requestedClasses = requestedPathClasses(question)
|
|
118
|
-
const classifier = createPathClassifier(toolCtx.repoRoot || null)
|
|
119
|
-
const classificationCache = new Map()
|
|
120
|
-
const pinnedFiles = new Set(pinnedSeeds.map(querySourceFile))
|
|
121
|
-
const classifiedSuppressed = new Set()
|
|
122
|
-
const pathPolicy = (id) => {
|
|
123
|
-
const node = g.byId.get(String(id))
|
|
124
|
-
const file = querySourceFile(node)
|
|
125
|
-
if (!file || pinnedFiles.has(file) || include_classified === true) return {ok: true}
|
|
126
|
-
// Extractor-proven test symbols (Rust #[cfg(test)]) live in production files; suppress them
|
|
127
|
-
// under the same policy as path-classified tests unless the question asks about tests.
|
|
128
|
-
if (node?.test_surface === true && !requestedClasses.has('test')) {
|
|
129
|
-
classifiedSuppressed.add(String(id))
|
|
130
|
-
return {ok: false, bucket: 'classified'}
|
|
131
|
-
}
|
|
132
|
-
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
|
|
133
|
-
const info = classificationCache.get(file)
|
|
134
|
-
const classes = QUERY_NON_PRODUCT.filter((name) => hasPathClass(info, name))
|
|
135
|
-
if (!classes.length && !info?.excluded) return {ok: true}
|
|
136
|
-
if (classes.some((name) => requestedClasses.has(name))) return {ok: true}
|
|
137
|
-
classifiedSuppressed.add(String(id))
|
|
138
|
-
return {ok: false, bucket: 'classified'}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const questionTerms = queryWords(question)
|
|
142
|
-
const start = seeds.map((seed) => String(seed.id))
|
|
143
|
-
const isLowSignal = (id) => {
|
|
144
|
-
if (include_low_signal === true || start.includes(String(id))) return false
|
|
145
|
-
const node = g.byId.get(String(id))
|
|
146
|
-
if (!node || !isSymbol(node.id) || !LOW_SIGNAL_SYMBOL_RE.test(String(node.symbol_kind || ''))) return false
|
|
147
|
-
const labelTerms = queryWords(node.label || String(node.id || '').split('#').pop() || '')
|
|
148
|
-
if ([...questionTerms].some((term) => labelTerms.has(term))) return false
|
|
149
|
-
return degreeOf(g, id) === 0
|
|
150
|
-
}
|
|
151
|
-
const charBudget = Math.max(400, (Number(token_budget) || 2000) * 4)
|
|
152
|
-
const nodeBudget = Math.max(20, Math.min(120, Math.round((Number(token_budget) || 2000) / 40)))
|
|
153
|
-
const depthOf = new Map()
|
|
154
|
-
|
|
155
|
-
if (mode === 'dfs') {
|
|
156
|
-
const stack = start.map((id) => ({id, depth: 0}))
|
|
157
|
-
const seen = new Set()
|
|
158
|
-
while (stack.length) {
|
|
159
|
-
const {id, depth: currentDepth} = stack.pop()
|
|
160
|
-
if (!depthOf.has(id) || currentDepth < depthOf.get(id)) depthOf.set(id, currentDepth)
|
|
161
|
-
if (seen.has(id)) continue
|
|
162
|
-
seen.add(id)
|
|
163
|
-
if (currentDepth >= maxDepth) continue
|
|
164
|
-
for (const [neighbor, relation] of neighbors(id)) {
|
|
165
|
-
if (!relationAllowed(relation) || !pathPolicy(neighbor).ok) continue
|
|
166
|
-
if (!seen.has(neighbor)) stack.push({id: neighbor, depth: currentDepth + 1})
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
let frontier = start.slice()
|
|
171
|
-
start.forEach((id) => depthOf.set(id, 0))
|
|
172
|
-
for (let currentDepth = 0; currentDepth < maxDepth && frontier.length; currentDepth++) {
|
|
173
|
-
const next = []
|
|
174
|
-
for (const id of frontier) {
|
|
175
|
-
for (const [neighbor, relation] of neighbors(id)) {
|
|
176
|
-
if (!relationAllowed(relation) || !pathPolicy(neighbor).ok) continue
|
|
177
|
-
if (!depthOf.has(neighbor)) {
|
|
178
|
-
depthOf.set(neighbor, currentDepth + 1)
|
|
179
|
-
next.push(neighbor)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
frontier = next
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const reachedBeforeSignalFilter = depthOf.size
|
|
188
|
-
const lowSignalSuppressed = [...depthOf.keys()].filter(isLowSignal).length
|
|
189
|
-
const ranked = [...depthOf.entries()]
|
|
190
|
-
.filter(([id]) => !isLowSignal(id))
|
|
191
|
-
.map(([id, distance]) => ({id, distance, degree: degreeOf(g, id)}))
|
|
192
|
-
.sort((a, b) => a.distance - b.distance || b.degree - a.degree)
|
|
193
|
-
const shown = ranked.slice(0, nodeBudget)
|
|
194
|
-
const shownIds = new Set(shown.map((node) => node.id))
|
|
195
|
-
const edgeSeen = new Set()
|
|
196
|
-
const shownEdges = []
|
|
197
|
-
for (const source of shownIds) {
|
|
198
|
-
for (const edge of g.out.get(source) || []) {
|
|
199
|
-
const target = String(edge.id)
|
|
200
|
-
if (edge.barrelProxy === true || !shownIds.has(target) || !relationAllowed(edge.relation)) continue
|
|
201
|
-
const key = `${source}|${edge.relation}|${target}`
|
|
202
|
-
if (edgeSeen.has(key)) continue
|
|
203
|
-
edgeSeen.add(key)
|
|
204
|
-
shownEdges.push([source, edge.relation, target])
|
|
205
|
-
if (shownEdges.length >= 160) break
|
|
206
|
-
}
|
|
207
|
-
if (shownEdges.length >= 160) break
|
|
208
|
-
}
|
|
209
|
-
const head = [
|
|
210
|
-
`Query: "${question || ''}" (${mode}, depth ${maxDepth}, flow ${direction}${relations ? `, relations ${[...relations].join('/')}` : ''})`,
|
|
211
|
-
`Seeds: ${seeds.map((seed) => seed.label ?? seed.id).join(', ')}`,
|
|
212
|
-
pinned.missing.length ? `Unresolved pinned seed files: ${pinned.missing.join(', ')}` : null,
|
|
213
|
-
exactSymbols.missing.length ? `Unresolved exact seed symbols: ${exactSymbols.missing.join(', ')}` : null,
|
|
214
|
-
exactSymbols.ambiguous.length ? `Ambiguous exact seed symbols: ${exactSymbols.ambiguous.join(', ')}` : null,
|
|
215
|
-
`Reached ${reachedBeforeSignalFilter} policy-eligible nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
|
|
216
|
-
classifiedSuppressed.size ? `Suppressed ${classifiedSuppressed.size} classified/non-product traversal node(s); ask for that class or pass include_classified:true.` : null,
|
|
217
|
-
lowSignalSuppressed ? `Suppressed ${lowSignalSuppressed} unreferenced constant/field node(s) with no query-term match; pass include_low_signal:true to inspect them.` : null,
|
|
218
|
-
include_classified === true ? 'Path policy: classified/non-product traversal explicitly enabled.' : `Path policy: production-first${requestedClasses.size ? `; explicit question classes enabled: ${[...requestedClasses].join(', ')}` : ''}.`,
|
|
219
|
-
'',
|
|
220
|
-
'Nodes:',
|
|
221
|
-
]
|
|
222
|
-
const nodeLines = shown.map((node) => ` [d${node.distance}] ${labelOf(g, node.id)} (deg ${node.degree}) [${node.id}]`)
|
|
223
|
-
const edgeLabel = makeEdgeLabel(g, shownIds)
|
|
224
|
-
const edgeLines = ['', 'Edges:', ...shownEdges.map(([source, relation, target]) => ` ${edgeLabel(source)} --${relation || 'rel'}--> ${edgeLabel(target)}`)]
|
|
225
|
-
let text = [...head.filter(Boolean), ...nodeLines, ...edgeLines].join('\n')
|
|
226
|
-
if (text.length > charBudget) text = text.slice(0, charBudget) + `\n... (truncated to ~${token_budget} tokens)`
|
|
227
|
-
return text
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function tShortestPath(g, {source, target, max_hops = 8} = {}) {
|
|
231
|
-
const sourceNode = resolveNode(g, source)
|
|
232
|
-
const targetNode = resolveNode(g, target)
|
|
233
|
-
if (!sourceNode) return `Source "${source}" not found.`
|
|
234
|
-
if (!targetNode) return `Target "${target}" not found.`
|
|
235
|
-
const sourceId = String(sourceNode.id)
|
|
236
|
-
const targetId = String(targetNode.id)
|
|
237
|
-
if (sourceId === targetId) return `Source and target are the same node: ${sourceNode.label ?? sourceId}.`
|
|
238
|
-
const limit = Math.max(1, Math.min(20, Number(max_hops) || 8))
|
|
239
|
-
const previous = new Map([[sourceId, null]])
|
|
240
|
-
const edgeTo = new Map()
|
|
241
|
-
let frontier = [sourceId]
|
|
242
|
-
let hops = 0
|
|
243
|
-
let found = false
|
|
244
|
-
while (frontier.length && hops < limit && !found) {
|
|
245
|
-
const next = []
|
|
246
|
-
for (const id of frontier) {
|
|
247
|
-
for (const [neighbor, edge] of connectivityNeighbors(g, id)) {
|
|
248
|
-
if (previous.has(neighbor)) continue
|
|
249
|
-
previous.set(neighbor, id)
|
|
250
|
-
edgeTo.set(neighbor, edge)
|
|
251
|
-
if (neighbor === targetId) { found = true; break }
|
|
252
|
-
next.push(neighbor)
|
|
253
|
-
}
|
|
254
|
-
if (found) break
|
|
255
|
-
}
|
|
256
|
-
frontier = next
|
|
257
|
-
hops++
|
|
258
|
-
}
|
|
259
|
-
if (!previous.has(targetId)) return `No path found between "${sourceNode.label ?? sourceId}" and "${targetNode.label ?? targetId}" within ${limit} hops.`
|
|
260
|
-
const path = []
|
|
261
|
-
for (let current = targetId; current != null; current = previous.get(current)) path.unshift(current)
|
|
262
|
-
const edgeLabel = makeEdgeLabel(g, path)
|
|
263
|
-
const hopCount = path.length - 1
|
|
264
|
-
const lines = path.map((id, index) => {
|
|
265
|
-
if (index === 0) return ` ${edgeLabel(id)}`
|
|
266
|
-
const edge = edgeTo.get(id) || {}
|
|
267
|
-
return edge.direction === 'backward'
|
|
268
|
-
? ` <--${edge.relation || 'rel'}-- ${edgeLabel(id)}`
|
|
269
|
-
: ` --${edge.relation || 'rel'}--> ${edgeLabel(id)}`
|
|
270
|
-
})
|
|
271
|
-
return [
|
|
272
|
-
`Shortest path (${hopCount} ${hopCount === 1 ? 'hop' : 'hops'}; undirected connectivity) from ${edgeLabel(sourceId)} to ${edgeLabel(targetId)}`,
|
|
273
|
-
'Edge arrows preserve stored graph direction; traversal may move with or against them.',
|
|
274
|
-
...lines,
|
|
275
|
-
].join('\n')
|
|
276
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// Stable public facade for graph loading, query seeding and graph state helpers.
|
|
2
|
-
export * from './graph/context-core.mjs'
|
|
3
|
-
export * from './graph/context-seeds.mjs'
|
|
4
|
-
export * from './graph/context-state.mjs'
|
|
5
|
-
export {prevGraphPathFor, edgeEndpoint, fileOfId, diffGraphs, formatGraphDiff} from './graph-diff.mjs'
|
package/src/mcp/graph-diff.mjs
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
// Structural graph delta helpers split from graph-context so the process-lifetime graph indexes stay small.
|
|
2
|
-
import {buildFileImportGraph, findSccs} from '../analysis/dep-rules.js'
|
|
3
|
-
import {folderModuleOf} from '../analysis/graph-analysis.aggregate.js'
|
|
4
|
-
import {isStructuralRelation} from '../graph/relations.js'
|
|
5
|
-
import {fileOfId} from '../graph/node-id.js'
|
|
6
|
-
|
|
7
|
-
// ---- graph diff ----------------------------------------------------------------------------------
|
|
8
|
-
// One previous state is enough (4-13 MB per repo — cheap): rebuild_graph snapshots the outgoing
|
|
9
|
-
// graph.json as graph.prev.json and reports the structural delta inline, at the exact moment the fix
|
|
10
|
-
// is being verified. graph_diff re-queries the same pair later. Raw node/edge dumps would be noise —
|
|
11
|
-
// the signal is aggregated: module-dependency drift, cycle count changes, newly orphaned symbols.
|
|
12
|
-
export const prevGraphPathFor = (graphPath) => String(graphPath).replace(/\.json$/, '.prev.json')
|
|
13
|
-
export const edgeEndpoint = (v) => String(v && typeof v === 'object' ? v.id : v)
|
|
14
|
-
export {fileOfId}
|
|
15
|
-
const folderOfFile = folderModuleOf
|
|
16
|
-
|
|
17
|
-
const terminalLineSuffix = (id) => {
|
|
18
|
-
const raw = String(id)
|
|
19
|
-
return raw.includes('#') ? raw.replace(/@\d+$/, '') : raw
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const sourceLine = (node) => {
|
|
23
|
-
const explicit = String(node?.source_location || '').match(/^L(\d+)/i)
|
|
24
|
-
if (explicit) return Number(explicit[1])
|
|
25
|
-
const suffix = String(node?.id || '').match(/@(\d+)$/)
|
|
26
|
-
return suffix ? Number(suffix[1]) : Number.MAX_SAFE_INTEGER
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Node ids retain source lines for precise read_source jumps, but location is not identity. This
|
|
30
|
-
// comparison-only index keeps file/name/kind/export/signature shape stable across line shifts and
|
|
31
|
-
// uses an ordinal only for true same-name overloads. Raw ids remain the user-facing evidence.
|
|
32
|
-
function stableNodeIndex(graph) {
|
|
33
|
-
const groups = new Map()
|
|
34
|
-
for (const node of graph.nodes || []) {
|
|
35
|
-
const raw = String(node?.id ?? '')
|
|
36
|
-
if (!raw) continue
|
|
37
|
-
const stem = terminalLineSuffix(raw)
|
|
38
|
-
const params = Number.isInteger(node?.complexity?.params) ? node.complexity.params : ''
|
|
39
|
-
const signature = [stem, node?.symbol_kind || '', node?.symbol_space || '', node?.exported === true ? 'exported' : '', params].join('\u0000')
|
|
40
|
-
if (!groups.has(signature)) groups.set(signature, [])
|
|
41
|
-
groups.get(signature).push(node)
|
|
42
|
-
}
|
|
43
|
-
const byRaw = new Map()
|
|
44
|
-
const rawByStable = new Map()
|
|
45
|
-
for (const [signature, nodes] of groups) {
|
|
46
|
-
nodes.sort((a, b) => sourceLine(a) - sourceLine(b) || String(a.id).localeCompare(String(b.id)))
|
|
47
|
-
nodes.forEach((node, ordinal) => {
|
|
48
|
-
const stable = `${signature}\u0000${ordinal}`
|
|
49
|
-
const raw = String(node.id)
|
|
50
|
-
byRaw.set(raw, stable)
|
|
51
|
-
rawByStable.set(stable, raw)
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
return {byRaw, rawByStable, keys: new Set(rawByStable.keys())}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Works on anything with {nodes, links}: the raw graph.json shape and the loadGraph struct alike.
|
|
58
|
-
export function diffGraphs(oldG, newG) {
|
|
59
|
-
const oldEdgeTypesV = Number(oldG.edgeTypesV) || 0
|
|
60
|
-
const newEdgeTypesV = Number(newG.edgeTypesV) || 0
|
|
61
|
-
const oldBarrelResolutionV = Number(oldG.barrelResolutionV) || 0
|
|
62
|
-
const newBarrelResolutionV = Number(newG.barrelResolutionV) || 0
|
|
63
|
-
const oldExtractorSchemaV = Number(oldG.extractorSchemaV) || 0
|
|
64
|
-
const newExtractorSchemaV = Number(newG.extractorSchemaV) || 0
|
|
65
|
-
const schemaChanges = [
|
|
66
|
-
oldEdgeTypesV !== newEdgeTypesV ? `edges v${oldEdgeTypesV}→v${newEdgeTypesV}` : null,
|
|
67
|
-
oldBarrelResolutionV !== newBarrelResolutionV ? `barrels v${oldBarrelResolutionV}→v${newBarrelResolutionV}` : null,
|
|
68
|
-
oldExtractorSchemaV !== newExtractorSchemaV ? `extractor v${oldExtractorSchemaV}→v${newExtractorSchemaV}` : null,
|
|
69
|
-
].filter(Boolean)
|
|
70
|
-
const schemaMigration = schemaChanges.length > 0
|
|
71
|
-
const oldNodeIndex = stableNodeIndex(oldG)
|
|
72
|
-
const newNodeIndex = stableNodeIndex(newG)
|
|
73
|
-
|
|
74
|
-
const edgeClass = (l) => l.typeOnly === true ? 'type' : l.compileOnly === true ? 'compile' : 'runtime'
|
|
75
|
-
const stableEndpoint = (index, value) => {
|
|
76
|
-
const raw = edgeEndpoint(value)
|
|
77
|
-
return index.byRaw.get(raw) || raw
|
|
78
|
-
}
|
|
79
|
-
const edgeKey = (l, index) => `${stableEndpoint(index, l.source)}|${l.relation || ''}|${schemaMigration ? 'untyped' : edgeClass(l)}|${stableEndpoint(index, l.target)}`
|
|
80
|
-
const edgeSet = (graph, index) => new Set((graph.links || []).map((l) => edgeKey(l, index)))
|
|
81
|
-
const oldEdges = edgeSet(oldG, oldNodeIndex)
|
|
82
|
-
const newEdges = edgeSet(newG, newNodeIndex)
|
|
83
|
-
|
|
84
|
-
const moduleEdges = (graph, classification) => {
|
|
85
|
-
const set = new Set()
|
|
86
|
-
for (const l of graph.links || []) {
|
|
87
|
-
if (isStructuralRelation(l.relation) || l.barrelProxy === true) continue
|
|
88
|
-
if (edgeClass(l) !== classification) continue
|
|
89
|
-
const a = folderOfFile(fileOfId(edgeEndpoint(l.source)))
|
|
90
|
-
const b = folderOfFile(fileOfId(edgeEndpoint(l.target)))
|
|
91
|
-
if (a !== b) set.add(`${a} → ${b}`)
|
|
92
|
-
}
|
|
93
|
-
return set
|
|
94
|
-
}
|
|
95
|
-
// A parser/schema upgrade can discover edges that the old graph could not represent (Rust use/mod in
|
|
96
|
-
// edgeTypesV2). Do not call that architecture drift: establish a fresh classification baseline.
|
|
97
|
-
const oldMods = schemaMigration ? new Set() : moduleEdges(oldG, 'runtime')
|
|
98
|
-
const newMods = schemaMigration ? new Set() : moduleEdges(newG, 'runtime')
|
|
99
|
-
const oldTypeMods = schemaMigration ? new Set() : moduleEdges(oldG, 'type')
|
|
100
|
-
const newTypeMods = schemaMigration ? new Set() : moduleEdges(newG, 'type')
|
|
101
|
-
const oldCompileMods = schemaMigration ? new Set() : moduleEdges(oldG, 'compile')
|
|
102
|
-
const newCompileMods = schemaMigration ? new Set() : moduleEdges(newG, 'compile')
|
|
103
|
-
|
|
104
|
-
const incoming = (graph, index) => {
|
|
105
|
-
const m = new Map()
|
|
106
|
-
for (const l of graph.links || []) {
|
|
107
|
-
if (isStructuralRelation(l.relation) || l.barrelProxy === true) continue
|
|
108
|
-
const t = stableEndpoint(index, l.target)
|
|
109
|
-
m.set(t, (m.get(t) || 0) + 1)
|
|
110
|
-
}
|
|
111
|
-
return m
|
|
112
|
-
}
|
|
113
|
-
const oldIn = incoming(oldG, oldNodeIndex)
|
|
114
|
-
const newIn = incoming(newG, newNodeIndex)
|
|
115
|
-
|
|
116
|
-
const cycles = (graph, includeTypeOnly) => {
|
|
117
|
-
try {
|
|
118
|
-
const sccs = findSccs(buildFileImportGraph(graph, {includeTypeOnly}).adj)
|
|
119
|
-
.map((members) => members.map(String).sort())
|
|
120
|
-
.sort((a, b) => b.length - a.length || a.join('\n').localeCompare(b.join('\n')))
|
|
121
|
-
return {
|
|
122
|
-
count: sccs.length,
|
|
123
|
-
largest: sccs[0]?.length || 0,
|
|
124
|
-
groups: sccs,
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
return null
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const cycleDelta = (before, after) => {
|
|
131
|
-
if (!before || !after) return null
|
|
132
|
-
const key = (group) => group.join('|')
|
|
133
|
-
const beforeKeys = new Set(before.groups.map(key))
|
|
134
|
-
const afterKeys = new Set(after.groups.map(key))
|
|
135
|
-
const overlap = (a, b) => {
|
|
136
|
-
const bSet = new Set(b)
|
|
137
|
-
return a.reduce((n, member) => n + (bSet.has(member) ? 1 : 0), 0)
|
|
138
|
-
}
|
|
139
|
-
const unmatchedBefore = before.groups.filter((group) => !afterKeys.has(key(group)))
|
|
140
|
-
const unmatchedAfter = after.groups.filter((group) => !beforeKeys.has(key(group)))
|
|
141
|
-
const changed = unmatchedAfter.filter((group) => unmatchedBefore.some((old) => overlap(group, old) >= 2))
|
|
142
|
-
const introduced = unmatchedAfter.filter((group) => !unmatchedBefore.some((old) => overlap(group, old) >= 2))
|
|
143
|
-
const resolved = unmatchedBefore.filter((group) => !unmatchedAfter.some((next) => overlap(group, next) >= 2))
|
|
144
|
-
return {
|
|
145
|
-
before: before.count,
|
|
146
|
-
after: after.count,
|
|
147
|
-
largestBefore: before.largest,
|
|
148
|
-
largestAfter: after.largest,
|
|
149
|
-
introduced: introduced.map(key),
|
|
150
|
-
resolved: resolved.map(key),
|
|
151
|
-
membershipChanged: changed.length,
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
schemaMigration: schemaMigration ? {from: oldEdgeTypesV, to: newEdgeTypesV, changes: schemaChanges} : null,
|
|
157
|
-
nodes: {
|
|
158
|
-
added: [...newNodeIndex.keys].filter((key) => !oldNodeIndex.keys.has(key)).map((key) => newNodeIndex.rawByStable.get(key)),
|
|
159
|
-
removed: [...oldNodeIndex.keys].filter((key) => !newNodeIndex.keys.has(key)).map((key) => oldNodeIndex.rawByStable.get(key))
|
|
160
|
-
},
|
|
161
|
-
edges: {
|
|
162
|
-
added: [...newEdges].filter((k) => !oldEdges.has(k)).length,
|
|
163
|
-
removed: [...oldEdges].filter((k) => !newEdges.has(k)).length
|
|
164
|
-
},
|
|
165
|
-
moduleEdges: {
|
|
166
|
-
added: [...newMods].filter((k) => !oldMods.has(k)),
|
|
167
|
-
removed: [...oldMods].filter((k) => !newMods.has(k)),
|
|
168
|
-
typeAdded: [...newTypeMods].filter((k) => !oldTypeMods.has(k)),
|
|
169
|
-
typeRemoved: [...oldTypeMods].filter((k) => !newTypeMods.has(k)),
|
|
170
|
-
compileAdded: [...newCompileMods].filter((k) => !oldCompileMods.has(k)),
|
|
171
|
-
compileRemoved: [...oldCompileMods].filter((k) => !newCompileMods.has(k)),
|
|
172
|
-
},
|
|
173
|
-
// survived the rebuild but lost every caller/importer — likely made dead by the change
|
|
174
|
-
orphaned: [...oldIn.keys()]
|
|
175
|
-
.filter((key) => newNodeIndex.keys.has(key) && !newIn.has(key))
|
|
176
|
-
.map((key) => newNodeIndex.rawByStable.get(key) || oldNodeIndex.rawByStable.get(key)),
|
|
177
|
-
cycles: {
|
|
178
|
-
runtime: schemaMigration ? null : cycleDelta(cycles(oldG, false), cycles(newG, false)),
|
|
179
|
-
// Backward-compatible field name: since edgeTypesV 2 this includes typeOnly and compileOnly.
|
|
180
|
-
typeInclusive: schemaMigration ? null : cycleDelta(cycles(oldG, true), cycles(newG, true)),
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function formatGraphDiff(d) {
|
|
186
|
-
if (!d.nodes.added.length && !d.nodes.removed.length && !d.edges.added && !d.edges.removed) {
|
|
187
|
-
return d.schemaMigration
|
|
188
|
-
? `Graph extractor/schema upgraded (${d.schemaMigration.changes.join(', ')}); compile-time baseline established. Runtime/compile-time cycle and module classifications are intentionally not compared on this rebuild.`
|
|
189
|
-
: 'No structural change between the two graph states.'
|
|
190
|
-
}
|
|
191
|
-
const cap = (list, n) => list.slice(0, n).map((x) => ` ${x}`).concat(list.length > n ? [` … +${list.length - n} more`] : [])
|
|
192
|
-
const lines = [`Structural delta: nodes +${d.nodes.added.length}/−${d.nodes.removed.length}, edges +${d.edges.added}/−${d.edges.removed}.`]
|
|
193
|
-
if (d.schemaMigration) lines.push(`Graph extractor/schema upgraded (${d.schemaMigration.changes.join(', ')}); runtime/compile-time cycle and module classifications are intentionally not compared until the next rebuild.`)
|
|
194
|
-
const runtime = d.cycles?.runtime
|
|
195
|
-
if (runtime && (runtime.before !== runtime.after || runtime.largestBefore !== runtime.largestAfter || runtime.introduced.length || runtime.resolved.length || runtime.membershipChanged)) {
|
|
196
|
-
const changes = []
|
|
197
|
-
if (runtime.introduced.length) changes.push(`${runtime.introduced.length} genuinely new runtime SCC(s) — review`)
|
|
198
|
-
if (runtime.resolved.length) changes.push(`${runtime.resolved.length} runtime SCC(s) resolved`)
|
|
199
|
-
if (runtime.membershipChanged) changes.push(`${runtime.membershipChanged} SCC membership change(s)`)
|
|
200
|
-
const verdict = changes.length ? `; ${changes.join('; ')}` : ''
|
|
201
|
-
lines.push(`Runtime import cycles: count ${runtime.before} → ${runtime.after}, largest SCC ${runtime.largestBefore} → ${runtime.largestAfter}${verdict}.`)
|
|
202
|
-
}
|
|
203
|
-
const all = d.cycles?.typeInclusive
|
|
204
|
-
if (all && (all.before !== all.after || all.largestBefore !== all.largestAfter) &&
|
|
205
|
-
(!runtime || all.before !== runtime.before || all.after !== runtime.after || all.largestBefore !== runtime.largestBefore || all.largestAfter !== runtime.largestAfter)) {
|
|
206
|
-
lines.push(`Compile-time-inclusive dependency SCCs: count ${all.before} → ${all.after}, largest ${all.largestBefore} → ${all.largestAfter} (compile-time coupling, not necessarily a runtime cycle).`)
|
|
207
|
-
}
|
|
208
|
-
if (d.moduleEdges.added.length) lines.push('NEW module dependencies (architecture drift — review):', ...cap(d.moduleEdges.added, 12))
|
|
209
|
-
if (d.moduleEdges.removed.length) lines.push('Removed module dependencies (decoupling confirmed):', ...cap(d.moduleEdges.removed, 12))
|
|
210
|
-
if (d.moduleEdges.typeAdded.length) lines.push('New type-only module dependencies (compile-time coupling):', ...cap(d.moduleEdges.typeAdded, 12))
|
|
211
|
-
if (d.moduleEdges.typeRemoved.length) lines.push('Removed type-only module dependencies:', ...cap(d.moduleEdges.typeRemoved, 12))
|
|
212
|
-
if (d.moduleEdges.compileAdded.length) lines.push('New compile-only module dependencies (compile-time coupling):', ...cap(d.moduleEdges.compileAdded, 12))
|
|
213
|
-
if (d.moduleEdges.compileRemoved.length) lines.push('Removed compile-only module dependencies:', ...cap(d.moduleEdges.compileRemoved, 12))
|
|
214
|
-
if (d.orphaned.length) lines.push('Symbols that lost their last caller/importer (now dead?):', ...cap(d.orphaned, 10))
|
|
215
|
-
if (d.nodes.added.length) lines.push('Added nodes:', ...cap(d.nodes.added, 12))
|
|
216
|
-
if (d.nodes.removed.length) lines.push('Removed nodes:', ...cap(d.nodes.removed, 12))
|
|
217
|
-
return lines.join('\n')
|
|
218
|
-
}
|