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.
- package/dist/diagnostics.js +3 -4
- package/dist/server/docs.js +2 -1
- package/dist/server/tools.js +1 -1
- package/dist/tools/check.js +8 -27
- package/dist/tools/core-module.js +3 -1
- package/dist/tools/entity-query.js +1 -0
- package/dist/tools/find-gaps.js +1 -1
- package/dist/tools/prolog-list.js +1 -43
- package/dist/tools/search.js +1 -1
- package/dist/tools/symbols.js +4 -1
- package/dist/tools/upsert.js +151 -34
- package/dist/tools-config.js +3 -2
- package/package.json +3 -3
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
|
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`;
|
package/dist/server/docs.js
CHANGED
|
@@ -35,6 +35,7 @@ function renderToolsDoc() {
|
|
|
35
35
|
return lines.join("\n");
|
|
36
36
|
}
|
|
37
37
|
export const PROMPTS = [
|
|
38
|
+
// implements REQ-002, REQ-013, REQ-mcp-search-discovery
|
|
38
39
|
{
|
|
39
40
|
name: "init-kibi",
|
|
40
41
|
description: "Bootstrap Kibi on an existing repository with zero entities.",
|
|
@@ -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
|
-
|
|
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',
|
package/dist/server/tools.js
CHANGED
|
@@ -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";
|
package/dist/tools/check.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import * as path from "node:path";
|
|
21
|
+
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
21
22
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
function resolveChecksPlPath() {
|
|
@@ -31,30 +32,15 @@ function resolveChecksPlPath() {
|
|
|
31
32
|
return installedChecksPl;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
catch {
|
|
35
|
+
catch {
|
|
36
|
+
// require.resolve not available or package not installed
|
|
37
|
+
}
|
|
35
38
|
const localChecksPl = path.join(process.cwd(), "packages/core/src/checks.pl");
|
|
36
39
|
if (existsSync(localChecksPl)) {
|
|
37
40
|
return localChecksPl;
|
|
38
41
|
}
|
|
39
42
|
throw new Error("Unable to resolve checks.pl path");
|
|
40
43
|
}
|
|
41
|
-
const ALL_RULES = [
|
|
42
|
-
"must-priority-coverage",
|
|
43
|
-
"no-dangling-refs",
|
|
44
|
-
"no-cycles",
|
|
45
|
-
"required-fields",
|
|
46
|
-
"symbol-coverage",
|
|
47
|
-
"symbol-traceability",
|
|
48
|
-
"deprecated-adr-no-successor",
|
|
49
|
-
"domain-contradictions",
|
|
50
|
-
];
|
|
51
|
-
const RULE_NAMES = new Set(ALL_RULES);
|
|
52
|
-
const DEFAULT_CHECKS_CONFIG = {
|
|
53
|
-
rules: Object.fromEntries(ALL_RULES.map((rule) => [rule, true])),
|
|
54
|
-
symbolTraceability: {
|
|
55
|
-
requireAdr: false,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
44
|
function formatDiagnosticsForMcp(diagnostics) {
|
|
59
45
|
return diagnostics.map((d) => ({
|
|
60
46
|
category: d.category,
|
|
@@ -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
|
|
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;
|
package/dist/tools/find-gaps.js
CHANGED
|
@@ -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);
|
package/dist/tools/search.js
CHANGED
|
@@ -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;
|
package/dist/tools/symbols.js
CHANGED
|
@@ -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"),
|
package/dist/tools/upsert.js
CHANGED
|
@@ -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(`
|
|
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
|
-
|
|
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
|
-
`
|
|
120
|
+
`kb_assert_entity_no_audit(${type}, ${props})`,
|
|
110
121
|
...relationshipGoals,
|
|
111
122
|
].join(", ");
|
|
112
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
230
|
+
prologValue = `${toPrologString(value)}`;
|
|
215
231
|
}
|
|
216
232
|
else if (typeof value === "string") {
|
|
217
|
-
prologValue =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
259
|
-
|
|
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
|
}
|
package/dist/tools-config.js
CHANGED
|
@@ -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.
|
|
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.
|
|
13
|
-
"kibi-core": "^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"
|