kibi-mcp 0.16.0 → 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 +16 -0
- package/dist/tools/check.js +2 -0
- package/dist/tools/model-requirement.js +10 -0
- package/dist/tools/semantic-advisor.js +42 -0
- package/dist/tools/sparql.js +96 -0
- package/dist/tools/suggest-predicates.js +1905 -9
- package/dist/tools/upsert.js +354 -85
- package/dist/tools/validate-upsert.js +43 -0
- package/dist/tools-config.js +109 -7
- 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,10 +29,13 @@ 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";
|
|
34
|
+
import { handleSparql } from "../tools/sparql.js";
|
|
33
35
|
import { handleKbStatus } from "../tools/status.js";
|
|
34
36
|
import { handleKbSuggestPredicates, } from "../tools/suggest-predicates.js";
|
|
35
37
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
38
|
+
import { handleKbValidateUpsert } from "../tools/validate-upsert.js";
|
|
36
39
|
const DEFAULT_TOOL_TIMEOUT_MS = 90_000;
|
|
37
40
|
const TOOL_TIMEOUT_ENV = "KIBI_MCP_TOOL_TIMEOUT_MS";
|
|
38
41
|
const defaultToolsServerDeps = {
|
|
@@ -79,13 +82,16 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
79
82
|
handleKbDelete,
|
|
80
83
|
handleKbFindGaps,
|
|
81
84
|
handleKbGraph,
|
|
85
|
+
handleSparql,
|
|
82
86
|
handleKbQuery,
|
|
83
87
|
handleKbSearch,
|
|
84
88
|
handleKbStatus,
|
|
89
|
+
handleKbSemanticAdvisor,
|
|
85
90
|
handleKbSkillsList,
|
|
86
91
|
handleKbSkillsLoad,
|
|
87
92
|
handleKbSkillsRead,
|
|
88
93
|
handleKbUpsert,
|
|
94
|
+
handleKbValidateUpsert,
|
|
89
95
|
handleKbModelRequirement,
|
|
90
96
|
handleKbSuggestPredicates,
|
|
91
97
|
handleKbAutopilotGenerate,
|
|
@@ -393,10 +399,20 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
393
399
|
const prolog = await runtime.ensureProlog();
|
|
394
400
|
return runtime.handleKbGraph(prolog, args);
|
|
395
401
|
}, runtime);
|
|
402
|
+
addTool(server, "kb_sparql_remote", toolDef("kb_sparql_remote").description, toolDef("kb_sparql_remote").inputSchema, async (args) => {
|
|
403
|
+
const prolog = await runtime.ensureProlog();
|
|
404
|
+
return runtime.handleSparql(prolog, args);
|
|
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);
|
|
396
409
|
addTool(server, "kb_upsert", toolDef("kb_upsert").description, toolDef("kb_upsert").inputSchema, async (args) => {
|
|
397
410
|
const prolog = await runtime.ensureProlog();
|
|
398
411
|
return runtime.handleKbUpsert(prolog, args);
|
|
399
412
|
}, runtime);
|
|
413
|
+
addTool(server, "kb_validate_upsert", toolDef("kb_validate_upsert").description, toolDef("kb_validate_upsert").inputSchema, async (args) => {
|
|
414
|
+
return runtime.handleKbValidateUpsert(args);
|
|
415
|
+
}, runtime);
|
|
400
416
|
addTool(server, "kb_delete", toolDef("kb_delete").description, toolDef("kb_delete").inputSchema, async (args) => {
|
|
401
417
|
const prolog = await runtime.ensureProlog();
|
|
402
418
|
return runtime.handleKbDelete(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);
|
|
@@ -308,6 +308,15 @@ export async function handleKbModelRequirement(_prolog, args) {
|
|
|
308
308
|
});
|
|
309
309
|
const applyPlan = strictWriteSetToApplyPlan(writeSet);
|
|
310
310
|
const migrationWarning = await getWorkspaceMigrationWarning();
|
|
311
|
+
const warnings = writeSet.isStrict
|
|
312
|
+
? []
|
|
313
|
+
: [
|
|
314
|
+
{
|
|
315
|
+
kind: "low_confidence_observation_downgrade",
|
|
316
|
+
message: `Claim confidence ${writeSet.confidence.toFixed(2)} is below the strict threshold 0.70, so Kibi emitted an observation fact instead of strict subject/property facts.`,
|
|
317
|
+
nextAction: "If this is normative, provide subjectKey, propertyKey, operator, and value explicitly, then apply the returned strict write-set sequentially.",
|
|
318
|
+
},
|
|
319
|
+
];
|
|
311
320
|
const strictSummary = writeSet.isStrict
|
|
312
321
|
? `Modeled strict requirement into ${applyPlan.length} sequential applyPlan step(s).`
|
|
313
322
|
: "Modeled a non-blocking observation review artifact; deterministic claim extraction stayed below the strict threshold.";
|
|
@@ -322,6 +331,7 @@ export async function handleKbModelRequirement(_prolog, args) {
|
|
|
322
331
|
confidence: writeSet.confidence,
|
|
323
332
|
extractionMode: extracted.extractionMode,
|
|
324
333
|
extractionWarnings: extracted.extractionWarnings,
|
|
334
|
+
warnings,
|
|
325
335
|
migrationWarning,
|
|
326
336
|
};
|
|
327
337
|
return {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { runJsonModuleQuery, toPrologAtom } from "./core-module.js";
|
|
2
|
+
// implements REQ-002, REQ-013
|
|
3
|
+
export async function handleSparql(prolog, args) {
|
|
4
|
+
validateSparqlArgs(args);
|
|
5
|
+
try {
|
|
6
|
+
const payload = await runJsonModuleQuery(prolog, "sparql_client.pl", `kibi_sparql_client:remote_sparql_select_json(${toPrologAtom(args.endpoint)}, ${toPrologAtom(args.query)}, ${toSparqlOptions(args)}, JsonString)`, "SPARQL remote query");
|
|
7
|
+
const rows = payload?.rows ?? [];
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: "text",
|
|
12
|
+
text: `Remote SPARQL query returned ${rows.length} row${rows.length === 1 ? "" : "s"}.`,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
...(payload !== undefined ? { structuredContent: payload } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
throw new Error(`SPARQL remote query failed: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function validateSparqlArgs(args) {
|
|
24
|
+
if (!args.endpoint || args.endpoint.trim().length === 0) {
|
|
25
|
+
throw new Error("SPARQL endpoint is required");
|
|
26
|
+
}
|
|
27
|
+
if (!args.query || args.query.trim().length === 0) {
|
|
28
|
+
throw new Error("SPARQL query is required");
|
|
29
|
+
}
|
|
30
|
+
if (!isRemoteHttpEndpoint(args.endpoint)) {
|
|
31
|
+
throw new Error("SPARQL endpoint must be an http:// or https:// URL");
|
|
32
|
+
}
|
|
33
|
+
if (!isSelectQuery(args.query)) {
|
|
34
|
+
throw new Error("SPARQL query must be a SELECT query");
|
|
35
|
+
}
|
|
36
|
+
if (!isPublicRemoteEndpoint(args.endpoint)) {
|
|
37
|
+
throw new Error("SPARQL endpoint must target a public remote host");
|
|
38
|
+
}
|
|
39
|
+
if (args.timeoutMs !== undefined &&
|
|
40
|
+
(!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
41
|
+
throw new Error("SPARQL timeoutMs must be a positive number when provided");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isRemoteHttpEndpoint(endpoint) {
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(endpoint);
|
|
47
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isSelectQuery(query) {
|
|
54
|
+
return /^\s*select\b/i.test(query);
|
|
55
|
+
}
|
|
56
|
+
function isPublicRemoteEndpoint(endpoint) {
|
|
57
|
+
const url = new URL(endpoint);
|
|
58
|
+
const host = normalizeHostname(url.hostname);
|
|
59
|
+
return !isLocalOrPrivateHost(host);
|
|
60
|
+
}
|
|
61
|
+
function normalizeHostname(hostname) {
|
|
62
|
+
return hostname
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/^\[/, "")
|
|
65
|
+
.replace(/\]$/, "")
|
|
66
|
+
.replace(/\.$/, "");
|
|
67
|
+
}
|
|
68
|
+
function isLocalOrPrivateHost(host) {
|
|
69
|
+
if (host === "localhost" ||
|
|
70
|
+
host.endsWith(".localhost") ||
|
|
71
|
+
host === "0.0.0.0" ||
|
|
72
|
+
host === "::1" ||
|
|
73
|
+
(host.includes(":") &&
|
|
74
|
+
(host.startsWith("fe80:") ||
|
|
75
|
+
host.startsWith("fc") ||
|
|
76
|
+
host.startsWith("fd")))) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const octets = host.split(".").map((part) => Number(part));
|
|
80
|
+
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet))) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const [first = -1, second = -1] = octets;
|
|
84
|
+
return (first === 10 ||
|
|
85
|
+
first === 127 ||
|
|
86
|
+
(first === 169 && second === 254) ||
|
|
87
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
88
|
+
(first === 192 && second === 168));
|
|
89
|
+
}
|
|
90
|
+
function toSparqlOptions(args) {
|
|
91
|
+
if (args.timeoutMs === undefined) {
|
|
92
|
+
return "[]";
|
|
93
|
+
}
|
|
94
|
+
const timeoutSeconds = Math.max(1, Math.ceil(args.timeoutMs / 1000));
|
|
95
|
+
return `[timeout(${timeoutSeconds})]`;
|
|
96
|
+
}
|