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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/tools/check.js
CHANGED
|
@@ -1,198 +1,21 @@
|
|
|
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 { checkSpec } from "kibi-cli/operations";
|
|
21
2
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
|
-
import { resolveCorePlPath } from "./core-module.js";
|
|
23
|
-
function formatDiagnosticsForMcp(diagnostics) {
|
|
24
|
-
return diagnostics.map((d) => ({
|
|
25
|
-
category: d.category,
|
|
26
|
-
severity: d.severity,
|
|
27
|
-
message: d.message,
|
|
28
|
-
...(d.file !== undefined ? { file: d.file } : {}),
|
|
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;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// implements REQ-002
|
|
85
|
-
function getEffectiveRules(configRules, requestedRules) {
|
|
86
|
-
if (requestedRules && requestedRules.length > 0) {
|
|
87
|
-
return new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
88
|
-
}
|
|
89
|
-
const effective = new Set();
|
|
90
|
-
for (const rule of RULE_NAMES) {
|
|
91
|
-
const enabled = configRules[rule] ?? true;
|
|
92
|
-
if (enabled) {
|
|
93
|
-
effective.add(rule);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return effective;
|
|
97
|
-
}
|
|
98
3
|
/**
|
|
99
4
|
* Handle kb_check tool calls - run validation rules on the KB
|
|
100
5
|
* Reuses validation logic from CLI check command
|
|
101
6
|
*/
|
|
102
7
|
// implements REQ-002
|
|
103
8
|
export async function handleKbCheck(prolog, args) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
// Ensure we read the latest KB state, not a cached snapshot.
|
|
120
|
-
prolog.invalidateCache();
|
|
121
|
-
// Run aggregated checks using same approach as CLI
|
|
122
|
-
// This now runs ALL rules including symbol-traceability
|
|
123
|
-
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist, checksConfig.symbolTraceability.requireAdr);
|
|
124
|
-
const diagnostics = aggregatedViolations.map((v) => ({
|
|
125
|
-
category: "SYNC_ERROR",
|
|
126
|
-
severity: "error",
|
|
127
|
-
message: v.description,
|
|
128
|
-
...(v.source !== undefined ? { file: v.source } : {}),
|
|
129
|
-
...(v.suggestion !== undefined ? { suggestion: v.suggestion } : {}),
|
|
130
|
-
}));
|
|
131
|
-
const summary = formatViolationText(aggregatedViolations);
|
|
132
|
-
return {
|
|
133
|
-
content: [
|
|
134
|
-
{
|
|
135
|
-
type: "text",
|
|
136
|
-
text: summary,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
structuredContent: {
|
|
140
|
-
violations: aggregatedViolations,
|
|
141
|
-
count: aggregatedViolations.length,
|
|
142
|
-
diagnostics: formatDiagnosticsForMcp(diagnostics),
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
-
throw new Error(`Check execution failed: ${message}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
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;
|
|
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
|
+
});
|
|
198
21
|
}
|