kibi-cli 0.1.4 → 0.1.6
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 +5 -0
- package/dist/commands/aggregated-checks.d.ts +9 -0
- package/dist/commands/aggregated-checks.d.ts.map +1 -0
- package/dist/commands/aggregated-checks.js +80 -0
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +147 -10
- package/dist/commands/doctor.js +23 -2
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +4 -2
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +20 -7
- package/dist/commands/sync.js +1 -1
- package/dist/prolog.d.ts +1 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +75 -17
- package/dist/traceability/git-staged.d.ts +29 -0
- package/dist/traceability/git-staged.d.ts.map +1 -0
- package/dist/traceability/git-staged.js +141 -0
- package/dist/traceability/symbol-extract.d.ts +15 -0
- package/dist/traceability/symbol-extract.d.ts.map +1 -0
- package/dist/traceability/symbol-extract.js +228 -0
- package/dist/traceability/temp-kb.d.ts +14 -0
- package/dist/traceability/temp-kb.d.ts.map +1 -0
- package/dist/traceability/temp-kb.js +121 -0
- package/dist/traceability/validate.d.ts +17 -0
- package/dist/traceability/validate.d.ts.map +1 -0
- package/dist/traceability/validate.js +150 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,11 @@ program
|
|
|
87
87
|
.command("check")
|
|
88
88
|
.description("Check KB consistency and integrity")
|
|
89
89
|
.option("--fix", "Suggest fixes for violations")
|
|
90
|
+
.option("--kb-path <dir>", "Path to KB directory (overrides branch resolution)")
|
|
91
|
+
.option("--rules <csv>", "Comma-separated allowlist of rule names to run")
|
|
92
|
+
.option("--staged", "Run check only against staged changes (experimental)")
|
|
93
|
+
.option("--min-links <n>", "Minimum number of links required for symbol coverage", "1")
|
|
94
|
+
.option("--dry-run", "Do not modify files; only print what would happen")
|
|
90
95
|
.action(async (options) => {
|
|
91
96
|
await checkCommand(options);
|
|
92
97
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PrologProcess } from "../prolog.js";
|
|
2
|
+
import type { Violation } from "./check.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run all checks using the aggregated Prolog predicates.
|
|
5
|
+
* This makes a single Prolog call and parses JSON output, significantly
|
|
6
|
+
* faster than running individual checks with multiple round-trips.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runAggregatedChecks(prolog: PrologProcess, rulesAllowlist: Set<string> | null): Promise<Violation[]>;
|
|
9
|
+
//# sourceMappingURL=aggregated-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregated-checks.d.ts","sourceRoot":"","sources":["../../src/commands/aggregated-checks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,cAAc,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GACjC,OAAO,CAAC,SAAS,EAAE,CAAC,CAoFtB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveKbPlPath } from "../prolog.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run all checks using the aggregated Prolog predicates.
|
|
5
|
+
* This makes a single Prolog call and parses JSON output, significantly
|
|
6
|
+
* faster than running individual checks with multiple round-trips.
|
|
7
|
+
*/
|
|
8
|
+
export async function runAggregatedChecks(prolog, rulesAllowlist) {
|
|
9
|
+
const violations = [];
|
|
10
|
+
// Build the check goal based on allowlist
|
|
11
|
+
const checkGoals = [];
|
|
12
|
+
const supportedRules = [
|
|
13
|
+
"must-priority-coverage",
|
|
14
|
+
"symbol-coverage",
|
|
15
|
+
"no-dangling-refs",
|
|
16
|
+
"no-cycles",
|
|
17
|
+
"required-fields",
|
|
18
|
+
"deprecated-adr-no-successor",
|
|
19
|
+
"domain-contradictions",
|
|
20
|
+
];
|
|
21
|
+
for (const rule of supportedRules) {
|
|
22
|
+
if (!rulesAllowlist || rulesAllowlist.has(rule)) {
|
|
23
|
+
// Convert kebab-case to snake_case for Prolog
|
|
24
|
+
const prologRule = rule.replace(/-/g, "_");
|
|
25
|
+
checkGoals.push(`findall(V, checks:check_${prologRule}(V), ${prologRule}_violations)`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (checkGoals.length === 0) {
|
|
29
|
+
return violations;
|
|
30
|
+
}
|
|
31
|
+
const checksPlPath = path.join(path.dirname(resolveKbPlPath()), "checks.pl");
|
|
32
|
+
const checksPlPathEscaped = checksPlPath.replace(/'/g, "''");
|
|
33
|
+
const query = `(use_module('${checksPlPathEscaped}'), call(checks:check_all_json(JsonString)))`;
|
|
34
|
+
try {
|
|
35
|
+
const result = await prolog.query(query);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
console.warn("Aggregated checks query failed, falling back to individual checks");
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
// Parse the JSON from the binding
|
|
41
|
+
let violationsDict;
|
|
42
|
+
try {
|
|
43
|
+
const jsonString = result.bindings.JsonString;
|
|
44
|
+
if (jsonString) {
|
|
45
|
+
violationsDict = JSON.parse(jsonString);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new Error("No JSON string in binding");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (parseError) {
|
|
52
|
+
console.warn("Failed to parse violations JSON:", parseError);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
// Convert Prolog violation terms to Violation objects
|
|
56
|
+
for (const [ruleKey, ruleViolations] of Object.entries(violationsDict)) {
|
|
57
|
+
const rule = ruleKey.replace(/_/g, "-");
|
|
58
|
+
for (const v of ruleViolations) {
|
|
59
|
+
if (typeof v === "string") {
|
|
60
|
+
// Parse Prolog violation/5 term: violation(Rule, EntityId, Desc, Sugg, Source)
|
|
61
|
+
const match = v.match(/violation\(([^,]+),\s*'([^']+)'\s*,\s*"([^"]*)"\s*,\s*"([^"]*)"\s*,\s*"([^"]*)"\)/);
|
|
62
|
+
if (match) {
|
|
63
|
+
violations.push({
|
|
64
|
+
rule: rule,
|
|
65
|
+
entityId: match[2],
|
|
66
|
+
description: match[3],
|
|
67
|
+
suggestion: match[4] || undefined,
|
|
68
|
+
source: match[5] || undefined,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.warn("Error running aggregated checks:", error);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -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":"AAsDA,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;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAqMvE"}
|
package/dist/commands/check.js
CHANGED
|
@@ -44,26 +44,163 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import * as path from "node:path";
|
|
46
46
|
import { PrologProcess } from "../prolog.js";
|
|
47
|
+
import { getStagedFiles } from "../traceability/git-staged.js";
|
|
48
|
+
import { extractSymbolsFromStagedFile } from "../traceability/symbol-extract.js";
|
|
49
|
+
import { cleanupTempKb, consultOverlay, createOverlayFacts, createTempKb } from "../traceability/temp-kb.js";
|
|
50
|
+
import { formatViolations as formatStagedViolations, validateStagedSymbols } from "../traceability/validate.js";
|
|
51
|
+
import { getCurrentBranch } from "./init-helpers.js";
|
|
52
|
+
import { runAggregatedChecks } from "./aggregated-checks.js";
|
|
47
53
|
export async function checkCommand(options) {
|
|
48
54
|
try {
|
|
49
|
-
|
|
55
|
+
// Resolve KB path with priority:
|
|
56
|
+
// --kb-path > git branch --show-current > KIBI_BRANCH env > develop > main
|
|
57
|
+
let resolvedKbPath = "";
|
|
58
|
+
if (options.kbPath) {
|
|
59
|
+
resolvedKbPath = options.kbPath;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const envBranch = process.env.KIBI_BRANCH;
|
|
63
|
+
let branch = envBranch || undefined;
|
|
64
|
+
if (!branch) {
|
|
65
|
+
try {
|
|
66
|
+
branch = await getCurrentBranch(process.cwd());
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
branch = undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!branch)
|
|
73
|
+
branch = envBranch || "develop";
|
|
74
|
+
// fallback to main if develop isn't present? keep path consistent
|
|
75
|
+
resolvedKbPath = path.join(process.cwd(), ".kb/branches", branch || "main");
|
|
76
|
+
}
|
|
77
|
+
// If --staged mode requested, run staged-symbol traceability gate.
|
|
78
|
+
// We skip creating the main prolog session entirely in this path.
|
|
79
|
+
if (options.staged) {
|
|
80
|
+
const minLinks = options.minLinks ? Number(options.minLinks) : 1;
|
|
81
|
+
let tempCtx = null;
|
|
82
|
+
try {
|
|
83
|
+
// Get staged files
|
|
84
|
+
const stagedFiles = getStagedFiles();
|
|
85
|
+
if (!stagedFiles || stagedFiles.length === 0) {
|
|
86
|
+
console.log("No staged files found.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
// Extract symbols from staged files
|
|
90
|
+
const allSymbols = [];
|
|
91
|
+
for (const f of stagedFiles) {
|
|
92
|
+
try {
|
|
93
|
+
const symbols = extractSymbolsFromStagedFile(f);
|
|
94
|
+
if (symbols && symbols.length) {
|
|
95
|
+
allSymbols.push(...symbols);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.error(`Error extracting symbols from staged file ${f.path}: ${e instanceof Error ? e.message : String(e)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (allSymbols.length === 0) {
|
|
103
|
+
console.log("No exported symbols found in staged files.");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
// Create temp KB
|
|
107
|
+
tempCtx = await createTempKb(resolvedKbPath);
|
|
108
|
+
// Write overlay facts THEN consult so Prolog sees the changed_symbol facts
|
|
109
|
+
const overlayFacts = createOverlayFacts(allSymbols);
|
|
110
|
+
const fs = await import("node:fs/promises");
|
|
111
|
+
await fs.writeFile(tempCtx.overlayPath, overlayFacts, "utf8");
|
|
112
|
+
await consultOverlay(tempCtx);
|
|
113
|
+
// Validate staged symbols using the temp KB prolog session
|
|
114
|
+
const violationsRaw = await validateStagedSymbols({ minLinks, prolog: tempCtx.prolog });
|
|
115
|
+
const violationsFormatted = formatStagedViolations(violationsRaw);
|
|
116
|
+
if (violationsRaw && violationsRaw.length > 0) {
|
|
117
|
+
console.log(violationsFormatted);
|
|
118
|
+
await cleanupTempKb(tempCtx.tempDir);
|
|
119
|
+
if (options.dryRun) {
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
console.log("✓ No violations found in staged symbols.");
|
|
125
|
+
await cleanupTempKb(tempCtx.tempDir);
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(`Error running staged validation: ${err instanceof Error ? err.message : String(err)}`);
|
|
130
|
+
if (tempCtx) {
|
|
131
|
+
try {
|
|
132
|
+
await cleanupTempKb(tempCtx.tempDir);
|
|
133
|
+
}
|
|
134
|
+
catch { }
|
|
135
|
+
}
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const prolog = new PrologProcess({ timeout: 120000 });
|
|
50
140
|
await prolog.start();
|
|
51
|
-
const
|
|
52
|
-
const attachResult = await prolog.query(`kb_attach('${
|
|
141
|
+
const kbPathEscaped = resolvedKbPath.replace(/'/g, "''");
|
|
142
|
+
const attachResult = await prolog.query(`kb_attach('${kbPathEscaped}')`);
|
|
53
143
|
if (!attachResult.success) {
|
|
54
144
|
await prolog.terminate();
|
|
55
145
|
console.error(`Error: Failed to attach KB: ${attachResult.error}`);
|
|
56
146
|
process.exit(1);
|
|
57
147
|
}
|
|
58
148
|
const violations = [];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
149
|
+
// Parse rules allowlist if provided
|
|
150
|
+
let rulesAllowlist = null;
|
|
151
|
+
if (options.rules) {
|
|
152
|
+
const parts = options.rules
|
|
153
|
+
.split(",")
|
|
154
|
+
.map((s) => s.trim())
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
rulesAllowlist = new Set(parts);
|
|
157
|
+
}
|
|
158
|
+
// Helper to conditionally run a check by name
|
|
159
|
+
async function runCheck(name, fn, ...args) {
|
|
160
|
+
if (rulesAllowlist?.has(name) === false)
|
|
161
|
+
return;
|
|
162
|
+
const res = await fn(prolog, ...args);
|
|
163
|
+
if (res && res.length)
|
|
164
|
+
violations.push(...res);
|
|
165
|
+
}
|
|
166
|
+
// Use aggregated checks (single Prolog call) when possible for better performance
|
|
167
|
+
// This is significantly faster in Bun/Docker environments where one-shot mode
|
|
168
|
+
// spawns a new Prolog process for each query
|
|
169
|
+
const supportedRules = [
|
|
170
|
+
'must-priority-coverage',
|
|
171
|
+
'symbol-coverage',
|
|
172
|
+
'no-dangling-refs',
|
|
173
|
+
'no-cycles',
|
|
174
|
+
'required-fields',
|
|
175
|
+
'deprecated-adr-no-successor',
|
|
176
|
+
'domain-contradictions',
|
|
177
|
+
];
|
|
178
|
+
const canUseAggregated = !rulesAllowlist ||
|
|
179
|
+
Array.from(rulesAllowlist).every(r => supportedRules.includes(r));
|
|
180
|
+
if (canUseAggregated) {
|
|
181
|
+
// Fast path: single Prolog call returning all violations
|
|
182
|
+
const aggregatedViolations = await runAggregatedChecks(prolog, rulesAllowlist);
|
|
183
|
+
violations.push(...aggregatedViolations);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Legacy path: individual checks for backward compatibility
|
|
187
|
+
await runCheck("must-priority-coverage", checkMustPriorityCoverage);
|
|
188
|
+
await runCheck("symbol-coverage", checkSymbolCoverage);
|
|
189
|
+
await runCheck("no-dangling-refs", checkNoDanglingRefs);
|
|
190
|
+
await runCheck("no-cycles", checkNoCycles);
|
|
191
|
+
const allEntityIds = await getAllEntityIds(prolog);
|
|
192
|
+
await runCheck("required-fields", checkRequiredFields, allEntityIds);
|
|
193
|
+
await runCheck("deprecated-adr-no-successor", checkDeprecatedAdrs);
|
|
194
|
+
await runCheck("domain-contradictions", checkDomainContradictions);
|
|
195
|
+
}
|
|
196
|
+
await runCheck("must-priority-coverage", checkMustPriorityCoverage);
|
|
197
|
+
await runCheck("symbol-coverage", checkSymbolCoverage);
|
|
198
|
+
await runCheck("no-dangling-refs", checkNoDanglingRefs);
|
|
199
|
+
await runCheck("no-cycles", checkNoCycles);
|
|
63
200
|
const allEntityIds = await getAllEntityIds(prolog);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
201
|
+
await runCheck("required-fields", checkRequiredFields, allEntityIds);
|
|
202
|
+
await runCheck("deprecated-adr-no-successor", checkDeprecatedAdrs);
|
|
203
|
+
await runCheck("domain-contradictions", checkDomainContradictions);
|
|
67
204
|
await prolog.query("kb_detach");
|
|
68
205
|
await prolog.terminate();
|
|
69
206
|
if (violations.length === 0) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -247,10 +247,30 @@ function checkPreCommitHook() {
|
|
|
247
247
|
try {
|
|
248
248
|
const preCommitStats = statSync(preCommitPath);
|
|
249
249
|
const preCommitExecutable = (preCommitStats.mode & 0o111) !== 0;
|
|
250
|
+
// Read hook content to determine whether it's using the new staged check
|
|
251
|
+
const content = readFileSync(preCommitPath, "utf-8");
|
|
252
|
+
const usesKibi = content.includes("kibi check");
|
|
253
|
+
const usesStaged = content.includes("kibi check --staged");
|
|
254
|
+
if (!usesKibi) {
|
|
255
|
+
// Fail if hook doesn't invoke kibi at all
|
|
256
|
+
return {
|
|
257
|
+
passed: false,
|
|
258
|
+
message: "pre-commit hook installed but does not invoke kibi",
|
|
259
|
+
remediation: "Run: kibi init --hooks to install recommended hooks",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
250
262
|
if (preCommitExecutable) {
|
|
263
|
+
if (usesStaged) {
|
|
264
|
+
return {
|
|
265
|
+
passed: true,
|
|
266
|
+
message: "Installed and executable (uses 'kibi check --staged')",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Warn but pass if using legacy kibi check without --staged
|
|
251
270
|
return {
|
|
252
271
|
passed: true,
|
|
253
|
-
message: "Installed and executable",
|
|
272
|
+
message: "Installed and executable (uses legacy 'kibi check' — consider running 'kibi init' to update hooks to use '--staged')",
|
|
273
|
+
remediation: "Run: kibi init --hooks to update git hooks to the latest template",
|
|
254
274
|
};
|
|
255
275
|
}
|
|
256
276
|
return {
|
|
@@ -262,7 +282,8 @@ function checkPreCommitHook() {
|
|
|
262
282
|
catch (error) {
|
|
263
283
|
return {
|
|
264
284
|
passed: false,
|
|
265
|
-
message: "Unable to check hook permissions",
|
|
285
|
+
message: "Unable to check hook permissions or read content",
|
|
286
|
+
remediation: "Run: kibi init --hooks",
|
|
266
287
|
};
|
|
267
288
|
}
|
|
268
289
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init-helpers.d.ts","sourceRoot":"","sources":["../../src/commands/init-helpers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init-helpers.d.ts","sourceRoot":"","sources":["../../src/commands/init-helpers.ts"],"names":[],"mappings":"AAkFA,wBAAsB,gBAAgB,CACpC,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAajD;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBnE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAapD"}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
fi
|
|
43
43
|
done
|
|
44
44
|
*/
|
|
45
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
45
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import fg from "fast-glob";
|
|
48
48
|
const POST_CHECKOUT_HOOK = `#!/bin/sh
|
|
@@ -53,7 +53,7 @@ kibi sync
|
|
|
53
53
|
`;
|
|
54
54
|
const PRE_COMMIT_HOOK = `#!/bin/sh
|
|
55
55
|
set -e
|
|
56
|
-
kibi check
|
|
56
|
+
kibi check --staged
|
|
57
57
|
`;
|
|
58
58
|
const DEFAULT_CONFIG = {
|
|
59
59
|
paths: {
|
|
@@ -136,6 +136,8 @@ ${content}`, {
|
|
|
136
136
|
writeFileSync(hookPath, `#!/bin/sh
|
|
137
137
|
${content}`, { mode: 0o755 });
|
|
138
138
|
}
|
|
139
|
+
// Explicitly ensure hook is executable (mode option can be inconsistent in Docker)
|
|
140
|
+
chmodSync(hookPath, 0o755);
|
|
139
141
|
}
|
|
140
142
|
export function installGitHooks(gitDir) {
|
|
141
143
|
const hooksDir = path.join(gitDir, "hooks");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/commands/query.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/commands/query.ts"],"names":[],"mappings":"AAoDA,UAAU,YAAY;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAwKf"}
|
package/dist/commands/query.js
CHANGED
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
import * as path from "node:path";
|
|
46
46
|
import Table from "cli-table3";
|
|
47
47
|
import { PrologProcess } from "../prolog.js";
|
|
48
|
+
import relationshipSchema from "../public/schemas/relationship.js";
|
|
49
|
+
const REL_TYPES = relationshipSchema.properties.type.enum;
|
|
48
50
|
export async function queryCommand(type, options) {
|
|
49
51
|
try {
|
|
50
|
-
const prolog = new PrologProcess();
|
|
52
|
+
const prolog = new PrologProcess({ timeout: 120000 });
|
|
51
53
|
await prolog.start();
|
|
52
54
|
await prolog.query("set_prolog_flag(answer_write_options, [max_depth(0), spacing(next_argument)])");
|
|
53
55
|
let currentBranch = "main";
|
|
@@ -73,15 +75,26 @@ export async function queryCommand(type, options) {
|
|
|
73
75
|
let results = [];
|
|
74
76
|
// Query relationships mode
|
|
75
77
|
if (options.relationships) {
|
|
76
|
-
const
|
|
78
|
+
const fromId = String(options.relationships);
|
|
79
|
+
const safeFromId = fromId.replace(/'/g, "''");
|
|
80
|
+
// Query all relationship types for the given source ID
|
|
81
|
+
const goal = `findall([Type,From,To], (From='${safeFromId}', kb_relationship(Type, From, To)), Results)`;
|
|
77
82
|
const queryResult = await prolog.query(goal);
|
|
78
83
|
if (queryResult.success && queryResult.bindings.Results) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const rows = parseListOfLists(queryResult.bindings.Results);
|
|
85
|
+
const parsed = rows
|
|
86
|
+
.filter((r) => r.length >= 3)
|
|
87
|
+
.map((r) => ({
|
|
88
|
+
type: parsePrologValue(r[0]),
|
|
89
|
+
from: parsePrologValue(r[1]),
|
|
90
|
+
to: parsePrologValue(r[2]),
|
|
84
91
|
}));
|
|
92
|
+
results = parsed.filter((rel) => rel &&
|
|
93
|
+
typeof rel.type === "string" &&
|
|
94
|
+
typeof rel.from === "string" &&
|
|
95
|
+
typeof rel.to === "string" &&
|
|
96
|
+
rel.from === fromId &&
|
|
97
|
+
REL_TYPES.includes(rel.type));
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
// Query entities mode
|
package/dist/commands/sync.js
CHANGED
|
@@ -327,7 +327,7 @@ export async function syncCommand(options = {}) {
|
|
|
327
327
|
process.exit(0);
|
|
328
328
|
}
|
|
329
329
|
// Connect to KB
|
|
330
|
-
const prolog = new PrologProcess();
|
|
330
|
+
const prolog = new PrologProcess({ timeout: 120000 });
|
|
331
331
|
await prolog.start();
|
|
332
332
|
const kbPath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
|
|
333
333
|
const mainPath = path.join(process.cwd(), ".kb/branches/main");
|
package/dist/prolog.d.ts
CHANGED
package/dist/prolog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prolog.d.ts","sourceRoot":"","sources":["../src/prolog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prolog.d.ts","sourceRoot":"","sources":["../src/prolog.ts"],"names":[],"mappings":"AAuDA,wBAAgB,eAAe,IAAI,MAAM,CAsCxC;AACD,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,cAAc,CACyC;IAC/D,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,aAAa,CAA6B;gBAEtC,OAAO,GAAE,aAAkB;IAKjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAoCd,YAAY;IAyCpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAsH1D,eAAe,IAAI,IAAI;IAIvB,OAAO,CAAC,eAAe;YAYT,YAAY;IA0B1B,OAAO,CAAC,WAAW;IA8EnB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,cAAc;IA8BtB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAIV,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBjC"}
|
package/dist/prolog.js
CHANGED
|
@@ -49,23 +49,32 @@ import path from "node:path";
|
|
|
49
49
|
import { fileURLToPath } from "node:url";
|
|
50
50
|
const importMetaDir = path.dirname(fileURLToPath(import.meta.url));
|
|
51
51
|
const require = createRequire(import.meta.url);
|
|
52
|
-
function resolveKbPlPath() {
|
|
53
|
-
|
|
52
|
+
export function resolveKbPlPath() {
|
|
53
|
+
const overrideKbPath = process.env.KIBI_KB_PL_PATH;
|
|
54
|
+
if (overrideKbPath && existsSync(overrideKbPath)) {
|
|
55
|
+
return overrideKbPath;
|
|
56
|
+
}
|
|
54
57
|
try {
|
|
55
|
-
const
|
|
56
|
-
const coreDir = path.dirname(corePkgJson);
|
|
57
|
-
const installedKbPl = path.join(coreDir, "src", "kb.pl");
|
|
58
|
+
const installedKbPl = require.resolve("kibi-core/src/kb.pl");
|
|
58
59
|
if (existsSync(installedKbPl))
|
|
59
60
|
return installedKbPl;
|
|
60
61
|
}
|
|
61
|
-
catch {
|
|
62
|
-
|
|
62
|
+
catch { }
|
|
63
|
+
const startDirs = [importMetaDir, process.cwd()];
|
|
64
|
+
for (const startDir of startDirs) {
|
|
65
|
+
let currentDir = path.resolve(startDir);
|
|
66
|
+
while (true) {
|
|
67
|
+
const candidate = path.join(currentDir, "packages", "core", "src", "kb.pl");
|
|
68
|
+
if (existsSync(candidate)) {
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
const parentDir = path.dirname(currentDir);
|
|
72
|
+
if (parentDir === currentDir) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
currentDir = parentDir;
|
|
76
|
+
}
|
|
63
77
|
}
|
|
64
|
-
// 2) Dev fallback for monorepo checkout
|
|
65
|
-
const devKbPl = path.resolve(importMetaDir, "../../core/src/kb.pl");
|
|
66
|
-
if (existsSync(devKbPl))
|
|
67
|
-
return devKbPl;
|
|
68
|
-
// 3) Hard fail with actionable message
|
|
69
78
|
throw new Error("Unable to resolve kb.pl. Expected kibi-core to be installed (node_modules) " +
|
|
70
79
|
"or to be running inside the monorepo checkout.");
|
|
71
80
|
}
|
|
@@ -111,7 +120,27 @@ export class PrologProcess {
|
|
|
111
120
|
await this.waitForReady();
|
|
112
121
|
}
|
|
113
122
|
async waitForReady() {
|
|
114
|
-
|
|
123
|
+
// Wait for Prolog to initialize and detect startup failures explicitly.
|
|
124
|
+
const start = Date.now();
|
|
125
|
+
const maxStartWait = 2000; // ms
|
|
126
|
+
while (Date.now() - start < maxStartWait) {
|
|
127
|
+
// If process exited or was killed, surface the error buffer.
|
|
128
|
+
if (!this.process || this.process.killed) {
|
|
129
|
+
throw new Error(`Prolog process terminated unexpectedly during startup: ${this.translateError(this.errorBuffer)}`);
|
|
130
|
+
}
|
|
131
|
+
// If stderr contains an ERROR, fail fast with translated message.
|
|
132
|
+
if (this.errorBuffer.includes("ERROR")) {
|
|
133
|
+
throw new Error(`Failed to load kb module: ${this.translateError(this.errorBuffer)}`);
|
|
134
|
+
}
|
|
135
|
+
// If stdout or stderr shows any output, assume ready.
|
|
136
|
+
if (this.outputBuffer.length > 0 || this.errorBuffer.length > 0) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
// brief pause
|
|
140
|
+
// eslint-disable-next-line no-await-in-loop
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
142
|
+
}
|
|
143
|
+
// Final sanity check
|
|
115
144
|
if (this.errorBuffer.includes("ERROR")) {
|
|
116
145
|
throw new Error(`Failed to load kb module: ${this.translateError(this.errorBuffer)}`);
|
|
117
146
|
}
|
|
@@ -149,13 +178,30 @@ export class PrologProcess {
|
|
|
149
178
|
this.errorBuffer = "";
|
|
150
179
|
this.process.stdin.write(`${goal}.
|
|
151
180
|
`);
|
|
181
|
+
const debug = !!process.env.KIBI_PROLOG_DEBUG;
|
|
182
|
+
const normalizedGoal = isSingleGoal
|
|
183
|
+
? this.normalizeGoal(goal)
|
|
184
|
+
: undefined;
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
if (debug && normalizedGoal)
|
|
187
|
+
console.error(`[prolog debug] start query: ${normalizedGoal}`);
|
|
152
188
|
return new Promise((resolve, reject) => {
|
|
153
189
|
const timeoutId = setTimeout(() => {
|
|
154
|
-
|
|
190
|
+
const msg = `Query timeout after ${this.timeout / 1000}s`;
|
|
191
|
+
if (debug) {
|
|
192
|
+
const tailOut = this.outputBuffer.slice(-2048);
|
|
193
|
+
const tailErr = this.errorBuffer.slice(-2048);
|
|
194
|
+
console.error(`[prolog debug] timeout: ${msg}`);
|
|
195
|
+
console.error(`[prolog debug] last stdout: ---\n${tailOut}\n---`);
|
|
196
|
+
console.error(`[prolog debug] last stderr: ---\n${tailErr}\n---`);
|
|
197
|
+
}
|
|
198
|
+
reject(new Error(msg));
|
|
155
199
|
}, this.timeout);
|
|
156
200
|
const checkResult = () => {
|
|
157
201
|
if (this.errorBuffer.length > 0 && this.errorBuffer.includes("ERROR")) {
|
|
158
202
|
clearTimeout(timeoutId);
|
|
203
|
+
if (debug && normalizedGoal)
|
|
204
|
+
console.error(`[prolog debug] query error: ${normalizedGoal} error=${this.errorBuffer.split("\n")[0]}`);
|
|
159
205
|
resolve({
|
|
160
206
|
success: false,
|
|
161
207
|
bindings: {},
|
|
@@ -163,7 +209,9 @@ export class PrologProcess {
|
|
|
163
209
|
});
|
|
164
210
|
}
|
|
165
211
|
else if (this.outputBuffer.includes("true.") ||
|
|
166
|
-
this.outputBuffer.match(/^[A-Z_][A-Za-z0-9_]*\s*=\s*.+\./m)
|
|
212
|
+
this.outputBuffer.match(/^[A-Z_][A-Za-z0-9_]*\s*=\s*.+\./m) ||
|
|
213
|
+
// Match multi-line output ending with ] (Prolog list/term output without trailing period)
|
|
214
|
+
this.outputBuffer.match(/\]\s*$/m)) {
|
|
167
215
|
clearTimeout(timeoutId);
|
|
168
216
|
const result = {
|
|
169
217
|
success: true,
|
|
@@ -172,11 +220,20 @@ export class PrologProcess {
|
|
|
172
220
|
if (cacheable) {
|
|
173
221
|
this.cache.set(goalKey, result);
|
|
174
222
|
}
|
|
223
|
+
if (debug && normalizedGoal) {
|
|
224
|
+
console.error(`[prolog debug] query success: ${normalizedGoal} elapsed=${(Date.now() - start) / 1000}s`);
|
|
225
|
+
}
|
|
175
226
|
resolve(result);
|
|
227
|
+
// Send newline to exit Prolog's interactive prompt
|
|
228
|
+
if (this.process?.stdin) {
|
|
229
|
+
this.process.stdin.write("\n");
|
|
230
|
+
}
|
|
176
231
|
}
|
|
177
232
|
else if (this.outputBuffer.includes("false.") ||
|
|
178
233
|
this.outputBuffer.includes("fail.")) {
|
|
179
234
|
clearTimeout(timeoutId);
|
|
235
|
+
if (debug && normalizedGoal)
|
|
236
|
+
console.error(`[prolog debug] query failed (false): ${normalizedGoal}`);
|
|
180
237
|
resolve({
|
|
181
238
|
success: false,
|
|
182
239
|
bindings: {},
|
|
@@ -255,7 +312,7 @@ export class PrologProcess {
|
|
|
255
312
|
(result.error.message.includes("timed out") ||
|
|
256
313
|
// Bun/Node differ here; keep a conservative timeout detection.
|
|
257
314
|
result.error.message.includes("ETIMEDOUT"))) {
|
|
258
|
-
throw new Error(
|
|
315
|
+
throw new Error(`Query timeout after ${this.timeout / 1000}s`);
|
|
259
316
|
}
|
|
260
317
|
const stdout = result.stdout ?? "";
|
|
261
318
|
const stderr = result.stderr ?? "";
|
|
@@ -310,7 +367,7 @@ export class PrologProcess {
|
|
|
310
367
|
return "Invalid query syntax";
|
|
311
368
|
}
|
|
312
369
|
if (errorText.includes("timeout_error")) {
|
|
313
|
-
return
|
|
370
|
+
return `Operation exceeded ${this.timeout / 1000}s timeout`;
|
|
314
371
|
}
|
|
315
372
|
const simpleError = errorText
|
|
316
373
|
.replace(/ERROR:\s*/g, "")
|
|
@@ -348,3 +405,4 @@ export class PrologProcess {
|
|
|
348
405
|
}
|
|
349
406
|
}
|
|
350
407
|
}
|
|
408
|
+
// FIX_VERSION_2024_03_06
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Status = "A" | "M" | "R" | "D";
|
|
2
|
+
export interface HunkRange {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface StagedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
status: Status;
|
|
9
|
+
oldPath?: string;
|
|
10
|
+
hunkRanges: HunkRange[];
|
|
11
|
+
content?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse null-separated name-status output from git
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseNameStatusNull(input: string): Array<{
|
|
17
|
+
status: string;
|
|
18
|
+
parts: string[];
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Parse unified diff hunks (new-file coordinates) from git diff output
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseHunksFromDiff(diffText: string, isNewFile?: boolean): HunkRange[];
|
|
24
|
+
/**
|
|
25
|
+
* Get staged files with statuses, hunks and content.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getStagedFiles(): StagedFile[];
|
|
28
|
+
export default getStagedFiles;
|
|
29
|
+
//# sourceMappingURL=git-staged.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-staged.d.ts","sourceRoot":"","sources":["../../src/traceability/git-staged.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAS5C;AAoBD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,UAAQ,GAChB,SAAS,EAAE,CAoBb;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,UAAU,EAAE,CAmF7C;AAED,eAAe,cAAc,CAAC"}
|