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 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
+ }
@@ -1,5 +1,10 @@
1
1
  export interface CheckOptions {
2
2
  fix?: boolean;
3
+ kbPath?: string;
4
+ rules?: string;
5
+ staged?: boolean;
6
+ minLinks?: string | number;
7
+ dryRun?: boolean;
3
8
  }
4
9
  export interface Violation {
5
10
  rule: string;
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AA+CA,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;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,CAkDvE"}
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"}
@@ -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
- const prolog = new PrologProcess();
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 kbPath = path.join(process.cwd(), ".kb/branches/main");
52
- const attachResult = await prolog.query(`kb_attach('${kbPath}')`);
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
- violations.push(...(await checkMustPriorityCoverage(prolog)));
60
- violations.push(...(await checkSymbolCoverage(prolog)));
61
- violations.push(...(await checkNoDanglingRefs(prolog)));
62
- violations.push(...(await checkNoCycles(prolog)));
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
- violations.push(...(await checkRequiredFields(prolog, allEntityIds)));
65
- violations.push(...(await checkDeprecatedAdrs(prolog)));
66
- violations.push(...(await checkDomainContradictions(prolog)));
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) {
@@ -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":"AAiFA,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,CAqBnE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAapD"}
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":"AAiDA,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,CA2Jf"}
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"}
@@ -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 goal = `findall([Type,From,To], kb_relationship(Type, ${options.relationships}, To), Results)`;
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 relationshipsData = parseListOfLists(queryResult.bindings.Results);
80
- results = relationshipsData.map((rel) => ({
81
- type: rel[0],
82
- from: options.relationships,
83
- to: rel[1],
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
@@ -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
@@ -1,3 +1,4 @@
1
+ export declare function resolveKbPlPath(): string;
1
2
  export interface PrologOptions {
2
3
  swiplPath?: string;
3
4
  timeout?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"prolog.d.ts","sourceRoot":"","sources":["../src/prolog.ts"],"names":[],"mappings":"AA4EA,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;IAapB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAmF1D,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"}
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
- // 1) Prefer installed dependency (works after publish)
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 corePkgJson = require.resolve("kibi-core/package.json");
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
- // ignore; fall back to dev layout
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
- await new Promise((resolve) => setTimeout(resolve, 500));
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
- reject(new Error("Query timeout after 30s"));
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("Query timeout after 30s");
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 "Operation exceeded 30s timeout";
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"}