@grafema/mcp 0.3.29 → 0.4.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/dist/definitions/enox-tools.d.ts.map +1 -1
- package/dist/definitions/enox-tools.js +5 -10
- package/dist/definitions/enox-tools.js.map +1 -1
- package/dist/definitions/query-tools.d.ts.map +1 -1
- package/dist/definitions/query-tools.js +127 -0
- package/dist/definitions/query-tools.js.map +1 -1
- package/dist/handlers/coverage-handlers.d.ts.map +1 -1
- package/dist/handlers/coverage-handlers.js +9 -0
- package/dist/handlers/coverage-handlers.js.map +1 -1
- package/dist/handlers/documentation-handlers.d.ts.map +1 -1
- package/dist/handlers/documentation-handlers.js +26 -5
- package/dist/handlers/documentation-handlers.js.map +1 -1
- package/dist/handlers/enox-handlers.d.ts +11 -2
- package/dist/handlers/enox-handlers.d.ts.map +1 -1
- package/dist/handlers/enox-handlers.js +14 -6
- package/dist/handlers/enox-handlers.js.map +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/query-handlers.d.ts +28 -1
- package/dist/handlers/query-handlers.d.ts.map +1 -1
- package/dist/handlers/query-handlers.js +156 -14
- package/dist/handlers/query-handlers.js.map +1 -1
- package/dist/server.js +10 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/definitions/enox-tools.ts +5 -10
- package/src/definitions/query-tools.ts +127 -0
- package/src/handlers/coverage-handlers.ts +10 -0
- package/src/handlers/documentation-handlers.ts +26 -5
- package/src/handlers/enox-handlers.ts +18 -7
- package/src/handlers/index.ts +1 -1
- package/src/handlers/query-handlers.ts +181 -13
- package/src/server.ts +18 -0
- package/src/types.ts +29 -0
- package/dist/definitions.d.ts +0 -23
- package/dist/definitions.d.ts.map +0 -1
- package/dist/definitions.js +0 -644
- package/dist/definitions.js.map +0 -1
- package/dist/handlers.d.ts +0 -61
- package/dist/handlers.d.ts.map +0 -1
- package/dist/handlers.js +0 -1310
- package/dist/handlers.js.map +0 -1
|
@@ -353,6 +353,133 @@ The question parameter guides what graph data to fetch and how to frame the summ
|
|
|
353
353
|
required: ['target'],
|
|
354
354
|
},
|
|
355
355
|
},
|
|
356
|
+
{
|
|
357
|
+
name: 'explain_fact',
|
|
358
|
+
description: `Explain WHY a derived (Datalog) fact holds — returns the rule that derived it plus the supporting body facts (why()/provenance).
|
|
359
|
+
|
|
360
|
+
This is the inverse of "what holds": instead of listing results, it justifies ONE
|
|
361
|
+
result. Provenance is computed on demand against the current graph snapshot.
|
|
362
|
+
|
|
363
|
+
Default program is the bundled depends.dl, so the common use is explaining a
|
|
364
|
+
MODULE→MODULE dependency edge:
|
|
365
|
+
- "Why does module A depend on B?" → explain_fact(predicate="depends", key=["<A_id>", "<B_id>"])
|
|
366
|
+
|
|
367
|
+
For a custom rule, pass its source. \`key\` is the fact's ground tuple as wire-string
|
|
368
|
+
terms (node ids as their decimal id). A null/"no derivation" result means the fact
|
|
369
|
+
is not derivable by the program (it does not hold as a derived fact).`,
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
predicate: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
description: 'The derived predicate to explain (e.g. "depends").',
|
|
376
|
+
},
|
|
377
|
+
key: {
|
|
378
|
+
type: 'array',
|
|
379
|
+
items: { type: 'string' },
|
|
380
|
+
description: 'The fact\'s ground key tuple as wire-string terms (node ids as decimal).',
|
|
381
|
+
},
|
|
382
|
+
source: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
description: 'Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl.',
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
required: ['predicate', 'key'],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'sim_datalog',
|
|
392
|
+
description: `Predict which NEW derived facts a hypothetical change would create — WITHOUT committing anything (what-if simulation).
|
|
393
|
+
|
|
394
|
+
Give it hypothetical nodes and/or edges; it evaluates the program over base ∪ overlay
|
|
395
|
+
and returns only the facts that are NEW vs the current graph (sim ∖ base).
|
|
396
|
+
|
|
397
|
+
Default program is the bundled depends.dl, so the common use is previewing module
|
|
398
|
+
dependencies:
|
|
399
|
+
- "If module A imported B, which NEW dependencies appear?" →
|
|
400
|
+
sim_datalog(predicate="depends", edges=[{src:"<A_id>", dst:"<B_id>", edgeType:"IMPORTS_FROM"}])
|
|
401
|
+
|
|
402
|
+
Hypothetical node ids may be NEW (invent a decimal id) — an edge may reference them,
|
|
403
|
+
so you can simulate a wholly new module/import, not only bridge existing nodes.
|
|
404
|
+
The committed graph is never touched. Companion to explain_gap: gap names the missing
|
|
405
|
+
premise, sim verifies that adding it produces the fact.`,
|
|
406
|
+
inputSchema: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
properties: {
|
|
409
|
+
predicate: {
|
|
410
|
+
type: 'string',
|
|
411
|
+
description: 'The derived predicate whose NEW facts to predict (e.g. "depends").',
|
|
412
|
+
},
|
|
413
|
+
nodes: {
|
|
414
|
+
type: 'array',
|
|
415
|
+
items: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
id: { type: 'string', description: 'Decimal node id (may be a new, invented id).' },
|
|
419
|
+
nodeType: { type: 'string', description: 'Node type (e.g. "MODULE").' },
|
|
420
|
+
name: { type: 'string', description: 'Node name attr.' },
|
|
421
|
+
file: { type: 'string', description: 'Node file attr.' },
|
|
422
|
+
},
|
|
423
|
+
required: ['id', 'nodeType'],
|
|
424
|
+
},
|
|
425
|
+
description: 'Hypothetical nodes to overlay.',
|
|
426
|
+
},
|
|
427
|
+
edges: {
|
|
428
|
+
type: 'array',
|
|
429
|
+
items: {
|
|
430
|
+
type: 'object',
|
|
431
|
+
properties: {
|
|
432
|
+
src: { type: 'string', description: 'Source node id (existing or hypothetical).' },
|
|
433
|
+
dst: { type: 'string', description: 'Target node id (existing or hypothetical).' },
|
|
434
|
+
edgeType: { type: 'string', description: 'Edge type (e.g. "IMPORTS_FROM").' },
|
|
435
|
+
},
|
|
436
|
+
required: ['src', 'dst', 'edgeType'],
|
|
437
|
+
},
|
|
438
|
+
description: 'Hypothetical edges to overlay.',
|
|
439
|
+
},
|
|
440
|
+
source: {
|
|
441
|
+
type: 'string',
|
|
442
|
+
description: 'Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl.',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ['predicate'],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'explain_gap',
|
|
450
|
+
description: `Explain why a derived (Datalog) fact does NOT hold — the why-not dual of explain_fact.
|
|
451
|
+
|
|
452
|
+
Returns the rule whose gap it characterizes, the body premises that WERE satisfiable
|
|
453
|
+
(with the head bound), and the first premise no binding satisfies:
|
|
454
|
+
- a MISSING positive premise → the gap closes by ADDING such a fact (verify with sim_datalog)
|
|
455
|
+
- a PRESENT negated premise → the gap closes by REMOVING the blocking fact
|
|
456
|
+
|
|
457
|
+
Default program is the bundled depends.dl, so the common use is explaining a missing
|
|
458
|
+
MODULE→MODULE dependency:
|
|
459
|
+
- "Why does module A NOT depend on B?" → explain_gap(predicate="depends", key=["<A_id>", "<B_id>"])
|
|
460
|
+
|
|
461
|
+
A "no gap" result means the fact actually IS derivable (use explain_fact), or no rule
|
|
462
|
+
head matches the key.`,
|
|
463
|
+
inputSchema: {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
predicate: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'The derived predicate of the missing fact (e.g. "depends").',
|
|
469
|
+
},
|
|
470
|
+
key: {
|
|
471
|
+
type: 'array',
|
|
472
|
+
items: { type: 'string' },
|
|
473
|
+
description: 'The missing fact\'s ground key tuple as wire-string terms (node ids as decimal).',
|
|
474
|
+
},
|
|
475
|
+
source: {
|
|
476
|
+
type: 'string',
|
|
477
|
+
description: 'Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl.',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
required: ['predicate', 'key'],
|
|
481
|
+
},
|
|
482
|
+
},
|
|
356
483
|
{
|
|
357
484
|
name: 'check_invariant',
|
|
358
485
|
description: `Check a one-off code invariant using a Datalog rule. Returns violations if broken.
|
|
@@ -31,9 +31,19 @@ export async function handleGetCoverage(args: GetCoverageArgs): Promise<ToolResu
|
|
|
31
31
|
output += `File breakdown:\n`;
|
|
32
32
|
output += ` Total files: ${result.total}\n`;
|
|
33
33
|
output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
|
|
34
|
+
if (result.failed.count > 0) {
|
|
35
|
+
output += ` Failed: ${result.failed.count} (${result.percentages.failed}%) - skipped/failed during analysis\n`;
|
|
36
|
+
}
|
|
34
37
|
output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
|
|
35
38
|
output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
|
|
36
39
|
|
|
40
|
+
if (result.failed.count > 0) {
|
|
41
|
+
output += `\nFailed files by reason:\n`;
|
|
42
|
+
for (const [category, files] of Object.entries(result.failed.byCategory)) {
|
|
43
|
+
output += ` ${category}: ${files.length} files\n`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
if (result.unsupported.count > 0) {
|
|
38
48
|
output += `\nUnsupported files by extension:\n`;
|
|
39
49
|
for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
|
|
@@ -40,11 +40,21 @@ Grafema is a static code analyzer that builds a graph of your codebase.
|
|
|
40
40
|
## Syntax
|
|
41
41
|
violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
|
|
42
42
|
|
|
43
|
-
##
|
|
44
|
-
-
|
|
45
|
-
- edge(Src, Dst, Type) - match edges
|
|
46
|
-
-
|
|
47
|
-
-
|
|
43
|
+
## Node / Edge / Attribute Predicates
|
|
44
|
+
- node(Id, Type) - match nodes by type (alias: type(Id, Type))
|
|
45
|
+
- edge(Src, Dst, Type) - match outgoing edges Src -> Dst of the given type
|
|
46
|
+
- incoming(Dst, Src, Type) - match edges pointing TO Dst (reverse of edge)
|
|
47
|
+
- path(Src, Dst) - transitive reachability Src -> Dst (BFS over edges)
|
|
48
|
+
- attr(Id, Name, Value) - match node attributes (name, file, line, ...; nested paths like "a.b" supported)
|
|
49
|
+
- attr_edge(Src, Dst, EdgeType, AttrName, Value) - match EDGE attributes; Src/Dst/EdgeType/AttrName must be bound, Value may be variable/constant/wildcard
|
|
50
|
+
- parent_function(NodeId, FunctionId) - bind the enclosing FUNCTION of NodeId via CONTAINS; NodeId must be bound; empty at module level
|
|
51
|
+
|
|
52
|
+
## Negation & String Predicates
|
|
53
|
+
- \\+ - negation (not); also for negative joins on a dst-position variable, e.g. \\+ edge(X, _, "CALLS")
|
|
54
|
+
- neq(X, Y) - inequality; BOTH arguments must be bound
|
|
55
|
+
- starts_with(Value, Prefix) - string prefix match
|
|
56
|
+
- not_starts_with(Value, Prefix) - negative string prefix match
|
|
57
|
+
- string_contains(Value, Substring) - substring match
|
|
48
58
|
|
|
49
59
|
## Numeric Comparison Predicates
|
|
50
60
|
- gt(Value, Threshold) - greater than
|
|
@@ -55,6 +65,11 @@ violation(X) :- node(X, "TYPE"), attr(X, "name", "value").
|
|
|
55
65
|
Values are parsed as floating-point numbers. Non-numeric values produce no matches.
|
|
56
66
|
Use with attr() to filter by metadata values (e.g., metrics, line numbers).
|
|
57
67
|
|
|
68
|
+
## Limitations
|
|
69
|
+
- No aggregations (count/sum/avg), no GROUP BY, no ORDER BY - aggregate/sort client-side.
|
|
70
|
+
- Predicate ORDER matters: bind a variable (via node/edge/attr) before a comparison /
|
|
71
|
+
string / attr_edge predicate uses it, or the query fails to place ("circular dependency").
|
|
72
|
+
|
|
58
73
|
## Examples
|
|
59
74
|
Find all functions:
|
|
60
75
|
violation(X) :- node(X, "FUNCTION").
|
|
@@ -67,6 +82,12 @@ Find files where parsing took > 500ms:
|
|
|
67
82
|
|
|
68
83
|
Find functions with more than 100 lines:
|
|
69
84
|
violation(X, Lines) :- node(X, "FUNCTION"), attr(X, "line", Start), attr(X, "endLine", End), gt(End, Start).
|
|
85
|
+
|
|
86
|
+
Find the enclosing function of every call:
|
|
87
|
+
violation(C, F) :- node(C, "CALL"), parent_function(C, F).
|
|
88
|
+
|
|
89
|
+
Find nodes that have incoming CALLS edges (i.e. are called):
|
|
90
|
+
violation(D) :- incoming(D, _, "CALLS").
|
|
70
91
|
`,
|
|
71
92
|
types: `
|
|
72
93
|
# Node & Edge Types
|
|
@@ -348,14 +348,25 @@ export async function handleRecall(args: RecallArgs): Promise<ToolResult> {
|
|
|
348
348
|
|
|
349
349
|
export interface SemanticSearchArgs {
|
|
350
350
|
query: string;
|
|
351
|
-
|
|
351
|
+
top_k?: number;
|
|
352
352
|
domain?: string;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Handle the `semantic_search` MCP tool.
|
|
357
|
+
*
|
|
358
|
+
* @param args - tool input. `top_k` (matching the published schema) caps the
|
|
359
|
+
* number of results; defaults to 10 per the schema's documented default.
|
|
360
|
+
* @param clientOverride - optional RFDB client, injected by tests; production
|
|
361
|
+
* callers omit it and the shared knowledge client is used.
|
|
362
|
+
*/
|
|
363
|
+
export async function handleSemanticSearch(
|
|
364
|
+
args: SemanticSearchArgs,
|
|
365
|
+
clientOverride?: RFDBClient,
|
|
366
|
+
): Promise<ToolResult> {
|
|
356
367
|
try {
|
|
357
|
-
const client = await getKnowledgeClient();
|
|
358
|
-
const limit = args.
|
|
368
|
+
const client = clientOverride ?? await getKnowledgeClient();
|
|
369
|
+
const limit = args.top_k ?? 10;
|
|
359
370
|
|
|
360
371
|
// TODO: Wire to RFDB embedding engine when enabled.
|
|
361
372
|
// For now, fall back to substring search via queryNodes.
|
|
@@ -378,9 +389,9 @@ export async function handleSemanticSearch(args: SemanticSearchArgs): Promise<To
|
|
|
378
389
|
for (let i = 0; i < results.length; i++) {
|
|
379
390
|
const node = results[i];
|
|
380
391
|
const meta = parseMeta(node);
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
lines.push(`${i + 1}.
|
|
392
|
+
// NOTE: this is substring matching, not embeddings — do not fabricate a
|
|
393
|
+
// similarity score (it would read to an agent as a real metric). Just rank.
|
|
394
|
+
lines.push(`${i + 1}. ${node.name} (${node.nodeType})`);
|
|
384
395
|
if (meta.domain) lines.push(` Domain: ${meta.domain}`);
|
|
385
396
|
if (meta.content) lines.push(` ${String(meta.content).slice(0, 200)}`);
|
|
386
397
|
lines.push('');
|
package/src/handlers/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP Tool Handlers — barrel export
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { handleQueryGraph, handleFindCalls, handleFindNodes } from './query-handlers.js';
|
|
5
|
+
export { handleQueryGraph, handleFindCalls, handleFindNodes, handleExplainFact, handleExplainGap, handleSimDatalog } from './query-handlers.js';
|
|
6
6
|
export { handleTraceAlias, handleTraceDataflow, handleTraceCalls, handleCheckInvariant, handleExplain, handleTraceEffects } from './dataflow-handlers.js';
|
|
7
7
|
export type { ExplainArgs } from './dataflow-handlers.js';
|
|
8
8
|
export { handleDiscoverServices, handleAnalyzeProject, handleGetAnalysisStatus, handleGetStats, handleGetSchema } from './analysis-handlers.js';
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
textResult,
|
|
14
14
|
errorResult,
|
|
15
15
|
} from '../utils.js';
|
|
16
|
-
import type { DatalogExplainResult, CypherResult } from '@grafema/types';
|
|
16
|
+
import type { DatalogExplainResult, CypherResult, FactWitness, GapWitness, SimEdge, SimNode } from '@grafema/types';
|
|
17
17
|
import type {
|
|
18
18
|
ToolResult,
|
|
19
19
|
QueryGraphArgs,
|
|
@@ -22,6 +22,9 @@ import type {
|
|
|
22
22
|
GraphNode,
|
|
23
23
|
DatalogBinding,
|
|
24
24
|
CallResult,
|
|
25
|
+
ExplainFactArgs,
|
|
26
|
+
ExplainGapArgs,
|
|
27
|
+
SimDatalogArgs,
|
|
25
28
|
} from '../types.js';
|
|
26
29
|
|
|
27
30
|
// === QUERY HANDLERS ===
|
|
@@ -524,22 +527,35 @@ async function enrichNodes(
|
|
|
524
527
|
|
|
525
528
|
/** Convert a semantic ID to human-readable "name in file.ts" format.
|
|
526
529
|
* Handles: grafema://host/path/file.ts#TYPE->name[scope], path/file.ts#TYPE->name,
|
|
527
|
-
* TYPE-%3Ename (URL-encoded, no file prefix), or raw node IDs.
|
|
528
|
-
|
|
530
|
+
* TYPE-%3Ename (URL-encoded, no file prefix), or raw node IDs.
|
|
531
|
+
*
|
|
532
|
+
* Exported for unit testing.
|
|
533
|
+
* @internal */
|
|
534
|
+
export function humanReadableId(semanticId: string): string {
|
|
529
535
|
if (!semanticId) return '?';
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
|
|
537
|
+
// Split file path from node descriptor on the FIRST literal '#' of the RAW
|
|
538
|
+
// (still percent-encoded) id, BEFORE decoding. The grafema:// URI form encodes
|
|
539
|
+
// the disambiguation counter ('#N', appended by computeSemanticIdV2 for hash
|
|
540
|
+
// collisions) as '%23N' inside the fragment. Decoding first and then splitting
|
|
541
|
+
// on the LAST '#' would mistake that counter for the path/fragment boundary and
|
|
542
|
+
// corrupt the label. A file path never contains a literal '#'. This mirrors the
|
|
543
|
+
// canonical parseSemanticIdV2 (packages/util/src/core/SemanticId.ts).
|
|
544
|
+
const hashIdx = semanticId.indexOf('#');
|
|
545
|
+
let rawFile = '';
|
|
546
|
+
let rawNode = semanticId;
|
|
538
547
|
if (hashIdx !== -1) {
|
|
539
|
-
|
|
540
|
-
|
|
548
|
+
rawFile = semanticId.slice(0, hashIdx);
|
|
549
|
+
rawNode = semanticId.slice(hashIdx + 1);
|
|
541
550
|
}
|
|
542
551
|
|
|
552
|
+
const decode = (s: string): string => {
|
|
553
|
+
try { return decodeURIComponent(s); } catch { return s; }
|
|
554
|
+
};
|
|
555
|
+
const filePart = decode(rawFile);
|
|
556
|
+
// Strip the trailing disambiguation counter ('#N') from the decoded descriptor.
|
|
557
|
+
const nodePart = decode(rawNode).replace(/#\d+$/, '');
|
|
558
|
+
|
|
543
559
|
const fileName = filePart ? (filePart.split('/').pop() || '') : '';
|
|
544
560
|
|
|
545
561
|
// Extract name from TYPE->name or TYPE->name[in:scope,h:hash]
|
|
@@ -619,3 +635,155 @@ async function grepAndEnrich(
|
|
|
619
635
|
return [];
|
|
620
636
|
}
|
|
621
637
|
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* explain_fact (why(), spec §11): explain WHY a derived fact holds — return the rule that derived
|
|
641
|
+
* it and the supporting body facts. Empty `source` ⇒ the bundled depends.dl (so `explain_fact`
|
|
642
|
+
* with predicate "depends" explains a DEPENDS_ON edge). v2-only.
|
|
643
|
+
*/
|
|
644
|
+
export async function handleExplainFact(args: ExplainFactArgs): Promise<ToolResult> {
|
|
645
|
+
const { predicate, key, source } = args;
|
|
646
|
+
if (!predicate || !Array.isArray(key)) {
|
|
647
|
+
return errorResult('explain_fact requires `predicate` (string) and `key` (array of wire-string terms).');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const db = await ensureAnalyzed();
|
|
651
|
+
if (!('explainDatalogFact' in db)) {
|
|
652
|
+
return errorResult('Backend does not support explain_fact (needs an rfdb-server with the derive engine enabled, i.e. RFDB_DERIVE_ENGINE not set to off).');
|
|
653
|
+
}
|
|
654
|
+
const fn = (db as unknown as {
|
|
655
|
+
explainDatalogFact: (s: string, p: string, k: string[]) => Promise<FactWitness | null>;
|
|
656
|
+
}).explainDatalogFact;
|
|
657
|
+
|
|
658
|
+
let witness: FactWitness | null;
|
|
659
|
+
try {
|
|
660
|
+
witness = await fn.call(db, source ?? '', predicate, key.map(String));
|
|
661
|
+
} catch (e) {
|
|
662
|
+
return errorResult(`explain_fact failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const factStr = `${predicate}(${key.join(', ')})`;
|
|
666
|
+
if (!witness) {
|
|
667
|
+
return textResult(
|
|
668
|
+
`No derivation: ${factStr} is not derivable by the program (it does not hold as a derived fact).`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const lines = [`${factStr} holds — derived by rule ${witness.ruleAstHash}:`];
|
|
673
|
+
if (witness.body.length === 0) {
|
|
674
|
+
lines.push(' (the rule body has no positive facts — supported by builtins/constants alone)');
|
|
675
|
+
} else {
|
|
676
|
+
for (const f of witness.body) {
|
|
677
|
+
lines.push(` • ${f.predicate}(${f.tuple.join(', ')})`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return textResult(guardResponseSize(lines.join('\n')));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* sim_datalog (what-if, spec §6): predict the NEW `predicate` facts a hypothetical overlay of
|
|
685
|
+
* nodes+edges would create — WITHOUT committing anything. Empty `source` ⇒ the bundled
|
|
686
|
+
* depends.dl (so the common use is "which NEW module dependencies would this import create?").
|
|
687
|
+
* v2-only. The companion to explain_gap: gap names the missing premise, sim verifies that
|
|
688
|
+
* adding it produces the fact.
|
|
689
|
+
*/
|
|
690
|
+
export async function handleSimDatalog(args: SimDatalogArgs): Promise<ToolResult> {
|
|
691
|
+
const { predicate, nodes, edges, source } = args;
|
|
692
|
+
if (!predicate) {
|
|
693
|
+
return errorResult('sim_datalog requires `predicate` (string).');
|
|
694
|
+
}
|
|
695
|
+
const hypNodes: SimNode[] = (nodes ?? []).map((n) => ({
|
|
696
|
+
id: String(n.id),
|
|
697
|
+
nodeType: n.nodeType,
|
|
698
|
+
name: n.name ?? '',
|
|
699
|
+
file: n.file ?? '',
|
|
700
|
+
}));
|
|
701
|
+
const hypEdges: SimEdge[] = (edges ?? []).map((e) => ({
|
|
702
|
+
src: String(e.src),
|
|
703
|
+
dst: String(e.dst),
|
|
704
|
+
edgeType: e.edgeType,
|
|
705
|
+
}));
|
|
706
|
+
if (hypNodes.length === 0 && hypEdges.length === 0) {
|
|
707
|
+
return errorResult('sim_datalog requires at least one hypothetical node or edge.');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const db = await ensureAnalyzed();
|
|
711
|
+
if (!('simDatalog' in db)) {
|
|
712
|
+
return errorResult('Backend does not support sim_datalog (needs an rfdb-server with the derive engine enabled, i.e. RFDB_DERIVE_ENGINE not set to off).');
|
|
713
|
+
}
|
|
714
|
+
const fn = (db as unknown as {
|
|
715
|
+
simDatalog: (s: string, p: string, n: SimNode[], e: SimEdge[]) => Promise<string[][]>;
|
|
716
|
+
}).simDatalog;
|
|
717
|
+
|
|
718
|
+
let rows: string[][];
|
|
719
|
+
try {
|
|
720
|
+
rows = await fn.call(db, source ?? '', predicate, hypNodes, hypEdges);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
return errorResult(`sim_datalog failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (rows.length === 0) {
|
|
726
|
+
return textResult(
|
|
727
|
+
`No new facts: the hypothetical change creates no NEW ${predicate}() facts (anything it derives already holds).`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const lines = [`${rows.length} NEW ${predicate}() fact(s) the hypothetical change would create:`];
|
|
731
|
+
for (const row of rows) {
|
|
732
|
+
lines.push(` • ${predicate}(${row.join(', ')})`);
|
|
733
|
+
}
|
|
734
|
+
return textResult(guardResponseSize(lines.join('\n')));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* explain_gap (why-not, spec §6): explain why a fact is NOT derived — the satisfied premise
|
|
739
|
+
* prefix and the first premise no binding satisfies. Empty `source` ⇒ the bundled depends.dl
|
|
740
|
+
* (so the common use is "why is there NO dependency A→B?"). v2-only.
|
|
741
|
+
*/
|
|
742
|
+
export async function handleExplainGap(args: ExplainGapArgs): Promise<ToolResult> {
|
|
743
|
+
const { predicate, key, source } = args;
|
|
744
|
+
if (!predicate || !Array.isArray(key)) {
|
|
745
|
+
return errorResult('explain_gap requires `predicate` (string) and `key` (array of wire-string terms).');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const db = await ensureAnalyzed();
|
|
749
|
+
if (!('explainDatalogGap' in db)) {
|
|
750
|
+
return errorResult('Backend does not support explain_gap (needs an rfdb-server with the derive engine enabled, i.e. RFDB_DERIVE_ENGINE not set to off).');
|
|
751
|
+
}
|
|
752
|
+
const fn = (db as unknown as {
|
|
753
|
+
explainDatalogGap: (s: string, p: string, k: string[]) => Promise<GapWitness | null>;
|
|
754
|
+
}).explainDatalogGap;
|
|
755
|
+
|
|
756
|
+
let gap: GapWitness | null;
|
|
757
|
+
try {
|
|
758
|
+
gap = await fn.call(db, source ?? '', predicate, key.map(String));
|
|
759
|
+
} catch (e) {
|
|
760
|
+
return errorResult(`explain_gap failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const factStr = `${predicate}(${key.join(', ')})`;
|
|
764
|
+
if (!gap) {
|
|
765
|
+
return textResult(
|
|
766
|
+
`No gap: ${factStr} is derivable (it holds — use explain_fact for its derivation), or no rule head matches this key.`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const lines = [`${factStr} does NOT hold — gap in rule ${gap.ruleAstHash}:`];
|
|
771
|
+
if (gap.satisfied.length === 0) {
|
|
772
|
+
lines.push(' satisfied premises: (none)');
|
|
773
|
+
} else {
|
|
774
|
+
lines.push(' satisfied premises:');
|
|
775
|
+
for (const f of gap.satisfied) {
|
|
776
|
+
lines.push(` • ${f.predicate}(${f.tuple.join(', ')})`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (gap.failingIsNegative) {
|
|
780
|
+
lines.push(
|
|
781
|
+
` blocking premise: NOT ${gap.failingPredicate}(…) — a PRESENT fact blocks the derivation; the gap closes by REMOVING it.`,
|
|
782
|
+
);
|
|
783
|
+
} else {
|
|
784
|
+
lines.push(
|
|
785
|
+
` missing premise: ${gap.failingPredicate}(…) — no binding satisfies it; the gap closes by ADDING such a fact (verify with sim_datalog).`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
return textResult(guardResponseSize(lines.join('\n')));
|
|
789
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -39,6 +39,9 @@ import { errorResult, log } from './utils.js';
|
|
|
39
39
|
import { getSocketPathOverride } from './state.js';
|
|
40
40
|
import {
|
|
41
41
|
handleQueryGraph,
|
|
42
|
+
handleExplainFact,
|
|
43
|
+
handleExplainGap,
|
|
44
|
+
handleSimDatalog,
|
|
42
45
|
handleFindCalls,
|
|
43
46
|
handleFindNodes,
|
|
44
47
|
handleTraceAlias,
|
|
@@ -101,6 +104,9 @@ import type {
|
|
|
101
104
|
GetFunctionDetailsArgs,
|
|
102
105
|
GetContextArgs,
|
|
103
106
|
QueryGraphArgs,
|
|
107
|
+
ExplainFactArgs,
|
|
108
|
+
ExplainGapArgs,
|
|
109
|
+
SimDatalogArgs,
|
|
104
110
|
FindCallsArgs,
|
|
105
111
|
FindNodesArgs,
|
|
106
112
|
TraceAliasArgs,
|
|
@@ -282,6 +288,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
282
288
|
result = await handleExplain(asArgs<ExplainArgs>(args));
|
|
283
289
|
break;
|
|
284
290
|
|
|
291
|
+
case 'explain_fact':
|
|
292
|
+
result = await handleExplainFact(asArgs<ExplainFactArgs>(args));
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case 'sim_datalog':
|
|
296
|
+
result = await handleSimDatalog(asArgs<SimDatalogArgs>(args));
|
|
297
|
+
break;
|
|
298
|
+
|
|
299
|
+
case 'explain_gap':
|
|
300
|
+
result = await handleExplainGap(asArgs<ExplainGapArgs>(args));
|
|
301
|
+
break;
|
|
302
|
+
|
|
285
303
|
case 'trace_effects':
|
|
286
304
|
result = await handleTraceEffects(asArgs<TraceEffectsArgs>(args));
|
|
287
305
|
break;
|
package/src/types.ts
CHANGED
|
@@ -51,6 +51,35 @@ export interface QueryGraphArgs {
|
|
|
51
51
|
count?: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export interface ExplainFactArgs {
|
|
55
|
+
/** The derived predicate (e.g. "depends"). */
|
|
56
|
+
predicate: string;
|
|
57
|
+
/** The fact's ground key tuple as wire-string terms (node ids as decimal, else string). */
|
|
58
|
+
key: string[];
|
|
59
|
+
/** Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl. */
|
|
60
|
+
source?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SimDatalogArgs {
|
|
64
|
+
/** The derived predicate whose NEW facts to predict (e.g. "depends"). */
|
|
65
|
+
predicate: string;
|
|
66
|
+
/** Hypothetical nodes: { id (decimal string), nodeType, name?, file? }. May be new ids. */
|
|
67
|
+
nodes?: Array<{ id: string; nodeType: string; name?: string; file?: string }>;
|
|
68
|
+
/** Hypothetical edges: { src, dst, edgeType }; endpoints existing OR hypothetical ids. */
|
|
69
|
+
edges?: Array<{ src: string; dst: string; edgeType: string }>;
|
|
70
|
+
/** Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl. */
|
|
71
|
+
source?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ExplainGapArgs {
|
|
75
|
+
/** The derived predicate of the MISSING fact (e.g. "depends"). */
|
|
76
|
+
predicate: string;
|
|
77
|
+
/** The missing fact's ground key tuple as wire-string terms (node ids as decimal). */
|
|
78
|
+
key: string[];
|
|
79
|
+
/** Optional Datalog program (derive engine); empty/omitted ⇒ the bundled depends.dl. */
|
|
80
|
+
source?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
export interface FindCallsArgs {
|
|
55
84
|
name: string;
|
|
56
85
|
limit?: number;
|
package/dist/definitions.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Tool Definitions
|
|
3
|
-
*/
|
|
4
|
-
interface SchemaProperty {
|
|
5
|
-
type: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
enum?: string[];
|
|
8
|
-
items?: SchemaProperty;
|
|
9
|
-
properties?: Record<string, SchemaProperty>;
|
|
10
|
-
required?: string[];
|
|
11
|
-
}
|
|
12
|
-
export interface ToolDefinition {
|
|
13
|
-
name: string;
|
|
14
|
-
description: string;
|
|
15
|
-
inputSchema: {
|
|
16
|
-
type: 'object';
|
|
17
|
-
properties: Record<string, SchemaProperty>;
|
|
18
|
-
required?: string[];
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export declare const TOOLS: ToolDefinition[];
|
|
22
|
-
export {};
|
|
23
|
-
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC5C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;QAC3C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,cAAc,EA+nBjC,CAAC"}
|