kibi-cli 0.2.7 → 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/aggregated-checks.js +4 -6
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +26 -24
- 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/query.d.ts.map +1 -1
- package/dist/commands/query.js +2 -8
- 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/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +0 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +0 -2
- package/dist/extractors/manifest.d.ts +2 -0
- package/dist/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +1 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +9 -1
- package/dist/public/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/public/extractors/symbols-coordinator.js +1 -2
- package/dist/public/prolog/index.d.ts.map +1 -1
- package/dist/public/prolog/index.js +1 -2
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +1 -3
- package/dist/public/schemas/relationship.d.ts.map +1 -1
- package/dist/public/schemas/relationship.js +1 -3
- 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/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +33 -7
- package/dist/traceability/symbol-extract.d.ts +6 -1
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +62 -34
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +4 -3
- package/dist/traceability/validate.d.ts.map +1 -1
- package/dist/traceability/validate.js +8 -7
- package/package.json +10 -2
- package/src/public/extractors/symbols-coordinator.ts +1 -2
- package/src/public/prolog/index.ts +1 -2
- package/src/public/schemas/entity.ts +1 -3
- package/src/public/schemas/relationship.ts +1 -3
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")
|
|
@@ -23,8 +23,7 @@ export async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr = f
|
|
|
23
23
|
try {
|
|
24
24
|
const result = await prolog.query(query);
|
|
25
25
|
if (!result.success) {
|
|
26
|
-
|
|
27
|
-
return [];
|
|
26
|
+
throw new Error(`Aggregated checks query failed: ${result.error || "Unknown error"}`);
|
|
28
27
|
}
|
|
29
28
|
let violationsDict;
|
|
30
29
|
try {
|
|
@@ -39,8 +38,7 @@ export async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr = f
|
|
|
39
38
|
violationsDict = parsed;
|
|
40
39
|
}
|
|
41
40
|
catch (parseError) {
|
|
42
|
-
|
|
43
|
-
return [];
|
|
41
|
+
throw new Error(`Failed to parse violations JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
44
42
|
}
|
|
45
43
|
for (const ruleViolations of Object.values(violationsDict)) {
|
|
46
44
|
for (const v of ruleViolations) {
|
|
@@ -59,7 +57,7 @@ export async function runAggregatedChecks(prolog, rulesAllowlist, requireAdr = f
|
|
|
59
57
|
return violations;
|
|
60
58
|
}
|
|
61
59
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
throw new Error(`Error running aggregated checks: ${message}`);
|
|
64
62
|
}
|
|
65
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAgDA,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+RvE"}
|
package/dist/commands/check.js
CHANGED
|
@@ -16,24 +16,24 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { extractFromManifest } from "../extractors/manifest.js";
|
|
19
20
|
import { PrologProcess } from "../prolog.js";
|
|
20
21
|
import { escapeAtom } from "../prolog/codec.js";
|
|
21
22
|
import { getStagedFiles } from "../traceability/git-staged.js";
|
|
22
23
|
import { validateStagedMarkdown } from "../traceability/markdown-validate.js";
|
|
23
|
-
import { extractSymbolsFromStagedFile } from "../traceability/symbol-extract.js";
|
|
24
|
+
import { extractSymbolsFromStagedFile, } from "../traceability/symbol-extract.js";
|
|
24
25
|
import { cleanupTempKb, consultOverlay, createOverlayFacts, createTempKb, } from "../traceability/temp-kb.js";
|
|
25
26
|
import { formatViolations as formatStagedViolations, validateStagedSymbols, } from "../traceability/validate.js";
|
|
26
27
|
import { loadConfig } from "../utils/config.js";
|
|
27
28
|
import { RULES, getEffectiveRules, } from "../utils/rule-registry.js";
|
|
28
29
|
import { runAggregatedChecks } from "./aggregated-checks.js";
|
|
29
30
|
import { getCurrentBranch } from "./init-helpers.js";
|
|
31
|
+
import { discoverSourceFiles } from "./sync/discovery.js";
|
|
30
32
|
// implements REQ-006
|
|
31
33
|
export async function checkCommand(options) {
|
|
32
34
|
let prolog = null;
|
|
33
35
|
let attached = false;
|
|
34
36
|
try {
|
|
35
|
-
// Resolve KB path with priority:
|
|
36
|
-
// --kb-path > git branch --show-current > KIBI_BRANCH env > develop > main
|
|
37
37
|
let resolvedKbPath = "";
|
|
38
38
|
if (options.kbPath) {
|
|
39
39
|
resolvedKbPath = options.kbPath;
|
|
@@ -54,13 +54,32 @@ export async function checkCommand(options) {
|
|
|
54
54
|
// fallback to main if develop isn't present? keep path consistent
|
|
55
55
|
resolvedKbPath = path.join(process.cwd(), ".kb/branches", branch || "main");
|
|
56
56
|
}
|
|
57
|
-
// If --staged mode requested, run staged-symbol traceability gate.
|
|
58
|
-
// We skip creating the main prolog session entirely in this path.
|
|
59
57
|
if (options.staged) {
|
|
60
58
|
const minLinks = options.minLinks ? Number(options.minLinks) : 1;
|
|
61
59
|
let tempCtx = null;
|
|
62
60
|
try {
|
|
63
|
-
|
|
61
|
+
const config = loadConfig(process.cwd());
|
|
62
|
+
const manifestLookup = new Map();
|
|
63
|
+
const { manifestFiles } = await discoverSourceFiles(process.cwd(), config.paths);
|
|
64
|
+
for (const manifestPath of manifestFiles) {
|
|
65
|
+
try {
|
|
66
|
+
const entries = extractFromManifest(manifestPath);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
// Prefer the per-symbol sourceFile; fall back to entity.source or manifest path
|
|
69
|
+
const sourceFile = entry.sourceFile || entry.entity.source || manifestPath;
|
|
70
|
+
const key = `${sourceFile}:${entry.entity.title}`;
|
|
71
|
+
// Extract requirement links (implements relationships to REQ-*)
|
|
72
|
+
const links = entry.relationships
|
|
73
|
+
.filter((r) => r.type === "implements" &&
|
|
74
|
+
r.to.match(/^[A-Z][A-Z0-9\-_]*$/))
|
|
75
|
+
.map((r) => r.to);
|
|
76
|
+
manifestLookup.set(key, { id: entry.entity.id, links });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore manifest parsing errors
|
|
81
|
+
}
|
|
82
|
+
}
|
|
64
83
|
const stagedFiles = getStagedFiles();
|
|
65
84
|
if (!stagedFiles || stagedFiles.length === 0) {
|
|
66
85
|
console.log("No staged files found.");
|
|
@@ -89,7 +108,7 @@ export async function checkCommand(options) {
|
|
|
89
108
|
const allSymbols = [];
|
|
90
109
|
for (const f of codeFiles) {
|
|
91
110
|
try {
|
|
92
|
-
const symbols = extractSymbolsFromStagedFile(f);
|
|
111
|
+
const symbols = extractSymbolsFromStagedFile(f, manifestLookup);
|
|
93
112
|
if (symbols?.length) {
|
|
94
113
|
allSymbols.push(...symbols);
|
|
95
114
|
}
|
|
@@ -108,12 +127,10 @@ export async function checkCommand(options) {
|
|
|
108
127
|
}
|
|
109
128
|
// Create temp KB
|
|
110
129
|
tempCtx = await createTempKb(resolvedKbPath);
|
|
111
|
-
// Write overlay facts THEN consult so Prolog sees the changed_symbol facts
|
|
112
130
|
const overlayFacts = createOverlayFacts(allSymbols);
|
|
113
131
|
const fs = await import("node:fs/promises");
|
|
114
132
|
await fs.writeFile(tempCtx.overlayPath, overlayFacts, "utf8");
|
|
115
133
|
await consultOverlay(tempCtx);
|
|
116
|
-
// Validate staged symbols using the temp KB prolog session
|
|
117
134
|
const violationsRaw = await validateStagedSymbols({
|
|
118
135
|
minLinks,
|
|
119
136
|
prolog: tempCtx.prolog,
|
|
@@ -153,13 +170,11 @@ export async function checkCommand(options) {
|
|
|
153
170
|
}
|
|
154
171
|
attached = true;
|
|
155
172
|
const violations = [];
|
|
156
|
-
// Load config to get rule enablement settings
|
|
157
173
|
const config = loadConfig(process.cwd());
|
|
158
174
|
const checksConfig = config.checks ?? {
|
|
159
175
|
rules: Object.fromEntries(RULES.map((r) => [r.name, true])),
|
|
160
176
|
symbolTraceability: { requireAdr: false },
|
|
161
177
|
};
|
|
162
|
-
// Get effective rules based on config and CLI --rules filter
|
|
163
178
|
const effectiveRules = getEffectiveRules(checksConfig.rules, options.rules);
|
|
164
179
|
// Helper to conditionally run a check by name
|
|
165
180
|
async function runCheck(name, fn, ...args) {
|
|
@@ -250,7 +265,6 @@ export async function checkCommand(options) {
|
|
|
250
265
|
}
|
|
251
266
|
async function checkMustPriorityCoverage(prolog) {
|
|
252
267
|
const violations = [];
|
|
253
|
-
// Find all must-priority requirements
|
|
254
268
|
const mustReqs = await findMustPriorityReqs(prolog);
|
|
255
269
|
for (const reqId of mustReqs) {
|
|
256
270
|
const entityResult = await prolog.query(`kb_entity('${reqId}', req, Props)`);
|
|
@@ -324,9 +338,7 @@ async function getAllEntityIds(prolog, type) {
|
|
|
324
338
|
}
|
|
325
339
|
async function checkNoDanglingRefs(prolog) {
|
|
326
340
|
const violations = [];
|
|
327
|
-
// Get all entity IDs once
|
|
328
341
|
const allEntityIds = new Set(await getAllEntityIds(prolog));
|
|
329
|
-
// Get all relationships by querying all known relationship types
|
|
330
342
|
const relTypes = [
|
|
331
343
|
"depends_on",
|
|
332
344
|
"verified_by",
|
|
@@ -379,7 +391,6 @@ async function checkNoDanglingRefs(prolog) {
|
|
|
379
391
|
}
|
|
380
392
|
async function checkNoCycles(prolog) {
|
|
381
393
|
const violations = [];
|
|
382
|
-
// Get all depends_on relationships
|
|
383
394
|
const depsResult = await prolog.query("findall([From,To], kb_relationship(depends_on, From, To), Deps)");
|
|
384
395
|
if (!depsResult.success || !depsResult.bindings.Deps) {
|
|
385
396
|
return violations;
|
|
@@ -393,7 +404,6 @@ async function checkNoCycles(prolog) {
|
|
|
393
404
|
if (!content) {
|
|
394
405
|
return violations;
|
|
395
406
|
}
|
|
396
|
-
// Build adjacency map
|
|
397
407
|
const graph = new Map();
|
|
398
408
|
const depMatches = content.matchAll(/\[([^,]+),([^\]]+)\]/g);
|
|
399
409
|
for (const depMatch of depMatches) {
|
|
@@ -407,7 +417,6 @@ async function checkNoCycles(prolog) {
|
|
|
407
417
|
fromList.push(to);
|
|
408
418
|
}
|
|
409
419
|
}
|
|
410
|
-
// DFS to detect cycles
|
|
411
420
|
const visited = new Set();
|
|
412
421
|
const recStack = new Set();
|
|
413
422
|
function hasCycleDFS(node, path) {
|
|
@@ -429,7 +438,6 @@ async function checkNoCycles(prolog) {
|
|
|
429
438
|
recStack.delete(node);
|
|
430
439
|
return null;
|
|
431
440
|
}
|
|
432
|
-
// Check each node for cycles
|
|
433
441
|
for (const node of graph.keys()) {
|
|
434
442
|
if (!visited.has(node)) {
|
|
435
443
|
const cyclePath = hasCycleDFS(node, []);
|
|
@@ -472,15 +480,12 @@ async function checkRequiredFields(prolog, allEntityIds) {
|
|
|
472
480
|
for (const entityId of allEntityIds) {
|
|
473
481
|
const result = await prolog.query(`kb_entity('${entityId}', Type, Props)`);
|
|
474
482
|
if (result.success && result.bindings.Props) {
|
|
475
|
-
// Parse properties list: [key1=value1, key2=value2, ...]
|
|
476
483
|
const propsStr = result.bindings.Props;
|
|
477
484
|
const propKeys = new Set();
|
|
478
|
-
// Extract keys from Props
|
|
479
485
|
const keyMatches = propsStr.matchAll(/(\w+)\s*=/g);
|
|
480
486
|
for (const match of keyMatches) {
|
|
481
487
|
propKeys.add(match[1]);
|
|
482
488
|
}
|
|
483
|
-
// Check for missing required fields
|
|
484
489
|
for (const field of required) {
|
|
485
490
|
if (!propKeys.has(field)) {
|
|
486
491
|
violations.push({
|
|
@@ -515,7 +520,6 @@ async function checkDeprecatedAdrs(prolog) {
|
|
|
515
520
|
.split(",")
|
|
516
521
|
.map((id) => id.trim().replace(/^'|'$/g, ""));
|
|
517
522
|
for (const adrId of adrIds) {
|
|
518
|
-
// Get source for better error message
|
|
519
523
|
const entityResult = await prolog.query(`kb_entity('${adrId}', adr, Props)`);
|
|
520
524
|
let source = "";
|
|
521
525
|
if (entityResult.success && entityResult.bindings.Props) {
|
|
@@ -585,10 +589,8 @@ async function checkSymbolTraceability(prolog, requireAdr) {
|
|
|
585
589
|
if (!result.success || !result.bindings.Violations) {
|
|
586
590
|
return violations;
|
|
587
591
|
}
|
|
588
|
-
// Parse the violations from Prolog format
|
|
589
592
|
const violationsStr = result.bindings.Violations;
|
|
590
593
|
if (violationsStr && violationsStr !== "[]") {
|
|
591
|
-
// Parse each violation term
|
|
592
594
|
const violationRegex = /violation\(([^,]+),'?([^',]+)'?,([^,]+),([^,]+),'?([^']*)'?\)/g;
|
|
593
595
|
let match;
|
|
594
596
|
do {
|
|
@@ -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
|
+
}
|