weavatrix 0.2.0 → 0.2.2
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 +104 -10
- package/package.json +5 -1
- package/skill/SKILL.md +34 -7
- package/src/analysis/change-classification.js +27 -2
- package/src/analysis/git-history.js +42 -0
- package/src/analysis/http-contract-wrappers.js +232 -0
- package/src/analysis/http-contracts.js +246 -17
- package/src/graph/edge-provenance.js +31 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +2 -2
- package/src/graph/internal-builder.build.js +4 -0
- package/src/graph/internal-builder.resolvers.js +18 -3
- package/src/mcp/catalog.mjs +50 -7
- package/src/mcp/graph-context.mjs +3 -0
- package/src/mcp/sync-payload.mjs +7 -0
- package/src/mcp/tool-result.mjs +9 -6
- package/src/mcp/tools-actions.mjs +4 -2
- package/src/mcp/tools-architecture.mjs +72 -16
- package/src/mcp/tools-company.mjs +11 -3
- package/src/mcp/tools-graph-hubs.mjs +22 -3
- package/src/mcp/tools-graph.mjs +4 -1
- package/src/mcp/tools-history.mjs +15 -9
- package/src/mcp/tools-impact-change.mjs +1 -1
- package/src/mcp/tools-impact.mjs +1 -0
- package/src/mcp-server.mjs +3 -1
- package/src/path-classification.js +5 -0
package/src/mcp/catalog.mjs
CHANGED
|
@@ -30,16 +30,58 @@ export const HOT_FILES = ['tools-graph.mjs', 'tools-impact.mjs', 'tools-health.m
|
|
|
30
30
|
|
|
31
31
|
function buildTools({tg, ti, th, ta, tar, thi, tc}) {
|
|
32
32
|
const tools = [
|
|
33
|
-
{cap: 'graph', name: 'graph_stats', description: 'Return summary statistics: node count, edge count, communities, confidence
|
|
33
|
+
{cap: 'graph', name: 'graph_stats', description: 'Return summary statistics: node count, edge count, communities, versioned edge-provenance/legacy-confidence breakdowns, and graph build time vs repo HEAD (staleness).', inputSchema: {type: 'object', properties: {}}, run: (g, a, ctx) => tg.tGraphStats(g, ctx)},
|
|
34
34
|
{cap: 'graph', name: 'get_node', description: 'Get full details for a specific node by label or ID.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Node label or ID to look up'}}, required: ['label']}, run: (g, a, ctx) => tg.tGetNode(g, a, ctx)},
|
|
35
35
|
{cap: 'graph', name: 'get_neighbors', description: 'Get all direct neighbors of a node with edge details (1 hop, call sites deduped). For transitive impact use get_dependents; for the impact of your current branch changes use change_impact.', inputSchema: {type: 'object', properties: {label: {type: 'string'}, relation_filter: {type: 'string', description: 'Optional: filter by relation type'}}, required: ['label']}, run: (g, a, ctx) => tg.tGetNeighbors(g, a, ctx)},
|
|
36
36
|
{cap: 'graph', name: 'query_graph', description: 'Explore the graph around a concept (BFS/DFS). Returns a focused, ranked subgraph — the closest, most-connected nodes near the matched seeds, with edges among them — not the full flood; states how many nodes were reached vs shown.', inputSchema: {type: 'object', properties: {question: {type: 'string', description: 'Natural language question or keyword search'}, mode: {type: 'string', enum: ['bfs', 'dfs'], default: 'bfs'}, depth: {type: 'integer', default: 3}, context_filter: {type: 'array', items: {type: 'string'}}, seed_files: {type: 'array', items: {type: 'string'}, maxItems: 12, description: 'Optional exact repo-relative file paths. When resolved, these are the only seeds unless augment_seeds is true'}, augment_seeds: {type: 'boolean', default: false, description: 'With seed_files, also add fuzzy question-derived seeds; false keeps traversal strictly pinned'}, token_budget: {type: 'integer', description: 'Higher budget shows more nodes/edges', default: 2000}}, required: ['question']}, run: (g, a) => tg.tQueryGraph(g, a)},
|
|
37
|
-
{cap: 'graph', name: 'god_nodes', description: 'Rank connectivity hubs by unique call/import/reference neighbors, with class/method ownership reported separately from runtime connectivity. Repeated call sites do not inflate the rank.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', default: 10}}}, run: (g, a) => tg.tGodNodes(g, a)},
|
|
37
|
+
{cap: 'graph', name: 'god_nodes', description: 'Rank production-code connectivity hubs by unique call/import/reference neighbors, with class/method ownership reported separately from runtime connectivity. Repeated call sites do not inflate the rank; classified tests, generated/build output and other non-product paths are excluded by default.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', default: 10}, include_classified: {type: 'boolean', default: false, description: 'Include tests/e2e/generated/build output/mocks/stories/docs/benchmarks/temp and paths explicitly excluded by repository classification'}}}, run: (g, a, ctx) => tg.tGodNodes(g, a, ctx)},
|
|
38
38
|
{cap: 'graph', name: 'shortest_path', description: 'Find the shortest path between two concepts in the knowledge graph.', inputSchema: {type: 'object', properties: {source: {type: 'string'}, target: {type: 'string'}, max_hops: {type: 'integer', default: 8}}, required: ['source', 'target']}, run: (g, a) => tg.tShortestPath(g, a)},
|
|
39
39
|
{cap: 'graph', name: 'get_dependents', description: 'Transitive blast-radius of ONE node: everything that calls/imports/inherits it, directly or through intermediaries (reverse edges, ranked by proximity then connectivity). For a symbol, also follows importers of its containing file. Use before refactoring; get_neighbors shows only 1 hop, change_impact covers your whole current diff at once.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Node label or ID'}, depth: {type: 'integer', description: 'Max reverse hops, default 3', default: 3}, max_nodes: {type: 'integer', description: 'Max dependents to list, default 40', default: 40}}, required: ['label']}, run: (g, a) => ti.tGetDependents(g, a)},
|
|
40
40
|
{cap: 'graph', name: 'change_impact', description: 'Verdict-first, symbol-aware blast radius. Parses a zero-context git diff, distinguishes additive exports from signature/body/removal risk, and reverse-traverses only exact changed seeds so a new API does not inherit every legacy importer of its file. Measured coverage is used when present; otherwise static test reachability is labelled actualCoverage=NOT_AVAILABLE. Pass `diff` for a not-checked-out PR; `files` without a diff remains a conservative fallback.', inputSchema: {type: 'object', properties: {base: {type: 'string', description: 'Base ref, e.g. origin/main or HEAD~1 (default: first existing of origin/HEAD, origin/main, origin/master, main, master)'}, diff: {type: 'string', maxLength: 2097152, description: 'Optional unified diff (prefer --unified=0) for a PR/change that is not checked out; enables symbol-level classification'}, files: {type: 'array', items: {type: 'string'}, maxItems: 500, description: 'Optional repo-relative changed-file hints. Without diff evidence these are classified conservatively rather than guessed additive'}, depth: {type: 'integer', description: 'Max reverse hops, default 2'}, max_nodes: {type: 'integer', description: 'Max impacted nodes to list, default 40'}}}, run: (g, a, ctx) => ti.tChangeImpact(g, a, ctx)},
|
|
41
41
|
{cap: 'graph', name: 'git_history', description: 'Behavioral architecture evidence from bounded local git history: churn × connectivity hotspots, hidden co-change coupling, and expected test/source coupling. Reads numstat only — never commit messages, authors, or source bodies.', inputSchema: {type: 'object', properties: {months: {type: 'integer', enum: [3, 6, 12], default: 6}, max_commits: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, min_pair_count: {type: 'integer', minimum: 2, maximum: 100, default: 3}, max_pairs: {type: 'integer', minimum: 1, maximum: 500, default: 100}, top_n: {type: 'integer', minimum: 1, maximum: 50, default: 10}}}, run: (g, a, ctx) => thi.tGitHistory(g, a, ctx)},
|
|
42
|
-
{
|
|
42
|
+
{
|
|
43
|
+
cap: 'crossrepo',
|
|
44
|
+
name: 'trace_api_contract',
|
|
45
|
+
description: 'Cross-repository HTTP contract, handler-liveness and blast-radius evidence. Select a registered backend and one or more registered clients; joins backend endpoints to built-in, configured, or conservatively auto-discovered HTTP wrappers. Medium/high-confidence external matches mark a handler NOT_DEAD_EXTERNAL_USE; no match remains UNKNOWN. Repository paths stay local and cannot be supplied through this tool.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
backend: {type: 'string', description: 'Backend repository UUID or exact unambiguous registry label'},
|
|
50
|
+
clients: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 20, description: 'Client repository UUIDs or exact unambiguous registry labels'},
|
|
51
|
+
method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']},
|
|
52
|
+
path: {type: 'string', maxLength: 2048, description: 'Optional full route or segment-aligned route fragment; /query matches /edgeAnalytics/query/... and {id}, :id and concrete parameter values are normalized'},
|
|
53
|
+
changed_files: {type: 'array', items: {type: 'string'}, maxItems: 500, description: 'Optional backend repo-relative changed files; only endpoints declared in those files are traced'},
|
|
54
|
+
client_names: {type: 'array', items: {type: 'string', pattern: '^[A-Za-z_$][\\w$]{0,127}$'}, maxItems: 40, uniqueItems: true, description: 'Extra object-style clients whose .get/.post/... methods perform HTTP requests; persistent per-repo configuration belongs in .weavatrix.json httpContracts.clientNames'},
|
|
55
|
+
client_wrappers: {
|
|
56
|
+
type: 'array', maxItems: 100,
|
|
57
|
+
description: 'Fixed-method wrapper calls. Use call+method for get(url), or object+member+method for transport.send(url). url_argument is zero-based.',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object', additionalProperties: false,
|
|
60
|
+
properties: {
|
|
61
|
+
call: {type: 'string', pattern: '^[A-Za-z_$][\\w$]{0,127}$'},
|
|
62
|
+
object: {type: 'string', pattern: '^[A-Za-z_$][\\w$]{0,127}$'},
|
|
63
|
+
member: {type: 'string', pattern: '^[A-Za-z_$][\\w$]{0,127}$'},
|
|
64
|
+
method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']},
|
|
65
|
+
url_argument: {type: 'integer', minimum: 0, maximum: 5, default: 0},
|
|
66
|
+
},
|
|
67
|
+
anyOf: [
|
|
68
|
+
{required: ['call', 'method'], not: {anyOf: [{required: ['object']}, {required: ['member']}]}},
|
|
69
|
+
{required: ['object', 'member', 'method'], not: {required: ['call']}},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
auto_discover_wrappers: {type: 'boolean', default: true, description: 'Discover only simple unambiguous functions that forward a URL parameter directly to a known object-style HTTP client'},
|
|
74
|
+
include_tests: {type: 'boolean', default: false},
|
|
75
|
+
max_impact_depth: {type: 'integer', minimum: 0, maximum: 5, default: 2},
|
|
76
|
+
max_endpoints: {type: 'integer', minimum: 1, maximum: 500, default: 250},
|
|
77
|
+
max_matches: {type: 'integer', minimum: 1, maximum: 5000, default: 1000},
|
|
78
|
+
max_affected_files: {type: 'integer', minimum: 1, maximum: 500, default: 100},
|
|
79
|
+
top_n: {type: 'integer', minimum: 1, maximum: 50, default: 10},
|
|
80
|
+
},
|
|
81
|
+
required: ['backend', 'clients'],
|
|
82
|
+
},
|
|
83
|
+
run: (g, a, ctx) => tc.tTraceApiContract(g, a, ctx),
|
|
84
|
+
},
|
|
43
85
|
{cap: 'graph', name: 'get_community', description: 'Get all nodes in a community by community ID (0-indexed by size).', inputSchema: {type: 'object', properties: {community_id: {type: 'integer', description: 'Community ID (0-indexed by size)'}}, required: ['community_id']}, run: (g, a) => tg.tGetCommunity(g, a)},
|
|
44
86
|
{cap: 'search', name: 'search_code', description: 'Full-text or regex search across the repo source (ripgrep-backed, Node fallback). The graph only stores structure — use this to find literal text/patterns, then get_node/get_neighbors for structure.', inputSchema: {type: 'object', properties: {query: {type: 'string', description: 'text or regex to search for'}, is_regex: {type: 'boolean', default: false}, glob: {type: 'string', description: 'optional path glob, e.g. "*.js" or "src/**"'}, max_results: {type: 'integer', default: 40}}, required: ['query']}, run: (g, a, ctx) => searchCode({repoRoot: ctx.repoRoot, resolveRg}, a)},
|
|
45
87
|
{cap: 'source', name: 'read_source', description: "Read the actual source of a node (by label/ID) or a repo-relative file path — the symbol's lines with context. The graph stores only locations, not source text. For a path read, pass start_line to anchor the window anywhere in the file (otherwise it shows the head).", inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'node label or ID'}, path: {type: 'string', description: 'or a repo-relative file path'}, start_line: {type: 'integer', description: 'anchor line: window = start_line-before .. start_line+after'}, before: {type: 'integer', default: 3}, after: {type: 'integer', default: 40}}}, run: (g, a, ctx) => readSource({repoRoot: ctx.repoRoot, resolveNode, isSymbol}, g, a)},
|
|
@@ -63,8 +105,10 @@ function buildTools({tg, ti, th, ta, tar, thi, tc}) {
|
|
|
63
105
|
{cap: 'hosted', name: 'pull_architecture_contract', description: 'NETWORK / explicit hosted profile: fetch the owner-approved target architecture for the active stable repository UUID, validate it and cache it locally. Sends only the opaque UUID with bearer auth; never source or paths.', inputSchema: {type: 'object', properties: {timeout_ms: {type: 'integer', minimum: 1000, maximum: 120000, default: 30000}}}, run: (g, a, ctx) => ta.tPullArchitectureContract(g, a, ctx)},
|
|
64
106
|
{cap: 'hosted', name: 'sync_graph', description: 'NETWORK / explicit hosted profile, off until configured: derive a bounded evidence snapshot locally and send it with an allowlisted graph payload to an endpoint you configure (env WEAVATRIX_SYNC_URL, WEAVATRIX_SYNC_TOKEN bearer auth). Local analyzers may read source files, but file bodies, snippets, absolute paths and unknown fields are never uploaded. Payload v3 includes architecture/health/stack/package evidence; request v2 only for graph-only compatibility.', inputSchema: {type: 'object', properties: {payload_version: {type: 'integer', enum: [2, 3], default: 3, description: '3 includes the evidence snapshot; 2 is graph-only compatibility and is never selected as a silent fallback'}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 120000, default: 30000, description: 'Network timeout in milliseconds'}}}, run: (g, a, ctx) => ta.tSyncGraph(g, a, ctx)},
|
|
65
107
|
]
|
|
66
|
-
// Every tool supports the same machine-output switch.
|
|
67
|
-
//
|
|
108
|
+
// Every tool supports the same machine-output switch. Text mode is TextContent-only so large
|
|
109
|
+
// analysis payloads are not duplicated into an agent's context; JSON opts into structuredContent
|
|
110
|
+
// and mirrors that envelope into TextContent for older workflow runners. Since MCP output schemas
|
|
111
|
+
// apply to every invocation, tools do not advertise one conditionally here.
|
|
68
112
|
return tools.map((tool) => ({
|
|
69
113
|
...tool,
|
|
70
114
|
inputSchema: {
|
|
@@ -75,11 +119,10 @@ function buildTools({tg, ti, th, ta, tar, thi, tc}) {
|
|
|
75
119
|
type: 'string',
|
|
76
120
|
enum: ['text', 'json'],
|
|
77
121
|
default: 'text',
|
|
78
|
-
description: 'text
|
|
122
|
+
description: 'text returns only the concise TextContent summary; json also returns and mirrors the stable structuredContent envelope',
|
|
79
123
|
},
|
|
80
124
|
},
|
|
81
125
|
},
|
|
82
|
-
outputSchema: {type: 'object', additionalProperties: true},
|
|
83
126
|
}))
|
|
84
127
|
}
|
|
85
128
|
|
|
@@ -8,6 +8,7 @@ import {spawnSync} from 'node:child_process'
|
|
|
8
8
|
import {childProcessEnv} from '../child-env.js'
|
|
9
9
|
import {resolveRepoPath} from '../repo-path.js'
|
|
10
10
|
import {isStructuralRelation} from '../graph/relations.js'
|
|
11
|
+
import {edgeProvenance} from '../graph/edge-provenance.js'
|
|
11
12
|
|
|
12
13
|
// ---- graph load + indexes -----------------------------------------------------------------------
|
|
13
14
|
export function loadGraph(path) {
|
|
@@ -36,6 +37,7 @@ export function loadGraph(path) {
|
|
|
36
37
|
const metadata = {
|
|
37
38
|
relation: e.relation,
|
|
38
39
|
confidence: e.confidence,
|
|
40
|
+
provenance: edgeProvenance(e),
|
|
39
41
|
...(e.typeOnly === true ? {typeOnly: true} : {}),
|
|
40
42
|
...(e.compileOnly === true ? {compileOnly: true} : {}),
|
|
41
43
|
...(Number.isInteger(e.line) ? {line: e.line} : {}),
|
|
@@ -51,6 +53,7 @@ export function loadGraph(path) {
|
|
|
51
53
|
nodes, links, byId, byLabel, out, inn,
|
|
52
54
|
repoBoundaryV: Number(raw.repoBoundaryV) || 0,
|
|
53
55
|
edgeTypesV: Number(raw.edgeTypesV) || 0,
|
|
56
|
+
edgeProvenanceV: Number(raw.edgeProvenanceV) || 0,
|
|
54
57
|
barrelResolutionV: Number(raw.barrelResolutionV) || 0,
|
|
55
58
|
extractorSchemaV: Number(raw.extractorSchemaV) || 0,
|
|
56
59
|
extImportsV: Number(raw.extImportsV) || 0,
|
package/src/mcp/sync-payload.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Versioned, explicit wire schema for sync_graph. Never forward graph.json wholesale: it is a local
|
|
2
2
|
// cache file and may contain future fields or attacker-injected data that are not safe to upload.
|
|
3
3
|
import {sanitizeEvidenceSnapshot} from './sync-evidence.mjs';
|
|
4
|
+
import {edgeProvenance} from '../graph/edge-provenance.js';
|
|
4
5
|
|
|
5
6
|
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
6
7
|
const ABSOLUTE_PATH_FRAGMENT = /(?:^|[\/\s"'`(=])[a-z]:[\\/]|(?:^|[\s"'`(=])(?:\\\\[^\\/\s]+(?:[\\/]|$)|file:(?:\/\/)?[\\/]|\/(?!\/)[^\s])/i;
|
|
@@ -202,6 +203,8 @@ function sanitizeLinkV3(value) {
|
|
|
202
203
|
if (safe) out[key] = safe;
|
|
203
204
|
else delete out[key];
|
|
204
205
|
}
|
|
206
|
+
const provenance = edgeProvenance(value);
|
|
207
|
+
if (provenance !== 'UNKNOWN') out.provenance = provenance;
|
|
205
208
|
const specifier = privacySafeText(value.specifier, 1024);
|
|
206
209
|
if (specifier) out.specifier = specifier;
|
|
207
210
|
else delete out.specifier;
|
|
@@ -265,6 +268,9 @@ export function createSyncPayloadV3(raw, evidence) {
|
|
|
265
268
|
// Reuse the v2 schema gates, but construct v3 arrays independently so the stricter path/identity
|
|
266
269
|
// rules cannot change graph-only compatibility for existing endpoints.
|
|
267
270
|
const base = createSyncPayload(raw);
|
|
271
|
+
if (!Number.isInteger(raw.edgeProvenanceV) || raw.edgeProvenanceV < 1) {
|
|
272
|
+
throw new Error('graph predates edge provenance metadata');
|
|
273
|
+
}
|
|
268
274
|
assertArrayLimit(raw, 'nodes', MAX_SYNC_NODES);
|
|
269
275
|
assertArrayLimit(raw, 'links', MAX_SYNC_LINKS);
|
|
270
276
|
assertArrayLimit(raw, 'externalImports', MAX_SYNC_EXTERNAL_IMPORTS);
|
|
@@ -285,6 +291,7 @@ export function createSyncPayloadV3(raw, evidence) {
|
|
|
285
291
|
syncPayloadV: 3,
|
|
286
292
|
repoBoundaryV: base.repoBoundaryV,
|
|
287
293
|
edgeTypesV: base.edgeTypesV,
|
|
294
|
+
edgeProvenanceV: Number.isInteger(raw.edgeProvenanceV) ? raw.edgeProvenanceV : 0,
|
|
288
295
|
extImportsV: base.extImportsV,
|
|
289
296
|
complexityV: base.complexityV,
|
|
290
297
|
evidenceV: 1,
|
package/src/mcp/tool-result.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// toolResult(), while legacy string-returning tools still receive a deterministic envelope.
|
|
1
|
+
// Text mode stays genuinely compact for agents and older clients: it returns TextContent only.
|
|
2
|
+
// JSON mode adds the stable machine-readable structuredContent envelope and mirrors that envelope
|
|
3
|
+
// into TextContent for workflow runners that cannot consume structured results directly.
|
|
5
4
|
|
|
6
5
|
export const TOOL_RESULT_SCHEMA = 'weavatrix.tool.v1'
|
|
7
6
|
|
|
@@ -44,8 +43,12 @@ export function normalizeToolResult({toolName, value, args, ctx, refresh, warnin
|
|
|
44
43
|
page: rich ? (value.page || {}) : {},
|
|
45
44
|
...(rich && value.completeness ? {completeness: value.completeness} : {}),
|
|
46
45
|
}
|
|
46
|
+
const json = args?.output_format === 'json'
|
|
47
47
|
return {
|
|
48
|
-
text:
|
|
49
|
-
|
|
48
|
+
text: json ? JSON.stringify(structured, null, 2) : `Repository: ${structured.repo.name}\n${text}`,
|
|
49
|
+
// MCP output schemas apply to every invocation of a tool. The catalog intentionally does not
|
|
50
|
+
// advertise one because text mode must not attach a second, potentially very large payload.
|
|
51
|
+
// JSON callers still receive the stable structured result as an optional MCP extension.
|
|
52
|
+
structured: json ? structured : undefined,
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -41,6 +41,7 @@ export async function tRebuildGraph(g, args, ctx) {
|
|
|
41
41
|
const before = g?.nodes ? {
|
|
42
42
|
nodes: g.nodes, links: g.links,
|
|
43
43
|
edgeTypesV: g.edgeTypesV || 0,
|
|
44
|
+
edgeProvenanceV: g.edgeProvenanceV || 0,
|
|
44
45
|
barrelResolutionV: g.barrelResolutionV || 0,
|
|
45
46
|
extractorSchemaV: g.extractorSchemaV || 0,
|
|
46
47
|
} : null
|
|
@@ -73,6 +74,7 @@ export async function tOpenRepo(g, args, ctx) {
|
|
|
73
74
|
try {
|
|
74
75
|
const saved = JSON.parse(readFileSync(graphPath, 'utf8'))
|
|
75
76
|
upgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
|
|
77
|
+
|| !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
|
|
76
78
|
} catch {
|
|
77
79
|
upgrade = true
|
|
78
80
|
}
|
|
@@ -80,7 +82,7 @@ export async function tOpenRepo(g, args, ctx) {
|
|
|
80
82
|
if (!existsSync(graphPath) || upgrade) {
|
|
81
83
|
if (args.build === false) {
|
|
82
84
|
return upgrade
|
|
83
|
-
? `The existing graph for ${repoPath} predates
|
|
85
|
+
? `The existing graph for ${repoPath} predates current typed-edge/provenance metadata. Re-call without build:false to upgrade it before switching.`
|
|
84
86
|
: `No graph yet for ${repoPath} (expected at ${graphPath}). Re-call without build:false to build one — large repos can take minutes.`
|
|
85
87
|
}
|
|
86
88
|
const mode = ['no-tests', 'tests-only', 'full'].includes(args.mode) ? args.mode : 'full'
|
|
@@ -99,7 +101,7 @@ export async function tOpenRepo(g, args, ctx) {
|
|
|
99
101
|
return `Failed to load ${graphPath} — still targeting the previous repo (${prev.repoRoot || 'none'}).`
|
|
100
102
|
}
|
|
101
103
|
registerRepository({repoPath, graphDir: graphOutDirForRepo(repoPath), graphHome: graphHomeDir()})
|
|
102
|
-
const buildNote = built ? (upgrade ? ' (graph upgraded to edge metadata
|
|
104
|
+
const buildNote = built ? (upgrade ? ' (graph upgraded to current edge metadata)' : ' (graph built fresh)') : ''
|
|
103
105
|
return `Opened ${repoPath}${buildNote}: ${loaded.nodes.length} nodes / ${loaded.links.length} edges. All tools now target this repo.`
|
|
104
106
|
}
|
|
105
107
|
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
// Executable target architecture for agents: read the active contract before a change, then run the
|
|
2
2
|
// same deterministic verifier after it. No tool silently edits policy or accepts debt.
|
|
3
3
|
import {contractForChange, loadArchitectureContract, normalizeArchitectureContract, verifyArchitecture} from '../analysis/architecture-contract.js'
|
|
4
|
+
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
4
5
|
import {detectRepoStack} from '../scan/discover.js'
|
|
5
6
|
import {toolResult} from './tool-result.mjs'
|
|
6
7
|
|
|
8
|
+
const PROVISIONAL_BUDGETS = Object.freeze({
|
|
9
|
+
runtimeCycles: 0,
|
|
10
|
+
maxFileLoc: 300,
|
|
11
|
+
maxFunctionLoc: 120,
|
|
12
|
+
maxCyclomatic: 15,
|
|
13
|
+
maxModuleFiles: 80,
|
|
14
|
+
minModuleCohesion: .5,
|
|
15
|
+
maxModuleBoundaryRatio: .65,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const remediation = () => ({
|
|
19
|
+
offlinePath: '.weavatrix/architecture.json',
|
|
20
|
+
hostedAction: 'Open Architecture -> choose intended style -> Save target & baseline',
|
|
21
|
+
nextTool: 'verify_architecture',
|
|
22
|
+
})
|
|
23
|
+
|
|
7
24
|
const stackIds = (repoRoot) => {
|
|
8
25
|
try {
|
|
9
26
|
const stack = detectRepoStack(repoRoot)
|
|
@@ -40,25 +57,62 @@ function starterContract(g) {
|
|
|
40
57
|
return normalizeArchitectureContract({
|
|
41
58
|
name: 'Proposed no-regressions baseline', style: 'custom', enforcement: 'ratchet', components,
|
|
42
59
|
dependencyRules: [],
|
|
43
|
-
budgets:
|
|
60
|
+
budgets: PROVISIONAL_BUDGETS,
|
|
44
61
|
technologies: {required: [], forbidden: []}, exceptions: [], ratchet: {baseline: {fingerprints: [], metrics: {}}},
|
|
45
62
|
})
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
function notConfiguredResult(g, action) {
|
|
49
|
-
const starter = starterContract(g)
|
|
65
|
+
function notConfiguredResult(g, action, {includeStarter = false} = {}) {
|
|
66
|
+
const starter = includeStarter ? starterContract(g) : null
|
|
67
|
+
const starterText = starter
|
|
68
|
+
? ` A source-free starter with ${starter.components.length} path territories is available in JSON output from this lookup.`
|
|
69
|
+
: ''
|
|
70
|
+
return toolResult([
|
|
71
|
+
`Architecture ${action} is NOT_CONFIGURED — no target contract is active.${starterText}`,
|
|
72
|
+
'Next: save .weavatrix/architecture.json (offline) or approve a target in Hosted, then call verify_architecture.',
|
|
73
|
+
].join('\n'), {
|
|
74
|
+
state: 'NOT_CONFIGURED',
|
|
75
|
+
remediation: remediation(),
|
|
76
|
+
...(starter ? {
|
|
77
|
+
starterSummary: {components: starter.components.length, budgets: starter.budgets},
|
|
78
|
+
starterContract: starter,
|
|
79
|
+
} : {}),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function classifyChangeFiles(files, repoRoot) {
|
|
84
|
+
const classifier = createPathClassifier(repoRoot)
|
|
85
|
+
const normalized = [...new Set((Array.isArray(files) ? files : [])
|
|
86
|
+
.slice(0, 200)
|
|
87
|
+
.map((file) => String(file || '').replace(/\\/g, '/').replace(/^\.\//, ''))
|
|
88
|
+
.filter((file) => file && !file.startsWith('../') && !file.includes('/../')))]
|
|
89
|
+
.sort((a, b) => a.localeCompare(b))
|
|
90
|
+
const testOnlyFiles = normalized.filter((file) => hasPathClass(classifier.explain(file), 'test', 'e2e'))
|
|
91
|
+
const testOnly = new Set(testOnlyFiles)
|
|
92
|
+
return {files: normalized, productFiles: normalized.filter((file) => !testOnly.has(file)), testOnlyFiles}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function provisionalPreflight(g, args, ctx) {
|
|
96
|
+
const surfaces = classifyChangeFiles(args?.files, ctx?.repoRoot)
|
|
97
|
+
const intent = String(args?.intent || '').slice(0, 500)
|
|
98
|
+
const budgetText = [
|
|
99
|
+
`no new runtime cycles (baseline ${PROVISIONAL_BUDGETS.runtimeCycles})`,
|
|
100
|
+
`file <= ${PROVISIONAL_BUDGETS.maxFileLoc} LOC`,
|
|
101
|
+
`function <= ${PROVISIONAL_BUDGETS.maxFunctionLoc} LOC`,
|
|
102
|
+
`cyclomatic <= ${PROVISIONAL_BUDGETS.maxCyclomatic}`,
|
|
103
|
+
].join('; ')
|
|
50
104
|
return toolResult([
|
|
51
|
-
`Architecture ${
|
|
52
|
-
`
|
|
53
|
-
|
|
105
|
+
`Architecture preflight is NOT_CONFIGURED for ${surfaces.files.length} file(s)${intent ? ` — ${intent}` : ''}.`,
|
|
106
|
+
`Provisional no-regression guidance (not enforced policy): ${budgetText}.`,
|
|
107
|
+
`${surfaces.productFiles.length} product file(s); ${surfaces.testOnlyFiles.length} test-only file(s). Save a target contract to make these budgets enforceable.`,
|
|
54
108
|
].join('\n'), {
|
|
55
109
|
state: 'NOT_CONFIGURED',
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
110
|
+
guidance: 'PROVISIONAL_BUDGETS',
|
|
111
|
+
enforceable: false,
|
|
112
|
+
intent,
|
|
113
|
+
...surfaces,
|
|
114
|
+
provisionalBudgets: PROVISIONAL_BUDGETS,
|
|
115
|
+
remediation: remediation(),
|
|
62
116
|
})
|
|
63
117
|
}
|
|
64
118
|
|
|
@@ -68,7 +122,7 @@ export function tGetArchitectureContract(g, args, ctx) {
|
|
|
68
122
|
const text = loaded.error
|
|
69
123
|
? `Architecture contract is invalid (${loaded.source || 'unknown'}): ${loaded.error}`
|
|
70
124
|
: 'No target architecture contract is active.'
|
|
71
|
-
if (!loaded.error) return notConfiguredResult(g, 'lookup')
|
|
125
|
+
if (!loaded.error) return notConfiguredResult(g, 'lookup', {includeStarter: true})
|
|
72
126
|
return toolResult(text, {state: 'ERROR', source: loaded.source, error: loaded.error})
|
|
73
127
|
}
|
|
74
128
|
const contract = loaded.contract
|
|
@@ -79,17 +133,19 @@ export function tGetArchitectureContract(g, args, ctx) {
|
|
|
79
133
|
].join('\n'), {state: 'ACTIVE', source: loaded.source, contract})
|
|
80
134
|
}
|
|
81
135
|
|
|
82
|
-
export function tPrepareChange(g, args, ctx) {
|
|
136
|
+
export function tPrepareChange(g, args = {}, ctx) {
|
|
83
137
|
const loaded = activeContract(ctx)
|
|
84
|
-
if (!loaded.contract) return
|
|
138
|
+
if (!loaded.contract) return provisionalPreflight(g, args, ctx)
|
|
85
139
|
const prepared = contractForChange(loaded.contract, args.files)
|
|
140
|
+
const surfaces = classifyChangeFiles(prepared.files, ctx?.repoRoot)
|
|
86
141
|
const intent = String(args.intent || '').slice(0, 500)
|
|
87
142
|
return toolResult([
|
|
88
143
|
`Architecture preflight for ${prepared.files.length} file(s)${intent ? ` — ${intent}` : ''}.`,
|
|
89
144
|
`Affected target components: ${prepared.components.join(', ') || '(unmapped)'}.`,
|
|
145
|
+
...(surfaces.testOnlyFiles.length ? [`Test-only surface: ${surfaces.testOnlyFiles.join(', ')}.`] : []),
|
|
90
146
|
`Applicable rules: ${prepared.rules.map((rule) => rule.id).join(', ') || '(none)'}.`,
|
|
91
147
|
'Run verify_architecture after the edit; do not silently add an exception or rewrite the target contract.',
|
|
92
|
-
].join('\n'), {state: 'READY', intent, contractHash: loaded.contract.contractHash, ...prepared})
|
|
148
|
+
].join('\n'), {state: 'READY', intent, contractHash: loaded.contract.contractHash, ...prepared, productFiles: surfaces.productFiles, testOnlyFiles: surfaces.testOnlyFiles})
|
|
93
149
|
}
|
|
94
150
|
|
|
95
151
|
export function tVerifyArchitecture(g, args, ctx) {
|
|
@@ -9,6 +9,7 @@ import {liveRepositoryRecords} from '../graph/repo-registry.js'
|
|
|
9
9
|
import {loadGraph} from './graph-context.mjs'
|
|
10
10
|
import {toolResult} from './tool-result.mjs'
|
|
11
11
|
|
|
12
|
+
const CROSS_REPO_HTTP_CONTRACT_V = 2
|
|
12
13
|
const selectorText = (value) => String(value ?? '').trim()
|
|
13
14
|
|
|
14
15
|
function selectRecord(records, selector) {
|
|
@@ -120,6 +121,10 @@ function verdictFor(analysis) {
|
|
|
120
121
|
affectedScreens: affectedScreens.size,
|
|
121
122
|
methodMismatches: analysis.totals.methodMismatches,
|
|
122
123
|
uncertainCalls: analysis.totals.uncertainCalls,
|
|
124
|
+
notDeadExternalUse: analysis.totals.notDeadExternalUse,
|
|
125
|
+
notDeadExternalHandlers: analysis.totals.notDeadExternalHandlers,
|
|
126
|
+
possibleExternalUse: analysis.totals.possibleExternalUse,
|
|
127
|
+
unknownLiveness: analysis.totals.unknownLiveness,
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -188,7 +193,7 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
188
193
|
return toolResult(
|
|
189
194
|
`VERDICT PARTIAL — selected repository graphs could not all be refreshed and loaded.\nCompleteness: partial — ${reasons.join('; ')}.`,
|
|
190
195
|
{
|
|
191
|
-
crossRepoHttpContractV:
|
|
196
|
+
crossRepoHttpContractV: CROSS_REPO_HTTP_CONTRACT_V,
|
|
192
197
|
status: 'PARTIAL',
|
|
193
198
|
repositories: {
|
|
194
199
|
backend: publicRecord(backend, backendAlias),
|
|
@@ -209,6 +214,9 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
209
214
|
path: args.path,
|
|
210
215
|
changedFiles: args.changed_files,
|
|
211
216
|
includeTests: args.include_tests === true,
|
|
217
|
+
clientNames: args.client_names,
|
|
218
|
+
wrappers: args.client_wrappers,
|
|
219
|
+
autoDiscoverWrappers: args.auto_discover_wrappers !== false,
|
|
212
220
|
maxImpactDepth: args.max_impact_depth,
|
|
213
221
|
maxEndpoints: args.max_endpoints,
|
|
214
222
|
maxMatches: args.max_matches,
|
|
@@ -236,7 +244,7 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
236
244
|
const screens = endpoint.affected.screens.slice(0, 3)
|
|
237
245
|
.map((screen) => ` screen ${screen.client}:${screen.file} (distance ${screen.distance})`)
|
|
238
246
|
return [
|
|
239
|
-
` ${endpoint.method} ${endpoint.path}${location} → ${endpoint.callsites.length} callsite(s), ${endpoint.affected.screens.length} screen(s), ${endpoint.affected.files.length} affected file(s)`,
|
|
247
|
+
` ${endpoint.method} ${endpoint.path}${location} [${endpoint.liveness.status}${endpoint.handler ? `; handler ${endpoint.handler}` : ''}] → ${endpoint.callsites.length} callsite(s), ${endpoint.affected.screens.length} screen(s), ${endpoint.affected.files.length} affected file(s)`,
|
|
240
248
|
...callsites,
|
|
241
249
|
...screens,
|
|
242
250
|
]
|
|
@@ -246,7 +254,7 @@ export async function tTraceApiContract(_g, args = {}, ctx = {}) {
|
|
|
246
254
|
: `Completeness: partial — ${completeness.reasons.join('; ')}.`,
|
|
247
255
|
]
|
|
248
256
|
const result = {
|
|
249
|
-
crossRepoHttpContractV:
|
|
257
|
+
crossRepoHttpContractV: CROSS_REPO_HTTP_CONTRACT_V,
|
|
250
258
|
verdict,
|
|
251
259
|
repositories: {
|
|
252
260
|
backend: publicRecord(backend, backendAlias),
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
// Connectivity-hub reporting, split from the general graph query tools.
|
|
2
2
|
import {connList} from './graph-context.mjs'
|
|
3
|
+
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
3
4
|
|
|
4
5
|
const isCompileTimeEdge = (edge) => edge?.typeOnly === true || edge?.compileOnly === true
|
|
6
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
|
+
const sourceFileOf = (node) => String(node?.source_file || (node?.file_type === 'code' ? node?.id : '') || '').replace(/\\/g, '/')
|
|
5
8
|
|
|
6
|
-
export function tGodNodes(g, {top_n = 10} = {}) {
|
|
9
|
+
export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx = {}) {
|
|
7
10
|
const n = Math.max(1, Math.min(100, Number(top_n) || 10))
|
|
11
|
+
const classifier = createPathClassifier(ctx.repoRoot || null)
|
|
12
|
+
const classificationByFile = new Map()
|
|
13
|
+
const isNonProduct = (node) => {
|
|
14
|
+
if (include_classified === true) return false
|
|
15
|
+
const file = sourceFileOf(node)
|
|
16
|
+
if (!file) return false
|
|
17
|
+
if (!classificationByFile.has(file)) classificationByFile.set(file, classifier.explain(file))
|
|
18
|
+
const info = classificationByFile.get(file)
|
|
19
|
+
return info.excluded || hasPathClass(info, ...NON_PRODUCT_CLASSES)
|
|
20
|
+
}
|
|
21
|
+
const excludedIds = new Set(g.nodes.filter(isNonProduct).map((node) => String(node.id)))
|
|
8
22
|
const scored = g.nodes
|
|
23
|
+
.filter((node) => !excludedIds.has(String(node.id)))
|
|
9
24
|
.map((node) => {
|
|
10
|
-
|
|
11
|
-
const
|
|
25
|
+
// Coupling from a suppressed artifact must not inflate an otherwise valid product hub.
|
|
26
|
+
const rawOuts = (g.out.get(String(node.id)) || []).filter((edge) => !excludedIds.has(String(edge.id)))
|
|
27
|
+
const rawIns = (g.inn.get(String(node.id)) || []).filter((edge) => !excludedIds.has(String(edge.id)))
|
|
12
28
|
const outs = connList(rawOuts)
|
|
13
29
|
const ins = connList(rawIns)
|
|
14
30
|
const ownedMethods = new Set(rawOuts.filter((e) => e.relation === 'method').map((e) => String(e.id)))
|
|
@@ -54,5 +70,8 @@ export function tGodNodes(g, {top_n = 10} = {}) {
|
|
|
54
70
|
...occurrenceHotspots.map((r) =>
|
|
55
71
|
` ${r.node.label ?? r.node.id} (${r.occurrences} occurrences across ${r.deg} unique neighbors; ${r.occurrences - r.deg} repeats) [${r.node.id}]`
|
|
56
72
|
),
|
|
73
|
+
excludedIds.size > 0 && include_classified !== true
|
|
74
|
+
? `${excludedIds.size} node(s) classified as tests/e2e/generated/build output/mocks/stories/docs/benchmarks/temp or explicitly excluded were omitted; pass include_classified:true to inspect them.`
|
|
75
|
+
: null,
|
|
57
76
|
].filter((line) => line != null).join('\n')
|
|
58
77
|
}
|
package/src/mcp/tools-graph.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveNodeInfo, resolveNode, ambiguityNote, findSeeds, resolveSeedFiles, undirectedNeighbors,
|
|
7
7
|
graphStaleness, fileStalenessNote,
|
|
8
8
|
} from './graph-context.mjs'
|
|
9
|
+
import {summarizeEdgeProvenance} from '../graph/edge-provenance.js'
|
|
9
10
|
|
|
10
11
|
const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
|
|
11
12
|
|
|
@@ -34,15 +35,17 @@ export function tGraphStats(g, ctx) {
|
|
|
34
35
|
.map(([k, v]) => `${k}: ${v}`)
|
|
35
36
|
.join(', ')
|
|
36
37
|
const freshness = ctx ? graphStaleness(ctx) : null
|
|
38
|
+
const provenance = summarizeEdgeProvenance(g.links)
|
|
37
39
|
return [
|
|
38
40
|
`Graph summary`,
|
|
39
41
|
ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
|
|
40
42
|
`- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
|
|
41
43
|
`- Edges: ${g.links.length}`,
|
|
42
44
|
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : `- Typed-edge metadata: unavailable (rebuild_graph required)`,
|
|
45
|
+
g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${fmt(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : `- Edge provenance: unavailable (rebuild_graph required)`,
|
|
43
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)`,
|
|
44
47
|
`- Relations: ${fmt(relCount)}`,
|
|
45
|
-
Object.keys(confCount).length ? `-
|
|
48
|
+
Object.keys(confCount).length ? `- Legacy confidence: ${fmt(confCount)}` : null,
|
|
46
49
|
`- Communities: ${comm.size} (top by size: ${topComm.map(([c, n]) => `#${c}=${n}`).join(', ')})`,
|
|
47
50
|
freshness?.builtAt ? `- Built: ${freshness.builtAt.toISOString()}${freshness.headAt ? ` (repo HEAD committed ${freshness.headAt.toISOString()})` : ''}` : null,
|
|
48
51
|
]
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
// Behavioral architecture evidence from bounded, local git history.
|
|
2
|
-
import {analyzeGitHistory, formatGitHistoryAnalytics} from '../analysis/git-history.js'
|
|
2
|
+
import {analyzeGitHistory, boundGitHistoryAnalytics, formatGitHistoryAnalytics} from '../analysis/git-history.js'
|
|
3
3
|
import {toolResult} from './tool-result.mjs'
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export function gitHistoryToolResult(result, args = {}) {
|
|
6
|
+
const bounded = boundGitHistoryAnalytics(result, {topN: args.top_n})
|
|
7
|
+
return toolResult(formatGitHistoryAnalytics(bounded.result, {topN: args.top_n}), bounded.result, {
|
|
8
|
+
page: bounded.page,
|
|
9
|
+
completeness: {
|
|
10
|
+
status: result.status,
|
|
11
|
+
complete: result.completeness?.complete === true,
|
|
12
|
+
reasons: result.completeness?.reasons || [],
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function tGitHistory(g, args = {}, ctx) {
|
|
6
18
|
if (!ctx?.repoRoot) return toolResult('Git history intelligence is unavailable: no repository root is active.', {status: 'unavailable'})
|
|
7
19
|
const result = await analyzeGitHistory({
|
|
8
20
|
repoRoot: ctx.repoRoot,
|
|
@@ -12,11 +24,5 @@ export async function tGitHistory(g, args, ctx) {
|
|
|
12
24
|
maxPairs: args.max_pairs,
|
|
13
25
|
minPairCount: args.min_pair_count,
|
|
14
26
|
})
|
|
15
|
-
return
|
|
16
|
-
completeness: {
|
|
17
|
-
status: result.status,
|
|
18
|
-
complete: result.completeness?.complete === true,
|
|
19
|
-
reasons: result.completeness?.reasons || [],
|
|
20
|
-
},
|
|
21
|
-
})
|
|
27
|
+
return gitHistoryToolResult(result, args)
|
|
22
28
|
}
|
|
@@ -214,7 +214,7 @@ export function tChangeImpactV2(g, args = {}, ctx = {}) {
|
|
|
214
214
|
'',
|
|
215
215
|
impacted.length
|
|
216
216
|
? `Blast radius: ${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
|
|
217
|
-
: seeds.size ? `Blast radius: nothing else depends on the exact changed symbols within ${maxDepth} hop(s).` : `Blast radius: 0 — additive
|
|
217
|
+
: seeds.size ? `Blast radius: nothing else depends on the exact changed symbols within ${maxDepth} hop(s).` : `Blast radius: 0 — additive, metadata-only, and test-only changes do not inherit the containing file's legacy importers.`,
|
|
218
218
|
hasCoverage
|
|
219
219
|
? 'Test evidence: measured coverage is shown where mapped; static reachability remains separate in structured output.'
|
|
220
220
|
: `Test evidence: actualCoverage NOT_AVAILABLE; static paths only (${staticTests.reachableFiles}/${staticTests.productFiles} product files reachable from indexed tests).`,
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -112,6 +112,7 @@ export async function tGraphDiff(g, args, ctx) {
|
|
|
112
112
|
nodes: (graph.nodes || []).filter((n) => String(n.id).startsWith(filter)),
|
|
113
113
|
links: (graph.links || []).filter((l) => String(edgeEndpoint(l.source)).startsWith(filter) || String(edgeEndpoint(l.target)).startsWith(filter)),
|
|
114
114
|
edgeTypesV: graph.edgeTypesV || 0,
|
|
115
|
+
edgeProvenanceV: graph.edgeProvenanceV || 0,
|
|
115
116
|
barrelResolutionV: graph.barrelResolutionV || 0,
|
|
116
117
|
extractorSchemaV: graph.extractorSchemaV || 0,
|
|
117
118
|
} : graph
|
package/src/mcp-server.mjs
CHANGED
|
@@ -217,7 +217,9 @@ async function main() {
|
|
|
217
217
|
if (schemaWarning) text += `\n\nWarning: ${schemaWarning.message}`
|
|
218
218
|
if (staleLine && staleNotices.shouldShow({line: staleLine, graphPath: callCtx.graphPath, force: tool.name === 'graph_stats'})) text += `\n\n${staleLine}`
|
|
219
219
|
}
|
|
220
|
-
|
|
220
|
+
const response = {content: [{type: 'text', text}]}
|
|
221
|
+
if (normalized.structured) response.structuredContent = normalized.structured
|
|
222
|
+
return reply(id, response)
|
|
221
223
|
} catch (e) {
|
|
222
224
|
log(`tool ${params?.name} threw: ${e.stack || e.message}`)
|
|
223
225
|
return reply(id, {content: [{type: 'text', text: `Tool error: ${e.message}`}], isError: true})
|
|
@@ -53,6 +53,11 @@ const DEFAULT_RULES = [
|
|
|
53
53
|
pattern: "generated/OpenAPI output path",
|
|
54
54
|
regex: /(^|\/)(?:generated|gen|openapi[-_]?generated|generated[-_]?client|api[-_]?client[-_]?generated)(\/|$)/i,
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
category: "generated",
|
|
58
|
+
pattern: "conventional build output path",
|
|
59
|
+
regex: /(^|\/)(?:dist|build|out|coverage|\.next)(\/|$)/i,
|
|
60
|
+
},
|
|
56
61
|
{
|
|
57
62
|
category: "mock",
|
|
58
63
|
pattern: "mock/fixture path or mockData filename",
|