kibi-mcp 0.3.1 → 0.3.3
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 +95 -0
- package/dist/server/session.js +6 -1
- package/dist/server/tools.js +48 -3
- package/dist/server.js +8 -1
- package/dist/tools/check.js +124 -388
- package/dist/tools/delete.js +4 -1
- package/dist/tools/query.js +1 -3
- package/dist/tools/upsert.js +38 -30
- package/dist/tools-config.js +31 -2
- package/package.json +4 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { resolveWorkspaceRoot } from "./workspace.js";
|
|
21
|
+
const DIAGNOSTIC_MODE_FLAG = "--diagnostic-mode";
|
|
22
|
+
/**
|
|
23
|
+
* Whether diagnostic mode is enabled via CLI flag.
|
|
24
|
+
* Set during server startup and never changes at runtime.
|
|
25
|
+
*/
|
|
26
|
+
export const DIAGNOSTIC_MODE_ENABLED = process.argv.includes(DIAGNOSTIC_MODE_FLAG);
|
|
27
|
+
/**
|
|
28
|
+
* Path to the diagnostic usage log file.
|
|
29
|
+
* Only valid when DIAGNOSTIC_MODE_ENABLED is true.
|
|
30
|
+
*/
|
|
31
|
+
let diagnosticUsageLogPath = null;
|
|
32
|
+
/**
|
|
33
|
+
* Initialize diagnostic mode: set up usage.log path.
|
|
34
|
+
* Called once during server startup.
|
|
35
|
+
*/
|
|
36
|
+
export function initializeDiagnosticMode() {
|
|
37
|
+
if (DIAGNOSTIC_MODE_ENABLED) {
|
|
38
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
39
|
+
diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
|
|
40
|
+
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Append a JSON line to the usage.log file.
|
|
45
|
+
* No-op if diagnostic mode is not enabled.
|
|
46
|
+
*/
|
|
47
|
+
export function appendUsageLogLine(entry) {
|
|
48
|
+
if (!DIAGNOSTIC_MODE_ENABLED || !diagnosticUsageLogPath) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const logDir = path.dirname(diagnosticUsageLogPath);
|
|
52
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
53
|
+
fs.appendFileSync(diagnosticUsageLogPath, `${JSON.stringify(entry)}\n`, {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Schema for _diagnostic_telemetry field added to tool inputs in diagnostic mode.
|
|
59
|
+
*/
|
|
60
|
+
export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
|
|
61
|
+
type: "object",
|
|
62
|
+
description: "REQUIRED when diagnostic mode is on. Provide self-reflection metadata about this tool call.",
|
|
63
|
+
properties: {
|
|
64
|
+
is_autonomous: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
description: "Set to TRUE if you are calling this tool on your own initiative to retrieve context. Set to FALSE if the user explicitly commanded you to use the knowledge base.",
|
|
67
|
+
},
|
|
68
|
+
reasoning: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "A brief, 1-2 sentence internal thought explaining exactly why you are calling this tool right now and what information you expect to get.",
|
|
71
|
+
},
|
|
72
|
+
confidence_score: {
|
|
73
|
+
type: "number",
|
|
74
|
+
description: "A score from 0.0 to 1.0 representing your confidence that the exact parameters, IDs, or tags you provided will yield a successful result.",
|
|
75
|
+
},
|
|
76
|
+
attempt_number: {
|
|
77
|
+
type: "integer",
|
|
78
|
+
description: "If you are retrying this exact task because a previous tool call failed or returned empty results, increment this number (start at 1).",
|
|
79
|
+
},
|
|
80
|
+
missing_context: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "If you had to split your task into multiple steps because this tool lacks a specific filtering or querying capability, describe what parameter is missing. Otherwise, leave empty.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Extract business args and telemetry from tool call arguments.
|
|
88
|
+
*/
|
|
89
|
+
export function extractToolCallPayload(args) {
|
|
90
|
+
const { _diagnostic_telemetry, ...businessArgs } = args;
|
|
91
|
+
const telemetry = _diagnostic_telemetry && typeof _diagnostic_telemetry === "object"
|
|
92
|
+
? _diagnostic_telemetry
|
|
93
|
+
: null;
|
|
94
|
+
return { businessArgs, telemetry };
|
|
95
|
+
}
|
package/dist/server/session.js
CHANGED
|
@@ -101,6 +101,7 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
101
101
|
// Exit
|
|
102
102
|
process.exit(exitCode);
|
|
103
103
|
}
|
|
104
|
+
// implements REQ-008
|
|
104
105
|
async function ensurePrologUnsafe() {
|
|
105
106
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
106
107
|
// Determine target branch: respect KIBI_BRANCH override or resolve from git
|
|
@@ -131,7 +132,11 @@ async function ensurePrologUnsafe() {
|
|
|
131
132
|
}
|
|
132
133
|
// Branch changed - need to detach and re-attach
|
|
133
134
|
debugLog(`[KIBI-MCP] Branch changed: ${activeBranchName} -> ${targetBranch}`);
|
|
134
|
-
//
|
|
135
|
+
// Persist and detach from old KB
|
|
136
|
+
const saveResult = await prologProcess.query("kb_save");
|
|
137
|
+
if (!saveResult.success) {
|
|
138
|
+
throw new Error(`Failed to save old KB before detach: ${saveResult.error || "Unknown error"}`);
|
|
139
|
+
}
|
|
135
140
|
const detachResult = await prologProcess.query("kb_detach");
|
|
136
141
|
if (!detachResult.success) {
|
|
137
142
|
debugLog(`[KIBI-MCP] Warning: failed to detach from old KB: ${detachResult.error || "Unknown error"}`);
|
package/dist/server/tools.js
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
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
21
|
import { TOOLS } from "../tools-config.js";
|
|
21
22
|
import { handleKbCheck } from "../tools/check.js";
|
|
22
23
|
import { handleKbDelete } from "../tools/delete.js";
|
|
23
24
|
import { handleKbQuery } from "../tools/query.js";
|
|
24
25
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
25
|
-
import { ensureProlog, inFlightRequests, isShuttingDown } from "./session.js";
|
|
26
|
+
import { activeBranchName, ensureProlog, inFlightRequests, isShuttingDown, prologProcess, } from "./session.js";
|
|
26
27
|
const ACTIVE_TOOLS = TOOLS;
|
|
27
28
|
function debugLog(...args) {
|
|
28
29
|
if (process.env.KIBI_MCP_DEBUG) {
|
|
@@ -131,12 +132,16 @@ export function jsonSchemaToZod(schema) {
|
|
|
131
132
|
}
|
|
132
133
|
function addTool(server, name, description, inputSchema, handler) {
|
|
133
134
|
const wrappedHandler = async (args) => {
|
|
135
|
+
const startedAt = new Date();
|
|
136
|
+
// Extract telemetry in diagnostic mode
|
|
137
|
+
const { businessArgs, telemetry } = DIAGNOSTIC_MODE_ENABLED
|
|
138
|
+
? extractToolCallPayload(args)
|
|
139
|
+
: { businessArgs: args, telemetry: null };
|
|
134
140
|
try {
|
|
135
141
|
// Validate that args is a valid object
|
|
136
142
|
if (typeof args !== "object" || args === null) {
|
|
137
143
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
138
144
|
}
|
|
139
|
-
const businessArgs = args;
|
|
140
145
|
// Check if shutting down before processing
|
|
141
146
|
if (isShuttingDown) {
|
|
142
147
|
throw new Error(`Tool ${name} rejected: server is shutting down`);
|
|
@@ -155,7 +160,47 @@ function addTool(server, name, description, inputSchema, handler) {
|
|
|
155
160
|
inFlightRequests.set(requestId, handlerPromise);
|
|
156
161
|
try {
|
|
157
162
|
// Execute handler
|
|
158
|
-
|
|
163
|
+
const result = await handlerPromise;
|
|
164
|
+
// Log usage in diagnostic mode
|
|
165
|
+
if (DIAGNOSTIC_MODE_ENABLED) {
|
|
166
|
+
const finishedAt = new Date();
|
|
167
|
+
appendUsageLogLine({
|
|
168
|
+
timestamp: finishedAt.toISOString(),
|
|
169
|
+
request_id: requestId,
|
|
170
|
+
tool: name,
|
|
171
|
+
telemetry,
|
|
172
|
+
business_args: businessArgs,
|
|
173
|
+
status: "success",
|
|
174
|
+
started_at: startedAt.toISOString(),
|
|
175
|
+
finished_at: finishedAt.toISOString(),
|
|
176
|
+
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
177
|
+
prolog_pid: prologProcess?.getPid() ?? null,
|
|
178
|
+
active_branch: activeBranchName,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
// Log error in diagnostic mode
|
|
185
|
+
if (DIAGNOSTIC_MODE_ENABLED) {
|
|
186
|
+
const finishedAt = new Date();
|
|
187
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
188
|
+
appendUsageLogLine({
|
|
189
|
+
timestamp: finishedAt.toISOString(),
|
|
190
|
+
request_id: requestId,
|
|
191
|
+
tool: name,
|
|
192
|
+
telemetry,
|
|
193
|
+
business_args: businessArgs,
|
|
194
|
+
status: "error",
|
|
195
|
+
started_at: startedAt.toISOString(),
|
|
196
|
+
finished_at: finishedAt.toISOString(),
|
|
197
|
+
duration_ms: finishedAt.getTime() - startedAt.getTime(),
|
|
198
|
+
prolog_pid: prologProcess?.getPid() ?? null,
|
|
199
|
+
active_branch: activeBranchName,
|
|
200
|
+
error_message: err.message,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
159
204
|
}
|
|
160
205
|
finally {
|
|
161
206
|
// Always clean up from Map when done (success or failure)
|
package/dist/server.js
CHANGED
|
@@ -15,17 +15,24 @@
|
|
|
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 { readFileSync } from "node:fs";
|
|
18
19
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { initializeDiagnosticMode } from "./diagnostics.js";
|
|
20
22
|
import { loadDefaultEnvFile } from "./env.js";
|
|
21
23
|
import { setupDocsAndPrompts } from "./server/docs.js";
|
|
22
24
|
import { registerAllTools } from "./server/tools.js";
|
|
23
25
|
import { connectTransport, setupTransportHandlers, } from "./server/transport.js";
|
|
26
|
+
// Read version from package.json to prevent drift
|
|
27
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
28
|
+
const VERSION = packageJson.version ?? "0.1.0";
|
|
24
29
|
export async function startServer() {
|
|
25
30
|
// Load environment configuration
|
|
26
31
|
loadDefaultEnvFile();
|
|
32
|
+
// Initialize diagnostic mode if --diagnostic-mode flag is present
|
|
33
|
+
initializeDiagnosticMode();
|
|
27
34
|
// Create MCP server
|
|
28
|
-
const server = new McpServer({ name: "kibi-mcp", version:
|
|
35
|
+
const server = new McpServer({ name: "kibi-mcp", version: VERSION });
|
|
29
36
|
// Setup documentation resources and prompts
|
|
30
37
|
setupDocsAndPrompts(server);
|
|
31
38
|
// Register all KB tools
|
package/dist/tools/check.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
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
|
-
*/
|
|
18
|
-
import { existsSync } from "node:fs";
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import * as path from "node:path";
|
|
21
|
-
import {
|
|
21
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
function resolveChecksPlPath() {
|
|
24
24
|
const overrideChecksPath = process.env.KIBI_CHECKS_PL_PATH;
|
|
@@ -38,6 +38,23 @@ function resolveChecksPlPath() {
|
|
|
38
38
|
}
|
|
39
39
|
throw new Error("Unable to resolve checks.pl path");
|
|
40
40
|
}
|
|
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
|
+
};
|
|
41
58
|
function formatDiagnosticsForMcp(diagnostics) {
|
|
42
59
|
return diagnostics.map((d) => ({
|
|
43
60
|
category: d.category,
|
|
@@ -47,85 +64,96 @@ function formatDiagnosticsForMcp(diagnostics) {
|
|
|
47
64
|
suggestion: d.suggestion,
|
|
48
65
|
}));
|
|
49
66
|
}
|
|
67
|
+
// implements REQ-002
|
|
68
|
+
function loadChecksConfig(workspaceRoot) {
|
|
69
|
+
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
70
|
+
if (!existsSync(configPath)) {
|
|
71
|
+
return DEFAULT_CHECKS_CONFIG;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const content = readFileSync(configPath, "utf8");
|
|
75
|
+
const parsed = JSON.parse(content);
|
|
76
|
+
const parsedRules = parsed.checks?.rules;
|
|
77
|
+
const normalizedRules = {
|
|
78
|
+
...DEFAULT_CHECKS_CONFIG.rules,
|
|
79
|
+
};
|
|
80
|
+
if (parsedRules && typeof parsedRules === "object") {
|
|
81
|
+
for (const [key, value] of Object.entries(parsedRules)) {
|
|
82
|
+
if (typeof value === "boolean") {
|
|
83
|
+
normalizedRules[key] = value;
|
|
84
|
+
}
|
|
85
|
+
// Ignore non-boolean values (they are not added to normalizedRules, preserving defaults)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const parsedSt = parsed.checks?.symbolTraceability;
|
|
89
|
+
const normalizedSt = { ...DEFAULT_CHECKS_CONFIG.symbolTraceability };
|
|
90
|
+
if (parsedSt && typeof parsedSt === "object") {
|
|
91
|
+
if (typeof parsedSt.requireAdr === "boolean") {
|
|
92
|
+
normalizedSt.requireAdr = parsedSt.requireAdr;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
rules: normalizedRules,
|
|
97
|
+
symbolTraceability: normalizedSt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return DEFAULT_CHECKS_CONFIG;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// implements REQ-002
|
|
105
|
+
function getEffectiveRules(configRules, requestedRules) {
|
|
106
|
+
const effective = new Set();
|
|
107
|
+
for (const rule of ALL_RULES) {
|
|
108
|
+
const enabled = configRules[rule] ?? true;
|
|
109
|
+
if (enabled) {
|
|
110
|
+
effective.add(rule);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
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
|
+
return effective;
|
|
122
|
+
}
|
|
50
123
|
/**
|
|
51
124
|
* Handle kb_check tool calls - run validation rules on the KB
|
|
52
125
|
* Reuses validation logic from CLI check command
|
|
53
126
|
*/
|
|
127
|
+
// implements REQ-002
|
|
54
128
|
export async function handleKbCheck(prolog, args) {
|
|
55
129
|
const { rules } = args;
|
|
56
130
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"must-priority-coverage",
|
|
62
|
-
"no-dangling-refs",
|
|
63
|
-
"no-cycles",
|
|
64
|
-
"required-fields",
|
|
65
|
-
"symbol-coverage",
|
|
66
|
-
"symbol-traceability",
|
|
67
|
-
"deprecated-adr-no-successor",
|
|
68
|
-
"domain-contradictions",
|
|
69
|
-
];
|
|
70
|
-
const rulesToRun = rules && rules.length > 0 ? rules : allRules;
|
|
71
|
-
const rulesAllowlist = new Set(rulesToRun);
|
|
72
|
-
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist);
|
|
73
|
-
if (aggregatedViolations) {
|
|
74
|
-
const diagnostics = aggregatedViolations.map((v) => ({
|
|
75
|
-
category: "SYNC_ERROR",
|
|
76
|
-
severity: "error",
|
|
77
|
-
message: v.description,
|
|
78
|
-
file: v.source,
|
|
79
|
-
suggestion: v.suggestion,
|
|
80
|
-
}));
|
|
81
|
-
const summary = aggregatedViolations.length === 0
|
|
82
|
-
? "No violations found"
|
|
83
|
-
: `${aggregatedViolations.length} violations found`;
|
|
131
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
132
|
+
const checksConfig = loadChecksConfig(workspaceRoot);
|
|
133
|
+
const rulesAllowlist = getEffectiveRules(checksConfig.rules, rules);
|
|
134
|
+
if (rulesAllowlist.size === 0) {
|
|
84
135
|
return {
|
|
85
|
-
content: [
|
|
86
|
-
{
|
|
87
|
-
type: "text",
|
|
88
|
-
text: summary,
|
|
89
|
-
},
|
|
90
|
-
],
|
|
136
|
+
content: [{ type: "text", text: "No violations found" }],
|
|
91
137
|
structuredContent: {
|
|
92
|
-
violations:
|
|
93
|
-
count:
|
|
94
|
-
diagnostics:
|
|
138
|
+
violations: [],
|
|
139
|
+
count: 0,
|
|
140
|
+
diagnostics: [],
|
|
95
141
|
},
|
|
96
142
|
};
|
|
97
143
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
violations.push(...(await checkNoDanglingRefs(prolog)));
|
|
103
|
-
}
|
|
104
|
-
if (rulesToRun.includes("no-cycles")) {
|
|
105
|
-
violations.push(...(await checkNoCycles(prolog)));
|
|
106
|
-
}
|
|
107
|
-
if (rulesToRun.includes("required-fields")) {
|
|
108
|
-
if (!allEntityIds) {
|
|
109
|
-
allEntityIds = await getAllEntityIds(prolog);
|
|
110
|
-
}
|
|
111
|
-
violations.push(...(await checkRequiredFields(prolog, allEntityIds)));
|
|
112
|
-
}
|
|
113
|
-
if (rulesToRun.includes("symbol-coverage")) {
|
|
114
|
-
violations.push(...(await checkSymbolCoverage(prolog)));
|
|
115
|
-
}
|
|
116
|
-
if (rulesToRun.includes("symbol-traceability")) {
|
|
117
|
-
violations.push(...(await checkSymbolTraceability(prolog, false)));
|
|
118
|
-
}
|
|
119
|
-
const diagnostics = violations.map((v) => ({
|
|
144
|
+
// Run aggregated checks using same approach as CLI
|
|
145
|
+
// This now runs ALL rules including symbol-traceability
|
|
146
|
+
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist, checksConfig.symbolTraceability.requireAdr);
|
|
147
|
+
const diagnostics = aggregatedViolations.map((v) => ({
|
|
120
148
|
category: "SYNC_ERROR",
|
|
121
149
|
severity: "error",
|
|
122
150
|
message: v.description,
|
|
123
151
|
file: v.source,
|
|
124
152
|
suggestion: v.suggestion,
|
|
125
153
|
}));
|
|
126
|
-
const summary =
|
|
154
|
+
const summary = aggregatedViolations.length === 0
|
|
127
155
|
? "No violations found"
|
|
128
|
-
: `${
|
|
156
|
+
: `${aggregatedViolations.length} violations found`;
|
|
129
157
|
return {
|
|
130
158
|
content: [
|
|
131
159
|
{
|
|
@@ -134,8 +162,8 @@ export async function handleKbCheck(prolog, args) {
|
|
|
134
162
|
},
|
|
135
163
|
],
|
|
136
164
|
structuredContent: {
|
|
137
|
-
violations,
|
|
138
|
-
count:
|
|
165
|
+
violations: aggregatedViolations,
|
|
166
|
+
count: aggregatedViolations.length,
|
|
139
167
|
diagnostics: formatDiagnosticsForMcp(diagnostics),
|
|
140
168
|
},
|
|
141
169
|
};
|
|
@@ -145,341 +173,49 @@ export async function handleKbCheck(prolog, args) {
|
|
|
145
173
|
throw new Error(`Check execution failed: ${message}`);
|
|
146
174
|
}
|
|
147
175
|
}
|
|
148
|
-
|
|
176
|
+
// implements REQ-002
|
|
177
|
+
async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
|
|
178
|
+
const violations = [];
|
|
149
179
|
const checksPlPath = resolveChecksPlPath();
|
|
150
180
|
const normalizedChecksPlPath = checksPlPath.replace(/\\/g, "/");
|
|
151
181
|
const checksPlPathEscaped = normalizedChecksPlPath.replace(/'/g, "''");
|
|
152
|
-
|
|
153
|
-
const ruleToPredicate = {
|
|
154
|
-
"must-priority-coverage": "check_must_priority_coverage",
|
|
155
|
-
"no-dangling-refs": "check_no_dangling_refs",
|
|
156
|
-
"no-cycles": "check_no_cycles",
|
|
157
|
-
"required-fields": "check_required_fields",
|
|
158
|
-
"symbol-coverage": "check_symbol_coverage",
|
|
159
|
-
};
|
|
160
|
-
for (const rule of rulesAllowlist) {
|
|
161
|
-
const predicate = ruleToPredicate[rule];
|
|
162
|
-
if (!predicate) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const query = `(use_module('${checksPlPathEscaped}'), call(checks:${predicate}(Violations)), findall(_{rule:Rule,entityId:EntityId,description:Description,suggestion:Suggestion,source:Source}, member(violation(Rule, EntityId, Description, Suggestion, Source), Violations), Rows), call(checks:atom_json_dict(JsonString, Rows, [])))`;
|
|
166
|
-
const result = await prolog.query(query);
|
|
167
|
-
if (!result.success || !result.bindings.JsonString) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
let parsedRows;
|
|
171
|
-
try {
|
|
172
|
-
parsedRows = JSON.parse(result.bindings.JsonString);
|
|
173
|
-
if (typeof parsedRows === "string") {
|
|
174
|
-
parsedRows = JSON.parse(parsedRows);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
if (!Array.isArray(parsedRows)) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
for (const row of parsedRows) {
|
|
184
|
-
if (!row || typeof row !== "object") {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const raw = row;
|
|
188
|
-
const rule = typeof raw.rule === "string" ? raw.rule : "";
|
|
189
|
-
if (!rulesAllowlist.has(rule)) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
const entityId = typeof raw.entityId === "string"
|
|
193
|
-
? raw.entityId
|
|
194
|
-
: typeof raw.entity_id === "string"
|
|
195
|
-
? raw.entity_id
|
|
196
|
-
: "";
|
|
197
|
-
const description = typeof raw.description === "string" ? raw.description : "";
|
|
198
|
-
const suggestion = typeof raw.suggestion === "string" ? raw.suggestion : undefined;
|
|
199
|
-
const source = typeof raw.source === "string" ? raw.source : undefined;
|
|
200
|
-
if (!rule || !entityId || !description) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
violations.push({
|
|
204
|
-
rule,
|
|
205
|
-
entityId,
|
|
206
|
-
description,
|
|
207
|
-
suggestion,
|
|
208
|
-
source,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return violations;
|
|
213
|
-
}
|
|
214
|
-
async function checkSymbolTraceability(prolog, requireAdr) {
|
|
215
|
-
const violations = [];
|
|
182
|
+
// Use check_all_json_with_options if available, otherwise fall back to check_all_json
|
|
216
183
|
const requireAdrStr = requireAdr ? "true" : "false";
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (violationsStr && violationsStr !== "[]") {
|
|
223
|
-
const violationRegex = /violation\(([^,]+),'?([^',]+)'?,([^,]+),([^,]+),'?([^']*)'?\)/g;
|
|
224
|
-
let match;
|
|
225
|
-
do {
|
|
226
|
-
match = violationRegex.exec(violationsStr);
|
|
227
|
-
if (match) {
|
|
228
|
-
violations.push({
|
|
229
|
-
rule: match[1].trim().replace(/^'|'$/g, ""),
|
|
230
|
-
entityId: match[2].trim(),
|
|
231
|
-
description: match[3].trim().replace(/^"|"$/g, ""),
|
|
232
|
-
suggestion: match[4].trim().replace(/^"|"$/g, ""),
|
|
233
|
-
source: match[5].trim() || undefined,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
} while (match);
|
|
237
|
-
}
|
|
238
|
-
return violations;
|
|
239
|
-
}
|
|
240
|
-
async function checkMustPriorityCoverage(prolog) {
|
|
241
|
-
const violations = [];
|
|
242
|
-
const gapsResult = await prolog.query("setof([Req,Reason], coverage_gap(Req, Reason), Rows)");
|
|
243
|
-
if (!gapsResult.success || !gapsResult.bindings.Rows) {
|
|
244
|
-
return violations;
|
|
245
|
-
}
|
|
246
|
-
const gaps = parsePairList(gapsResult.bindings.Rows);
|
|
247
|
-
for (const [reqId, reason] of gaps) {
|
|
248
|
-
const entityResult = await prolog.query(`kb_entity('${reqId}', req, Props)`);
|
|
249
|
-
let source = "";
|
|
250
|
-
if (entityResult.success && entityResult.bindings.Props) {
|
|
251
|
-
const propsStr = entityResult.bindings.Props;
|
|
252
|
-
const sourceMatch = propsStr.match(/source\s*=\s*\^\^?\("([^"]+)"/);
|
|
253
|
-
if (sourceMatch) {
|
|
254
|
-
source = sourceMatch[1];
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const missing = [];
|
|
258
|
-
if (reason.includes("scenario")) {
|
|
259
|
-
missing.push("scenario");
|
|
260
|
-
}
|
|
261
|
-
if (reason.includes("test")) {
|
|
262
|
-
missing.push("test");
|
|
263
|
-
}
|
|
264
|
-
violations.push({
|
|
265
|
-
rule: "must-priority-coverage",
|
|
266
|
-
entityId: reqId,
|
|
267
|
-
description: `Must-priority requirement lacks ${missing.join(" and ")} coverage`,
|
|
268
|
-
source,
|
|
269
|
-
suggestion: missing
|
|
270
|
-
.map((m) => `Create ${m} that covers this requirement`)
|
|
271
|
-
.join("; "),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return violations;
|
|
275
|
-
}
|
|
276
|
-
async function getAllEntityIds(prolog, type) {
|
|
277
|
-
const typeFilter = type ? `, Type = ${type}` : "";
|
|
278
|
-
const query = `findall(Id, (kb_entity(Id, Type, _)${typeFilter}), Ids)`;
|
|
184
|
+
const query = `(use_module('${checksPlPathEscaped}'),
|
|
185
|
+
( predicate_property(checks:check_all_json_with_options(_, _), _)
|
|
186
|
+
-> call(checks:check_all_json_with_options(JsonString, ${requireAdrStr}))
|
|
187
|
+
; call(checks:check_all_json(JsonString))
|
|
188
|
+
))`;
|
|
279
189
|
const result = await prolog.query(query);
|
|
280
|
-
if (!result.success
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
const idsStr = result.bindings.Ids;
|
|
284
|
-
const match = idsStr.match(/\[(.*)\]/);
|
|
285
|
-
if (!match) {
|
|
286
|
-
return [];
|
|
287
|
-
}
|
|
288
|
-
const content = match[1].trim();
|
|
289
|
-
if (!content) {
|
|
290
|
-
return [];
|
|
190
|
+
if (!result.success) {
|
|
191
|
+
throw new Error(`Aggregated checks query failed: ${result.error || "Unknown error"}`);
|
|
291
192
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const allEntityIds = new Set(await getAllEntityIds(prolog));
|
|
298
|
-
// Get all relationships by querying all known relationship types
|
|
299
|
-
const relTypes = [
|
|
300
|
-
"depends_on",
|
|
301
|
-
"verified_by",
|
|
302
|
-
"validates",
|
|
303
|
-
"specified_by",
|
|
304
|
-
"relates_to",
|
|
305
|
-
];
|
|
306
|
-
const allRels = [];
|
|
307
|
-
for (const relType of relTypes) {
|
|
308
|
-
const relsResult = await prolog.query(`findall([From,To], kb_relationship(${relType}, From, To), Rels)`);
|
|
309
|
-
if (relsResult.success && relsResult.bindings.Rels) {
|
|
310
|
-
const relsStr = relsResult.bindings.Rels;
|
|
311
|
-
const match = relsStr.match(/\[(.*)\]/);
|
|
312
|
-
if (match) {
|
|
313
|
-
const content = match[1].trim();
|
|
314
|
-
if (content) {
|
|
315
|
-
const relMatches = content.matchAll(/\[([^,]+),([^\]]+)\]/g);
|
|
316
|
-
for (const relMatch of relMatches) {
|
|
317
|
-
const fromId = relMatch[1].trim().replace(/^'|'$/g, "");
|
|
318
|
-
const toId = relMatch[2].trim().replace(/^'|'$/g, "");
|
|
319
|
-
allRels.push({ from: fromId, to: toId });
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Check all collected relationships for dangling refs
|
|
326
|
-
for (const rel of allRels) {
|
|
327
|
-
if (!allEntityIds.has(rel.from)) {
|
|
328
|
-
violations.push({
|
|
329
|
-
rule: "no-dangling-refs",
|
|
330
|
-
entityId: rel.from,
|
|
331
|
-
description: `Relationship references non-existent entity: ${rel.from}`,
|
|
332
|
-
suggestion: "Remove relationship or create missing entity",
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
if (!allEntityIds.has(rel.to)) {
|
|
336
|
-
violations.push({
|
|
337
|
-
rule: "no-dangling-refs",
|
|
338
|
-
entityId: rel.to,
|
|
339
|
-
description: `Relationship references non-existent entity: ${rel.to}`,
|
|
340
|
-
suggestion: "Remove relationship or create missing entity",
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return violations;
|
|
345
|
-
}
|
|
346
|
-
async function checkNoCycles(prolog) {
|
|
347
|
-
const violations = [];
|
|
348
|
-
// Get all depends_on relationships
|
|
349
|
-
const depsResult = await prolog.query("findall([From,To], kb_relationship(depends_on, From, To), Deps)");
|
|
350
|
-
if (!depsResult.success || !depsResult.bindings.Deps) {
|
|
351
|
-
return violations;
|
|
352
|
-
}
|
|
353
|
-
const depsStr = depsResult.bindings.Deps;
|
|
354
|
-
const match = depsStr.match(/\[(.*)\]/);
|
|
355
|
-
if (!match) {
|
|
356
|
-
return violations;
|
|
357
|
-
}
|
|
358
|
-
const content = match[1].trim();
|
|
359
|
-
if (!content) {
|
|
360
|
-
return violations;
|
|
361
|
-
}
|
|
362
|
-
// Build adjacency map
|
|
363
|
-
const graph = new Map();
|
|
364
|
-
const depMatches = content.matchAll(/\[([^,]+),([^\]]+)\]/g);
|
|
365
|
-
for (const depMatch of depMatches) {
|
|
366
|
-
const from = depMatch[1].trim().replace(/^'|'$/g, "");
|
|
367
|
-
const to = depMatch[2].trim().replace(/^'|'$/g, "");
|
|
368
|
-
if (!graph.has(from)) {
|
|
369
|
-
graph.set(from, []);
|
|
193
|
+
let violationsDict;
|
|
194
|
+
try {
|
|
195
|
+
const jsonString = result.bindings.JsonString;
|
|
196
|
+
if (!jsonString) {
|
|
197
|
+
throw new Error("No JSON string in binding");
|
|
370
198
|
}
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
199
|
+
let parsed = JSON.parse(jsonString);
|
|
200
|
+
if (typeof parsed === "string") {
|
|
201
|
+
parsed = JSON.parse(parsed);
|
|
374
202
|
}
|
|
203
|
+
violationsDict = parsed;
|
|
375
204
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const recStack = new Set();
|
|
379
|
-
function hasCycleDFS(node, path) {
|
|
380
|
-
visited.add(node);
|
|
381
|
-
recStack.add(node);
|
|
382
|
-
path.push(node);
|
|
383
|
-
const neighbors = graph.get(node) || [];
|
|
384
|
-
for (const neighbor of neighbors) {
|
|
385
|
-
if (!visited.has(neighbor)) {
|
|
386
|
-
const cyclePath = hasCycleDFS(neighbor, [...path]);
|
|
387
|
-
if (cyclePath)
|
|
388
|
-
return cyclePath;
|
|
389
|
-
}
|
|
390
|
-
else if (recStack.has(neighbor)) {
|
|
391
|
-
// Cycle detected
|
|
392
|
-
return [...path, neighbor];
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
recStack.delete(node);
|
|
396
|
-
return null;
|
|
205
|
+
catch (parseError) {
|
|
206
|
+
throw new Error(`Failed to parse violations JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
397
207
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (cyclePath) {
|
|
403
|
-
const cycleWithSources = [];
|
|
404
|
-
for (const entityId of cyclePath) {
|
|
405
|
-
const entityResult = await prolog.query(`kb_entity('${entityId}', _, Props)`);
|
|
406
|
-
let sourceName = entityId;
|
|
407
|
-
if (entityResult.success && entityResult.bindings.Props) {
|
|
408
|
-
const propsStr = entityResult.bindings.Props;
|
|
409
|
-
const sourceMatch = propsStr.match(/source\s*=\s*\^\^?\("([^"]+)"/);
|
|
410
|
-
if (sourceMatch) {
|
|
411
|
-
sourceName = path.basename(sourceMatch[1], ".md");
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
cycleWithSources.push(sourceName);
|
|
415
|
-
}
|
|
208
|
+
for (const ruleViolations of Object.values(violationsDict)) {
|
|
209
|
+
for (const v of ruleViolations) {
|
|
210
|
+
const isAllowed = rulesAllowlist.has(v.rule);
|
|
211
|
+
if (isAllowed) {
|
|
416
212
|
violations.push({
|
|
417
|
-
rule:
|
|
418
|
-
entityId:
|
|
419
|
-
description:
|
|
420
|
-
suggestion:
|
|
213
|
+
rule: v.rule,
|
|
214
|
+
entityId: v.entityId,
|
|
215
|
+
description: v.description,
|
|
216
|
+
suggestion: v.suggestion || undefined,
|
|
217
|
+
source: v.source || undefined,
|
|
421
218
|
});
|
|
422
|
-
break; // Report only first cycle found
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return violations;
|
|
427
|
-
}
|
|
428
|
-
async function checkRequiredFields(prolog, allEntityIds) {
|
|
429
|
-
const violations = [];
|
|
430
|
-
const required = [
|
|
431
|
-
"id",
|
|
432
|
-
"title",
|
|
433
|
-
"status",
|
|
434
|
-
"created_at",
|
|
435
|
-
"updated_at",
|
|
436
|
-
"source",
|
|
437
|
-
];
|
|
438
|
-
for (const entityId of allEntityIds) {
|
|
439
|
-
const result = await prolog.query(`kb_entity('${entityId}', Type, Props)`);
|
|
440
|
-
if (result.success && result.bindings.Props) {
|
|
441
|
-
// Parse properties list: [key1=value1, key2=value2, ...]
|
|
442
|
-
const propsStr = result.bindings.Props;
|
|
443
|
-
const propKeys = new Set();
|
|
444
|
-
// Extract keys from Props
|
|
445
|
-
const keyMatches = propsStr.matchAll(/(\w+)\s*=/g);
|
|
446
|
-
for (const match of keyMatches) {
|
|
447
|
-
propKeys.add(match[1]);
|
|
448
|
-
}
|
|
449
|
-
// Check for missing required fields
|
|
450
|
-
for (const field of required) {
|
|
451
|
-
if (!propKeys.has(field)) {
|
|
452
|
-
violations.push({
|
|
453
|
-
rule: "required-fields",
|
|
454
|
-
entityId: entityId,
|
|
455
|
-
description: `Missing required field: ${field}`,
|
|
456
|
-
suggestion: `Add ${field} to entity definition`,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return violations;
|
|
463
|
-
}
|
|
464
|
-
async function checkSymbolCoverage(prolog) {
|
|
465
|
-
const violations = [];
|
|
466
|
-
const uncoveredResult = await prolog.query("setof(Symbol, (kb_entity(Symbol, symbol, _), \\+ transitively_implements(Symbol, _)), Symbols)");
|
|
467
|
-
if (uncoveredResult.success && uncoveredResult.bindings.Symbols) {
|
|
468
|
-
const symbolsStr = uncoveredResult.bindings.Symbols;
|
|
469
|
-
const match = symbolsStr.match(/\[(.*)\]/);
|
|
470
|
-
if (match) {
|
|
471
|
-
const content = match[1].trim();
|
|
472
|
-
if (content) {
|
|
473
|
-
const symbolMatches = content.matchAll(/'([^']+)'/g);
|
|
474
|
-
for (const symbolMatch of symbolMatches) {
|
|
475
|
-
const symbolId = symbolMatch[1];
|
|
476
|
-
violations.push({
|
|
477
|
-
rule: "symbol-coverage",
|
|
478
|
-
entityId: symbolId,
|
|
479
|
-
description: "Code symbol is not traceable to any functional requirement.",
|
|
480
|
-
suggestion: "Update symbols.yaml to link this symbol to a related requirement.",
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
219
|
}
|
|
484
220
|
}
|
|
485
221
|
}
|
package/dist/tools/delete.js
CHANGED
|
@@ -51,7 +51,10 @@ export async function handleKbDelete(prolog, args) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
// Save KB to disk
|
|
54
|
-
await prolog.query("kb_save");
|
|
54
|
+
const saveResult = await prolog.query("kb_save");
|
|
55
|
+
if (!saveResult.success) {
|
|
56
|
+
throw new Error(`Failed to save KB after delete: ${saveResult.error || "Unknown error"}`);
|
|
57
|
+
}
|
|
55
58
|
prolog.invalidateCache();
|
|
56
59
|
return {
|
|
57
60
|
content: [
|
package/dist/tools/query.js
CHANGED
|
@@ -45,9 +45,7 @@ export async function handleKbQuery(prolog, args) {
|
|
|
45
45
|
goal = `findall(['${safeId}',Type,Props], kb_entity('${safeId}', Type, Props), Results)`;
|
|
46
46
|
}
|
|
47
47
|
else if (tags && tags.length > 0) {
|
|
48
|
-
//
|
|
49
|
-
// issues with tag list formats are resolved, to avoid fetching all entities
|
|
50
|
-
// before filtering in JS for large knowledge bases.
|
|
48
|
+
// JS-side fallback until REQ-mcp-tag-filtering-server-side is implemented.
|
|
51
49
|
if (type) {
|
|
52
50
|
const safeType = escapeAtomContent(type);
|
|
53
51
|
goal = `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
|
package/dist/tools/upsert.js
CHANGED
|
@@ -27,6 +27,7 @@ const validateRelationship = ajv.compile(relationshipSchema);
|
|
|
27
27
|
* Handle kb.upsert tool calls
|
|
28
28
|
* Accepts { type, id, properties } — the flat format matching the tool schema.
|
|
29
29
|
* Validates the assembled entity against JSON Schema before Prolog writes.
|
|
30
|
+
* implements REQ-002, REQ-011
|
|
30
31
|
*/
|
|
31
32
|
export async function handleKbUpsert(prolog, args) {
|
|
32
33
|
const { type, id, properties, relationships = [] } = args;
|
|
@@ -80,49 +81,56 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
80
81
|
for (const entity of entities) {
|
|
81
82
|
const id = entity.id;
|
|
82
83
|
const type = entity.type;
|
|
83
|
-
// Check if entity exists
|
|
84
|
+
// Check if entity exists before transaction (to determine created vs updated)
|
|
84
85
|
const checkGoal = `once(kb_entity('${escapeAtom(id)}', _, _))`;
|
|
85
86
|
const checkResult = await prolog.query(checkGoal);
|
|
86
87
|
const isUpdate = checkResult.success;
|
|
87
88
|
// Build property list for Prolog
|
|
88
89
|
const props = buildPropertyList(entity);
|
|
89
|
-
//
|
|
90
|
+
// Build relationship goals
|
|
91
|
+
const relationshipGoals = [];
|
|
92
|
+
for (const rel of relationships) {
|
|
93
|
+
const relType = rel.type;
|
|
94
|
+
const from = rel.from;
|
|
95
|
+
const to = rel.to;
|
|
96
|
+
const metadata = buildRelationshipMetadata(rel);
|
|
97
|
+
relationshipGoals.push(`kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
|
|
98
|
+
}
|
|
99
|
+
// Build atomic transaction goal wrapping entity + all relationships
|
|
100
|
+
// implements REQ-002, REQ-011
|
|
101
|
+
let transactionGoal;
|
|
102
|
+
if (relationshipGoals.length === 0) {
|
|
103
|
+
// Simple case: just entity
|
|
104
|
+
transactionGoal = `rdf_transaction((kb_assert_entity(${type}, ${props})))`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Entity + relationships in one transaction
|
|
108
|
+
const goals = [
|
|
109
|
+
`kb_assert_entity(${type}, ${props})`,
|
|
110
|
+
...relationshipGoals,
|
|
111
|
+
].join(", ");
|
|
112
|
+
transactionGoal = `rdf_transaction((${goals}))`;
|
|
113
|
+
}
|
|
114
|
+
const txResult = await prolog.query(transactionGoal);
|
|
115
|
+
if (!txResult.success) {
|
|
116
|
+
throw new Error(`Failed to upsert entity ${id}: ${txResult.error || "Unknown error"} (goal: ${transactionGoal})`);
|
|
117
|
+
}
|
|
118
|
+
// Update counters
|
|
90
119
|
if (isUpdate) {
|
|
91
|
-
// Update counter only. kb_assert_entity implements upsert semantics in Prolog.
|
|
92
120
|
updated++;
|
|
93
121
|
}
|
|
94
122
|
else {
|
|
95
123
|
created++;
|
|
96
124
|
}
|
|
97
|
-
|
|
98
|
-
const assertResult = await prolog.query(assertGoal);
|
|
99
|
-
if (!assertResult.success) {
|
|
100
|
-
throw new Error(`Failed to assert entity ${id}: ${assertResult.error || "Unknown error"}`);
|
|
101
|
-
}
|
|
125
|
+
relationshipsCreated += relationships.length;
|
|
102
126
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const relType = rel.type;
|
|
106
|
-
const from = rel.from;
|
|
107
|
-
const to = rel.to;
|
|
108
|
-
// Build metadata
|
|
109
|
-
const metadata = buildRelationshipMetadata(rel);
|
|
110
|
-
const relGoal = `kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`;
|
|
111
|
-
const relResult = await prolog.query(relGoal);
|
|
112
|
-
if (!relResult.success) {
|
|
113
|
-
throw new Error(`Failed to assert relationship ${relType} from ${from} to ${to}: ${relResult.error || "Unknown error"}`);
|
|
114
|
-
}
|
|
115
|
-
relationshipsCreated++;
|
|
116
|
-
}
|
|
117
|
-
// Note: kb_save is intentionally NOT called here for performance.
|
|
118
|
-
// Callers that need durability across restarts should explicitly call kb_save.
|
|
119
|
-
// This allows batching multiple upserts before a single disk write.
|
|
120
|
-
prolog.invalidateCache();
|
|
121
|
-
// Save KB to disk to ensure durability across process restarts
|
|
122
|
-
await prolog.query("kb_save");
|
|
123
|
-
prolog.invalidateCache();
|
|
124
|
-
// multiple upserts and save once at the end for better performance.
|
|
127
|
+
// Save KB to disk after all entities/relationships are written to ensure
|
|
128
|
+
// durability across process restarts.
|
|
125
129
|
prolog.invalidateCache();
|
|
130
|
+
const saveResult = await prolog.query("kb_save");
|
|
131
|
+
if (!saveResult.success) {
|
|
132
|
+
throw new Error(`Failed to save KB after upsert: ${saveResult.error || "Unknown error"}`);
|
|
133
|
+
}
|
|
126
134
|
let contradictionPairsDetected;
|
|
127
135
|
if (type === "req" && !args._skipContradictionCheck) {
|
|
128
136
|
contradictionPairsDetected = await detectContradictionPairs(prolog, id);
|
package/dist/tools-config.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
|
|
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
|
-
*/
|
|
18
|
-
|
|
17
|
+
*/
|
|
18
|
+
import { DIAGNOSTIC_MODE_ENABLED, DIAGNOSTIC_TELEMETRY_SCHEMA, } from "./diagnostics.js";
|
|
19
|
+
const BASE_TOOLS = [
|
|
19
20
|
// implements REQ-002
|
|
20
21
|
{
|
|
21
22
|
name: "kb_query",
|
|
@@ -216,3 +217,31 @@ export const TOOLS = [
|
|
|
216
217
|
},
|
|
217
218
|
},
|
|
218
219
|
];
|
|
220
|
+
/**
|
|
221
|
+
* Inject _diagnostic_telemetry schema into tool inputs when diagnostic mode is enabled.
|
|
222
|
+
*/
|
|
223
|
+
function withDiagnosticTelemetrySchema(tools) {
|
|
224
|
+
return tools.map((tool) => {
|
|
225
|
+
const schema = tool.inputSchema;
|
|
226
|
+
const properties = schema.properties && typeof schema.properties === "object"
|
|
227
|
+
? schema.properties
|
|
228
|
+
: {};
|
|
229
|
+
return {
|
|
230
|
+
...tool,
|
|
231
|
+
inputSchema: {
|
|
232
|
+
...schema,
|
|
233
|
+
properties: {
|
|
234
|
+
...properties,
|
|
235
|
+
_diagnostic_telemetry: DIAGNOSTIC_TELEMETRY_SCHEMA,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Active tools list.
|
|
243
|
+
* In diagnostic mode, all tools include the _diagnostic_telemetry parameter.
|
|
244
|
+
*/
|
|
245
|
+
export const TOOLS = DIAGNOSTIC_MODE_ENABLED
|
|
246
|
+
? withDiagnosticTelemetrySchema(BASE_TOOLS)
|
|
247
|
+
: BASE_TOOLS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-cli": "^0.2.
|
|
12
|
+
"kibi-cli": "^0.2.7",
|
|
13
13
|
"kibi-core": "^0.1.10",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/bun": "latest",
|
|
43
|
-
"@types/node": "latest"
|
|
43
|
+
"@types/node": "latest",
|
|
44
|
+
"typescript": "^5.7.0"
|
|
44
45
|
},
|
|
45
46
|
"license": "AGPL-3.0-or-later",
|
|
46
47
|
"author": "Piotr Franczyk",
|