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
@@ -0,0 +1,88 @@
1
+ import { presentAutopilot, selectAutopilotCandidates, } from "kibi-cli/operations";
2
+ import { resolveWorkspaceRoot } from "../workspace.js";
3
+ import { activation, candidate, record, signal, } from "./autopilot-test-parsers.js";
4
+ let testDependencies;
5
+ export function setTestDependencies(dependencies) {
6
+ testDependencies = dependencies;
7
+ }
8
+ export function resetTestDependencies() {
9
+ testDependencies = undefined;
10
+ }
11
+ async function invoke(dependencies, name, values) {
12
+ const callable = dependencies[name];
13
+ return typeof callable === "function"
14
+ ? Reflect.apply(callable, undefined, [...values])
15
+ : undefined;
16
+ }
17
+ export async function executeTestDependencies(prolog, input) {
18
+ const dependencies = testDependencies;
19
+ if (!dependencies)
20
+ return null;
21
+ const root = resolveWorkspaceRoot();
22
+ const policy = activation(await invoke(dependencies, "resolveActivationPolicy", [root, prolog]));
23
+ if (!policy)
24
+ return null;
25
+ const discovered = record(await invoke(dependencies, "discoverProviderEvidence", [root, policy]));
26
+ const rawSummary = record(discovered?.summary);
27
+ const summary = {
28
+ activationState: policy.activationState,
29
+ activationMode: policy.activationMode,
30
+ applyBlocked: policy.applyBlocked,
31
+ reason: policy.reason,
32
+ providersRun: [],
33
+ providerCounts: {},
34
+ detectedLanguages: [],
35
+ detectedTestFrameworks: [],
36
+ excludedRoots: [],
37
+ truncated: false,
38
+ scanWarnings: [],
39
+ ...(rawSummary ? Object.fromEntries(Object.entries(rawSummary)) : {}),
40
+ };
41
+ const rawEntities = await invoke(dependencies, "loadEntities", [prolog, {}]);
42
+ const existingIds = new Set(Array.isArray(rawEntities)
43
+ ? rawEntities
44
+ .map((entry) => record(entry)?.id)
45
+ .filter((id) => typeof id === "string")
46
+ : []);
47
+ const discoveryInput = {
48
+ evidence: Array.isArray(discovered?.evidence) ? discovered.evidence : [],
49
+ };
50
+ const context = { ids: existingIds, workspaceRoot: root };
51
+ const minConfidence = Math.max(0.6, Math.min(0.95, input.minConfidence ?? 0.8));
52
+ const all = [];
53
+ for (const name of [
54
+ "buildTypedMarkdownCandidates",
55
+ "buildSymbolManifestCandidates",
56
+ "buildGenericMarkdownCandidates",
57
+ "buildNormativeRequirementCandidates",
58
+ "buildProviderEvidenceCandidates",
59
+ ]) {
60
+ const result = await invoke(dependencies, name, [
61
+ discoveryInput,
62
+ context,
63
+ minConfidence,
64
+ ]);
65
+ if (Array.isArray(result))
66
+ all.push(...result.flatMap((entry) => candidate(entry) ?? []));
67
+ }
68
+ const rawSignals = await invoke(dependencies, "collectSourceOnlyAuthoringSignals", [discoveryInput, context, minConfidence]);
69
+ const signals = Array.isArray(rawSignals)
70
+ ? rawSignals.flatMap((entry) => signal(entry) ?? [])
71
+ : [];
72
+ const selected = selectAutopilotCandidates(all.filter((entry) => entry.confidence >= minConfidence), existingIds, input.entityTypes, Math.max(1, Math.min(200, Math.trunc(input.maxCandidates ?? 50))));
73
+ const warning = await invoke(dependencies, "getWorkspaceMigrationWarning", [
74
+ root,
75
+ ]);
76
+ return presentAutopilot({
77
+ root,
78
+ activation: policy,
79
+ discoverySummary: summary,
80
+ migrationWarning: typeof warning === "string" ? warning : null,
81
+ ...(input.bootstrapContext
82
+ ? { bootstrapContext: input.bootstrapContext }
83
+ : {}),
84
+ candidates: selected.candidates,
85
+ sourceOnlySignals: signals,
86
+ suppressedCandidates: selected.suppressed,
87
+ });
88
+ }
@@ -0,0 +1,120 @@
1
+ export function record(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value)
3
+ ? Object.fromEntries(Object.entries(value))
4
+ : null;
5
+ }
6
+ export function candidate(value) {
7
+ const item = record(value);
8
+ if (!item ||
9
+ typeof item.candidateId !== "string" ||
10
+ typeof item.entityType !== "string" ||
11
+ typeof item.title !== "string" ||
12
+ typeof item.sourceKind !== "string" ||
13
+ typeof item.sourcePath !== "string" ||
14
+ typeof item.confidence !== "number" ||
15
+ !Array.isArray(item.applyPlan))
16
+ return null;
17
+ const relationships = Array.isArray(item.relationships)
18
+ ? item.relationships.flatMap((entry) => {
19
+ const relation = record(entry);
20
+ return relation &&
21
+ typeof relation.type === "string" &&
22
+ typeof relation.from === "string" &&
23
+ typeof relation.to === "string"
24
+ ? [{ type: relation.type, from: relation.from, to: relation.to }]
25
+ : [];
26
+ })
27
+ : [];
28
+ const applyPlan = item.applyPlan
29
+ .map(record)
30
+ .filter((entry) => entry !== null);
31
+ return {
32
+ candidateId: item.candidateId,
33
+ entityType: item.entityType,
34
+ title: item.title,
35
+ sourceKind: item.sourceKind,
36
+ sourcePath: item.sourcePath,
37
+ confidence: item.confidence,
38
+ confidenceBand: typeof item.confidenceBand === "string" ? item.confidenceBand : "medium",
39
+ evidence: Array.isArray(item.evidence)
40
+ ? item.evidence.filter((entry) => typeof entry === "string")
41
+ : [],
42
+ relationships,
43
+ applyPlan,
44
+ };
45
+ }
46
+ export function signal(value) {
47
+ const item = record(value);
48
+ if (!item ||
49
+ !["req", "scenario", "test"].includes(String(item.kind)) ||
50
+ typeof item.title !== "string" ||
51
+ typeof item.sourcePath !== "string" ||
52
+ typeof item.confidence !== "number")
53
+ return null;
54
+ const kind = item.kind === "req"
55
+ ? "req"
56
+ : item.kind === "scenario"
57
+ ? "scenario"
58
+ : "test";
59
+ return {
60
+ kind,
61
+ title: item.title,
62
+ sourcePath: item.sourcePath,
63
+ confidence: item.confidence,
64
+ evidence: Array.isArray(item.evidence)
65
+ ? item.evidence.filter((entry) => typeof entry === "string")
66
+ : [],
67
+ };
68
+ }
69
+ export function activation(value) {
70
+ const item = record(value);
71
+ if (!item ||
72
+ typeof item.activationState !== "string" ||
73
+ typeof item.activationMode !== "string" ||
74
+ typeof item.applyBlocked !== "boolean" ||
75
+ typeof item.allowCandidateGeneration !== "boolean" ||
76
+ typeof item.reason !== "string")
77
+ return null;
78
+ const states = new Set([
79
+ "root_uninitialized",
80
+ "root_partial",
81
+ "vendored_only",
82
+ "root_active_thin",
83
+ "root_active_seeded",
84
+ ]);
85
+ const modes = new Set([
86
+ "cold_start_bootstrap",
87
+ "repair_bootstrap",
88
+ "attached_thin_handoff",
89
+ "attached_seeded_handoff",
90
+ "vendored_blocked",
91
+ ]);
92
+ if (!states.has(item.activationState) || !modes.has(item.activationMode))
93
+ return null;
94
+ return {
95
+ activationState: item.activationState === "root_partial"
96
+ ? "root_partial"
97
+ : item.activationState === "vendored_only"
98
+ ? "vendored_only"
99
+ : item.activationState === "root_active_thin"
100
+ ? "root_active_thin"
101
+ : item.activationState === "root_active_seeded"
102
+ ? "root_active_seeded"
103
+ : "root_uninitialized",
104
+ activationMode: item.activationMode === "repair_bootstrap"
105
+ ? "repair_bootstrap"
106
+ : item.activationMode === "attached_thin_handoff"
107
+ ? "attached_thin_handoff"
108
+ : item.activationMode === "attached_seeded_handoff"
109
+ ? "attached_seeded_handoff"
110
+ : item.activationMode === "vendored_blocked"
111
+ ? "vendored_blocked"
112
+ : "cold_start_bootstrap",
113
+ applyBlocked: item.applyBlocked,
114
+ allowCandidateGeneration: item.allowCandidateGeneration,
115
+ reason: item.reason,
116
+ ...(typeof item.handoffMessage === "string"
117
+ ? { handoffMessage: item.handoffMessage }
118
+ : {}),
119
+ };
120
+ }
@@ -1,114 +1,21 @@
1
- import { collectFullKbQualityDiagnostics, } from "kibi-cli/public/impact-diagnostics";
1
+ import { checkSpec } from "kibi-cli/operations";
2
2
  import { resolveWorkspaceRoot } from "../workspace.js";
3
- import { getEffectiveRules, loadChecksConfig } from "./check-config.js";
4
- import { buildStructuredContent, formatImpactText, formatQualityDiagnosticsText, formatViolationText, } from "./check-format.js";
5
- import { analyzeKbCheckImpact } from "./check-impact.js";
6
- import { runAggregatedChecks } from "./check-prolog.js";
7
- function qualityDiagnosticsFromImpact(impactResult) {
8
- if (impactResult === undefined) {
9
- return [];
10
- }
11
- return impactResult.impactDiagnostics.filter((diagnostic) => !diagnostic.blocking && diagnostic.severity !== "error");
12
- }
13
- function buildSummary(input) {
14
- const sections = [formatViolationText(input.violations)];
15
- if (input.impactResult !== undefined) {
16
- sections.push(formatImpactText(input.impactResult));
17
- }
18
- if (input.qualityDiagnostics.length > 0) {
19
- sections.push(formatQualityDiagnosticsText(input.qualityDiagnostics));
20
- }
21
- return sections.join("\n");
22
- }
23
3
  /**
24
4
  * Handle kb_check tool calls - run validation rules on the KB
25
5
  * Reuses validation logic from CLI check command
26
6
  */
27
7
  // implements REQ-002
28
8
  export async function handleKbCheck(prolog, args) {
29
- const { rules, workspaceRoot: workspaceOverride } = args;
30
- const hasExplicitRules = rules !== undefined;
31
- try {
32
- const workspaceRoot = workspaceOverride ?? resolveWorkspaceRoot();
33
- const checksConfig = await loadChecksConfig(workspaceRoot);
34
- const rulesAllowlist = getEffectiveRules(checksConfig.rules, rules);
35
- const impactResult = analyzeKbCheckImpact(workspaceRoot, args);
36
- const impactQualityDiagnostics = qualityDiagnosticsFromImpact(impactResult);
37
- if (rulesAllowlist.size === 0) {
38
- const qualityDiagnostics = hasExplicitRules
39
- ? []
40
- : impactResult
41
- ? impactQualityDiagnostics
42
- : await collectFullKbQualityDiagnostics({
43
- prolog,
44
- ...(args.maxDiagnostics !== undefined
45
- ? { maxDiagnostics: args.maxDiagnostics }
46
- : {}),
47
- });
48
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: buildSummary({
53
- violations: [],
54
- impactResult,
55
- qualityDiagnostics,
56
- }),
57
- },
58
- ],
59
- structuredContent: buildStructuredContent({
60
- violations: [],
61
- diagnostics: [],
62
- qualityDiagnostics,
63
- impactResult,
64
- }),
65
- };
66
- }
67
- // Ensure we read the latest KB state, not a cached snapshot.
68
- prolog.invalidateCache();
69
- // Run aggregated checks using same approach as CLI
70
- // This now runs ALL rules including symbol-traceability
71
- const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist, checksConfig.symbolTraceability.requireAdr);
72
- const diagnostics = aggregatedViolations.map((v) => ({
73
- category: "SYNC_ERROR",
74
- severity: "error",
75
- message: v.description,
76
- ...(v.source !== undefined ? { file: v.source } : {}),
77
- ...(v.suggestion !== undefined ? { suggestion: v.suggestion } : {}),
78
- }));
79
- const qualityDiagnostics = hasExplicitRules
80
- ? impactQualityDiagnostics
81
- : impactResult
82
- ? impactQualityDiagnostics
83
- : await collectFullKbQualityDiagnostics({
84
- prolog,
85
- hardViolationEntityIds: new Set(aggregatedViolations.map((violation) => violation.entityId)),
86
- ...(args.maxDiagnostics !== undefined
87
- ? { maxDiagnostics: args.maxDiagnostics }
88
- : {}),
89
- });
90
- const summary = buildSummary({
91
- violations: aggregatedViolations,
92
- impactResult,
93
- qualityDiagnostics,
94
- });
95
- return {
96
- content: [
97
- {
98
- type: "text",
99
- text: summary,
100
- },
101
- ],
102
- structuredContent: buildStructuredContent({
103
- violations: aggregatedViolations,
104
- diagnostics,
105
- qualityDiagnostics,
106
- impactResult,
107
- }),
108
- };
109
- }
110
- catch (error) {
111
- const message = error instanceof Error ? error.message : String(error);
112
- throw new Error(`Check execution failed: ${message}`);
113
- }
9
+ const workspaceRoot = args.workspaceRoot ?? resolveWorkspaceRoot();
10
+ return checkSpec.execute({ ...args }, {
11
+ workspaceRoot,
12
+ signal: new AbortController().signal,
13
+ clock: () => new Date(),
14
+ prolog: {
15
+ query: (goal) => prolog.query(goal),
16
+ nextSolution: async () => null,
17
+ invalidateCache: () => prolog.invalidateCache(),
18
+ save: () => prolog.query("kb_save"),
19
+ },
20
+ });
114
21
  }
@@ -1,28 +1,14 @@
1
- import { runJsonModuleQuery, toPrologList } from "./core-module.js";
1
+ import { executeCoverage } from "kibi-cli/operations";
2
2
  // implements REQ-002, REQ-013
3
3
  export async function handleKbCoverage(prolog, args) {
4
- const by = args.by ?? "req";
5
- const limit = args.limit ?? 100;
6
- const offset = args.offset ?? 0;
7
- const includePassing = args.includePassing ?? false;
8
- const includeTransitive = args.includeTransitive ?? true;
9
- try {
10
- const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:coverage_report_json('${by}', ${toPrologList(args.tags)}, ${includePassing}, ${includeTransitive}, ${limit}, ${offset}, JsonString)`, "Coverage execution");
11
- const summary = payload?.summary ?? {};
12
- const fullyCovered = Number(summary.fullyCovered ?? 0);
13
- const total = Number(summary.total ?? 0);
14
- return {
15
- content: [
16
- {
17
- type: "text",
18
- text: `Coverage summary: ${fullyCovered} fully covered out of ${total}.`,
19
- },
20
- ],
21
- ...(payload !== undefined ? { structuredContent: payload } : {}),
22
- };
23
- }
24
- catch (error) {
25
- const message = error instanceof Error ? error.message : String(error);
26
- throw new Error(`Coverage execution failed: ${message}`);
27
- }
4
+ return executeCoverage({ ...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
+ });
28
14
  }
@@ -1,113 +1,6 @@
1
- import { escapeAtom, parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
2
- /**
3
- * Handle kb.delete tool calls
4
- * Prevents deletion of entities with dependents (referential integrity)
5
- */
6
- export async function handleKbDelete(
7
- // implements REQ-002, REQ-011
8
- prolog, args) {
9
- const { ids } = args;
10
- if (!ids || ids.length === 0) {
11
- throw new Error("At least one ID required for delete");
12
- }
13
- let deleted = 0;
14
- let skipped = 0;
15
- const errors = [];
16
- try {
17
- for (const id of ids) {
18
- const safeId = escapeAtom(id);
19
- // Check if entity exists
20
- const checkGoal = `once(kb_entity('${safeId}', _, _))`;
21
- const checkResult = await prolog.query(checkGoal);
22
- if (!checkResult.success) {
23
- errors.push(`Entity ${id} does not exist`);
24
- skipped++;
25
- continue;
26
- }
27
- // Check for dependents (entities that reference this one)
28
- const dependentsGoal = `findall([RelType,From], (member(RelType, [depends_on, verified_by, validates, specified_by, relates_to, guards, publishes, consumes]), kb_relationship(RelType, From, '${safeId}')), Dependents)`;
29
- const dependentsResult = await prolog.query(dependentsGoal);
30
- if (!dependentsResult.success) {
31
- errors.push(`Failed to inspect dependents for entity ${id}: ${dependentsResult.error ?? "Query failed"}`);
32
- skipped++;
33
- continue;
34
- }
35
- const hasDependents = dependentsResult.bindings.Dependents !== undefined &&
36
- dependentsResult.bindings.Dependents !== "[]";
37
- if (hasDependents) {
38
- errors.push(`Cannot delete entity ${id}: has dependents (other entities reference it)`);
39
- skipped++;
40
- }
41
- if (hasDependents) {
42
- continue;
43
- }
44
- // No dependents, safe to delete
45
- const entityMetadata = await loadEntityMetadataForDelete(prolog, id, safeId);
46
- const deleteGoal = buildDeleteGoal(safeId, entityMetadata);
47
- const deleteResult = await prolog.query(deleteGoal);
48
- if (!deleteResult.success) {
49
- errors.push(`Failed to delete entity ${id}: ${deleteResult.error || "Unknown error"}`);
50
- skipped++;
51
- }
52
- else {
53
- deleted++;
54
- }
55
- }
56
- // Save KB to disk
57
- const saveResult = await prolog.query("kb_save");
58
- if (!saveResult.success) {
59
- throw new Error(`Failed to save KB after delete: ${saveResult.error || "Unknown error"}`);
60
- }
61
- prolog.invalidateCache();
62
- return {
63
- content: [
64
- {
65
- type: "text",
66
- text: `Deleted ${deleted} entities. Skipped ${skipped}. ${errors.length > 0 ? `Errors: ${errors.join("; ")}` : ""}`,
67
- },
68
- ],
69
- structuredContent: {
70
- deleted,
71
- skipped,
72
- errors,
73
- },
74
- };
75
- }
76
- catch (error) {
77
- const message = error instanceof Error ? error.message : String(error);
78
- throw new Error(`Delete execution failed: ${message}`);
79
- }
80
- }
81
- async function loadEntityMetadataForDelete(prolog, id, safeId) {
82
- const result = await prolog.query(`findall(['${safeId}',Type,Props], kb_entity('${safeId}', Type, Props), Results)`);
83
- if (!result.success) {
84
- throw new Error(`Failed to load metadata for entity ${id}: ${result.error || "Unknown error"}`);
85
- }
86
- const rows = result.bindings.Results
87
- ? parseListOfLists(result.bindings.Results)
88
- : [];
89
- if (rows.length === 0) {
90
- throw new Error(`Failed to load metadata for entity ${id}: Entity not found`);
91
- }
92
- const entity = parseEntityFromList(rows[0] ?? []);
93
- const type = String(entity.type ?? "unknown");
94
- const { id: _entityId, type: _entityType, ...props } = entity;
95
- return { type, props };
96
- }
97
- function buildDeleteGoal(safeId, metadata) {
98
- const auditProps = [
99
- `id='${safeId}'`,
100
- ...serializeDeleteProps(metadata.props),
101
- ];
102
- return `kb_retract_entity('${safeId}', ${metadata.type}, [${auditProps.join(", ")}])`;
103
- }
104
- function serializeDeleteProps(props) {
105
- const orderedKeys = ["title", "source", "text_ref"];
106
- return orderedKeys.flatMap((key) => {
107
- const value = props[key];
108
- if (typeof value !== "string") {
109
- return [];
110
- }
111
- return `${key}=${JSON.stringify(value)}`;
112
- });
1
+ import { deleteSpec } from "kibi-cli/operations";
2
+ import { createMutationContext } from "./mutation-context.js";
3
+ // implements REQ-kibi-operation-interface-parity
4
+ export async function handleKbDelete(prolog, args) {
5
+ return deleteSpec.execute(args, createMutationContext(prolog));
113
6
  }
@@ -0,0 +1,23 @@
1
+ import { resolveWorkspaceRoot } from "../workspace.js";
2
+ export function createDiscoveryContext(prolog) {
3
+ // implements REQ-kibi-operation-interface-parity
4
+ let lastResult = null;
5
+ const port = {
6
+ query: async (goal) => {
7
+ lastResult = await prolog.query(goal);
8
+ return lastResult;
9
+ },
10
+ nextSolution: async () => {
11
+ const result = lastResult;
12
+ lastResult = null;
13
+ return result;
14
+ },
15
+ save: () => prolog.query("kb_save"),
16
+ };
17
+ return {
18
+ workspaceRoot: resolveWorkspaceRoot(),
19
+ signal: new AbortController().signal,
20
+ clock: () => new Date(),
21
+ prolog: port,
22
+ };
23
+ }
@@ -1,111 +1 @@
1
- import { escapeAtomContent, parseEntityFromBinding, parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
2
- export const VALID_ENTITY_TYPES = [
3
- // implements REQ-002
4
- "req",
5
- "scenario",
6
- "test",
7
- "adr",
8
- "flag",
9
- "event",
10
- "symbol",
11
- "fact",
12
- ];
13
- // implements REQ-002, REQ-013
14
- export function validateEntityType(type) {
15
- if (type && !VALID_ENTITY_TYPES.includes(type)) {
16
- throw new Error(`Invalid type '${type}'. Valid types: ${VALID_ENTITY_TYPES.join(", ")}. Use a single type value, or omit this parameter to query all entities.`);
17
- }
18
- }
19
- // implements REQ-002, REQ-013
20
- export function buildEntityGoal(args) {
21
- const { type, id, tags, sourceFile } = args;
22
- if (sourceFile) {
23
- const safeSource = escapeAtomContent(sourceFile);
24
- if (type) {
25
- const safeType = escapeAtomContent(type);
26
- return `findall([Id,'${safeType}',Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, '${safeType}', Props)), Results)`;
27
- }
28
- return `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
29
- }
30
- if (id && type) {
31
- const safeId = escapeAtomContent(id);
32
- const safeType = escapeAtomContent(type);
33
- return `findall(['${safeId}','${safeType}',Props], kb_entity('${safeId}', '${safeType}', Props), Results)`;
34
- }
35
- if (id) {
36
- const safeId = escapeAtomContent(id);
37
- return `findall(['${safeId}',Type,Props], kb_entity('${safeId}', Type, Props), Results)`;
38
- }
39
- if (tags && tags.length > 0) {
40
- if (type) {
41
- const safeType = escapeAtomContent(type);
42
- return `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
43
- }
44
- return "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
45
- }
46
- if (type) {
47
- const safeType = escapeAtomContent(type);
48
- return `findall([Id,'${safeType}',Props], kb_entity(Id, '${safeType}', Props), Results)`;
49
- }
50
- return "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)";
51
- }
52
- // implements REQ-002, REQ-013
53
- export async function loadEntities(prolog, args) {
54
- validateEntityType(args.type);
55
- const goal = buildEntityGoal(args);
56
- const queryResult = await prolog.query(goal);
57
- let results = [];
58
- if (!queryResult.success) {
59
- throw new Error(queryResult.error || "Query failed with unknown error");
60
- }
61
- if (queryResult.bindings.Results) {
62
- const entitiesData = parseListOfLists(queryResult.bindings.Results);
63
- for (const data of entitiesData) {
64
- results.push(parseEntityFromList(data));
65
- }
66
- }
67
- else if (queryResult.bindings.Result) {
68
- results = [parseEntityFromBinding(queryResult.bindings.Result)];
69
- }
70
- if (args.tags && args.tags.length > 0) {
71
- const requestedTags = args.tags;
72
- results = dedupeEntities(results.filter((entity) => hasAnyTag(entity, requestedTags)));
73
- }
74
- return dedupeEntities(results);
75
- }
76
- // implements REQ-002
77
- export function paginateResults(results, limit = 100, offset = 0) {
78
- return results.slice(offset, offset + limit);
79
- }
80
- function hasAnyTag(entity, requestedTags) {
81
- const expected = new Set(requestedTags.map(normalizeTagValue));
82
- const rawTags = entity.tags;
83
- if (!Array.isArray(rawTags) || rawTags.length === 0) {
84
- return false;
85
- }
86
- for (const tag of rawTags) {
87
- if (expected.has(normalizeTagValue(tag))) {
88
- return true;
89
- }
90
- }
91
- return false;
92
- }
93
- function normalizeTagValue(tag) {
94
- return String(tag).trim();
95
- }
96
- // implements REQ-002
97
- export function dedupeEntities(entities) {
98
- const seen = new Set();
99
- const deduped = [];
100
- for (const entity of entities) {
101
- const id = String(entity.id ?? "");
102
- const type = String(entity.type ?? "");
103
- const key = `${type}::${id}`;
104
- if (seen.has(key)) {
105
- continue;
106
- }
107
- seen.add(key);
108
- deduped.push(entity);
109
- }
110
- return deduped;
111
- }
1
+ export { VALID_ENTITY_TYPES, buildEntityGoal, dedupeEntities, loadEntities, paginateResults, validateEntityType, } from "kibi-cli/operations";
@@ -1,29 +1,14 @@
1
- import { runJsonModuleQuery, toPrologAtom, toPrologList, } from "./core-module.js";
2
- import { validateEntityType } from "./entity-query.js";
1
+ import { executeFindGaps } from "kibi-cli/operations";
3
2
  // implements REQ-002, REQ-013
4
3
  export async function handleKbFindGaps(prolog, args) {
5
- validateEntityType(args.type);
6
- const limit = args.limit ?? 100;
7
- const offset = args.offset ?? 0;
8
- try {
9
- const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:find_gaps_json(${toPrologAtom(args.type)}, ${toPrologList(args.missingRelationships)}, ${toPrologList(args.presentRelationships)}, ${toPrologList(args.tags)}, ${toPrologAtom(args.sourceFile)}, ${limit}, ${offset}, JsonString)`, "Find-gaps execution");
10
- const rows = payload?.rows ?? [];
11
- return {
12
- content: [
13
- {
14
- type: "text",
15
- text: rows.length === 0
16
- ? "No matching gaps found."
17
- : `Found ${payload?.count ?? rows.length} gap rows. Showing ${rows.length}: ${rows
18
- .map((row) => row.id)
19
- .join(", ")}`,
20
- },
21
- ],
22
- ...(payload !== undefined ? { structuredContent: payload } : {}),
23
- };
24
- }
25
- catch (error) {
26
- const message = error instanceof Error ? error.message : String(error);
27
- throw new Error(`Find-gaps execution failed: ${message}`);
28
- }
4
+ return executeFindGaps({ ...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
+ });
29
14
  }