kibi-mcp 0.2.4 → 0.3.1
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/env.js +0 -27
- package/dist/server/docs.js +260 -0
- package/dist/server/session.js +218 -0
- package/dist/server/tools.js +199 -0
- package/dist/server/transport.js +71 -0
- package/dist/server.js +13 -846
- package/dist/tools/check.js +32 -27
- package/dist/tools/delete.js +1 -20
- package/dist/tools/prolog-list.js +2 -38
- package/dist/tools/query.js +1 -255
- package/dist/tools/symbols.js +0 -27
- package/dist/tools/upsert.js +9 -11
- package/dist/tools-config.js +10 -44
- package/dist/workspace.js +3 -42
- package/package.json +3 -3
- package/dist/tools/branch.js +0 -208
- package/dist/tools/context.js +0 -270
- package/dist/tools/coverage-report.js +0 -91
- package/dist/tools/derive.js +0 -311
- package/dist/tools/impact.js +0 -70
- package/dist/tools/list-types.js +0 -75
- package/dist/tools/query-relationships.js +0 -176
- package/dist/tools/suggest-shared-facts.js +0 -138
package/dist/tools/check.js
CHANGED
|
@@ -15,33 +15,6 @@
|
|
|
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
|
-
/*
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
45
18
|
import { existsSync } from "node:fs";
|
|
46
19
|
import { createRequire } from "node:module";
|
|
47
20
|
import * as path from "node:path";
|
|
@@ -90,6 +63,9 @@ export async function handleKbCheck(prolog, args) {
|
|
|
90
63
|
"no-cycles",
|
|
91
64
|
"required-fields",
|
|
92
65
|
"symbol-coverage",
|
|
66
|
+
"symbol-traceability",
|
|
67
|
+
"deprecated-adr-no-successor",
|
|
68
|
+
"domain-contradictions",
|
|
93
69
|
];
|
|
94
70
|
const rulesToRun = rules && rules.length > 0 ? rules : allRules;
|
|
95
71
|
const rulesAllowlist = new Set(rulesToRun);
|
|
@@ -137,6 +113,9 @@ export async function handleKbCheck(prolog, args) {
|
|
|
137
113
|
if (rulesToRun.includes("symbol-coverage")) {
|
|
138
114
|
violations.push(...(await checkSymbolCoverage(prolog)));
|
|
139
115
|
}
|
|
116
|
+
if (rulesToRun.includes("symbol-traceability")) {
|
|
117
|
+
violations.push(...(await checkSymbolTraceability(prolog, false)));
|
|
118
|
+
}
|
|
140
119
|
const diagnostics = violations.map((v) => ({
|
|
141
120
|
category: "SYNC_ERROR",
|
|
142
121
|
severity: "error",
|
|
@@ -232,6 +211,32 @@ async function runAggregatedChecks(prolog, rulesAllowlist) {
|
|
|
232
211
|
}
|
|
233
212
|
return violations;
|
|
234
213
|
}
|
|
214
|
+
async function checkSymbolTraceability(prolog, requireAdr) {
|
|
215
|
+
const violations = [];
|
|
216
|
+
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
|
+
}
|
|
235
240
|
async function checkMustPriorityCoverage(prolog) {
|
|
236
241
|
const violations = [];
|
|
237
242
|
const gapsResult = await prolog.query("setof([Req,Reason], coverage_gap(Req, Reason), Rows)");
|
package/dist/tools/delete.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
-
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify
|
|
6
|
-
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
-
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
-
(at your option) any later version.
|
|
9
|
-
|
|
10
|
-
This program is distributed in the hope that it will be useful,
|
|
11
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
-
GNU Affero General Public License for more details.
|
|
14
|
-
|
|
15
|
-
You should have received a copy of the GNU Affero General Public License
|
|
16
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
18
|
-
function escapeAtom(value) {
|
|
19
|
-
return value.replace(/'/g, "''");
|
|
20
|
-
}
|
|
1
|
+
import { escapeAtom } from "kibi-cli/prolog/codec";
|
|
21
2
|
/**
|
|
22
3
|
* Handle kb.delete tool calls
|
|
23
4
|
* Prevents deletion of entities with dependents (referential integrity)
|
|
@@ -15,33 +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
|
-
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
18
|
+
// implements REQ-002
|
|
45
19
|
export function parseAtomList(raw) {
|
|
46
20
|
const trimmed = raw.trim();
|
|
47
21
|
if (trimmed === "[]" || trimmed.length === 0) {
|
|
@@ -55,6 +29,7 @@ export function parseAtomList(raw) {
|
|
|
55
29
|
.map((token) => stripQuotes(token.trim()))
|
|
56
30
|
.filter((token) => token.length > 0);
|
|
57
31
|
}
|
|
32
|
+
// implements REQ-002
|
|
58
33
|
export function parsePairList(raw) {
|
|
59
34
|
const rows = parseListRows(raw);
|
|
60
35
|
const pairs = [];
|
|
@@ -66,17 +41,6 @@ export function parsePairList(raw) {
|
|
|
66
41
|
}
|
|
67
42
|
return pairs;
|
|
68
43
|
}
|
|
69
|
-
export function parseTriples(raw) {
|
|
70
|
-
const rows = parseListRows(raw);
|
|
71
|
-
const triples = [];
|
|
72
|
-
for (const row of rows) {
|
|
73
|
-
const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
|
|
74
|
-
if (parts.length >= 3) {
|
|
75
|
-
triples.push([parts[0], parts[1], parts[2]]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return triples;
|
|
79
|
-
}
|
|
80
44
|
function parseListRows(raw) {
|
|
81
45
|
const trimmed = raw.trim();
|
|
82
46
|
if (trimmed === "[]" || trimmed.length === 0) {
|
package/dist/tools/query.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
-
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify
|
|
6
|
-
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
-
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
-
(at your option) any later version.
|
|
9
|
-
|
|
10
|
-
This program is distributed in the hope that it will be useful,
|
|
11
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
-
GNU Affero General Public License for more details.
|
|
14
|
-
|
|
15
|
-
You should have received a copy of the GNU Affero General Public License
|
|
16
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Escape a string for embedding inside a single-quoted Prolog atom.
|
|
20
|
-
* Doubles single-quote characters per ISO Prolog standard.
|
|
21
|
-
*/
|
|
22
|
-
function escapeAtomContent(value) {
|
|
23
|
-
return value.replace(/'/g, "''");
|
|
24
|
-
}
|
|
1
|
+
import { escapeAtomContent, parseEntityFromBinding, parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
|
|
25
2
|
export const VALID_ENTITY_TYPES = [
|
|
26
3
|
"req",
|
|
27
4
|
"scenario",
|
|
@@ -174,234 +151,3 @@ function dedupeEntities(entities) {
|
|
|
174
151
|
}
|
|
175
152
|
return deduped;
|
|
176
153
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Parse a Prolog list of lists into a JavaScript array.
|
|
179
|
-
* Input: "[[a,b,c],[d,e,f]]"
|
|
180
|
-
* Output: [["a", "b", "c"], ["d", "e", "f"]]
|
|
181
|
-
*/
|
|
182
|
-
export function parseListOfLists(listStr) {
|
|
183
|
-
const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
184
|
-
if (cleaned === "") {
|
|
185
|
-
return [];
|
|
186
|
-
}
|
|
187
|
-
const results = [];
|
|
188
|
-
let depth = 0;
|
|
189
|
-
let current = "";
|
|
190
|
-
let currentList = [];
|
|
191
|
-
for (let i = 0; i < cleaned.length; i++) {
|
|
192
|
-
const char = cleaned[i];
|
|
193
|
-
if (char === "[") {
|
|
194
|
-
depth++;
|
|
195
|
-
if (depth > 1)
|
|
196
|
-
current += char;
|
|
197
|
-
}
|
|
198
|
-
else if (char === "]") {
|
|
199
|
-
depth--;
|
|
200
|
-
if (depth === 0) {
|
|
201
|
-
if (current) {
|
|
202
|
-
currentList.push(current.trim());
|
|
203
|
-
current = "";
|
|
204
|
-
}
|
|
205
|
-
if (currentList.length > 0) {
|
|
206
|
-
results.push(currentList);
|
|
207
|
-
currentList = [];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
current += char;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else if (char === "," && depth === 1) {
|
|
215
|
-
if (current) {
|
|
216
|
-
currentList.push(current.trim());
|
|
217
|
-
current = "";
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
else if (char === "," && depth === 0) {
|
|
221
|
-
// Skip comma between lists
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
current += char;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return results;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Parse a single entity from Prolog binding format.
|
|
231
|
-
* Input: "[abc123, req, [id=abc123, title=\"Test\", ...]]"
|
|
232
|
-
*/
|
|
233
|
-
export function parseEntityFromBinding(bindingStr) {
|
|
234
|
-
const cleaned = bindingStr.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
235
|
-
const parts = splitTopLevel(cleaned, ",");
|
|
236
|
-
if (parts.length < 3) {
|
|
237
|
-
return {};
|
|
238
|
-
}
|
|
239
|
-
const id = parts[0].trim();
|
|
240
|
-
const type = parts[1].trim();
|
|
241
|
-
const propsStr = parts.slice(2).join(",").trim();
|
|
242
|
-
const props = parsePropertyList(propsStr);
|
|
243
|
-
return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Parse entity from array returned by parseListOfLists.
|
|
247
|
-
* Input: ["abc123", "req", "[id=abc123, title=\"Test\", ...]"]
|
|
248
|
-
*/
|
|
249
|
-
export function parseEntityFromList(data) {
|
|
250
|
-
if (data.length < 3) {
|
|
251
|
-
return {};
|
|
252
|
-
}
|
|
253
|
-
const id = data[0].trim();
|
|
254
|
-
const type = data[1].trim();
|
|
255
|
-
const propsStr = data[2].trim();
|
|
256
|
-
const props = parsePropertyList(propsStr);
|
|
257
|
-
return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Parse Prolog property list into JavaScript object.
|
|
261
|
-
*/
|
|
262
|
-
export function parsePropertyList(propsStr) {
|
|
263
|
-
const props = {};
|
|
264
|
-
let cleaned = propsStr.trim();
|
|
265
|
-
if (cleaned.startsWith("[")) {
|
|
266
|
-
cleaned = cleaned.substring(1);
|
|
267
|
-
}
|
|
268
|
-
if (cleaned.endsWith("]")) {
|
|
269
|
-
cleaned = cleaned.substring(0, cleaned.length - 1);
|
|
270
|
-
}
|
|
271
|
-
const pairs = splitTopLevel(cleaned, ",");
|
|
272
|
-
for (const pair of pairs) {
|
|
273
|
-
const eqIndex = pair.indexOf("=");
|
|
274
|
-
if (eqIndex === -1)
|
|
275
|
-
continue;
|
|
276
|
-
const key = pair.substring(0, eqIndex).trim();
|
|
277
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
278
|
-
if (key === "..." || value === "..." || value === "...|...") {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const parsed = parsePrologValue(value);
|
|
282
|
-
props[key] = parsed;
|
|
283
|
-
}
|
|
284
|
-
return props;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Parse a single Prolog value, handling typed literals and URIs.
|
|
288
|
-
*/
|
|
289
|
-
export function parsePrologValue(valueInput) {
|
|
290
|
-
const value = valueInput.trim();
|
|
291
|
-
// Handle typed literal: ^^("value", type)
|
|
292
|
-
if (value.startsWith("^^(")) {
|
|
293
|
-
const innerStart = value.indexOf("(") + 1;
|
|
294
|
-
let depth = 1;
|
|
295
|
-
let innerEnd = innerStart;
|
|
296
|
-
for (let i = innerStart; i < value.length; i++) {
|
|
297
|
-
if (value[i] === "(")
|
|
298
|
-
depth++;
|
|
299
|
-
if (value[i] === ")") {
|
|
300
|
-
depth--;
|
|
301
|
-
if (depth === 0) {
|
|
302
|
-
innerEnd = i;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const innerContent = value.substring(innerStart, innerEnd);
|
|
308
|
-
const parts = splitTopLevel(innerContent, ",");
|
|
309
|
-
if (parts.length >= 2) {
|
|
310
|
-
let literalValue = parts[0].trim();
|
|
311
|
-
if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
|
|
312
|
-
literalValue = literalValue.substring(1, literalValue.length - 1);
|
|
313
|
-
}
|
|
314
|
-
// Handle array notation
|
|
315
|
-
if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
|
|
316
|
-
const listContent = literalValue.substring(1, literalValue.length - 1);
|
|
317
|
-
if (listContent === "") {
|
|
318
|
-
return [];
|
|
319
|
-
}
|
|
320
|
-
return splitTopLevel(listContent, ",").map((item) => item.trim());
|
|
321
|
-
}
|
|
322
|
-
return literalValue;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Handle URI
|
|
326
|
-
if (value.startsWith("file:///")) {
|
|
327
|
-
const lastSlash = value.lastIndexOf("/");
|
|
328
|
-
if (lastSlash !== -1) {
|
|
329
|
-
return value.substring(lastSlash + 1);
|
|
330
|
-
}
|
|
331
|
-
return value;
|
|
332
|
-
}
|
|
333
|
-
// Handle quoted string
|
|
334
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
335
|
-
return value.substring(1, value.length - 1);
|
|
336
|
-
}
|
|
337
|
-
// Handle quoted atom
|
|
338
|
-
if (value.startsWith("'") && value.endsWith("'")) {
|
|
339
|
-
return value.substring(1, value.length - 1);
|
|
340
|
-
}
|
|
341
|
-
// Handle list
|
|
342
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
343
|
-
const listContent = value.substring(1, value.length - 1);
|
|
344
|
-
if (listContent === "") {
|
|
345
|
-
return [];
|
|
346
|
-
}
|
|
347
|
-
const items = splitTopLevel(listContent, ",").map((item) => {
|
|
348
|
-
return parsePrologValue(item.trim());
|
|
349
|
-
});
|
|
350
|
-
return items;
|
|
351
|
-
}
|
|
352
|
-
return value;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Split a string by delimiter at the top level (not inside brackets or quotes).
|
|
356
|
-
*/
|
|
357
|
-
export function splitTopLevel(str, delimiter) {
|
|
358
|
-
const results = [];
|
|
359
|
-
let current = "";
|
|
360
|
-
let depth = 0;
|
|
361
|
-
let inQuotes = false;
|
|
362
|
-
for (let i = 0; i < str.length; i++) {
|
|
363
|
-
const char = str[i];
|
|
364
|
-
const prevChar = i > 0 ? str[i - 1] : "";
|
|
365
|
-
if (char === '"' && prevChar !== "\\") {
|
|
366
|
-
inQuotes = !inQuotes;
|
|
367
|
-
current += char;
|
|
368
|
-
}
|
|
369
|
-
else if (!inQuotes && (char === "[" || char === "(")) {
|
|
370
|
-
depth++;
|
|
371
|
-
current += char;
|
|
372
|
-
}
|
|
373
|
-
else if (!inQuotes && (char === "]" || char === ")")) {
|
|
374
|
-
depth--;
|
|
375
|
-
current += char;
|
|
376
|
-
}
|
|
377
|
-
else if (!inQuotes && depth === 0 && char === delimiter) {
|
|
378
|
-
if (current) {
|
|
379
|
-
results.push(current);
|
|
380
|
-
current = "";
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
current += char;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (current) {
|
|
388
|
-
results.push(current);
|
|
389
|
-
}
|
|
390
|
-
return results;
|
|
391
|
-
}
|
|
392
|
-
function stripOuterQuotes(value) {
|
|
393
|
-
if (value.startsWith("'") && value.endsWith("'")) {
|
|
394
|
-
return value.slice(1, -1);
|
|
395
|
-
}
|
|
396
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
397
|
-
return value.slice(1, -1);
|
|
398
|
-
}
|
|
399
|
-
return value;
|
|
400
|
-
}
|
|
401
|
-
function normalizeEntityId(value) {
|
|
402
|
-
if (!value.startsWith("file:///")) {
|
|
403
|
-
return value;
|
|
404
|
-
}
|
|
405
|
-
const idx = value.lastIndexOf("/");
|
|
406
|
-
return idx === -1 ? value : value.slice(idx + 1);
|
|
407
|
-
}
|
package/dist/tools/symbols.js
CHANGED
|
@@ -15,33 +15,6 @@
|
|
|
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
|
-
/*
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
45
18
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
46
19
|
import path from "node:path";
|
|
47
20
|
import { dump as dumpYAML, load as parseYAML } from "js-yaml";
|
package/dist/tools/upsert.js
CHANGED
|
@@ -16,18 +16,10 @@
|
|
|
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
20
|
import entitySchema from "kibi-cli/schemas/entity";
|
|
20
21
|
import relationshipSchema from "kibi-cli/schemas/relationship";
|
|
21
22
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
22
|
-
function escapeAtom(value) {
|
|
23
|
-
return value.replace(/'/g, "''");
|
|
24
|
-
}
|
|
25
|
-
function toPrologAtom(value) {
|
|
26
|
-
const simplePrologAtom = /^[a-z][a-zA-Z0-9_]*$/;
|
|
27
|
-
return simplePrologAtom.test(value)
|
|
28
|
-
? value
|
|
29
|
-
: `'${value.replace(/'/g, "''")}'`;
|
|
30
|
-
}
|
|
31
23
|
const ajv = new Ajv({ strict: false });
|
|
32
24
|
const validateEntity = ajv.compile(entitySchema);
|
|
33
25
|
const validateRelationship = ajv.compile(relationshipSchema);
|
|
@@ -122,11 +114,17 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
122
114
|
}
|
|
123
115
|
relationshipsCreated++;
|
|
124
116
|
}
|
|
125
|
-
//
|
|
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
|
|
126
122
|
await prolog.query("kb_save");
|
|
127
123
|
prolog.invalidateCache();
|
|
124
|
+
// multiple upserts and save once at the end for better performance.
|
|
125
|
+
prolog.invalidateCache();
|
|
128
126
|
let contradictionPairsDetected;
|
|
129
|
-
if (type === "req") {
|
|
127
|
+
if (type === "req" && !args._skipContradictionCheck) {
|
|
130
128
|
contradictionPairsDetected = await detectContradictionPairs(prolog, id);
|
|
131
129
|
}
|
|
132
130
|
if (type === "symbol") {
|
package/dist/tools-config.js
CHANGED
|
@@ -15,37 +15,11 @@
|
|
|
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
|
-
/*
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
45
18
|
export const TOOLS = [
|
|
19
|
+
// implements REQ-002
|
|
46
20
|
{
|
|
47
21
|
name: "kb_query",
|
|
48
|
-
description: "Read entities from the KB with filters. Use for discovery and lookup before edits. Do not use for writes. No mutation side effects.",
|
|
22
|
+
description: "Read entities from the KB with filters. Use for discovery and lookup before edits. Do not use for writes. No mutation side effects. Tags filter by metadata tags only, not entity IDs.",
|
|
49
23
|
inputSchema: {
|
|
50
24
|
type: "object",
|
|
51
25
|
properties: {
|
|
@@ -91,7 +65,7 @@ export const TOOLS = [
|
|
|
91
65
|
},
|
|
92
66
|
{
|
|
93
67
|
name: "kb_upsert",
|
|
94
|
-
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. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
|
|
68
|
+
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.",
|
|
95
69
|
inputSchema: {
|
|
96
70
|
type: "object",
|
|
97
71
|
required: ["type", "id", "properties"],
|
|
@@ -124,18 +98,7 @@ export const TOOLS = [
|
|
|
124
98
|
},
|
|
125
99
|
status: {
|
|
126
100
|
type: "string",
|
|
127
|
-
|
|
128
|
-
"active",
|
|
129
|
-
"draft",
|
|
130
|
-
"archived",
|
|
131
|
-
"deleted",
|
|
132
|
-
"approved",
|
|
133
|
-
"rejected",
|
|
134
|
-
"pending",
|
|
135
|
-
"in_progress",
|
|
136
|
-
"superseded",
|
|
137
|
-
],
|
|
138
|
-
description: "Required lifecycle state. Allowed values are fixed enum options. Example: 'active'.",
|
|
101
|
+
description: "Required lifecycle state. Allowed values depend on entity type; backward-compatible legacy statuses are also accepted. Examples: 'open', 'passing', 'accepted', 'active'.",
|
|
139
102
|
},
|
|
140
103
|
source: {
|
|
141
104
|
type: "string",
|
|
@@ -228,7 +191,7 @@ export const TOOLS = [
|
|
|
228
191
|
},
|
|
229
192
|
{
|
|
230
193
|
name: "kb_check",
|
|
231
|
-
description: "Run KB validation rules and return violations. Use before or after mutations. Do not use for point lookups. No write side effects.",
|
|
194
|
+
description: "Run KB validation rules and return violations. Use before or after mutations. Do not use for point lookups. No write side effects. Prefer explicit rules for faster iteration.",
|
|
232
195
|
inputSchema: {
|
|
233
196
|
type: "object",
|
|
234
197
|
properties: {
|
|
@@ -238,13 +201,16 @@ export const TOOLS = [
|
|
|
238
201
|
type: "string",
|
|
239
202
|
enum: [
|
|
240
203
|
"must-priority-coverage",
|
|
204
|
+
"symbol-coverage",
|
|
205
|
+
"symbol-traceability",
|
|
241
206
|
"no-dangling-refs",
|
|
242
207
|
"no-cycles",
|
|
243
208
|
"required-fields",
|
|
244
|
-
"
|
|
209
|
+
"deprecated-adr-no-successor",
|
|
210
|
+
"domain-contradictions",
|
|
245
211
|
],
|
|
246
212
|
},
|
|
247
|
-
description: "Optional rule subset. Allowed: must-priority-coverage, no-dangling-refs, no-cycles, required-fields,
|
|
213
|
+
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.",
|
|
248
214
|
},
|
|
249
215
|
},
|
|
250
216
|
},
|
package/dist/workspace.js
CHANGED
|
@@ -15,33 +15,6 @@
|
|
|
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
|
-
/*
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
45
18
|
import fs from "node:fs";
|
|
46
19
|
import path from "node:path";
|
|
47
20
|
const WORKSPACE_ENV_KEYS = [
|
|
@@ -50,6 +23,7 @@ const WORKSPACE_ENV_KEYS = [
|
|
|
50
23
|
"KIBI_ROOT",
|
|
51
24
|
];
|
|
52
25
|
const KB_PATH_ENV_KEYS = ["KIBI_KB_PATH", "KB_PATH"];
|
|
26
|
+
// implements REQ-002, REQ-012
|
|
53
27
|
export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
54
28
|
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
55
29
|
if (envRoot) {
|
|
@@ -65,21 +39,7 @@ export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
|
65
39
|
}
|
|
66
40
|
return path.resolve(startDir);
|
|
67
41
|
}
|
|
68
|
-
|
|
69
|
-
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
70
|
-
if (envRoot) {
|
|
71
|
-
return { root: path.resolve(envRoot), reason: "env" };
|
|
72
|
-
}
|
|
73
|
-
const kbRoot = findUpwards(startDir, ".kb");
|
|
74
|
-
if (kbRoot) {
|
|
75
|
-
return { root: kbRoot, reason: "kb" };
|
|
76
|
-
}
|
|
77
|
-
const gitRoot = findUpwards(startDir, ".git");
|
|
78
|
-
if (gitRoot) {
|
|
79
|
-
return { root: gitRoot, reason: "git" };
|
|
80
|
-
}
|
|
81
|
-
return { root: path.resolve(startDir), reason: "cwd" };
|
|
82
|
-
}
|
|
42
|
+
// implements REQ-002, REQ-012
|
|
83
43
|
export function resolveKbPath(workspaceRoot, branch) {
|
|
84
44
|
const envPath = readFirstEnv(KB_PATH_ENV_KEYS);
|
|
85
45
|
if (envPath) {
|
|
@@ -91,6 +51,7 @@ export function resolveKbPath(workspaceRoot, branch) {
|
|
|
91
51
|
}
|
|
92
52
|
return path.join(workspaceRoot, ".kb", "branches", branch);
|
|
93
53
|
}
|
|
54
|
+
// implements REQ-002
|
|
94
55
|
export function resolveEnvFilePath(envFileName, workspaceRoot) {
|
|
95
56
|
if (path.isAbsolute(envFileName)) {
|
|
96
57
|
return envFileName;
|