kibi-mcp 0.16.1 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/semantic-advisor/analyze-prose.js +1367 -0
- package/dist/semantic-advisor/prose-coverage-evaluator.js +66 -0
- package/dist/semantic-advisor/types.js +1 -0
- package/dist/server/kb-freshness.js +88 -0
- package/dist/server/session.js +65 -4
- package/dist/server/tools.js +5 -0
- package/dist/tools/check.js +2 -0
- package/dist/tools/semantic-advisor.js +42 -0
- package/dist/tools/suggest-predicates.js +1902 -9
- package/dist/tools/upsert.js +153 -8
- package/dist/tools/validate-upsert.js +7 -1
- package/dist/tools-config.js +38 -2
- package/package.json +7 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { analyzeSemanticAdvisorInput } from "./analyze-prose.js";
|
|
2
|
+
export function evaluateProseCoverageCorpus(cases) {
|
|
3
|
+
const failures = cases.flatMap((testCase) => evaluateCase(testCase));
|
|
4
|
+
const passed = cases.length - failures.length;
|
|
5
|
+
return {
|
|
6
|
+
coverage: cases.length === 0 ? 1 : passed / cases.length,
|
|
7
|
+
summary: {
|
|
8
|
+
total: cases.length,
|
|
9
|
+
passed,
|
|
10
|
+
failed: failures.length,
|
|
11
|
+
},
|
|
12
|
+
failures,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function evaluateCase(testCase) {
|
|
16
|
+
const result = analyzeSemanticAdvisorInput({
|
|
17
|
+
payload: {
|
|
18
|
+
type: "req",
|
|
19
|
+
id: `REQ-CORPUS-${testCase.id}`,
|
|
20
|
+
properties: {
|
|
21
|
+
title: testCase.id,
|
|
22
|
+
status: "open",
|
|
23
|
+
source: "mcp://kibi/prose-coverage-corpus",
|
|
24
|
+
text_ref: testCase.text,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const suggestion = result.receipt.suggestions[0];
|
|
29
|
+
if (!suggestion) {
|
|
30
|
+
return [failure(testCase, "No semantic advisor suggestion was produced")];
|
|
31
|
+
}
|
|
32
|
+
if (suggestion.kind !== testCase.expected.kind) {
|
|
33
|
+
return [
|
|
34
|
+
failure(testCase, `Expected ${testCase.expected.kind} but received ${suggestion.kind}`),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
if (testCase.expected.kind === "predicate" &&
|
|
38
|
+
testCase.expected.predicate_name &&
|
|
39
|
+
suggestion.kind === "predicate" &&
|
|
40
|
+
suggestion.predicate.predicate_name !== testCase.expected.predicate_name) {
|
|
41
|
+
return [
|
|
42
|
+
failure(testCase, `Expected predicate ${testCase.expected.predicate_name} but received ${suggestion.predicate.predicate_name}`),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
if (testCase.expected.kind === "strict_property") {
|
|
46
|
+
if (suggestion.kind !== "strict_property") {
|
|
47
|
+
return [failure(testCase, "Expected strict property suggestion")];
|
|
48
|
+
}
|
|
49
|
+
if (testCase.expected.property_key &&
|
|
50
|
+
suggestion.claim.property_key !== testCase.expected.property_key) {
|
|
51
|
+
return [
|
|
52
|
+
failure(testCase, `Expected property ${testCase.expected.property_key} but received ${suggestion.claim.property_key}`),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
if (testCase.expected.operator &&
|
|
56
|
+
suggestion.claim.operator !== testCase.expected.operator) {
|
|
57
|
+
return [
|
|
58
|
+
failure(testCase, `Expected operator ${testCase.expected.operator} but received ${suggestion.claim.operator}`),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
function failure(testCase, reason) {
|
|
65
|
+
return { id: testCase.id, text: testCase.text, reason };
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class KbRefreshError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "KbRefreshError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function readBranchKbStamp(branchPath) {
|
|
10
|
+
const stamp = emptyStamp(branchPath);
|
|
11
|
+
const errors = [];
|
|
12
|
+
try {
|
|
13
|
+
const dirStat = await stat(branchPath);
|
|
14
|
+
stamp.dirDev = dirStat.dev;
|
|
15
|
+
stamp.dirIno = dirStat.ino;
|
|
16
|
+
stamp.dirMtimeMs = dirStat.mtimeMs;
|
|
17
|
+
stamp.dirCtimeMs = dirStat.ctimeMs;
|
|
18
|
+
stamp.dirMissing = false;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
stamp.dirMissing = true;
|
|
22
|
+
errors.push(formatStatError(branchPath, error));
|
|
23
|
+
}
|
|
24
|
+
const rdfPath = path.join(branchPath, "kb.rdf");
|
|
25
|
+
try {
|
|
26
|
+
const rdfStat = await stat(rdfPath);
|
|
27
|
+
stamp.rdfDev = rdfStat.dev;
|
|
28
|
+
stamp.rdfIno = rdfStat.ino;
|
|
29
|
+
stamp.rdfSize = rdfStat.size;
|
|
30
|
+
stamp.rdfMtimeMs = rdfStat.mtimeMs;
|
|
31
|
+
stamp.rdfCtimeMs = rdfStat.ctimeMs;
|
|
32
|
+
stamp.rdfMissing = false;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
stamp.rdfMissing = true;
|
|
36
|
+
errors.push(formatStatError(rdfPath, error));
|
|
37
|
+
}
|
|
38
|
+
stamp.errorMessage = errors.length > 0 ? errors.join("; ") : null;
|
|
39
|
+
return stamp;
|
|
40
|
+
}
|
|
41
|
+
export function sameBranchKbStamp(a, b) {
|
|
42
|
+
return (a.branchPath === b.branchPath &&
|
|
43
|
+
a.rdfDev === b.rdfDev &&
|
|
44
|
+
a.rdfIno === b.rdfIno &&
|
|
45
|
+
a.rdfSize === b.rdfSize &&
|
|
46
|
+
a.rdfMtimeMs === b.rdfMtimeMs &&
|
|
47
|
+
a.rdfCtimeMs === b.rdfCtimeMs &&
|
|
48
|
+
a.dirDev === b.dirDev &&
|
|
49
|
+
a.dirIno === b.dirIno &&
|
|
50
|
+
a.dirMtimeMs === b.dirMtimeMs &&
|
|
51
|
+
a.dirCtimeMs === b.dirCtimeMs &&
|
|
52
|
+
a.rdfMissing === b.rdfMissing &&
|
|
53
|
+
a.dirMissing === b.dirMissing &&
|
|
54
|
+
a.errorMessage === b.errorMessage);
|
|
55
|
+
}
|
|
56
|
+
export function describeBranchKbStamp(stamp) {
|
|
57
|
+
return [
|
|
58
|
+
`branchPath=${stamp.branchPath}`,
|
|
59
|
+
`rdf(dev=${stamp.rdfDev}, ino=${stamp.rdfIno}, size=${stamp.rdfSize}, mtimeMs=${stamp.rdfMtimeMs}, ctimeMs=${stamp.rdfCtimeMs})`,
|
|
60
|
+
`dir(dev=${stamp.dirDev}, ino=${stamp.dirIno}, mtimeMs=${stamp.dirMtimeMs}, ctimeMs=${stamp.dirCtimeMs})`,
|
|
61
|
+
`rdfMissing=${stamp.rdfMissing}`,
|
|
62
|
+
`dirMissing=${stamp.dirMissing}`,
|
|
63
|
+
`errorMessage=${stamp.errorMessage ?? "null"}`,
|
|
64
|
+
].join(" ");
|
|
65
|
+
}
|
|
66
|
+
function emptyStamp(branchPath) {
|
|
67
|
+
return {
|
|
68
|
+
branchPath,
|
|
69
|
+
rdfDev: null,
|
|
70
|
+
rdfIno: null,
|
|
71
|
+
rdfSize: null,
|
|
72
|
+
rdfMtimeMs: null,
|
|
73
|
+
rdfCtimeMs: null,
|
|
74
|
+
dirDev: null,
|
|
75
|
+
dirIno: null,
|
|
76
|
+
dirMtimeMs: null,
|
|
77
|
+
dirCtimeMs: null,
|
|
78
|
+
rdfMissing: true,
|
|
79
|
+
dirMissing: true,
|
|
80
|
+
errorMessage: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function formatStatError(statPath, error) {
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
return `${statPath}: ${error.message}`;
|
|
86
|
+
}
|
|
87
|
+
return `${statPath}: ${String(error)}`;
|
|
88
|
+
}
|
package/dist/server/session.js
CHANGED
|
@@ -22,6 +22,7 @@ import { PrologProcess } from "kibi-cli/prolog";
|
|
|
22
22
|
import { copyCleanSnapshot, getBranchDiagnostic, isValidBranchName, resolveActiveBranch, } from "kibi-cli/public/branch-resolver";
|
|
23
23
|
import { getBranchOverride, isMcpDebugEnabled } from "../env.js";
|
|
24
24
|
import { resolveKbPath, resolveWorkspaceRoot } from "../workspace.js";
|
|
25
|
+
import { KbRefreshError, describeBranchKbStamp, readBranchKbStamp, sameBranchKbStamp, } from "./kb-freshness.js";
|
|
25
26
|
const defaultSessionDeps = {
|
|
26
27
|
PrologProcess,
|
|
27
28
|
copyCleanSnapshot,
|
|
@@ -39,6 +40,11 @@ let isInitialized = false;
|
|
|
39
40
|
export let activeBranchName = "develop";
|
|
40
41
|
let ensurePrologTail = Promise.resolve();
|
|
41
42
|
let prologResetGeneration = 0;
|
|
43
|
+
export let attachedBranchKbPath = null;
|
|
44
|
+
let attachedBranchStamp = null;
|
|
45
|
+
export function updateAttachedBranchStamp(stamp) {
|
|
46
|
+
attachedBranchStamp = stamp;
|
|
47
|
+
}
|
|
42
48
|
export let isShuttingDown = false;
|
|
43
49
|
let shutdownTimeout = null;
|
|
44
50
|
export const inFlightRequests = new Map();
|
|
@@ -49,6 +55,8 @@ export function resetSessionStateForTests() {
|
|
|
49
55
|
activeBranchName = "develop";
|
|
50
56
|
ensurePrologTail = Promise.resolve();
|
|
51
57
|
prologResetGeneration = 0;
|
|
58
|
+
attachedBranchKbPath = null;
|
|
59
|
+
attachedBranchStamp = null;
|
|
52
60
|
isShuttingDown = false;
|
|
53
61
|
inFlightRequests.clear();
|
|
54
62
|
if (shutdownTimeout) {
|
|
@@ -146,6 +154,8 @@ export async function resetProlog(reason) {
|
|
|
146
154
|
const current = prologProcess;
|
|
147
155
|
prologProcess = null;
|
|
148
156
|
isInitialized = false;
|
|
157
|
+
attachedBranchKbPath = null;
|
|
158
|
+
attachedBranchStamp = null;
|
|
149
159
|
if (current) {
|
|
150
160
|
try {
|
|
151
161
|
await current.terminate();
|
|
@@ -155,6 +165,35 @@ export async function resetProlog(reason) {
|
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
}
|
|
168
|
+
async function refreshAttachedBranchKb(prolog, kbPath, assertGeneration) {
|
|
169
|
+
prolog.invalidateCache();
|
|
170
|
+
const detachResult = await prolog.query("kb_detach");
|
|
171
|
+
await assertGeneration();
|
|
172
|
+
if (!detachResult.success) {
|
|
173
|
+
throw new KbRefreshError(`KB refresh failed: detach failed: ${detachResult.error || "Unknown error"}`);
|
|
174
|
+
}
|
|
175
|
+
const attachResult = await prolog.query(`kb_attach('${kbPath}')`);
|
|
176
|
+
await assertGeneration();
|
|
177
|
+
if (!attachResult.success) {
|
|
178
|
+
throw new KbRefreshError(`KB refresh failed: attach failed: ${attachResult.error || "Unknown error"}`);
|
|
179
|
+
}
|
|
180
|
+
return await readBranchKbStamp(kbPath);
|
|
181
|
+
}
|
|
182
|
+
async function refreshAttachedBranchKbWithRetry(prolog, kbPath, currentStamp, assertGeneration) {
|
|
183
|
+
let postAttachStamp = await refreshAttachedBranchKb(prolog, kbPath, assertGeneration);
|
|
184
|
+
if (sameBranchKbStamp(postAttachStamp, currentStamp)) {
|
|
185
|
+
return postAttachStamp;
|
|
186
|
+
}
|
|
187
|
+
const preRetryStamp = await readBranchKbStamp(kbPath);
|
|
188
|
+
postAttachStamp = await refreshAttachedBranchKb(prolog, kbPath, assertGeneration);
|
|
189
|
+
if (!sameBranchKbStamp(postAttachStamp, preRetryStamp)) {
|
|
190
|
+
throw new KbRefreshError("KB refresh failed: stamp changed during attach");
|
|
191
|
+
}
|
|
192
|
+
return postAttachStamp;
|
|
193
|
+
}
|
|
194
|
+
function usesBranchKbPath(kbPath) {
|
|
195
|
+
return kbPath.includes("/.kb/branches/") || kbPath.includes("\\.kb\\branches\\");
|
|
196
|
+
}
|
|
158
197
|
// implements REQ-008
|
|
159
198
|
async function ensurePrologUnsafe() {
|
|
160
199
|
const generationAtStart = prologResetGeneration;
|
|
@@ -194,8 +233,27 @@ async function ensurePrologUnsafe() {
|
|
|
194
233
|
}
|
|
195
234
|
// Check if we need to switch branches
|
|
196
235
|
if (isInitialized && prologProcess?.isRunning()) {
|
|
236
|
+
const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
197
237
|
if (targetBranch === activeBranchName) {
|
|
198
|
-
|
|
238
|
+
const currentStamp = await readBranchKbStamp(kbPath);
|
|
239
|
+
if (usesBranchKbPath(kbPath) &&
|
|
240
|
+
(currentStamp.rdfMissing ||
|
|
241
|
+
currentStamp.dirMissing ||
|
|
242
|
+
currentStamp.errorMessage !== null)) {
|
|
243
|
+
throw new KbRefreshError(`KB refresh failed: branch KB snapshot is unstable: ${describeBranchKbStamp(currentStamp)}`);
|
|
244
|
+
}
|
|
245
|
+
const shouldRefresh = attachedBranchKbPath === kbPath &&
|
|
246
|
+
attachedBranchStamp !== null &&
|
|
247
|
+
usesBranchKbPath(kbPath) &&
|
|
248
|
+
!sameBranchKbStamp(currentStamp, attachedBranchStamp);
|
|
249
|
+
if (shouldRefresh) {
|
|
250
|
+
attachedBranchStamp = await refreshAttachedBranchKbWithRetry(prologProcess, kbPath, currentStamp, assertGeneration);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
attachedBranchKbPath = kbPath;
|
|
254
|
+
attachedBranchStamp = currentStamp;
|
|
255
|
+
}
|
|
256
|
+
attachedBranchKbPath = kbPath;
|
|
199
257
|
return prologProcess;
|
|
200
258
|
}
|
|
201
259
|
// Branch changed - need to detach and re-attach
|
|
@@ -214,16 +272,17 @@ async function ensurePrologUnsafe() {
|
|
|
214
272
|
}
|
|
215
273
|
// Ensure new branch KB exists
|
|
216
274
|
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
217
|
-
const newKbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
218
275
|
// Attach to new branch KB
|
|
219
|
-
const attachResult = await prologProcess.query(`kb_attach('${
|
|
276
|
+
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
220
277
|
await assertGeneration();
|
|
221
278
|
if (!attachResult.success) {
|
|
222
279
|
throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
|
|
223
280
|
}
|
|
224
281
|
activeBranchName = targetBranch;
|
|
282
|
+
attachedBranchKbPath = kbPath;
|
|
283
|
+
attachedBranchStamp = await readBranchKbStamp(kbPath);
|
|
225
284
|
debugLog(`[KIBI-MCP] Re-attached to branch: ${targetBranch}`);
|
|
226
|
-
debugLog(`[KIBI-MCP] KB path: ${
|
|
285
|
+
debugLog(`[KIBI-MCP] KB path: ${kbPath}`);
|
|
227
286
|
return prologProcess;
|
|
228
287
|
}
|
|
229
288
|
// First initialization
|
|
@@ -275,6 +334,8 @@ async function ensurePrologUnsafe() {
|
|
|
275
334
|
if (!attachResult.success) {
|
|
276
335
|
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
|
277
336
|
}
|
|
337
|
+
attachedBranchKbPath = kbPath;
|
|
338
|
+
attachedBranchStamp = await readBranchKbStamp(kbPath);
|
|
278
339
|
isInitialized = true;
|
|
279
340
|
debugLog(`[KIBI-MCP] Prolog process started (PID: ${prologProcess.getPid()})`);
|
|
280
341
|
debugLog(`[KIBI-MCP] KB attached: ${kbPath}`);
|
package/dist/server/tools.js
CHANGED
|
@@ -29,6 +29,7 @@ import { handleKbGraph } from "../tools/graph.js";
|
|
|
29
29
|
import { handleKbModelRequirement, } from "../tools/model-requirement.js";
|
|
30
30
|
import { handleKbQuery } from "../tools/query.js";
|
|
31
31
|
import { handleKbSearch } from "../tools/search.js";
|
|
32
|
+
import { handleKbSemanticAdvisor, } from "../tools/semantic-advisor.js";
|
|
32
33
|
import { handleKbSkillsList, handleKbSkillsLoad, handleKbSkillsRead, } from "../tools/skills.js";
|
|
33
34
|
import { handleSparql } from "../tools/sparql.js";
|
|
34
35
|
import { handleKbStatus } from "../tools/status.js";
|
|
@@ -85,6 +86,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
85
86
|
handleKbQuery,
|
|
86
87
|
handleKbSearch,
|
|
87
88
|
handleKbStatus,
|
|
89
|
+
handleKbSemanticAdvisor,
|
|
88
90
|
handleKbSkillsList,
|
|
89
91
|
handleKbSkillsLoad,
|
|
90
92
|
handleKbSkillsRead,
|
|
@@ -401,6 +403,9 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
401
403
|
const prolog = await runtime.ensureProlog();
|
|
402
404
|
return runtime.handleSparql(prolog, args);
|
|
403
405
|
}, runtime);
|
|
406
|
+
addTool(server, "kb_semantic_advisor", toolDef("kb_semantic_advisor").description, toolDef("kb_semantic_advisor").inputSchema, async (args) => {
|
|
407
|
+
return runtime.handleKbSemanticAdvisor(args);
|
|
408
|
+
}, runtime);
|
|
404
409
|
addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
|
|
405
410
|
const prolog = await runtime.ensureProlog();
|
|
406
411
|
return runtime.handleKbUpsert(prolog, args);
|
package/dist/tools/check.js
CHANGED
|
@@ -116,6 +116,8 @@ export async function handleKbCheck(prolog, args) {
|
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
+
// Ensure we read the latest KB state, not a cached snapshot.
|
|
120
|
+
prolog.invalidateCache();
|
|
119
121
|
// Run aggregated checks using same approach as CLI
|
|
120
122
|
// This now runs ALL rules including symbol-traceability
|
|
121
123
|
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist, checksConfig.symbolTraceability.requireAdr);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
|
|
2
|
+
function normalizeText(value) {
|
|
3
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
4
|
+
if (!text) {
|
|
5
|
+
throw new Error("Semantic advisor failed: text must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
return text;
|
|
8
|
+
}
|
|
9
|
+
function optionalString(value) {
|
|
10
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
11
|
+
return text.length > 0 ? text : undefined;
|
|
12
|
+
}
|
|
13
|
+
export async function handleKbSemanticAdvisor(args) {
|
|
14
|
+
const text = normalizeText(args.text);
|
|
15
|
+
const id = optionalString(args.id) ?? "REQ-SEMANTIC-ADVISOR-PREVIEW";
|
|
16
|
+
const title = optionalString(args.title) ?? text.split(/[.!?]/, 1)[0] ?? text;
|
|
17
|
+
const source = optionalString(args.source) ?? "mcp://kibi/semantic-advisor";
|
|
18
|
+
const result = analyzeSemanticAdvisorInput({
|
|
19
|
+
payload: {
|
|
20
|
+
type: optionalString(args.type) ?? "req",
|
|
21
|
+
id,
|
|
22
|
+
properties: {
|
|
23
|
+
title,
|
|
24
|
+
status: optionalString(args.status) ?? "open",
|
|
25
|
+
source,
|
|
26
|
+
text_ref: text,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: `kb_semantic_advisor: ${result.receipt.summary} Suggestions: ${result.receipt.suggestions.map((suggestion) => suggestion.kind).join(", ") || "none"}.`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
structuredContent: {
|
|
38
|
+
receipt: result.receipt,
|
|
39
|
+
warnings: result.warnings,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|