@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.
Files changed (46) hide show
  1. package/dist/definitions/enox-tools.d.ts.map +1 -1
  2. package/dist/definitions/enox-tools.js +5 -10
  3. package/dist/definitions/enox-tools.js.map +1 -1
  4. package/dist/definitions/query-tools.d.ts.map +1 -1
  5. package/dist/definitions/query-tools.js +127 -0
  6. package/dist/definitions/query-tools.js.map +1 -1
  7. package/dist/handlers/coverage-handlers.d.ts.map +1 -1
  8. package/dist/handlers/coverage-handlers.js +9 -0
  9. package/dist/handlers/coverage-handlers.js.map +1 -1
  10. package/dist/handlers/documentation-handlers.d.ts.map +1 -1
  11. package/dist/handlers/documentation-handlers.js +26 -5
  12. package/dist/handlers/documentation-handlers.js.map +1 -1
  13. package/dist/handlers/enox-handlers.d.ts +11 -2
  14. package/dist/handlers/enox-handlers.d.ts.map +1 -1
  15. package/dist/handlers/enox-handlers.js +14 -6
  16. package/dist/handlers/enox-handlers.js.map +1 -1
  17. package/dist/handlers/index.d.ts +1 -1
  18. package/dist/handlers/index.d.ts.map +1 -1
  19. package/dist/handlers/index.js +1 -1
  20. package/dist/handlers/index.js.map +1 -1
  21. package/dist/handlers/query-handlers.d.ts +28 -1
  22. package/dist/handlers/query-handlers.d.ts.map +1 -1
  23. package/dist/handlers/query-handlers.js +156 -14
  24. package/dist/handlers/query-handlers.js.map +1 -1
  25. package/dist/server.js +10 -1
  26. package/dist/server.js.map +1 -1
  27. package/dist/types.d.ts +35 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/package.json +8 -8
  30. package/src/definitions/enox-tools.ts +5 -10
  31. package/src/definitions/query-tools.ts +127 -0
  32. package/src/handlers/coverage-handlers.ts +10 -0
  33. package/src/handlers/documentation-handlers.ts +26 -5
  34. package/src/handlers/enox-handlers.ts +18 -7
  35. package/src/handlers/index.ts +1 -1
  36. package/src/handlers/query-handlers.ts +181 -13
  37. package/src/server.ts +18 -0
  38. package/src/types.ts +29 -0
  39. package/dist/definitions.d.ts +0 -23
  40. package/dist/definitions.d.ts.map +0 -1
  41. package/dist/definitions.js +0 -644
  42. package/dist/definitions.js.map +0 -1
  43. package/dist/handlers.d.ts +0 -61
  44. package/dist/handlers.d.ts.map +0 -1
  45. package/dist/handlers.js +0 -1310
  46. 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
- ## Available Predicates
44
- - type(Id, Type) - match nodes (alias: node)
45
- - edge(Src, Dst, Type) - match edges
46
- - attr(Id, Name, Value) - match node attributes (name, file, line, etc.)
47
- - \\+ - negation (not)
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
- limit?: number;
351
+ top_k?: number;
352
352
  domain?: string;
353
353
  }
354
354
 
355
- export async function handleSemanticSearch(args: SemanticSearchArgs): Promise<ToolResult> {
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.limit ?? 20;
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
- // Pseudo-similarity score based on match quality (placeholder until embeddings)
382
- const similarity = (1.0 - (i * 0.05)).toFixed(2);
383
- lines.push(`${i + 1}. [${similarity}] ${node.name} (${node.nodeType})`);
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('');
@@ -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
- function humanReadableId(semanticId: string): string {
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
- // Decode URI components first
531
- let id = semanticId;
532
- try { id = decodeURIComponent(id); } catch { /* keep as-is */ }
533
-
534
- // Find the # separator between file path and node descriptor
535
- const hashIdx = id.lastIndexOf('#');
536
- let filePart = '';
537
- let nodePart = id;
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
- filePart = id.slice(0, hashIdx);
540
- nodePart = id.slice(hashIdx + 1);
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;
@@ -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"}