kibi-mcp 0.3.3 → 0.5.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/diagnostics.js +28 -0
- package/dist/server/docs.js +22 -6
- package/dist/server/tools.js +29 -1
- package/dist/tools/check.js +27 -30
- package/dist/tools/core-module.js +85 -0
- package/dist/tools/coverage.js +28 -0
- package/dist/tools/entity-query.js +111 -0
- package/dist/tools/find-gaps.js +29 -0
- package/dist/tools/graph.js +27 -0
- package/dist/tools/prolog-list.js +1 -43
- package/dist/tools/query.js +4 -109
- package/dist/tools/search.js +37 -0
- package/dist/tools/status.js +20 -0
- package/dist/tools/symbols.js +4 -1
- package/dist/tools/upsert.js +151 -34
- package/dist/tools-config.js +169 -2
- package/package.json +3 -3
package/dist/diagnostics.js
CHANGED
|
@@ -83,6 +83,34 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
|
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
|
+
// implements REQ-002
|
|
87
|
+
export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
88
|
+
const fields = {
|
|
89
|
+
telemetry_status: telemetry ? "provided" : "missing",
|
|
90
|
+
};
|
|
91
|
+
const structuredContent = result && typeof result === "object" && "structuredContent" in result
|
|
92
|
+
? result
|
|
93
|
+
.structuredContent
|
|
94
|
+
: undefined;
|
|
95
|
+
if (toolName === "kb_query" || toolName === "kb_search") {
|
|
96
|
+
const resultCount = Number(structuredContent?.count ?? 0);
|
|
97
|
+
fields.result_count = resultCount;
|
|
98
|
+
fields.zero_results = resultCount === 0;
|
|
99
|
+
fields.result_summary =
|
|
100
|
+
resultCount === 0 ? "0 results" : `${resultCount} results`;
|
|
101
|
+
}
|
|
102
|
+
if (toolName === "kb_check") {
|
|
103
|
+
const violationCount = Number(structuredContent?.count ?? 0);
|
|
104
|
+
fields.violation_count = violationCount;
|
|
105
|
+
fields.requested_rules = Array.isArray(args.rules) ? args.rules : [];
|
|
106
|
+
fields.result_summary =
|
|
107
|
+
violationCount === 0 ? "0 violations" : `${violationCount} violations`;
|
|
108
|
+
}
|
|
109
|
+
if (!fields.result_summary) {
|
|
110
|
+
fields.result_summary = `${toolName} completed`;
|
|
111
|
+
}
|
|
112
|
+
return fields;
|
|
113
|
+
}
|
|
86
114
|
/**
|
|
87
115
|
* Extract business args and telemetry from tool call arguments.
|
|
88
116
|
*/
|
package/dist/server/docs.js
CHANGED
|
@@ -35,6 +35,7 @@ function renderToolsDoc() {
|
|
|
35
35
|
return lines.join("\n");
|
|
36
36
|
}
|
|
37
37
|
export const PROMPTS = [
|
|
38
|
+
// implements REQ-002, REQ-013, REQ-mcp-search-discovery
|
|
38
39
|
{
|
|
39
40
|
name: "init-kibi",
|
|
40
41
|
description: "Bootstrap Kibi on an existing repository with zero entities.",
|
|
@@ -104,8 +105,13 @@ export const PROMPTS = [
|
|
|
104
105
|
"",
|
|
105
106
|
"Treat this server as a branch-aware knowledge graph interface for software traceability.",
|
|
106
107
|
"",
|
|
107
|
-
"The server exposes
|
|
108
|
-
"- `
|
|
108
|
+
"The server exposes a curated public tool surface for KB operations:",
|
|
109
|
+
"- `kb_search`: Discovery across metadata and markdown body text",
|
|
110
|
+
"- `kb_query`: Exact lookup of entities by type, ID, tags, or source file",
|
|
111
|
+
"- `kb_status`: Branch, snapshot, and freshness inspection",
|
|
112
|
+
"- `kb_find_gaps`: Bulk missing/present relationship analysis",
|
|
113
|
+
"- `kb_coverage`: Curated coverage reporting",
|
|
114
|
+
"- `kb_graph`: Bounded graph traversal from seed IDs",
|
|
109
115
|
"- `kb_upsert`: Create or update entities and their relationships",
|
|
110
116
|
"- `kb_delete`: Remove entities by ID (with dependency safety checks)",
|
|
111
117
|
"- `kb_check`: Validate KB integrity against configurable rules",
|
|
@@ -113,7 +119,7 @@ export const PROMPTS = [
|
|
|
113
119
|
"Core modeling principles:",
|
|
114
120
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
115
121
|
"- Reuse canonical fact IDs across requirements; shared constrained facts make contradictions detectable.",
|
|
116
|
-
"- Use `
|
|
122
|
+
"- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
|
|
117
123
|
"- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
|
|
118
124
|
"- Run `kb_check` after meaningful mutations to catch integrity issues early.",
|
|
119
125
|
"- Prefer explicit IDs and enum values to avoid invalid parameters.",
|
|
@@ -128,7 +134,7 @@ export const PROMPTS = [
|
|
|
128
134
|
"",
|
|
129
135
|
"Follow this sequence for reliable operation:",
|
|
130
136
|
"",
|
|
131
|
-
"1. **
|
|
137
|
+
"1. **Discover first**: Call `kb_search` for exploratory discovery, then `kb_query` to confirm exact current state before mutation.",
|
|
132
138
|
"2. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
|
|
133
139
|
"3. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
|
|
134
140
|
"4. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
|
|
@@ -167,8 +173,8 @@ function registerDocResources() {
|
|
|
167
173
|
"kibi-mcp is a stdio MCP server for querying and mutating the Kibi knowledge base.",
|
|
168
174
|
"",
|
|
169
175
|
"Scope:",
|
|
170
|
-
"-
|
|
171
|
-
"-
|
|
176
|
+
"- Read-only discovery and reporting (`kb_search`, `kb_query`, `kb_status`, `kb_find_gaps`, `kb_coverage`, `kb_graph`)",
|
|
177
|
+
"- KB mutation and validation (`kb_upsert`, `kb_delete`, `kb_check`)",
|
|
172
178
|
"- Automatic branch-local attachment for the active workspace",
|
|
173
179
|
"",
|
|
174
180
|
"Use this server when you need branch-local, machine-readable project memory.",
|
|
@@ -188,6 +194,11 @@ function registerDocResources() {
|
|
|
188
194
|
const examples = [
|
|
189
195
|
"# kibi-mcp Examples",
|
|
190
196
|
"",
|
|
197
|
+
"## Discover before mutating",
|
|
198
|
+
'1. `kb_search` with `{ "query": "login flow" }` to discover related requirements, tests, and ADRs',
|
|
199
|
+
'2. `kb_query` with `{ "type": "req", "sourceFile": "src/auth/login.ts" }` for exact follow-up',
|
|
200
|
+
"3. `kb_status` with `{}` when branch attachment or freshness confidence matters",
|
|
201
|
+
"",
|
|
191
202
|
"## Model requirements as reusable facts",
|
|
192
203
|
'1. `kb_query` with `{ "type": "fact" }` to find existing fact IDs before creating new ones',
|
|
193
204
|
"2. `kb_upsert` for the fact entity first (create-before-link)",
|
|
@@ -195,6 +206,11 @@ function registerDocResources() {
|
|
|
195
206
|
"4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
|
|
196
207
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
197
208
|
"",
|
|
209
|
+
"## Find missing coverage",
|
|
210
|
+
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
|
211
|
+
'2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review evaluated coverage rows',
|
|
212
|
+
'3. `kb_graph` with `{ "seedIds": ["REQ-001"], "direction": "both", "depth": 2 }` to inspect neighboring entities',
|
|
213
|
+
"",
|
|
198
214
|
"## Add a requirement and link it to a test",
|
|
199
215
|
'1. `kb_query` with `{ "type": "test" }` to check for existing test IDs',
|
|
200
216
|
'2. `kb_query` with `{ "id": "REQ-XXX" }` to verify the requirement exists',
|
package/dist/server/tools.js
CHANGED
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { z } from "zod";
|
|
20
|
-
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, extractToolCallPayload, } from "../diagnostics.js";
|
|
20
|
+
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
21
21
|
import { TOOLS } from "../tools-config.js";
|
|
22
22
|
import { handleKbCheck } from "../tools/check.js";
|
|
23
|
+
import { handleKbCoverage } from "../tools/coverage.js";
|
|
23
24
|
import { handleKbDelete } from "../tools/delete.js";
|
|
25
|
+
import { handleKbFindGaps } from "../tools/find-gaps.js";
|
|
26
|
+
import { handleKbGraph } from "../tools/graph.js";
|
|
24
27
|
import { handleKbQuery } from "../tools/query.js";
|
|
28
|
+
import { handleKbSearch } from "../tools/search.js";
|
|
29
|
+
import { handleKbStatus } from "../tools/status.js";
|
|
25
30
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
26
31
|
import { activeBranchName, ensureProlog, inFlightRequests, isShuttingDown, prologProcess, } from "./session.js";
|
|
27
32
|
const ACTIVE_TOOLS = TOOLS;
|
|
@@ -164,6 +169,7 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
164
169
|
// Log usage in diagnostic mode
|
|
165
170
|
if (DIAGNOSTIC_MODE_ENABLED) {
|
|
166
171
|
const finishedAt = new Date();
|
|
172
|
+
const diagnosticFields = deriveDiagnosticFields(name, businessArgs, telemetry, result);
|
|
167
173
|
appendUsageLogLine({
|
|
168
174
|
timestamp: finishedAt.toISOString(),
|
|
169
175
|
request_id: requestId,
|
|
@@ -176,6 +182,7 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
176
182
|
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
177
183
|
prolog_pid: prologProcess?.getPid() ?? null,
|
|
178
184
|
active_branch: activeBranchName,
|
|
185
|
+
...diagnosticFields,
|
|
179
186
|
});
|
|
180
187
|
}
|
|
181
188
|
return result;
|
|
@@ -218,6 +225,7 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
218
225
|
};
|
|
219
226
|
server.registerTool(name, { description, inputSchema: jsonSchemaToZod(inputSchema) }, wrappedHandler);
|
|
220
227
|
}
|
|
228
|
+
// implements REQ-002, REQ-013
|
|
221
229
|
export function registerAllTools(server) {
|
|
222
230
|
const toolDef = (name) => {
|
|
223
231
|
const t = ACTIVE_TOOLS.find((t) => t.name === name);
|
|
@@ -229,6 +237,26 @@ export function registerAllTools(server) {
|
|
|
229
237
|
const prolog = await ensureProlog();
|
|
230
238
|
return handleKbQuery(prolog, args);
|
|
231
239
|
});
|
|
240
|
+
addTool(server, "kb_search", toolDef("kb_search").description, toolDef("kb_search").inputSchema, async (args) => {
|
|
241
|
+
const prolog = await ensureProlog();
|
|
242
|
+
return handleKbSearch(prolog, args);
|
|
243
|
+
});
|
|
244
|
+
addTool(server, "kb_status", toolDef("kb_status").description, toolDef("kb_status").inputSchema, async (args) => {
|
|
245
|
+
const prolog = await ensureProlog();
|
|
246
|
+
return handleKbStatus(prolog, args);
|
|
247
|
+
});
|
|
248
|
+
addTool(server, "kb_find_gaps", toolDef("kb_find_gaps").description, toolDef("kb_find_gaps").inputSchema, async (args) => {
|
|
249
|
+
const prolog = await ensureProlog();
|
|
250
|
+
return handleKbFindGaps(prolog, args);
|
|
251
|
+
});
|
|
252
|
+
addTool(server, "kb_coverage", toolDef("kb_coverage").description, toolDef("kb_coverage").inputSchema, async (args) => {
|
|
253
|
+
const prolog = await ensureProlog();
|
|
254
|
+
return handleKbCoverage(prolog, args);
|
|
255
|
+
});
|
|
256
|
+
addTool(server, "kb_graph", toolDef("kb_graph").description, toolDef("kb_graph").inputSchema, async (args) => {
|
|
257
|
+
const prolog = await ensureProlog();
|
|
258
|
+
return handleKbGraph(prolog, args);
|
|
259
|
+
});
|
|
232
260
|
addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
|
|
233
261
|
const prolog = await ensureProlog();
|
|
234
262
|
return handleKbUpsert(prolog, args);
|
package/dist/tools/check.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import * as path from "node:path";
|
|
21
|
+
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
21
22
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
function resolveChecksPlPath() {
|
|
@@ -31,30 +32,15 @@ function resolveChecksPlPath() {
|
|
|
31
32
|
return installedChecksPl;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
catch {
|
|
35
|
+
catch {
|
|
36
|
+
// require.resolve not available or package not installed
|
|
37
|
+
}
|
|
35
38
|
const localChecksPl = path.join(process.cwd(), "packages/core/src/checks.pl");
|
|
36
39
|
if (existsSync(localChecksPl)) {
|
|
37
40
|
return localChecksPl;
|
|
38
41
|
}
|
|
39
42
|
throw new Error("Unable to resolve checks.pl path");
|
|
40
43
|
}
|
|
41
|
-
const ALL_RULES = [
|
|
42
|
-
"must-priority-coverage",
|
|
43
|
-
"no-dangling-refs",
|
|
44
|
-
"no-cycles",
|
|
45
|
-
"required-fields",
|
|
46
|
-
"symbol-coverage",
|
|
47
|
-
"symbol-traceability",
|
|
48
|
-
"deprecated-adr-no-successor",
|
|
49
|
-
"domain-contradictions",
|
|
50
|
-
];
|
|
51
|
-
const RULE_NAMES = new Set(ALL_RULES);
|
|
52
|
-
const DEFAULT_CHECKS_CONFIG = {
|
|
53
|
-
rules: Object.fromEntries(ALL_RULES.map((rule) => [rule, true])),
|
|
54
|
-
symbolTraceability: {
|
|
55
|
-
requireAdr: false,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
44
|
function formatDiagnosticsForMcp(diagnostics) {
|
|
59
45
|
return diagnostics.map((d) => ({
|
|
60
46
|
category: d.category,
|
|
@@ -64,6 +50,24 @@ function formatDiagnosticsForMcp(diagnostics) {
|
|
|
64
50
|
suggestion: d.suggestion,
|
|
65
51
|
}));
|
|
66
52
|
}
|
|
53
|
+
function formatViolationText(violations) {
|
|
54
|
+
if (violations.length === 0) {
|
|
55
|
+
return "No violations found";
|
|
56
|
+
}
|
|
57
|
+
const details = violations.map((violation) => {
|
|
58
|
+
const parts = [
|
|
59
|
+
violation.rule,
|
|
60
|
+
violation.entityId,
|
|
61
|
+
violation.source ?? "unknown-source",
|
|
62
|
+
violation.description,
|
|
63
|
+
];
|
|
64
|
+
if (violation.suggestion) {
|
|
65
|
+
parts.push(`Suggestion: ${violation.suggestion}`);
|
|
66
|
+
}
|
|
67
|
+
return `- ${parts.join(" | ")}`;
|
|
68
|
+
});
|
|
69
|
+
return `${violations.length} violations found\n${details.join("\n")}`;
|
|
70
|
+
}
|
|
67
71
|
// implements REQ-002
|
|
68
72
|
function loadChecksConfig(workspaceRoot) {
|
|
69
73
|
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
@@ -103,21 +107,16 @@ function loadChecksConfig(workspaceRoot) {
|
|
|
103
107
|
}
|
|
104
108
|
// implements REQ-002
|
|
105
109
|
function getEffectiveRules(configRules, requestedRules) {
|
|
110
|
+
if (requestedRules && requestedRules.length > 0) {
|
|
111
|
+
return new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
112
|
+
}
|
|
106
113
|
const effective = new Set();
|
|
107
|
-
for (const rule of
|
|
114
|
+
for (const rule of RULE_NAMES) {
|
|
108
115
|
const enabled = configRules[rule] ?? true;
|
|
109
116
|
if (enabled) {
|
|
110
117
|
effective.add(rule);
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
|
-
if (requestedRules && requestedRules.length > 0) {
|
|
114
|
-
const allowed = new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
115
|
-
for (const rule of Array.from(effective)) {
|
|
116
|
-
if (!allowed.has(rule)) {
|
|
117
|
-
effective.delete(rule);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
120
|
return effective;
|
|
122
121
|
}
|
|
123
122
|
/**
|
|
@@ -151,9 +150,7 @@ export async function handleKbCheck(prolog, args) {
|
|
|
151
150
|
file: v.source,
|
|
152
151
|
suggestion: v.suggestion,
|
|
153
152
|
}));
|
|
154
|
-
const summary = aggregatedViolations
|
|
155
|
-
? "No violations found"
|
|
156
|
-
: `${aggregatedViolations.length} violations found`;
|
|
153
|
+
const summary = formatViolationText(aggregatedViolations);
|
|
157
154
|
return {
|
|
158
155
|
content: [
|
|
159
156
|
{
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PrologProcess } from "kibi-cli/prolog";
|
|
5
|
+
import { escapeAtomContent } from "kibi-cli/prolog/codec";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
// implements REQ-002, REQ-013
|
|
8
|
+
export function resolveCorePlPath(fileName) {
|
|
9
|
+
const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
|
|
10
|
+
const override = process.env[envKey];
|
|
11
|
+
if (override && existsSync(override)) {
|
|
12
|
+
return override;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const installedPath = require.resolve(`kibi-core/src/${fileName}`);
|
|
16
|
+
if (existsSync(installedPath)) {
|
|
17
|
+
return installedPath;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// require.resolve not available or package not installed
|
|
22
|
+
}
|
|
23
|
+
const localPath = path.join(process.cwd(), "packages/core/src", fileName);
|
|
24
|
+
if (existsSync(localPath)) {
|
|
25
|
+
return localPath;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unable to resolve core module path for ${fileName}`);
|
|
28
|
+
}
|
|
29
|
+
// implements REQ-002, REQ-013
|
|
30
|
+
export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel) {
|
|
31
|
+
const modulePath = escapeAtomContent(resolveCorePlPath(fileName).replace(/\\/g, "/"));
|
|
32
|
+
if (!(prolog instanceof PrologProcess)) {
|
|
33
|
+
const mockedResult = await prolog.query(`(use_module('${modulePath}'), ${goal})`);
|
|
34
|
+
if (!mockedResult.success) {
|
|
35
|
+
throw new Error(`${errorLabel} query failed: ${mockedResult.error || "Unknown error"}`);
|
|
36
|
+
}
|
|
37
|
+
const mockedJson = mockedResult.bindings.JsonString;
|
|
38
|
+
if (!mockedJson) {
|
|
39
|
+
throw new Error(`${errorLabel} query returned no JsonString binding`);
|
|
40
|
+
}
|
|
41
|
+
let mockedParsed = JSON.parse(mockedJson);
|
|
42
|
+
if (typeof mockedParsed === "string") {
|
|
43
|
+
mockedParsed = JSON.parse(mockedParsed);
|
|
44
|
+
}
|
|
45
|
+
return mockedParsed;
|
|
46
|
+
}
|
|
47
|
+
const oneShotCapable = prolog;
|
|
48
|
+
prolog.invalidateCache();
|
|
49
|
+
const result = oneShotCapable.useOneShotMode
|
|
50
|
+
? await prolog.query(`(use_module('${modulePath}'), ${goal})`)
|
|
51
|
+
: await runInteractiveModuleQuery(prolog, modulePath, goal, errorLabel);
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new Error(`${errorLabel} query failed: ${result.error || "Unknown error"}`);
|
|
54
|
+
}
|
|
55
|
+
const rawJson = result.bindings.JsonString;
|
|
56
|
+
if (!rawJson) {
|
|
57
|
+
throw new Error(`${errorLabel} query returned no JsonString binding`);
|
|
58
|
+
}
|
|
59
|
+
let parsed = JSON.parse(rawJson);
|
|
60
|
+
if (typeof parsed === "string") {
|
|
61
|
+
parsed = JSON.parse(parsed);
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
async function runInteractiveModuleQuery(prolog, modulePath, goal, errorLabel) {
|
|
66
|
+
const loadResult = await prolog.query(`use_module('${modulePath}')`);
|
|
67
|
+
if (!loadResult.success) {
|
|
68
|
+
throw new Error(`${errorLabel} module load failed: ${loadResult.error || "Unknown error"}`);
|
|
69
|
+
}
|
|
70
|
+
return prolog.query(goal);
|
|
71
|
+
}
|
|
72
|
+
// implements REQ-002, REQ-013
|
|
73
|
+
export function toPrologAtom(value) {
|
|
74
|
+
if (!value) {
|
|
75
|
+
return "none";
|
|
76
|
+
}
|
|
77
|
+
return `'${escapeAtomContent(value)}'`;
|
|
78
|
+
}
|
|
79
|
+
// implements REQ-002, REQ-013
|
|
80
|
+
export function toPrologList(values) {
|
|
81
|
+
if (!values || values.length === 0) {
|
|
82
|
+
return "[]";
|
|
83
|
+
}
|
|
84
|
+
return `[${values.map((value) => `'${escapeAtomContent(value)}'`).join(",")}]`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runJsonModuleQuery, toPrologList } from "./core-module.js";
|
|
2
|
+
// implements REQ-002, REQ-013
|
|
3
|
+
export async function handleKbCoverage(prolog, args) {
|
|
4
|
+
const by = args.by ?? "req";
|
|
5
|
+
const limit = args.limit ?? 100;
|
|
6
|
+
const offset = args.offset ?? 0;
|
|
7
|
+
const includePassing = args.includePassing ?? false;
|
|
8
|
+
const includeTransitive = args.includeTransitive ?? true;
|
|
9
|
+
try {
|
|
10
|
+
const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:coverage_report_json('${by}', ${toPrologList(args.tags)}, ${includePassing}, ${includeTransitive}, ${limit}, ${offset}, JsonString)`, "Coverage execution");
|
|
11
|
+
const summary = payload?.summary ?? {};
|
|
12
|
+
const fullyCovered = Number(summary.fullyCovered ?? 0);
|
|
13
|
+
const total = Number(summary.total ?? 0);
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `Coverage summary: ${fullyCovered} fully covered out of ${total}.`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
structuredContent: payload,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(`Coverage execution failed: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { escapeAtomContent, parseEntityFromBinding, parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
|
|
2
|
+
export const VALID_ENTITY_TYPES = [
|
|
3
|
+
// implements REQ-002
|
|
4
|
+
"req",
|
|
5
|
+
"scenario",
|
|
6
|
+
"test",
|
|
7
|
+
"adr",
|
|
8
|
+
"flag",
|
|
9
|
+
"event",
|
|
10
|
+
"symbol",
|
|
11
|
+
"fact",
|
|
12
|
+
];
|
|
13
|
+
// implements REQ-002, REQ-013
|
|
14
|
+
export function validateEntityType(type) {
|
|
15
|
+
if (type && !VALID_ENTITY_TYPES.includes(type)) {
|
|
16
|
+
throw new Error(`Invalid type '${type}'. Valid types: ${VALID_ENTITY_TYPES.join(", ")}. Use a single type value, or omit this parameter to query all entities.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// implements REQ-002, REQ-013
|
|
20
|
+
export function buildEntityGoal(args) {
|
|
21
|
+
const { type, id, tags, sourceFile } = args;
|
|
22
|
+
if (sourceFile) {
|
|
23
|
+
const safeSource = escapeAtomContent(sourceFile);
|
|
24
|
+
if (type) {
|
|
25
|
+
const safeType = escapeAtomContent(type);
|
|
26
|
+
return `findall([Id,'${safeType}',Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, '${safeType}', Props)), Results)`;
|
|
27
|
+
}
|
|
28
|
+
return `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
|
|
29
|
+
}
|
|
30
|
+
if (id && type) {
|
|
31
|
+
const safeId = escapeAtomContent(id);
|
|
32
|
+
const safeType = escapeAtomContent(type);
|
|
33
|
+
return `findall(['${safeId}','${safeType}',Props], kb_entity('${safeId}', '${safeType}', Props), Results)`;
|
|
34
|
+
}
|
|
35
|
+
if (id) {
|
|
36
|
+
const safeId = escapeAtomContent(id);
|
|
37
|
+
return `findall(['${safeId}',Type,Props], kb_entity('${safeId}', Type, Props), Results)`;
|
|
38
|
+
}
|
|
39
|
+
if (tags && tags.length > 0) {
|
|
40
|
+
if (type) {
|
|
41
|
+
const safeType = escapeAtomContent(type);
|
|
42
|
+
return `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
|
43
|
+
}
|
|
44
|
+
return "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
|
|
45
|
+
}
|
|
46
|
+
if (type) {
|
|
47
|
+
const safeType = escapeAtomContent(type);
|
|
48
|
+
return `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
|
49
|
+
}
|
|
50
|
+
return "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
|
|
51
|
+
}
|
|
52
|
+
// implements REQ-002, REQ-013
|
|
53
|
+
export async function loadEntities(prolog, args) {
|
|
54
|
+
validateEntityType(args.type);
|
|
55
|
+
const goal = buildEntityGoal(args);
|
|
56
|
+
const queryResult = await prolog.query(goal);
|
|
57
|
+
let results = [];
|
|
58
|
+
if (!queryResult.success) {
|
|
59
|
+
throw new Error(queryResult.error || "Query failed with unknown error");
|
|
60
|
+
}
|
|
61
|
+
if (queryResult.bindings.Results) {
|
|
62
|
+
const entitiesData = parseListOfLists(queryResult.bindings.Results);
|
|
63
|
+
for (const data of entitiesData) {
|
|
64
|
+
results.push(parseEntityFromList(data));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (queryResult.bindings.Result) {
|
|
68
|
+
results = [parseEntityFromBinding(queryResult.bindings.Result)];
|
|
69
|
+
}
|
|
70
|
+
if (args.tags && args.tags.length > 0) {
|
|
71
|
+
const requestedTags = args.tags;
|
|
72
|
+
results = dedupeEntities(results.filter((entity) => hasAnyTag(entity, requestedTags)));
|
|
73
|
+
}
|
|
74
|
+
return dedupeEntities(results);
|
|
75
|
+
}
|
|
76
|
+
// implements REQ-002
|
|
77
|
+
export function paginateResults(results, limit = 100, offset = 0) {
|
|
78
|
+
return results.slice(offset, offset + limit);
|
|
79
|
+
}
|
|
80
|
+
function hasAnyTag(entity, requestedTags) {
|
|
81
|
+
const expected = new Set(requestedTags.map(normalizeTagValue));
|
|
82
|
+
const rawTags = entity.tags;
|
|
83
|
+
if (!Array.isArray(rawTags) || rawTags.length === 0) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (const tag of rawTags) {
|
|
87
|
+
if (expected.has(normalizeTagValue(tag))) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function normalizeTagValue(tag) {
|
|
94
|
+
return String(tag).trim();
|
|
95
|
+
}
|
|
96
|
+
// implements REQ-002
|
|
97
|
+
export function dedupeEntities(entities) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const deduped = [];
|
|
100
|
+
for (const entity of entities) {
|
|
101
|
+
const id = String(entity.id ?? "");
|
|
102
|
+
const type = String(entity.type ?? "");
|
|
103
|
+
const key = `${type}::${id}`;
|
|
104
|
+
if (seen.has(key)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
seen.add(key);
|
|
108
|
+
deduped.push(entity);
|
|
109
|
+
}
|
|
110
|
+
return deduped;
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { runJsonModuleQuery, toPrologAtom, toPrologList, } from "./core-module.js";
|
|
2
|
+
import { validateEntityType } from "./entity-query.js";
|
|
3
|
+
// implements REQ-002, REQ-013
|
|
4
|
+
export async function handleKbFindGaps(prolog, args) {
|
|
5
|
+
validateEntityType(args.type);
|
|
6
|
+
const limit = args.limit ?? 100;
|
|
7
|
+
const offset = args.offset ?? 0;
|
|
8
|
+
try {
|
|
9
|
+
const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:find_gaps_json(${toPrologAtom(args.type)}, ${toPrologList(args.missingRelationships)}, ${toPrologList(args.presentRelationships)}, ${toPrologList(args.tags)}, ${toPrologAtom(args.sourceFile)}, ${limit}, ${offset}, JsonString)`, "Find-gaps execution");
|
|
10
|
+
const rows = payload?.rows ?? [];
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: rows.length === 0
|
|
16
|
+
? "No matching gaps found."
|
|
17
|
+
: `Found ${payload?.count ?? rows.length} gap rows. Showing ${rows.length}: ${rows
|
|
18
|
+
.map((row) => row.id)
|
|
19
|
+
.join(", ")}`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
structuredContent: payload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new Error(`Find-gaps execution failed: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { runJsonModuleQuery, toPrologList } from "./core-module.js";
|
|
2
|
+
// implements REQ-002, REQ-013
|
|
3
|
+
export async function handleKbGraph(prolog, args) {
|
|
4
|
+
const direction = args.direction ?? "outgoing";
|
|
5
|
+
const depth = args.depth ?? 1;
|
|
6
|
+
const maxNodes = args.maxNodes ?? 200;
|
|
7
|
+
const maxEdges = args.maxEdges ?? 500;
|
|
8
|
+
try {
|
|
9
|
+
const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${toPrologList(args.seedIds)}, ${toPrologList(args.relationships)}, '${direction}', ${depth}, ${toPrologList(args.entityTypes)}, ${maxNodes}, ${maxEdges}, JsonString)`, "Graph execution");
|
|
10
|
+
const nodes = payload?.nodes ?? [];
|
|
11
|
+
return {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: nodes.length === 0
|
|
16
|
+
? "Graph traversal returned no nodes."
|
|
17
|
+
: `Graph traversal returned ${nodes.length} nodes and ${(payload?.edges ?? []).length} edges from ${args.seedIds.join(", ")}.`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
structuredContent: payload,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new Error(`Graph execution failed: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
+
import { splitTopLevel } from "kibi-cli/prolog/codec";
|
|
18
19
|
// implements REQ-002
|
|
19
20
|
export function parseAtomList(raw) {
|
|
20
21
|
const trimmed = raw.trim();
|
|
@@ -86,49 +87,6 @@ function unwrapList(value) {
|
|
|
86
87
|
}
|
|
87
88
|
return value;
|
|
88
89
|
}
|
|
89
|
-
function splitTopLevel(input, delimiter) {
|
|
90
|
-
const parts = [];
|
|
91
|
-
let current = "";
|
|
92
|
-
let depth = 0;
|
|
93
|
-
let inDoubleQuotes = false;
|
|
94
|
-
let inSingleQuotes = false;
|
|
95
|
-
for (let i = 0; i < input.length; i++) {
|
|
96
|
-
const ch = input[i];
|
|
97
|
-
const prev = i > 0 ? input[i - 1] : "";
|
|
98
|
-
if (ch === '"' && !inSingleQuotes && prev !== "\\") {
|
|
99
|
-
inDoubleQuotes = !inDoubleQuotes;
|
|
100
|
-
current += ch;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (ch === "'" && !inDoubleQuotes && prev !== "\\") {
|
|
104
|
-
inSingleQuotes = !inSingleQuotes;
|
|
105
|
-
current += ch;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!inSingleQuotes && !inDoubleQuotes && (ch === "[" || ch === "(")) {
|
|
109
|
-
depth++;
|
|
110
|
-
current += ch;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (!inSingleQuotes && !inDoubleQuotes && (ch === "]" || ch === ")")) {
|
|
114
|
-
depth--;
|
|
115
|
-
current += ch;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!inSingleQuotes && !inDoubleQuotes && depth === 0 && ch === delimiter) {
|
|
119
|
-
if (current.length > 0) {
|
|
120
|
-
parts.push(current);
|
|
121
|
-
}
|
|
122
|
-
current = "";
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
current += ch;
|
|
126
|
-
}
|
|
127
|
-
if (current.length > 0) {
|
|
128
|
-
parts.push(current);
|
|
129
|
-
}
|
|
130
|
-
return parts;
|
|
131
|
-
}
|
|
132
90
|
function stripQuotes(value) {
|
|
133
91
|
if (value.startsWith("'") && value.endsWith("'")) {
|
|
134
92
|
return value.slice(1, -1);
|