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.
Files changed (43) hide show
  1. package/bin/kibi-mcp +1 -1
  2. package/dist/diagnostics-helpers.js +22 -0
  3. package/dist/diagnostics.js +113 -4
  4. package/dist/runtime/mcp-runtime.js +43 -0
  5. package/dist/server/docs.js +11 -10
  6. package/dist/server/json-schema-to-zod.js +73 -2
  7. package/dist/server/session.js +33 -15
  8. package/dist/server/tool-registration.js +35 -59
  9. package/dist/server/tools-runtime.js +52 -2
  10. package/dist/server/tools.js +19 -5
  11. package/dist/server/transport.js +9 -5
  12. package/dist/tools/autopilot-generate.js +37 -646
  13. package/dist/tools/autopilot-test-compat.js +88 -0
  14. package/dist/tools/autopilot-test-parsers.js +120 -0
  15. package/dist/tools/check-config.js +67 -0
  16. package/dist/tools/check-format.js +92 -0
  17. package/dist/tools/check-impact.js +45 -0
  18. package/dist/tools/check-prolog.js +51 -0
  19. package/dist/tools/check.js +13 -190
  20. package/dist/tools/coverage.js +11 -25
  21. package/dist/tools/delete.js +5 -112
  22. package/dist/tools/discovery-adapter.js +23 -0
  23. package/dist/tools/entity-query.js +1 -111
  24. package/dist/tools/find-gaps.js +11 -26
  25. package/dist/tools/graph.js +11 -24
  26. package/dist/tools/model-requirement.js +8 -347
  27. package/dist/tools/mutation-context.js +28 -0
  28. package/dist/tools/query-plan-safety.js +58 -0
  29. package/dist/tools/query.js +5 -50
  30. package/dist/tools/relationship-validation.js +11 -0
  31. package/dist/tools/search.js +4 -37
  32. package/dist/tools/semantic-advisor.js +2 -40
  33. package/dist/tools/skills.js +25 -47
  34. package/dist/tools/sparql.js +12 -93
  35. package/dist/tools/status.js +5 -19
  36. package/dist/tools/suggest-predicates.js +11 -2448
  37. package/dist/tools/upsert.js +21 -794
  38. package/dist/tools/validate-upsert.js +16 -40
  39. package/dist/tools-config.js +75 -764
  40. package/package.json +4 -3
  41. package/dist/semantic-advisor/analyze-prose.js +0 -1367
  42. package/dist/semantic-advisor/prose-coverage-evaluator.js +0 -66
  43. /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
+ }
@@ -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 { rules, workspaceRoot: workspaceOverride } = args;
105
- try {
106
- const workspaceRoot = workspaceOverride ?? resolveWorkspaceRoot();
107
- const checksConfig = await loadChecksConfig(workspaceRoot);
108
- const rulesAllowlist = getEffectiveRules(checksConfig.rules, rules);
109
- if (rulesAllowlist.size === 0) {
110
- return {
111
- content: [{ type: "text", text: "No violations found" }],
112
- structuredContent: {
113
- violations: [],
114
- count: 0,
115
- diagnostics: [],
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
  }