kibi-cli 0.2.8 → 0.3.0
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/cli.js +62 -0
- package/dist/commands/coverage.d.ts +12 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +24 -0
- package/dist/commands/discovery-shared.d.ts +11 -0
- package/dist/commands/discovery-shared.d.ts.map +1 -0
- package/dist/commands/discovery-shared.js +280 -0
- package/dist/commands/doctor.js +8 -8
- package/dist/commands/gaps.d.ts +12 -0
- package/dist/commands/gaps.d.ts.map +1 -0
- package/dist/commands/gaps.js +28 -0
- package/dist/commands/graph.d.ts +13 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +9 -0
- package/dist/search-ranking.d.ts +9 -0
- package/dist/search-ranking.d.ts.map +1 -0
- package/dist/search-ranking.js +143 -0
- package/package.json +6 -2
package/dist/cli.js
CHANGED
|
@@ -19,10 +19,15 @@ import { readFileSync } from "node:fs";
|
|
|
19
19
|
import { Command } from "commander";
|
|
20
20
|
import { branchEnsureCommand } from "./commands/branch.js";
|
|
21
21
|
import { checkCommand } from "./commands/check.js";
|
|
22
|
+
import { coverageCommand } from "./commands/coverage.js";
|
|
22
23
|
import { doctorCommand } from "./commands/doctor.js";
|
|
24
|
+
import { gapsCommand } from "./commands/gaps.js";
|
|
23
25
|
import { gcCommand } from "./commands/gc.js";
|
|
26
|
+
import { graphCommand } from "./commands/graph.js";
|
|
24
27
|
import { initCommand } from "./commands/init.js";
|
|
25
28
|
import { queryCommand } from "./commands/query.js";
|
|
29
|
+
import { searchCommand } from "./commands/search.js";
|
|
30
|
+
import { statusCommand } from "./commands/status.js";
|
|
26
31
|
import { syncCommand } from "./commands/sync.js";
|
|
27
32
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
28
33
|
const VERSION = packageJson.version ?? "0.1.0";
|
|
@@ -59,6 +64,63 @@ program
|
|
|
59
64
|
.action(async (type, options) => {
|
|
60
65
|
await queryCommand(type, options);
|
|
61
66
|
});
|
|
67
|
+
program
|
|
68
|
+
.command("search [query]")
|
|
69
|
+
.description("Search knowledge base metadata and markdown content")
|
|
70
|
+
.option("--type <type>", "Filter by entity type")
|
|
71
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
72
|
+
.option("--limit <n>", "Limit results", "20")
|
|
73
|
+
.option("--offset <n>", "Skip results", "0")
|
|
74
|
+
.action(async (query, options) => {
|
|
75
|
+
await searchCommand(query, options);
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command("status")
|
|
79
|
+
.description("Show KB snapshot and freshness metadata")
|
|
80
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
await statusCommand(options);
|
|
83
|
+
});
|
|
84
|
+
program
|
|
85
|
+
.command("gaps [type]")
|
|
86
|
+
.description("Find entities missing or present on selected relationships")
|
|
87
|
+
.option("--missing-rel <rels>", "Comma-separated missing relationship filters")
|
|
88
|
+
.option("--present-rel <rels>", "Comma-separated present relationship filters")
|
|
89
|
+
.option("--tag <tags>", "Comma-separated tag filter")
|
|
90
|
+
.option("--source <path>", "Source file substring filter")
|
|
91
|
+
.option("--limit <n>", "Limit results", "100")
|
|
92
|
+
.option("--offset <n>", "Skip results", "0")
|
|
93
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
94
|
+
.action(async (type, options) => {
|
|
95
|
+
await gapsCommand(type, options);
|
|
96
|
+
});
|
|
97
|
+
program
|
|
98
|
+
.command("coverage")
|
|
99
|
+
.description("Generate curated coverage reports")
|
|
100
|
+
.option("--by <group>", "Coverage mode: req|symbol|type", "req")
|
|
101
|
+
.option("--tag <tags>", "Comma-separated tag filter")
|
|
102
|
+
.option("--include-passing", "Include passing rows", false)
|
|
103
|
+
.option("--no-include-transitive", "Disable transitive symbol coverage")
|
|
104
|
+
.option("--limit <n>", "Limit results", "100")
|
|
105
|
+
.option("--offset <n>", "Skip results", "0")
|
|
106
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
await coverageCommand(options);
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command("graph")
|
|
112
|
+
.description("Traverse the KB graph from one or more seed IDs")
|
|
113
|
+
.option("--from <ids>", "Comma-separated seed IDs")
|
|
114
|
+
.option("--relationships <rels>", "Comma-separated relationship filter")
|
|
115
|
+
.option("--direction <direction>", "Direction: outgoing|incoming|both", "outgoing")
|
|
116
|
+
.option("--depth <n>", "Traversal depth", "1")
|
|
117
|
+
.option("--entity-types <types>", "Comma-separated entity type filter")
|
|
118
|
+
.option("--max-nodes <n>", "Maximum node count", "200")
|
|
119
|
+
.option("--max-edges <n>", "Maximum edge count", "500")
|
|
120
|
+
.option("--format <format>", "Output format: json|table", "table")
|
|
121
|
+
.action(async (options) => {
|
|
122
|
+
await graphCommand(options);
|
|
123
|
+
});
|
|
62
124
|
program
|
|
63
125
|
.command("check")
|
|
64
126
|
.description("Check KB consistency and integrity")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface CoverageOptions {
|
|
2
|
+
by?: "req" | "symbol" | "type";
|
|
3
|
+
tag?: string;
|
|
4
|
+
includePassing?: boolean;
|
|
5
|
+
includeTransitive?: boolean;
|
|
6
|
+
limit?: string;
|
|
7
|
+
offset?: string;
|
|
8
|
+
format?: "json" | "table";
|
|
9
|
+
}
|
|
10
|
+
export declare function coverageCommand(options: CoverageOptions): Promise<void>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/commands/coverage.ts"],"names":[],"mappings":"AAQA,UAAU,eAAe;IACvB,EAAE,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC7E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { escapeAtom } from "../prolog/codec.js";
|
|
2
|
+
import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
|
|
3
|
+
// implements REQ-002, REQ-003
|
|
4
|
+
export async function coverageCommand(options) {
|
|
5
|
+
await withPrologProcess(async (prolog) => {
|
|
6
|
+
const kbPath = await resolveCurrentKbPath();
|
|
7
|
+
const by = options.by || "req";
|
|
8
|
+
const tags = options.tag
|
|
9
|
+
? `[${options.tag
|
|
10
|
+
.split(",")
|
|
11
|
+
.map((item) => item.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((item) => `'${escapeAtom(item)}'`)
|
|
14
|
+
.join(",")}]`
|
|
15
|
+
: "[]";
|
|
16
|
+
const includePassing = options.includePassing ?? false;
|
|
17
|
+
const includeTransitive = options.includeTransitive ?? true;
|
|
18
|
+
const limit = Number.parseInt(options.limit || "100", 10);
|
|
19
|
+
const offset = Number.parseInt(options.offset || "0", 10);
|
|
20
|
+
const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:coverage_report_json('${by}', ${tags}, ${includePassing}, ${includeTransitive}, ${limit}, ${offset}, JsonString)`, "coverage query failed", kbPath);
|
|
21
|
+
const summary = (result.summary ?? {});
|
|
22
|
+
printDiscoveryResult(options.format, result, `Coverage summary: ${summary.fullyCovered ?? 0} fully covered out of ${summary.total ?? 0}.`);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PrologProcess } from "../prolog.js";
|
|
2
|
+
export interface DiscoveryCommandOptions {
|
|
3
|
+
format?: "json" | "table";
|
|
4
|
+
}
|
|
5
|
+
export declare function withAttachedBranchProlog<T>(callback: (prolog: PrologProcess) => Promise<T>): Promise<T>;
|
|
6
|
+
export declare function withPrologProcess<T>(callback: (prolog: PrologProcess) => Promise<T>): Promise<T>;
|
|
7
|
+
export declare function resolveCurrentKbPath(): Promise<string>;
|
|
8
|
+
export declare function resolveCoreModulePath(fileName: string): string;
|
|
9
|
+
export declare function runJsonModuleQuery<T>(prolog: PrologProcess, fileName: string, goal: string, errorLabel: string, kbPath?: string): Promise<T>;
|
|
10
|
+
export declare function printDiscoveryResult(format: "json" | "table" | undefined, structured: unknown, fallbackText: string): void;
|
|
11
|
+
//# sourceMappingURL=discovery-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery-shared.d.ts","sourceRoot":"","sources":["../../src/commands/discovery-shared.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAmB,MAAM,cAAc,CAAC;AAI9D,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAGD,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,CAAC,CAAC,CAcZ;AAGD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS5D;AAGD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE9D;AAGD,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAGD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,EACpC,UAAU,EAAE,OAAO,EACnB,YAAY,EAAE,MAAM,GACnB,IAAI,CAQN"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { PrologProcess, resolveKbPlPath } from "../prolog.js";
|
|
4
|
+
import { escapeAtom } from "../prolog/codec.js";
|
|
5
|
+
import { getCurrentBranch } from "./init-helpers.js";
|
|
6
|
+
// implements REQ-003
|
|
7
|
+
export async function withAttachedBranchProlog(callback) {
|
|
8
|
+
let prolog = null;
|
|
9
|
+
let attached = false;
|
|
10
|
+
try {
|
|
11
|
+
prolog = new PrologProcess({ timeout: 120000 });
|
|
12
|
+
await prolog.start();
|
|
13
|
+
await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
|
|
14
|
+
let branch;
|
|
15
|
+
try {
|
|
16
|
+
branch = process.env.KIBI_BRANCH || (await getCurrentBranch(process.cwd()));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
branch = process.env.KIBI_BRANCH || "main";
|
|
20
|
+
}
|
|
21
|
+
const kbPath = path.join(process.cwd(), ".kb/branches", branch);
|
|
22
|
+
const attachResult = await prolog.query(`kb_attach('${escapeAtom(kbPath)}')`);
|
|
23
|
+
if (!attachResult.success) {
|
|
24
|
+
throw new Error(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
|
|
25
|
+
}
|
|
26
|
+
attached = true;
|
|
27
|
+
return await callback(prolog);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (prolog) {
|
|
31
|
+
if (attached) {
|
|
32
|
+
try {
|
|
33
|
+
await prolog.query("kb_detach");
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await prolog.terminate();
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// implements REQ-003
|
|
45
|
+
export async function withPrologProcess(callback) {
|
|
46
|
+
const prolog = new PrologProcess({ timeout: 120000 });
|
|
47
|
+
try {
|
|
48
|
+
await prolog.start();
|
|
49
|
+
;
|
|
50
|
+
prolog.useOneShotMode = true;
|
|
51
|
+
await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
|
|
52
|
+
return await callback(prolog);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
try {
|
|
56
|
+
await prolog.terminate();
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// implements REQ-003
|
|
62
|
+
export async function resolveCurrentKbPath() {
|
|
63
|
+
let branch;
|
|
64
|
+
try {
|
|
65
|
+
branch = process.env.KIBI_BRANCH || (await getCurrentBranch(process.cwd()));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
branch = process.env.KIBI_BRANCH || "main";
|
|
69
|
+
}
|
|
70
|
+
return path.join(process.cwd(), ".kb/branches", branch);
|
|
71
|
+
}
|
|
72
|
+
// implements REQ-003
|
|
73
|
+
export function resolveCoreModulePath(fileName) {
|
|
74
|
+
return path.join(path.dirname(resolveKbPlPath()), fileName);
|
|
75
|
+
}
|
|
76
|
+
// implements REQ-003
|
|
77
|
+
export async function runJsonModuleQuery(prolog, fileName, goal, errorLabel, kbPath) {
|
|
78
|
+
const modulePath = escapeAtom(resolveCoreModulePath(fileName).replace(/\\/g, "/"));
|
|
79
|
+
const wrappedGoal = kbPath
|
|
80
|
+
? `(use_module('${modulePath}'), kb_attach('${escapeAtom(kbPath)}'), ${goal}, kb_detach)`
|
|
81
|
+
: `(use_module('${modulePath}'), ${goal})`;
|
|
82
|
+
const result = await prolog.query(wrappedGoal);
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
throw new Error(`${errorLabel}: ${result.error || "Unknown error"}`);
|
|
85
|
+
}
|
|
86
|
+
const rawJson = result.bindings.JsonString;
|
|
87
|
+
if (!rawJson) {
|
|
88
|
+
throw new Error(`${errorLabel}: missing JsonString binding`);
|
|
89
|
+
}
|
|
90
|
+
let parsed = JSON.parse(rawJson);
|
|
91
|
+
if (typeof parsed === "string") {
|
|
92
|
+
parsed = JSON.parse(parsed);
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
// implements REQ-003
|
|
97
|
+
export function printDiscoveryResult(format, structured, fallbackText) {
|
|
98
|
+
if (format === "json") {
|
|
99
|
+
console.log(JSON.stringify(structured, null, 2));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const rendered = renderDiscoveryTable(structured);
|
|
103
|
+
console.log(rendered || fallbackText);
|
|
104
|
+
}
|
|
105
|
+
function renderDiscoveryTable(structured) {
|
|
106
|
+
if (!structured || typeof structured !== "object") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const payload = structured;
|
|
110
|
+
if (Array.isArray(payload.results)) {
|
|
111
|
+
return renderSearchTable(payload);
|
|
112
|
+
}
|
|
113
|
+
if (typeof payload.branch === "string" && typeof payload.syncState === "string") {
|
|
114
|
+
return renderStatusTable(payload);
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(payload.nodes) && Array.isArray(payload.edges)) {
|
|
117
|
+
return renderGraphTable(payload);
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(payload.rows) && payload.summary && typeof payload.summary === "object") {
|
|
120
|
+
return renderCoverageTable(payload);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(payload.rows)) {
|
|
123
|
+
return renderGapsTable(payload);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function renderSearchTable(payload) {
|
|
128
|
+
const rows = Array.isArray(payload.results) ? payload.results : [];
|
|
129
|
+
const table = new Table({
|
|
130
|
+
head: ["ID", "Type", "Title", "Score", "Reasons", "Snippet"],
|
|
131
|
+
wordWrap: true,
|
|
132
|
+
colWidths: [20, 10, 32, 8, 28, 44],
|
|
133
|
+
});
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const match = row;
|
|
136
|
+
const entity = (match.entity ?? {});
|
|
137
|
+
const reasons = Array.isArray(match.reasons) ? match.reasons.join(", ") : "";
|
|
138
|
+
table.push([
|
|
139
|
+
stringifyCell(entity.id),
|
|
140
|
+
stringifyCell(entity.type),
|
|
141
|
+
stringifyCell(entity.title),
|
|
142
|
+
stringifyCell(match.score),
|
|
143
|
+
reasons,
|
|
144
|
+
stringifyCell(match.snippet),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
`Search results: ${stringifyCell(payload.count)} total`,
|
|
149
|
+
table.toString(),
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
152
|
+
function renderStatusTable(payload) {
|
|
153
|
+
const table = new Table({
|
|
154
|
+
head: ["Field", "Value"],
|
|
155
|
+
colWidths: [18, 72],
|
|
156
|
+
wordWrap: true,
|
|
157
|
+
});
|
|
158
|
+
table.push(["Branch", stringifyCell(payload.branch)], ["Sync State", stringifyCell(payload.syncState)], ["Dirty", stringifyCell(payload.dirty)], ["Snapshot", stringifyCell(payload.snapshotId)], ["Synced At", stringifyCell(payload.syncedAt)], ["KB Path", stringifyCell(payload.kbPath)]);
|
|
159
|
+
return table.toString();
|
|
160
|
+
}
|
|
161
|
+
function renderGapsTable(payload) {
|
|
162
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
163
|
+
const table = new Table({
|
|
164
|
+
head: ["ID", "Type", "Status", "Missing", "Present", "Source"],
|
|
165
|
+
colWidths: [20, 10, 12, 24, 24, 40],
|
|
166
|
+
wordWrap: true,
|
|
167
|
+
});
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
const item = row;
|
|
170
|
+
table.push([
|
|
171
|
+
stringifyCell(item.id),
|
|
172
|
+
stringifyCell(item.type),
|
|
173
|
+
stringifyCell(item.status),
|
|
174
|
+
joinCells(item.missingRelationships),
|
|
175
|
+
joinCells(item.presentRelationships),
|
|
176
|
+
stringifyCell(item.source),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
return [`Gap rows: ${stringifyCell(payload.count)}`, table.toString()].join("\n");
|
|
180
|
+
}
|
|
181
|
+
function renderCoverageTable(payload) {
|
|
182
|
+
const summary = (payload.summary ?? {});
|
|
183
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
184
|
+
const summaryTable = new Table({
|
|
185
|
+
head: ["Metric", "Value"],
|
|
186
|
+
colWidths: [24, 16],
|
|
187
|
+
});
|
|
188
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
189
|
+
summaryTable.push([key, stringifyCell(value)]);
|
|
190
|
+
}
|
|
191
|
+
const firstRow = rows[0];
|
|
192
|
+
const isRequirementCoverage = firstRow && Object.hasOwn(firstRow, "scenarioCount");
|
|
193
|
+
const table = isRequirementCoverage
|
|
194
|
+
? new Table({
|
|
195
|
+
head: ["ID", "Status", "Priority", "Coverage", "Scen", "Tests", "Symbols", "Gaps"],
|
|
196
|
+
colWidths: [20, 12, 12, 18, 8, 8, 10, 28],
|
|
197
|
+
wordWrap: true,
|
|
198
|
+
})
|
|
199
|
+
: new Table({
|
|
200
|
+
head: ["ID", "Type", "Coverage", "Details", "Gaps"],
|
|
201
|
+
colWidths: [20, 10, 18, 28, 28],
|
|
202
|
+
wordWrap: true,
|
|
203
|
+
});
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const item = row;
|
|
206
|
+
if (isRequirementCoverage) {
|
|
207
|
+
table.push([
|
|
208
|
+
stringifyCell(item.id),
|
|
209
|
+
stringifyCell(item.status),
|
|
210
|
+
stringifyCell(item.priority),
|
|
211
|
+
stringifyCell(item.coverageStatus),
|
|
212
|
+
stringifyCell(item.scenarioCount),
|
|
213
|
+
stringifyCell(item.testCount),
|
|
214
|
+
stringifyCell(item.transitiveSymbolCount),
|
|
215
|
+
joinCells(item.gaps),
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
table.push([
|
|
220
|
+
stringifyCell(item.id),
|
|
221
|
+
stringifyCell(item.type),
|
|
222
|
+
stringifyCell(item.coverageStatus),
|
|
223
|
+
`req=${stringifyCell(item.directRequirementCount)} test=${stringifyCell(item.testCount)} count=${stringifyCell(item.count)}`,
|
|
224
|
+
joinCells(item.gaps),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [summaryTable.toString(), table.toString()].join("\n\n");
|
|
229
|
+
}
|
|
230
|
+
function renderGraphTable(payload) {
|
|
231
|
+
const nodes = Array.isArray(payload.nodes) ? payload.nodes : [];
|
|
232
|
+
const edges = Array.isArray(payload.edges) ? payload.edges : [];
|
|
233
|
+
const nodeTable = new Table({
|
|
234
|
+
head: ["Node ID", "Type", "Title", "Status"],
|
|
235
|
+
colWidths: [22, 10, 36, 12],
|
|
236
|
+
wordWrap: true,
|
|
237
|
+
});
|
|
238
|
+
for (const row of nodes) {
|
|
239
|
+
const item = row;
|
|
240
|
+
nodeTable.push([
|
|
241
|
+
stringifyCell(item.id),
|
|
242
|
+
stringifyCell(item.type),
|
|
243
|
+
stringifyCell(item.title),
|
|
244
|
+
stringifyCell(item.status),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
const edgeTable = new Table({
|
|
248
|
+
head: ["Relationship", "From", "To"],
|
|
249
|
+
colWidths: [18, 22, 22],
|
|
250
|
+
wordWrap: true,
|
|
251
|
+
});
|
|
252
|
+
for (const row of edges) {
|
|
253
|
+
const item = row;
|
|
254
|
+
edgeTable.push([
|
|
255
|
+
stringifyCell(item.type),
|
|
256
|
+
stringifyCell(item.from),
|
|
257
|
+
stringifyCell(item.to),
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
return [
|
|
261
|
+
`Nodes: ${nodes.length} Edges: ${edges.length} Truncated: ${stringifyCell(payload.truncated)}`,
|
|
262
|
+
nodeTable.toString(),
|
|
263
|
+
edgeTable.toString(),
|
|
264
|
+
].join("\n\n");
|
|
265
|
+
}
|
|
266
|
+
function stringifyCell(value) {
|
|
267
|
+
if (value === null || value === undefined || value === "") {
|
|
268
|
+
return "-";
|
|
269
|
+
}
|
|
270
|
+
if (typeof value === "string") {
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
return String(value);
|
|
274
|
+
}
|
|
275
|
+
function joinCells(value) {
|
|
276
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
277
|
+
return "-";
|
|
278
|
+
}
|
|
279
|
+
return value.map((item) => stringifyCell(item)).join(", ");
|
|
280
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -198,7 +198,7 @@ function checkGitHooks() {
|
|
|
198
198
|
return {
|
|
199
199
|
passed: false,
|
|
200
200
|
message: "Partially installed",
|
|
201
|
-
remediation: "Run: kibi init
|
|
201
|
+
remediation: "Run: kibi init",
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
function checkPreCommitHook() {
|
|
@@ -218,7 +218,7 @@ function checkPreCommitHook() {
|
|
|
218
218
|
return {
|
|
219
219
|
passed: false,
|
|
220
220
|
message: "Not installed",
|
|
221
|
-
remediation: "Run: kibi init
|
|
221
|
+
remediation: "Run: kibi init",
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
try {
|
|
@@ -233,7 +233,7 @@ function checkPreCommitHook() {
|
|
|
233
233
|
return {
|
|
234
234
|
passed: false,
|
|
235
235
|
message: "pre-commit hook installed but does not invoke kibi",
|
|
236
|
-
remediation: "Run: kibi init
|
|
236
|
+
remediation: "Run: kibi init to install recommended hooks",
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
239
|
if (preCommitExecutable) {
|
|
@@ -247,7 +247,7 @@ function checkPreCommitHook() {
|
|
|
247
247
|
return {
|
|
248
248
|
passed: true,
|
|
249
249
|
message: "Installed and executable (uses legacy 'kibi check' — consider running 'kibi init' to update hooks to use '--staged')",
|
|
250
|
-
remediation: "Run: kibi init
|
|
250
|
+
remediation: "Run: kibi init to update git hooks to the latest template",
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
return {
|
|
@@ -260,7 +260,7 @@ function checkPreCommitHook() {
|
|
|
260
260
|
return {
|
|
261
261
|
passed: false,
|
|
262
262
|
message: "Unable to check hook permissions or read content",
|
|
263
|
-
remediation: "Run: kibi init
|
|
263
|
+
remediation: "Run: kibi init",
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
266
|
}
|
|
@@ -281,7 +281,7 @@ function checkPostRewriteHook() {
|
|
|
281
281
|
return {
|
|
282
282
|
passed: false,
|
|
283
283
|
message: "Not installed",
|
|
284
|
-
remediation: "Run: kibi init
|
|
284
|
+
remediation: "Run: kibi init",
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
try {
|
|
@@ -294,7 +294,7 @@ function checkPostRewriteHook() {
|
|
|
294
294
|
return {
|
|
295
295
|
passed: false,
|
|
296
296
|
message: "post-rewrite hook installed but does not invoke kibi",
|
|
297
|
-
remediation: "Run: kibi init
|
|
297
|
+
remediation: "Run: kibi init to install recommended hooks",
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
if (postRewriteExecutable) {
|
|
@@ -313,7 +313,7 @@ function checkPostRewriteHook() {
|
|
|
313
313
|
return {
|
|
314
314
|
passed: false,
|
|
315
315
|
message: "Unable to check hook permissions or read content",
|
|
316
|
-
remediation: "Run: kibi init
|
|
316
|
+
remediation: "Run: kibi init",
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface GapsOptions {
|
|
2
|
+
missingRel?: string;
|
|
3
|
+
presentRel?: string;
|
|
4
|
+
tag?: string;
|
|
5
|
+
source?: string;
|
|
6
|
+
limit?: string;
|
|
7
|
+
offset?: string;
|
|
8
|
+
format?: "json" | "table";
|
|
9
|
+
}
|
|
10
|
+
export declare function gapsCommand(type: string | undefined, options: GapsOptions): Promise<void>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=gaps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gaps.d.ts","sourceRoot":"","sources":["../../src/commands/gaps.ts"],"names":[],"mappings":"AAQA,UAAU,WAAW;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { escapeAtom } from "../prolog/codec.js";
|
|
2
|
+
import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
|
|
3
|
+
// implements REQ-002, REQ-003
|
|
4
|
+
export async function gapsCommand(type, options) {
|
|
5
|
+
await withPrologProcess(async (prolog) => {
|
|
6
|
+
const kbPath = await resolveCurrentKbPath();
|
|
7
|
+
const missing = csvToPrologList(options.missingRel);
|
|
8
|
+
const present = csvToPrologList(options.presentRel);
|
|
9
|
+
const tags = csvToPrologList(options.tag);
|
|
10
|
+
const source = options.source ? `'${escapeAtom(options.source)}'` : "none";
|
|
11
|
+
const limit = Number.parseInt(options.limit || "100", 10);
|
|
12
|
+
const offset = Number.parseInt(options.offset || "0", 10);
|
|
13
|
+
const typeArg = type ? `'${escapeAtom(type)}'` : "none";
|
|
14
|
+
const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:find_gaps_json(${typeArg}, ${missing}, ${present}, ${tags}, ${source}, ${limit}, ${offset}, JsonString)`, "gaps query failed", kbPath);
|
|
15
|
+
printDiscoveryResult(options.format, result, `Found ${result.count ?? 0} gap rows.`);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function csvToPrologList(value) {
|
|
19
|
+
if (!value?.trim()) {
|
|
20
|
+
return "[]";
|
|
21
|
+
}
|
|
22
|
+
return `[${value
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((item) => item.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((item) => `'${escapeAtom(item)}'`)
|
|
27
|
+
.join(",")}]`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface GraphOptions {
|
|
2
|
+
from?: string;
|
|
3
|
+
relationships?: string;
|
|
4
|
+
direction?: "outgoing" | "incoming" | "both";
|
|
5
|
+
depth?: string;
|
|
6
|
+
entityTypes?: string;
|
|
7
|
+
maxNodes?: string;
|
|
8
|
+
maxEdges?: string;
|
|
9
|
+
format?: "json" | "table";
|
|
10
|
+
}
|
|
11
|
+
export declare function graphCommand(options: GraphOptions): Promise<void>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/commands/graph.ts"],"names":[],"mappings":"AAQA,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { escapeAtom } from "../prolog/codec.js";
|
|
2
|
+
import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
|
|
3
|
+
// implements REQ-002, REQ-003
|
|
4
|
+
export async function graphCommand(options) {
|
|
5
|
+
if (!options.from?.trim()) {
|
|
6
|
+
console.error("Error: --from is required");
|
|
7
|
+
process.exitCode = 1;
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await withPrologProcess(async (prolog) => {
|
|
11
|
+
const kbPath = await resolveCurrentKbPath();
|
|
12
|
+
const seedIds = csvToPrologList(options.from);
|
|
13
|
+
const relationships = csvToPrologList(options.relationships);
|
|
14
|
+
const direction = options.direction || "outgoing";
|
|
15
|
+
const depth = Number.parseInt(options.depth || "1", 10);
|
|
16
|
+
const entityTypes = csvToPrologList(options.entityTypes);
|
|
17
|
+
const maxNodes = Number.parseInt(options.maxNodes || "200", 10);
|
|
18
|
+
const maxEdges = Number.parseInt(options.maxEdges || "500", 10);
|
|
19
|
+
const result = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${seedIds}, ${relationships}, '${direction}', ${depth}, ${entityTypes}, ${maxNodes}, ${maxEdges}, JsonString)`, "graph query failed", kbPath);
|
|
20
|
+
const nodes = Array.isArray(result.nodes) ? result.nodes.length : 0;
|
|
21
|
+
const edges = Array.isArray(result.edges) ? result.edges.length : 0;
|
|
22
|
+
printDiscoveryResult(options.format, result, `Graph traversal returned ${nodes} nodes and ${edges} edges.`);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function csvToPrologList(value) {
|
|
26
|
+
if (!value?.trim()) {
|
|
27
|
+
return "[]";
|
|
28
|
+
}
|
|
29
|
+
return `[${value
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((item) => item.trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((item) => `'${escapeAtom(item)}'`)
|
|
34
|
+
.join(",")}]`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface SearchOptions {
|
|
2
|
+
type?: string;
|
|
3
|
+
format?: "json" | "table";
|
|
4
|
+
limit?: string;
|
|
5
|
+
offset?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function searchCommand(query: string | undefined, options: SearchOptions): Promise<void>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"AAYA,UAAU,aAAa;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { rankEntities } from "../search-ranking.js";
|
|
2
|
+
import { VALID_ENTITY_TYPES, queryEntities, } from "../query/service.js";
|
|
3
|
+
import { printDiscoveryResult, withAttachedBranchProlog, } from "./discovery-shared.js";
|
|
4
|
+
// implements REQ-mcp-search-discovery, REQ-003
|
|
5
|
+
export async function searchCommand(query, options) {
|
|
6
|
+
if (!query?.trim()) {
|
|
7
|
+
console.error("Error: search query is required");
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (options.type && !VALID_ENTITY_TYPES.includes(options.type)) {
|
|
12
|
+
console.error(`Error: invalid type '${options.type}'. Valid types: ${VALID_ENTITY_TYPES.join(", ")}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await withAttachedBranchProlog(async (prolog) => {
|
|
17
|
+
const limit = Number.parseInt(options.limit || "20", 10);
|
|
18
|
+
const offset = Number.parseInt(options.offset || "0", 10);
|
|
19
|
+
const result = await executeSearch(prolog, query, options.type, limit, offset);
|
|
20
|
+
printDiscoveryResult(options.format, result, buildSearchText(query, result));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function executeSearch(prolog, query, type, limit, offset) {
|
|
24
|
+
const entitiesResult = await queryEntities(prolog, {
|
|
25
|
+
type,
|
|
26
|
+
limit: 100000,
|
|
27
|
+
offset: 0,
|
|
28
|
+
});
|
|
29
|
+
const matches = await rankEntities(entitiesResult.entities, query, process.cwd());
|
|
30
|
+
const paginated = matches.slice(offset, offset + limit);
|
|
31
|
+
return { results: paginated, count: matches.length };
|
|
32
|
+
}
|
|
33
|
+
function buildSearchText(query, result) {
|
|
34
|
+
if (result.count === 0) {
|
|
35
|
+
return `No search results for '${query}'.`;
|
|
36
|
+
}
|
|
37
|
+
return `Found ${result.count} search results for '${query}'. Showing ${result.results.length}: ${result.results.map((match) => match.entity.id).join(", ")}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAOA,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBzE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
|
|
2
|
+
// implements REQ-002, REQ-003
|
|
3
|
+
export async function statusCommand(options) {
|
|
4
|
+
await withPrologProcess(async (prolog) => {
|
|
5
|
+
const kbPath = await resolveCurrentKbPath();
|
|
6
|
+
const result = await runJsonModuleQuery(prolog, "status.pl", "status:kb_status_json(JsonString)", "status query failed", kbPath);
|
|
7
|
+
printDiscoveryResult(options.format, result, `Branch ${result.branch} is ${result.syncState} (dirty=${result.dirty})`);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SearchMatch {
|
|
2
|
+
entity: Record<string, unknown>;
|
|
3
|
+
score: number;
|
|
4
|
+
reasons: string[];
|
|
5
|
+
snippet?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function rankEntities(entities: Record<string, unknown>[], query: string, workspaceRoot: string): Promise<SearchMatch[]>;
|
|
8
|
+
export declare function loadMarkdownBody(source: string, workspaceRoot: string): Promise<string | null>;
|
|
9
|
+
//# sourceMappingURL=search-ranking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAuBxB;AA2FD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
// implements REQ-mcp-search-discovery, REQ-002, REQ-003
|
|
5
|
+
export async function rankEntities(entities, query, workspaceRoot) {
|
|
6
|
+
const matches = [];
|
|
7
|
+
for (const entity of entities) {
|
|
8
|
+
const match = await rankEntity(entity, query, workspaceRoot);
|
|
9
|
+
if (match) {
|
|
10
|
+
matches.push(match);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
matches.sort((left, right) => {
|
|
14
|
+
if (right.score !== left.score) {
|
|
15
|
+
return right.score - left.score;
|
|
16
|
+
}
|
|
17
|
+
const leftType = String(left.entity.type ?? "");
|
|
18
|
+
const rightType = String(right.entity.type ?? "");
|
|
19
|
+
if (leftType !== rightType) {
|
|
20
|
+
return leftType.localeCompare(rightType);
|
|
21
|
+
}
|
|
22
|
+
return String(left.entity.id ?? "").localeCompare(String(right.entity.id ?? ""));
|
|
23
|
+
});
|
|
24
|
+
return matches;
|
|
25
|
+
}
|
|
26
|
+
async function rankEntity(entity, query, workspaceRoot) {
|
|
27
|
+
const normalizedQuery = normalize(query);
|
|
28
|
+
const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
29
|
+
const reasons = [];
|
|
30
|
+
let score = 0;
|
|
31
|
+
const id = String(entity.id ?? "");
|
|
32
|
+
const title = String(entity.title ?? "");
|
|
33
|
+
const type = String(entity.type ?? "");
|
|
34
|
+
const source = String(entity.source ?? "");
|
|
35
|
+
const owner = String(entity.owner ?? "");
|
|
36
|
+
const priority = String(entity.priority ?? "");
|
|
37
|
+
const severity = String(entity.severity ?? "");
|
|
38
|
+
const tags = Array.isArray(entity.tags)
|
|
39
|
+
? entity.tags.map((tag) => String(tag))
|
|
40
|
+
: [];
|
|
41
|
+
const normalizedTitle = normalize(title);
|
|
42
|
+
const normalizedId = normalize(id);
|
|
43
|
+
if (normalizedTitle === normalizedQuery) {
|
|
44
|
+
score += 100;
|
|
45
|
+
reasons.push("exact title match");
|
|
46
|
+
}
|
|
47
|
+
else if (normalizedTitle.includes(normalizedQuery)) {
|
|
48
|
+
score += 60;
|
|
49
|
+
reasons.push("title phrase match");
|
|
50
|
+
}
|
|
51
|
+
if (normalizedId === normalizedQuery) {
|
|
52
|
+
score += 90;
|
|
53
|
+
reasons.push("exact ID match");
|
|
54
|
+
}
|
|
55
|
+
else if (normalizedId.includes(normalizedQuery)) {
|
|
56
|
+
score += 55;
|
|
57
|
+
reasons.push("ID match");
|
|
58
|
+
}
|
|
59
|
+
const metadataFields = [type, source, owner, priority, severity];
|
|
60
|
+
const metadataMatched = metadataFields.some((field) => normalize(field).includes(normalizedQuery));
|
|
61
|
+
if (metadataMatched) {
|
|
62
|
+
score += 20;
|
|
63
|
+
reasons.push("metadata match");
|
|
64
|
+
}
|
|
65
|
+
const matchingTags = tags.filter((tag) => normalize(tag).includes(normalizedQuery));
|
|
66
|
+
if (matchingTags.length > 0) {
|
|
67
|
+
score += 30;
|
|
68
|
+
reasons.push("tag match");
|
|
69
|
+
}
|
|
70
|
+
const titleTokenMatches = countTokenMatches(normalizedTitle, tokens);
|
|
71
|
+
if (titleTokenMatches > 0) {
|
|
72
|
+
score += titleTokenMatches * 8;
|
|
73
|
+
reasons.push("title token coverage");
|
|
74
|
+
}
|
|
75
|
+
const bodyText = await loadMarkdownBody(source, workspaceRoot);
|
|
76
|
+
let snippet;
|
|
77
|
+
if (bodyText) {
|
|
78
|
+
const normalizedBody = normalize(bodyText);
|
|
79
|
+
if (normalizedBody.includes(normalizedQuery)) {
|
|
80
|
+
score += 15;
|
|
81
|
+
reasons.push("markdown body match");
|
|
82
|
+
snippet = buildSnippet(bodyText, query);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const bodyTokenMatches = countTokenMatches(normalizedBody, tokens);
|
|
86
|
+
if (bodyTokenMatches > 0) {
|
|
87
|
+
score += bodyTokenMatches * 3;
|
|
88
|
+
reasons.push("markdown body token coverage");
|
|
89
|
+
snippet = buildSnippet(bodyText, query);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (score === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
entity,
|
|
98
|
+
score,
|
|
99
|
+
reasons: Array.from(new Set(reasons)),
|
|
100
|
+
snippet,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function loadMarkdownBody(source, workspaceRoot) {
|
|
104
|
+
if (!source) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const normalizedSource = source.split("#", 1)[0]?.trim() ?? "";
|
|
108
|
+
if (!normalizedSource.endsWith(".md")) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Resolve to absolute path; relative paths are resolved against workspaceRoot.
|
|
112
|
+
const resolved = path.resolve(path.isAbsolute(normalizedSource) ? normalizedSource : path.join(workspaceRoot, normalizedSource));
|
|
113
|
+
// Reject paths that escape the workspace root to prevent path traversal.
|
|
114
|
+
const normalizedRoot = path.resolve(workspaceRoot);
|
|
115
|
+
if (!resolved.startsWith(normalizedRoot + path.sep)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const fileContent = await fs.readFile(resolved, "utf8");
|
|
120
|
+
return matter(fileContent).content;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function normalize(value) {
|
|
127
|
+
return value.trim().toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
function countTokenMatches(haystack, tokens) {
|
|
130
|
+
return tokens.filter((token) => haystack.includes(token)).length;
|
|
131
|
+
}
|
|
132
|
+
function buildSnippet(bodyText, query) {
|
|
133
|
+
const lines = bodyText
|
|
134
|
+
.split(/\r?\n/)
|
|
135
|
+
.map((line) => line.trim())
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
const normalizedQuery = normalize(query);
|
|
138
|
+
const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
|
|
139
|
+
if (!matchedLine) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return matchedLine.length > 160 ? `${matchedLine.slice(0, 157)}...` : matchedLine;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"./diagnostics": {
|
|
59
59
|
"types": "./dist/diagnostics.d.ts",
|
|
60
60
|
"default": "./dist/diagnostics.js"
|
|
61
|
+
},
|
|
62
|
+
"./search-ranking": {
|
|
63
|
+
"types": "./dist/search-ranking.d.ts",
|
|
64
|
+
"default": "./dist/search-ranking.js"
|
|
61
65
|
}
|
|
62
66
|
},
|
|
63
67
|
"types": "./dist/cli.d.ts",
|
|
@@ -68,7 +72,7 @@
|
|
|
68
72
|
"fast-glob": "^3.2.12",
|
|
69
73
|
"gray-matter": "^4.0.3",
|
|
70
74
|
"js-yaml": "^4.1.0",
|
|
71
|
-
"kibi-core": "^0.
|
|
75
|
+
"kibi-core": "^0.2.0",
|
|
72
76
|
"ts-morph": "^23.0.0"
|
|
73
77
|
},
|
|
74
78
|
"devDependencies": {
|