kibi-mcp 0.4.0 → 0.5.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/dist/diagnostics.js +3 -4
- package/dist/server/docs.js +13 -3
- package/dist/server/tools.js +1 -1
- package/dist/tools/check.js +7 -47
- package/dist/tools/core-module.js +6 -14
- package/dist/tools/entity-query.js +1 -0
- package/dist/tools/find-gaps.js +1 -1
- package/dist/tools/prolog-list.js +1 -43
- package/dist/tools/search.js +1 -1
- package/dist/tools/symbols.js +6 -1
- package/dist/tools/upsert.js +161 -35
- package/dist/tools-config.js +3 -2
- package/package.json +3 -3
package/dist/diagnostics.js
CHANGED
|
@@ -89,7 +89,8 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
|
89
89
|
telemetry_status: telemetry ? "provided" : "missing",
|
|
90
90
|
};
|
|
91
91
|
const structuredContent = result && typeof result === "object" && "structuredContent" in result
|
|
92
|
-
? result
|
|
92
|
+
? result
|
|
93
|
+
.structuredContent
|
|
93
94
|
: undefined;
|
|
94
95
|
if (toolName === "kb_query" || toolName === "kb_search") {
|
|
95
96
|
const resultCount = Number(structuredContent?.count ?? 0);
|
|
@@ -103,9 +104,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
|
|
|
103
104
|
fields.violation_count = violationCount;
|
|
104
105
|
fields.requested_rules = Array.isArray(args.rules) ? args.rules : [];
|
|
105
106
|
fields.result_summary =
|
|
106
|
-
violationCount === 0
|
|
107
|
-
? "0 violations"
|
|
108
|
-
: `${violationCount} violations`;
|
|
107
|
+
violationCount === 0 ? "0 violations" : `${violationCount} violations`;
|
|
109
108
|
}
|
|
110
109
|
if (!fields.result_summary) {
|
|
111
110
|
fields.result_summary = `${toolName} completed`;
|
package/dist/server/docs.js
CHANGED
|
@@ -30,11 +30,13 @@ function renderToolsDoc() {
|
|
|
30
30
|
const required = Array.isArray(tool.inputSchema?.required)
|
|
31
31
|
? tool.inputSchema.required.join(", ")
|
|
32
32
|
: "none";
|
|
33
|
-
lines.push(`| ${tool.name} | ${tool.description} | ${required} |`);
|
|
34
33
|
}
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("Modeling note: Prefer query-first discovery; create `fact` entities before `req` entities and express semantics via `constrains` + `requires_property`.");
|
|
35
36
|
return lines.join("\n");
|
|
36
37
|
}
|
|
37
38
|
export const PROMPTS = [
|
|
39
|
+
// implements REQ-002, REQ-013, REQ-mcp-search-discovery
|
|
38
40
|
{
|
|
39
41
|
name: "init-kibi",
|
|
40
42
|
description: "Bootstrap Kibi on an existing repository with zero entities.",
|
|
@@ -123,6 +125,8 @@ export const PROMPTS = [
|
|
|
123
125
|
"- Run `kb_check` after meaningful mutations to catch integrity issues early.",
|
|
124
126
|
"- Prefer explicit IDs and enum values to avoid invalid parameters.",
|
|
125
127
|
"- Assume every write can affect downstream traceability queries.",
|
|
128
|
+
"- Model requirements by first creating/reusing fact entities, then express req semantics with `constrains` + `requires_property` relationships (create-before-link).",
|
|
129
|
+
"- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
|
|
126
130
|
].join("\n"),
|
|
127
131
|
},
|
|
128
132
|
{
|
|
@@ -130,7 +134,6 @@ export const PROMPTS = [
|
|
|
130
134
|
description: "Step-by-step call order for discovery, mutation, and verification.",
|
|
131
135
|
text: [
|
|
132
136
|
"# kibi-mcp Workflow",
|
|
133
|
-
"",
|
|
134
137
|
"Follow this sequence for reliable operation:",
|
|
135
138
|
"",
|
|
136
139
|
"1. **Discover first**: Call `kb_search` for exploratory discovery, then `kb_query` to confirm exact current state before mutation.",
|
|
@@ -196,7 +199,7 @@ function registerDocResources() {
|
|
|
196
199
|
"## Discover before mutating",
|
|
197
200
|
'1. `kb_search` with `{ "query": "login flow" }` to discover related requirements, tests, and ADRs',
|
|
198
201
|
'2. `kb_query` with `{ "type": "req", "sourceFile": "src/auth/login.ts" }` for exact follow-up',
|
|
199
|
-
|
|
202
|
+
"3. `kb_status` with `{}` when branch attachment or freshness confidence matters",
|
|
200
203
|
"",
|
|
201
204
|
"## Model requirements as reusable facts",
|
|
202
205
|
'1. `kb_query` with `{ "type": "fact" }` to find existing fact IDs before creating new ones',
|
|
@@ -205,6 +208,13 @@ function registerDocResources() {
|
|
|
205
208
|
"4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
|
|
206
209
|
'5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
|
|
207
210
|
"",
|
|
211
|
+
"Note: Create or reuse `fact` entities first, then create `req` entities and link with `constrains` and `requires_property` (create-before-link). Use `flag` for runtime/config gates; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
|
|
212
|
+
"",
|
|
213
|
+
"## Find missing coverage",
|
|
214
|
+
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
|
215
|
+
"",
|
|
216
|
+
"## Find missing coverage",
|
|
217
|
+
"",
|
|
208
218
|
"## Find missing coverage",
|
|
209
219
|
'1. `kb_find_gaps` with `{ "type": "req", "missingRelationships": ["specified_by", "verified_by"] }` to find under-linked requirements',
|
|
210
220
|
'2. `kb_coverage` with `{ "by": "req", "includePassing": false }` to review evaluated coverage rows',
|
package/dist/server/tools.js
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { DIAGNOSTIC_MODE_ENABLED, appendUsageLogLine, deriveDiagnosticFields, extractToolCallPayload, } from "../diagnostics.js";
|
|
21
|
-
import { handleKbCoverage } from "../tools/coverage.js";
|
|
22
21
|
import { TOOLS } from "../tools-config.js";
|
|
23
22
|
import { handleKbCheck } from "../tools/check.js";
|
|
23
|
+
import { handleKbCoverage } from "../tools/coverage.js";
|
|
24
24
|
import { handleKbDelete } from "../tools/delete.js";
|
|
25
25
|
import { handleKbFindGaps } from "../tools/find-gaps.js";
|
|
26
26
|
import { handleKbGraph } from "../tools/graph.js";
|
package/dist/tools/check.js
CHANGED
|
@@ -16,45 +16,10 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
-
import { createRequire } from "node:module";
|
|
20
19
|
import * as path from "node:path";
|
|
20
|
+
import { DEFAULT_CHECKS_CONFIG, RULE_NAMES, } from "kibi-cli/public/check-types";
|
|
21
21
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
22
|
-
|
|
23
|
-
function resolveChecksPlPath() {
|
|
24
|
-
const overrideChecksPath = process.env.KIBI_CHECKS_PL_PATH;
|
|
25
|
-
if (overrideChecksPath && existsSync(overrideChecksPath)) {
|
|
26
|
-
return overrideChecksPath;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const installedChecksPl = require.resolve("kibi-core/src/checks.pl");
|
|
30
|
-
if (existsSync(installedChecksPl)) {
|
|
31
|
-
return installedChecksPl;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch { }
|
|
35
|
-
const localChecksPl = path.join(process.cwd(), "packages/core/src/checks.pl");
|
|
36
|
-
if (existsSync(localChecksPl)) {
|
|
37
|
-
return localChecksPl;
|
|
38
|
-
}
|
|
39
|
-
throw new Error("Unable to resolve checks.pl path");
|
|
40
|
-
}
|
|
41
|
-
const ALL_RULES = [
|
|
42
|
-
"must-priority-coverage",
|
|
43
|
-
"no-dangling-refs",
|
|
44
|
-
"no-cycles",
|
|
45
|
-
"required-fields",
|
|
46
|
-
"symbol-coverage",
|
|
47
|
-
"symbol-traceability",
|
|
48
|
-
"deprecated-adr-no-successor",
|
|
49
|
-
"domain-contradictions",
|
|
50
|
-
];
|
|
51
|
-
const RULE_NAMES = new Set(ALL_RULES);
|
|
52
|
-
const DEFAULT_CHECKS_CONFIG = {
|
|
53
|
-
rules: Object.fromEntries(ALL_RULES.map((rule) => [rule, true])),
|
|
54
|
-
symbolTraceability: {
|
|
55
|
-
requireAdr: false,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
22
|
+
import { resolveCorePlPath } from "./core-module.js";
|
|
58
23
|
function formatDiagnosticsForMcp(diagnostics) {
|
|
59
24
|
return diagnostics.map((d) => ({
|
|
60
25
|
category: d.category,
|
|
@@ -121,21 +86,16 @@ function loadChecksConfig(workspaceRoot) {
|
|
|
121
86
|
}
|
|
122
87
|
// implements REQ-002
|
|
123
88
|
function getEffectiveRules(configRules, requestedRules) {
|
|
89
|
+
if (requestedRules && requestedRules.length > 0) {
|
|
90
|
+
return new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
91
|
+
}
|
|
124
92
|
const effective = new Set();
|
|
125
|
-
for (const rule of
|
|
93
|
+
for (const rule of RULE_NAMES) {
|
|
126
94
|
const enabled = configRules[rule] ?? true;
|
|
127
95
|
if (enabled) {
|
|
128
96
|
effective.add(rule);
|
|
129
97
|
}
|
|
130
98
|
}
|
|
131
|
-
if (requestedRules && requestedRules.length > 0) {
|
|
132
|
-
const allowed = new Set(requestedRules.filter((rule) => RULE_NAMES.has(rule)));
|
|
133
|
-
for (const rule of Array.from(effective)) {
|
|
134
|
-
if (!allowed.has(rule)) {
|
|
135
|
-
effective.delete(rule);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
99
|
return effective;
|
|
140
100
|
}
|
|
141
101
|
/**
|
|
@@ -192,7 +152,7 @@ export async function handleKbCheck(prolog, args) {
|
|
|
192
152
|
// implements REQ-002
|
|
193
153
|
async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr) {
|
|
194
154
|
const violations = [];
|
|
195
|
-
const checksPlPath =
|
|
155
|
+
const checksPlPath = resolveCorePlPath("checks.pl");
|
|
196
156
|
const normalizedChecksPlPath = checksPlPath.replace(/\\/g, "/");
|
|
197
157
|
const checksPlPathEscaped = normalizedChecksPlPath.replace(/'/g, "''");
|
|
198
158
|
// Use check_all_json_with_options if available, otherwise fall back to check_all_json
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { PrologProcess } from "kibi-cli/prolog";
|
|
3
|
+
import { PrologProcess, resolveKbPlPath } from "kibi-cli/prolog";
|
|
5
4
|
import { escapeAtomContent } from "kibi-cli/prolog/codec";
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
5
|
// implements REQ-002, REQ-013
|
|
8
6
|
export function resolveCorePlPath(fileName) {
|
|
9
7
|
const envKey = `KIBI_${fileName.replace(/\W/g, "_").toUpperCase()}_PATH`;
|
|
@@ -11,18 +9,12 @@ export function resolveCorePlPath(fileName) {
|
|
|
11
9
|
if (override && existsSync(override)) {
|
|
12
10
|
return override;
|
|
13
11
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch { }
|
|
21
|
-
const localPath = path.join(process.cwd(), "packages/core/src", fileName);
|
|
22
|
-
if (existsSync(localPath)) {
|
|
23
|
-
return localPath;
|
|
12
|
+
const kbPlPath = resolveKbPlPath();
|
|
13
|
+
const sibling = path.join(path.dirname(kbPlPath), fileName);
|
|
14
|
+
if (existsSync(sibling)) {
|
|
15
|
+
return sibling;
|
|
24
16
|
}
|
|
25
|
-
throw new Error(`
|
|
17
|
+
throw new Error(`Root-consistency error: resolveKbPlPath() resolved to '${kbPlPath}' but sibling '${fileName}' not found at '${sibling}'`);
|
|
26
18
|
}
|
|
27
19
|
// implements REQ-002, REQ-013
|
|
28
20
|
export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel) {
|
package/dist/tools/find-gaps.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runJsonModuleQuery, toPrologAtom, toPrologList } from "./core-module.js";
|
|
1
|
+
import { runJsonModuleQuery, toPrologAtom, toPrologList, } from "./core-module.js";
|
|
2
2
|
import { validateEntityType } from "./entity-query.js";
|
|
3
3
|
// implements REQ-002, REQ-013
|
|
4
4
|
export async function handleKbFindGaps(prolog, args) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
+
import { splitTopLevel } from "kibi-cli/prolog/codec";
|
|
18
19
|
// implements REQ-002
|
|
19
20
|
export function parseAtomList(raw) {
|
|
20
21
|
const trimmed = raw.trim();
|
|
@@ -86,49 +87,6 @@ function unwrapList(value) {
|
|
|
86
87
|
}
|
|
87
88
|
return value;
|
|
88
89
|
}
|
|
89
|
-
function splitTopLevel(input, delimiter) {
|
|
90
|
-
const parts = [];
|
|
91
|
-
let current = "";
|
|
92
|
-
let depth = 0;
|
|
93
|
-
let inDoubleQuotes = false;
|
|
94
|
-
let inSingleQuotes = false;
|
|
95
|
-
for (let i = 0; i < input.length; i++) {
|
|
96
|
-
const ch = input[i];
|
|
97
|
-
const prev = i > 0 ? input[i - 1] : "";
|
|
98
|
-
if (ch === '"' && !inSingleQuotes && prev !== "\\") {
|
|
99
|
-
inDoubleQuotes = !inDoubleQuotes;
|
|
100
|
-
current += ch;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (ch === "'" && !inDoubleQuotes && prev !== "\\") {
|
|
104
|
-
inSingleQuotes = !inSingleQuotes;
|
|
105
|
-
current += ch;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!inSingleQuotes && !inDoubleQuotes && (ch === "[" || ch === "(")) {
|
|
109
|
-
depth++;
|
|
110
|
-
current += ch;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (!inSingleQuotes && !inDoubleQuotes && (ch === "]" || ch === ")")) {
|
|
114
|
-
depth--;
|
|
115
|
-
current += ch;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!inSingleQuotes && !inDoubleQuotes && depth === 0 && ch === delimiter) {
|
|
119
|
-
if (current.length > 0) {
|
|
120
|
-
parts.push(current);
|
|
121
|
-
}
|
|
122
|
-
current = "";
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
current += ch;
|
|
126
|
-
}
|
|
127
|
-
if (current.length > 0) {
|
|
128
|
-
parts.push(current);
|
|
129
|
-
}
|
|
130
|
-
return parts;
|
|
131
|
-
}
|
|
132
90
|
function stripQuotes(value) {
|
|
133
91
|
if (value.startsWith("'") && value.endsWith("'")) {
|
|
134
92
|
return value.slice(1, -1);
|
package/dist/tools/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rankEntities } from "kibi-cli/search-ranking";
|
|
2
|
-
import { loadEntities, paginateResults, validateEntityType, } from "./entity-query.js";
|
|
3
2
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
3
|
+
import { loadEntities, paginateResults, validateEntityType, } from "./entity-query.js";
|
|
4
4
|
// implements REQ-mcp-search-discovery, REQ-002
|
|
5
5
|
export async function handleKbSearch(prolog, args) {
|
|
6
6
|
const { query, type, limit = 20, offset = 0 } = args;
|
package/dist/tools/symbols.js
CHANGED
|
@@ -45,6 +45,7 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
45
45
|
".cjs",
|
|
46
46
|
]);
|
|
47
47
|
export async function handleKbSymbolsRefresh(args) {
|
|
48
|
+
// implements REQ-vscode-traceability
|
|
48
49
|
const dryRun = args.dryRun === true;
|
|
49
50
|
const workspaceRoot = resolveWorkspaceRoot();
|
|
50
51
|
const manifestPath = resolveManifestPath(workspaceRoot);
|
|
@@ -112,6 +113,7 @@ export async function handleKbSymbolsRefresh(args) {
|
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = resolveWorkspaceRoot()) {
|
|
116
|
+
// implements REQ-vscode-traceability
|
|
115
117
|
const manifestPath = resolveManifestPath(workspaceRoot);
|
|
116
118
|
const rawContent = readFileSync(manifestPath, "utf8");
|
|
117
119
|
const parsed = parseYAML(rawContent);
|
|
@@ -151,6 +153,7 @@ export async function refreshCoordinatesForSymbolId(symbolId, workspaceRoot = re
|
|
|
151
153
|
return { refreshed, found: true };
|
|
152
154
|
}
|
|
153
155
|
export function resolveManifestPath(workspaceRoot) {
|
|
156
|
+
// implements REQ-002, REQ-013
|
|
154
157
|
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
155
158
|
if (existsSync(configPath)) {
|
|
156
159
|
try {
|
|
@@ -168,7 +171,9 @@ export function resolveManifestPath(workspaceRoot) {
|
|
|
168
171
|
: path.resolve(workspaceRoot, config.symbolsManifest);
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
|
-
catch {
|
|
174
|
+
catch {
|
|
175
|
+
// config file missing or malformed; fall through to defaults
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
const candidates = [
|
|
174
179
|
path.join(workspaceRoot, "symbols.yaml"),
|
package/dist/tools/upsert.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import Ajv from "ajv";
|
|
19
|
-
import { escapeAtom, toPrologAtom } from "kibi-cli/prolog/codec";
|
|
19
|
+
import { escapeAtom, toPrologAtom, toPrologString, } from "kibi-cli/prolog/codec";
|
|
20
20
|
import entitySchema from "kibi-cli/schemas/entity";
|
|
21
21
|
import relationshipSchema from "kibi-cli/schemas/relationship";
|
|
22
22
|
import { refreshCoordinatesForSymbolId } from "./symbols.js";
|
|
23
|
+
let refreshCoordinatesForSymbolIdImpl = refreshCoordinatesForSymbolId;
|
|
23
24
|
const ajv = new Ajv({ strict: false });
|
|
24
25
|
const validateEntity = ajv.compile(entitySchema);
|
|
25
26
|
const validateRelationship = ajv.compile(relationshipSchema);
|
|
@@ -73,6 +74,10 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
73
74
|
throw new Error(`Relationship validation failed at index ${i}: ${errorMessages}`);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
validateRelationshipSources(id, relationships);
|
|
78
|
+
// Validate strict-lane fact_kind pairing for constrains/requires_property
|
|
79
|
+
// implements REQ-011
|
|
80
|
+
await validateStrictLanePairing(prolog, relationships);
|
|
76
81
|
let created = 0;
|
|
77
82
|
let updated = 0;
|
|
78
83
|
let relationshipsCreated = 0;
|
|
@@ -94,26 +99,44 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
94
99
|
const from = rel.from;
|
|
95
100
|
const to = rel.to;
|
|
96
101
|
const metadata = buildRelationshipMetadata(rel);
|
|
97
|
-
relationshipGoals.push(`
|
|
102
|
+
relationshipGoals.push(`kb_assert_relationship_no_audit(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
|
|
98
103
|
}
|
|
99
104
|
// Build atomic transaction goal wrapping entity + all relationships
|
|
105
|
+
// For requirements, also include contradiction check within the transaction
|
|
100
106
|
// implements REQ-002, REQ-011
|
|
101
107
|
let transactionGoal;
|
|
108
|
+
const needsContradictionCheck = type === "req" && !args._skipContradictionCheck;
|
|
102
109
|
if (relationshipGoals.length === 0) {
|
|
103
110
|
// Simple case: just entity
|
|
104
|
-
|
|
111
|
+
if (needsContradictionCheck) {
|
|
112
|
+
transactionGoal = `rdf_transaction((kb_assert_entity_no_audit(${type}, ${props}), check_req_contradiction('${escapeAtom(id)}')))`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
transactionGoal = `rdf_transaction((kb_assert_entity_no_audit(${type}, ${props})))`;
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
else {
|
|
107
119
|
// Entity + relationships in one transaction
|
|
108
120
|
const goals = [
|
|
109
|
-
`
|
|
121
|
+
`kb_assert_entity_no_audit(${type}, ${props})`,
|
|
110
122
|
...relationshipGoals,
|
|
111
123
|
].join(", ");
|
|
112
|
-
|
|
124
|
+
if (needsContradictionCheck) {
|
|
125
|
+
transactionGoal = `rdf_transaction((${goals}, check_req_contradiction('${escapeAtom(id)}')))`;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
transactionGoal = `rdf_transaction((${goals}))`;
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
const txResult = await prolog.query(transactionGoal);
|
|
115
132
|
if (!txResult.success) {
|
|
116
|
-
|
|
133
|
+
// Format error message without exposing raw transaction goal
|
|
134
|
+
const formattedError = formatUpsertError(id, txResult.error);
|
|
135
|
+
throw new Error(formattedError);
|
|
136
|
+
}
|
|
137
|
+
await recordEntityAudit(prolog, type, entity);
|
|
138
|
+
for (const rel of relationships) {
|
|
139
|
+
await recordRelationshipAudit(prolog, rel);
|
|
117
140
|
}
|
|
118
141
|
// Update counters
|
|
119
142
|
if (isUpdate) {
|
|
@@ -131,13 +154,9 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
131
154
|
if (!saveResult.success) {
|
|
132
155
|
throw new Error(`Failed to save KB after upsert: ${saveResult.error || "Unknown error"}`);
|
|
133
156
|
}
|
|
134
|
-
let contradictionPairsDetected;
|
|
135
|
-
if (type === "req" && !args._skipContradictionCheck) {
|
|
136
|
-
contradictionPairsDetected = await detectContradictionPairs(prolog, id);
|
|
137
|
-
}
|
|
138
157
|
if (type === "symbol") {
|
|
139
158
|
try {
|
|
140
|
-
await
|
|
159
|
+
await refreshCoordinatesForSymbolIdImpl(id);
|
|
141
160
|
}
|
|
142
161
|
catch (error) {
|
|
143
162
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -150,16 +169,13 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
150
169
|
content: [
|
|
151
170
|
{
|
|
152
171
|
type: "text",
|
|
153
|
-
text:
|
|
154
|
-
? `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s). Contradiction probe detected ${contradictionPairsDetected} potential conflict pair(s).`
|
|
155
|
-
: `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
|
|
172
|
+
text: `Upserted ${id} (${created > 0 ? "created" : "updated"}) with ${relationshipsCreated} relationship(s).`,
|
|
156
173
|
},
|
|
157
174
|
],
|
|
158
175
|
structuredContent: {
|
|
159
176
|
created,
|
|
160
177
|
updated,
|
|
161
178
|
relationships_created: relationshipsCreated,
|
|
162
|
-
contradiction_pairs_detected: contradictionPairsDetected,
|
|
163
179
|
},
|
|
164
180
|
};
|
|
165
181
|
}
|
|
@@ -168,27 +184,34 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
168
184
|
throw new Error(`Upsert execution failed: ${message}`);
|
|
169
185
|
}
|
|
170
186
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
const raw = result.bindings.Count;
|
|
179
|
-
const count = Number(raw);
|
|
180
|
-
return Number.isFinite(count) ? count : 0;
|
|
181
|
-
}
|
|
187
|
+
export const __test__ = {
|
|
188
|
+
// implements REQ-vscode-traceability
|
|
189
|
+
setRefreshCoordinatesForSymbolIdForTests(fn) {
|
|
190
|
+
refreshCoordinatesForSymbolIdImpl = fn ?? refreshCoordinatesForSymbolId;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
182
193
|
/**
|
|
183
194
|
* Build Prolog property list from entity object
|
|
184
195
|
* Returns simple Key=Value format without typed literals
|
|
185
196
|
* Example output: "[id='test-1', title=\"Test\", status=active]"
|
|
197
|
+
* implements REQ-002
|
|
186
198
|
*/
|
|
187
199
|
function buildPropertyList(entity) {
|
|
188
200
|
const pairs = [];
|
|
189
201
|
// Defined internally to ensure thread safety and avoid initialization order issues.
|
|
190
202
|
// Using simple arrays instead of Sets is performant enough for small lists and avoids Set allocation overhead.
|
|
191
|
-
|
|
203
|
+
// implements REQ-002
|
|
204
|
+
const ATOM_FIELDS = [
|
|
205
|
+
"status",
|
|
206
|
+
"owner",
|
|
207
|
+
"priority",
|
|
208
|
+
"severity",
|
|
209
|
+
// Typed fact enum fields must be atoms for Prolog validation
|
|
210
|
+
"fact_kind",
|
|
211
|
+
"operator",
|
|
212
|
+
"value_type",
|
|
213
|
+
"polarity",
|
|
214
|
+
];
|
|
192
215
|
const STRING_FIELDS = [
|
|
193
216
|
"id",
|
|
194
217
|
"title",
|
|
@@ -200,6 +223,8 @@ function buildPropertyList(entity) {
|
|
|
200
223
|
for (const [key, value] of Object.entries(entity)) {
|
|
201
224
|
if (key === "type")
|
|
202
225
|
continue;
|
|
226
|
+
if (value === undefined || value === null)
|
|
227
|
+
continue;
|
|
203
228
|
let prologValue;
|
|
204
229
|
if (key === "id" && typeof value === "string") {
|
|
205
230
|
prologValue = `'${value.replace(/'/g, "''")}'`;
|
|
@@ -211,10 +236,10 @@ function buildPropertyList(entity) {
|
|
|
211
236
|
prologValue = toPrologAtom(value);
|
|
212
237
|
}
|
|
213
238
|
else if (STRING_FIELDS.includes(key) && typeof value === "string") {
|
|
214
|
-
prologValue =
|
|
239
|
+
prologValue = `${toPrologString(value)}`;
|
|
215
240
|
}
|
|
216
241
|
else if (typeof value === "string") {
|
|
217
|
-
prologValue =
|
|
242
|
+
prologValue = `${toPrologString(value)}`;
|
|
218
243
|
}
|
|
219
244
|
else if (typeof value === "number") {
|
|
220
245
|
prologValue = String(value);
|
|
@@ -223,7 +248,7 @@ function buildPropertyList(entity) {
|
|
|
223
248
|
prologValue = value ? "true" : "false";
|
|
224
249
|
}
|
|
225
250
|
else {
|
|
226
|
-
prologValue =
|
|
251
|
+
prologValue = `${toPrologString(String(value))}`;
|
|
227
252
|
}
|
|
228
253
|
pairs.push(`${key}=${prologValue}`);
|
|
229
254
|
}
|
|
@@ -240,21 +265,122 @@ function buildRelationshipMetadata(rel) {
|
|
|
240
265
|
continue;
|
|
241
266
|
let prologValue;
|
|
242
267
|
if (typeof value === "string") {
|
|
243
|
-
prologValue =
|
|
268
|
+
prologValue = `${toPrologString(value)}`;
|
|
244
269
|
}
|
|
245
270
|
else if (typeof value === "number") {
|
|
246
271
|
prologValue = String(value);
|
|
247
272
|
}
|
|
248
273
|
else {
|
|
249
|
-
prologValue =
|
|
274
|
+
prologValue = `${toPrologString(String(value))}`;
|
|
250
275
|
}
|
|
251
276
|
pairs.push(`${key}=${prologValue}`);
|
|
252
277
|
}
|
|
253
278
|
return `[${pairs.join(", ")}]`;
|
|
254
279
|
}
|
|
255
280
|
/**
|
|
256
|
-
*
|
|
281
|
+
* Ensure all relationship rows belong to the entity being upserted.
|
|
282
|
+
* Rejects foreign-source relationship writes in the same request.
|
|
283
|
+
* implements REQ-002, REQ-011
|
|
284
|
+
*/
|
|
285
|
+
function validateRelationshipSources(entityId, relationships) {
|
|
286
|
+
for (const rel of relationships) {
|
|
287
|
+
if (rel.from !== entityId) {
|
|
288
|
+
throw new Error(`Relationship source must match the upserted entity ${entityId}; received from=${String(rel.from)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Validate strict-lane fact_kind pairing for constrains/requires_property relationships.
|
|
294
|
+
* constrains targets must be subject, observation, or meta facts (or legacy without fact_kind).
|
|
295
|
+
* requires_property targets must be property_value, observation, or meta facts (or legacy without fact_kind).
|
|
296
|
+
* implements REQ-011
|
|
257
297
|
*/
|
|
258
|
-
function
|
|
259
|
-
|
|
298
|
+
async function validateStrictLanePairing(prolog, relationships) {
|
|
299
|
+
// implements REQ-011
|
|
300
|
+
for (const rel of relationships) {
|
|
301
|
+
if (rel.type === "constrains") {
|
|
302
|
+
const targetId = rel.to;
|
|
303
|
+
// Reject if target is a fact with fact_kind=property_value.
|
|
304
|
+
// Allow: legacy (no fact_kind), subject, observation, meta, or non-existent
|
|
305
|
+
// (non-existent targets are caught by relationship type validation elsewhere).
|
|
306
|
+
const rejectResult = await prolog.query(`once((kb_entity('${escapeAtom(targetId)}', fact, _SlpProps), memberchk(fact_kind=_SlpFK, _SlpProps), normalize_term_atom(_SlpFK, property_value)))`);
|
|
307
|
+
if (rejectResult.success) {
|
|
308
|
+
throw new Error(`Relationship 'constrains' requires target '${targetId}' to be a subject, observation, or meta fact. Property_value facts cannot be direct targets of constrains relationships.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (rel.type === "requires_property") {
|
|
312
|
+
const targetId = rel.to;
|
|
313
|
+
// Reject if target is a fact with fact_kind=subject.
|
|
314
|
+
// Allow: legacy (no fact_kind), property_value, observation, meta, or non-existent.
|
|
315
|
+
const rejectResult = await prolog.query(`once((kb_entity('${escapeAtom(targetId)}', fact, _SlpProps), memberchk(fact_kind=_SlpFK, _SlpProps), normalize_term_atom(_SlpFK, subject)))`);
|
|
316
|
+
if (rejectResult.success) {
|
|
317
|
+
throw new Error(`Relationship 'requires_property' requires target '${targetId}' to be a property_value, observation, or meta fact. Subject facts cannot be direct targets of requires_property relationships.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Record audit entry for a successfully committed entity mutation.
|
|
324
|
+
* Called only after the RDF transaction succeeds.
|
|
325
|
+
* implements REQ-011
|
|
326
|
+
*/
|
|
327
|
+
async function recordEntityAudit(prolog, type, entity) {
|
|
328
|
+
const props = buildPropertyList(entity);
|
|
329
|
+
const result = await prolog.query(`kb_log_entity_upsert(${type}, ${props})`);
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
throw new Error(`Failed to record audit entry for ${String(entity.id)}: ${result.error || "Unknown error"}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Record audit entry for a successfully committed relationship mutation.
|
|
336
|
+
* Called only after the RDF transaction succeeds.
|
|
337
|
+
* implements REQ-011
|
|
338
|
+
*/
|
|
339
|
+
async function recordRelationshipAudit(prolog, rel) {
|
|
340
|
+
const relType = rel.type;
|
|
341
|
+
const from = rel.from;
|
|
342
|
+
const to = rel.to;
|
|
343
|
+
const metadata = buildRelationshipMetadata(rel);
|
|
344
|
+
const result = await prolog.query(`kb_log_relationship_upsert(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
|
|
345
|
+
if (!result.success) {
|
|
346
|
+
throw new Error(`Failed to record relationship audit entry ${from}->${to}: ${result.error || "Unknown error"}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Format upsert error message for user-facing display.
|
|
351
|
+
* Removes raw transaction goals and extracts meaningful contradiction details.
|
|
352
|
+
* implements REQ-011
|
|
353
|
+
*/
|
|
354
|
+
function formatUpsertError(entityId, rawError) {
|
|
355
|
+
if (!rawError) {
|
|
356
|
+
return `Failed to upsert entity ${entityId}: Unknown error`;
|
|
357
|
+
}
|
|
358
|
+
// Check for contradiction error - Prolog returns kb_contradiction([...]) term
|
|
359
|
+
// Try to extract readable details from the term
|
|
360
|
+
const contradictionMatch = rawError.match(/kb_contradiction\(\s*\[([^\]]+)\]\s*\)/);
|
|
361
|
+
if (contradictionMatch) {
|
|
362
|
+
// Extract individual conflict details from the list
|
|
363
|
+
const details = contradictionMatch[1];
|
|
364
|
+
// Parse out readable parts - each entry is like 'Reason'-'ReqId'
|
|
365
|
+
const conflicts = [];
|
|
366
|
+
const conflictRegex = /'([^']+)'-'([^']+)'/g;
|
|
367
|
+
let execResult = conflictRegex.exec(details);
|
|
368
|
+
while (execResult !== null) {
|
|
369
|
+
const reason = execResult[1];
|
|
370
|
+
const otherReq = execResult[2];
|
|
371
|
+
conflicts.push(` - Conflicts with ${otherReq}: ${reason}`);
|
|
372
|
+
execResult = conflictRegex.exec(details);
|
|
373
|
+
}
|
|
374
|
+
if (conflicts.length > 0) {
|
|
375
|
+
const uniqueConflicts = [...new Set(conflicts)];
|
|
376
|
+
return `Contradiction detected for requirement ${entityId}:\n${uniqueConflicts.join("\n")}\n\nTo resolve:\n 1. Add a supersedes relationship from the new requirement to the conflicting one, OR\n 2. Deprecate the conflicting requirement before creating the new one.`;
|
|
377
|
+
}
|
|
378
|
+
return `Contradiction detected for entity ${entityId}: This requirement conflicts with existing requirements. Add a supersedes relationship to the conflicting requirement, or deprecate the old requirement before creating the new one.`;
|
|
379
|
+
}
|
|
380
|
+
// Check for RDF transaction error
|
|
381
|
+
if (rawError.includes("rdf_transaction")) {
|
|
382
|
+
return `Failed to upsert entity ${entityId}: Transaction failed`;
|
|
383
|
+
}
|
|
384
|
+
// Default: return cleaned error without raw goal
|
|
385
|
+
return `Failed to upsert entity ${entityId}: ${rawError}`;
|
|
260
386
|
}
|
package/dist/tools-config.js
CHANGED
|
@@ -232,7 +232,7 @@ const BASE_TOOLS = [
|
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
name: "kb_upsert",
|
|
235
|
-
description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
|
|
235
|
+
description: "Create or update one entity and optional relationships. Use for KB mutations after validating intent. Use the `relationships` array for batch creation of multiple links in a single call (e.g., linking a requirement to multiple tests or facts). Prefer modeling requirements as reusable fact links (`constrains`, `requires_property`) so consistency and contradiction checks remain queryable. Relationship endpoints must already exist in KB. For requirements, the write will be rejected if it contradicts existing current requirements that constrain the same subject with incompatible properties. To replace a conflicting requirement, include a `supersedes` relationship from the new requirement to the old one in the same request. Do not use for read-only inspection. Side effects: writes KB, may refresh symbol coordinates.",
|
|
236
236
|
inputSchema: {
|
|
237
237
|
type: "object",
|
|
238
238
|
required: ["type", "id", "properties"],
|
|
@@ -375,9 +375,10 @@ const BASE_TOOLS = [
|
|
|
375
375
|
"required-fields",
|
|
376
376
|
"deprecated-adr-no-successor",
|
|
377
377
|
"domain-contradictions",
|
|
378
|
+
"strict-fact-shape",
|
|
378
379
|
],
|
|
379
380
|
},
|
|
380
|
-
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. If omitted, server runs all.",
|
|
381
|
+
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. If omitted, server runs all.",
|
|
381
382
|
},
|
|
382
383
|
},
|
|
383
384
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
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.3
|
|
13
|
-
"kibi-core": "^0.
|
|
12
|
+
"kibi-cli": "^0.4.3",
|
|
13
|
+
"kibi-core": "^0.3.0",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|