kibi-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,7 +89,8 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
89
89
  telemetry_status: telemetry ? "provided" : "missing",
90
90
  };
91
91
  const structuredContent = result && typeof result === "object" && "structuredContent" in result
92
- ? result.structuredContent
92
+ ? result
93
+ .structuredContent
93
94
  : undefined;
94
95
  if (toolName === "kb_query" || toolName === "kb_search") {
95
96
  const resultCount = Number(structuredContent?.count ?? 0);
@@ -103,9 +104,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
103
104
  fields.violation_count = violationCount;
104
105
  fields.requested_rules = Array.isArray(args.rules) ? args.rules : [];
105
106
  fields.result_summary =
106
- violationCount === 0
107
- ? "0 violations"
108
- : `${violationCount} violations`;
107
+ violationCount === 0 ? "0 violations" : `${violationCount} violations`;
109
108
  }
110
109
  if (!fields.result_summary) {
111
110
  fields.result_summary = `${toolName} completed`;
@@ -35,6 +35,7 @@ function renderToolsDoc() {
35
35
  return lines.join("\n");
36
36
  }
37
37
  export const PROMPTS = [
38
+ // implements REQ-002, REQ-013, REQ-mcp-search-discovery
38
39
  {
39
40
  name: "init-kibi",
40
41
  description: "Bootstrap Kibi on an existing repository with zero entities.",
@@ -196,7 +197,7 @@ function registerDocResources() {
196
197
  "## Discover before mutating",
197
198
  '1. `kb_search` with `{ "query": "login flow" }` to discover related requirements, tests, and ADRs',
198
199
  '2. `kb_query` with `{ "type": "req", "sourceFile": "src/auth/login.ts" }` for exact follow-up',
199
- '3. `kb_status` with `{}` when branch attachment or freshness confidence matters',
200
+ "3. `kb_status` with `{}` when branch attachment or freshness confidence matters",
200
201
  "",
201
202
  "## Model requirements as reusable facts",
202
203
  '1. `kb_query` with `{ "type": "fact" }` to find existing fact IDs before creating new ones',
@@ -18,9 +18,9 @@
18
18
  import process from "node:process";
19
19
  import { z } from "zod";
20
20
  import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
21
- import { handleKbCoverage } from "../tools/coverage.js";
22
21
  import { TOOLS } from "../tools-config.js";
23
22
  import { handleKbCheck } from "../tools/check.js";
23
+ import { handleKbCoverage } from "../tools/coverage.js";
24
24
  import { handleKbDelete } from "../tools/delete.js";
25
25
  import { handleKbFindGaps } from "../tools/find-gaps.js";
26
26
  import { handleKbGraph } from "../tools/graph.js";
@@ -18,6 +18,7 @@
18
18
  import { existsSync, readFileSync } from "node:fs";
19
19
  import { createRequire } from "node:module";
20
20
  import * as path from "node:path";
21
+ import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
21
22
  import { resolveWorkspaceRoot } from "../workspace.js";
22
23
  const require = createRequire(import.meta.url);
23
24
  function resolveChecksPlPath() {
@@ -31,30 +32,15 @@ function resolveChecksPlPath() {
31
32
  return installedChecksPl;
32
33
  }
33
34
  }
34
- catch { }
35
+ catch {
36
+ // require.resolve not available or package not installed
37
+ }
35
38
  const localChecksPl = path.join(process.cwd(), "packages/core/src/checks.pl");
36
39
  if (existsSync(localChecksPl)) {
37
40
  return localChecksPl;
38
41
  }
39
42
  throw new Error("Unable to resolve checks.pl path");
40
43
  }
41
- const ALL_RULES = [
42
- "must-priority-coverage",
43
- "no-dangling-refs",
44
- "no-cycles",
45
- "required-fields",
46
- "symbol-coverage",
47
- "symbol-traceability",
48
- "deprecated-adr-no-successor",
49
- "domain-contradictions",
50
- ];
51
- const RULE_NAMES = new Set(ALL_RULES);
52
- const DEFAULT_CHECKS_CONFIG = {
53
- rules: Object.fromEntries(ALL_RULES.map((rule) => [rule, true])),
54
- symbolTraceability: {
55
- requireAdr: false,
56
- },
57
- };
58
44
  function formatDiagnosticsForMcp(diagnostics) {
59
45
  return diagnostics.map((d) => ({
60
46
  category: d.category,
@@ -121,21 +107,16 @@ function loadChecksConfig(workspaceRoot) {
121
107
  }
122
108
  // implements REQ-002
123
109
  function getEffectiveRules(configRules, requestedRules) {
110
+ if (requestedRules && requestedRules.length > 0) {
111
+ return new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
112
+ }
124
113
  const effective = new Set();
125
- for (const rule of ALL_RULES) {
114
+ for (const rule of RULE_NAMES) {
126
115
  const enabled = configRules[rule] ?? true;
127
116
  if (enabled) {
128
117
  effective.add(rule);
129
118
  }
130
119
  }
131
- if (requestedRules && requestedRules.length > 0) {
132
- const allowed = new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
133
- for (const rule of Array.from(effective)) {
134
- if (!allowed.has(rule)) {
135
- effective.delete(rule);
136
- }
137
- }
138
- }
139
120
  return effective;
140
121
  }
141
122
  /**
@@ -17,7 +17,9 @@ export function resolveCorePlPath(fileName) {
17
17
  return installedPath;
18
18
  }
19
19
  }
20
- catch { }
20
+ catch {
21
+ // require.resolve not available or package not installed
22
+ }
21
23
  const localPath = path.join(process.cwd(), "packages/core/src", fileName);
22
24
  if (existsSync(localPath)) {
23
25
  return localPath;
@@ -1,5 +1,6 @@
1
1
  import { escapeAtomContent, parseEntityFromBinding, parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
2
2
  export const VALID_ENTITY_TYPES = [
3
+ // implements REQ-002
3
4
  "req",
4
5
  "scenario",
5
6
  "test",
@@ -1,4 +1,4 @@
1
- import { runJsonModuleQuery, toPrologAtom, toPrologList } from "./core-module.js";
1
+ import { runJsonModuleQuery, toPrologAtom, toPrologList, } from "./core-module.js";
2
2
  import { validateEntityType } from "./entity-query.js";
3
3
  // implements REQ-002, REQ-013
4
4
  export async function handleKbFindGaps(prolog, args) {
@@ -15,6 +15,7 @@
15
15
  You should have received a copy of the GNU Affero General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
+ import { splitTopLevel } from "kibi-cli/prolog/codec";
18
19
  // implements REQ-002
19
20
  export function parseAtomList(raw) {
20
21
  const trimmed = raw.trim();
@@ -86,49 +87,6 @@ function unwrapList(value) {
86
87
  }
87
88
  return value;
88
89
  }
89
- function splitTopLevel(input, delimiter) {
90
- const parts = [];
91
- let current = "";
92
- let depth = 0;
93
- let inDoubleQuotes = false;
94
- let inSingleQuotes = false;
95
- for (let i = 0; i < input.length; i++) {
96
- const ch = input[i];
97
- const prev = i > 0 ? input[i - 1] : "";
98
- if (ch === '"' && !inSingleQuotes && prev !== "\\") {
99
- inDoubleQuotes = !inDoubleQuotes;
100
- current += ch;
101
- continue;
102
- }
103
- if (ch === "'" && !inDoubleQuotes && prev !== "\\") {
104
- inSingleQuotes = !inSingleQuotes;
105
- current += ch;
106
- continue;
107
- }
108
- if (!inSingleQuotes && !inDoubleQuotes && (ch === "[" || ch === "(")) {
109
- depth++;
110
- current += ch;
111
- continue;
112
- }
113
- if (!inSingleQuotes && !inDoubleQuotes && (ch === "]" || ch === ")")) {
114
- depth--;
115
- current += ch;
116
- continue;
117
- }
118
- if (!inSingleQuotes && !inDoubleQuotes && depth === 0 && ch === delimiter) {
119
- if (current.length > 0) {
120
- parts.push(current);
121
- }
122
- current = "";
123
- continue;
124
- }
125
- current += ch;
126
- }
127
- if (current.length > 0) {
128
- parts.push(current);
129
- }
130
- return parts;
131
- }
132
90
  function stripQuotes(value) {
133
91
  if (value.startsWith("'") && value.endsWith("'")) {
134
92
  return value.slice(1, -1);
@@ -1,6 +1,6 @@
1
1
  import { rankEntities } from "kibi-cli/search-ranking";
2
- import { loadEntities, paginateResults, validateEntityType, } from "./entity-query.js";
3
2
  import { resolveWorkspaceRoot } from "../workspace.js";
3
+ import { loadEntities, paginateResults, validateEntityType, } from "./entity-query.js";
4
4
  // implements REQ-mcp-search-discovery, REQ-002
5
5
  export async function handleKbSearch(prolog, args) {
6
6
  const { query, type, limit = 20, offset = 0 } = args;
@@ -151,6 +151,7 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
151
151
  return { refreshed, found: true };
152
152
  }
153
153
  export function resolveManifestPath(workspaceRoot) {
154
+ // implements REQ-002, REQ-013
154
155
  const configPath = path.join(workspaceRoot, ".kb", "config.json");
155
156
  if (existsSync(configPath)) {
156
157
  try {
@@ -168,7 +169,9 @@ export function resolveManifestPath(workspaceRoot) {
168
169
  : path.resolve(workspaceRoot, config.symbolsManifest);
169
170
  }
170
171
  }
171
- catch { }
172
+ catch {
173
+ // config file missing or malformed; fall through to defaults
174
+ }
172
175
  }
173
176
  const candidates = [
174
177
  path.join(workspaceRoot, "symbols.yaml"),
@@ -16,7 +16,7 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import Ajv from "ajv";
19
- import { escapeAtom, toPrologAtom } from "kibi-cli/prolog/codec";
19
+ import { escapeAtom, toPrologAtom, toPrologString, } from "kibi-cli/prolog/codec";
20
20
  import entitySchema from "kibi-cli/schemas/entity";
21
21
  import relationshipSchema from "kibi-cli/schemas/relationship";
22
22
  import { refreshCoordinatesForSymbolId } from "./symbols.js";
@@ -73,6 +73,10 @@ export async function handleKbUpsert(prolog, args) {
73
73
  throw new Error(`Relationship validation failed at index ${i}: ${errorMessages}`);
74
74
  }
75
75
  }
76
+ validateRelationshipSources(id, relationships);
77
+ // Validate strict-lane fact_kind pairing for constrains/requires_property
78
+ // implements REQ-011
79
+ await validateStrictLanePairing(prolog, relationships);
76
80
  let created = 0;
77
81
  let updated = 0;
78
82
  let relationshipsCreated = 0;
@@ -94,26 +98,44 @@ export async function handleKbUpsert(prolog, args) {
94
98
  const from = rel.from;
95
99
  const to = rel.to;
96
100
  const metadata = buildRelationshipMetadata(rel);
97
- relationshipGoals.push(`kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
101
+ relationshipGoals.push(`kb_assert_relationship_no_audit(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
98
102
  }
99
103
  // Build atomic transaction goal wrapping entity + all relationships
104
+ // For requirements, also include contradiction check within the transaction
100
105
  // implements REQ-002, REQ-011
101
106
  let transactionGoal;
107
+ const needsContradictionCheck = type === "req" && !args._skipContradictionCheck;
102
108
  if (relationshipGoals.length === 0) {
103
109
  // Simple case: just entity
104
- transactionGoal = `rdf_transaction((kb_assert_entity(${type}, ${props})))`;
110
+ if (needsContradictionCheck) {
111
+ transactionGoal = `rdf_transaction((kb_assert_entity_no_audit(${type}, ${props}), check_req_contradiction('${escapeAtom(id)}')))`;
112
+ }
113
+ else {
114
+ transactionGoal = `rdf_transaction((kb_assert_entity_no_audit(${type}, ${props})))`;
115
+ }
105
116
  }
106
117
  else {
107
118
  // Entity + relationships in one transaction
108
119
  const goals = [
109
- `kb_assert_entity(${type}, ${props})`,
120
+ `kb_assert_entity_no_audit(${type}, ${props})`,
110
121
  ...relationshipGoals,
111
122
  ].join(", ");
112
- transactionGoal = `rdf_transaction((${goals}))`;
123
+ if (needsContradictionCheck) {
124
+ transactionGoal = `rdf_transaction((${goals}, check_req_contradiction('${escapeAtom(id)}')))`;
125
+ }
126
+ else {
127
+ transactionGoal = `rdf_transaction((${goals}))`;
128
+ }
113
129
  }
114
130
  const txResult = await prolog.query(transactionGoal);
115
131
  if (!txResult.success) {
116
- throw new Error(`Failed to upsert entity ${id}: ${txResult.error || "Unknown error"} (goal: ${transactionGoal})`);
132
+ // Format error message without exposing raw transaction goal
133
+ const formattedError = formatUpsertError(id, txResult.error);
134
+ throw new Error(formattedError);
135
+ }
136
+ await recordEntityAudit(prolog, type, entity);
137
+ for (const rel of relationships) {
138
+ await recordRelationshipAudit(prolog, rel);
117
139
  }
118
140
  // Update counters
119
141
  if (isUpdate) {
@@ -131,10 +153,6 @@ export async function handleKbUpsert(prolog, args) {
131
153
  if (!saveResult.success) {
132
154
  throw new Error(`Failed to save KB after upsert: ${saveResult.error || "Unknown error"}`);
133
155
  }
134
- let contradictionPairsDetected;
135
- if (type === "req" && !args._skipContradictionCheck) {
136
- contradictionPairsDetected = await detectContradictionPairs(prolog, id);
137
- }
138
156
  if (type === "symbol") {
139
157
  try {
140
158
  await refreshCoordinatesForSymbolId(id);
@@ -150,16 +168,13 @@ export async function handleKbUpsert(prolog, args) {
150
168
  content: [
151
169
  {
152
170
  type: "text",
153
- text: contradictionPairsDetected && contradictionPairsDetected > 0
154
- ? `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s). Contradiction probe detected ${contradictionPairsDetected} potential conflict pair(s).`
155
- : `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
171
+ text: `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
156
172
  },
157
173
  ],
158
174
  structuredContent: {
159
175
  created,
160
176
  updated,
161
177
  relationships_created: relationshipsCreated,
162
- contradiction_pairs_detected: contradictionPairsDetected,
163
178
  },
164
179
  };
165
180
  }
@@ -168,27 +183,28 @@ export async function handleKbUpsert(prolog, args) {
168
183
  throw new Error(`Upsert execution failed: ${message}`);
169
184
  }
170
185
  }
171
- async function detectContradictionPairs(prolog, reqId) {
172
- const escaped = escapeAtom(reqId);
173
- const goal = `aggregate_all(count, (contradicting_reqs(A, B, _), (A = '${escaped}' ; B = '${escaped}' ; A = 'file:///${escaped}' ; B = 'file:///${escaped}')), Count)`;
174
- const result = await prolog.query(goal);
175
- if (!result.success) {
176
- return 0;
177
- }
178
- const raw = result.bindings.Count;
179
- const count = Number(raw);
180
- return Number.isFinite(count) ? count : 0;
181
- }
182
186
  /**
183
187
  * Build Prolog property list from entity object
184
188
  * Returns simple Key=Value format without typed literals
185
189
  * Example output: "[id='test-1', title=\"Test\", status=active]"
190
+ * implements REQ-002
186
191
  */
187
192
  function buildPropertyList(entity) {
188
193
  const pairs = [];
189
194
  // Defined internally to ensure thread safety and avoid initialization order issues.
190
195
  // Using simple arrays instead of Sets is performant enough for small lists and avoids Set allocation overhead.
191
- const ATOM_FIELDS = ["status", "owner", "priority", "severity"];
196
+ // implements REQ-002
197
+ const ATOM_FIELDS = [
198
+ "status",
199
+ "owner",
200
+ "priority",
201
+ "severity",
202
+ // Typed fact enum fields must be atoms for Prolog validation
203
+ "fact_kind",
204
+ "operator",
205
+ "value_type",
206
+ "polarity",
207
+ ];
192
208
  const STRING_FIELDS = [
193
209
  "id",
194
210
  "title",
@@ -211,10 +227,10 @@ function buildPropertyList(entity) {
211
227
  prologValue = toPrologAtom(value);
212
228
  }
213
229
  else if (STRING_FIELDS.includes(key) && typeof value === "string") {
214
- prologValue = `"${escapeQuotes(value)}"`;
230
+ prologValue = `${toPrologString(value)}`;
215
231
  }
216
232
  else if (typeof value === "string") {
217
- prologValue = `"${escapeQuotes(value)}"`;
233
+ prologValue = `${toPrologString(value)}`;
218
234
  }
219
235
  else if (typeof value === "number") {
220
236
  prologValue = String(value);
@@ -223,7 +239,7 @@ function buildPropertyList(entity) {
223
239
  prologValue = value ? "true" : "false";
224
240
  }
225
241
  else {
226
- prologValue = `"${escapeQuotes(String(value))}"`;
242
+ prologValue = `${toPrologString(String(value))}`;
227
243
  }
228
244
  pairs.push(`${key}=${prologValue}`);
229
245
  }
@@ -240,21 +256,122 @@ function buildRelationshipMetadata(rel) {
240
256
  continue;
241
257
  let prologValue;
242
258
  if (typeof value === "string") {
243
- prologValue = `"${escapeQuotes(value)}"`;
259
+ prologValue = `${toPrologString(value)}`;
244
260
  }
245
261
  else if (typeof value === "number") {
246
262
  prologValue = String(value);
247
263
  }
248
264
  else {
249
- prologValue = `"${escapeQuotes(String(value))}"`;
265
+ prologValue = `${toPrologString(String(value))}`;
250
266
  }
251
267
  pairs.push(`${key}=${prologValue}`);
252
268
  }
253
269
  return `[${pairs.join(", ")}]`;
254
270
  }
255
271
  /**
256
- * Escape double quotes in strings for Prolog
272
+ * Ensure all relationship rows belong to the entity being upserted.
273
+ * Rejects foreign-source relationship writes in the same request.
274
+ * implements REQ-002, REQ-011
275
+ */
276
+ function validateRelationshipSources(entityId, relationships) {
277
+ for (const rel of relationships) {
278
+ if (rel.from !== entityId) {
279
+ throw new Error(`Relationship source must match the upserted entity ${entityId}; received from=${String(rel.from)}`);
280
+ }
281
+ }
282
+ }
283
+ /**
284
+ * Validate strict-lane fact_kind pairing for constrains/requires_property relationships.
285
+ * constrains targets must be subject, observation, or meta facts (or legacy without fact_kind).
286
+ * requires_property targets must be property_value, observation, or meta facts (or legacy without fact_kind).
287
+ * implements REQ-011
288
+ */
289
+ async function validateStrictLanePairing(prolog, relationships) {
290
+ // implements REQ-011
291
+ for (const rel of relationships) {
292
+ if (rel.type === "constrains") {
293
+ const targetId = rel.to;
294
+ // Reject if target is a fact with fact_kind=property_value.
295
+ // Allow: legacy (no fact_kind), subject, observation, meta, or non-existent
296
+ // (non-existent targets are caught by relationship type validation elsewhere).
297
+ const rejectResult = await prolog.query(`once((kb_entity('${escapeAtom(targetId)}', fact, _SlpProps), memberchk(fact_kind=_SlpFK, _SlpProps), normalize_term_atom(_SlpFK, property_value)))`);
298
+ if (rejectResult.success) {
299
+ throw new Error(`Relationship 'constrains' requires target '${targetId}' to be a subject, observation, or meta fact. Property_value facts cannot be direct targets of constrains relationships.`);
300
+ }
301
+ }
302
+ else if (rel.type === "requires_property") {
303
+ const targetId = rel.to;
304
+ // Reject if target is a fact with fact_kind=subject.
305
+ // Allow: legacy (no fact_kind), property_value, observation, meta, or non-existent.
306
+ const rejectResult = await prolog.query(`once((kb_entity('${escapeAtom(targetId)}', fact, _SlpProps), memberchk(fact_kind=_SlpFK, _SlpProps), normalize_term_atom(_SlpFK, subject)))`);
307
+ if (rejectResult.success) {
308
+ throw new Error(`Relationship 'requires_property' requires target '${targetId}' to be a property_value, observation, or meta fact. Subject facts cannot be direct targets of requires_property relationships.`);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ /**
314
+ * Record audit entry for a successfully committed entity mutation.
315
+ * Called only after the RDF transaction succeeds.
316
+ * implements REQ-011
317
+ */
318
+ async function recordEntityAudit(prolog, type, entity) {
319
+ const props = buildPropertyList(entity);
320
+ const result = await prolog.query(`kb_log_entity_upsert(${type}, ${props})`);
321
+ if (!result.success) {
322
+ throw new Error(`Failed to record audit entry for ${String(entity.id)}: ${result.error || "Unknown error"}`);
323
+ }
324
+ }
325
+ /**
326
+ * Record audit entry for a successfully committed relationship mutation.
327
+ * Called only after the RDF transaction succeeds.
328
+ * implements REQ-011
257
329
  */
258
- function escapeQuotes(str) {
259
- return str.replace(/"/g, '\\"');
330
+ async function recordRelationshipAudit(prolog, rel) {
331
+ const relType = rel.type;
332
+ const from = rel.from;
333
+ const to = rel.to;
334
+ const metadata = buildRelationshipMetadata(rel);
335
+ const result = await prolog.query(`kb_log_relationship_upsert(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
336
+ if (!result.success) {
337
+ throw new Error(`Failed to record relationship audit entry ${from}->${to}: ${result.error || "Unknown error"}`);
338
+ }
339
+ }
340
+ /**
341
+ * Format upsert error message for user-facing display.
342
+ * Removes raw transaction goals and extracts meaningful contradiction details.
343
+ * implements REQ-011
344
+ */
345
+ function formatUpsertError(entityId, rawError) {
346
+ if (!rawError) {
347
+ return `Failed to upsert entity ${entityId}: Unknown error`;
348
+ }
349
+ // Check for contradiction error - Prolog returns kb_contradiction([...]) term
350
+ // Try to extract readable details from the term
351
+ const contradictionMatch = rawError.match(/kb_contradiction\(\s*\[([^\]]+)\]\s*\)/);
352
+ if (contradictionMatch) {
353
+ // Extract individual conflict details from the list
354
+ const details = contradictionMatch[1];
355
+ // Parse out readable parts - each entry is like 'Reason'-'ReqId'
356
+ const conflicts = [];
357
+ const conflictRegex = /'([^']+)'-'([^']+)'/g;
358
+ let execResult = conflictRegex.exec(details);
359
+ while (execResult !== null) {
360
+ const reason = execResult[1];
361
+ const otherReq = execResult[2];
362
+ conflicts.push(` - Conflicts with ${otherReq}: ${reason}`);
363
+ execResult = conflictRegex.exec(details);
364
+ }
365
+ if (conflicts.length > 0) {
366
+ const uniqueConflicts = [...new Set(conflicts)];
367
+ return `Contradiction detected for requirement ${entityId}:\n${uniqueConflicts.join("\n")}\n\nTo resolve:\n 1. Add a supersedes relationship from the new requirement to the conflicting one, OR\n 2. Deprecate the conflicting requirement before creating the new one.`;
368
+ }
369
+ return `Contradiction detected for entity ${entityId}: This requirement conflicts with existing requirements. Add a supersedes relationship to the conflicting requirement, or deprecate the old requirement before creating the new one.`;
370
+ }
371
+ // Check for RDF transaction error
372
+ if (rawError.includes("rdf_transaction")) {
373
+ return `Failed to upsert entity ${entityId}: Transaction failed`;
374
+ }
375
+ // Default: return cleaned error without raw goal
376
+ return `Failed to upsert entity ${entityId}: ${rawError}`;
260
377
  }
@@ -232,7 +232,7 @@ const BASE_TOOLS = [
232
232
  },
233
233
  {
234
234
  name: "kb_upsert",
235
- description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
235
+ description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. For requirements, the write will be rejected if it contradicts existing current requirements that constrain the same subject with incompatible properties. To replace a conflicting requirement, include a `supersedes` relationship from the new requirement to the old one in the same request. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
236
236
  inputSchema: {
237
237
  type: "object",
238
238
  required: ["type", "id", "properties"],
@@ -375,9 +375,10 @@ const BASE_TOOLS = [
375
375
  "required-fields",
376
376
  "deprecated-adr-no-successor",
377
377
  "domain-contradictions",
378
+ "strict-fact-shape",
378
379
  ],
379
380
  },
380
- description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions. If omitted, server runs all.",
381
+ description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape. If omitted, server runs all.",
381
382
  },
382
383
  },
383
384
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.26.0",
6
6
  "ajv": "^8.18.0",
@@ -9,8 +9,8 @@
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.3.0",
13
- "kibi-core": "^0.2.0",
12
+ "kibi-cli": "^0.4.0",
13
+ "kibi-core": "^0.3.0",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"