kibi-mcp 0.19.0 → 0.19.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/bin/kibi-mcp +1 -1
- package/dist/runtime/mcp-runtime.js +43 -0
- package/dist/server/docs.js +4 -4
- package/dist/server/json-schema-to-zod.js +73 -2
- package/dist/server/session.js +33 -15
- package/dist/server/tool-registration.js +35 -62
- package/dist/server/tools-runtime.js +52 -2
- package/dist/server/tools.js +19 -5
- package/dist/server/transport.js +9 -5
- package/dist/tools/autopilot-generate.js +37 -646
- package/dist/tools/autopilot-test-compat.js +88 -0
- package/dist/tools/autopilot-test-parsers.js +120 -0
- package/dist/tools/check.js +13 -106
- package/dist/tools/coverage.js +11 -25
- package/dist/tools/delete.js +5 -112
- package/dist/tools/discovery-adapter.js +23 -0
- package/dist/tools/entity-query.js +1 -111
- package/dist/tools/find-gaps.js +11 -26
- package/dist/tools/graph.js +11 -24
- package/dist/tools/model-requirement.js +8 -347
- package/dist/tools/mutation-context.js +28 -0
- package/dist/tools/query.js +5 -50
- package/dist/tools/relationship-validation.js +9 -116
- package/dist/tools/search.js +4 -37
- package/dist/tools/semantic-advisor.js +2 -40
- package/dist/tools/skills.js +17 -57
- package/dist/tools/sparql.js +12 -93
- package/dist/tools/status.js +5 -19
- package/dist/tools/suggest-predicates.js +11 -2448
- package/dist/tools/upsert.js +21 -816
- package/dist/tools/validate-upsert.js +12 -48
- package/dist/tools-config.js +75 -802
- package/package.json +4 -3
- package/dist/semantic-advisor/analyze-prose.js +0 -1392
- package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
- package/dist/semantic-advisor/types.js +0 -1
package/dist/tools/graph.js
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { executeGraph } from "kibi-cli/operations";
|
|
2
2
|
// implements REQ-002, REQ-013
|
|
3
3
|
export async function handleKbGraph(prolog, args) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type: "text",
|
|
15
|
-
text: nodes.length === 0
|
|
16
|
-
? "Graph traversal returned no nodes."
|
|
17
|
-
: `Graph traversal returned ${nodes.length} nodes and ${(payload?.edges ?? []).length} edges from ${args.seedIds.join(", ")}.`,
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
...(payload !== undefined ? { structuredContent: payload } : {}),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
-
throw new Error(`Graph execution failed: ${message}`);
|
|
26
|
-
}
|
|
4
|
+
return executeGraph({ ...args }, {
|
|
5
|
+
workspaceRoot: process.cwd(),
|
|
6
|
+
signal: new AbortController().signal,
|
|
7
|
+
clock: () => new Date(),
|
|
8
|
+
prolog: {
|
|
9
|
+
query: (goal) => prolog.query(goal),
|
|
10
|
+
nextSolution: async () => null,
|
|
11
|
+
save: () => prolog.query("kb_save"),
|
|
12
|
+
},
|
|
13
|
+
});
|
|
27
14
|
}
|
|
@@ -1,351 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { buildStrictWriteSet, } from "kibi-cli/public/check-types";
|
|
4
|
-
import { getSchemaVersionStatus } from "kibi-cli/schema-version";
|
|
1
|
+
import { modelRequirementSpec, } from "kibi-cli/operations";
|
|
2
|
+
import { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "kibi-cli/operations/modeling/model-requirement";
|
|
5
3
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
6
|
-
|
|
7
|
-
const NORMATIVE_SECTION_PATTERN = /\b(requirements?|polic(?:y|ies)|rules?)\b/i;
|
|
8
|
-
function normalizeText(text) {
|
|
9
|
-
const normalized = String(text ?? "").trim();
|
|
10
|
-
if (!normalized) {
|
|
11
|
-
throw new Error("Requirement modeling failed: text must be a non-empty string");
|
|
12
|
-
}
|
|
13
|
-
return normalized;
|
|
14
|
-
}
|
|
15
|
-
function normalizeOptionalString(value) {
|
|
16
|
-
const normalized = String(value ?? "").trim();
|
|
17
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
18
|
-
}
|
|
19
|
-
function normalizeSourceFiles(sourceFiles) {
|
|
20
|
-
const seen = new Set();
|
|
21
|
-
const normalized = [];
|
|
22
|
-
for (const sourceFile of sourceFiles ?? []) {
|
|
23
|
-
const trimmed = String(sourceFile ?? "").trim();
|
|
24
|
-
if (!trimmed || seen.has(trimmed))
|
|
25
|
-
continue;
|
|
26
|
-
seen.add(trimmed);
|
|
27
|
-
normalized.push(trimmed);
|
|
28
|
-
}
|
|
29
|
-
return normalized;
|
|
30
|
-
}
|
|
31
|
-
function clampConfidence(confidence) {
|
|
32
|
-
const numeric = typeof confidence === "number" && Number.isFinite(confidence)
|
|
33
|
-
? confidence
|
|
34
|
-
: 0.8;
|
|
35
|
-
return Math.round(Math.min(1, Math.max(0, numeric)) * 100) / 100;
|
|
36
|
-
}
|
|
37
|
-
function normalizeClaimValue(value) {
|
|
38
|
-
if (typeof value === "string" ||
|
|
39
|
-
typeof value === "number" ||
|
|
40
|
-
typeof value === "boolean") {
|
|
41
|
-
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
42
|
-
throw new Error("Requirement modeling failed: value must be a finite number");
|
|
43
|
-
}
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
throw new Error("Requirement modeling failed: value must be a string, number, or boolean");
|
|
47
|
-
}
|
|
48
|
-
function stripListPrefix(value) {
|
|
49
|
-
return value
|
|
50
|
-
.replace(/^\s*[-*+]\s+/, "")
|
|
51
|
-
.replace(/^\s*\d+[.)]\s+/, "")
|
|
52
|
-
.trim();
|
|
53
|
-
}
|
|
54
|
-
function trimSentenceTail(value) {
|
|
55
|
-
return value.replace(/[\s.?!:;]+$/g, "").trim();
|
|
56
|
-
}
|
|
57
|
-
function cleanSubject(value) {
|
|
58
|
-
const cleaned = trimSentenceTail(stripListPrefix(value));
|
|
59
|
-
return cleaned.replace(/^(?:the|a|an)\s+/i, "").trim() || cleaned;
|
|
60
|
-
}
|
|
61
|
-
function cleanPredicate(value) {
|
|
62
|
-
return trimSentenceTail(stripListPrefix(value)) || "statement";
|
|
63
|
-
}
|
|
64
|
-
function fallbackSubjectFromSource(source) {
|
|
65
|
-
const basename = path
|
|
66
|
-
.basename(source, path.extname(source))
|
|
67
|
-
.replace(/[-_]+/g, " ")
|
|
68
|
-
.trim();
|
|
69
|
-
return basename || "Requirement";
|
|
70
|
-
}
|
|
71
|
-
function hasExplicitClaimFields(args) {
|
|
72
|
-
return (args.subjectKey !== undefined ||
|
|
73
|
-
args.propertyKey !== undefined ||
|
|
74
|
-
args.operator !== undefined ||
|
|
75
|
-
args.value !== undefined);
|
|
76
|
-
}
|
|
77
|
-
function buildFallbackClaim(statement, source, confidence, provenance) {
|
|
78
|
-
return {
|
|
79
|
-
claim: {
|
|
80
|
-
source,
|
|
81
|
-
subjectKey: fallbackSubjectFromSource(source),
|
|
82
|
-
propertyKey: "statement",
|
|
83
|
-
operator: "eq",
|
|
84
|
-
value: statement,
|
|
85
|
-
confidence: Math.min(confidence, STRICT_FALLBACK_CONFIDENCE),
|
|
86
|
-
...(provenance ? { provenance } : {}),
|
|
87
|
-
},
|
|
88
|
-
extractionMode: "fallback",
|
|
89
|
-
extractionWarnings: [
|
|
90
|
-
"Deterministic claim extraction could not infer a strict semantic claim; emitted a review artifact instead.",
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function extractHeuristicClaim(statement, source, confidence, provenance) {
|
|
95
|
-
const normalized = stripListPrefix(statement);
|
|
96
|
-
const retentionMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+retained\s+for\s+(?<value>\d+)\s+(?<unit>day|days|month|months|year|years)\.?$/i);
|
|
97
|
-
if (retentionMatch?.groups) {
|
|
98
|
-
const { unit, subject, value } = retentionMatch.groups;
|
|
99
|
-
if (!unit || !subject || !value)
|
|
100
|
-
return null;
|
|
101
|
-
const normalizedUnit = unit.toLowerCase().startsWith("day")
|
|
102
|
-
? "Days"
|
|
103
|
-
: unit.toLowerCase().startsWith("month")
|
|
104
|
-
? "Months"
|
|
105
|
-
: "Years";
|
|
106
|
-
return {
|
|
107
|
-
claim: {
|
|
108
|
-
source,
|
|
109
|
-
subjectKey: cleanSubject(subject),
|
|
110
|
-
propertyKey: `Retention ${normalizedUnit}`,
|
|
111
|
-
operator: "eq",
|
|
112
|
-
value: Number(value),
|
|
113
|
-
confidence,
|
|
114
|
-
...(provenance ? { provenance } : {}),
|
|
115
|
-
},
|
|
116
|
-
extractionMode: "heuristic",
|
|
117
|
-
extractionWarnings: [],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const enabledMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<state>enabled|disabled)\.?$/i);
|
|
121
|
-
if (enabledMatch?.groups) {
|
|
122
|
-
const { subject, state } = enabledMatch.groups;
|
|
123
|
-
if (!subject || !state)
|
|
124
|
-
return null;
|
|
125
|
-
return {
|
|
126
|
-
claim: {
|
|
127
|
-
source,
|
|
128
|
-
subjectKey: cleanSubject(subject),
|
|
129
|
-
propertyKey: "enabled",
|
|
130
|
-
operator: "bool",
|
|
131
|
-
value: state.toLowerCase() === "enabled",
|
|
132
|
-
confidence,
|
|
133
|
-
...(provenance ? { provenance } : {}),
|
|
134
|
-
},
|
|
135
|
-
extractionMode: "heuristic",
|
|
136
|
-
extractionWarnings: [],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const forbiddenMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+not\s+(?<predicate>.+?)\.?$/i);
|
|
140
|
-
if (forbiddenMatch?.groups) {
|
|
141
|
-
const { subject, predicate } = forbiddenMatch.groups;
|
|
142
|
-
if (!subject || !predicate)
|
|
143
|
-
return null;
|
|
144
|
-
return {
|
|
145
|
-
claim: {
|
|
146
|
-
source,
|
|
147
|
-
subjectKey: cleanSubject(subject),
|
|
148
|
-
propertyKey: cleanPredicate(predicate),
|
|
149
|
-
operator: "polarity",
|
|
150
|
-
value: "forbid",
|
|
151
|
-
confidence,
|
|
152
|
-
...(provenance ? { provenance } : {}),
|
|
153
|
-
},
|
|
154
|
-
extractionMode: "heuristic",
|
|
155
|
-
extractionWarnings: [],
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const requiredMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+(?<predicate>.+?)\.?$/i);
|
|
159
|
-
if (requiredMatch?.groups) {
|
|
160
|
-
const { subject, predicate } = requiredMatch.groups;
|
|
161
|
-
if (!subject || !predicate)
|
|
162
|
-
return null;
|
|
163
|
-
return {
|
|
164
|
-
claim: {
|
|
165
|
-
source,
|
|
166
|
-
subjectKey: cleanSubject(subject),
|
|
167
|
-
propertyKey: cleanPredicate(predicate),
|
|
168
|
-
operator: "polarity",
|
|
169
|
-
value: "require",
|
|
170
|
-
confidence,
|
|
171
|
-
...(provenance ? { provenance } : {}),
|
|
172
|
-
},
|
|
173
|
-
extractionMode: "heuristic",
|
|
174
|
-
extractionWarnings: [],
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
export function estimateNormativeSignalConfidence(statement, heading) {
|
|
180
|
-
const normalizedStatement = stripListPrefix(statement).toLowerCase();
|
|
181
|
-
if (!/\b(must|shall|should)\b/.test(normalizedStatement)) {
|
|
182
|
-
return 0;
|
|
183
|
-
}
|
|
184
|
-
let confidence = normalizedStatement.includes(" shall ")
|
|
185
|
-
? 0.86
|
|
186
|
-
: normalizedStatement.includes(" must ")
|
|
187
|
-
? 0.84
|
|
188
|
-
: 0.78;
|
|
189
|
-
if (heading && NORMATIVE_SECTION_PATTERN.test(heading)) {
|
|
190
|
-
confidence += 0.08;
|
|
191
|
-
}
|
|
192
|
-
return Math.round(Math.min(0.95, confidence) * 100) / 100;
|
|
193
|
-
}
|
|
194
|
-
export function extractRequirementClaim(args) {
|
|
195
|
-
const statement = normalizeText(args.text);
|
|
196
|
-
const sourceFiles = normalizeSourceFiles(args.sourceFiles);
|
|
197
|
-
const source = normalizeOptionalString(args.source) ?? sourceFiles[0];
|
|
198
|
-
if (!source) {
|
|
199
|
-
throw new Error("Requirement modeling failed: provide source or at least one sourceFiles entry");
|
|
200
|
-
}
|
|
201
|
-
const provenance = normalizeOptionalString(args.provenance);
|
|
202
|
-
const confidence = clampConfidence(args.confidence);
|
|
203
|
-
if (hasExplicitClaimFields(args)) {
|
|
204
|
-
if (!normalizeOptionalString(args.subjectKey) ||
|
|
205
|
-
!normalizeOptionalString(args.propertyKey) ||
|
|
206
|
-
args.operator === undefined ||
|
|
207
|
-
args.value === undefined) {
|
|
208
|
-
throw new Error("Requirement modeling failed: subjectKey, propertyKey, operator, and value must all be provided when any extracted claim field is supplied");
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
statement,
|
|
212
|
-
source,
|
|
213
|
-
sourceFiles,
|
|
214
|
-
claim: {
|
|
215
|
-
source,
|
|
216
|
-
subjectKey: normalizeOptionalString(args.subjectKey),
|
|
217
|
-
propertyKey: normalizeOptionalString(args.propertyKey),
|
|
218
|
-
operator: args.operator,
|
|
219
|
-
value: normalizeClaimValue(args.value),
|
|
220
|
-
confidence,
|
|
221
|
-
...(provenance ? { provenance } : {}),
|
|
222
|
-
},
|
|
223
|
-
extractionMode: "provided",
|
|
224
|
-
extractionWarnings: [],
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const heuristic = extractHeuristicClaim(statement, source, confidence, provenance);
|
|
228
|
-
if (heuristic) {
|
|
229
|
-
return {
|
|
230
|
-
statement,
|
|
231
|
-
source,
|
|
232
|
-
sourceFiles,
|
|
233
|
-
...heuristic,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
statement,
|
|
238
|
-
source,
|
|
239
|
-
sourceFiles,
|
|
240
|
-
...buildFallbackClaim(statement, source, confidence, provenance),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
function toRelationshipPlanRows(relationships) {
|
|
244
|
-
return relationships.map((relationship) => ({
|
|
245
|
-
type: relationship.type,
|
|
246
|
-
from: relationship.from,
|
|
247
|
-
to: relationship.to,
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
export function strictWriteSetToApplyPlan(writeSet) {
|
|
251
|
-
if (!writeSet.isStrict) {
|
|
252
|
-
return [
|
|
253
|
-
{
|
|
254
|
-
type: writeSet.observationFact.type,
|
|
255
|
-
id: writeSet.observationFact.id,
|
|
256
|
-
properties: writeSet.observationFact.properties,
|
|
257
|
-
relationships: [],
|
|
258
|
-
},
|
|
259
|
-
];
|
|
260
|
-
}
|
|
261
|
-
return [
|
|
262
|
-
{
|
|
263
|
-
type: writeSet.subjectFact.type,
|
|
264
|
-
id: writeSet.subjectFact.id,
|
|
265
|
-
properties: writeSet.subjectFact.properties,
|
|
266
|
-
relationships: [],
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
type: writeSet.propertyFact.type,
|
|
270
|
-
id: writeSet.propertyFact.id,
|
|
271
|
-
properties: writeSet.propertyFact.properties,
|
|
272
|
-
relationships: [],
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
type: writeSet.req.type,
|
|
276
|
-
id: writeSet.req.id,
|
|
277
|
-
properties: writeSet.req.properties,
|
|
278
|
-
relationships: toRelationshipPlanRows(writeSet.relationships),
|
|
279
|
-
},
|
|
280
|
-
];
|
|
281
|
-
}
|
|
282
|
-
export function writeSetPrimaryEntityId(writeSet) {
|
|
283
|
-
return writeSet.isStrict ? writeSet.req.id : writeSet.observationFact.id;
|
|
284
|
-
}
|
|
285
|
-
export async function getWorkspaceMigrationWarning(workspaceRoot = resolveWorkspaceRoot()) {
|
|
286
|
-
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
287
|
-
let rawConfig;
|
|
288
|
-
try {
|
|
289
|
-
rawConfig = await readFile(configPath, "utf8");
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
try {
|
|
295
|
-
const parsed = JSON.parse(rawConfig);
|
|
296
|
-
const status = getSchemaVersionStatus(parsed ?? undefined);
|
|
297
|
-
return status.warning;
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
return "KB config schemaVersion could not be read and should be checked before applying automated modeling.";
|
|
301
|
-
}
|
|
302
|
-
}
|
|
4
|
+
export { estimateNormativeSignalConfidence, extractRequirementClaim, getWorkspaceMigrationWarning, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, };
|
|
303
5
|
export async function handleKbModelRequirement(_prolog, args) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
309
|
-
const applyPlan = strictWriteSetToApplyPlan(writeSet);
|
|
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
|
-
];
|
|
320
|
-
const strictSummary = writeSet.isStrict
|
|
321
|
-
? `Modeled strict requirement into ${applyPlan.length} sequential applyPlan step(s).`
|
|
322
|
-
: "Modeled a non-blocking observation review artifact; deterministic claim extraction stayed below the strict threshold.";
|
|
323
|
-
const structuredContent = {
|
|
324
|
-
statement: extracted.statement,
|
|
325
|
-
source: extracted.source,
|
|
326
|
-
sourceFiles: extracted.sourceFiles,
|
|
327
|
-
claim: extracted.claim,
|
|
328
|
-
writeSet,
|
|
329
|
-
applyPlan,
|
|
330
|
-
isStrict: writeSet.isStrict,
|
|
331
|
-
confidence: writeSet.confidence,
|
|
332
|
-
extractionMode: extracted.extractionMode,
|
|
333
|
-
extractionWarnings: extracted.extractionWarnings,
|
|
334
|
-
warnings,
|
|
335
|
-
migrationWarning,
|
|
336
|
-
};
|
|
337
|
-
return {
|
|
338
|
-
content: [
|
|
339
|
-
{
|
|
340
|
-
type: "text",
|
|
341
|
-
text: migrationWarning
|
|
342
|
-
? `${strictSummary} Migration warning included.`
|
|
343
|
-
: strictSummary,
|
|
344
|
-
},
|
|
345
|
-
],
|
|
346
|
-
structuredContent,
|
|
347
|
-
applyPlan,
|
|
348
|
-
writeSet,
|
|
349
|
-
migrationWarning,
|
|
6
|
+
const context = {
|
|
7
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
8
|
+
signal: new AbortController().signal,
|
|
9
|
+
clock: () => new Date(),
|
|
350
10
|
};
|
|
11
|
+
return modelRequirementSpec.execute(args, context);
|
|
351
12
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
3
|
+
const filesystem = {
|
|
4
|
+
readFile: (filePath) => fs.readFile(filePath, "utf8"),
|
|
5
|
+
writeFile: (filePath, data) => fs.writeFile(filePath, data, "utf8"),
|
|
6
|
+
mkdir: async (directoryPath) => {
|
|
7
|
+
await fs.mkdir(directoryPath, { recursive: true });
|
|
8
|
+
},
|
|
9
|
+
stat: (filePath) => fs.stat(filePath),
|
|
10
|
+
};
|
|
11
|
+
// implements REQ-kibi-operation-interface-parity
|
|
12
|
+
export function createMutationContext(prolog) {
|
|
13
|
+
const port = prolog === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: {
|
|
16
|
+
query: (goal) => prolog.query(goal),
|
|
17
|
+
nextSolution: async () => null,
|
|
18
|
+
invalidateCache: () => prolog.invalidateCache(),
|
|
19
|
+
save: () => prolog.query("kb_save"),
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
23
|
+
signal: new AbortController().signal,
|
|
24
|
+
clock: () => new Date(),
|
|
25
|
+
fs: filesystem,
|
|
26
|
+
...(port === undefined ? {} : { prolog: port }),
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/tools/query.js
CHANGED
|
@@ -1,52 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Handle kb.query tool calls
|
|
5
|
-
* Reuses query logic from CLI command
|
|
6
|
-
*/
|
|
1
|
+
import { VALID_ENTITY_TYPES, querySpec } from "kibi-cli/operations";
|
|
2
|
+
import { createDiscoveryContext } from "./discovery-adapter.js";
|
|
3
|
+
export { VALID_ENTITY_TYPES };
|
|
7
4
|
export async function handleKbQuery(prolog, args) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const queryArgs = {
|
|
11
|
-
...(type !== undefined ? { type } : {}),
|
|
12
|
-
...(id !== undefined ? { id } : {}),
|
|
13
|
-
...(tags !== undefined ? { tags } : {}),
|
|
14
|
-
...(sourceFile !== undefined ? { sourceFile } : {}),
|
|
15
|
-
};
|
|
16
|
-
const results = await loadEntities(prolog, queryArgs);
|
|
17
|
-
const paginated = paginateResults(results, limit, offset);
|
|
18
|
-
// Build human-readable text with entity IDs and titles
|
|
19
|
-
let text;
|
|
20
|
-
if (results.length === 0) {
|
|
21
|
-
text = `No entities found${type ? ` of type '${type}'` : ""}.`;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const details = paginated
|
|
25
|
-
.map((e) => {
|
|
26
|
-
const id = e.id.replace(/^file:\/\/.*\//, "");
|
|
27
|
-
const title = e.title;
|
|
28
|
-
const status = e.status;
|
|
29
|
-
return `${id} (${title}, status=${status})`;
|
|
30
|
-
})
|
|
31
|
-
.join(", ");
|
|
32
|
-
text = `Found ${results.length} entities${type ? ` of type '${type}'` : ""}. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${details}`;
|
|
33
|
-
}
|
|
34
|
-
// Return MCP structured response
|
|
35
|
-
return {
|
|
36
|
-
content: [
|
|
37
|
-
{
|
|
38
|
-
type: "text",
|
|
39
|
-
text,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
structuredContent: {
|
|
43
|
-
entities: paginated,
|
|
44
|
-
count: results.length,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
-
throw new Error(`Query execution failed: ${message}`);
|
|
51
|
-
}
|
|
5
|
+
// implements REQ-kibi-operation-interface-parity
|
|
6
|
+
return querySpec.execute(args, createDiscoveryContext(prolog));
|
|
52
7
|
}
|
|
@@ -1,118 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const value = record[field];
|
|
4
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
5
|
-
throw new Error(`Relationship ${field} must be a non-empty string`);
|
|
6
|
-
}
|
|
7
|
-
return value;
|
|
8
|
-
}
|
|
9
|
-
function normalizePrologAtom(value) {
|
|
10
|
-
return value.replace(/^['"]|['"]$/g, "");
|
|
11
|
-
}
|
|
12
|
-
async function resolveEndpointType(input) {
|
|
13
|
-
if (input.endpointId === input.entity.id) {
|
|
14
|
-
const type = input.entity.type;
|
|
15
|
-
if (typeof type === "string" && type.trim() !== "") {
|
|
16
|
-
return type;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
let result;
|
|
20
|
-
try {
|
|
21
|
-
result = await input.prolog.query(`kb_entity('${escapeAtom(input.endpointId)}', Type, _)`);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
if (error instanceof Error) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
if (!result.success) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const rawType = result.bindings?.Type;
|
|
33
|
-
if (typeof rawType !== "string" || rawType.trim() === "") {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return normalizePrologAtom(rawType);
|
|
37
|
-
}
|
|
38
|
-
function relationshipRecipe(tuple) {
|
|
39
|
-
if (tuple.relType === "verified_by" &&
|
|
40
|
-
tuple.fromType === "fact" &&
|
|
41
|
-
tuple.toType === "test") {
|
|
42
|
-
return [
|
|
43
|
-
"Facts are not directly verified by tests.",
|
|
44
|
-
"Create or update a requirement and link REQ -> TEST with verified_by.",
|
|
45
|
-
"Link the requirement to the fact with constrains or requires_property.",
|
|
46
|
-
].join(" ");
|
|
47
|
-
}
|
|
48
|
-
if (tuple.relType === "validates" &&
|
|
49
|
-
tuple.fromType === "test" &&
|
|
50
|
-
tuple.toType === "fact") {
|
|
51
|
-
return [
|
|
52
|
-
"Tests validate requirements or scenarios, not facts directly.",
|
|
53
|
-
"Create or update a requirement and link TEST -> REQ with validates.",
|
|
54
|
-
"Link the requirement to the fact with constrains or requires_property.",
|
|
55
|
-
].join(" ");
|
|
56
|
-
}
|
|
57
|
-
if (tuple.relType === "verified_by") {
|
|
58
|
-
return "verified_by is only valid as req/scenario -> test.";
|
|
59
|
-
}
|
|
60
|
-
if (tuple.relType === "validates") {
|
|
61
|
-
return "validates is only valid as test -> req/scenario.";
|
|
62
|
-
}
|
|
63
|
-
return "Use a typed relationship from docs/entity-schema.md, or relates_to only as a reviewed escape hatch.";
|
|
64
|
-
}
|
|
65
|
-
export function formatInvalidRelationshipTuple(tuple) {
|
|
66
|
-
return `Invalid relationship: ${tuple.relType} from ${tuple.fromType} to ${tuple.toType}. ${relationshipRecipe(tuple)}`;
|
|
67
|
-
}
|
|
68
|
-
export function formatInvalidRelationshipError(rawError) {
|
|
69
|
-
const placeholderMatch = rawError.match(/Invalid relationship:\s*~w from ~w to ~w-\[([^,\]]+),([^,\]]+),([^\]]+)\]/);
|
|
70
|
-
if (placeholderMatch) {
|
|
71
|
-
const [, relType, fromType, toType] = placeholderMatch;
|
|
72
|
-
if (relType && fromType && toType) {
|
|
73
|
-
return formatInvalidRelationshipTuple({ relType, fromType, toType });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const readableMatch = rawError.match(/Invalid relationship:\s*([^\s]+) from ([^\s]+) to ([^\s.]+)/);
|
|
77
|
-
if (readableMatch) {
|
|
78
|
-
const [, relType, fromType, toType] = readableMatch;
|
|
79
|
-
if (relType && fromType && toType) {
|
|
80
|
-
return formatInvalidRelationshipTuple({ relType, fromType, toType });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
export function formatRelationshipSourceMismatch(entityId, relationship) {
|
|
86
|
-
const from = getStringField(relationship, "from");
|
|
87
|
-
const to = getStringField(relationship, "to");
|
|
88
|
-
return [
|
|
89
|
-
`Relationship source must match the upserted entity ${entityId}; received from=${from}.`,
|
|
90
|
-
`To add ${from} -> ${to}, upsert ${from} instead and include the relationship in that call.`,
|
|
91
|
-
].join(" ");
|
|
92
|
-
}
|
|
1
|
+
import { formatInvalidRelationshipError, formatInvalidRelationshipTuple, formatRelationshipSourceMismatch, validateLiveRelationshipTargets as validateSharedTargets, } from "kibi-cli/operations/mutation/relationships";
|
|
2
|
+
export { formatInvalidRelationshipError, formatInvalidRelationshipTuple, formatRelationshipSourceMismatch, };
|
|
93
3
|
export async function validateLiveRelationshipTargets(prolog, entity, relationships) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
prolog,
|
|
102
|
-
entity,
|
|
103
|
-
endpointId: getStringField(relationship, "to"),
|
|
104
|
-
});
|
|
105
|
-
if (fromType === null || toType === null) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const tuple = {
|
|
109
|
-
relType: getStringField(relationship, "type"),
|
|
110
|
-
fromType,
|
|
111
|
-
toType,
|
|
112
|
-
};
|
|
113
|
-
const result = await prolog.query(`once(kb:validate_relationship(${toPrologAtom(tuple.relType)}, ${toPrologAtom(tuple.fromType)}, ${toPrologAtom(tuple.toType)}))`);
|
|
114
|
-
if (!result.success) {
|
|
115
|
-
throw new Error(formatInvalidRelationshipTuple(tuple));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
4
|
+
const port = {
|
|
5
|
+
query: (goal) => prolog.query(goal),
|
|
6
|
+
nextSolution: async () => null,
|
|
7
|
+
invalidateCache: () => prolog.invalidateCache(),
|
|
8
|
+
save: () => prolog.query("kb_save"),
|
|
9
|
+
};
|
|
10
|
+
await validateSharedTargets(port, entity, relationships);
|
|
118
11
|
}
|
package/dist/tools/search.js
CHANGED
|
@@ -1,39 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadEntities, paginateResults, validateEntityType, } from "./entity-query.js";
|
|
4
|
-
// implements REQ-mcp-search-discovery, REQ-002
|
|
1
|
+
import { searchSpec } from "kibi-cli/operations";
|
|
2
|
+
import { createDiscoveryContext } from "./discovery-adapter.js";
|
|
5
3
|
export async function handleKbSearch(prolog, args) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (!trimmedQuery) {
|
|
9
|
-
throw new Error("Search execution failed: query must be a non-empty string");
|
|
10
|
-
}
|
|
11
|
-
validateEntityType(type);
|
|
12
|
-
try {
|
|
13
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
14
|
-
const entities = await loadEntities(prolog, {
|
|
15
|
-
...(type !== undefined ? { type } : {}),
|
|
16
|
-
});
|
|
17
|
-
const matches = await rankEntities(entities, trimmedQuery, workspaceRoot);
|
|
18
|
-
const paginated = paginateResults(matches, limit, offset);
|
|
19
|
-
const text = matches.length === 0
|
|
20
|
-
? `No search results for '${trimmedQuery}'.`
|
|
21
|
-
: `Found ${matches.length} search results for '${trimmedQuery}'. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${paginated
|
|
22
|
-
.map((match) => `${match.entity.id} [${match.reasons.join(", ")}]`)
|
|
23
|
-
.join(", ")}`;
|
|
24
|
-
return {
|
|
25
|
-
content: [{ type: "text", text }],
|
|
26
|
-
structuredContent: {
|
|
27
|
-
results: paginated,
|
|
28
|
-
count: matches.length,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
-
if (message.startsWith("Search execution failed:")) {
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
throw new Error(`Search execution failed: ${message}`);
|
|
38
|
-
}
|
|
4
|
+
// implements REQ-kibi-operation-interface-parity, REQ-mcp-search-discovery
|
|
5
|
+
return searchSpec.execute(args, createDiscoveryContext(prolog));
|
|
39
6
|
}
|