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.
Files changed (36) hide show
  1. package/bin/kibi-mcp +1 -1
  2. package/dist/runtime/mcp-runtime.js +43 -0
  3. package/dist/server/docs.js +4 -4
  4. package/dist/server/json-schema-to-zod.js +73 -2
  5. package/dist/server/session.js +33 -15
  6. package/dist/server/tool-registration.js +35 -62
  7. package/dist/server/tools-runtime.js +52 -2
  8. package/dist/server/tools.js +19 -5
  9. package/dist/server/transport.js +9 -5
  10. package/dist/tools/autopilot-generate.js +37 -646
  11. package/dist/tools/autopilot-test-compat.js +88 -0
  12. package/dist/tools/autopilot-test-parsers.js +120 -0
  13. package/dist/tools/check.js +13 -106
  14. package/dist/tools/coverage.js +11 -25
  15. package/dist/tools/delete.js +5 -112
  16. package/dist/tools/discovery-adapter.js +23 -0
  17. package/dist/tools/entity-query.js +1 -111
  18. package/dist/tools/find-gaps.js +11 -26
  19. package/dist/tools/graph.js +11 -24
  20. package/dist/tools/model-requirement.js +8 -347
  21. package/dist/tools/mutation-context.js +28 -0
  22. package/dist/tools/query.js +5 -50
  23. package/dist/tools/relationship-validation.js +9 -116
  24. package/dist/tools/search.js +4 -37
  25. package/dist/tools/semantic-advisor.js +2 -40
  26. package/dist/tools/skills.js +17 -57
  27. package/dist/tools/sparql.js +12 -93
  28. package/dist/tools/status.js +5 -19
  29. package/dist/tools/suggest-predicates.js +11 -2448
  30. package/dist/tools/upsert.js +21 -816
  31. package/dist/tools/validate-upsert.js +12 -48
  32. package/dist/tools-config.js +75 -802
  33. package/package.json +4 -3
  34. package/dist/semantic-advisor/analyze-prose.js +0 -1392
  35. package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
  36. package/dist/semantic-advisor/types.js +0 -1
@@ -1,27 +1,14 @@
1
- import { runJsonModuleQuery, toPrologList } from "./core-module.js";
1
+ import { executeGraph } from "kibi-cli/operations";
2
2
  // implements REQ-002, REQ-013
3
3
  export async function handleKbGraph(prolog, args) {
4
- const direction = args.direction ?? "outgoing";
5
- const depth = args.depth ?? 1;
6
- const maxNodes = args.maxNodes ?? 200;
7
- const maxEdges = args.maxEdges ?? 500;
8
- try {
9
- const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${toPrologList(args.seedIds)}, ${toPrologList(args.relationships)}, '${direction}', ${depth}, ${toPrologList(args.entityTypes)}, ${maxNodes}, ${maxEdges}, JsonString)`, "Graph execution");
10
- const nodes = payload?.nodes ?? [];
11
- return {
12
- content: [
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 { readFile } from "node:fs/promises";
2
- import path from "node:path";
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
- const STRICT_FALLBACK_CONFIDENCE = 0.69;
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 extracted = extractRequirementClaim(args);
305
- const writeSet = buildStrictWriteSet({
306
- claim: extracted.claim,
307
- statement: extracted.statement,
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
+ }
@@ -1,52 +1,7 @@
1
- import { loadEntities, paginateResults, } from "./entity-query.js";
2
- export { VALID_ENTITY_TYPES } from "./entity-query.js";
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
- const { type, id, tags, sourceFile, limit = 100, offset = 0 } = args;
9
- try {
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 { escapeAtom, toPrologAtom } from "kibi-cli/prolog/codec";
2
- function getStringField(record, field) {
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
- for (const relationship of relationships) {
95
- const fromType = await resolveEndpointType({
96
- prolog,
97
- entity,
98
- endpointId: getStringField(relationship, "from"),
99
- });
100
- const toType = await resolveEndpointType({
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
  }
@@ -1,39 +1,6 @@
1
- import { rankEntities } from "kibi-cli/search-ranking";
2
- import { resolveWorkspaceRoot } from "../workspace.js";
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
- const { query, type, limit = 20, offset = 0 } = args;
7
- const trimmedQuery = query.trim();
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
  }