kibi-mcp 0.17.5 → 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/diagnostics-helpers.js +22 -0
- package/dist/diagnostics.js +113 -4
- package/dist/runtime/mcp-runtime.js +43 -0
- package/dist/server/docs.js +11 -10
- package/dist/server/json-schema-to-zod.js +73 -2
- package/dist/server/session.js +33 -15
- package/dist/server/tool-registration.js +35 -59
- 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-config.js +67 -0
- package/dist/tools/check-format.js +92 -0
- package/dist/tools/check-impact.js +45 -0
- package/dist/tools/check-prolog.js +51 -0
- package/dist/tools/check.js +13 -190
- 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-plan-safety.js +58 -0
- package/dist/tools/query.js +5 -50
- package/dist/tools/relationship-validation.js +11 -0
- package/dist/tools/search.js +4 -37
- package/dist/tools/semantic-advisor.js +2 -40
- package/dist/tools/skills.js +25 -47
- 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 -794
- package/dist/tools/validate-upsert.js +16 -40
- package/dist/tools-config.js +75 -764
- package/package.json +4 -3
- package/dist/semantic-advisor/analyze-prose.js +0 -1367
- package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
- /package/dist/{semantic-advisor/types.js → tools/check-types.js} +0 -0
package/bin/kibi-mcp
CHANGED
|
@@ -98,7 +98,7 @@ if (args.includes("-h") || args.includes("--help")) {
|
|
|
98
98
|
|
|
99
99
|
if (args.includes("--diagnostic-mode")) {
|
|
100
100
|
globalThis.__KIBI_MCP_DIAGNOSTIC_MODE = true;
|
|
101
|
-
process.env.KIBI_WORKSPACE
|
|
101
|
+
process.env.KIBI_WORKSPACE ??= process.cwd();
|
|
102
102
|
process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -70,12 +70,32 @@ export function classifyDiagnosticError(error) {
|
|
|
70
70
|
return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
|
|
71
71
|
if (err.message.startsWith("Relationship source must match the upserted entity"))
|
|
72
72
|
return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
|
|
73
|
+
if (lower.includes("contradiction detected for requirement")) {
|
|
74
|
+
const fields = buildErrorFields(err, "semantic_contradiction", "validation", "Requirement prose/facts contradict existing contradiction-ready requirements.");
|
|
75
|
+
return {
|
|
76
|
+
...fields,
|
|
77
|
+
semantic_outcome: "conflict-blocked",
|
|
78
|
+
...semanticContradictionIds(err.message),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
73
81
|
if (lower.includes("module load failed"))
|
|
74
82
|
return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
|
|
75
83
|
if (lower.includes("query failed"))
|
|
76
84
|
return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
|
|
77
85
|
return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
|
|
78
86
|
}
|
|
87
|
+
function semanticContradictionIds(message) {
|
|
88
|
+
const checked = message.match(/Contradiction detected for requirement\s+([^:\s]+)/i);
|
|
89
|
+
const conflicts = [
|
|
90
|
+
...message.matchAll(/Conflicts with\s+([^:\s]+)/gi),
|
|
91
|
+
].flatMap((match) => (match[1] ? [match[1]] : []));
|
|
92
|
+
return {
|
|
93
|
+
...(checked?.[1] ? { semantic_checked_req_id: checked[1] } : {}),
|
|
94
|
+
...(conflicts.length > 0
|
|
95
|
+
? { semantic_conflicting_req_ids: conflicts }
|
|
96
|
+
: {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
79
99
|
export function deriveDiagnosticHints(input) {
|
|
80
100
|
const error = input.error instanceof Error ? input.error : new Error(String(input.error));
|
|
81
101
|
const message = error.message.toLowerCase();
|
|
@@ -89,6 +109,8 @@ export function deriveDiagnosticHints(input) {
|
|
|
89
109
|
return `tool_timeout ${mappedStage}: retry the same request after reducing payload size or scope.`;
|
|
90
110
|
if (error_category === "relationship_source_mismatch")
|
|
91
111
|
return `relationship_source_mismatch ${mappedStage}: align the relationship source with the upserted entity id.`;
|
|
112
|
+
if (error_category === "semantic_contradiction")
|
|
113
|
+
return `semantic_contradiction ${mappedStage}: add a supersedes relationship to the conflicting requirement, deprecate the older requirement, or align the modeled facts.`;
|
|
92
114
|
if (error_category === "prolog_process_not_started")
|
|
93
115
|
return `prolog_process_not_started ${mappedStage}: restart the MCP server before retrying the tool call.`;
|
|
94
116
|
if (error_category === "prolog_query_failed")
|
package/dist/diagnostics.js
CHANGED
|
@@ -58,6 +58,88 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
|
|
|
58
58
|
},
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
|
+
function isRecord(value) {
|
|
62
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
function structuredContentFrom(result) {
|
|
65
|
+
if (!isRecord(result))
|
|
66
|
+
return undefined;
|
|
67
|
+
const structuredContent = result.structuredContent;
|
|
68
|
+
return isRecord(structuredContent) ? structuredContent : undefined;
|
|
69
|
+
}
|
|
70
|
+
function stringArray(value) {
|
|
71
|
+
if (!Array.isArray(value))
|
|
72
|
+
return [];
|
|
73
|
+
return value.filter((item) => typeof item === "string");
|
|
74
|
+
}
|
|
75
|
+
function suggestionKindsFrom(value) {
|
|
76
|
+
if (!Array.isArray(value))
|
|
77
|
+
return [];
|
|
78
|
+
return value.flatMap((item) => {
|
|
79
|
+
if (!isRecord(item) || typeof item.kind !== "string")
|
|
80
|
+
return [];
|
|
81
|
+
return [item.kind];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function appendSemanticAdvisorFields(fields, receipt) {
|
|
85
|
+
if (!isRecord(receipt))
|
|
86
|
+
return;
|
|
87
|
+
const readiness = receipt.logic_readiness;
|
|
88
|
+
const lane = receipt.candidate_lane;
|
|
89
|
+
const suggestionKinds = suggestionKindsFrom(receipt.suggestions);
|
|
90
|
+
if (typeof readiness === "string") {
|
|
91
|
+
fields.semantic_logic_readiness = readiness;
|
|
92
|
+
}
|
|
93
|
+
if (typeof lane === "string") {
|
|
94
|
+
fields.semantic_candidate_lane = lane;
|
|
95
|
+
}
|
|
96
|
+
fields.semantic_suggestion_kinds = suggestionKinds;
|
|
97
|
+
fields.semantic_suggestion_count = suggestionKinds.length;
|
|
98
|
+
fields.semantic_next_tools = stringArray(receipt.suggested_next_tools);
|
|
99
|
+
}
|
|
100
|
+
function appendPredicateSuggestionFields(fields, structuredContent) {
|
|
101
|
+
const candidates = Array.isArray(structuredContent.candidates)
|
|
102
|
+
? structuredContent.candidates
|
|
103
|
+
: [];
|
|
104
|
+
const topCandidate = candidates.find(isRecord);
|
|
105
|
+
fields.predicate_candidate_count = candidates.length;
|
|
106
|
+
if (topCandidate) {
|
|
107
|
+
if (typeof topCandidate.predicate_name === "string") {
|
|
108
|
+
fields.predicate_top_name = topCandidate.predicate_name;
|
|
109
|
+
}
|
|
110
|
+
if (typeof topCandidate.score === "number") {
|
|
111
|
+
fields.predicate_top_score = topCandidate.score;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (typeof structuredContent.recommendedAction === "string") {
|
|
115
|
+
fields.predicate_recommended_action = structuredContent.recommendedAction;
|
|
116
|
+
}
|
|
117
|
+
fields.predicate_relationship_plan = isRecord(structuredContent.relationshipPlan);
|
|
118
|
+
}
|
|
119
|
+
function appendContradictionCheckFields(fields, contradictionCheck) {
|
|
120
|
+
if (!isRecord(contradictionCheck))
|
|
121
|
+
return;
|
|
122
|
+
const outcome = contradictionCheck.outcome;
|
|
123
|
+
if (typeof outcome === "string") {
|
|
124
|
+
fields.semantic_contradiction_outcome = outcome;
|
|
125
|
+
}
|
|
126
|
+
const checkedReqId = contradictionCheck.checked_req_id;
|
|
127
|
+
if (typeof checkedReqId === "string") {
|
|
128
|
+
fields.semantic_checked_req_id = checkedReqId;
|
|
129
|
+
}
|
|
130
|
+
const strictReadiness = contradictionCheck.strict_readiness;
|
|
131
|
+
if (typeof strictReadiness === "string") {
|
|
132
|
+
fields.semantic_strict_readiness = strictReadiness;
|
|
133
|
+
}
|
|
134
|
+
const subjectKey = contradictionCheck.subject_key;
|
|
135
|
+
if (typeof subjectKey === "string") {
|
|
136
|
+
fields.semantic_subject_key = subjectKey;
|
|
137
|
+
}
|
|
138
|
+
const propertyKey = contradictionCheck.property_key;
|
|
139
|
+
if (typeof propertyKey === "string") {
|
|
140
|
+
fields.semantic_property_key = propertyKey;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
61
143
|
export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
62
144
|
const fields = {
|
|
63
145
|
telemetry_status: telemetry ? "provided" : "missing",
|
|
@@ -67,10 +149,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
|
67
149
|
fields.telemetry_confidence_score = telemetry.confidence_score ?? null;
|
|
68
150
|
fields.telemetry_attempt_number = telemetry.attempt_number ?? null;
|
|
69
151
|
}
|
|
70
|
-
const structuredContent = result
|
|
71
|
-
? result
|
|
72
|
-
.structuredContent
|
|
73
|
-
: undefined;
|
|
152
|
+
const structuredContent = structuredContentFrom(result);
|
|
74
153
|
if (toolName === "kb_query" || toolName === "kb_search") {
|
|
75
154
|
const resultCount = Number(structuredContent?.count ?? 0);
|
|
76
155
|
fields.result_count = resultCount;
|
|
@@ -85,6 +164,36 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
|
85
164
|
fields.result_summary =
|
|
86
165
|
violationCount === 0 ? "0 violations" : `${violationCount} violations`;
|
|
87
166
|
}
|
|
167
|
+
if (toolName === "kb_semantic_advisor" && structuredContent) {
|
|
168
|
+
appendSemanticAdvisorFields(fields, structuredContent.receipt);
|
|
169
|
+
const readiness = fields.semantic_logic_readiness;
|
|
170
|
+
const lane = fields.semantic_candidate_lane;
|
|
171
|
+
if (typeof readiness === "string" && typeof lane === "string") {
|
|
172
|
+
fields.result_summary = `semantic advisor ${readiness} via ${lane}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (toolName === "kb_suggest_predicates" && structuredContent) {
|
|
176
|
+
appendPredicateSuggestionFields(fields, structuredContent);
|
|
177
|
+
const count = Number(fields.predicate_candidate_count ?? 0);
|
|
178
|
+
const topName = fields.predicate_top_name;
|
|
179
|
+
fields.result_summary =
|
|
180
|
+
typeof topName === "string"
|
|
181
|
+
? `${count} predicate candidates; top=${topName}`
|
|
182
|
+
: `${count} predicate candidates`;
|
|
183
|
+
}
|
|
184
|
+
if (toolName === "kb_upsert" && structuredContent) {
|
|
185
|
+
const created = Number(structuredContent.created ?? 0);
|
|
186
|
+
const updated = Number(structuredContent.updated ?? 0);
|
|
187
|
+
fields.upsert_created = created;
|
|
188
|
+
fields.upsert_updated = updated;
|
|
189
|
+
fields.upsert_relationships_created = Number(structuredContent.relationships_created ?? 0);
|
|
190
|
+
appendContradictionCheckFields(fields, structuredContent.contradictionCheck);
|
|
191
|
+
appendSemanticAdvisorFields(fields, structuredContent.semanticAdvisor);
|
|
192
|
+
const readiness = fields.semantic_logic_readiness;
|
|
193
|
+
if (typeof readiness === "string") {
|
|
194
|
+
fields.result_summary = `upsert ${created > 0 ? "created" : "updated"}; semantic ${readiness}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
88
197
|
if (!fields.result_summary) {
|
|
89
198
|
fields.result_summary = `${toolName} completed`;
|
|
90
199
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// implements REQ-kibi-operation-interface-parity
|
|
2
|
+
export function createMcpRuntime(session) {
|
|
3
|
+
const sessionPrologs = new WeakMap();
|
|
4
|
+
return {
|
|
5
|
+
open: async (spec, options = {}) => {
|
|
6
|
+
const fs = options.fs ?? session.fs;
|
|
7
|
+
const git = options.git ?? session.git;
|
|
8
|
+
const net = options.net ?? session.net;
|
|
9
|
+
const context = {
|
|
10
|
+
workspaceRoot: options.workspaceRoot ?? session.workspaceRoot,
|
|
11
|
+
signal: options.signal ?? session.signal ?? new AbortController().signal,
|
|
12
|
+
clock: options.clock ?? session.clock ?? (() => new Date()),
|
|
13
|
+
...(fs ? { fs } : {}),
|
|
14
|
+
...(git ? { git } : {}),
|
|
15
|
+
...(net ? { net } : {}),
|
|
16
|
+
};
|
|
17
|
+
if (!spec.requiresProlog) {
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
if (options.prolog) {
|
|
21
|
+
return { ...context, prolog: options.prolog };
|
|
22
|
+
}
|
|
23
|
+
const sessionProlog = await session.ensureProlog();
|
|
24
|
+
const operationContext = {
|
|
25
|
+
...context,
|
|
26
|
+
prolog: session.adaptProlog(sessionProlog),
|
|
27
|
+
};
|
|
28
|
+
sessionPrologs.set(operationContext, sessionProlog);
|
|
29
|
+
return operationContext;
|
|
30
|
+
},
|
|
31
|
+
afterSuccess: async (spec) => {
|
|
32
|
+
if (spec.effects.includes("kb-write") &&
|
|
33
|
+
(await session.attachedBranchKbPath()) !== null) {
|
|
34
|
+
await session.refreshAttachedBranchStamp();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
close: async (context, outcome) => {
|
|
38
|
+
await session.requestCleanup?.(context, outcome);
|
|
39
|
+
sessionPrologs.delete(context);
|
|
40
|
+
},
|
|
41
|
+
sessionProlog: (context) => sessionPrologs.get(context),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/server/docs.js
CHANGED
|
@@ -16,9 +16,6 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import { TOOLS } from "../tools-config.js";
|
|
19
|
-
// INTENTIONAL: TOOLS is imported as a Zod-inferred schema type; ToolConfig is the runtime
|
|
20
|
-
// interface with looser Record<string, unknown> inputSchema. The cast is safe because the
|
|
21
|
-
// tool definitions are statically authored and validated at startup.
|
|
22
19
|
const ACTIVE_TOOLS = TOOLS;
|
|
23
20
|
function renderToolsDoc() {
|
|
24
21
|
const lines = [
|
|
@@ -87,7 +84,10 @@ export const PROMPTS = [
|
|
|
87
84
|
"## Rules",
|
|
88
85
|
"- Never apply bootstrap writes without user-facing preview and explicit approval.",
|
|
89
86
|
"- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
|
|
90
|
-
"-
|
|
87
|
+
"- Prefer MCP tools; CLI routes are available when MCP is unavailable.",
|
|
88
|
+
"- If Kibi MCP tools are visible, use MCP. If MCP availability is unknown and a trusted local Kibi CLI is available, use its dedicated JSON routes with `--input <file|->`.",
|
|
89
|
+
"- If neither interface is available, Kibi operation is blocked; tell the operator to enable MCP or the trusted project-local CLI. Do not infer MCP availability from config file existence.",
|
|
90
|
+
"- Do not read or edit `.kb/` files directly. Query before mutate. Run `kb_upsert` sequentially. Run `kb_check` before completion.",
|
|
91
91
|
].join("\n"),
|
|
92
92
|
},
|
|
93
93
|
{
|
|
@@ -107,7 +107,7 @@ export const PROMPTS = [
|
|
|
107
107
|
"- `kb_graph`: Bounded graph traversal from seed IDs",
|
|
108
108
|
"- `kb_upsert`: Create or update entities and their relationships",
|
|
109
109
|
"- `kb_delete`: Remove entities by ID (with dependency safety checks)",
|
|
110
|
-
"- `kb_check`: Validate KB integrity against configurable rules",
|
|
110
|
+
"- `kb_check`: Validate KB integrity against configurable rules; omit `rules` for final full validation plus full-KB `qualityDiagnostics[]` audit review",
|
|
111
111
|
"",
|
|
112
112
|
"Core modeling principles:",
|
|
113
113
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
@@ -118,7 +118,7 @@ export const PROMPTS = [
|
|
|
118
118
|
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
|
119
119
|
"- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
|
|
120
120
|
"- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
|
|
121
|
-
"- Run `kb_check`
|
|
121
|
+
"- Run `kb_check` with explicit `rules` during iteration for scoped feedback; run an unfiltered `kb_check` before completion to include the full-KB `qualityDiagnostics[]` audit scan.",
|
|
122
122
|
"- Prefer explicit IDs and enum values to avoid invalid parameters.",
|
|
123
123
|
"- Model requirements by first creating/reusing fact entities (create-before-link).",
|
|
124
124
|
"- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
|
|
@@ -138,7 +138,8 @@ export const PROMPTS = [
|
|
|
138
138
|
"5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
|
|
139
139
|
"6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `kb_suggest_predicates` before writing `fact_kind: observation`. Apply the selected `fact_kind: predicate` applyPlan, then attach the returned `relationshipPlan` as `requires_predicate` while preserving existing req metadata; use the returned `review:ontology-gap` observation when no predicate fits.",
|
|
140
140
|
"7. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
|
|
141
|
-
"8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
|
|
141
|
+
"8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need so scoped validation stays fast and skips the full-KB advisory scan.",
|
|
142
|
+
"9. **Final check**: Run `kb_check` without `rules` before completion so hard `violations[]` and advisory full-KB `qualityDiagnostics[]` are both reviewed.",
|
|
142
143
|
"",
|
|
143
144
|
"If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
|
|
144
145
|
].join("\n"),
|
|
@@ -203,7 +204,7 @@ function registerDocResources() {
|
|
|
203
204
|
"2. `kb_upsert` for the fact entity first (create-before-link)",
|
|
204
205
|
"3. `kb_upsert` for the req entity and include `relationships` with `constrains` and `requires_property`",
|
|
205
206
|
"4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
|
|
206
|
-
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
207
|
+
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; supplying `rules` skips the full-KB advisory scan',
|
|
207
208
|
"",
|
|
208
209
|
"## Model requirements as ontology predicates",
|
|
209
210
|
'1. Spell out the requirement prose and call `kb_suggest_predicates` with `{ "text": "...", "requirementId": "REQ-..." }`',
|
|
@@ -222,9 +223,9 @@ function registerDocResources() {
|
|
|
222
223
|
'1. `kb_query` with `{ "type": "test" }` to check for existing test IDs',
|
|
223
224
|
'2. `kb_query` with `{ "id": "REQ-XXX" }` to verify the requirement exists',
|
|
224
225
|
"3. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
|
|
225
|
-
'4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
226
|
+
'4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; use an unfiltered `kb_check` before completion for full-KB `qualityDiagnostics[]`',
|
|
226
227
|
"",
|
|
227
|
-
"Note: Always use query-first pattern. Specify only needed rules in kb_check for faster validation.",
|
|
228
|
+
"Note: Always use query-first pattern. Specify only needed rules in kb_check for faster iteration; omit rules for the final full validation and full-KB quality diagnostics review.",
|
|
228
229
|
].join("\n");
|
|
229
230
|
return [
|
|
230
231
|
{
|
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
function hasRequiredProperties(value, schema) {
|
|
3
|
+
if (schema === null || typeof schema !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const condition = schema;
|
|
6
|
+
const required = Array.isArray(condition.required)
|
|
7
|
+
? condition.required.filter((key) => typeof key === "string" && key.length > 0)
|
|
8
|
+
: [];
|
|
9
|
+
if (required.length > 0 &&
|
|
10
|
+
!required.every((key) => Object.hasOwn(value, key))) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(condition.anyOf) &&
|
|
14
|
+
!condition.anyOf.some((entry) => hasRequiredProperties(value, entry))) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(condition.allOf) &&
|
|
18
|
+
!condition.allOf.every((entry) => hasRequiredProperties(value, entry))) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function conditionalRequiredKeys(value, condition) {
|
|
24
|
+
if (condition === null || typeof condition !== "object")
|
|
25
|
+
return [];
|
|
26
|
+
const rule = condition;
|
|
27
|
+
if (!("if" in rule) || !("then" in rule))
|
|
28
|
+
return [];
|
|
29
|
+
if (!hasRequiredProperties(value, rule.if))
|
|
30
|
+
return [];
|
|
31
|
+
if (rule.then === null || typeof rule.then !== "object")
|
|
32
|
+
return [];
|
|
33
|
+
const required = rule.then.required;
|
|
34
|
+
return Array.isArray(required)
|
|
35
|
+
? required.filter((key) => typeof key === "string" && key.length > 0)
|
|
36
|
+
: [];
|
|
37
|
+
}
|
|
2
38
|
// implements REQ-002
|
|
3
39
|
export function jsonSchemaToZod(schema) {
|
|
4
40
|
if (!schema || typeof schema !== "object") {
|
|
@@ -43,7 +79,29 @@ export function jsonSchemaToZod(schema) {
|
|
|
43
79
|
? z.object(shape)
|
|
44
80
|
: z.looseObject(shape);
|
|
45
81
|
const description = typeof obj.description === "string" ? obj.description : undefined;
|
|
46
|
-
|
|
82
|
+
let result = description
|
|
83
|
+
? objectSchema.describe(description)
|
|
84
|
+
: objectSchema;
|
|
85
|
+
if (Array.isArray(obj.allOf)) {
|
|
86
|
+
result = result
|
|
87
|
+
.superRefine((value, context) => {
|
|
88
|
+
if (value === null || typeof value !== "object")
|
|
89
|
+
return;
|
|
90
|
+
for (const condition of obj.allOf) {
|
|
91
|
+
for (const key of conditionalRequiredKeys(value, condition)) {
|
|
92
|
+
if (!Object.hasOwn(value, key)) {
|
|
93
|
+
context.addIssue({
|
|
94
|
+
code: "custom",
|
|
95
|
+
path: [key],
|
|
96
|
+
message: `Required by conditional JSON Schema rule: ${key}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.meta({ allOf: obj.allOf });
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
47
105
|
}
|
|
48
106
|
case "array": {
|
|
49
107
|
const itemSchema = jsonSchemaToZod(obj.items);
|
|
@@ -55,7 +113,17 @@ export function jsonSchemaToZod(schema) {
|
|
|
55
113
|
if (typeof obj.maxItems === "number") {
|
|
56
114
|
arraySchema = arraySchema.max(obj.maxItems);
|
|
57
115
|
}
|
|
58
|
-
|
|
116
|
+
let result = description
|
|
117
|
+
? arraySchema.describe(description)
|
|
118
|
+
: arraySchema;
|
|
119
|
+
if (obj.uniqueItems === true) {
|
|
120
|
+
result = result
|
|
121
|
+
.refine((values) => Array.isArray(values) &&
|
|
122
|
+
new Set(values.map((value) => JSON.stringify(value))).size ===
|
|
123
|
+
values.length, { message: "Array items must be unique" })
|
|
124
|
+
.meta({ uniqueItems: true });
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
59
127
|
}
|
|
60
128
|
case "string": {
|
|
61
129
|
let s = z.string();
|
|
@@ -66,6 +134,9 @@ export function jsonSchemaToZod(schema) {
|
|
|
66
134
|
if (typeof obj.maxLength === "number") {
|
|
67
135
|
s = s.max(obj.maxLength);
|
|
68
136
|
}
|
|
137
|
+
if (typeof obj.pattern === "string") {
|
|
138
|
+
s = s.regex(new RegExp(obj.pattern));
|
|
139
|
+
}
|
|
69
140
|
return description ? s.describe(description) : s;
|
|
70
141
|
}
|
|
71
142
|
case "number": {
|
package/dist/server/session.js
CHANGED
|
@@ -42,6 +42,8 @@ let ensurePrologTail = Promise.resolve();
|
|
|
42
42
|
let prologResetGeneration = 0;
|
|
43
43
|
export let attachedBranchKbPath = null;
|
|
44
44
|
let attachedBranchStamp = null;
|
|
45
|
+
// implements REQ-mcp-kb-freshness
|
|
46
|
+
// covered_by TEST-mcp-kb-freshness
|
|
45
47
|
export function updateAttachedBranchStamp(stamp) {
|
|
46
48
|
attachedBranchStamp = stamp;
|
|
47
49
|
}
|
|
@@ -85,7 +87,7 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
|
|
|
85
87
|
}
|
|
86
88
|
const branchPath = sessionDeps.resolveKbPath(workspaceRoot, branch);
|
|
87
89
|
if (sessionDeps.fs.existsSync(branchPath)) {
|
|
88
|
-
return;
|
|
90
|
+
return false;
|
|
89
91
|
}
|
|
90
92
|
// Try to copy from the previously active branch if available
|
|
91
93
|
const previousBranch = activeBranchName;
|
|
@@ -96,11 +98,12 @@ export function ensureBranchKbExists(workspaceRoot, branch) {
|
|
|
96
98
|
// Copy from previous branch for continuity
|
|
97
99
|
sessionDeps.copyCleanSnapshot(previousBranchPath, branchPath);
|
|
98
100
|
debugLog(`[KIBI-MCP] Created branch KB for '${branch}' from '${previousBranch}'`);
|
|
99
|
-
return;
|
|
101
|
+
return false;
|
|
100
102
|
}
|
|
101
103
|
// No previous branch available - create empty KB
|
|
102
104
|
sessionDeps.fs.mkdirSync(branchPath, { recursive: true });
|
|
103
105
|
debugLog(`[KIBI-MCP] Created empty branch KB for '${branch}'`);
|
|
106
|
+
return true;
|
|
104
107
|
}
|
|
105
108
|
export async function initiateGracefulShutdown(exitCode = 0) {
|
|
106
109
|
if (isShuttingDown) {
|
|
@@ -108,6 +111,18 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
108
111
|
}
|
|
109
112
|
isShuttingDown = true;
|
|
110
113
|
debugLog(`[KIBI-MCP] Initiating graceful shutdown (exit code: ${exitCode})`);
|
|
114
|
+
const currentProlog = prologProcess;
|
|
115
|
+
prologProcess = null;
|
|
116
|
+
if (currentProlog?.isRunning()) {
|
|
117
|
+
debugLog("[KIBI-MCP] Cancelling active Prolog work...");
|
|
118
|
+
try {
|
|
119
|
+
await currentProlog.terminate();
|
|
120
|
+
debugLog("[KIBI-MCP] Prolog process terminated");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error("[KIBI-MCP] Error terminating Prolog:", error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
111
126
|
// Wait for in-flight requests
|
|
112
127
|
if (inFlightRequests.size > 0) {
|
|
113
128
|
debugLog(`[KIBI-MCP] Waiting for ${inFlightRequests.size} in-flight requests to complete...`);
|
|
@@ -133,17 +148,6 @@ export async function initiateGracefulShutdown(exitCode = 0) {
|
|
|
133
148
|
}
|
|
134
149
|
}
|
|
135
150
|
}
|
|
136
|
-
// Cleanup Prolog process
|
|
137
|
-
if (prologProcess?.isRunning()) {
|
|
138
|
-
debugLog("[KIBI-MCP] Terminating Prolog process...");
|
|
139
|
-
try {
|
|
140
|
-
await prologProcess.terminate();
|
|
141
|
-
debugLog("[KIBI-MCP] Prolog process terminated");
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
console.error("[KIBI-MCP] Error terminating Prolog:", error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
151
|
// Exit
|
|
148
152
|
process.exit(exitCode);
|
|
149
153
|
}
|
|
@@ -271,13 +275,20 @@ async function ensurePrologUnsafe() {
|
|
|
271
275
|
// Continue anyway - we'll try to attach to the new KB
|
|
272
276
|
}
|
|
273
277
|
// Ensure new branch KB exists
|
|
274
|
-
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
278
|
+
const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
275
279
|
// Attach to new branch KB
|
|
276
280
|
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
277
281
|
await assertGeneration();
|
|
278
282
|
if (!attachResult.success) {
|
|
279
283
|
throw new Error(`Failed to attach to new branch KB: ${attachResult.error || "Unknown error"}`);
|
|
280
284
|
}
|
|
285
|
+
if (createdEmptyBranch) {
|
|
286
|
+
const initialSaveResult = await prologProcess.query("kb_save");
|
|
287
|
+
await assertGeneration();
|
|
288
|
+
if (!initialSaveResult.success) {
|
|
289
|
+
throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
281
292
|
activeBranchName = targetBranch;
|
|
282
293
|
attachedBranchKbPath = kbPath;
|
|
283
294
|
attachedBranchStamp = await readBranchKbStamp(kbPath);
|
|
@@ -327,13 +338,20 @@ async function ensurePrologUnsafe() {
|
|
|
327
338
|
debugLog(`[KIBI-MCP] KIBI_BRANCH env: ${envBranch || "not set"}`);
|
|
328
339
|
debugLog(`[KIBI-MCP] Resolved branch: ${targetBranch}`);
|
|
329
340
|
activeBranchName = targetBranch;
|
|
330
|
-
ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
341
|
+
const createdEmptyBranch = ensureBranchKbExists(workspaceRoot, targetBranch);
|
|
331
342
|
const kbPath = sessionDeps.resolveKbPath(workspaceRoot, targetBranch);
|
|
332
343
|
const attachResult = await prologProcess.query(`kb_attach('${kbPath}')`);
|
|
333
344
|
await assertGeneration();
|
|
334
345
|
if (!attachResult.success) {
|
|
335
346
|
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
|
336
347
|
}
|
|
348
|
+
if (createdEmptyBranch) {
|
|
349
|
+
const initialSaveResult = await prologProcess.query("kb_save");
|
|
350
|
+
await assertGeneration();
|
|
351
|
+
if (!initialSaveResult.success) {
|
|
352
|
+
throw new Error(`Failed to initialize empty branch KB: ${initialSaveResult.error || "Unknown error"}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
337
355
|
attachedBranchKbPath = kbPath;
|
|
338
356
|
attachedBranchStamp = await readBranchKbStamp(kbPath);
|
|
339
357
|
isInitialized = true;
|