kibi-mcp 0.17.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/diagnostics-helpers.js +22 -0
- package/dist/diagnostics.js +113 -4
- package/dist/semantic-advisor/analyze-prose.js +25 -0
- package/dist/server/docs.js +7 -6
- package/dist/server/tool-registration.js +4 -1
- 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-types.js +1 -0
- package/dist/tools/check.js +64 -148
- package/dist/tools/query-plan-safety.js +58 -0
- package/dist/tools/relationship-validation.js +118 -0
- package/dist/tools/skills.js +20 -2
- package/dist/tools/upsert.js +23 -1
- package/dist/tools/validate-upsert.js +13 -1
- package/dist/tools-config.js +40 -2
- package/package.json +3 -3
|
@@ -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
|
}
|
|
@@ -385,6 +385,16 @@ function detectStrictPropertySuggestion(payload, statement) {
|
|
|
385
385
|
value_int: Number(comparative.groups.value),
|
|
386
386
|
}, "Comparative numeric prose is a strict property constraint.", 0.86);
|
|
387
387
|
}
|
|
388
|
+
const decisecondSlot = statement.match(/^.+?\s+(?:must|shall|should)\s+normalize\s+into\s+canonical\s+integer\s+decisecond\s+(?<subject>[a-z][a-z0-9_-]*)\s+values?\.?$/i);
|
|
389
|
+
if (decisecondSlot?.groups?.subject) {
|
|
390
|
+
return strictSuggestion(payload, decisecondSlot[0], {
|
|
391
|
+
subject_key: normalizeSubjectKey(decisecondSlot.groups.subject),
|
|
392
|
+
property_key: "slot_precision",
|
|
393
|
+
operator: "eq",
|
|
394
|
+
value_type: "string",
|
|
395
|
+
value_string: "decisecond",
|
|
396
|
+
}, "Canonical decisecond slot prose is a strict precision property.", 0.9);
|
|
397
|
+
}
|
|
388
398
|
const threshold = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+return\s+within\s+(?<value>\d+)\s+(?<unit>ms|milliseconds?|s|sec|secs|seconds?)\.?$/i);
|
|
389
399
|
if (threshold?.groups?.subject &&
|
|
390
400
|
threshold.groups.value &&
|
|
@@ -598,6 +608,21 @@ function detectPredicateSuggestion(payload, statement) {
|
|
|
598
608
|
};
|
|
599
609
|
return predicateSuggestion(payload, ownership[0], predicate, "Ownership prose assigns responsibility for a resource or behavior and should be queryable as a predicate.");
|
|
600
610
|
}
|
|
611
|
+
const mergePolicy = statement.match(/^(?<inputs>.+?)\s+saved\s+in\s+the\s+same\s+(?<slot>.+?)\s+must\s+merge\s+into\s+(?<target>.+?)\s+instead\s+of\s+creating\s+.+?\.?$/i);
|
|
612
|
+
if (mergePolicy?.groups?.inputs &&
|
|
613
|
+
mergePolicy.groups.slot &&
|
|
614
|
+
mergePolicy.groups.target) {
|
|
615
|
+
const inputs = normalizePredicateToken(mergePolicy.groups.inputs);
|
|
616
|
+
const slot = normalizePredicateToken(mergePolicy.groups.slot);
|
|
617
|
+
const target = normalizePredicateToken(mergePolicy.groups.target);
|
|
618
|
+
const predicate = {
|
|
619
|
+
predicate_name: "merge_policy",
|
|
620
|
+
predicate_args: [inputs, slot, target],
|
|
621
|
+
canonical_key: `merge_policy(${inputs},${slot},${target})`,
|
|
622
|
+
polarity: "assert",
|
|
623
|
+
};
|
|
624
|
+
return predicateSuggestion(payload, mergePolicy[0], predicate, "Same-slot merge prose defines ontology-lite merge behavior and should be queryable as a predicate.");
|
|
625
|
+
}
|
|
601
626
|
const retryPolicy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+retry\s+up\s+to\s+(?<count>\d+)\s+(?<unit>times|attempts?)\.?$/i);
|
|
602
627
|
if (retryPolicy?.groups?.subject &&
|
|
603
628
|
retryPolicy.groups.count &&
|
package/dist/server/docs.js
CHANGED
|
@@ -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
|
{
|
|
@@ -88,7 +88,10 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
88
88
|
});
|
|
89
89
|
register({
|
|
90
90
|
name: "kb_validate_upsert",
|
|
91
|
-
handler: async (args) =>
|
|
91
|
+
handler: async (args) => {
|
|
92
|
+
const prolog = await runtime.ensureProlog();
|
|
93
|
+
return runtime.handleKbValidateUpsert(prolog, args);
|
|
94
|
+
},
|
|
92
95
|
});
|
|
93
96
|
register({
|
|
94
97
|
name: "kb_delete",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile } from "node:fs/promises";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
21
|
+
// implements REQ-002
|
|
22
|
+
export async function loadChecksConfig(workspaceRoot) {
|
|
23
|
+
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
24
|
+
try {
|
|
25
|
+
const content = await readFile(configPath, "utf8");
|
|
26
|
+
const parsed = JSON.parse(content);
|
|
27
|
+
const parsedRules = parsed.checks?.rules;
|
|
28
|
+
const normalizedRules = {
|
|
29
|
+
...DEFAULT_CHECKS_CONFIG.rules,
|
|
30
|
+
};
|
|
31
|
+
if (parsedRules && typeof parsedRules === "object") {
|
|
32
|
+
for (const [key, value] of Object.entries(parsedRules)) {
|
|
33
|
+
if (typeof value === "boolean") {
|
|
34
|
+
normalizedRules[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const parsedSt = parsed.checks?.symbolTraceability;
|
|
39
|
+
const normalizedSt = { ...DEFAULT_CHECKS_CONFIG.symbolTraceability };
|
|
40
|
+
if (parsedSt &&
|
|
41
|
+
typeof parsedSt === "object" &&
|
|
42
|
+
typeof parsedSt.requireAdr === "boolean") {
|
|
43
|
+
normalizedSt.requireAdr = parsedSt.requireAdr;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
rules: normalizedRules,
|
|
47
|
+
symbolTraceability: normalizedSt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return DEFAULT_CHECKS_CONFIG;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// implements REQ-002
|
|
55
|
+
export function getEffectiveRules(configRules, requestedRules) {
|
|
56
|
+
if (requestedRules && requestedRules.length > 0) {
|
|
57
|
+
return new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
58
|
+
}
|
|
59
|
+
const effective = new Set();
|
|
60
|
+
for (const rule of RULE_NAMES) {
|
|
61
|
+
const enabled = configRules[rule] ?? true;
|
|
62
|
+
if (enabled) {
|
|
63
|
+
effective.add(rule);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return effective;
|
|
67
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function formatDiagnosticsForMcp(diagnostics) {
|
|
2
|
+
return diagnostics.map((d) => ({
|
|
3
|
+
category: d.category,
|
|
4
|
+
severity: d.severity,
|
|
5
|
+
message: d.message,
|
|
6
|
+
...(d.file !== undefined ? { file: d.file } : {}),
|
|
7
|
+
...(d.suggestion !== undefined ? { suggestion: d.suggestion } : {}),
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
export function formatImpactText(impactResult) {
|
|
11
|
+
if (impactResult.impactDiagnostics.length === 0) {
|
|
12
|
+
return "No impact diagnostics found";
|
|
13
|
+
}
|
|
14
|
+
const details = impactResult.impactDiagnostics.map((diagnostic) => {
|
|
15
|
+
const files = diagnostic.files.length > 0
|
|
16
|
+
? diagnostic.files.join(", ")
|
|
17
|
+
: "unknown-source";
|
|
18
|
+
return `- ${diagnostic.id} | ${diagnostic.severity} | ${files} | ${diagnostic.message} Suggestion: ${diagnostic.suggestion}`;
|
|
19
|
+
});
|
|
20
|
+
return `${impactResult.impactDiagnostics.length} impact diagnostics found\n${details.join("\n")}`;
|
|
21
|
+
}
|
|
22
|
+
function formatOptionalList(label, values) {
|
|
23
|
+
if (values === undefined || values.length === 0) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
return [`${label}: ${values.join(", ")}`];
|
|
27
|
+
}
|
|
28
|
+
export function formatQualityDiagnosticsText(diagnostics) {
|
|
29
|
+
if (diagnostics.length === 0) {
|
|
30
|
+
return "No quality diagnostics found";
|
|
31
|
+
}
|
|
32
|
+
const details = diagnostics.map((diagnostic) => {
|
|
33
|
+
const parts = [
|
|
34
|
+
diagnostic.id,
|
|
35
|
+
diagnostic.severity,
|
|
36
|
+
diagnostic.category,
|
|
37
|
+
diagnostic.message,
|
|
38
|
+
];
|
|
39
|
+
const metadata = [
|
|
40
|
+
`Blocking: ${diagnostic.blocking ? "yes" : "no"}`,
|
|
41
|
+
...formatOptionalList("Files", diagnostic.files),
|
|
42
|
+
...formatOptionalList("Docs", diagnostic.docs),
|
|
43
|
+
...(diagnostic.entityId !== undefined
|
|
44
|
+
? [`Entity: ${diagnostic.entityId}`]
|
|
45
|
+
: []),
|
|
46
|
+
...(diagnostic.source !== undefined
|
|
47
|
+
? [`Source: ${diagnostic.source}`]
|
|
48
|
+
: []),
|
|
49
|
+
`Suggestion: ${diagnostic.suggestion}`,
|
|
50
|
+
];
|
|
51
|
+
return `- ${parts.join(" | ")}\n ${metadata.join("\n ")}`;
|
|
52
|
+
});
|
|
53
|
+
return `${diagnostics.length} quality diagnostic${diagnostics.length === 1 ? "" : "s"} found\n${details.join("\n")}`;
|
|
54
|
+
}
|
|
55
|
+
export function buildStructuredContent(input) {
|
|
56
|
+
const qualityDiagnostics = input.qualityDiagnostics ?? [];
|
|
57
|
+
return {
|
|
58
|
+
violations: [...input.violations],
|
|
59
|
+
count: input.violations.length,
|
|
60
|
+
diagnostics: formatDiagnosticsForMcp(input.diagnostics),
|
|
61
|
+
...(qualityDiagnostics.length > 0
|
|
62
|
+
? { qualityDiagnostics: [...qualityDiagnostics] }
|
|
63
|
+
: {}),
|
|
64
|
+
...(input.impactResult
|
|
65
|
+
? {
|
|
66
|
+
impactDiagnostics: input.impactResult.impactDiagnostics,
|
|
67
|
+
sourceFiles: input.impactResult.sourceFiles,
|
|
68
|
+
extractedSymbols: input.impactResult.extractedSymbols,
|
|
69
|
+
linkedEntities: input.impactResult.linkedEntities,
|
|
70
|
+
nextActions: input.impactResult.nextActions,
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function formatViolationText(violations) {
|
|
76
|
+
if (violations.length === 0) {
|
|
77
|
+
return "No violations found";
|
|
78
|
+
}
|
|
79
|
+
const details = violations.map((violation) => {
|
|
80
|
+
const parts = [
|
|
81
|
+
violation.rule,
|
|
82
|
+
violation.entityId,
|
|
83
|
+
violation.source ?? "unknown-source",
|
|
84
|
+
violation.description,
|
|
85
|
+
];
|
|
86
|
+
if (violation.suggestion) {
|
|
87
|
+
parts.push(`Suggestion: ${violation.suggestion}`);
|
|
88
|
+
}
|
|
89
|
+
return `- ${parts.join(" | ")}`;
|
|
90
|
+
});
|
|
91
|
+
return `${violations.length} violations found\n${details.join("\n")}`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
+
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { analyzeChangedFileImpact, } from "kibi-cli/public/impact-diagnostics";
|
|
19
|
+
export function hasImpactOptions(args) {
|
|
20
|
+
return Boolean(args.includeImpactDiagnostics ||
|
|
21
|
+
args.staged ||
|
|
22
|
+
args.includeWorkingTreeDiff ||
|
|
23
|
+
(args.sourceFiles && args.sourceFiles.length > 0));
|
|
24
|
+
}
|
|
25
|
+
export function analyzeKbCheckImpact(workspaceRoot, args) {
|
|
26
|
+
if (!hasImpactOptions(args)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return analyzeChangedFileImpact({
|
|
30
|
+
workspaceRoot,
|
|
31
|
+
...(args.sourceFiles !== undefined
|
|
32
|
+
? { sourceFiles: args.sourceFiles }
|
|
33
|
+
: {}),
|
|
34
|
+
...(args.staged !== undefined ? { staged: args.staged } : {}),
|
|
35
|
+
...(args.includeWorkingTreeDiff !== undefined
|
|
36
|
+
? { includeWorkingTreeDiff: args.includeWorkingTreeDiff }
|
|
37
|
+
: {}),
|
|
38
|
+
...(args.includeImpactDiagnostics !== undefined
|
|
39
|
+
? { includeImpactDiagnostics: args.includeImpactDiagnostics }
|
|
40
|
+
: {}),
|
|
41
|
+
...(args.maxDiagnostics !== undefined
|
|
42
|
+
? { maxDiagnostics: args.maxDiagnostics }
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolveCorePlPath } from "./core-module.js";
|
|
2
|
+
import { collectQueryPlanSafetyViolations } from "./query-plan-safety.js";
|
|
3
|
+
// implements REQ-002
|
|
4
|
+
export async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
|
|
5
|
+
const violations = [];
|
|
6
|
+
const checksPlPath = resolveCorePlPath("checks.pl");
|
|
7
|
+
if (rulesAllowlist.has("query-plan-safety")) {
|
|
8
|
+
violations.push(...collectQueryPlanSafetyViolations(checksPlPath));
|
|
9
|
+
}
|
|
10
|
+
const normalizedChecksPlPath = checksPlPath.replace(/\\/g, "/");
|
|
11
|
+
const checksPlPathEscaped = normalizedChecksPlPath.replace(/'/g, "''");
|
|
12
|
+
const requireAdrStr = requireAdr ? "true" : "false";
|
|
13
|
+
const query = `(load_files('${checksPlPathEscaped}', [if(changed)]),
|
|
14
|
+
( predicate_property(checks:check_all_json_with_options(_, _), _)
|
|
15
|
+
-> call(checks:check_all_json_with_options(JsonString, ${requireAdrStr}))
|
|
16
|
+
; call(checks:check_all_json(JsonString))
|
|
17
|
+
))`;
|
|
18
|
+
const result = await prolog.query(query);
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
throw new Error(`Aggregated checks query failed: ${result.error || "Unknown error"}`);
|
|
21
|
+
}
|
|
22
|
+
const violationsDict = parseViolations(result.bindings.JsonString);
|
|
23
|
+
for (const ruleViolations of Object.values(violationsDict)) {
|
|
24
|
+
for (const v of ruleViolations) {
|
|
25
|
+
const isAllowed = rulesAllowlist.has(v.rule);
|
|
26
|
+
if (isAllowed) {
|
|
27
|
+
violations.push({
|
|
28
|
+
rule: v.rule,
|
|
29
|
+
entityId: v.entityId,
|
|
30
|
+
description: v.description,
|
|
31
|
+
...(v.suggestion ? { suggestion: v.suggestion } : {}),
|
|
32
|
+
...(v.source ? { source: v.source } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return violations;
|
|
38
|
+
}
|
|
39
|
+
function parseViolations(jsonString) {
|
|
40
|
+
try {
|
|
41
|
+
if (!jsonString || typeof jsonString !== "string") {
|
|
42
|
+
throw new Error("No JSON string in binding");
|
|
43
|
+
}
|
|
44
|
+
const parsed = JSON.parse(jsonString);
|
|
45
|
+
const violations = typeof parsed === "string" ? JSON.parse(parsed) : parsed;
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
catch (parseError) {
|
|
49
|
+
throw new Error(`Failed to parse violations JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/tools/check.js
CHANGED
|
@@ -1,99 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
Kibi — repo-local, per-branch, queryable long-term memory for software projects
|
|
3
|
-
Copyright (C) 2026 Piotr Franczyk
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify
|
|
6
|
-
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
-
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
-
(at your option) any later version.
|
|
9
|
-
|
|
10
|
-
This program is distributed in the hope that it will be useful,
|
|
11
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
-
GNU Affero General Public License for more details.
|
|
14
|
-
|
|
15
|
-
You should have received a copy of the GNU Affero General Public License
|
|
16
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
18
|
-
import { readFile } from "node:fs/promises";
|
|
19
|
-
import * as path from "node:path";
|
|
20
|
-
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
1
|
+
import { collectFullKbQualityDiagnostics, } from "kibi-cli/public/impact-diagnostics";
|
|
21
2
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...(d.suggestion !== undefined ? { suggestion: d.suggestion } : {}),
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
function formatViolationText(violations) {
|
|
33
|
-
if (violations.length === 0) {
|
|
34
|
-
return "No violations found";
|
|
35
|
-
}
|
|
36
|
-
const details = violations.map((violation) => {
|
|
37
|
-
const parts = [
|
|
38
|
-
violation.rule,
|
|
39
|
-
violation.entityId,
|
|
40
|
-
violation.source ?? "unknown-source",
|
|
41
|
-
violation.description,
|
|
42
|
-
];
|
|
43
|
-
if (violation.suggestion) {
|
|
44
|
-
parts.push(`Suggestion: ${violation.suggestion}`);
|
|
45
|
-
}
|
|
46
|
-
return `- ${parts.join(" | ")}`;
|
|
47
|
-
});
|
|
48
|
-
return `${violations.length} violations found\n${details.join("\n")}`;
|
|
49
|
-
}
|
|
50
|
-
// implements REQ-002
|
|
51
|
-
async function loadChecksConfig(workspaceRoot) {
|
|
52
|
-
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
53
|
-
try {
|
|
54
|
-
const content = await readFile(configPath, "utf8");
|
|
55
|
-
const parsed = JSON.parse(content);
|
|
56
|
-
const parsedRules = parsed.checks?.rules;
|
|
57
|
-
const normalizedRules = {
|
|
58
|
-
...DEFAULT_CHECKS_CONFIG.rules,
|
|
59
|
-
};
|
|
60
|
-
if (parsedRules && typeof parsedRules === "object") {
|
|
61
|
-
for (const [key, value] of Object.entries(parsedRules)) {
|
|
62
|
-
if (typeof value === "boolean") {
|
|
63
|
-
normalizedRules[key] = value;
|
|
64
|
-
}
|
|
65
|
-
// Ignore non-boolean values (they are not added to normalizedRules, preserving defaults)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const parsedSt = parsed.checks?.symbolTraceability;
|
|
69
|
-
const normalizedSt = { ...DEFAULT_CHECKS_CONFIG.symbolTraceability };
|
|
70
|
-
if (parsedSt && typeof parsedSt === "object") {
|
|
71
|
-
if (typeof parsedSt.requireAdr === "boolean") {
|
|
72
|
-
normalizedSt.requireAdr = parsedSt.requireAdr;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
rules: normalizedRules,
|
|
77
|
-
symbolTraceability: normalizedSt,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return DEFAULT_CHECKS_CONFIG;
|
|
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 [];
|
|
82
10
|
}
|
|
11
|
+
return impactResult.impactDiagnostics.filter((diagnostic) => !diagnostic.blocking && diagnostic.severity !== "error");
|
|
83
12
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
13
|
+
function buildSummary(input) {
|
|
14
|
+
const sections = [formatViolationText(input.violations)];
|
|
15
|
+
if (input.impactResult !== undefined) {
|
|
16
|
+
sections.push(formatImpactText(input.impactResult));
|
|
88
17
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const enabled = configRules[rule] ?? true;
|
|
92
|
-
if (enabled) {
|
|
93
|
-
effective.add(rule);
|
|
94
|
-
}
|
|
18
|
+
if (input.qualityDiagnostics.length > 0) {
|
|
19
|
+
sections.push(formatQualityDiagnosticsText(input.qualityDiagnostics));
|
|
95
20
|
}
|
|
96
|
-
return
|
|
21
|
+
return sections.join("\n");
|
|
97
22
|
}
|
|
98
23
|
/**
|
|
99
24
|
* Handle kb_check tool calls - run validation rules on the KB
|
|
@@ -102,18 +27,41 @@ function getEffectiveRules(configRules, requestedRules) {
|
|
|
102
27
|
// implements REQ-002
|
|
103
28
|
export async function handleKbCheck(prolog, args) {
|
|
104
29
|
const { rules, workspaceRoot: workspaceOverride } = args;
|
|
30
|
+
const hasExplicitRules = rules !== undefined;
|
|
105
31
|
try {
|
|
106
32
|
const workspaceRoot = workspaceOverride ?? resolveWorkspaceRoot();
|
|
107
33
|
const checksConfig = await loadChecksConfig(workspaceRoot);
|
|
108
34
|
const rulesAllowlist = getEffectiveRules(checksConfig.rules, rules);
|
|
35
|
+
const impactResult = analyzeKbCheckImpact(workspaceRoot, args);
|
|
36
|
+
const impactQualityDiagnostics = qualityDiagnosticsFromImpact(impactResult);
|
|
109
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
|
+
});
|
|
110
48
|
return {
|
|
111
|
-
content: [
|
|
112
|
-
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: buildSummary({
|
|
53
|
+
violations: [],
|
|
54
|
+
impactResult,
|
|
55
|
+
qualityDiagnostics,
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
structuredContent: buildStructuredContent({
|
|
113
60
|
violations: [],
|
|
114
|
-
count: 0,
|
|
115
61
|
diagnostics: [],
|
|
116
|
-
|
|
62
|
+
qualityDiagnostics,
|
|
63
|
+
impactResult,
|
|
64
|
+
}),
|
|
117
65
|
};
|
|
118
66
|
}
|
|
119
67
|
// Ensure we read the latest KB state, not a cached snapshot.
|
|
@@ -128,7 +76,22 @@ export async function handleKbCheck(prolog, args) {
|
|
|
128
76
|
...(v.source !== undefined ? { file: v.source } : {}),
|
|
129
77
|
...(v.suggestion !== undefined ? { suggestion: v.suggestion } : {}),
|
|
130
78
|
}));
|
|
131
|
-
const
|
|
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
|
+
});
|
|
132
95
|
return {
|
|
133
96
|
content: [
|
|
134
97
|
{
|
|
@@ -136,11 +99,12 @@ export async function handleKbCheck(prolog, args) {
|
|
|
136
99
|
text: summary,
|
|
137
100
|
},
|
|
138
101
|
],
|
|
139
|
-
structuredContent: {
|
|
102
|
+
structuredContent: buildStructuredContent({
|
|
140
103
|
violations: aggregatedViolations,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
104
|
+
diagnostics,
|
|
105
|
+
qualityDiagnostics,
|
|
106
|
+
impactResult,
|
|
107
|
+
}),
|
|
144
108
|
};
|
|
145
109
|
}
|
|
146
110
|
catch (error) {
|
|
@@ -148,51 +112,3 @@ export async function handleKbCheck(prolog, args) {
|
|
|
148
112
|
throw new Error(`Check execution failed: ${message}`);
|
|
149
113
|
}
|
|
150
114
|
}
|
|
151
|
-
// implements REQ-002
|
|
152
|
-
async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
|
|
153
|
-
const violations = [];
|
|
154
|
-
const checksPlPath = resolveCorePlPath("checks.pl");
|
|
155
|
-
const normalizedChecksPlPath = checksPlPath.replace(/\\/g, "/");
|
|
156
|
-
const checksPlPathEscaped = normalizedChecksPlPath.replace(/'/g, "''");
|
|
157
|
-
// Use check_all_json_with_options if available, otherwise fall back to check_all_json
|
|
158
|
-
const requireAdrStr = requireAdr ? "true" : "false";
|
|
159
|
-
const query = `(use_module('${checksPlPathEscaped}'),
|
|
160
|
-
( predicate_property(checks:check_all_json_with_options(_, _), _)
|
|
161
|
-
-> call(checks:check_all_json_with_options(JsonString, ${requireAdrStr}))
|
|
162
|
-
; call(checks:check_all_json(JsonString))
|
|
163
|
-
))`;
|
|
164
|
-
const result = await prolog.query(query);
|
|
165
|
-
if (!result.success) {
|
|
166
|
-
throw new Error(`Aggregated checks query failed: ${result.error || "Unknown error"}`);
|
|
167
|
-
}
|
|
168
|
-
let violationsDict;
|
|
169
|
-
try {
|
|
170
|
-
const jsonString = result.bindings.JsonString;
|
|
171
|
-
if (!jsonString) {
|
|
172
|
-
throw new Error("No JSON string in binding");
|
|
173
|
-
}
|
|
174
|
-
let parsed = JSON.parse(jsonString);
|
|
175
|
-
if (typeof parsed === "string") {
|
|
176
|
-
parsed = JSON.parse(parsed);
|
|
177
|
-
}
|
|
178
|
-
violationsDict = parsed;
|
|
179
|
-
}
|
|
180
|
-
catch (parseError) {
|
|
181
|
-
throw new Error(`Failed to parse violations JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
182
|
-
}
|
|
183
|
-
for (const ruleViolations of Object.values(violationsDict)) {
|
|
184
|
-
for (const v of ruleViolations) {
|
|
185
|
-
const isAllowed = rulesAllowlist.has(v.rule);
|
|
186
|
-
if (isAllowed) {
|
|
187
|
-
violations.push({
|
|
188
|
-
rule: v.rule,
|
|
189
|
-
entityId: v.entityId,
|
|
190
|
-
description: v.description,
|
|
191
|
-
...(v.suggestion ? { suggestion: v.suggestion } : {}),
|
|
192
|
-
...(v.source ? { source: v.source } : {}),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return violations;
|
|
198
|
-
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
const GENERATOR_PATTERN = /\b(?:kb_entity|kb_relationship|member|memberchk|findall|setof|bagof)\s*\(/;
|
|
3
|
+
const NEGATION_PATTERN = /\\\+\s*/;
|
|
4
|
+
export function collectQueryPlanSafetyViolations(checksPlPath) {
|
|
5
|
+
const source = fs.readFileSync(checksPlPath, "utf8");
|
|
6
|
+
return analyzeSource(source).map((violation) => ({
|
|
7
|
+
rule: "query-plan-safety",
|
|
8
|
+
entityId: violation.predicate,
|
|
9
|
+
description: violation.description,
|
|
10
|
+
suggestion: violation.suggestion,
|
|
11
|
+
source: `${checksPlPath}:${violation.line}`,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function analyzeSource(source) {
|
|
15
|
+
const lines = source.split(/\r?\n/);
|
|
16
|
+
const violations = [];
|
|
17
|
+
let clauseStart = 0;
|
|
18
|
+
let predicate = "unknown";
|
|
19
|
+
let clauseLines = [];
|
|
20
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
21
|
+
const line = lines[index] ?? "";
|
|
22
|
+
if (clauseLines.length === 0 && line.trim().length === 0) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (clauseLines.length === 0) {
|
|
26
|
+
clauseStart = index + 1;
|
|
27
|
+
predicate = predicateNameFrom(line) ?? "unknown";
|
|
28
|
+
}
|
|
29
|
+
clauseLines.push(line);
|
|
30
|
+
if (line.trim().endsWith(".")) {
|
|
31
|
+
const violation = analyzeClause(predicate, clauseStart, clauseLines);
|
|
32
|
+
if (violation)
|
|
33
|
+
violations.push(violation);
|
|
34
|
+
clauseLines = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return violations;
|
|
38
|
+
}
|
|
39
|
+
function predicateNameFrom(line) {
|
|
40
|
+
const match = line.match(/^\s*([a-z][a-zA-Z0-9_]*)\s*\(/);
|
|
41
|
+
return match?.[1] ?? null;
|
|
42
|
+
}
|
|
43
|
+
function analyzeClause(predicate, startLine, lines) {
|
|
44
|
+
const negationIndex = lines.findIndex((line) => NEGATION_PATTERN.test(line));
|
|
45
|
+
if (negationIndex < 0)
|
|
46
|
+
return null;
|
|
47
|
+
const hasLaterGenerator = lines
|
|
48
|
+
.slice(negationIndex + 1)
|
|
49
|
+
.some((line) => GENERATOR_PATTERN.test(line));
|
|
50
|
+
if (!hasLaterGenerator)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
predicate,
|
|
54
|
+
line: startLine + negationIndex,
|
|
55
|
+
description: "Negation appears before later generator calls in the same clause.",
|
|
56
|
+
suggestion: "Move kb_entity/kb_relationship/member/findall generators before \\+/1 so variables are bound before negation.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
}
|
|
93
|
+
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
|
+
}
|
|
118
|
+
}
|
package/dist/tools/skills.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function handleKbSkillsLoad(args) {
|
|
|
36
36
|
content: [
|
|
37
37
|
{
|
|
38
38
|
type: "text",
|
|
39
|
-
text: `Loaded bundled skill ${bundle.manifest.id} with ${resources.length} resources`,
|
|
39
|
+
text: `Loaded bundled skill ${bundle.manifest.id} with ${resources.length} resources: ${formatResourceList(resources)}`,
|
|
40
40
|
},
|
|
41
41
|
],
|
|
42
42
|
structuredContent: payload,
|
|
@@ -65,7 +65,25 @@ export async function handleKbSkillsRead(args) {
|
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
-
throw new Error(`Skills read failed: ${message}`);
|
|
68
|
+
throw new Error(`Skills read failed: ${message}${resourceListHint(args.id)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function formatResourceList(resources) {
|
|
72
|
+
return resources.length === 0 ? "none" : resources.join(", ");
|
|
73
|
+
}
|
|
74
|
+
function resourceListHint(id) {
|
|
75
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const bundle = loadBundledSkill(id);
|
|
80
|
+
return `. Declared resources: ${formatResourceList(bundle.manifest.resources ?? [])}`;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
69
87
|
}
|
|
70
88
|
}
|
|
71
89
|
function assertNonEmptyString(value, field) {
|
package/dist/tools/upsert.js
CHANGED
|
@@ -27,6 +27,7 @@ import { isMcpDebugEnabled } from "../env.js";
|
|
|
27
27
|
import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
|
|
28
28
|
import { readBranchKbStamp } from "../server/kb-freshness.js";
|
|
29
29
|
import { attachedBranchKbPath, updateAttachedBranchStamp, } from "../server/session.js";
|
|
30
|
+
import { formatInvalidRelationshipError, formatRelationshipSourceMismatch, validateLiveRelationshipTargets, } from "./relationship-validation.js";
|
|
30
31
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
31
32
|
let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
|
|
32
33
|
const ajv = new Ajv({ strict: false });
|
|
@@ -209,6 +210,7 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
209
210
|
// Validate strict-lane fact_kind pairing for constrains/requires_property
|
|
210
211
|
// implements REQ-011
|
|
211
212
|
await validateStrictLanePairing(prolog, effectiveRelationships);
|
|
213
|
+
await validateLiveRelationshipTargets(prolog, entity, effectiveRelationships);
|
|
212
214
|
// Process entities
|
|
213
215
|
for (const entity of entities) {
|
|
214
216
|
const id = entity.id;
|
|
@@ -318,6 +320,19 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
318
320
|
relationships_created: relationshipsCreated,
|
|
319
321
|
warnings: [...semanticAdvisor.warnings, ...coverageWarnings],
|
|
320
322
|
semanticAdvisor: semanticAdvisor.receipt,
|
|
323
|
+
...(type === "req"
|
|
324
|
+
? {
|
|
325
|
+
contradictionCheck: {
|
|
326
|
+
outcome: args._skipContradictionCheck
|
|
327
|
+
? "skipped"
|
|
328
|
+
: "no-conflict",
|
|
329
|
+
checked_req_id: entity.id,
|
|
330
|
+
strict_readiness: semanticAdvisor.receipt.logic_readiness === "modeled"
|
|
331
|
+
? "modeled"
|
|
332
|
+
: semanticAdvisor.receipt.candidate_lane,
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
: {}),
|
|
321
336
|
},
|
|
322
337
|
};
|
|
323
338
|
}
|
|
@@ -508,6 +523,9 @@ function buildPropertyList(entity) {
|
|
|
508
523
|
"priority",
|
|
509
524
|
"severity",
|
|
510
525
|
"symbol_role",
|
|
526
|
+
"granularity_reason",
|
|
527
|
+
"verification_scope",
|
|
528
|
+
"verification_perspective",
|
|
511
529
|
// Typed fact enum fields must be atoms for Prolog validation
|
|
512
530
|
"fact_kind",
|
|
513
531
|
"operator",
|
|
@@ -587,7 +605,7 @@ function buildRelationshipMetadata(rel) {
|
|
|
587
605
|
function validateRelationshipSources(entityId, relationships) {
|
|
588
606
|
for (const rel of relationships) {
|
|
589
607
|
if (rel.from !== entityId) {
|
|
590
|
-
throw new Error(
|
|
608
|
+
throw new Error(formatRelationshipSourceMismatch(entityId, rel));
|
|
591
609
|
}
|
|
592
610
|
}
|
|
593
611
|
}
|
|
@@ -766,6 +784,10 @@ function formatUpsertError(entityId, rawError) {
|
|
|
766
784
|
if (!rawError) {
|
|
767
785
|
return `Failed to upsert entity ${entityId}: Unknown error`;
|
|
768
786
|
}
|
|
787
|
+
const invalidRelationshipError = formatInvalidRelationshipError(rawError);
|
|
788
|
+
if (invalidRelationshipError !== null) {
|
|
789
|
+
return `Failed to upsert entity ${entityId}: ${invalidRelationshipError}`;
|
|
790
|
+
}
|
|
769
791
|
// Check for contradiction error - Prolog returns kb_contradiction([...]) term
|
|
770
792
|
// Try to extract readable details from the term
|
|
771
793
|
const contradictionMatch = rawError.match(/kb_contradiction\(\s*\[([^\]]+)\]\s*\)/);
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
|
|
2
|
+
import { validateLiveRelationshipTargets } from "./relationship-validation.js";
|
|
2
3
|
import { validateKbUpsertArgs } from "./upsert.js";
|
|
3
|
-
|
|
4
|
+
function isUpsertArgs(value) {
|
|
5
|
+
return "type" in value && "id" in value && "properties" in value;
|
|
6
|
+
}
|
|
7
|
+
export async function handleKbValidateUpsert(prologOrArgs, maybeArgs) {
|
|
4
8
|
try {
|
|
9
|
+
const args = maybeArgs ?? (isUpsertArgs(prologOrArgs) ? prologOrArgs : null);
|
|
10
|
+
if (args === null) {
|
|
11
|
+
throw new Error("kb_validate_upsert requires an upsert payload");
|
|
12
|
+
}
|
|
13
|
+
const prolog = maybeArgs === undefined ? null : prologOrArgs;
|
|
5
14
|
const { entity } = validateKbUpsertArgs(args);
|
|
15
|
+
if (prolog !== null && !isUpsertArgs(prolog)) {
|
|
16
|
+
await validateLiveRelationshipTargets(prolog, entity, args.relationships ?? []);
|
|
17
|
+
}
|
|
6
18
|
const semanticAdvisor = analyzeSemanticAdvisorInput({
|
|
7
19
|
payload: { ...args },
|
|
8
20
|
});
|
package/dist/tools-config.js
CHANGED
|
@@ -421,6 +421,16 @@ const BASE_TOOLS = [
|
|
|
421
421
|
],
|
|
422
422
|
description: "Optional role classification for symbol entities. Example: 'behavioral'.",
|
|
423
423
|
},
|
|
424
|
+
verification_scope: {
|
|
425
|
+
type: "string",
|
|
426
|
+
enum: ["unit", "integration", "end_to_end"],
|
|
427
|
+
description: "Optional typed verification scope for test entities. Example: 'end_to_end'.",
|
|
428
|
+
},
|
|
429
|
+
verification_perspective: {
|
|
430
|
+
type: "string",
|
|
431
|
+
enum: ["internal", "consumer"],
|
|
432
|
+
description: "Optional typed verification perspective for test entities. Example: 'consumer'.",
|
|
433
|
+
},
|
|
424
434
|
fact_kind: {
|
|
425
435
|
type: "string",
|
|
426
436
|
enum: [
|
|
@@ -592,7 +602,7 @@ const BASE_TOOLS = [
|
|
|
592
602
|
},
|
|
593
603
|
{
|
|
594
604
|
name: "kb_check",
|
|
595
|
-
description: "Run KB validation rules and return violations. Use before or after mutations. Do not use for point lookups. No write side effects. Prefer explicit rules for faster iteration.",
|
|
605
|
+
description: "Run KB validation rules and return violations. Use before or after mutations, and after meaningful source edits with impact options to surface symbol granularity and semantic-review diagnostics. Do not use for point lookups. No write side effects. Prefer explicit rules for faster iteration; omit rules for final full validation plus full-KB qualityDiagnostics review.",
|
|
596
606
|
inputSchema: {
|
|
597
607
|
type: "object",
|
|
598
608
|
properties: {
|
|
@@ -611,9 +621,37 @@ const BASE_TOOLS = [
|
|
|
611
621
|
"domain-contradictions",
|
|
612
622
|
"strict-fact-shape",
|
|
613
623
|
"strict-req-fact-pairing",
|
|
624
|
+
"predicate-verifiability",
|
|
625
|
+
"query-plan-safety",
|
|
614
626
|
],
|
|
615
627
|
},
|
|
616
|
-
description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape, strict-req-fact-pairing. If omitted, server runs all.",
|
|
628
|
+
description: "Optional rule subset. Allowed: must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, no-cycles, required-fields, deprecated-adr-no-successor, domain-contradictions, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, query-plan-safety. If omitted, server runs all rules plus the full-KB qualityDiagnostics audit scan; if supplied, server preserves scoped validation and skips the full-KB advisory scan.",
|
|
629
|
+
},
|
|
630
|
+
sourceFiles: {
|
|
631
|
+
type: "array",
|
|
632
|
+
items: { type: "string" },
|
|
633
|
+
description: "Optional repo-relative source files to inspect for early impact diagnostics. Use with includeImpactDiagnostics after meaningful source edits.",
|
|
634
|
+
},
|
|
635
|
+
staged: {
|
|
636
|
+
type: "boolean",
|
|
637
|
+
description: "When true, inspect staged source changes for impact diagnostics using the shared CLI impact analyzer without shelling out to kibi check.",
|
|
638
|
+
},
|
|
639
|
+
includeWorkingTreeDiff: {
|
|
640
|
+
type: "boolean",
|
|
641
|
+
description: "When true, inspect current unstaged working-tree diffs for impact diagnostics. Pair with sourceFiles to scope the analysis.",
|
|
642
|
+
},
|
|
643
|
+
includeImpactDiagnostics: {
|
|
644
|
+
type: "boolean",
|
|
645
|
+
description: "When true, include changed-file impact diagnostics such as symbol_granularity_violation and symbol_semantic_review_needed in structured output.",
|
|
646
|
+
},
|
|
647
|
+
maxDiagnostics: {
|
|
648
|
+
type: "integer",
|
|
649
|
+
minimum: 0,
|
|
650
|
+
description: "Optional maximum number of impact diagnostics to return. Graph validation violations are not capped by this value.",
|
|
651
|
+
},
|
|
652
|
+
workspaceRoot: {
|
|
653
|
+
type: "string",
|
|
654
|
+
description: "Optional workspace root for impact diagnostics and .kb/config.json lookup. Defaults to the MCP server workspace.",
|
|
617
655
|
},
|
|
618
656
|
},
|
|
619
657
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"fast-glob": "^3.2.12",
|
|
10
10
|
"gray-matter": "^4.0.3",
|
|
11
11
|
"js-yaml": "^4.1.0",
|
|
12
|
-
"kibi-cli": "^0.
|
|
13
|
-
"kibi-core": "^0.
|
|
12
|
+
"kibi-cli": "^0.14.0",
|
|
13
|
+
"kibi-core": "^0.7.0",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|