kibi-mcp 0.3.0 → 0.3.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/server/session.js +6 -1
- package/dist/tools/check.js +124 -388
- package/dist/tools/delete.js +4 -1
- package/dist/tools/prolog-list.js +2 -11
- package/dist/tools/upsert.js +38 -30
- package/dist/tools-config.js +2 -12
- package/dist/workspace.js +3 -15
- package/package.json +3 -3
- package/dist/tools/branch.js +0 -181
- package/dist/tools/context.js +0 -64
- package/dist/tools/coverage-report.js +0 -74
- package/dist/tools/derive.js +0 -292
- package/dist/tools/impact.js +0 -51
- package/dist/tools/list-types.js +0 -58
- package/dist/tools/query-relationships.js +0 -159
- package/dist/tools/suggest-shared-facts.js +0 -121
package/dist/tools/upsert.js
CHANGED
|
@@ -27,6 +27,7 @@ const validateRelationship = ajv.compile(relationshipSchema);
|
|
|
27
27
|
* Handle kb.upsert tool calls
|
|
28
28
|
* Accepts { type, id, properties } — the flat format matching the tool schema.
|
|
29
29
|
* Validates the assembled entity against JSON Schema before Prolog writes.
|
|
30
|
+
* implements REQ-002, REQ-011
|
|
30
31
|
*/
|
|
31
32
|
export async function handleKbUpsert(prolog, args) {
|
|
32
33
|
const { type, id, properties, relationships = [] } = args;
|
|
@@ -80,49 +81,56 @@ export async function handleKbUpsert(prolog, args) {
|
|
|
80
81
|
for (const entity of entities) {
|
|
81
82
|
const id = entity.id;
|
|
82
83
|
const type = entity.type;
|
|
83
|
-
// Check if entity exists
|
|
84
|
+
// Check if entity exists before transaction (to determine created vs updated)
|
|
84
85
|
const checkGoal = `once(kb_entity('${escapeAtom(id)}', _, _))`;
|
|
85
86
|
const checkResult = await prolog.query(checkGoal);
|
|
86
87
|
const isUpdate = checkResult.success;
|
|
87
88
|
// Build property list for Prolog
|
|
88
89
|
const props = buildPropertyList(entity);
|
|
89
|
-
//
|
|
90
|
+
// Build relationship goals
|
|
91
|
+
const relationshipGoals = [];
|
|
92
|
+
for (const rel of relationships) {
|
|
93
|
+
const relType = rel.type;
|
|
94
|
+
const from = rel.from;
|
|
95
|
+
const to = rel.to;
|
|
96
|
+
const metadata = buildRelationshipMetadata(rel);
|
|
97
|
+
relationshipGoals.push(`kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`);
|
|
98
|
+
}
|
|
99
|
+
// Build atomic transaction goal wrapping entity + all relationships
|
|
100
|
+
// implements REQ-002, REQ-011
|
|
101
|
+
let transactionGoal;
|
|
102
|
+
if (relationshipGoals.length === 0) {
|
|
103
|
+
// Simple case: just entity
|
|
104
|
+
transactionGoal = `rdf_transaction((kb_assert_entity(${type}, ${props})))`;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Entity + relationships in one transaction
|
|
108
|
+
const goals = [
|
|
109
|
+
`kb_assert_entity(${type}, ${props})`,
|
|
110
|
+
...relationshipGoals,
|
|
111
|
+
].join(", ");
|
|
112
|
+
transactionGoal = `rdf_transaction((${goals}))`;
|
|
113
|
+
}
|
|
114
|
+
const txResult = await prolog.query(transactionGoal);
|
|
115
|
+
if (!txResult.success) {
|
|
116
|
+
throw new Error(`Failed to upsert entity ${id}: ${txResult.error || "Unknown error"} (goal: ${transactionGoal})`);
|
|
117
|
+
}
|
|
118
|
+
// Update counters
|
|
90
119
|
if (isUpdate) {
|
|
91
|
-
// Update counter only. kb_assert_entity implements upsert semantics in Prolog.
|
|
92
120
|
updated++;
|
|
93
121
|
}
|
|
94
122
|
else {
|
|
95
123
|
created++;
|
|
96
124
|
}
|
|
97
|
-
|
|
98
|
-
const assertResult = await prolog.query(assertGoal);
|
|
99
|
-
if (!assertResult.success) {
|
|
100
|
-
throw new Error(`Failed to assert entity ${id}: ${assertResult.error || "Unknown error"}`);
|
|
101
|
-
}
|
|
125
|
+
relationshipsCreated += relationships.length;
|
|
102
126
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const relType = rel.type;
|
|
106
|
-
const from = rel.from;
|
|
107
|
-
const to = rel.to;
|
|
108
|
-
// Build metadata
|
|
109
|
-
const metadata = buildRelationshipMetadata(rel);
|
|
110
|
-
const relGoal = `kb_assert_relationship(${relType}, '${escapeAtom(from)}', '${escapeAtom(to)}', ${metadata})`;
|
|
111
|
-
const relResult = await prolog.query(relGoal);
|
|
112
|
-
if (!relResult.success) {
|
|
113
|
-
throw new Error(`Failed to assert relationship ${relType} from ${from} to ${to}: ${relResult.error || "Unknown error"}`);
|
|
114
|
-
}
|
|
115
|
-
relationshipsCreated++;
|
|
116
|
-
}
|
|
117
|
-
// Note: kb_save is intentionally NOT called here for performance.
|
|
118
|
-
// Callers that need durability across restarts should explicitly call kb_save.
|
|
119
|
-
// This allows batching multiple upserts before a single disk write.
|
|
120
|
-
prolog.invalidateCache();
|
|
121
|
-
// Save KB to disk to ensure durability across process restarts
|
|
122
|
-
await prolog.query("kb_save");
|
|
123
|
-
prolog.invalidateCache();
|
|
124
|
-
// multiple upserts and save once at the end for better performance.
|
|
127
|
+
// Save KB to disk after all entities/relationships are written to ensure
|
|
128
|
+
// durability across process restarts.
|
|
125
129
|
prolog.invalidateCache();
|
|
130
|
+
const saveResult = await prolog.query("kb_save");
|
|
131
|
+
if (!saveResult.success) {
|
|
132
|
+
throw new Error(`Failed to save KB after upsert: ${saveResult.error || "Unknown error"}`);
|
|
133
|
+
}
|
|
126
134
|
let contradictionPairsDetected;
|
|
127
135
|
if (type === "req" && !args._skipContradictionCheck) {
|
|
128
136
|
contradictionPairsDetected = await detectContradictionPairs(prolog, id);
|
package/dist/tools-config.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
export const TOOLS = [
|
|
19
|
+
// implements REQ-002
|
|
19
20
|
{
|
|
20
21
|
name: "kb_query",
|
|
21
22
|
description: "Read entities from the KB with filters. Use for discovery and lookup before edits. Do not use for writes. No mutation side effects. Tags filter by metadata tags only, not entity IDs.",
|
|
@@ -97,18 +98,7 @@ export const TOOLS = [
|
|
|
97
98
|
},
|
|
98
99
|
status: {
|
|
99
100
|
type: "string",
|
|
100
|
-
|
|
101
|
-
"active",
|
|
102
|
-
"draft",
|
|
103
|
-
"archived",
|
|
104
|
-
"deleted",
|
|
105
|
-
"approved",
|
|
106
|
-
"rejected",
|
|
107
|
-
"pending",
|
|
108
|
-
"in_progress",
|
|
109
|
-
"superseded",
|
|
110
|
-
],
|
|
111
|
-
description: "Required lifecycle state. Allowed values are fixed enum options. Example: 'active'.",
|
|
101
|
+
description: "Required lifecycle state. Allowed values depend on entity type; backward-compatible legacy statuses are also accepted. Examples: 'open', 'passing', 'accepted', 'active'.",
|
|
112
102
|
},
|
|
113
103
|
source: {
|
|
114
104
|
type: "string",
|
package/dist/workspace.js
CHANGED
|
@@ -23,6 +23,7 @@ const WORKSPACE_ENV_KEYS = [
|
|
|
23
23
|
"KIBI_ROOT",
|
|
24
24
|
];
|
|
25
25
|
const KB_PATH_ENV_KEYS = ["KIBI_KB_PATH", "KB_PATH"];
|
|
26
|
+
// implements REQ-002, REQ-012
|
|
26
27
|
export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
27
28
|
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
28
29
|
if (envRoot) {
|
|
@@ -38,21 +39,7 @@ export function resolveWorkspaceRoot(startDir = process.cwd()) {
|
|
|
38
39
|
}
|
|
39
40
|
return path.resolve(startDir);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
const envRoot = readFirstEnv(WORKSPACE_ENV_KEYS);
|
|
43
|
-
if (envRoot) {
|
|
44
|
-
return { root: path.resolve(envRoot), reason: "env" };
|
|
45
|
-
}
|
|
46
|
-
const kbRoot = findUpwards(startDir, ".kb");
|
|
47
|
-
if (kbRoot) {
|
|
48
|
-
return { root: kbRoot, reason: "kb" };
|
|
49
|
-
}
|
|
50
|
-
const gitRoot = findUpwards(startDir, ".git");
|
|
51
|
-
if (gitRoot) {
|
|
52
|
-
return { root: gitRoot, reason: "git" };
|
|
53
|
-
}
|
|
54
|
-
return { root: path.resolve(startDir), reason: "cwd" };
|
|
55
|
-
}
|
|
42
|
+
// implements REQ-002, REQ-012
|
|
56
43
|
export function resolveKbPath(workspaceRoot, branch) {
|
|
57
44
|
const envPath = readFirstEnv(KB_PATH_ENV_KEYS);
|
|
58
45
|
if (envPath) {
|
|
@@ -64,6 +51,7 @@ export function resolveKbPath(workspaceRoot, branch) {
|
|
|
64
51
|
}
|
|
65
52
|
return path.join(workspaceRoot, ".kb", "branches", branch);
|
|
66
53
|
}
|
|
54
|
+
// implements REQ-002
|
|
67
55
|
export function resolveEnvFilePath(envFileName, workspaceRoot) {
|
|
68
56
|
if (path.isAbsolute(envFileName)) {
|
|
69
57
|
return envFileName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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.2.
|
|
13
|
-
"kibi-core": "^0.1.
|
|
12
|
+
"kibi-cli": "^0.2.7",
|
|
13
|
+
"kibi-core": "^0.1.10",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|
package/dist/tools/branch.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
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 { execSync } from "node:child_process";
|
|
19
|
-
import * as fs from "node:fs";
|
|
20
|
-
import * as path from "node:path";
|
|
21
|
-
import { resolveKbPath, resolveWorkspaceRoot } from "../workspace.js";
|
|
22
|
-
/**
|
|
23
|
-
* Handle kb_branch_ensure tool calls - create branch KB if not exists
|
|
24
|
-
*/
|
|
25
|
-
export async function handleKbBranchEnsure(_prolog, args) {
|
|
26
|
-
const { branch } = args;
|
|
27
|
-
if (!branch || branch.trim() === "") {
|
|
28
|
-
throw new Error("Branch name is required");
|
|
29
|
-
}
|
|
30
|
-
// Sanitize branch name (prevent path traversal)
|
|
31
|
-
const isSafe = (name) => {
|
|
32
|
-
// No empty or excessively long names
|
|
33
|
-
if (!name || name.length > 255)
|
|
34
|
-
return false;
|
|
35
|
-
// No path traversal or absolute paths
|
|
36
|
-
if (name.includes("..") || path.isAbsolute(name) || name.startsWith("/")) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
// Whitelist characters (alphanumeric, dot, underscore, hyphen, forward slash)
|
|
40
|
-
if (!/^[a-zA-Z0-9._\-/]+$/.test(name))
|
|
41
|
-
return false;
|
|
42
|
-
// No redundant slashes or trailing slash/dot
|
|
43
|
-
if (name.includes("//") ||
|
|
44
|
-
name.endsWith("/") ||
|
|
45
|
-
name.endsWith(".") ||
|
|
46
|
-
name.includes("\\")) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
};
|
|
51
|
-
if (!isSafe(branch)) {
|
|
52
|
-
throw new Error(`Invalid branch name: ${branch}`);
|
|
53
|
-
}
|
|
54
|
-
const safeBranch = branch;
|
|
55
|
-
try {
|
|
56
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
57
|
-
const branchPath = resolveKbPath(workspaceRoot, safeBranch);
|
|
58
|
-
const developPath = resolveKbPath(workspaceRoot, "develop");
|
|
59
|
-
// Check if branch KB already exists
|
|
60
|
-
if (fs.existsSync(branchPath)) {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
{
|
|
64
|
-
type: "text",
|
|
65
|
-
text: `Branch KB '${safeBranch}' already exists`,
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
structuredContent: {
|
|
69
|
-
created: false,
|
|
70
|
-
path: branchPath,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
// Ensure develop branch exists
|
|
75
|
-
if (!fs.existsSync(developPath)) {
|
|
76
|
-
throw new Error("Develop branch KB does not exist. Run 'kb init' first.");
|
|
77
|
-
}
|
|
78
|
-
// Copy develop branch KB to new branch
|
|
79
|
-
fs.cpSync(developPath, branchPath, { recursive: true });
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: "text",
|
|
84
|
-
text: `Created branch KB '${safeBranch}' from develop`,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
structuredContent: {
|
|
88
|
-
created: true,
|
|
89
|
-
path: branchPath,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
-
throw new Error(`Branch ensure failed: ${message}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Handle kb_branch_gc tool calls - garbage collect stale branch KBs
|
|
100
|
-
*/
|
|
101
|
-
export async function handleKbBranchGc(_prolog, args) {
|
|
102
|
-
const { dry_run = true } = args;
|
|
103
|
-
try {
|
|
104
|
-
const workspaceRoot = resolveWorkspaceRoot();
|
|
105
|
-
const kbRoot = path.dirname(resolveKbPath(workspaceRoot, "develop"));
|
|
106
|
-
// Check if .kb/branches exists
|
|
107
|
-
if (!fs.existsSync(kbRoot)) {
|
|
108
|
-
return {
|
|
109
|
-
content: [
|
|
110
|
-
{
|
|
111
|
-
type: "text",
|
|
112
|
-
text: "No branch KBs found (.kb/branches does not exist)",
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
structuredContent: {
|
|
116
|
-
stale: [],
|
|
117
|
-
deleted: 0,
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
let gitBranches;
|
|
122
|
-
try {
|
|
123
|
-
execSync("git rev-parse --git-dir", {
|
|
124
|
-
encoding: "utf-8",
|
|
125
|
-
cwd: workspaceRoot,
|
|
126
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
127
|
-
env: process.env,
|
|
128
|
-
});
|
|
129
|
-
const output = execSync("git branch --format='%(refname:short)'", {
|
|
130
|
-
encoding: "utf-8",
|
|
131
|
-
cwd: workspaceRoot,
|
|
132
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
-
env: process.env,
|
|
134
|
-
});
|
|
135
|
-
gitBranches = new Set(output
|
|
136
|
-
.trim()
|
|
137
|
-
.split("\n")
|
|
138
|
-
.map((b) => b.trim().replace(/^'|'$/g, ""))
|
|
139
|
-
.filter((b) => b));
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
-
throw new Error(`Not in a git repository or git command failed: ${message}`);
|
|
144
|
-
}
|
|
145
|
-
// Get all KB branches
|
|
146
|
-
const kbBranches = fs
|
|
147
|
-
.readdirSync(kbRoot, { withFileTypes: true })
|
|
148
|
-
.filter((dirent) => dirent.isDirectory())
|
|
149
|
-
.map((dirent) => dirent.name);
|
|
150
|
-
// Find stale branches (KB exists but git branch doesn't, excluding develop)
|
|
151
|
-
const staleBranches = kbBranches.filter((kb) => kb !== "develop" && !gitBranches.has(kb));
|
|
152
|
-
// Delete stale branches if not dry run
|
|
153
|
-
let deletedCount = 0;
|
|
154
|
-
if (!dry_run && staleBranches.length > 0) {
|
|
155
|
-
for (const branch of staleBranches) {
|
|
156
|
-
const branchPath = path.join(kbRoot, branch);
|
|
157
|
-
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
158
|
-
deletedCount++;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const summary = dry_run
|
|
162
|
-
? `Found ${staleBranches.length} stale branch KB(s) (dry run - not deleted)`
|
|
163
|
-
: `Deleted ${deletedCount} stale branch KB(s)`;
|
|
164
|
-
return {
|
|
165
|
-
content: [
|
|
166
|
-
{
|
|
167
|
-
type: "text",
|
|
168
|
-
text: summary,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
structuredContent: {
|
|
172
|
-
stale: staleBranches,
|
|
173
|
-
deleted: deletedCount,
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
179
|
-
throw new Error(`Branch GC failed: ${message}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
package/dist/tools/context.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { parseEntityFromList, parseListOfLists, } from "kibi-cli/prolog/codec";
|
|
2
|
-
export async function handleKbContext(prolog, args) {
|
|
3
|
-
const { sourceFile } = args;
|
|
4
|
-
try {
|
|
5
|
-
const safeSource = sourceFile.replace(/'/g, "\\'");
|
|
6
|
-
const entityGoal = `findall([Id,Type,Props], (kb_entities_by_source('${safeSource}', SourceIds), member(Id, SourceIds), kb_entity(Id, Type, Props)), Results)`;
|
|
7
|
-
const entityQueryResult = await prolog.query(entityGoal);
|
|
8
|
-
const entities = [];
|
|
9
|
-
const entityIds = [];
|
|
10
|
-
if (entityQueryResult.success && entityQueryResult.bindings.Results) {
|
|
11
|
-
const entitiesData = parseListOfLists(entityQueryResult.bindings.Results);
|
|
12
|
-
for (const data of entitiesData) {
|
|
13
|
-
const entity = parseEntityFromList(data);
|
|
14
|
-
entities.push({
|
|
15
|
-
id: entity.id,
|
|
16
|
-
type: entity.type,
|
|
17
|
-
title: entity.title,
|
|
18
|
-
status: entity.status,
|
|
19
|
-
tags: entity.tags || [],
|
|
20
|
-
});
|
|
21
|
-
entityIds.push(entity.id);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const relationships = [];
|
|
25
|
-
for (const entityId of entityIds) {
|
|
26
|
-
const relGoal = `findall([RelType,FromId,ToId], (kb_relationship(RelType, FromId, ToId), (FromId = '${entityId}' ; ToId = '${entityId}')), RelResults)`;
|
|
27
|
-
const relQueryResult = await prolog.query(relGoal);
|
|
28
|
-
if (relQueryResult.success && relQueryResult.bindings.RelResults) {
|
|
29
|
-
const relData = parseListOfLists(relQueryResult.bindings.RelResults);
|
|
30
|
-
for (const rel of relData) {
|
|
31
|
-
relationships.push({
|
|
32
|
-
relType: rel[0],
|
|
33
|
-
fromId: rel[1],
|
|
34
|
-
toId: rel[2],
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const text = entities.length > 0
|
|
40
|
-
? `Found ${entities.length} KB entities linked to source file "${sourceFile}": ${entities.map((e) => e.id).join(", ")}`
|
|
41
|
-
: `No KB entities found for source file "${sourceFile}"`;
|
|
42
|
-
return {
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: "text",
|
|
46
|
-
text,
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
structuredContent: {
|
|
50
|
-
sourceFile,
|
|
51
|
-
entities,
|
|
52
|
-
relationships,
|
|
53
|
-
provenance: {
|
|
54
|
-
predicate: "kb_entities_by_source",
|
|
55
|
-
deterministic: true,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
-
throw new Error(`Context query failed: ${message}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { parseAtomList, parsePairList } from "./prolog-list.js";
|
|
2
|
-
export async function handleKbCoverageReport(prolog, args) {
|
|
3
|
-
const requested = args.type ?? "all";
|
|
4
|
-
if (args.type && args.type !== "req" && args.type !== "symbol") {
|
|
5
|
-
throw new Error("'type' must be one of: req, symbol");
|
|
6
|
-
}
|
|
7
|
-
const coverage = {};
|
|
8
|
-
const predicates = [];
|
|
9
|
-
if (requested === "all" || requested === "req") {
|
|
10
|
-
const reqIds = await queryAtoms(prolog, "setof(Req, kb_entity(Req, req, _), Reqs)", "Reqs");
|
|
11
|
-
const gapPairs = await queryPairs(prolog, "setof([Req,Reason], coverage_gap(Req, Reason), Rows)", "Rows");
|
|
12
|
-
const gaps = gapPairs.map(([req, reason]) => ({ req, reason }));
|
|
13
|
-
coverage.requirements = {
|
|
14
|
-
total: reqIds.length,
|
|
15
|
-
with_gaps: gaps.length,
|
|
16
|
-
healthy: Math.max(reqIds.length - gaps.length, 0),
|
|
17
|
-
gaps,
|
|
18
|
-
};
|
|
19
|
-
predicates.push("coverage_gap");
|
|
20
|
-
}
|
|
21
|
-
if (requested === "all" || requested === "symbol") {
|
|
22
|
-
const symbolIds = await queryAtoms(prolog, "setof(Symbol, kb_entity(Symbol, symbol, _), Symbols)", "Symbols");
|
|
23
|
-
const untestedResult = await prolog.query("untested_symbols(Symbols)");
|
|
24
|
-
const untestedSymbols = untestedResult.success && untestedResult.bindings.Symbols
|
|
25
|
-
? parseAtomList(untestedResult.bindings.Symbols)
|
|
26
|
-
: [];
|
|
27
|
-
coverage.symbols = {
|
|
28
|
-
total: symbolIds.length,
|
|
29
|
-
untested: untestedSymbols.length,
|
|
30
|
-
tested: Math.max(symbolIds.length - untestedSymbols.length, 0),
|
|
31
|
-
untested_symbols: untestedSymbols,
|
|
32
|
-
};
|
|
33
|
-
predicates.push("untested_symbols");
|
|
34
|
-
}
|
|
35
|
-
const summaryParts = [];
|
|
36
|
-
if (coverage.requirements) {
|
|
37
|
-
summaryParts.push(`${coverage.requirements.healthy}/${coverage.requirements.total} requirements healthy`);
|
|
38
|
-
}
|
|
39
|
-
if (coverage.symbols) {
|
|
40
|
-
summaryParts.push(`${coverage.symbols.tested}/${coverage.symbols.total} symbols tested`);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
content: [
|
|
44
|
-
{
|
|
45
|
-
type: "text",
|
|
46
|
-
text: summaryParts.length > 0
|
|
47
|
-
? `Coverage report: ${summaryParts.join("; ")}.`
|
|
48
|
-
: "Coverage report: no data.",
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
structuredContent: {
|
|
52
|
-
requested_type: requested,
|
|
53
|
-
coverage,
|
|
54
|
-
provenance: {
|
|
55
|
-
deterministic: true,
|
|
56
|
-
predicates,
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
async function queryAtoms(prolog, goal, bindingName) {
|
|
62
|
-
const result = await prolog.query(goal);
|
|
63
|
-
if (!result.success || !result.bindings[bindingName]) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
return parseAtomList(result.bindings[bindingName]);
|
|
67
|
-
}
|
|
68
|
-
async function queryPairs(prolog, goal, bindingName) {
|
|
69
|
-
const result = await prolog.query(goal);
|
|
70
|
-
if (!result.success || !result.bindings[bindingName]) {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
return parsePairList(result.bindings[bindingName]);
|
|
74
|
-
}
|