kibi-mcp 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/session.js +6 -1
- package/dist/tools/check.js +124 -388
- package/dist/tools/delete.js +4 -1
- package/dist/tools/upsert.js +38 -30
- package/package.json +2 -2
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/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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|