kibi-mcp 0.3.0 → 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.
@@ -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
- // Detach from old KB
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"}`);
@@ -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 { parsePairList } from "./prolog-list.js";
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 violations = [];
58
- let allEntityIds = null;
59
- // Run all validation rules (or specific rules if provided)
60
- const allRules = [
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: aggregatedViolations,
93
- count: aggregatedViolations.length,
94
- diagnostics: formatDiagnosticsForMcp(diagnostics),
138
+ violations: [],
139
+ count: 0,
140
+ diagnostics: [],
95
141
  },
96
142
  };
97
143
  }
98
- if (rulesToRun.includes("must-priority-coverage")) {
99
- violations.push(...(await checkMustPriorityCoverage(prolog)));
100
- }
101
- if (rulesToRun.includes("no-dangling-refs")) {
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 = violations.length === 0
154
+ const summary = aggregatedViolations.length === 0
127
155
  ? "No violations found"
128
- : `${violations.length} violations found`;
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: violations.length,
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
- async function runAggregatedChecks(prolog, rulesAllowlist) {
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
- const violations = [];
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 result = await prolog.query(`findall(violation(Rule, EntityId, Desc, Sugg, Src), checks:symbol_traceability_violation(${requireAdrStr}, violation(Rule, EntityId, Desc, Sugg, Src)), Violations)`);
218
- if (!result.success || !result.bindings.Violations) {
219
- return violations;
220
- }
221
- const violationsStr = result.bindings.Violations;
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 || !result.bindings.Ids) {
281
- return [];
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
- return content.split(",").map((id) => id.trim().replace(/^'|'$/g, ""));
293
- }
294
- async function checkNoDanglingRefs(prolog) {
295
- const violations = [];
296
- // Get all entity IDs once
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
- const fromList = graph.get(from);
372
- if (fromList) {
373
- fromList.push(to);
199
+ let parsed = JSON.parse(jsonString);
200
+ if (typeof parsed === "string") {
201
+ parsed = JSON.parse(parsed);
374
202
  }
203
+ violationsDict = parsed;
375
204
  }
376
- // DFS to detect cycles
377
- const visited = new Set();
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
- // Check each node for cycles
399
- for (const node of graph.keys()) {
400
- if (!visited.has(node)) {
401
- const cyclePath = hasCycleDFS(node, []);
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: "no-cycles",
418
- entityId: cyclePath[0],
419
- description: `Circular dependency detected: ${cycleWithSources.join(" → ")}`,
420
- suggestion: "Break the cycle by removing one of the depends_on relationships",
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
  }
@@ -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: [
@@ -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
+ // implements REQ-002
18
19
  export function parseAtomList(raw) {
19
20
  const trimmed = raw.trim();
20
21
  if (trimmed === "[]" || trimmed.length === 0) {
@@ -28,6 +29,7 @@ export function parseAtomList(raw) {
28
29
  .map((token) => stripQuotes(token.trim()))
29
30
  .filter((token) => token.length > 0);
30
31
  }
32
+ // implements REQ-002
31
33
  export function parsePairList(raw) {
32
34
  const rows = parseListRows(raw);
33
35
  const pairs = [];
@@ -39,17 +41,6 @@ export function parsePairList(raw) {
39
41
  }
40
42
  return pairs;
41
43
  }
42
- export function parseTriples(raw) {
43
- const rows = parseListRows(raw);
44
- const triples = [];
45
- for (const row of rows) {
46
- const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
47
- if (parts.length >= 3) {
48
- triples.push([parts[0], parts[1], parts[2]]);
49
- }
50
- }
51
- return triples;
52
- }
53
44
  function parseListRows(raw) {
54
45
  const trimmed = raw.trim();
55
46
  if (trimmed === "[]" || trimmed.length === 0) {