weavatrix 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +14 -71
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +45 -0
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +114 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +197 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +97 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/mcp-server.mjs +35 -146
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -1,497 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {readFileSync, statSync} from 'node:fs'
|
|
6
|
-
import {join} from 'node:path'
|
|
7
|
-
import {spawnSync} from 'node:child_process'
|
|
8
|
-
import {childProcessEnv} from '../child-env.js'
|
|
9
|
-
import {resolveRepoPath} from '../repo-path.js'
|
|
10
|
-
import {isStructuralRelation} from '../graph/relations.js'
|
|
11
|
-
import {edgeProvenance} from '../graph/edge-provenance.js'
|
|
12
|
-
import {mergePrecisionOverlay, precisionSemanticInputsMatch, readPrecisionOverlay} from '../precision/lsp-overlay.js'
|
|
13
|
-
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
14
|
-
|
|
15
|
-
// ---- graph load + indexes -----------------------------------------------------------------------
|
|
16
|
-
export function loadGraph(path, {repoRoot = null} = {}) {
|
|
17
|
-
const saved = JSON.parse(readFileSync(path, 'utf8'))
|
|
18
|
-
const overlay = readPrecisionOverlay(path, saved)
|
|
19
|
-
const safeOverlay = repoRoot && typeof overlay?.semanticInputFingerprint === 'string'
|
|
20
|
-
&& !precisionSemanticInputsMatch(overlay, repoRoot, saved)
|
|
21
|
-
? null : overlay
|
|
22
|
-
const raw = mergePrecisionOverlay(saved, safeOverlay)
|
|
23
|
-
const nodes = Array.isArray(raw.nodes) ? raw.nodes : []
|
|
24
|
-
const links = Array.isArray(raw.links) ? raw.links : []
|
|
25
|
-
const byId = new Map()
|
|
26
|
-
const byLabel = new Map()
|
|
27
|
-
for (const n of nodes) {
|
|
28
|
-
if (!n || n.id == null) continue
|
|
29
|
-
byId.set(String(n.id), n)
|
|
30
|
-
const key = String(n.label ?? n.id).toLowerCase()
|
|
31
|
-
if (!byLabel.has(key)) byLabel.set(key, [])
|
|
32
|
-
byLabel.get(key).push(n)
|
|
33
|
-
}
|
|
34
|
-
const out = new Map() // id -> [{id, relation, confidence}]
|
|
35
|
-
const inn = new Map()
|
|
36
|
-
const push = (map, k, v) => {
|
|
37
|
-
if (!map.has(k)) map.set(k, [])
|
|
38
|
-
map.get(k).push(v)
|
|
39
|
-
}
|
|
40
|
-
for (const e of links) {
|
|
41
|
-
if (!e || e.source == null || e.target == null) continue
|
|
42
|
-
const s = String(e.source)
|
|
43
|
-
const t = String(e.target)
|
|
44
|
-
const metadata = {
|
|
45
|
-
relation: e.relation,
|
|
46
|
-
confidence: e.confidence,
|
|
47
|
-
provenance: edgeProvenance(e),
|
|
48
|
-
...(e.typeOnly === true ? {typeOnly: true} : {}),
|
|
49
|
-
...(e.compileOnly === true ? {compileOnly: true} : {}),
|
|
50
|
-
...(Number.isInteger(e.line) ? {line: e.line} : {}),
|
|
51
|
-
...(typeof e.specifier === 'string' ? {specifier: e.specifier} : {}),
|
|
52
|
-
...(e.barrelProxy === true ? {barrelProxy: true} : {}),
|
|
53
|
-
...(e.semanticOrigin === true ? {semanticOrigin: true} : {}),
|
|
54
|
-
...(typeof e.viaBarrel === 'string' ? {viaBarrel: e.viaBarrel} : {}),
|
|
55
|
-
}
|
|
56
|
-
push(out, s, {id: t, ...metadata})
|
|
57
|
-
push(inn, t, {id: s, ...metadata})
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
nodes, links, byId, byLabel, out, inn,
|
|
61
|
-
repoBoundaryV: Number(raw.repoBoundaryV) || 0,
|
|
62
|
-
edgeTypesV: Number(raw.edgeTypesV) || 0,
|
|
63
|
-
edgeProvenanceV: Number(raw.edgeProvenanceV) || 0,
|
|
64
|
-
barrelResolutionV: Number(raw.barrelResolutionV) || 0,
|
|
65
|
-
reExportOccurrencesV: Number(raw.reExportOccurrencesV) || 0,
|
|
66
|
-
symbolSpacesV: Number(raw.symbolSpacesV) || 0,
|
|
67
|
-
reExportOccurrences: Array.isArray(raw.reExportOccurrences) ? raw.reExportOccurrences : [],
|
|
68
|
-
jsExportRecords: raw.jsExportRecords && typeof raw.jsExportRecords === 'object' ? raw.jsExportRecords : {},
|
|
69
|
-
extractorSchemaV: Number(raw.extractorSchemaV) || 0,
|
|
70
|
-
extImportsV: Number(raw.extImportsV) || 0,
|
|
71
|
-
complexityV: Number(raw.complexityV) || 0,
|
|
72
|
-
graphBuildMode: ['full', 'no-tests', 'tests-only'].includes(raw.graphBuildMode) ? raw.graphBuildMode : 'full',
|
|
73
|
-
graphBuildScope: typeof raw.graphBuildScope === 'string' ? raw.graphBuildScope : null,
|
|
74
|
-
graphRevision: typeof raw.graphRevision === 'string' ? raw.graphRevision : null,
|
|
75
|
-
repositoryFreshnessProbeV: Number(raw.repositoryFreshnessProbeV) || 0,
|
|
76
|
-
repositoryFreshnessBuilderSchemaV: Number(raw.repositoryFreshnessBuilderSchemaV) || 0,
|
|
77
|
-
repositoryFreshnessBuilderVersion: typeof raw.repositoryFreshnessBuilderVersion === 'string' ? raw.repositoryFreshnessBuilderVersion : null,
|
|
78
|
-
repositoryFreshnessProbe: typeof raw.repositoryFreshnessProbe === 'string' ? raw.repositoryFreshnessProbe : null,
|
|
79
|
-
repositoryFreshnessMode: typeof raw.repositoryFreshnessMode === 'string' ? raw.repositoryFreshnessMode : null,
|
|
80
|
-
graphPrecisionMode: raw.graphPrecisionMode === 'off' ? 'off' : 'lsp',
|
|
81
|
-
precisionOverlayV: Number(raw.precisionOverlayV) || 0,
|
|
82
|
-
precision: raw.precision || null,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export const isSymbol = (id) => String(id).includes('#')
|
|
87
|
-
export const labelOf = (g, id) => {
|
|
88
|
-
const n = g.byId.get(String(id))
|
|
89
|
-
return n ? String(n.label ?? n.id) : String(id)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Connectivity ignores structural file/symbol and class/method ownership, so runtime/compile-time
|
|
93
|
-
// dependency ranks never treat nesting as a call or import.
|
|
94
|
-
export const connList = (list) => (list || []).filter((e) => !isStructuralRelation(e.relation) && e.barrelProxy !== true)
|
|
95
|
-
export const degreeOf = (g, id) => connList(g.out.get(id)).length + connList(g.inn.get(id)).length
|
|
96
|
-
export const uniqueConnCount = (list) => new Set(connList(list).map((e) => String(e.id))).size
|
|
97
|
-
|
|
98
|
-
// Resolve a user-supplied "label" to a node: exact id → exact label → ci label → substring (best degree).
|
|
99
|
-
// Returns {node, matches, alternates} so callers can disclose ambiguity instead of silently picking one.
|
|
100
|
-
export function resolveNodeInfo(g, query) {
|
|
101
|
-
const q = String(query ?? '').trim()
|
|
102
|
-
if (!q) return {node: null, matches: 0, alternates: []}
|
|
103
|
-
if (g.byId.has(q)) return {node: g.byId.get(q), matches: 1, alternates: []}
|
|
104
|
-
const lc = q.toLowerCase()
|
|
105
|
-
const exactLabel = g.byLabel.get(lc)
|
|
106
|
-
if (exactLabel?.length) return pickBest(g, exactLabel)
|
|
107
|
-
// substring over id + label
|
|
108
|
-
const hits = []
|
|
109
|
-
for (const n of g.nodes) {
|
|
110
|
-
const id = String(n.id).toLowerCase()
|
|
111
|
-
const lbl = String(n.label ?? '').toLowerCase()
|
|
112
|
-
if (id.includes(lc) || lbl.includes(lc)) hits.push(n)
|
|
113
|
-
if (hits.length > 500) break
|
|
114
|
-
}
|
|
115
|
-
return hits.length ? pickBest(g, hits) : {node: null, matches: 0, alternates: []}
|
|
116
|
-
}
|
|
117
|
-
export const resolveNode = (g, query) => resolveNodeInfo(g, query).node
|
|
118
|
-
function pickBest(g, list) {
|
|
119
|
-
const node = bestByDegree(g, list)
|
|
120
|
-
const alternates = list
|
|
121
|
-
.filter((n) => n !== node)
|
|
122
|
-
.sort((a, b) => degreeOf(g, b.id) - degreeOf(g, a.id))
|
|
123
|
-
.slice(0, 4)
|
|
124
|
-
.map((n) => `${n.label ?? n.id} [${n.id}]`)
|
|
125
|
-
return {node, matches: list.length, alternates}
|
|
126
|
-
}
|
|
127
|
-
// One-line disclosure when a fuzzy label matched several nodes — silently picking one hides wrong-node errors.
|
|
128
|
-
export function ambiguityNote(query, info) {
|
|
129
|
-
if (!info.node || info.matches <= 1) return null
|
|
130
|
-
const more = info.matches - 1 - info.alternates.length
|
|
131
|
-
return `Note: "${query}" matched ${info.matches} nodes; using the best-connected. Others: ${info.alternates.join(', ')}${more > 0 ? ` (+${more} more)` : ''}`
|
|
132
|
-
}
|
|
133
|
-
const bestByDegree = (g, list) =>
|
|
134
|
-
list.reduce((best, n) => (degreeOf(g, n.id) > degreeOf(g, best.id) ? n : best), list[0])
|
|
135
|
-
|
|
136
|
-
// Instructional words describe the requested answer, not repository concepts. Letting them become
|
|
137
|
-
// fuzzy seeds made broad prompts select symbols such as `jest.config.cjs#path` for an HTTP request
|
|
138
|
-
// path. Exact file/symbol questions still have seed_files / inspect_symbol as explicit controls.
|
|
139
|
-
const QUERY_STOP = new Set('a an and are around architecture best code do does exact explain find focus focused for from how identify in inspect inspection is logic me of or path production project repository request requests rest show symbol symbols the through to trace what where which with'.split(' '))
|
|
140
|
-
const QUERY_INTENTS = [
|
|
141
|
-
['bootstrap', ['bootstrap', 'startup', 'entrypoint', 'entry', 'main', 'root', 'app', 'application', 'applications', 'index', 'server', 'cli']],
|
|
142
|
-
['tool-execution', ['tool', 'tools', 'tooling', 'mcp', 'execution', 'execute', 'invocation', 'invoke', 'dispatch', 'dispatcher', 'handler', 'catalog', 'registry']],
|
|
143
|
-
['auth', ['auth', 'authentication', 'authorization', 'login', 'session', 'authgate']],
|
|
144
|
-
['routing', ['routing', 'router', 'routes', 'route', 'navigation']],
|
|
145
|
-
['layout', ['layout', 'layouts', 'shell']],
|
|
146
|
-
['api', ['api', 'apis', 'endpoint', 'endpoints', 'client']],
|
|
147
|
-
['state', ['state', 'store', 'stores', 'reducer', 'context']],
|
|
148
|
-
]
|
|
149
|
-
const INTENT_BY_TERM = new Map(QUERY_INTENTS
|
|
150
|
-
.filter(([id]) => id !== 'tool-execution')
|
|
151
|
-
.flatMap(([id, terms]) => terms.map((term) => [term, {id, terms}])))
|
|
152
|
-
const TOOL_EXECUTION_TERMS = QUERY_INTENTS.find(([id]) => id === 'tool-execution')[1]
|
|
153
|
-
const TOOL_EXECUTION_TRIGGERS = new Set(['tool', 'tools', 'tooling', 'mcp'])
|
|
154
|
-
const wordsOf = (value) => String(value ?? '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean)
|
|
155
|
-
const normPath = (value) => String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
156
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
157
|
-
const CLASS_QUERY_TERMS = Object.freeze({
|
|
158
|
-
test: ['test', 'tests', 'testing', 'spec', 'specs', 'unit'],
|
|
159
|
-
e2e: ['e2e', 'playwright', 'cypress'],
|
|
160
|
-
generated: ['generated', 'autogenerated', 'dist'],
|
|
161
|
-
mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
|
|
162
|
-
story: ['story', 'stories', 'storybook'],
|
|
163
|
-
docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
|
|
164
|
-
benchmark: ['benchmark', 'benchmarks', 'bench'],
|
|
165
|
-
temp: ['temp', 'temporary', 'tmp'],
|
|
166
|
-
})
|
|
167
|
-
const CODE_FILE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|kt|kts|cs|rb|php)$/i
|
|
168
|
-
const DATA_OR_PROSE_RE = /\.(?:json|ya?ml|toml|ini|md|mdx|rst|adoc|html?|css|scss|less|svg)$/i
|
|
169
|
-
const LANGUAGE_EXTENSIONS = Object.freeze({
|
|
170
|
-
rust: ['rs'], python: ['py', 'pyi'], typescript: ['ts', 'tsx', 'mts', 'cts'],
|
|
171
|
-
javascript: ['js', 'jsx', 'mjs', 'cjs'], go: ['go'], java: ['java'], csharp: ['cs'],
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
function requestedLanguages(query) {
|
|
175
|
-
const raw = String(query || '')
|
|
176
|
-
const words = new Set(wordsOf(query))
|
|
177
|
-
const requested = new Set()
|
|
178
|
-
if (words.has('rust')) requested.add('rust')
|
|
179
|
-
if (words.has('python') || words.has('py')) requested.add('python')
|
|
180
|
-
if (words.has('typescript') || words.has('ts') || /typescript/i.test(raw)) requested.add('typescript')
|
|
181
|
-
if (words.has('javascript') || words.has('js') || words.has('nodejs') || /javascript|node\.?js/i.test(raw)) requested.add('javascript')
|
|
182
|
-
if (words.has('golang') || /(?:^|[^A-Za-z])Go(?:[^A-Za-z]|$)/.test(raw)) requested.add('go')
|
|
183
|
-
if (words.has('java')) requested.add('java')
|
|
184
|
-
if (words.has('csharp') || words.has('dotnet') || /csharp|(?:^|[^A-Za-z])C#(?:[^A-Za-z]|$)/i.test(raw)) requested.add('csharp')
|
|
185
|
-
return new Set([...requested].flatMap((language) => LANGUAGE_EXTENSIONS[language]))
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const matchesLanguage = (node, extensions) => {
|
|
189
|
-
if (!extensions.size) return true
|
|
190
|
-
const match = /\.([^.\/]+)$/.exec(sourceFileOf(node))
|
|
191
|
-
return !!match && extensions.has(match[1].toLowerCase())
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const sourceFileOf = (node) => {
|
|
195
|
-
const source = node?.source_file || String(node?.id ?? '').split('#', 1)[0]
|
|
196
|
-
return normPath(source)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function requestedPathClasses(query) {
|
|
200
|
-
const words = new Set(wordsOf(query))
|
|
201
|
-
const requested = new Set()
|
|
202
|
-
for (const [category, terms] of Object.entries(CLASS_QUERY_TERMS)) {
|
|
203
|
-
if (terms.some((term) => words.has(term))) requested.add(category)
|
|
204
|
-
}
|
|
205
|
-
// E2E is also classified as test. Conversely, a broad request for tests should be allowed to
|
|
206
|
-
// surface all test kinds instead of silently hiding integration/e2e files.
|
|
207
|
-
if (requested.has('test')) requested.add('e2e')
|
|
208
|
-
if (requested.has('e2e')) requested.add('test')
|
|
209
|
-
return requested
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isQueryEligible(node, requestedClasses, classificationCache, classifier) {
|
|
213
|
-
const source = sourceFileOf(node)
|
|
214
|
-
if (!source) return true
|
|
215
|
-
// Query ranking needs path/config classes, not generated-header inspection. Supplying bounded
|
|
216
|
-
// content avoids opening every source file in a large repository merely to choose a few seeds.
|
|
217
|
-
if (!classificationCache.has(source)) classificationCache.set(source, classifier.explain(source, {content: ''}))
|
|
218
|
-
const info = classificationCache.get(source)
|
|
219
|
-
const classified = NON_PRODUCT_CLASSES.filter((category) => hasPathClass(info, category))
|
|
220
|
-
return classified.length === 0 || classified.some((category) => requestedClasses.has(category))
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// A graph query has no repository filesystem context, so it cannot safely execute package scripts or
|
|
224
|
-
// guess a package.json beside an arbitrary graph path. Prefer evidence already present in the graph:
|
|
225
|
-
// conventional executable paths, explicit entry metadata when a builder provides it, and entry-like
|
|
226
|
-
// topology (few importers, real outgoing runtime links).
|
|
227
|
-
function entrypointSignal(g, node, source, stem) {
|
|
228
|
-
if (isSymbol(node.id) || !CODE_FILE_RE.test(source)) return 0
|
|
229
|
-
const segments = source.split('/')
|
|
230
|
-
const depth = segments.length
|
|
231
|
-
let score = 0
|
|
232
|
-
if (node.entrypoint === true || node.is_entrypoint === true || node.declared_entry === true) score = 72
|
|
233
|
-
if (/^bin\//.test(source) || /\/(?:bin|cmd)\//.test(source)) score = Math.max(score, 62)
|
|
234
|
-
if (depth <= 2 && /^(?:index|main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 60)
|
|
235
|
-
if (depth <= 2 && /(?:^|[-_.])(?:main|server|cli|bootstrap|entry)(?:$|[-_.])/.test(stem)) score = Math.max(score, 57)
|
|
236
|
-
if (depth <= 3 && /^(?:main|app|server|cli|bootstrap|entry|run)$/.test(stem)) score = Math.max(score, 52)
|
|
237
|
-
const incoming = uniqueConnCount(g.inn.get(String(node.id)))
|
|
238
|
-
const outgoing = uniqueConnCount(g.out.get(String(node.id)))
|
|
239
|
-
if (score && incoming === 0 && outgoing > 0) score += Math.min(7, 2 + outgoing)
|
|
240
|
-
return score
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function toolExecutionSignal(node, source, words, stem) {
|
|
244
|
-
if (!CODE_FILE_RE.test(source)) return 0
|
|
245
|
-
let score = 0
|
|
246
|
-
if (/(^|\/)(?:mcp(?:[-_.][^/]*)?|tools?)(?:\/|[-_.])/.test(source)) score = Math.max(score, 51)
|
|
247
|
-
// Dispatch/catalog files explain how the tool set is assembled and invoked; individual
|
|
248
|
-
// `tools-*` modules explain only one capability. Prefer the control plane for broad questions.
|
|
249
|
-
if (/^(?:catalog|dispatch(?:er)?|registry)$/.test(stem)) score = Math.max(score, 68)
|
|
250
|
-
if (/^(?:tool[-_.]?(?:handler|runner|executor)|tools?[-_.])/.test(stem)) score = Math.max(score, 55)
|
|
251
|
-
if ([...words].some((word) => /^(?:dispatch|dispatcher|toolcall|toolhandler|executetool|invoketool|calltool)$/.test(word))) score = Math.max(score, 64)
|
|
252
|
-
if (source.includes('/mcp/') || stem.includes('mcp-server')) score = Math.max(score, 48)
|
|
253
|
-
return score
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function queryConcepts(query) {
|
|
257
|
-
const tokens = wordsOf(query)
|
|
258
|
-
const toolExecution = tokens.some((token) => TOOL_EXECUTION_TRIGGERS.has(token))
|
|
259
|
-
const explanatoryWork = tokens.some((token) => token === 'how' || token === 'explain')
|
|
260
|
-
const seen = new Set()
|
|
261
|
-
const concepts = []
|
|
262
|
-
for (const raw of tokens) {
|
|
263
|
-
if (raw.length < 2 || QUERY_STOP.has(raw)) continue
|
|
264
|
-
if (explanatoryWork && (raw === 'work' || raw === 'works' || raw === 'working')) continue
|
|
265
|
-
if (toolExecution && TOOL_EXECUTION_TERMS.includes(raw)) {
|
|
266
|
-
if (seen.has('tool-execution')) continue
|
|
267
|
-
const trigger = tokens.find((token) => TOOL_EXECUTION_TRIGGERS.has(token)) || raw
|
|
268
|
-
seen.add('tool-execution')
|
|
269
|
-
concepts.push({id: 'tool-execution', raw: trigger, terms: [trigger, ...TOOL_EXECUTION_TERMS.filter((term) => term !== trigger)]})
|
|
270
|
-
continue
|
|
271
|
-
}
|
|
272
|
-
const intent = INTENT_BY_TERM.get(raw)
|
|
273
|
-
const id = intent?.id || raw
|
|
274
|
-
if (seen.has(id)) continue
|
|
275
|
-
seen.add(id)
|
|
276
|
-
concepts.push({id, raw, terms: intent ? [raw, ...intent.terms.filter((term) => term !== raw)] : [raw]})
|
|
277
|
-
}
|
|
278
|
-
return concepts
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// A code-shaped identifier in prose is stronger evidence than surrounding architecture words.
|
|
282
|
-
// For example, `startMitigate` should seed its exact controller/service/messaging declarations,
|
|
283
|
-
// not one arbitrary file for each of "controller", "service", and "flow". This remains bounded
|
|
284
|
-
// label matching rather than semantic search; ambiguous identifiers are deliberately all retained.
|
|
285
|
-
function exactIdentifierSeeds(g, query, limit, {repoRoot = null} = {}) {
|
|
286
|
-
const identifiers = [...new Set((String(query || '').match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [])
|
|
287
|
-
.filter((token) => /(?:[a-z0-9][A-Z]|_)/.test(token))
|
|
288
|
-
.map((token) => token.toLowerCase()))]
|
|
289
|
-
if (!identifiers.length) return []
|
|
290
|
-
const requestedClasses = requestedPathClasses(query)
|
|
291
|
-
const languageExtensions = requestedLanguages(query)
|
|
292
|
-
const classifier = createPathClassifier(repoRoot)
|
|
293
|
-
const classificationCache = new Map()
|
|
294
|
-
const matches = []
|
|
295
|
-
for (const identifier of identifiers) {
|
|
296
|
-
const candidates = g.nodes.filter((node) => {
|
|
297
|
-
const label = String(node.label || '').replace(/\(\)$/, '').toLowerCase()
|
|
298
|
-
return label === identifier
|
|
299
|
-
&& matchesLanguage(node, languageExtensions)
|
|
300
|
-
&& isQueryEligible(node, requestedClasses, classificationCache, classifier)
|
|
301
|
-
}).sort((left, right) => degreeOf(g, right.id) - degreeOf(g, left.id)
|
|
302
|
-
|| String(left.id).localeCompare(String(right.id)))
|
|
303
|
-
for (const node of candidates) {
|
|
304
|
-
if (!matches.some((existing) => String(existing.id) === String(node.id))) matches.push(node)
|
|
305
|
-
if (matches.length >= limit) return matches
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return matches
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function conceptScore(g, node, concept, queryContext) {
|
|
312
|
-
const id = normPath(node.id)
|
|
313
|
-
const label = String(node.label ?? '').toLowerCase()
|
|
314
|
-
const source = sourceFileOf(node)
|
|
315
|
-
const stem = (label.split('/').pop() || '').replace(/\.[^.]+$/, '')
|
|
316
|
-
const words = new Set(wordsOf(`${node.id} ${node.label ?? ''} ${node.source_file ?? ''}`))
|
|
317
|
-
const segments = new Set(source.split('/').flatMap((part) => wordsOf(part.replace(/\.[^.]+$/, ''))))
|
|
318
|
-
let match = 0
|
|
319
|
-
concept.terms.forEach((term, index) => {
|
|
320
|
-
const primary = index === 0
|
|
321
|
-
if (label === term || stem === term) match = Math.max(match, primary ? 60 : 42)
|
|
322
|
-
else if (segments.has(term)) match = Math.max(match, primary ? 48 : 36)
|
|
323
|
-
else if (words.has(term)) match = Math.max(match, primary ? 36 : 25)
|
|
324
|
-
else if (term.length >= 4 && term !== 'tool' && term !== 'tools' && (id.includes(term) || label.includes(term))) match = Math.max(match, primary ? 12 : 7)
|
|
325
|
-
})
|
|
326
|
-
const extension = (/\.([^.\/]+)$/.exec(source) || [])[1] || ''
|
|
327
|
-
const languageConcept = {
|
|
328
|
-
rust: ['rs'], python: ['py', 'pyi'], py: ['py', 'pyi'],
|
|
329
|
-
typescript: ['ts', 'tsx', 'mts', 'cts'], ts: ['ts', 'tsx', 'mts', 'cts'],
|
|
330
|
-
javascript: ['js', 'jsx', 'mjs', 'cjs'], js: ['js', 'jsx', 'mjs', 'cjs'], nodejs: ['js', 'jsx', 'mjs', 'cjs'],
|
|
331
|
-
golang: ['go'], go: ['go'], java: ['java'], csharp: ['cs'], dotnet: ['cs'],
|
|
332
|
-
}[concept.raw]
|
|
333
|
-
if (languageConcept?.includes(extension) && queryContext.languageExtensions.has(extension)) match = Math.max(match, 58)
|
|
334
|
-
const fileNode = !isSymbol(node.id)
|
|
335
|
-
const depth = source ? source.split('/').length : 9
|
|
336
|
-
if (concept.id === 'bootstrap') match = Math.max(match, entrypointSignal(g, node, source, stem))
|
|
337
|
-
if (concept.id === 'tool-execution') match = Math.max(match, toolExecutionSignal(node, source, words, stem))
|
|
338
|
-
if (!match) return 0
|
|
339
|
-
let score = match + (fileNode ? 7 : 0) + Math.max(0, 4 - depth) + Math.min(2, degreeOf(g, node.id) / 40)
|
|
340
|
-
if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && DATA_OR_PROSE_RE.test(source)) score -= 34
|
|
341
|
-
if ((concept.id === 'bootstrap' || concept.id === 'tool-execution') && !fileNode) score -= 18
|
|
342
|
-
if (queryContext.runtimeIntent && !queryContext.maintenanceIntent && /^(?:scripts?|tools?\/scripts?)\//.test(source)) score -= 32
|
|
343
|
-
// A web/demo index can be a legitimate entry point, but it is not the server/tool entry point of
|
|
344
|
-
// a backend/tool-execution question. Keep it queryable for explicit UI/site questions while
|
|
345
|
-
// preventing it from beating the executable and dispatcher merely because its basename is index.
|
|
346
|
-
if (queryContext.runtimeIntent && /^(?:site|website|public|static|assets)\//.test(source)) score -= 28
|
|
347
|
-
return Math.max(0, score)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Natural-language graph search keeps one strong candidate per concept before filling by aggregate
|
|
351
|
-
// score. This prevents a broad architecture question from spending every seed on one dense API area.
|
|
352
|
-
export function findSeeds(g, query, limit = 8, {repoRoot = null} = {}) {
|
|
353
|
-
const exact = exactIdentifierSeeds(g, query, limit, {repoRoot})
|
|
354
|
-
if (exact.length) return exact
|
|
355
|
-
const concepts = queryConcepts(query)
|
|
356
|
-
if (!concepts.length || limit <= 0) return []
|
|
357
|
-
const requestedClasses = requestedPathClasses(query)
|
|
358
|
-
const languageExtensions = requestedLanguages(query)
|
|
359
|
-
const queryContext = {
|
|
360
|
-
runtimeIntent: concepts.some((concept) => concept.id === 'bootstrap' || concept.id === 'tool-execution'),
|
|
361
|
-
maintenanceIntent: wordsOf(query).some((word) => ['script', 'scripts', 'build', 'release', 'publish', 'packaging'].includes(word)),
|
|
362
|
-
languageExtensions,
|
|
363
|
-
}
|
|
364
|
-
const classifier = createPathClassifier(repoRoot)
|
|
365
|
-
const classificationCache = new Map()
|
|
366
|
-
const rows = g.nodes.filter((node) => matchesLanguage(node, languageExtensions)
|
|
367
|
-
&& isQueryEligible(node, requestedClasses, classificationCache, classifier)).map((node) => {
|
|
368
|
-
const scores = concepts.map((concept) => conceptScore(g, node, concept, queryContext))
|
|
369
|
-
return {node, scores, total: Math.max(...scores) + scores.reduce((sum, score) => sum + score, 0) / 10}
|
|
370
|
-
})
|
|
371
|
-
const chosen = []
|
|
372
|
-
const used = new Set()
|
|
373
|
-
for (let index = 0; index < concepts.length && chosen.length < limit; index++) {
|
|
374
|
-
const best = rows.filter((row) => !used.has(String(row.node.id)) && row.scores[index] > 0)
|
|
375
|
-
.sort((a, b) => b.scores[index] - a.scores[index] || String(a.node.id).localeCompare(String(b.node.id)))[0]
|
|
376
|
-
if (best) { chosen.push(best.node); used.add(String(best.node.id)) }
|
|
377
|
-
}
|
|
378
|
-
rows.filter((row) => row.total > 0 && !used.has(String(row.node.id)))
|
|
379
|
-
.sort((a, b) => b.total - a.total || String(a.node.id).localeCompare(String(b.node.id)))
|
|
380
|
-
.slice(0, Math.max(0, limit - chosen.length))
|
|
381
|
-
.forEach((row) => chosen.push(row.node))
|
|
382
|
-
return chosen
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export function resolveSeedFiles(g, requested, limit = 12) {
|
|
386
|
-
const files = Array.isArray(requested) ? requested.slice(0, limit) : []
|
|
387
|
-
const seeds = []
|
|
388
|
-
const missing = []
|
|
389
|
-
for (const raw of files) {
|
|
390
|
-
const wanted = normPath(raw)
|
|
391
|
-
const node = g.nodes.find((candidate) => !isSymbol(candidate.id)
|
|
392
|
-
&& (normPath(candidate.id) === wanted || normPath(candidate.source_file) === wanted))
|
|
393
|
-
if (!node) missing.push(String(raw))
|
|
394
|
-
else if (!seeds.some((seed) => String(seed.id) === String(node.id))) seeds.push(node)
|
|
395
|
-
}
|
|
396
|
-
return {seeds, missing}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// undirected adjacency for reachability (query/shortest path)
|
|
400
|
-
export function undirectedNeighbors(g, id) {
|
|
401
|
-
const seen = new Map()
|
|
402
|
-
for (const e of g.out.get(id) || []) if (e.barrelProxy !== true) seen.set(e.id, e.relation)
|
|
403
|
-
for (const e of g.inn.get(id) || []) if (e.barrelProxy !== true && !seen.has(e.id)) seen.set(e.id, e.relation)
|
|
404
|
-
return seen
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// ---- staleness ----------------------------------------------------------------------------------
|
|
408
|
-
// The graph is a point-in-time build of graph.json; without a freshness signal an agent cannot tell
|
|
409
|
-
// whether answers reflect the current code. Compare graph.json mtime with the repo's latest commit
|
|
410
|
-
// (cheap `git log -1`), cached for 60s so per-tool warnings don't spawn git on every call.
|
|
411
|
-
let stalenessCache = {key: '', checkedAt: 0, info: null}
|
|
412
|
-
export function graphStaleness(ctx) {
|
|
413
|
-
const now = Date.now()
|
|
414
|
-
if (stalenessCache.info && stalenessCache.key === ctx.graphPath && now - stalenessCache.checkedAt < 60_000) return stalenessCache.info
|
|
415
|
-
const info = {builtAt: null, headAt: null, stale: false, behind: null}
|
|
416
|
-
try { info.builtAt = statSync(ctx.graphPath).mtime } catch { /* no graph file — nothing to report */ }
|
|
417
|
-
if (ctx.repoRoot && info.builtAt) {
|
|
418
|
-
try {
|
|
419
|
-
const head = spawnSync('git', ['-C', ctx.repoRoot, 'log', '-1', '--format=%cI'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
420
|
-
const iso = (head.stdout || '').trim()
|
|
421
|
-
if (head.status === 0 && iso) {
|
|
422
|
-
info.headAt = new Date(iso)
|
|
423
|
-
if (info.headAt > info.builtAt) {
|
|
424
|
-
info.stale = true
|
|
425
|
-
const cnt = spawnSync('git', ['-C', ctx.repoRoot, 'rev-list', '--count', `--since=${info.builtAt.toISOString()}`, 'HEAD'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
426
|
-
if (cnt.status === 0) info.behind = Number(cnt.stdout.trim()) || null
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch { /* git unavailable — degrade to builtAt only */ }
|
|
430
|
-
// Uncommitted work drifts line numbers just as hard as commits do (that is how agents get bitten:
|
|
431
|
-
// they edit, then re-query). Count dirty files actually TOUCHED after the build — a dirty file
|
|
432
|
-
// older than the graph was already part of it.
|
|
433
|
-
try {
|
|
434
|
-
const st = spawnSync('git', ['-C', ctx.repoRoot, 'status', '--porcelain'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
|
|
435
|
-
if (st.status === 0) {
|
|
436
|
-
let newer = 0
|
|
437
|
-
for (const ln of String(st.stdout || '').split(/\r?\n/).filter(Boolean).slice(0, 200)) {
|
|
438
|
-
const p = ln.slice(3).trim().replace(/^"|"$/g, '')
|
|
439
|
-
try { if (statSync(join(ctx.repoRoot, p)).mtime > info.builtAt) newer++ } catch { newer++ } // deleted counts as drift
|
|
440
|
-
}
|
|
441
|
-
info.dirtyNewer = newer
|
|
442
|
-
if (newer > 0) info.stale = true
|
|
443
|
-
}
|
|
444
|
-
} catch { /* git unavailable */ }
|
|
445
|
-
}
|
|
446
|
-
stalenessCache = {key: ctx.graphPath, checkedAt: now, info}
|
|
447
|
-
return info
|
|
448
|
-
}
|
|
449
|
-
export const resetStalenessCache = () => { stalenessCache = {key: '', checkedAt: 0, info: null} }
|
|
450
|
-
export function stalenessLine(ctx) {
|
|
451
|
-
const s = graphStaleness(ctx)
|
|
452
|
-
if (!s.stale) return null
|
|
453
|
-
const bits = []
|
|
454
|
-
if (s.headAt && s.headAt > s.builtAt) bits.push(`${s.behind != null ? `${s.behind} commit${s.behind === 1 ? '' : 's'}` : 'commits'} newer than the graph`)
|
|
455
|
-
if (s.dirtyNewer) bits.push(`${s.dirtyNewer} uncommitted file(s) edited after the build`)
|
|
456
|
-
return `Warning: graph may be stale — the repo has ${bits.join(' and ')} (built ${s.builtAt.toISOString()}). Line numbers may have drifted; call rebuild_graph.`
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Per-file drift check for tools that print exact line numbers: the global warning says the REPO
|
|
460
|
-
// moved; this says THIS file moved — the difference between "be careful" and "these numbers are off".
|
|
461
|
-
export function fileStalenessNote(ctx, sourceFile) {
|
|
462
|
-
if (!ctx?.repoRoot || !sourceFile) return null
|
|
463
|
-
const s = graphStaleness(ctx)
|
|
464
|
-
if (!s.builtAt) return null
|
|
465
|
-
try {
|
|
466
|
-
const resolved = resolveRepoPath(ctx.repoRoot, String(sourceFile))
|
|
467
|
-
if (resolved.ok && statSync(resolved.path).mtime > s.builtAt) {
|
|
468
|
-
return `Note: ${sourceFile} changed after the graph was built — line numbers above may have drifted (rebuild_graph refreshes them).`
|
|
469
|
-
}
|
|
470
|
-
} catch { /* file gone — the read tools will surface that themselves */ }
|
|
471
|
-
return null
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Raw graph.json (with externalImports, file_type, source_end …) for the analysis modules — the MCP's
|
|
475
|
-
// own loadGraph struct strips those fields. Cached by mtime; rebuild_graph changes the mtime → refresh.
|
|
476
|
-
let rawGraphCache = {path: '', mtimeMs: 0, data: null}
|
|
477
|
-
export function rawGraph(ctx) {
|
|
478
|
-
const mtimeMs = statSync(ctx.graphPath).mtimeMs
|
|
479
|
-
if (!rawGraphCache.data || rawGraphCache.path !== ctx.graphPath || rawGraphCache.mtimeMs !== mtimeMs) {
|
|
480
|
-
rawGraphCache = {path: ctx.graphPath, mtimeMs, data: JSON.parse(readFileSync(ctx.graphPath, 'utf8'))}
|
|
481
|
-
}
|
|
482
|
-
return rawGraphCache.data
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Consumers that need semantic precision use the revision-matched effective graph. Structural Git
|
|
486
|
-
// baselines and graph_diff deliberately keep using rawGraph so provider availability cannot fabricate
|
|
487
|
-
// architecture drift.
|
|
488
|
-
export function effectiveRawGraph(ctx) {
|
|
489
|
-
const raw = rawGraph(ctx)
|
|
490
|
-
const overlay = readPrecisionOverlay(ctx.graphPath, raw)
|
|
491
|
-
const safeOverlay = ctx?.repoRoot && typeof overlay?.semanticInputFingerprint === 'string'
|
|
492
|
-
&& !precisionSemanticInputsMatch(overlay, ctx.repoRoot, raw)
|
|
493
|
-
? null : overlay
|
|
494
|
-
return mergePrecisionOverlay(raw, safeOverlay)
|
|
495
|
-
}
|
|
496
|
-
|
|
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'
|
|
497
5
|
export {prevGraphPathFor, edgeEndpoint, fileOfId, diffGraphs, formatGraphDiff} from './graph-diff.mjs'
|