kibi-mcp 0.3.0 → 0.3.1
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/tools/prolog-list.js +2 -11
- 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
|
@@ -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
|
+
// implements REQ-002
|
|
18
19
|
export function parseAtomList(raw) {
|
|
19
20
|
const trimmed = raw.trim();
|
|
20
21
|
if (trimmed === "[]" || trimmed.length === 0) {
|
|
@@ -28,6 +29,7 @@ export function parseAtomList(raw) {
|
|
|
28
29
|
.map((token) => stripQuotes(token.trim()))
|
|
29
30
|
.filter((token) => token.length > 0);
|
|
30
31
|
}
|
|
32
|
+
// implements REQ-002
|
|
31
33
|
export function parsePairList(raw) {
|
|
32
34
|
const rows = parseListRows(raw);
|
|
33
35
|
const pairs = [];
|
|
@@ -39,17 +41,6 @@ export function parsePairList(raw) {
|
|
|
39
41
|
}
|
|
40
42
|
return pairs;
|
|
41
43
|
}
|
|
42
|
-
export function parseTriples(raw) {
|
|
43
|
-
const rows = parseListRows(raw);
|
|
44
|
-
const triples = [];
|
|
45
|
-
for (const row of rows) {
|
|
46
|
-
const parts = splitTopLevel(row, ",").map((part) => stripQuotes(part.trim()));
|
|
47
|
-
if (parts.length >= 3) {
|
|
48
|
-
triples.push([parts[0], parts[1], parts[2]]);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return triples;
|
|
52
|
-
}
|
|
53
44
|
function parseListRows(raw) {
|
|
54
45
|
const trimmed = raw.trim();
|
|
55
46
|
if (trimmed === "[]" || trimmed.length === 0) {
|
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.1",
|
|
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.6",
|
|
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
|
-
}
|
package/dist/tools/derive.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { escapeAtom } from "kibi-cli/prolog/codec";
|
|
2
|
-
import { parseAtomList, parsePairList } from "./prolog-list.js";
|
|
3
|
-
const RULES = [
|
|
4
|
-
"transitively_implements",
|
|
5
|
-
"transitively_depends",
|
|
6
|
-
"impacted_by_change",
|
|
7
|
-
"affected_symbols",
|
|
8
|
-
"coverage_gap",
|
|
9
|
-
"untested_symbols",
|
|
10
|
-
"stale",
|
|
11
|
-
"orphaned",
|
|
12
|
-
"conflicting",
|
|
13
|
-
"deprecated_still_used",
|
|
14
|
-
"current_adr",
|
|
15
|
-
"adr_chain",
|
|
16
|
-
"superseded_by",
|
|
17
|
-
"domain_contradictions",
|
|
18
|
-
];
|
|
19
|
-
export async function handleKbDerive(prolog, args) {
|
|
20
|
-
const params = args.params ?? {};
|
|
21
|
-
const { rule } = args;
|
|
22
|
-
if (!RULES.includes(rule)) {
|
|
23
|
-
throw new Error(`Unsupported rule '${rule}'`);
|
|
24
|
-
}
|
|
25
|
-
let rows = [];
|
|
26
|
-
switch (rule) {
|
|
27
|
-
case "transitively_implements":
|
|
28
|
-
rows = await deriveTransitivelyImplements(prolog, params);
|
|
29
|
-
break;
|
|
30
|
-
case "transitively_depends":
|
|
31
|
-
rows = await deriveTransitivelyDepends(prolog, params);
|
|
32
|
-
break;
|
|
33
|
-
case "impacted_by_change":
|
|
34
|
-
rows = await deriveImpactedByChange(prolog, params);
|
|
35
|
-
break;
|
|
36
|
-
case "affected_symbols":
|
|
37
|
-
rows = await deriveAffectedSymbols(prolog, params);
|
|
38
|
-
break;
|
|
39
|
-
case "coverage_gap":
|
|
40
|
-
rows = await deriveCoverageGap(prolog, params);
|
|
41
|
-
break;
|
|
42
|
-
case "untested_symbols":
|
|
43
|
-
rows = await deriveUntestedSymbols(prolog);
|
|
44
|
-
break;
|
|
45
|
-
case "stale":
|
|
46
|
-
rows = await deriveStale(prolog, params);
|
|
47
|
-
break;
|
|
48
|
-
case "orphaned":
|
|
49
|
-
rows = await deriveOrphaned(prolog, params);
|
|
50
|
-
break;
|
|
51
|
-
case "conflicting":
|
|
52
|
-
rows = await deriveConflicting(prolog, params);
|
|
53
|
-
break;
|
|
54
|
-
case "deprecated_still_used":
|
|
55
|
-
rows = await deriveDeprecatedStillUsed(prolog, params);
|
|
56
|
-
break;
|
|
57
|
-
case "current_adr":
|
|
58
|
-
rows = await deriveCurrentAdr(prolog);
|
|
59
|
-
break;
|
|
60
|
-
case "adr_chain":
|
|
61
|
-
rows = await deriveAdrChain(prolog, params);
|
|
62
|
-
break;
|
|
63
|
-
case "superseded_by":
|
|
64
|
-
rows = await deriveSupersededBy(prolog, params);
|
|
65
|
-
break;
|
|
66
|
-
case "domain_contradictions":
|
|
67
|
-
rows = await deriveDomainContradictions(prolog);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
content: [
|
|
72
|
-
{
|
|
73
|
-
type: "text",
|
|
74
|
-
text: `Derived ${rows.length} row(s) for rule '${rule}'.`,
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
structuredContent: {
|
|
78
|
-
rule,
|
|
79
|
-
params,
|
|
80
|
-
count: rows.length,
|
|
81
|
-
rows,
|
|
82
|
-
provenance: {
|
|
83
|
-
predicate: rule,
|
|
84
|
-
deterministic: true,
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
async function deriveTransitivelyImplements(prolog, params) {
|
|
90
|
-
const symbolFilter = asOptionalString(params.symbol);
|
|
91
|
-
const reqFilter = asOptionalString(params.req);
|
|
92
|
-
const cond = makeConjunction([
|
|
93
|
-
symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "",
|
|
94
|
-
reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "",
|
|
95
|
-
]);
|
|
96
|
-
const goal = `setof([Symbol,Req], (transitively_implements(Symbol, Req)${cond}), Rows)`;
|
|
97
|
-
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
98
|
-
return pairs.map(([symbol, req]) => ({ symbol, req }));
|
|
99
|
-
}
|
|
100
|
-
async function deriveTransitivelyDepends(prolog, params) {
|
|
101
|
-
const req1Filter = asOptionalString(params.req1);
|
|
102
|
-
const req2Filter = asOptionalString(params.req2);
|
|
103
|
-
const cond = makeConjunction([
|
|
104
|
-
req1Filter ? `Req1='${escapeAtom(req1Filter)}'` : "",
|
|
105
|
-
req2Filter ? `Req2='${escapeAtom(req2Filter)}'` : "",
|
|
106
|
-
]);
|
|
107
|
-
const goal = `setof([Req1,Req2], (transitively_depends(Req1, Req2)${cond}), Rows)`;
|
|
108
|
-
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
109
|
-
return pairs.map(([req1, req2]) => ({ req1, req2 }));
|
|
110
|
-
}
|
|
111
|
-
async function deriveImpactedByChange(prolog, params) {
|
|
112
|
-
const changed = asRequiredString(params.changed, "params.changed is required");
|
|
113
|
-
const goal = `setof(Entity, impacted_by_change(Entity, '${escapeAtom(changed)}'), Rows)`;
|
|
114
|
-
const entities = await queryAtomRows(prolog, goal, "Rows");
|
|
115
|
-
return entities.map((entity) => ({ changed, entity }));
|
|
116
|
-
}
|
|
117
|
-
async function deriveAffectedSymbols(prolog, params) {
|
|
118
|
-
const req = asRequiredString(params.req, "params.req is required");
|
|
119
|
-
const goal = `affected_symbols('${escapeAtom(req)}', Symbols)`;
|
|
120
|
-
const result = await prolog.query(goal);
|
|
121
|
-
if (!result.success || !result.bindings.Symbols) {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
const symbols = parseAtomList(result.bindings.Symbols);
|
|
125
|
-
return symbols.map((symbol) => ({ req, symbol }));
|
|
126
|
-
}
|
|
127
|
-
async function deriveCoverageGap(prolog, params) {
|
|
128
|
-
const reqFilter = asOptionalString(params.req);
|
|
129
|
-
const cond = reqFilter ? `Req='${escapeAtom(reqFilter)}'` : "";
|
|
130
|
-
const goal = `setof([Req,Reason], (coverage_gap(Req, Reason)${makeConjunction([cond])}), Rows)`;
|
|
131
|
-
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
132
|
-
return pairs.map(([req, reason]) => ({ req, reason }));
|
|
133
|
-
}
|
|
134
|
-
async function deriveUntestedSymbols(prolog) {
|
|
135
|
-
const result = await prolog.query("untested_symbols(Symbols)");
|
|
136
|
-
if (!result.success || !result.bindings.Symbols) {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
const symbols = parseAtomList(result.bindings.Symbols);
|
|
140
|
-
return symbols.map((symbol) => ({ symbol }));
|
|
141
|
-
}
|
|
142
|
-
async function deriveStale(prolog, params) {
|
|
143
|
-
const maxAgeDays = Number(params.max_age_days ?? params.maxAgeDays);
|
|
144
|
-
if (!Number.isFinite(maxAgeDays)) {
|
|
145
|
-
throw new Error("params.max_age_days is required and must be numeric");
|
|
146
|
-
}
|
|
147
|
-
const entityFilter = asOptionalString(params.entity);
|
|
148
|
-
const cond = entityFilter ? `Entity='${escapeAtom(entityFilter)}'` : "";
|
|
149
|
-
const goal = `setof(Entity, (stale(Entity, ${maxAgeDays})${makeConjunction([cond])}), Rows)`;
|
|
150
|
-
const entities = await queryAtomRows(prolog, goal, "Rows");
|
|
151
|
-
return entities.map((entity) => ({ entity, max_age_days: maxAgeDays }));
|
|
152
|
-
}
|
|
153
|
-
async function deriveOrphaned(prolog, params) {
|
|
154
|
-
const symbolFilter = asOptionalString(params.symbol);
|
|
155
|
-
const cond = symbolFilter ? `Symbol='${escapeAtom(symbolFilter)}'` : "";
|
|
156
|
-
const goal = `setof(Symbol, (orphaned(Symbol)${makeConjunction([cond])}), Rows)`;
|
|
157
|
-
const symbols = await queryAtomRows(prolog, goal, "Rows");
|
|
158
|
-
return symbols.map((symbol) => ({ symbol }));
|
|
159
|
-
}
|
|
160
|
-
async function deriveConflicting(prolog, params) {
|
|
161
|
-
const adr1Filter = asOptionalString(params.adr1);
|
|
162
|
-
const adr2Filter = asOptionalString(params.adr2);
|
|
163
|
-
const cond = makeConjunction([
|
|
164
|
-
adr1Filter ? `Adr1='${escapeAtom(adr1Filter)}'` : "",
|
|
165
|
-
adr2Filter ? `Adr2='${escapeAtom(adr2Filter)}'` : "",
|
|
166
|
-
]);
|
|
167
|
-
const goal = `setof([Adr1,Adr2], (conflicting(Adr1, Adr2)${cond}), Rows)`;
|
|
168
|
-
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
169
|
-
return pairs.map(([adr1, adr2]) => ({ adr1, adr2 }));
|
|
170
|
-
}
|
|
171
|
-
async function deriveDeprecatedStillUsed(prolog, params) {
|
|
172
|
-
const adrFilter = asOptionalString(params.adr);
|
|
173
|
-
const goal = adrFilter
|
|
174
|
-
? `deprecated_still_used('${escapeAtom(adrFilter)}', Symbols)`
|
|
175
|
-
: "setof([Adr,Symbols], deprecated_still_used(Adr, Symbols), Rows)";
|
|
176
|
-
if (adrFilter) {
|
|
177
|
-
const result = await prolog.query(goal);
|
|
178
|
-
if (!result.success || !result.bindings.Symbols) {
|
|
179
|
-
return [];
|
|
180
|
-
}
|
|
181
|
-
return [
|
|
182
|
-
{ adr: adrFilter, symbols: parseAtomList(result.bindings.Symbols) },
|
|
183
|
-
];
|
|
184
|
-
}
|
|
185
|
-
const pairs = await queryPairRows(prolog, goal, "Rows");
|
|
186
|
-
return pairs.map(([adr, symbolsRaw]) => ({
|
|
187
|
-
adr,
|
|
188
|
-
symbols: parseAtomList(symbolsRaw),
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
async function queryAtomRows(prolog, goal, bindingName) {
|
|
192
|
-
const result = await prolog.query(goal);
|
|
193
|
-
if (!result.success || !result.bindings[bindingName]) {
|
|
194
|
-
return [];
|
|
195
|
-
}
|
|
196
|
-
return parseAtomList(result.bindings[bindingName]);
|
|
197
|
-
}
|
|
198
|
-
async function queryPairRows(prolog, goal, bindingName) {
|
|
199
|
-
const result = await prolog.query(goal);
|
|
200
|
-
if (!result.success || !result.bindings[bindingName]) {
|
|
201
|
-
return [];
|
|
202
|
-
}
|
|
203
|
-
return parsePairList(result.bindings[bindingName]);
|
|
204
|
-
}
|
|
205
|
-
function asOptionalString(value) {
|
|
206
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
207
|
-
}
|
|
208
|
-
function asRequiredString(value, message) {
|
|
209
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
210
|
-
throw new Error(message);
|
|
211
|
-
}
|
|
212
|
-
return value;
|
|
213
|
-
}
|
|
214
|
-
function makeConjunction(parts) {
|
|
215
|
-
const filtered = parts.filter((part) => part.length > 0);
|
|
216
|
-
if (filtered.length === 0) {
|
|
217
|
-
return "";
|
|
218
|
-
}
|
|
219
|
-
return `, ${filtered.join(", ")}`;
|
|
220
|
-
}
|
|
221
|
-
async function deriveCurrentAdr(prolog) {
|
|
222
|
-
// Query for all current ADRs and their titles
|
|
223
|
-
const result = await prolog.query("setof([Id,TitleAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), current_adr(Id)), Rows)");
|
|
224
|
-
if (!result.success || !result.bindings.Rows) {
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
const pairs = parsePairList(result.bindings.Rows);
|
|
228
|
-
return pairs.map(([id, title]) => ({ id, title }));
|
|
229
|
-
}
|
|
230
|
-
async function deriveAdrChain(prolog, params) {
|
|
231
|
-
const adr = asRequiredString(params.adr, "params.adr is required");
|
|
232
|
-
// Query for the full chain including status
|
|
233
|
-
const result = await prolog.query(`findall([Id,TitleAtom,StatusAtom], (kb_entity(Id, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom), memberchk(status=Status, Props), normalize_term_atom(Status, StatusAtom), adr_chain('${escapeAtom(adr)}', Chain), member(Id, Chain)), Rows)`);
|
|
234
|
-
if (!result.success || !result.bindings.Rows) {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
// Parse triplets and include status
|
|
238
|
-
const triplets = parseTripleList(result.bindings.Rows);
|
|
239
|
-
return triplets.map(([id, title, status]) => ({ id, title, status }));
|
|
240
|
-
}
|
|
241
|
-
async function deriveSupersededBy(prolog, params) {
|
|
242
|
-
const adr = asRequiredString(params.adr, "params.adr is required");
|
|
243
|
-
// Query for direct supersession
|
|
244
|
-
const result = await prolog.query(`superseded_by('${escapeAtom(adr)}', NewAdr), kb_entity(NewAdr, adr, Props), memberchk(title=Title, Props), normalize_term_atom(Title, TitleAtom)`);
|
|
245
|
-
if (!result.success ||
|
|
246
|
-
!result.bindings.NewAdr ||
|
|
247
|
-
!result.bindings.TitleAtom) {
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
const newAdr = String(result.bindings.NewAdr).replace(/^'|'$/g, "");
|
|
251
|
-
const newAdrTitle = String(result.bindings.TitleAtom).replace(/^'|'$/g, "");
|
|
252
|
-
return [
|
|
253
|
-
{
|
|
254
|
-
adr,
|
|
255
|
-
successor_id: newAdr,
|
|
256
|
-
successor_title: newAdrTitle,
|
|
257
|
-
},
|
|
258
|
-
];
|
|
259
|
-
}
|
|
260
|
-
function parseTripleList(raw) {
|
|
261
|
-
const match = raw.match(/\[(.*)\]/);
|
|
262
|
-
if (!match) {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
const content = match[1].trim();
|
|
266
|
-
if (!content) {
|
|
267
|
-
return [];
|
|
268
|
-
}
|
|
269
|
-
// Parse triplets: [[a,b,c],[x,y,z],...]
|
|
270
|
-
const triplets = [];
|
|
271
|
-
const tripletRegex = /\[([^,]+),([^,]+),([^\]]+)\]/g;
|
|
272
|
-
let tripletMatch;
|
|
273
|
-
do {
|
|
274
|
-
tripletMatch = tripletRegex.exec(content);
|
|
275
|
-
if (tripletMatch !== null) {
|
|
276
|
-
triplets.push([
|
|
277
|
-
tripletMatch[1].trim().replace(/^'|'$/g, ""),
|
|
278
|
-
tripletMatch[2].trim().replace(/^'|'$/g, ""),
|
|
279
|
-
tripletMatch[3].trim().replace(/^'|'$/g, ""),
|
|
280
|
-
]);
|
|
281
|
-
}
|
|
282
|
-
} while (tripletMatch !== null);
|
|
283
|
-
return triplets;
|
|
284
|
-
}
|
|
285
|
-
async function deriveDomainContradictions(prolog) {
|
|
286
|
-
const result = await prolog.query("setof([ReqA,ReqB,Reason], contradicting_reqs(ReqA, ReqB, Reason), Rows)");
|
|
287
|
-
if (!result.success || !result.bindings.Rows) {
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
|
-
const rows = parseTripleList(result.bindings.Rows);
|
|
291
|
-
return rows.map(([reqA, reqB, reason]) => ({ reqA, reqB, reason }));
|
|
292
|
-
}
|
package/dist/tools/impact.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { escapeAtom } from "kibi-cli/prolog/codec";
|
|
2
|
-
import { parseAtomList } from "./prolog-list.js";
|
|
3
|
-
export async function handleKbImpact(prolog, args) {
|
|
4
|
-
if (!args.entity || typeof args.entity !== "string") {
|
|
5
|
-
throw new Error("'entity' is required");
|
|
6
|
-
}
|
|
7
|
-
const goal = `setof(Id, (impacted_by_change(Id, '${escapeAtom(args.entity)}'), Id \\= '${escapeAtom(args.entity)}'), Impacted)`;
|
|
8
|
-
const impactedIds = await queryAtoms(prolog, goal, "Impacted");
|
|
9
|
-
const impacted = [];
|
|
10
|
-
for (const id of impactedIds) {
|
|
11
|
-
const type = await getEntityType(prolog, id);
|
|
12
|
-
impacted.push({ id, type: type ?? "unknown" });
|
|
13
|
-
}
|
|
14
|
-
impacted.sort((a, b) => {
|
|
15
|
-
if (a.type === b.type) {
|
|
16
|
-
return a.id.localeCompare(b.id);
|
|
17
|
-
}
|
|
18
|
-
return a.type.localeCompare(b.type);
|
|
19
|
-
});
|
|
20
|
-
return {
|
|
21
|
-
content: [
|
|
22
|
-
{
|
|
23
|
-
type: "text",
|
|
24
|
-
text: `Impact analysis for '${args.entity}': ${impacted.length} impacted entity(s).`,
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
structuredContent: {
|
|
28
|
-
entity: args.entity,
|
|
29
|
-
impacted,
|
|
30
|
-
count: impacted.length,
|
|
31
|
-
provenance: {
|
|
32
|
-
predicate: "impacted_by_change",
|
|
33
|
-
deterministic: true,
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
async function queryAtoms(prolog, goal, bindingName) {
|
|
39
|
-
const result = await prolog.query(goal);
|
|
40
|
-
if (!result.success || !result.bindings[bindingName]) {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
return parseAtomList(result.bindings[bindingName]);
|
|
44
|
-
}
|
|
45
|
-
async function getEntityType(prolog, id) {
|
|
46
|
-
const result = await prolog.query(`kb_entity('${escapeAtom(id)}', Type, _)`);
|
|
47
|
-
if (!result.success || !result.bindings.Type) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
return result.bindings.Type;
|
|
51
|
-
}
|
package/dist/tools/list-types.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handle kb_list_entity_types tool calls
|
|
3
|
-
* Returns the static list of supported KB entity type names (req, scenario, test, adr, flag, event, symbol, fact).
|
|
4
|
-
*/
|
|
5
|
-
export async function handleKbListEntityTypes() {
|
|
6
|
-
return {
|
|
7
|
-
content: [
|
|
8
|
-
{
|
|
9
|
-
type: "text",
|
|
10
|
-
text: "Available entity types: req, scenario, test, adr, flag, event, symbol, fact",
|
|
11
|
-
},
|
|
12
|
-
],
|
|
13
|
-
structuredContent: {
|
|
14
|
-
types: [
|
|
15
|
-
"req",
|
|
16
|
-
"scenario",
|
|
17
|
-
"test",
|
|
18
|
-
"adr",
|
|
19
|
-
"flag",
|
|
20
|
-
"event",
|
|
21
|
-
"symbol",
|
|
22
|
-
"fact",
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Handle kb_list_relationship_types tool calls
|
|
29
|
-
* Returns the static list of supported KB relationship type names (depends_on, specified_by, verified_by, etc.).
|
|
30
|
-
*/
|
|
31
|
-
export async function handleKbListRelationshipTypes() {
|
|
32
|
-
return {
|
|
33
|
-
content: [
|
|
34
|
-
{
|
|
35
|
-
type: "text",
|
|
36
|
-
text: "Available relationship types: depends_on, specified_by, verified_by, validates, implements, covered_by, constrained_by, constrains, requires_property, guards, publishes, consumes, supersedes, relates_to",
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
structuredContent: {
|
|
40
|
-
types: [
|
|
41
|
-
"depends_on",
|
|
42
|
-
"specified_by",
|
|
43
|
-
"verified_by",
|
|
44
|
-
"validates",
|
|
45
|
-
"implements",
|
|
46
|
-
"covered_by",
|
|
47
|
-
"constrained_by",
|
|
48
|
-
"constrains",
|
|
49
|
-
"requires_property",
|
|
50
|
-
"guards",
|
|
51
|
-
"publishes",
|
|
52
|
-
"consumes",
|
|
53
|
-
"supersedes",
|
|
54
|
-
"relates_to",
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
const VALID_REL_TYPES = [
|
|
2
|
-
"depends_on",
|
|
3
|
-
"specified_by",
|
|
4
|
-
"verified_by",
|
|
5
|
-
"validates",
|
|
6
|
-
"implements",
|
|
7
|
-
"covered_by",
|
|
8
|
-
"constrained_by",
|
|
9
|
-
"constrains",
|
|
10
|
-
"requires_property",
|
|
11
|
-
"guards",
|
|
12
|
-
"publishes",
|
|
13
|
-
"consumes",
|
|
14
|
-
"supersedes",
|
|
15
|
-
"relates_to",
|
|
16
|
-
];
|
|
17
|
-
/**
|
|
18
|
-
* Handle kb_query_relationships tool calls.
|
|
19
|
-
* Queries the kb_relationship/3 predicate which has arity (Type, From, To).
|
|
20
|
-
*
|
|
21
|
-
* Note: kb_relationship/3 requires RelType to be bound (atom_concat/3 in Prolog
|
|
22
|
-
* does not work with an unbound first argument). When no type filter is given,
|
|
23
|
-
* we iterate over all known type values.
|
|
24
|
-
*/
|
|
25
|
-
export async function handleKbQueryRelationships(prolog, args) {
|
|
26
|
-
const { from, to, type } = args;
|
|
27
|
-
if (type && !VALID_REL_TYPES.includes(type)) {
|
|
28
|
-
throw new Error(`Invalid relationship type '${type}'. Valid types: ${VALID_REL_TYPES.join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
// When type is specified we run one query; otherwise iterate all known types
|
|
31
|
-
// (kb_relationship/3 requires the type to be bound due to atom_concat/3 in Prolog).
|
|
32
|
-
const typesToQuery = type ? [type] : VALID_REL_TYPES;
|
|
33
|
-
const allRelationships = [];
|
|
34
|
-
for (const relType of typesToQuery) {
|
|
35
|
-
// We collect what we actually need based on which args are bound.
|
|
36
|
-
// When both from and to are specified, we just need to check existence.
|
|
37
|
-
// Otherwise collect the unbound sides.
|
|
38
|
-
let goal;
|
|
39
|
-
if (from && to) {
|
|
40
|
-
// Check if the specific triple exists
|
|
41
|
-
goal = `(kb_relationship('${relType}', '${from}', '${to}') -> Results = [['${from}','${to}']] ; Results = [])`;
|
|
42
|
-
}
|
|
43
|
-
else if (from) {
|
|
44
|
-
goal = `findall(To, kb_relationship('${relType}', '${from}', To), Results)`;
|
|
45
|
-
}
|
|
46
|
-
else if (to) {
|
|
47
|
-
goal = `findall(From, kb_relationship('${relType}', From, '${to}'), Results)`;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
goal = `findall([From,To], kb_relationship('${relType}', From, To), Results)`;
|
|
51
|
-
}
|
|
52
|
-
const queryResult = await prolog.query(goal);
|
|
53
|
-
if (!queryResult.success) {
|
|
54
|
-
throw new Error(queryResult.error || "Relationship query failed");
|
|
55
|
-
}
|
|
56
|
-
if (queryResult.bindings.Results) {
|
|
57
|
-
const raw = queryResult.bindings.Results;
|
|
58
|
-
if (from && to) {
|
|
59
|
-
// Results is either [[from,to]] or []
|
|
60
|
-
const pairs = parsePairResults(raw);
|
|
61
|
-
for (const [pairFrom, pairTo] of pairs) {
|
|
62
|
-
allRelationships.push({ relType, from: pairFrom, to: pairTo });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else if (from) {
|
|
66
|
-
// Results is [To, To, ...]
|
|
67
|
-
const ids = parseIdList(raw);
|
|
68
|
-
for (const toId of ids) {
|
|
69
|
-
allRelationships.push({ relType, from, to: toId });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else if (to) {
|
|
73
|
-
// Results is [From, From, ...]
|
|
74
|
-
const ids = parseIdList(raw);
|
|
75
|
-
for (const fromId of ids) {
|
|
76
|
-
allRelationships.push({ relType, from: fromId, to });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
// Results is [[From,To], ...]
|
|
81
|
-
const pairs = parsePairResults(raw);
|
|
82
|
-
for (const [pairFrom, pairTo] of pairs) {
|
|
83
|
-
allRelationships.push({ relType, from: pairFrom, to: pairTo });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const text = allRelationships.length === 0
|
|
89
|
-
? "No relationships found."
|
|
90
|
-
: `Found ${allRelationships.length} relationship(s): ${allRelationships
|
|
91
|
-
.map((r) => `${r.from} -[${r.relType}]-> ${r.to}`)
|
|
92
|
-
.join(", ")}`;
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: "text", text }],
|
|
95
|
-
structuredContent: {
|
|
96
|
-
relationships: allRelationships,
|
|
97
|
-
count: allRelationships.length,
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Parse a flat Prolog list of atoms "[A,B,C]" into a string array.
|
|
103
|
-
*/
|
|
104
|
-
function parseIdList(raw) {
|
|
105
|
-
const cleaned = raw.trim();
|
|
106
|
-
if (cleaned === "[]" || cleaned === "")
|
|
107
|
-
return [];
|
|
108
|
-
const inner = cleaned.replace(/^\[/, "").replace(/\]$/, "");
|
|
109
|
-
return inner
|
|
110
|
-
.split(",")
|
|
111
|
-
.map((s) => s.trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""))
|
|
112
|
-
.filter(Boolean);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Parse Prolog findall result "[[From,To],...]" into [from, to] pairs.
|
|
116
|
-
*/
|
|
117
|
-
function parsePairResults(raw) {
|
|
118
|
-
const cleaned = raw.trim();
|
|
119
|
-
if (cleaned === "[]" || cleaned === "")
|
|
120
|
-
return [];
|
|
121
|
-
const inner = cleaned.replace(/^\[/, "").replace(/\]$/, "");
|
|
122
|
-
const pairs = [];
|
|
123
|
-
let depth = 0;
|
|
124
|
-
let current = "";
|
|
125
|
-
for (let i = 0; i < inner.length; i++) {
|
|
126
|
-
const ch = inner[i];
|
|
127
|
-
if (ch === "[") {
|
|
128
|
-
depth++;
|
|
129
|
-
current += ch;
|
|
130
|
-
}
|
|
131
|
-
else if (ch === "]") {
|
|
132
|
-
depth--;
|
|
133
|
-
current += ch;
|
|
134
|
-
if (depth === 0) {
|
|
135
|
-
const pair = parsePair(current.trim());
|
|
136
|
-
if (pair)
|
|
137
|
-
pairs.push(pair);
|
|
138
|
-
current = "";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else if (ch === "," && depth === 0) {
|
|
142
|
-
// top-level separator between pairs — skip
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
current += ch;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return pairs;
|
|
149
|
-
}
|
|
150
|
-
function parsePair(pairStr) {
|
|
151
|
-
// expect "[From,To]"
|
|
152
|
-
const inner = pairStr.replace(/^\[/, "").replace(/\]$/, "").trim();
|
|
153
|
-
const parts = inner
|
|
154
|
-
.split(",")
|
|
155
|
-
.map((s) => s.trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""));
|
|
156
|
-
if (parts.length < 2)
|
|
157
|
-
return null;
|
|
158
|
-
return [parts[0], parts[1]];
|
|
159
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { parseAtomList } from "./prolog-list.js";
|
|
2
|
-
/**
|
|
3
|
-
* Handle analyze_shared_facts tool calls
|
|
4
|
-
* Analyzes requirements to suggest shared domain facts for extraction
|
|
5
|
-
*/
|
|
6
|
-
export async function handleSuggestSharedFacts(prolog, args) {
|
|
7
|
-
const minFreq = args.min_frequency ?? 2;
|
|
8
|
-
try {
|
|
9
|
-
// Query all requirements with their text properties
|
|
10
|
-
const reqsResult = await prolog.query("findall([Id,Title], (kb_entity(Id, req, Props), memberchk(title=Title, Props)), Reqs)");
|
|
11
|
-
if (!reqsResult.success || !reqsResult.bindings.Reqs) {
|
|
12
|
-
return {
|
|
13
|
-
content: [{ type: "text", text: "No requirements found in KB" }],
|
|
14
|
-
structuredContent: { suggestions: [], count: 0 },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
const reqsList = parseAtomList(reqsResult.bindings.Reqs);
|
|
18
|
-
const requirements = [];
|
|
19
|
-
// Parse the list-of-lists format from Prolog
|
|
20
|
-
const reqMatch = reqsList.join("").matchAll(/\[([^,]+),([^\]]+)\]/g);
|
|
21
|
-
if (reqMatch) {
|
|
22
|
-
for (const match of reqMatch) {
|
|
23
|
-
const id = match[1].trim().replace(/^'|'$/g, "");
|
|
24
|
-
const title = match[2].trim().replace(/^'|'$/g, "");
|
|
25
|
-
requirements.push({ id, title, description: title });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Query all existing facts for context
|
|
29
|
-
const factsResult = await prolog.query("findall([Id,Title], (kb_entity(Id, fact, Props), memberchk(title=Title, Props)), Facts)");
|
|
30
|
-
if (!factsResult.success || !factsResult.bindings.Facts) {
|
|
31
|
-
return {
|
|
32
|
-
content: [{ type: "text", text: "No facts found in KB" }],
|
|
33
|
-
structuredContent: { suggestions: [], count: 0 },
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const factsList = parseAtomList(factsResult.bindings.Facts);
|
|
37
|
-
const existingFacts = new Set();
|
|
38
|
-
const factMatch = factsList.join("").matchAll(/\[([^,]+),([^\]]+)\]/g);
|
|
39
|
-
if (factMatch) {
|
|
40
|
-
for (const match of factMatch) {
|
|
41
|
-
const title = match[2].trim().replace(/^'|'$/g, "");
|
|
42
|
-
existingFacts.add(title.toLowerCase());
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// Extract and analyze domain concepts from requirements
|
|
46
|
-
const suggestions = analyzeSharedConcepts(requirements, existingFacts, minFreq);
|
|
47
|
-
return {
|
|
48
|
-
content: [
|
|
49
|
-
{
|
|
50
|
-
type: "text",
|
|
51
|
-
text: `Found ${suggestions.length} potential shared fact(s) to consider creating.`,
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
structuredContent: {
|
|
55
|
-
suggestions,
|
|
56
|
-
count: suggestions.length,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
-
throw new Error(`Shared facts analysis failed: ${message}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Lightweight heuristic to identify shared domain concepts
|
|
67
|
-
* Focuses on:
|
|
68
|
-
* - Capitalized terms (possible domain concepts)
|
|
69
|
-
* - Repeated phrases across multiple requirements
|
|
70
|
-
* - Excludes existing facts
|
|
71
|
-
*/
|
|
72
|
-
function analyzeSharedConcepts(requirements, existingFacts, minFreq) {
|
|
73
|
-
const conceptCounts = new Map();
|
|
74
|
-
for (const req of requirements) {
|
|
75
|
-
const originalText = `${req.title} ${req.description || ""}`;
|
|
76
|
-
const text = originalText.toLowerCase();
|
|
77
|
-
// Extract capitalized terms (potential domain concepts)
|
|
78
|
-
// Pattern: words starting with capital letters that aren't at sentence start
|
|
79
|
-
const capitalizedTerms = originalText.matchAll(/\b([A-Z][a-z]+)\b/g);
|
|
80
|
-
// Extract repeated phrases (2+ words)
|
|
81
|
-
// Extract repeated phrases (2+ words)
|
|
82
|
-
const words = text.split(/\s+/).filter((w) => w.length > 3);
|
|
83
|
-
for (let i = 0; i < words.length - 1; i++) {
|
|
84
|
-
const phrase = `${words[i]} ${words[i + 1]}`;
|
|
85
|
-
if (!conceptCounts.has(phrase)) {
|
|
86
|
-
conceptCounts.set(phrase, new Set());
|
|
87
|
-
}
|
|
88
|
-
conceptCounts.get(phrase).add(req.id);
|
|
89
|
-
}
|
|
90
|
-
// Also track individual capitalized terms
|
|
91
|
-
for (const match of capitalizedTerms) {
|
|
92
|
-
const lowerTerm = match[1].toLowerCase(); // Get the captured group
|
|
93
|
-
if (!conceptCounts.has(lowerTerm)) {
|
|
94
|
-
conceptCounts.set(lowerTerm, new Set());
|
|
95
|
-
}
|
|
96
|
-
conceptCounts.get(lowerTerm).add(req.id);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// Generate suggestions
|
|
100
|
-
const suggestions = [];
|
|
101
|
-
for (const [concept, reqIds] of conceptCounts) {
|
|
102
|
-
if (reqIds.size >= minFreq) {
|
|
103
|
-
// Skip if this concept already exists as a fact
|
|
104
|
-
if (!existingFacts.has(concept)) {
|
|
105
|
-
suggestions.push({
|
|
106
|
-
concept: capitalizeConcept(concept),
|
|
107
|
-
mentions: reqIds.size,
|
|
108
|
-
requirements: Array.from(reqIds),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Sort by frequency (most mentioned first)
|
|
114
|
-
return suggestions.sort((a, b) => b.mentions - a.mentions);
|
|
115
|
-
}
|
|
116
|
-
function capitalizeConcept(concept) {
|
|
117
|
-
return concept
|
|
118
|
-
.split(/\s+/)
|
|
119
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
120
|
-
.join(" ");
|
|
121
|
-
}
|