kibi-mcp 0.16.1 → 0.17.3

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.
@@ -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
+ }
@@ -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
- // Still on the same branch - return existing connection
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('${newKbPath}')`);
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: ${newKbPath}`);
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}`);
@@ -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);
@@ -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
+ }