kibi-runtime 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine-daemon.js +2402 -0
- package/dist/engine.d.ts +65 -0
- package/dist/engine.js +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +279965 -0
- package/dist/skill-operations.d.ts +69 -0
- package/dist/skill-operations.js +105 -0
- package/dist/skill-system/errors.d.ts +18 -0
- package/dist/skill-system/errors.js +36 -0
- package/dist/skill-system/loader.d.ts +8 -0
- package/dist/skill-system/loader.js +81 -0
- package/dist/skill-system/paths.d.ts +5 -0
- package/dist/skill-system/paths.js +25 -0
- package/dist/skill-system/types.d.ts +14 -0
- package/dist/skill-system/types.js +1 -0
- package/dist/skill-system/validation.d.ts +9 -0
- package/dist/skill-system/validation.js +119 -0
- package/dist/skills/kibi-bootstrap/SKILL.md +58 -0
- package/dist/skills/kibi-bootstrap/resources/bootstrap.md +11 -0
- package/dist/skills/kibi-bootstrap/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-bootstrap/resources/operation-access.md +5 -0
- package/dist/skills/kibi-bootstrap/resources/source-authoring.md +8 -0
- package/dist/skills/kibi-freshness/SKILL.md +74 -0
- package/dist/skills/kibi-freshness/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-freshness/resources/operation-access.md +6 -0
- package/dist/skills/kibi-freshness/resources/source-authoring.md +7 -0
- package/dist/skills/kibi-traceability/SKILL.md +60 -0
- package/dist/skills/kibi-traceability/resources/branch-lifecycle.md +7 -0
- package/dist/skills/kibi-traceability/resources/operation-access.md +5 -0
- package/dist/skills/kibi-traceability/resources/source-authoring.md +6 -0
- package/dist/skills/kibi-traceability/resources/traceability.md +8 -0
- package/dist/skills/kibi-usage/SKILL.md +152 -0
- package/dist/skills/kibi-usage/resources/branch-lifecycle.md +31 -0
- package/dist/skills/kibi-usage/resources/fact-lanes.md +235 -0
- package/dist/skills/kibi-usage/resources/kb-improvement.md +38 -0
- package/dist/skills/kibi-usage/resources/logic-ir.md +43 -0
- package/dist/skills/kibi-usage/resources/operation-access.md +42 -0
- package/dist/skills/kibi-usage/resources/relationship-directions.md +119 -0
- package/dist/skills/kibi-usage/resources/source-authoring.md +31 -0
- package/dist/skills/kibi-usage/resources/ui-requirements.md +96 -0
- package/dist/skills/kibi-usage/resources/workflows.md +150 -0
- package/dist/skills.d.ts +3 -0
- package/dist/skills.js +2 -0
- package/package.json +37 -0
|
@@ -0,0 +1,2402 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../cli/src/engine-daemon.ts
|
|
4
|
+
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// ../cli/src/engine.ts
|
|
7
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
8
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
closeSync,
|
|
12
|
+
existsSync as existsSync3,
|
|
13
|
+
fsyncSync,
|
|
14
|
+
mkdirSync as mkdirSync2,
|
|
15
|
+
openSync,
|
|
16
|
+
readFileSync as readFileSync2,
|
|
17
|
+
readdirSync as readdirSync2,
|
|
18
|
+
realpathSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
statSync as statSync2,
|
|
22
|
+
unlinkSync,
|
|
23
|
+
writeFileSync as writeFileSync2
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import * as net from "node:net";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import * as path4 from "node:path";
|
|
28
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29
|
+
|
|
30
|
+
// ../cli/src/prolog.ts
|
|
31
|
+
import { spawn } from "node:child_process";
|
|
32
|
+
import { existsSync } from "node:fs";
|
|
33
|
+
import { createRequire } from "node:module";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
|
|
37
|
+
// ../cli/src/env.ts
|
|
38
|
+
function getEnvValue(key) {
|
|
39
|
+
const value = process.env[key];
|
|
40
|
+
return typeof value === "string" ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
function getKbPlPathOverride() {
|
|
43
|
+
return getEnvValue("KIBI_KB_PL_PATH");
|
|
44
|
+
}
|
|
45
|
+
function isPrologDebugEnabled() {
|
|
46
|
+
return Boolean(getEnvValue("KIBI_PROLOG_DEBUG"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ../cli/src/prolog.ts
|
|
50
|
+
var importMetaDir = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
var PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
52
|
+
var PROLOG_OUTPUT_OVERFLOW_ERROR = "Query exceeded bounded Prolog output capacity (ENOBUFS); narrow the operation or reduce stored entity size";
|
|
53
|
+
var INTERACTIVE_QUERY_FRAME_END = "__KIBI_QUERY_FRAME_END__";
|
|
54
|
+
var require2 = createRequire(import.meta.url);
|
|
55
|
+
function packageVersion(packagePath) {
|
|
56
|
+
try {
|
|
57
|
+
const packageJson = require2(packagePath);
|
|
58
|
+
return typeof packageJson.version === "string" ? packageJson.version : "unknown";
|
|
59
|
+
} catch {
|
|
60
|
+
return "unknown";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var KIBI_PACKAGE_VERSIONS = [
|
|
64
|
+
`kibi-cli@${packageVersion(path.join(importMetaDir, "..", "package.json"))}`,
|
|
65
|
+
`kibi-core@${packageVersion(path.join(importMetaDir, "..", "..", "core", "package.json"))}`,
|
|
66
|
+
`kibi-mcp@${packageVersion(path.join(importMetaDir, "..", "..", "mcp", "package.json"))}`
|
|
67
|
+
].join(",");
|
|
68
|
+
function resolveKbPlPath() {
|
|
69
|
+
const overrideKbPath = getKbPlPathOverride();
|
|
70
|
+
if (overrideKbPath) {
|
|
71
|
+
return overrideKbPath;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
try {
|
|
75
|
+
return require2.resolve("kibi-core/src/kb.pl");
|
|
76
|
+
} catch {
|
|
77
|
+
const coreMain = require2.resolve("kibi-core");
|
|
78
|
+
const coreDir = path.dirname(coreMain);
|
|
79
|
+
return path.join(coreDir, "src", "kb.pl");
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
let currentDir = importMetaDir;
|
|
83
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
84
|
+
const candidate = path.join(currentDir, "packages", "core", "src", "kb.pl");
|
|
85
|
+
if (existsSync(candidate)) {
|
|
86
|
+
return candidate;
|
|
87
|
+
}
|
|
88
|
+
currentDir = path.dirname(currentDir);
|
|
89
|
+
}
|
|
90
|
+
throw new Error("Unable to resolve kb.pl. Expected kibi-core to be installed (node_modules) " + "or to be running inside the monorepo checkout.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class PrologProcess {
|
|
94
|
+
process = null;
|
|
95
|
+
swiplPath;
|
|
96
|
+
timeout;
|
|
97
|
+
outputBuffer = "";
|
|
98
|
+
outputBufferBytes = 0;
|
|
99
|
+
outputOverflowed = false;
|
|
100
|
+
errorBuffer = "";
|
|
101
|
+
errorBufferBytes = 0;
|
|
102
|
+
cache = new Map;
|
|
103
|
+
interactiveQueryTail = Promise.resolve();
|
|
104
|
+
terminationPromise = null;
|
|
105
|
+
oneShotProcesses = new Set;
|
|
106
|
+
useOneShotMode;
|
|
107
|
+
attachedKbPath = null;
|
|
108
|
+
onProcessExit = null;
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.swiplPath = options.swiplPath || "swipl";
|
|
111
|
+
this.timeout = options.timeout || 30000;
|
|
112
|
+
this.useOneShotMode = options.oneShot ?? false;
|
|
113
|
+
}
|
|
114
|
+
async start() {
|
|
115
|
+
if (!existsSync(this.swiplPath) && this.swiplPath !== "swipl") {
|
|
116
|
+
throw new Error(`SWI-Prolog not found at ${this.swiplPath}. Please install SWI-Prolog or check your PATH.`);
|
|
117
|
+
}
|
|
118
|
+
const kbPath = resolveKbPlPath();
|
|
119
|
+
this.process = spawn(this.swiplPath, [
|
|
120
|
+
"-g",
|
|
121
|
+
`use_module('${kbPath}'), use_module(library(semweb/rdf_db)), set_prolog_flag(answer_write_options, [max_depth(0), quoted(true)])`,
|
|
122
|
+
"--quiet"
|
|
123
|
+
], {
|
|
124
|
+
detached: process.platform !== "win32",
|
|
125
|
+
env: {
|
|
126
|
+
...process.env,
|
|
127
|
+
KIBI_RUNTIME_NAME: process.versions.bun !== undefined ? "bun" : "node",
|
|
128
|
+
KIBI_RUNTIME_VERSION: process.versions.bun ?? process.versions.node ?? "unknown",
|
|
129
|
+
KIBI_PACKAGE_VERSIONS: process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
this.clearQueryBuffers();
|
|
133
|
+
if (!this.process.stdout || !this.process.stderr || !this.process.stdin) {
|
|
134
|
+
throw new Error("Failed to spawn Prolog process");
|
|
135
|
+
}
|
|
136
|
+
this.process.stdout.on("data", (chunk) => {
|
|
137
|
+
this.appendOutputChunk(chunk);
|
|
138
|
+
});
|
|
139
|
+
this.process.stderr.on("data", (chunk) => {
|
|
140
|
+
this.appendErrorChunk(chunk);
|
|
141
|
+
});
|
|
142
|
+
this.process.stdin.write(`true.
|
|
143
|
+
`);
|
|
144
|
+
if (!this.onProcessExit) {
|
|
145
|
+
this.onProcessExit = () => {
|
|
146
|
+
this.terminate();
|
|
147
|
+
};
|
|
148
|
+
process.on("exit", this.onProcessExit);
|
|
149
|
+
}
|
|
150
|
+
await this.waitForReady();
|
|
151
|
+
}
|
|
152
|
+
async waitForReady() {
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
const maxStartWait = 2000;
|
|
155
|
+
while (Date.now() - start < maxStartWait) {
|
|
156
|
+
if (!this.process || this.process.killed) {
|
|
157
|
+
throw new Error(`Prolog process terminated unexpectedly during startup: ${this.translateError(this.errorBuffer)}`);
|
|
158
|
+
}
|
|
159
|
+
if (this.errorBuffer.includes("ERROR")) {
|
|
160
|
+
throw new Error(`Failed to load kb module: ${this.translateError(this.errorBuffer)}`);
|
|
161
|
+
}
|
|
162
|
+
if (this.outputBuffer.includes("true.")) {
|
|
163
|
+
this.clearQueryBuffers();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
167
|
+
}
|
|
168
|
+
if (this.errorBuffer.includes("ERROR")) {
|
|
169
|
+
throw new Error(`Failed to load kb module: ${this.translateError(this.errorBuffer)}`);
|
|
170
|
+
}
|
|
171
|
+
this.clearQueryBuffers();
|
|
172
|
+
}
|
|
173
|
+
async query(goal) {
|
|
174
|
+
const isSingleGoal = typeof goal === "string";
|
|
175
|
+
const goalKey = isSingleGoal ? goal : null;
|
|
176
|
+
const cacheable = goalKey !== null && this.isCacheableGoal(goalKey);
|
|
177
|
+
if (cacheable) {
|
|
178
|
+
const cachedResult = this.cache.get(goalKey);
|
|
179
|
+
if (cachedResult) {
|
|
180
|
+
return cachedResult;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.useOneShotMode) {
|
|
184
|
+
const oneShotResult = await this.queryOneShot(goal);
|
|
185
|
+
const isBatchGoal = isSingleGoal && /^\s*\(/.test(this.normalizeGoal(goal));
|
|
186
|
+
const shouldCache = cacheable && !isBatchGoal;
|
|
187
|
+
if (!shouldCache && oneShotResult.success) {
|
|
188
|
+
this.invalidateCache();
|
|
189
|
+
}
|
|
190
|
+
if (shouldCache && oneShotResult.success) {
|
|
191
|
+
this.cache.set(goalKey, oneShotResult);
|
|
192
|
+
}
|
|
193
|
+
return oneShotResult;
|
|
194
|
+
}
|
|
195
|
+
if (!isSingleGoal) {
|
|
196
|
+
const batchGoal = `rdf_transaction((${goal.map((item) => this.normalizeGoal(item)).join(", ")}))`;
|
|
197
|
+
return this.query(batchGoal);
|
|
198
|
+
}
|
|
199
|
+
if (!this.isProcessUsable()) {
|
|
200
|
+
if (this.process === null) {
|
|
201
|
+
return this.execOneShot(goal, this.attachedKbPath);
|
|
202
|
+
}
|
|
203
|
+
throw new Error("Prolog process not started");
|
|
204
|
+
}
|
|
205
|
+
const runInteractiveQuery = async () => {
|
|
206
|
+
this.clearQueryBuffers();
|
|
207
|
+
const debug = isPrologDebugEnabled();
|
|
208
|
+
const normalizedGoal = this.normalizeGoal(goal);
|
|
209
|
+
const goalLabel = this.goalLabel(normalizedGoal);
|
|
210
|
+
const shouldPersist = this.isMutatingGoal(normalizedGoal) && !normalizedGoal.includes("kb_save") && !normalizedGoal.includes("kb_commit_upsert");
|
|
211
|
+
const goalWithPersistence = shouldPersist ? `(${normalizedGoal}, kb_save)` : normalizedGoal;
|
|
212
|
+
const wrappedGoal = /^once\s*\(/.test(goalWithPersistence) ? `catch(${goalWithPersistence}, _E, (print_message(error, _E), fail))` : `catch(once((${goalWithPersistence})), _E, (print_message(error, _E), fail))`;
|
|
213
|
+
const start = Date.now();
|
|
214
|
+
if (debug) {
|
|
215
|
+
console.error(`[prolog debug] start query: ${goalLabel}`);
|
|
216
|
+
}
|
|
217
|
+
this.process?.stdin?.write(`${wrappedGoal}.
|
|
218
|
+
writeln('${INTERACTIVE_QUERY_FRAME_END}').
|
|
219
|
+
`);
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
let settled = false;
|
|
222
|
+
const timeoutId = setTimeout(() => {
|
|
223
|
+
const stage = this.lastDiagnosticStage(this.errorBuffer) ?? "unknown";
|
|
224
|
+
const msg = `Query timeout after ${this.timeout / 1000}s (stage=${stage}, pid=${this.process?.pid ?? 0}, killed=${this.process?.killed ? "yes" : "no"}, exitCode=${this.process?.exitCode ?? "null"}, goal=${goalLabel})`;
|
|
225
|
+
if (debug) {
|
|
226
|
+
console.error(`[prolog debug] timeout: ${msg}`);
|
|
227
|
+
const runtime = this.errorBuffer.split(`
|
|
228
|
+
`).find((line) => line.includes("__KIBI_RUNTIME__"));
|
|
229
|
+
console.error(`[prolog debug] runtime=${runtime ?? "unknown"} packages=${process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS}`);
|
|
230
|
+
}
|
|
231
|
+
settled = true;
|
|
232
|
+
this.terminate().finally(() => {
|
|
233
|
+
reject(new Error(msg));
|
|
234
|
+
});
|
|
235
|
+
}, this.timeout);
|
|
236
|
+
const checkResult = () => {
|
|
237
|
+
if (settled) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.outputOverflowed) {
|
|
241
|
+
clearTimeout(timeoutId);
|
|
242
|
+
settled = true;
|
|
243
|
+
this.terminate().finally(() => {
|
|
244
|
+
resolve({
|
|
245
|
+
success: false,
|
|
246
|
+
bindings: {},
|
|
247
|
+
error: PROLOG_OUTPUT_OVERFLOW_ERROR
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!this.isProcessUsable()) {
|
|
253
|
+
clearTimeout(timeoutId);
|
|
254
|
+
settled = true;
|
|
255
|
+
resolve({
|
|
256
|
+
success: false,
|
|
257
|
+
bindings: {},
|
|
258
|
+
error: "Query interrupted because the Prolog process terminated"
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.errorBuffer.length > 0 && this.errorBuffer.includes("ERROR")) {
|
|
263
|
+
clearTimeout(timeoutId);
|
|
264
|
+
settled = true;
|
|
265
|
+
if (debug) {
|
|
266
|
+
console.error(`[prolog debug] query error: ${goalLabel} error=${this.errorBuffer.split(`
|
|
267
|
+
`)[0]}`);
|
|
268
|
+
}
|
|
269
|
+
resolve({
|
|
270
|
+
success: false,
|
|
271
|
+
bindings: {},
|
|
272
|
+
error: this.addDiagnosticStage(this.translateError(this.errorBuffer), normalizedGoal, this.errorBuffer)
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const frameEnd = this.outputBuffer.indexOf(INTERACTIVE_QUERY_FRAME_END);
|
|
277
|
+
if (frameEnd >= 0) {
|
|
278
|
+
clearTimeout(timeoutId);
|
|
279
|
+
settled = true;
|
|
280
|
+
const framedOutput = this.outputBuffer.slice(0, frameEnd).trimEnd();
|
|
281
|
+
if (/(?:^|\n)(?:false|fail)\.\s*$/.test(framedOutput)) {
|
|
282
|
+
resolve({
|
|
283
|
+
success: false,
|
|
284
|
+
bindings: {},
|
|
285
|
+
error: "Query failed"
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const result = {
|
|
290
|
+
success: true,
|
|
291
|
+
bindings: this.extractBindings(framedOutput)
|
|
292
|
+
};
|
|
293
|
+
const isBatchGoal = /^\s*\(/.test(normalizedGoal);
|
|
294
|
+
const shouldCache = cacheable && !isBatchGoal;
|
|
295
|
+
if (!shouldCache) {
|
|
296
|
+
this.invalidateCache();
|
|
297
|
+
}
|
|
298
|
+
if (shouldCache) {
|
|
299
|
+
this.cache.set(goalKey, result);
|
|
300
|
+
}
|
|
301
|
+
if (debug) {
|
|
302
|
+
console.error(`[prolog debug] query success: ${goalLabel} elapsed=${(Date.now() - start) / 1000}s`);
|
|
303
|
+
}
|
|
304
|
+
resolve(result);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
setTimeout(checkResult, 50);
|
|
308
|
+
};
|
|
309
|
+
checkResult();
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
const previousQuery = this.interactiveQueryTail;
|
|
313
|
+
let releaseQuery;
|
|
314
|
+
this.interactiveQueryTail = new Promise((resolve) => {
|
|
315
|
+
releaseQuery = resolve;
|
|
316
|
+
});
|
|
317
|
+
await previousQuery;
|
|
318
|
+
try {
|
|
319
|
+
if (!this.isProcessUsable()) {
|
|
320
|
+
throw new Error("Prolog process not started");
|
|
321
|
+
}
|
|
322
|
+
return await runInteractiveQuery();
|
|
323
|
+
} finally {
|
|
324
|
+
releaseQuery();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async queryBatch(goals) {
|
|
328
|
+
return this.query([...goals]);
|
|
329
|
+
}
|
|
330
|
+
invalidateCache() {
|
|
331
|
+
this.cache.clear();
|
|
332
|
+
}
|
|
333
|
+
isCacheableGoal(goal) {
|
|
334
|
+
const trimmed = goal.trim();
|
|
335
|
+
if (trimmed.startsWith("rdf_transaction(") || this.isMutatingGoal(trimmed)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return !(trimmed.startsWith("kb_attach(") || trimmed.startsWith("kb_detach") || trimmed.startsWith("kb_save") || trimmed.startsWith("kb_storage_status") || trimmed.startsWith("kb_storage_compact") || trimmed.startsWith("kb_storage_export") || trimmed.startsWith("status:") || trimmed.startsWith("kb_commit_upsert(") || trimmed.startsWith("kb_assert_") || trimmed.startsWith("kb_delete_") || trimmed.startsWith("kb_retract_"));
|
|
339
|
+
}
|
|
340
|
+
isMutatingGoal(goal) {
|
|
341
|
+
return /(?:kb_(?:assert|delete|retract|commit|save|log)_|\bkb_save\b|\brdf_(?:assert|retract))/u.test(goal);
|
|
342
|
+
}
|
|
343
|
+
isExplicitSaveGoal(goal) {
|
|
344
|
+
return goal.trim().startsWith("kb_save");
|
|
345
|
+
}
|
|
346
|
+
async queryOneShot(goal) {
|
|
347
|
+
if (Array.isArray(goal)) {
|
|
348
|
+
return this.execOneShot(goal, this.attachedKbPath);
|
|
349
|
+
}
|
|
350
|
+
const trimmedGoal = this.normalizeGoal(goal);
|
|
351
|
+
if (trimmedGoal.startsWith("kb_detach")) {
|
|
352
|
+
this.attachedKbPath = null;
|
|
353
|
+
return { success: true, bindings: {} };
|
|
354
|
+
}
|
|
355
|
+
const attachMatch = trimmedGoal.match(/^kb_attach\('(.+)'\)$/);
|
|
356
|
+
if (attachMatch) {
|
|
357
|
+
const attachPath = attachMatch[1] ?? null;
|
|
358
|
+
if (!attachPath) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
bindings: {},
|
|
362
|
+
error: "Invalid KB attach path"
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (this.attachedKbPath !== null) {
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
bindings: {},
|
|
369
|
+
error: "KB already attached"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const attachResult = await this.execOneShot(trimmedGoal, null);
|
|
373
|
+
if (attachResult.success) {
|
|
374
|
+
this.attachedKbPath = attachPath;
|
|
375
|
+
}
|
|
376
|
+
return attachResult;
|
|
377
|
+
}
|
|
378
|
+
return this.execOneShot(trimmedGoal, this.attachedKbPath);
|
|
379
|
+
}
|
|
380
|
+
async execOneShot(goal, kbPath) {
|
|
381
|
+
const originalGoalList = Array.isArray(goal) ? goal.map((item) => this.normalizeGoal(item)) : [this.normalizeGoal(goal)];
|
|
382
|
+
const explicitSaveRequested = Array.isArray(goal) ? originalGoalList.some((item) => this.isExplicitSaveGoal(item)) : false;
|
|
383
|
+
const goalList = explicitSaveRequested ? originalGoalList.filter((item) => !this.isExplicitSaveGoal(item)) : originalGoalList;
|
|
384
|
+
const isBatch = goalList.length > 1;
|
|
385
|
+
const combinedGoal = goalList.length === 0 ? "true" : goalList.length === 1 ? goalList[0] : `(${goalList.join(", ")})`;
|
|
386
|
+
const kbModulePath = resolveKbPlPath();
|
|
387
|
+
const prologGoal = [
|
|
388
|
+
`use_module('${kbModulePath}')`,
|
|
389
|
+
"use_module(library(semweb/rdf_db))",
|
|
390
|
+
"set_prolog_flag(answer_write_options, [max_depth(0), quoted(true)])",
|
|
391
|
+
"getenv('KIBI_GOAL', GoalAtom)",
|
|
392
|
+
"read_term_from_atom(GoalAtom, Goal, [variable_names(Vars)])",
|
|
393
|
+
kbPath ? "getenv('KIBI_KB_PATH', KBPath), kb_attach(KBPath)" : "true",
|
|
394
|
+
isBatch ? "WrappedGoal = rdf_transaction(Goal)" : "WrappedGoal = Goal",
|
|
395
|
+
"(catch(call(WrappedGoal), E, (print_message(error, E), fail)) -> QuerySucceeded = true ; QuerySucceeded = false)",
|
|
396
|
+
kbPath && (explicitSaveRequested || goalList.some((item) => this.isMutatingGoal(item))) ? "(QuerySucceeded == true -> kb_save ; true)" : "true",
|
|
397
|
+
kbPath ? "kb_detach" : "true",
|
|
398
|
+
"(QuerySucceeded == true -> (forall(member(Name=Value, Vars), (write(Name), write('='), write_term(Value, [quoted(true), max_depth(0)]), writeln('.'))), writeln('__KIBI_TRUE__.')) ; writeln('__KIBI_FALSE__.'))"
|
|
399
|
+
].join(", ");
|
|
400
|
+
const runtimeName = process.versions.bun !== undefined ? "bun" : "node";
|
|
401
|
+
const runtimeVersion = process.versions.bun ?? process.versions.node ?? "unknown";
|
|
402
|
+
const packageVersions = process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS;
|
|
403
|
+
let child;
|
|
404
|
+
try {
|
|
405
|
+
child = spawn(this.swiplPath, ["-q", "-g", prologGoal, "-t", "halt"], {
|
|
406
|
+
detached: process.platform !== "win32",
|
|
407
|
+
env: {
|
|
408
|
+
...process.env,
|
|
409
|
+
KIBI_GOAL: combinedGoal,
|
|
410
|
+
KIBI_RUNTIME_NAME: runtimeName,
|
|
411
|
+
KIBI_RUNTIME_VERSION: runtimeVersion,
|
|
412
|
+
KIBI_PACKAGE_VERSIONS: packageVersions,
|
|
413
|
+
...kbPath ? { KIBI_KB_PATH: kbPath } : {}
|
|
414
|
+
},
|
|
415
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
bindings: {},
|
|
421
|
+
error: error instanceof Error ? error.message : String(error)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
this.oneShotProcesses.add(child);
|
|
425
|
+
let stdout = "";
|
|
426
|
+
let stderr = "";
|
|
427
|
+
let outputBytes = 0;
|
|
428
|
+
let errorBytes = 0;
|
|
429
|
+
let outputOverflowed = false;
|
|
430
|
+
let timedOut = false;
|
|
431
|
+
let settled = false;
|
|
432
|
+
let closed = false;
|
|
433
|
+
let termination = null;
|
|
434
|
+
const appendChunk = (chunk, target) => {
|
|
435
|
+
if (outputOverflowed)
|
|
436
|
+
return;
|
|
437
|
+
const bytes = target === "stdout" ? outputBytes : errorBytes;
|
|
438
|
+
const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - bytes;
|
|
439
|
+
if (chunk.byteLength > remaining) {
|
|
440
|
+
const clipped = chunk.subarray(0, Math.max(0, remaining)).toString();
|
|
441
|
+
if (target === "stdout")
|
|
442
|
+
stdout += clipped;
|
|
443
|
+
else
|
|
444
|
+
stderr += clipped;
|
|
445
|
+
outputOverflowed = true;
|
|
446
|
+
termination ??= this.terminateProcessTree(child);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (target === "stdout") {
|
|
450
|
+
stdout += chunk.toString();
|
|
451
|
+
outputBytes += chunk.byteLength;
|
|
452
|
+
} else {
|
|
453
|
+
stderr += chunk.toString();
|
|
454
|
+
errorBytes += chunk.byteLength;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
child.stdout?.on("data", (chunk) => appendChunk(chunk, "stdout"));
|
|
458
|
+
child.stderr?.on("data", (chunk) => appendChunk(chunk, "stderr"));
|
|
459
|
+
const completion = new Promise((resolve) => {
|
|
460
|
+
child.once("error", (error) => {
|
|
461
|
+
closed = true;
|
|
462
|
+
resolve({ code: null, signal: null, error });
|
|
463
|
+
});
|
|
464
|
+
child.once("close", (code, signal) => {
|
|
465
|
+
closed = true;
|
|
466
|
+
resolve({ code, signal });
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
const timeoutId = setTimeout(() => {
|
|
470
|
+
if (settled || closed)
|
|
471
|
+
return;
|
|
472
|
+
timedOut = true;
|
|
473
|
+
termination ??= this.terminateProcessTree(child);
|
|
474
|
+
}, this.timeout);
|
|
475
|
+
const result = await completion;
|
|
476
|
+
settled = true;
|
|
477
|
+
clearTimeout(timeoutId);
|
|
478
|
+
if (termination !== null)
|
|
479
|
+
await termination;
|
|
480
|
+
this.oneShotProcesses.delete(child);
|
|
481
|
+
const stage = this.lastDiagnosticStage(stderr) ?? "unknown";
|
|
482
|
+
if (isPrologDebugEnabled()) {
|
|
483
|
+
const runtime = stderr.split(`
|
|
484
|
+
`).find((line) => line.includes("__KIBI_RUNTIME__"));
|
|
485
|
+
if (runtime !== undefined) {
|
|
486
|
+
console.error(`[prolog debug] ${runtime}`);
|
|
487
|
+
}
|
|
488
|
+
console.error(`[prolog debug] child pid=${child.pid ?? 0} runtime=${runtimeName}@${runtimeVersion} packages=${packageVersions} stage=${stage}`);
|
|
489
|
+
}
|
|
490
|
+
if (timedOut) {
|
|
491
|
+
throw new Error(`Query timeout after ${this.timeout / 1000}s (stage=${stage}, pid=${child.pid ?? 0}, goal=${this.goalLabel(combinedGoal ?? "true")})`);
|
|
492
|
+
}
|
|
493
|
+
if (outputOverflowed) {
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
bindings: {},
|
|
497
|
+
error: this.addDiagnosticStage(PROLOG_OUTPUT_OVERFLOW_ERROR, combinedGoal ?? "true", stderr)
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (result.error !== undefined) {
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
bindings: {},
|
|
504
|
+
error: this.addDiagnosticStage(result.error.message, combinedGoal ?? "true", stderr)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (stderr.includes("ERROR")) {
|
|
508
|
+
return {
|
|
509
|
+
success: false,
|
|
510
|
+
bindings: {},
|
|
511
|
+
error: this.addDiagnosticStage(this.translateError(stderr), combinedGoal ?? "true", stderr)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (stdout.includes("__KIBI_TRUE__")) {
|
|
515
|
+
const clean = stdout.split(`
|
|
516
|
+
`).filter((line) => !line.includes("__KIBI_TRUE__")).join(`
|
|
517
|
+
`);
|
|
518
|
+
return {
|
|
519
|
+
success: true,
|
|
520
|
+
bindings: this.extractBindings(clean)
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (stdout.includes("__KIBI_FALSE__")) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
bindings: {},
|
|
527
|
+
error: "Query returned false"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
bindings: {},
|
|
533
|
+
error: this.addDiagnosticStage(`Query failed - stdout: ${stdout.substring(0, 200)}, stderr: ${stderr.substring(0, 200)}`, combinedGoal ?? "true", stderr)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
normalizeGoal(goal) {
|
|
537
|
+
return goal.trim().replace(/\.+\s*$/, "");
|
|
538
|
+
}
|
|
539
|
+
clearQueryBuffers() {
|
|
540
|
+
this.outputBuffer = "";
|
|
541
|
+
this.outputBufferBytes = 0;
|
|
542
|
+
this.outputOverflowed = false;
|
|
543
|
+
this.errorBuffer = "";
|
|
544
|
+
this.errorBufferBytes = 0;
|
|
545
|
+
}
|
|
546
|
+
appendOutputChunk(chunk) {
|
|
547
|
+
if (this.outputOverflowed)
|
|
548
|
+
return;
|
|
549
|
+
const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.outputBufferBytes;
|
|
550
|
+
if (chunk.byteLength > remaining) {
|
|
551
|
+
this.outputBuffer += chunk.subarray(0, remaining).toString();
|
|
552
|
+
this.outputBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
|
|
553
|
+
this.outputOverflowed = true;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
this.outputBuffer += chunk.toString();
|
|
557
|
+
this.outputBufferBytes += chunk.byteLength;
|
|
558
|
+
}
|
|
559
|
+
appendErrorChunk(chunk) {
|
|
560
|
+
if (this.outputOverflowed)
|
|
561
|
+
return;
|
|
562
|
+
const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.errorBufferBytes;
|
|
563
|
+
if (chunk.byteLength > remaining) {
|
|
564
|
+
this.errorBuffer += chunk.subarray(0, remaining).toString();
|
|
565
|
+
this.errorBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
|
|
566
|
+
this.outputOverflowed = true;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
this.errorBuffer += chunk.toString();
|
|
570
|
+
this.errorBufferBytes += chunk.byteLength;
|
|
571
|
+
}
|
|
572
|
+
goalLabel(goal) {
|
|
573
|
+
const match = goal.trim().match(/^([a-z][A-Za-z0-9_]*(?::[a-z][A-Za-z0-9_]*)?)/);
|
|
574
|
+
return match?.[1] ?? "anonymous";
|
|
575
|
+
}
|
|
576
|
+
addDiagnosticStage(message, goal, diagnostics) {
|
|
577
|
+
if (this.goalLabel(goal) !== "kb_commit_upsert")
|
|
578
|
+
return message;
|
|
579
|
+
const stage = this.lastDiagnosticStage(diagnostics);
|
|
580
|
+
return stage === null ? message : `${message} (stage=${stage})`;
|
|
581
|
+
}
|
|
582
|
+
lastDiagnosticStage(text) {
|
|
583
|
+
const matches = [...text.matchAll(/__KIBI_STAGE__:([^\s\r\n]+)/g)];
|
|
584
|
+
return matches.at(-1)?.[1] ?? null;
|
|
585
|
+
}
|
|
586
|
+
signalProcessTree(child, signal) {
|
|
587
|
+
const pid = child.pid;
|
|
588
|
+
if (pid === undefined || pid <= 0)
|
|
589
|
+
return;
|
|
590
|
+
if (process.platform !== "win32") {
|
|
591
|
+
try {
|
|
592
|
+
process.kill(-pid, signal);
|
|
593
|
+
return;
|
|
594
|
+
} catch {}
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
child.kill(signal);
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
async terminateProcessTree(child) {
|
|
601
|
+
let closed = false;
|
|
602
|
+
const exited = new Promise((resolve) => {
|
|
603
|
+
const markExited = () => {
|
|
604
|
+
closed = true;
|
|
605
|
+
resolve();
|
|
606
|
+
};
|
|
607
|
+
child.once("close", markExited);
|
|
608
|
+
child.once("error", markExited);
|
|
609
|
+
});
|
|
610
|
+
if (child.exitCode === null)
|
|
611
|
+
this.signalProcessTree(child, "SIGTERM");
|
|
612
|
+
await Promise.race([
|
|
613
|
+
exited,
|
|
614
|
+
new Promise((resolve) => setTimeout(resolve, 1000))
|
|
615
|
+
]);
|
|
616
|
+
if (!closed) {
|
|
617
|
+
this.signalProcessTree(child, "SIGKILL");
|
|
618
|
+
await Promise.race([
|
|
619
|
+
exited,
|
|
620
|
+
new Promise((resolve) => setTimeout(resolve, 1000))
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
isProcessUsable() {
|
|
625
|
+
return Boolean(this.process?.stdin && !this.process?.killed && this.process?.exitCode === null);
|
|
626
|
+
}
|
|
627
|
+
extractBindings(output) {
|
|
628
|
+
const bindings = {};
|
|
629
|
+
let variableName;
|
|
630
|
+
let valueLines = [];
|
|
631
|
+
const flush = () => {
|
|
632
|
+
if (variableName === undefined)
|
|
633
|
+
return;
|
|
634
|
+
bindings[variableName] = valueLines.join(`
|
|
635
|
+
`).trim().replace(/\.$/, "").replace(/,$/, "");
|
|
636
|
+
variableName = undefined;
|
|
637
|
+
valueLines = [];
|
|
638
|
+
};
|
|
639
|
+
for (const line of output.split(`
|
|
640
|
+
`)) {
|
|
641
|
+
const match = line.match(/^([A-Z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
642
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
643
|
+
flush();
|
|
644
|
+
variableName = match[1];
|
|
645
|
+
valueLines = [match[2]];
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (variableName !== undefined)
|
|
649
|
+
valueLines.push(line);
|
|
650
|
+
}
|
|
651
|
+
flush();
|
|
652
|
+
return bindings;
|
|
653
|
+
}
|
|
654
|
+
translateError(errorText) {
|
|
655
|
+
const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME)__:[^\r\n]*\r?\n?/gm, "");
|
|
656
|
+
if (cleanError.includes("stale_snapshot")) {
|
|
657
|
+
return "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)";
|
|
658
|
+
}
|
|
659
|
+
if ((cleanError.includes("audit_log") || cleanError.includes("audit.log") || cleanError.includes("Resource temporarily unavailable")) && (cleanError.includes("lock") || cleanError.includes("permission"))) {
|
|
660
|
+
return "Audit journal is locked by another Kibi runtime; restart the stale MCP/CLI session before retrying";
|
|
661
|
+
}
|
|
662
|
+
if (cleanError.includes("does not exist") && /entity [`'"].+?[`'"]/.test(cleanError)) {
|
|
663
|
+
const entityIdMatch = cleanError.match(/entity [`'"]+(.+?)[`'"]+ does not exist/);
|
|
664
|
+
const matchedEntityId = entityIdMatch?.[1];
|
|
665
|
+
const entityId = matchedEntityId !== undefined ? matchedEntityId.replace(/^`?'+|'+`?$/g, "") : "unknown";
|
|
666
|
+
if (cleanError.includes("Target entity does not exist")) {
|
|
667
|
+
return `Target entity does not exist: ${entityId}`;
|
|
668
|
+
}
|
|
669
|
+
if (cleanError.includes("Source entity does not exist")) {
|
|
670
|
+
return `Source entity does not exist: ${entityId}`;
|
|
671
|
+
}
|
|
672
|
+
return `Entity does not exist: ${entityId}`;
|
|
673
|
+
}
|
|
674
|
+
if (cleanError.includes("Type error: `relationship' expected")) {
|
|
675
|
+
const relMatch = cleanError.match(/\(Invalid relationship: ([^)]+)\)/);
|
|
676
|
+
const invalidRelationship = relMatch?.[1];
|
|
677
|
+
if (invalidRelationship !== undefined) {
|
|
678
|
+
return `Invalid relationship: ${invalidRelationship.trim()}`;
|
|
679
|
+
}
|
|
680
|
+
return "Invalid relationship type or direction";
|
|
681
|
+
}
|
|
682
|
+
if (cleanError.includes("existence_error") || cleanError.includes("Unknown procedure")) {
|
|
683
|
+
return "Predicate or file not found";
|
|
684
|
+
}
|
|
685
|
+
if (cleanError.includes("permission_error")) {
|
|
686
|
+
return "Access denied or KB locked";
|
|
687
|
+
}
|
|
688
|
+
if (cleanError.includes("syntax_error") || cleanError.includes("Operator expected")) {
|
|
689
|
+
return "Invalid query syntax";
|
|
690
|
+
}
|
|
691
|
+
if (cleanError.includes("timeout_error")) {
|
|
692
|
+
return `Operation exceeded ${this.timeout / 1000}s timeout`;
|
|
693
|
+
}
|
|
694
|
+
const simpleError = (cleanError.replace(/ERROR:\s*/g, "").replace(/^\*\*.*\*\*$/gm, "").replace(/^\s+/gm, "").split(`
|
|
695
|
+
`)[0] ?? "").trim();
|
|
696
|
+
return simpleError || "Unknown error";
|
|
697
|
+
}
|
|
698
|
+
isRunning() {
|
|
699
|
+
return Boolean(this.process && !this.process.killed && this.process?.exitCode === null);
|
|
700
|
+
}
|
|
701
|
+
getPid() {
|
|
702
|
+
return this.process?.pid || 0;
|
|
703
|
+
}
|
|
704
|
+
async terminate() {
|
|
705
|
+
if (this.terminationPromise) {
|
|
706
|
+
await this.terminationPromise;
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (this.onProcessExit) {
|
|
710
|
+
process.off("exit", this.onProcessExit);
|
|
711
|
+
this.onProcessExit = null;
|
|
712
|
+
}
|
|
713
|
+
const current = this.process;
|
|
714
|
+
const oneShots = [...this.oneShotProcesses];
|
|
715
|
+
this.process = null;
|
|
716
|
+
this.clearQueryBuffers();
|
|
717
|
+
if (current || oneShots.length > 0) {
|
|
718
|
+
this.terminationPromise = (async () => {
|
|
719
|
+
if (current) {
|
|
720
|
+
current.stdin?.end();
|
|
721
|
+
await this.terminateProcessTree(current);
|
|
722
|
+
}
|
|
723
|
+
await Promise.all(oneShots.map((child) => this.terminateProcessTree(child)));
|
|
724
|
+
})();
|
|
725
|
+
try {
|
|
726
|
+
await this.terminationPromise;
|
|
727
|
+
} finally {
|
|
728
|
+
this.terminationPromise = null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ../cli/src/prolog/codec.ts
|
|
735
|
+
function parseListOfLists(listStr) {
|
|
736
|
+
const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
737
|
+
if (cleaned === "") {
|
|
738
|
+
return [];
|
|
739
|
+
}
|
|
740
|
+
const results = [];
|
|
741
|
+
let depth = 0;
|
|
742
|
+
let current = "";
|
|
743
|
+
let currentList = [];
|
|
744
|
+
for (let i = 0;i < cleaned.length; i++) {
|
|
745
|
+
const char = cleaned[i];
|
|
746
|
+
if (char === "[") {
|
|
747
|
+
depth++;
|
|
748
|
+
if (depth > 1)
|
|
749
|
+
current += char;
|
|
750
|
+
} else if (char === "]") {
|
|
751
|
+
depth--;
|
|
752
|
+
if (depth === 0) {
|
|
753
|
+
if (current) {
|
|
754
|
+
currentList.push(current.trim());
|
|
755
|
+
current = "";
|
|
756
|
+
}
|
|
757
|
+
if (currentList.length > 0) {
|
|
758
|
+
results.push(currentList);
|
|
759
|
+
currentList = [];
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
current += char;
|
|
763
|
+
}
|
|
764
|
+
} else if (char === "," && depth === 1) {
|
|
765
|
+
if (current) {
|
|
766
|
+
currentList.push(current.trim());
|
|
767
|
+
current = "";
|
|
768
|
+
}
|
|
769
|
+
} else if (char === "," && depth === 0) {} else {
|
|
770
|
+
current += char;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return results;
|
|
774
|
+
}
|
|
775
|
+
function parseEntityFromList(data) {
|
|
776
|
+
if (data.length < 3) {
|
|
777
|
+
return {};
|
|
778
|
+
}
|
|
779
|
+
const [idPart, typePart, propsPart] = data;
|
|
780
|
+
if (idPart === undefined || typePart === undefined || propsPart === undefined) {
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
const id = idPart.trim();
|
|
784
|
+
const type = typePart.trim();
|
|
785
|
+
const propsStr = propsPart.trim();
|
|
786
|
+
const props = parsePropertyList(propsStr);
|
|
787
|
+
return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
|
|
788
|
+
}
|
|
789
|
+
function parsePropertyList(propsStr) {
|
|
790
|
+
const props = {};
|
|
791
|
+
let cleaned = propsStr.trim();
|
|
792
|
+
if (cleaned.startsWith("[")) {
|
|
793
|
+
cleaned = cleaned.substring(1);
|
|
794
|
+
}
|
|
795
|
+
if (cleaned.endsWith("]")) {
|
|
796
|
+
cleaned = cleaned.substring(0, cleaned.length - 1);
|
|
797
|
+
}
|
|
798
|
+
const pairs = splitTopLevelGeneral(cleaned, ",");
|
|
799
|
+
for (const pair of pairs) {
|
|
800
|
+
const eqIndex = pair.indexOf("=");
|
|
801
|
+
if (eqIndex === -1)
|
|
802
|
+
continue;
|
|
803
|
+
const key = pair.substring(0, eqIndex).trim();
|
|
804
|
+
const value = pair.substring(eqIndex + 1).trim();
|
|
805
|
+
if (key === "..." || value === "..." || value === "...|...") {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
let parsed = parsePrologValue(value);
|
|
809
|
+
if ((key === "rule_ir" || key === "semantic_inventory" || key === "verification_receipts" || key === "verification_contract") && typeof parsed === "string") {
|
|
810
|
+
try {
|
|
811
|
+
parsed = JSON.parse(parsed);
|
|
812
|
+
} catch {}
|
|
813
|
+
}
|
|
814
|
+
if (!Object.hasOwn(props, key)) {
|
|
815
|
+
props[key] = parsed;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const existing = props[key];
|
|
819
|
+
props[key] = Array.isArray(existing) ? [...existing, parsed] : [existing, parsed];
|
|
820
|
+
}
|
|
821
|
+
return props;
|
|
822
|
+
}
|
|
823
|
+
function parsePrologValue(valueInput) {
|
|
824
|
+
const value = valueInput.trim();
|
|
825
|
+
if (value.startsWith("^^(")) {
|
|
826
|
+
const innerStart = value.indexOf("(") + 1;
|
|
827
|
+
let depth = 1;
|
|
828
|
+
let innerEnd = innerStart;
|
|
829
|
+
for (let i = innerStart;i < value.length; i++) {
|
|
830
|
+
if (value[i] === "(")
|
|
831
|
+
depth++;
|
|
832
|
+
if (value[i] === ")") {
|
|
833
|
+
depth--;
|
|
834
|
+
if (depth === 0) {
|
|
835
|
+
innerEnd = i;
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const innerContent = value.substring(innerStart, innerEnd);
|
|
841
|
+
const parts = splitTopLevelGeneral(innerContent, ",");
|
|
842
|
+
if (parts.length >= 2) {
|
|
843
|
+
const literalPart = parts[0];
|
|
844
|
+
const datatypePart = parts[1];
|
|
845
|
+
if (literalPart === undefined || datatypePart === undefined) {
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
let literalValue = literalPart.trim();
|
|
849
|
+
const datatype = datatypePart.trim();
|
|
850
|
+
if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
|
|
851
|
+
literalValue = literalValue.substring(1, literalValue.length - 1);
|
|
852
|
+
}
|
|
853
|
+
if (datatype.includes("#integer")) {
|
|
854
|
+
return Number.parseInt(literalValue, 10);
|
|
855
|
+
}
|
|
856
|
+
if (datatype.includes("#decimal") || datatype.includes("#double")) {
|
|
857
|
+
return Number.parseFloat(literalValue);
|
|
858
|
+
}
|
|
859
|
+
if (datatype.includes("#boolean")) {
|
|
860
|
+
return literalValue === "true";
|
|
861
|
+
}
|
|
862
|
+
if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
|
|
863
|
+
const listContent = literalValue.substring(1, literalValue.length - 1);
|
|
864
|
+
if (listContent === "") {
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
return splitTopLevelGeneral(listContent, ",").map((item) => item.trim());
|
|
868
|
+
}
|
|
869
|
+
return literalValue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (value.startsWith("file:///")) {
|
|
873
|
+
const lastSlash = value.lastIndexOf("/");
|
|
874
|
+
if (lastSlash !== -1) {
|
|
875
|
+
return value.substring(lastSlash + 1);
|
|
876
|
+
}
|
|
877
|
+
return value;
|
|
878
|
+
}
|
|
879
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
880
|
+
try {
|
|
881
|
+
return JSON.parse(value);
|
|
882
|
+
} catch {
|
|
883
|
+
return value.substring(1, value.length - 1);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
887
|
+
return value.substring(1, value.length - 1);
|
|
888
|
+
}
|
|
889
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
890
|
+
const listContent = value.substring(1, value.length - 1);
|
|
891
|
+
if (listContent === "") {
|
|
892
|
+
return [];
|
|
893
|
+
}
|
|
894
|
+
const items = splitTopLevelGeneral(listContent, ",").map((item) => {
|
|
895
|
+
return parsePrologValue(item.trim());
|
|
896
|
+
});
|
|
897
|
+
return items;
|
|
898
|
+
}
|
|
899
|
+
return value;
|
|
900
|
+
}
|
|
901
|
+
function splitTopLevelGeneral(str, delimiter) {
|
|
902
|
+
const results = [];
|
|
903
|
+
let current = "";
|
|
904
|
+
let depth = 0;
|
|
905
|
+
let inDoubleQuotes = false;
|
|
906
|
+
let inSingleQuotes = false;
|
|
907
|
+
for (let i = 0;i < str.length; i++) {
|
|
908
|
+
const char = str[i];
|
|
909
|
+
const prevChar = i > 0 ? str[i - 1] : "";
|
|
910
|
+
if (char === '"' && !inSingleQuotes && prevChar !== "\\") {
|
|
911
|
+
inDoubleQuotes = !inDoubleQuotes;
|
|
912
|
+
current += char;
|
|
913
|
+
} else if (char === "'" && !inDoubleQuotes && prevChar !== "\\") {
|
|
914
|
+
inSingleQuotes = !inSingleQuotes;
|
|
915
|
+
current += char;
|
|
916
|
+
} else if (!inDoubleQuotes && !inSingleQuotes && (char === "[" || char === "(")) {
|
|
917
|
+
depth++;
|
|
918
|
+
current += char;
|
|
919
|
+
} else if (!inDoubleQuotes && !inSingleQuotes && (char === "]" || char === ")")) {
|
|
920
|
+
depth--;
|
|
921
|
+
current += char;
|
|
922
|
+
} else if (!inDoubleQuotes && !inSingleQuotes && depth === 0 && char === delimiter) {
|
|
923
|
+
if (current) {
|
|
924
|
+
results.push(current);
|
|
925
|
+
current = "";
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
current += char;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (current) {
|
|
932
|
+
results.push(current);
|
|
933
|
+
}
|
|
934
|
+
return results;
|
|
935
|
+
}
|
|
936
|
+
function stripOuterQuotes(value) {
|
|
937
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
938
|
+
return value.slice(1, -1);
|
|
939
|
+
}
|
|
940
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
941
|
+
return value.slice(1, -1);
|
|
942
|
+
}
|
|
943
|
+
return value;
|
|
944
|
+
}
|
|
945
|
+
function normalizeEntityId(value) {
|
|
946
|
+
if (!value.startsWith("file:///")) {
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
const idx = value.lastIndexOf("/");
|
|
950
|
+
return idx === -1 ? value : value.slice(idx + 1);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ../cli/src/utils/branch-resolver.ts
|
|
954
|
+
import {
|
|
955
|
+
execFileSync as rawExecFileSync,
|
|
956
|
+
execSync as rawExecSync
|
|
957
|
+
} from "node:child_process";
|
|
958
|
+
import * as path3 from "node:path";
|
|
959
|
+
|
|
960
|
+
// ../cli/src/utils/branch-store-locator.ts
|
|
961
|
+
import { createHash } from "node:crypto";
|
|
962
|
+
import {
|
|
963
|
+
existsSync as existsSync2,
|
|
964
|
+
mkdirSync,
|
|
965
|
+
readFileSync,
|
|
966
|
+
readdirSync,
|
|
967
|
+
statSync,
|
|
968
|
+
writeFileSync
|
|
969
|
+
} from "node:fs";
|
|
970
|
+
import path2 from "node:path";
|
|
971
|
+
var BRANCH_STORE_MANIFEST_VERSION = 1;
|
|
972
|
+
function branchStoreKey(branch) {
|
|
973
|
+
return createHash("sha256").update(branch, "utf8").digest("hex");
|
|
974
|
+
}
|
|
975
|
+
function branchStoresRoot(workspaceRoot) {
|
|
976
|
+
return path2.join(path2.resolve(workspaceRoot), ".kb", "branches");
|
|
977
|
+
}
|
|
978
|
+
function branchStorePath(workspaceRoot, branch) {
|
|
979
|
+
return path2.join(branchStoresRoot(workspaceRoot), branchStoreKey(branch));
|
|
980
|
+
}
|
|
981
|
+
function branchStoreManifestPath(storePath) {
|
|
982
|
+
return path2.join(storePath, "branch.json");
|
|
983
|
+
}
|
|
984
|
+
function expectedBranchStoreManifest(branch) {
|
|
985
|
+
return {
|
|
986
|
+
version: BRANCH_STORE_MANIFEST_VERSION,
|
|
987
|
+
branch,
|
|
988
|
+
key: branchStoreKey(branch)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function readBranchStoreManifest(storePath) {
|
|
992
|
+
const manifestPath = branchStoreManifestPath(storePath);
|
|
993
|
+
if (!existsSync2(manifestPath))
|
|
994
|
+
return null;
|
|
995
|
+
try {
|
|
996
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
997
|
+
if (parsed.version !== BRANCH_STORE_MANIFEST_VERSION || typeof parsed.branch !== "string" || typeof parsed.key !== "string") {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
version: BRANCH_STORE_MANIFEST_VERSION,
|
|
1002
|
+
branch: parsed.branch,
|
|
1003
|
+
key: parsed.key
|
|
1004
|
+
};
|
|
1005
|
+
} catch {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function branchStoreManifestMatches(storePath, branch) {
|
|
1010
|
+
const manifest = readBranchStoreManifest(storePath);
|
|
1011
|
+
const expected = expectedBranchStoreManifest(branch);
|
|
1012
|
+
return manifest !== null && manifest.version === expected.version && manifest.branch === expected.branch && manifest.key === expected.key && path2.basename(storePath) === expected.key;
|
|
1013
|
+
}
|
|
1014
|
+
function ensureBranchStoreManifest(workspaceRoot, branch) {
|
|
1015
|
+
const storePath = branchStorePath(workspaceRoot, branch);
|
|
1016
|
+
mkdirSync(storePath, { recursive: true });
|
|
1017
|
+
const manifestPath = branchStoreManifestPath(storePath);
|
|
1018
|
+
const existing = readBranchStoreManifest(storePath);
|
|
1019
|
+
if (existing !== null && !branchStoreManifestMatches(storePath, branch)) {
|
|
1020
|
+
throw new Error(`Branch store identity mismatch at ${storePath}; refusing to attach it to '${branch}'.`);
|
|
1021
|
+
}
|
|
1022
|
+
if (existing === null) {
|
|
1023
|
+
if (existsSync2(manifestPath)) {
|
|
1024
|
+
throw new Error(`Branch store manifest is invalid at ${manifestPath}; refusing to overwrite it.`);
|
|
1025
|
+
}
|
|
1026
|
+
const entries = readdirSync(storePath);
|
|
1027
|
+
if (entries.length > 0) {
|
|
1028
|
+
throw new Error(`Branch store at ${storePath} has compiled data but no valid identity manifest; refusing to adopt it.`);
|
|
1029
|
+
}
|
|
1030
|
+
writeFileSync(manifestPath, `${JSON.stringify(expectedBranchStoreManifest(branch), null, 2)}
|
|
1031
|
+
`, { mode: 384 });
|
|
1032
|
+
}
|
|
1033
|
+
return storePath;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ../cli/src/utils/branch-resolver.ts
|
|
1037
|
+
var defaultDeps = {
|
|
1038
|
+
execSync: rawExecSync,
|
|
1039
|
+
execFileSync: rawExecFileSync
|
|
1040
|
+
};
|
|
1041
|
+
var VOLATILE_ARTIFACTS = new Set([
|
|
1042
|
+
"sync-cache.json",
|
|
1043
|
+
"journal.log",
|
|
1044
|
+
"audit.log",
|
|
1045
|
+
"lock",
|
|
1046
|
+
"lockfile",
|
|
1047
|
+
".lock"
|
|
1048
|
+
]);
|
|
1049
|
+
var VOLATILE_EXTENSIONS = new Set([".lock", ".tmp", ".temp", ".pid"]);
|
|
1050
|
+
function isValidBranchName(name) {
|
|
1051
|
+
if (!name || name.length === 0 || name.length > 255)
|
|
1052
|
+
return false;
|
|
1053
|
+
const hasControlCharacter = [...name].some((character) => {
|
|
1054
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
1055
|
+
return codePoint <= 32 || codePoint === 127;
|
|
1056
|
+
});
|
|
1057
|
+
if (name.includes("..") || path3.isAbsolute(name) || name.startsWith("/")) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
if (name.includes("//") || name.endsWith("/") || name.endsWith(".") || name.includes("\\") || name.startsWith("-") || name.includes("@{") || name.includes("..") || hasControlCharacter || /[~^:?*\[\]]/.test(name)) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
const components = name.split("/");
|
|
1064
|
+
if (components.some((component) => component.startsWith(".") || component.endsWith(".lock"))) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
defaultDeps.execFileSync("git", ["check-ref-format", "--branch", name], {
|
|
1069
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1070
|
+
timeout: 5000
|
|
1071
|
+
});
|
|
1072
|
+
return true;
|
|
1073
|
+
} catch {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// ../cli/src/engine.ts
|
|
1079
|
+
var ENGINE_PROTOCOL_VERSION = 1;
|
|
1080
|
+
var ENGINE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1081
|
+
var ENGINE_PACKAGE_VERSIONS = process.env.KIBI_PACKAGE_VERSIONS ?? "unknown";
|
|
1082
|
+
var ENGINE_QUERY_CACHE_MAX_ENTRIES = 128;
|
|
1083
|
+
var ENGINE_QUERY_CACHE_MAX_RESULT_BYTES = 8 * 1024 * 1024;
|
|
1084
|
+
var ENGINE_FRESHNESS_CACHE_MS = 100;
|
|
1085
|
+
var ENGINE_PUBLICATION_LOCK_STALE_MS = 5000;
|
|
1086
|
+
function engineIdleTimeoutMs() {
|
|
1087
|
+
const configured = Number.parseInt(process.env.KIBI_ENGINE_IDLE_TIMEOUT_MS ?? "", 10);
|
|
1088
|
+
return Number.isFinite(configured) && configured >= 100 ? configured : ENGINE_IDLE_TIMEOUT_MS;
|
|
1089
|
+
}
|
|
1090
|
+
function quoteProlog(value) {
|
|
1091
|
+
return value.replaceAll("'", "''");
|
|
1092
|
+
}
|
|
1093
|
+
function cacheableEngineGoal(goal) {
|
|
1094
|
+
const normalized = goal.trim();
|
|
1095
|
+
if (!normalized || normalized.startsWith("(") || normalized.includes("kb_storage_status")) {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
return normalized.startsWith("findall(") || normalized.startsWith("kb_entity(") || normalized.startsWith("kb_entities_by_source(") || normalized.startsWith("kb_relationship(");
|
|
1099
|
+
}
|
|
1100
|
+
function mutatingEngineGoal(goal) {
|
|
1101
|
+
return /(?:kb_(?:assert|retract|commit|save|delete)|rdf_transaction|rdf_assert|rdf_retract)/.test(goal);
|
|
1102
|
+
}
|
|
1103
|
+
function maskPrologData(goal) {
|
|
1104
|
+
const chars = [...goal];
|
|
1105
|
+
let quote = null;
|
|
1106
|
+
let lineComment = false;
|
|
1107
|
+
let blockComment = false;
|
|
1108
|
+
for (let index = 0;index < chars.length; index += 1) {
|
|
1109
|
+
const current = chars[index];
|
|
1110
|
+
const next = chars[index + 1];
|
|
1111
|
+
if (lineComment) {
|
|
1112
|
+
if (current === `
|
|
1113
|
+
`)
|
|
1114
|
+
lineComment = false;
|
|
1115
|
+
else
|
|
1116
|
+
chars[index] = " ";
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (blockComment) {
|
|
1120
|
+
if (current === "*" && next === "/") {
|
|
1121
|
+
chars[index] = " ";
|
|
1122
|
+
chars[index + 1] = " ";
|
|
1123
|
+
index += 1;
|
|
1124
|
+
blockComment = false;
|
|
1125
|
+
} else if (current !== `
|
|
1126
|
+
`) {
|
|
1127
|
+
chars[index] = " ";
|
|
1128
|
+
}
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (quote !== null) {
|
|
1132
|
+
if (current === "\\") {
|
|
1133
|
+
chars[index] = " ";
|
|
1134
|
+
if (index + 1 < chars.length) {
|
|
1135
|
+
chars[index + 1] = " ";
|
|
1136
|
+
index += 1;
|
|
1137
|
+
}
|
|
1138
|
+
} else if (current === quote) {
|
|
1139
|
+
if (next === quote) {
|
|
1140
|
+
chars[index] = " ";
|
|
1141
|
+
chars[index + 1] = " ";
|
|
1142
|
+
index += 1;
|
|
1143
|
+
} else {
|
|
1144
|
+
chars[index] = " ";
|
|
1145
|
+
quote = null;
|
|
1146
|
+
}
|
|
1147
|
+
} else if (current !== `
|
|
1148
|
+
`) {
|
|
1149
|
+
chars[index] = " ";
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (current === "%") {
|
|
1154
|
+
chars[index] = " ";
|
|
1155
|
+
lineComment = true;
|
|
1156
|
+
} else if (current === "/" && next === "*") {
|
|
1157
|
+
chars[index] = " ";
|
|
1158
|
+
chars[index + 1] = " ";
|
|
1159
|
+
index += 1;
|
|
1160
|
+
blockComment = true;
|
|
1161
|
+
} else if (current === "'" || current === '"' || current === "`") {
|
|
1162
|
+
chars[index] = " ";
|
|
1163
|
+
quote = current;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return chars.join("");
|
|
1167
|
+
}
|
|
1168
|
+
function safeEngineGoal(goal) {
|
|
1169
|
+
const executable = maskPrologData(goal);
|
|
1170
|
+
if (/\b(?:halt|abort|shell|system|process_create|consult|load_files|open|close|delete_file|rename_file|make_directory|thread_create|rdf_attach_db|rdf_load|rdf_save|assertz|asserta|retractall|set_prolog_flag)\s*\(/.test(executable)) {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
return /(?:^|[(:,\s])(?:kb_|checks:|status:|discovery:|requirement_proof:|findall\(|aggregate_all\(|rdf_transaction\(|use_module\(|atom_json_dict\(|member\(|once\(|true\b|fail\b)/.test(goal.trim());
|
|
1174
|
+
}
|
|
1175
|
+
function frame(value) {
|
|
1176
|
+
const payload = Buffer.from(JSON.stringify(value), "utf8");
|
|
1177
|
+
const header = Buffer.allocUnsafe(4);
|
|
1178
|
+
header.writeUInt32BE(payload.byteLength, 0);
|
|
1179
|
+
return Buffer.concat([header, payload]);
|
|
1180
|
+
}
|
|
1181
|
+
function writeSocketFrame(socket, value) {
|
|
1182
|
+
if (socket.destroyed || !socket.writable)
|
|
1183
|
+
return;
|
|
1184
|
+
try {
|
|
1185
|
+
socket.write(frame(value));
|
|
1186
|
+
} catch {}
|
|
1187
|
+
}
|
|
1188
|
+
function parseFrames(buffer, onFrame) {
|
|
1189
|
+
let cursor = 0;
|
|
1190
|
+
while (buffer.byteLength - cursor >= 4) {
|
|
1191
|
+
const length = buffer.readUInt32BE(cursor);
|
|
1192
|
+
if (length > 64 * 1024 * 1024) {
|
|
1193
|
+
throw new Error("Kibi engine frame exceeds 64 MiB limit");
|
|
1194
|
+
}
|
|
1195
|
+
if (buffer.byteLength - cursor - 4 < length)
|
|
1196
|
+
break;
|
|
1197
|
+
const payload = buffer.subarray(cursor + 4, cursor + 4 + length);
|
|
1198
|
+
onFrame(JSON.parse(payload.toString("utf8")));
|
|
1199
|
+
cursor += 4 + length;
|
|
1200
|
+
}
|
|
1201
|
+
return buffer.subarray(cursor);
|
|
1202
|
+
}
|
|
1203
|
+
function runtimeDirectory() {
|
|
1204
|
+
const configured = process.env.KIBI_RUNTIME_DIR ?? process.env.XDG_RUNTIME_DIR ?? path4.join(os.tmpdir(), "kibi-runtime");
|
|
1205
|
+
const candidates = [configured, path4.join(os.tmpdir(), "kibi-runtime")];
|
|
1206
|
+
for (const candidate of candidates) {
|
|
1207
|
+
try {
|
|
1208
|
+
mkdirSync2(candidate, { recursive: true, mode: 448 });
|
|
1209
|
+
const probe = path4.join(candidate, `.kibi-write-${process.pid}`);
|
|
1210
|
+
writeFileSync2(probe, "", { mode: 384 });
|
|
1211
|
+
unlinkSync(probe);
|
|
1212
|
+
return candidate;
|
|
1213
|
+
} catch {}
|
|
1214
|
+
}
|
|
1215
|
+
throw new Error("Unable to create a writable Kibi engine runtime directory");
|
|
1216
|
+
}
|
|
1217
|
+
function fsyncPath(filePath) {
|
|
1218
|
+
if (!existsSync3(filePath))
|
|
1219
|
+
return;
|
|
1220
|
+
let descriptor = null;
|
|
1221
|
+
try {
|
|
1222
|
+
descriptor = openSync(filePath, "r");
|
|
1223
|
+
fsyncSync(descriptor);
|
|
1224
|
+
} finally {
|
|
1225
|
+
if (descriptor !== null)
|
|
1226
|
+
closeSync(descriptor);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function fsyncJournaledBranchStore(branchPath) {
|
|
1230
|
+
const rdfRoot = path4.join(branchPath, "rdf");
|
|
1231
|
+
const directories = [];
|
|
1232
|
+
const pending = existsSync3(rdfRoot) ? [rdfRoot] : [];
|
|
1233
|
+
while (pending.length > 0) {
|
|
1234
|
+
const directory = pending.pop();
|
|
1235
|
+
if (directory === undefined)
|
|
1236
|
+
break;
|
|
1237
|
+
directories.push(directory);
|
|
1238
|
+
for (const entry of readdirSync2(directory, { withFileTypes: true })) {
|
|
1239
|
+
const entryPath = path4.join(directory, entry.name);
|
|
1240
|
+
if (entry.isDirectory())
|
|
1241
|
+
pending.push(entryPath);
|
|
1242
|
+
else if (entry.isFile() && entry.name !== "lock")
|
|
1243
|
+
fsyncPath(entryPath);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
for (const metadata of ["CURRENT", "storage.json", "kb.rdf"]) {
|
|
1247
|
+
fsyncPath(path4.join(branchPath, metadata));
|
|
1248
|
+
}
|
|
1249
|
+
if (process.platform !== "win32") {
|
|
1250
|
+
for (const directory of directories.reverse())
|
|
1251
|
+
fsyncPath(directory);
|
|
1252
|
+
fsyncPath(branchPath);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function engineSocketPath(workspaceRoot, branch) {
|
|
1256
|
+
const canonicalRoot = (() => {
|
|
1257
|
+
try {
|
|
1258
|
+
return realpathSync(workspaceRoot);
|
|
1259
|
+
} catch {
|
|
1260
|
+
return path4.resolve(workspaceRoot);
|
|
1261
|
+
}
|
|
1262
|
+
})();
|
|
1263
|
+
const key = createHash2("sha256").update(`${canonicalRoot}\x00${branch}`).digest("hex").slice(0, 32);
|
|
1264
|
+
if (process.platform === "win32") {
|
|
1265
|
+
return `\\\\.\\pipe\\kibi-${key}`;
|
|
1266
|
+
}
|
|
1267
|
+
return path4.join(runtimeDirectory(), `kibi-${key}.sock`);
|
|
1268
|
+
}
|
|
1269
|
+
function enginePidPath(workspaceRoot, branch) {
|
|
1270
|
+
const socket = engineSocketPath(workspaceRoot, branch);
|
|
1271
|
+
if (process.platform === "win32") {
|
|
1272
|
+
return path4.join(runtimeDirectory(), `kibi-${createHash2("sha256").update(`${path4.resolve(workspaceRoot)}\x00${branch}`).digest("hex")}.pid`);
|
|
1273
|
+
}
|
|
1274
|
+
return `${socket}.pid`;
|
|
1275
|
+
}
|
|
1276
|
+
function engineStartLockPath(workspaceRoot, branch) {
|
|
1277
|
+
if (process.platform === "win32") {
|
|
1278
|
+
return path4.join(runtimeDirectory(), `kibi-${createHash2("sha256").update(`${path4.resolve(workspaceRoot)}\x00${branch}`).digest("hex")}.start.lock`);
|
|
1279
|
+
}
|
|
1280
|
+
return `${engineSocketPath(workspaceRoot, branch)}.start.lock`;
|
|
1281
|
+
}
|
|
1282
|
+
function enginePublicationLockPath(workspaceRoot, branch) {
|
|
1283
|
+
return `${engineSocketPath(workspaceRoot, branch)}.publish.lock`;
|
|
1284
|
+
}
|
|
1285
|
+
function publicationLockOwner(lockPath) {
|
|
1286
|
+
try {
|
|
1287
|
+
const [pidText, createdText] = readFileSync2(lockPath, "utf8").trim().split(":");
|
|
1288
|
+
const pid = Number.parseInt(pidText ?? "", 10);
|
|
1289
|
+
const createdAt = Number.parseInt(createdText ?? "", 10);
|
|
1290
|
+
if (!Number.isInteger(pid) || pid <= 1 || !Number.isFinite(createdAt)) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
return { pid, createdAt };
|
|
1294
|
+
} catch {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function processIsAlive(pid) {
|
|
1299
|
+
try {
|
|
1300
|
+
process.kill(pid, 0);
|
|
1301
|
+
return true;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function clearStalePublicationLock(lockPath) {
|
|
1307
|
+
if (!existsSync3(lockPath))
|
|
1308
|
+
return true;
|
|
1309
|
+
const owner = publicationLockOwner(lockPath);
|
|
1310
|
+
let age = 0;
|
|
1311
|
+
try {
|
|
1312
|
+
age = Date.now() - statSync2(lockPath).mtimeMs;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
if ((owner === null || !processIsAlive(owner.pid)) && age > ENGINE_PUBLICATION_LOCK_STALE_MS) {
|
|
1317
|
+
try {
|
|
1318
|
+
unlinkSync(lockPath);
|
|
1319
|
+
return true;
|
|
1320
|
+
} catch {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
function publicationLockIsActive(lockPath) {
|
|
1327
|
+
if (!existsSync3(lockPath))
|
|
1328
|
+
return false;
|
|
1329
|
+
if (clearStalePublicationLock(lockPath))
|
|
1330
|
+
return false;
|
|
1331
|
+
return existsSync3(lockPath);
|
|
1332
|
+
}
|
|
1333
|
+
function tryAcquireStartLock(lockPath) {
|
|
1334
|
+
try {
|
|
1335
|
+
const descriptor = openSync(lockPath, "wx", 384);
|
|
1336
|
+
writeFileSync2(lockPath, `${process.pid}:${Date.now()}
|
|
1337
|
+
`, { flag: "w" });
|
|
1338
|
+
return descriptor;
|
|
1339
|
+
} catch {
|
|
1340
|
+
try {
|
|
1341
|
+
const stamp = readFileSync2(lockPath, "utf8").trim().split(":");
|
|
1342
|
+
const pid = Number.parseInt(stamp[0] ?? "", 10);
|
|
1343
|
+
const created = Number.parseInt(stamp[1] ?? "", 10);
|
|
1344
|
+
let age = 0;
|
|
1345
|
+
try {
|
|
1346
|
+
age = Date.now() - statSync2(lockPath).mtimeMs;
|
|
1347
|
+
} catch {
|
|
1348
|
+
age = 0;
|
|
1349
|
+
}
|
|
1350
|
+
if (!Number.isFinite(created) && age <= 5000)
|
|
1351
|
+
return null;
|
|
1352
|
+
let alive = false;
|
|
1353
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1354
|
+
try {
|
|
1355
|
+
process.kill(pid, 0);
|
|
1356
|
+
alive = true;
|
|
1357
|
+
} catch {
|
|
1358
|
+
alive = false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (!alive && (Number.isFinite(created) ? Date.now() - created > 5000 : age > 5000)) {
|
|
1362
|
+
unlinkSync(lockPath);
|
|
1363
|
+
}
|
|
1364
|
+
} catch {}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function releaseStartLock(lockPath, descriptor) {
|
|
1369
|
+
if (descriptor !== null) {
|
|
1370
|
+
try {
|
|
1371
|
+
closeSync(descriptor);
|
|
1372
|
+
} catch {}
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
unlinkSync(lockPath);
|
|
1376
|
+
} catch {}
|
|
1377
|
+
}
|
|
1378
|
+
function recoverStaleJournalLock(branchPath) {
|
|
1379
|
+
const lockPath = path4.join(branchPath, "rdf", "lock");
|
|
1380
|
+
if (!existsSync3(lockPath))
|
|
1381
|
+
return;
|
|
1382
|
+
let contents = "";
|
|
1383
|
+
try {
|
|
1384
|
+
contents = readFileSync2(lockPath, "utf8");
|
|
1385
|
+
} catch {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const match = /pid\((\d+)\)/.exec(contents);
|
|
1389
|
+
if (!match)
|
|
1390
|
+
return;
|
|
1391
|
+
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
1392
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
|
|
1393
|
+
return;
|
|
1394
|
+
try {
|
|
1395
|
+
process.kill(pid, 0);
|
|
1396
|
+
return;
|
|
1397
|
+
} catch {
|
|
1398
|
+
try {
|
|
1399
|
+
rmSync(lockPath, { force: true });
|
|
1400
|
+
} catch {}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function daemonEntry() {
|
|
1404
|
+
const moduleDirectory = path4.dirname(fileURLToPath2(import.meta.url));
|
|
1405
|
+
const candidates = [
|
|
1406
|
+
path4.join(moduleDirectory, "engine-daemon.js"),
|
|
1407
|
+
path4.join(moduleDirectory, "..", "dist", "engine-daemon.js"),
|
|
1408
|
+
path4.join(moduleDirectory, "..", "node_modules", "kibi-cli", "dist", "engine-daemon.js")
|
|
1409
|
+
];
|
|
1410
|
+
for (const candidate of candidates) {
|
|
1411
|
+
if (existsSync3(candidate))
|
|
1412
|
+
return candidate;
|
|
1413
|
+
}
|
|
1414
|
+
throw new Error("The Kibi engine is not built. Run `npm run build:cli` before using the CLI engine.");
|
|
1415
|
+
}
|
|
1416
|
+
function connectSocket(socketPath, timeoutMs) {
|
|
1417
|
+
return new Promise((resolve3, reject) => {
|
|
1418
|
+
const socket = net.createConnection(socketPath);
|
|
1419
|
+
let settled = false;
|
|
1420
|
+
const timer = setTimeout(() => {
|
|
1421
|
+
if (settled)
|
|
1422
|
+
return;
|
|
1423
|
+
settled = true;
|
|
1424
|
+
socket.destroy();
|
|
1425
|
+
reject(new Error(`Timed out connecting to Kibi engine at ${socketPath}`));
|
|
1426
|
+
}, timeoutMs);
|
|
1427
|
+
socket.once("connect", () => {
|
|
1428
|
+
if (settled)
|
|
1429
|
+
return;
|
|
1430
|
+
settled = true;
|
|
1431
|
+
clearTimeout(timer);
|
|
1432
|
+
resolve3(socket);
|
|
1433
|
+
});
|
|
1434
|
+
socket.on("error", (error) => {
|
|
1435
|
+
if (settled)
|
|
1436
|
+
return;
|
|
1437
|
+
settled = true;
|
|
1438
|
+
clearTimeout(timer);
|
|
1439
|
+
reject(error);
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
async function connectWithRetry(socketPath, timeoutMs) {
|
|
1444
|
+
const deadline = Date.now() + timeoutMs;
|
|
1445
|
+
let lastError;
|
|
1446
|
+
while (Date.now() < deadline) {
|
|
1447
|
+
const errorPath = `${socketPath}.error`;
|
|
1448
|
+
if (existsSync3(errorPath)) {
|
|
1449
|
+
try {
|
|
1450
|
+
const diagnostic = readFileSync2(errorPath, "utf8").trim();
|
|
1451
|
+
if (diagnostic)
|
|
1452
|
+
throw new Error(diagnostic);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
throw new Error(`Kibi engine failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (process.platform !== "win32" && !existsSync3(socketPath)) {
|
|
1458
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
return await connectSocket(socketPath, Math.min(250, deadline - Date.now()));
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
lastError = error;
|
|
1465
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
throw new Error(`Unable to connect to Kibi engine at ${socketPath}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
class EngineClient {
|
|
1472
|
+
workspaceRoot;
|
|
1473
|
+
branch;
|
|
1474
|
+
timeout;
|
|
1475
|
+
allowPublicationLock;
|
|
1476
|
+
socket = null;
|
|
1477
|
+
daemon = null;
|
|
1478
|
+
inputBuffer = Buffer.alloc(0);
|
|
1479
|
+
requestId = 0;
|
|
1480
|
+
pending = new Map;
|
|
1481
|
+
requestTail = Promise.resolve();
|
|
1482
|
+
lastResult = null;
|
|
1483
|
+
constructor(options) {
|
|
1484
|
+
if (!isValidBranchName(options.branch)) {
|
|
1485
|
+
throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
|
|
1486
|
+
}
|
|
1487
|
+
const resolvedWorkspaceRoot = path4.resolve(options.workspaceRoot);
|
|
1488
|
+
try {
|
|
1489
|
+
this.workspaceRoot = realpathSync(resolvedWorkspaceRoot);
|
|
1490
|
+
} catch {
|
|
1491
|
+
this.workspaceRoot = resolvedWorkspaceRoot;
|
|
1492
|
+
}
|
|
1493
|
+
this.branch = options.branch;
|
|
1494
|
+
this.timeout = options.timeout ?? 120000;
|
|
1495
|
+
this.allowPublicationLock = options.allowPublicationLock ?? false;
|
|
1496
|
+
}
|
|
1497
|
+
async start(allowSpawn = true) {
|
|
1498
|
+
if (this.socket !== null && !this.socket.destroyed)
|
|
1499
|
+
return;
|
|
1500
|
+
const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
|
|
1501
|
+
const publicationLockPath = enginePublicationLockPath(this.workspaceRoot, this.branch);
|
|
1502
|
+
if (!this.allowPublicationLock && publicationLockIsActive(publicationLockPath)) {
|
|
1503
|
+
throw new Error(`Kibi engine publication is in progress: ${publicationLockPath}`);
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
if (existsSync3(socketPath)) {
|
|
1507
|
+
this.socket = await connectWithRetry(socketPath, 150);
|
|
1508
|
+
} else {
|
|
1509
|
+
throw new Error("engine socket is absent");
|
|
1510
|
+
}
|
|
1511
|
+
} catch {
|
|
1512
|
+
if (!allowSpawn)
|
|
1513
|
+
return;
|
|
1514
|
+
const nodePath = process.env.KIBI_NODE_PATH ?? "node";
|
|
1515
|
+
const entry = daemonEntry();
|
|
1516
|
+
const lockPath = engineStartLockPath(this.workspaceRoot, this.branch);
|
|
1517
|
+
const startDeadline = Date.now() + this.timeout;
|
|
1518
|
+
let lock = null;
|
|
1519
|
+
while (lock === null && Date.now() < startDeadline) {
|
|
1520
|
+
if (existsSync3(socketPath)) {
|
|
1521
|
+
try {
|
|
1522
|
+
this.socket = await connectWithRetry(socketPath, 100);
|
|
1523
|
+
break;
|
|
1524
|
+
} catch {}
|
|
1525
|
+
}
|
|
1526
|
+
if (this.socket === null) {
|
|
1527
|
+
lock = tryAcquireStartLock(lockPath);
|
|
1528
|
+
if (lock === null) {
|
|
1529
|
+
await new Promise((resolve3) => setTimeout(resolve3, 50));
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (this.socket === null && lock === null) {
|
|
1534
|
+
throw new Error(`Unable to connect to Kibi engine at ${socketPath} while another starter owns ${lockPath}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (lock !== null) {
|
|
1537
|
+
try {
|
|
1538
|
+
rmSync(`${socketPath}.error`, { force: true });
|
|
1539
|
+
try {
|
|
1540
|
+
const daemon = spawn2(nodePath, [
|
|
1541
|
+
entry,
|
|
1542
|
+
"--workspace",
|
|
1543
|
+
this.workspaceRoot,
|
|
1544
|
+
"--branch",
|
|
1545
|
+
this.branch,
|
|
1546
|
+
"--socket",
|
|
1547
|
+
socketPath
|
|
1548
|
+
], {
|
|
1549
|
+
detached: true,
|
|
1550
|
+
stdio: "ignore",
|
|
1551
|
+
env: {
|
|
1552
|
+
...process.env,
|
|
1553
|
+
KIBI_ENGINE_PROTOCOL: String(ENGINE_PROTOCOL_VERSION)
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
await new Promise((resolve3, reject) => {
|
|
1557
|
+
daemon.once("spawn", resolve3);
|
|
1558
|
+
daemon.once("error", reject);
|
|
1559
|
+
});
|
|
1560
|
+
this.daemon = daemon;
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
throw new Error(`Kibi engine requires Node.js >=18 to host the daemon; failed to start '${nodePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1563
|
+
}
|
|
1564
|
+
this.daemon.unref();
|
|
1565
|
+
this.socket = await connectWithRetry(socketPath, this.timeout);
|
|
1566
|
+
} finally {
|
|
1567
|
+
releaseStartLock(lockPath, lock);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (this.socket === null)
|
|
1572
|
+
return;
|
|
1573
|
+
this.attachSocket(this.socket);
|
|
1574
|
+
}
|
|
1575
|
+
attachSocket(socket) {
|
|
1576
|
+
socket.on("data", (chunk) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
1579
|
+
this.inputBuffer = parseFrames(Buffer.concat([this.inputBuffer, bytes]), (value) => this.handleResponse(value));
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
socket.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
socket.on("close", () => {
|
|
1585
|
+
this.socket = null;
|
|
1586
|
+
const error = new Error("Kibi engine connection closed");
|
|
1587
|
+
for (const pending of this.pending.values())
|
|
1588
|
+
pending.reject(error);
|
|
1589
|
+
this.pending.clear();
|
|
1590
|
+
});
|
|
1591
|
+
socket.on("error", (error) => {
|
|
1592
|
+
for (const pending of this.pending.values())
|
|
1593
|
+
pending.reject(error);
|
|
1594
|
+
this.pending.clear();
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
handleResponse(value) {
|
|
1598
|
+
if (value === null || typeof value !== "object")
|
|
1599
|
+
return;
|
|
1600
|
+
const response = value;
|
|
1601
|
+
if (typeof response.id !== "number")
|
|
1602
|
+
return;
|
|
1603
|
+
const pending = this.pending.get(response.id);
|
|
1604
|
+
if (!pending)
|
|
1605
|
+
return;
|
|
1606
|
+
this.pending.delete(response.id);
|
|
1607
|
+
if (response.ok)
|
|
1608
|
+
pending.resolve(response.result);
|
|
1609
|
+
else
|
|
1610
|
+
pending.reject(new Error(response.error ?? "Kibi engine request failed"));
|
|
1611
|
+
}
|
|
1612
|
+
async request(request, signal) {
|
|
1613
|
+
await this.start();
|
|
1614
|
+
const socket = this.socket;
|
|
1615
|
+
if (socket === null || socket.destroyed)
|
|
1616
|
+
throw new Error("Kibi engine is not connected");
|
|
1617
|
+
const id = ++this.requestId;
|
|
1618
|
+
const result = new Promise((resolve3, reject) => {
|
|
1619
|
+
this.pending.set(id, {
|
|
1620
|
+
resolve: resolve3,
|
|
1621
|
+
reject
|
|
1622
|
+
});
|
|
1623
|
+
try {
|
|
1624
|
+
socket.write(frame({
|
|
1625
|
+
...request,
|
|
1626
|
+
id,
|
|
1627
|
+
protocolVersion: ENGINE_PROTOCOL_VERSION,
|
|
1628
|
+
packageVersions: ENGINE_PACKAGE_VERSIONS,
|
|
1629
|
+
workspaceRoot: this.workspaceRoot,
|
|
1630
|
+
branch: this.branch
|
|
1631
|
+
}));
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
this.pending.delete(id);
|
|
1634
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (signal !== undefined) {
|
|
1638
|
+
const abort = () => {
|
|
1639
|
+
if (!this.pending.has(id))
|
|
1640
|
+
return;
|
|
1641
|
+
this.pending.delete(id);
|
|
1642
|
+
this.cancel(id);
|
|
1643
|
+
reject(new Error("Kibi engine request cancelled"));
|
|
1644
|
+
};
|
|
1645
|
+
if (signal.aborted)
|
|
1646
|
+
abort();
|
|
1647
|
+
else
|
|
1648
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
return result;
|
|
1652
|
+
}
|
|
1653
|
+
async query(goal, signal) {
|
|
1654
|
+
let result;
|
|
1655
|
+
const previous = this.requestTail;
|
|
1656
|
+
let release;
|
|
1657
|
+
this.requestTail = new Promise((resolve3) => {
|
|
1658
|
+
release = resolve3;
|
|
1659
|
+
});
|
|
1660
|
+
await previous;
|
|
1661
|
+
try {
|
|
1662
|
+
result = await this.request({ method: "query", goal }, signal);
|
|
1663
|
+
this.lastResult = result;
|
|
1664
|
+
return result;
|
|
1665
|
+
} finally {
|
|
1666
|
+
release();
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async command(command, signal) {
|
|
1670
|
+
return this.request({ method: "command", command }, signal);
|
|
1671
|
+
}
|
|
1672
|
+
async queryBatch(goals) {
|
|
1673
|
+
const normalized = goals.map((goal) => goal.trim().replace(/\.+\s*$/, ""));
|
|
1674
|
+
return this.query(`rdf_transaction((${normalized.join(", ")}))`);
|
|
1675
|
+
}
|
|
1676
|
+
async queryEntities(input, signal) {
|
|
1677
|
+
const result = await this.command({
|
|
1678
|
+
version: 1,
|
|
1679
|
+
kind: "entities",
|
|
1680
|
+
...input.type !== undefined ? { type: input.type } : {},
|
|
1681
|
+
...input.id !== undefined ? { id: input.id } : {},
|
|
1682
|
+
...input.tags !== undefined ? { tags: input.tags } : {},
|
|
1683
|
+
...input.sourceFile !== undefined ? { sourceFile: input.sourceFile } : {},
|
|
1684
|
+
limit: input.limit,
|
|
1685
|
+
offset: input.offset
|
|
1686
|
+
}, signal);
|
|
1687
|
+
if (!result.success) {
|
|
1688
|
+
throw new Error(result.error ?? "Indexed entity query failed");
|
|
1689
|
+
}
|
|
1690
|
+
const rows = result.bindings.Rows ? parseListOfLists(result.bindings.Rows).map(parseEntityFromList) : [];
|
|
1691
|
+
const count = Number.parseInt(result.bindings.Count ?? "0", 10);
|
|
1692
|
+
return {
|
|
1693
|
+
entities: rows,
|
|
1694
|
+
count: Number.isFinite(count) ? count : rows.length
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
async searchEntities(input, signal) {
|
|
1698
|
+
const result = await this.command({
|
|
1699
|
+
version: 1,
|
|
1700
|
+
kind: "search",
|
|
1701
|
+
query: input.query,
|
|
1702
|
+
...input.type !== undefined ? { type: input.type } : {},
|
|
1703
|
+
limit: input.limit,
|
|
1704
|
+
offset: input.offset
|
|
1705
|
+
}, signal);
|
|
1706
|
+
if (!result.success) {
|
|
1707
|
+
throw new Error(result.error ?? "Indexed search candidate query failed");
|
|
1708
|
+
}
|
|
1709
|
+
const rows = result.bindings.Rows ? parseListOfLists(result.bindings.Rows).map(parseEntityFromList) : [];
|
|
1710
|
+
const count = Number.parseInt(result.bindings.Count ?? "0", 10);
|
|
1711
|
+
return {
|
|
1712
|
+
entities: rows,
|
|
1713
|
+
count: Number.isFinite(count) ? count : rows.length
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
cancel(requestId) {
|
|
1717
|
+
const socket = this.socket;
|
|
1718
|
+
if (socket === null || socket.destroyed)
|
|
1719
|
+
return;
|
|
1720
|
+
const id = ++this.requestId;
|
|
1721
|
+
socket.write(frame({
|
|
1722
|
+
id,
|
|
1723
|
+
method: "cancel",
|
|
1724
|
+
cancelOf: requestId,
|
|
1725
|
+
protocolVersion: ENGINE_PROTOCOL_VERSION,
|
|
1726
|
+
packageVersions: ENGINE_PACKAGE_VERSIONS,
|
|
1727
|
+
workspaceRoot: this.workspaceRoot,
|
|
1728
|
+
branch: this.branch
|
|
1729
|
+
}));
|
|
1730
|
+
}
|
|
1731
|
+
async nextSolution() {
|
|
1732
|
+
const result = this.lastResult;
|
|
1733
|
+
this.lastResult = null;
|
|
1734
|
+
return result;
|
|
1735
|
+
}
|
|
1736
|
+
async save(signal) {
|
|
1737
|
+
return this.query("kb_save", signal);
|
|
1738
|
+
}
|
|
1739
|
+
async storageStatus() {
|
|
1740
|
+
return this.request({ method: "status" });
|
|
1741
|
+
}
|
|
1742
|
+
async checkpoint() {
|
|
1743
|
+
return this.command({ version: 1, kind: "checkpoint" });
|
|
1744
|
+
}
|
|
1745
|
+
async queryStatusJson() {
|
|
1746
|
+
return this.command({ version: 1, kind: "status" });
|
|
1747
|
+
}
|
|
1748
|
+
async compact() {
|
|
1749
|
+
return this.command({ version: 1, kind: "compact" });
|
|
1750
|
+
}
|
|
1751
|
+
async exportStorage(targetDirectory) {
|
|
1752
|
+
return this.request({
|
|
1753
|
+
method: "export",
|
|
1754
|
+
targetDirectory
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
invalidateCache() {
|
|
1758
|
+
this.lastResult = null;
|
|
1759
|
+
}
|
|
1760
|
+
isRunning() {
|
|
1761
|
+
return this.socket !== null && !this.socket.destroyed;
|
|
1762
|
+
}
|
|
1763
|
+
getPid() {
|
|
1764
|
+
const pidPath = enginePidPath(this.workspaceRoot, this.branch);
|
|
1765
|
+
try {
|
|
1766
|
+
return Number.parseInt(readFileSync2(pidPath, "utf8").trim(), 10) || 0;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return 0;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async stop(startIfMissing = true) {
|
|
1772
|
+
const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
|
|
1773
|
+
if (!this.isRunning()) {
|
|
1774
|
+
try {
|
|
1775
|
+
await this.start(startIfMissing);
|
|
1776
|
+
} catch {
|
|
1777
|
+
if (!startIfMissing)
|
|
1778
|
+
return;
|
|
1779
|
+
throw new Error("Unable to start Kibi engine for stop request");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (!this.isRunning()) {
|
|
1783
|
+
if (existsSync3(socketPath)) {
|
|
1784
|
+
throw new Error(`Kibi engine socket remains present but is not reachable: ${socketPath}`);
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
let requestError;
|
|
1789
|
+
try {
|
|
1790
|
+
await this.request({ method: "stop" });
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
requestError = error;
|
|
1793
|
+
}
|
|
1794
|
+
this.socket?.destroy();
|
|
1795
|
+
this.socket = null;
|
|
1796
|
+
const deadline = Date.now() + 5000;
|
|
1797
|
+
while (existsSync3(socketPath) && Date.now() < deadline) {
|
|
1798
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
1799
|
+
}
|
|
1800
|
+
if (existsSync3(socketPath)) {
|
|
1801
|
+
const detail = requestError instanceof Error ? `: ${requestError.message}` : "";
|
|
1802
|
+
throw new Error(`Kibi engine did not stop within 5 seconds${detail}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async terminate() {
|
|
1806
|
+
this.socket?.end();
|
|
1807
|
+
this.socket?.destroy();
|
|
1808
|
+
this.socket = null;
|
|
1809
|
+
this.daemon = null;
|
|
1810
|
+
this.lastResult = null;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
async function ensureJournaledBranchStoreAsync(branchPath) {
|
|
1814
|
+
mkdirSync2(branchPath, { recursive: true });
|
|
1815
|
+
const markerPath = path4.join(branchPath, "storage.json");
|
|
1816
|
+
const marker = existsSync3(markerPath) ? readFileSync2(markerPath, "utf8") : "";
|
|
1817
|
+
if (marker.includes("kibi.rdf-journal.v1")) {
|
|
1818
|
+
const rdfDirectory = path4.join(branchPath, "rdf");
|
|
1819
|
+
const currentPath = path4.join(branchPath, "CURRENT");
|
|
1820
|
+
const sentinelPath = path4.join(branchPath, "kb.rdf");
|
|
1821
|
+
if (!existsSync3(rdfDirectory) || !existsSync3(currentPath)) {
|
|
1822
|
+
recoverInterruptedGeneration(branchPath);
|
|
1823
|
+
if (existsSync3(rdfDirectory) && existsSync3(currentPath)) {
|
|
1824
|
+
recoverStaleJournalLock(branchPath);
|
|
1825
|
+
archiveLegacyFiles(branchPath);
|
|
1826
|
+
if (!existsSync3(sentinelPath)) {
|
|
1827
|
+
writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
|
|
1828
|
+
`, { mode: 384 });
|
|
1829
|
+
}
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const legacyRdf2 = path4.join(branchPath, "kb.rdf");
|
|
1833
|
+
const legacyAudit2 = path4.join(branchPath, "audit.log");
|
|
1834
|
+
const legacyContents = existsSync3(legacyRdf2) ? readFileSync2(legacyRdf2, "utf8") : "";
|
|
1835
|
+
if (existsSync3(legacyAudit2) || existsSync3(legacyRdf2) && !legacyContents.includes("kibi.rdf-journal.v1")) {
|
|
1836
|
+
rmSync(rdfDirectory, { recursive: true, force: true });
|
|
1837
|
+
rmSync(currentPath, { force: true });
|
|
1838
|
+
rmSync(markerPath, { force: true });
|
|
1839
|
+
} else {
|
|
1840
|
+
throw new Error(`Journaled branch store is incomplete at ${branchPath}; restore the immutable legacy/ backup or remove the interrupted generation before retrying`);
|
|
1841
|
+
}
|
|
1842
|
+
} else {
|
|
1843
|
+
recoverStaleJournalLock(branchPath);
|
|
1844
|
+
archiveLegacyFiles(branchPath);
|
|
1845
|
+
if (!existsSync3(sentinelPath)) {
|
|
1846
|
+
writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
|
|
1847
|
+
`, { mode: 384 });
|
|
1848
|
+
}
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const legacyRdf = path4.join(branchPath, "kb.rdf");
|
|
1853
|
+
const legacyAudit = path4.join(branchPath, "audit.log");
|
|
1854
|
+
if (!existsSync3(legacyRdf) && !existsSync3(legacyAudit)) {
|
|
1855
|
+
mkdirSync2(path4.join(branchPath, "rdf"), { recursive: true });
|
|
1856
|
+
writeFileSync2(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
|
|
1857
|
+
`, { mode: 384 });
|
|
1858
|
+
writeFileSync2(path4.join(branchPath, "CURRENT"), `generation-1:0
|
|
1859
|
+
`, {
|
|
1860
|
+
mode: 384
|
|
1861
|
+
});
|
|
1862
|
+
writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
|
|
1863
|
+
`, { mode: 384 });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (existsSync3(legacyRdf)) {
|
|
1867
|
+
const legacyContents = readFileSync2(legacyRdf, "utf8");
|
|
1868
|
+
if (!/<rdf:RDF(?:\s|>)/.test(legacyContents)) {
|
|
1869
|
+
throw new Error(`Legacy Kibi store is corrupt at ${legacyRdf}: expected RDF/XML root`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const parent = path4.dirname(branchPath);
|
|
1873
|
+
const migrationKey = createHash2("sha256").update(path4.resolve(branchPath)).digest("hex").slice(0, 16);
|
|
1874
|
+
const staging = path4.join(parent, `.${migrationKey}.migration.${process.pid}.${Date.now()}`);
|
|
1875
|
+
mkdirSync2(staging, { recursive: true, mode: 448 });
|
|
1876
|
+
const testOneShot = false;
|
|
1877
|
+
const migrator = new PrologProcess({
|
|
1878
|
+
timeout: 120000,
|
|
1879
|
+
oneShot: testOneShot
|
|
1880
|
+
});
|
|
1881
|
+
try {
|
|
1882
|
+
if (!testOneShot)
|
|
1883
|
+
await migrator.start();
|
|
1884
|
+
const attachGoal = `kb_attach('${quoteProlog(branchPath)}')`;
|
|
1885
|
+
const migrateGoal = `kb_migrate_legacy('${quoteProlog(branchPath)}','${quoteProlog(staging)}')`;
|
|
1886
|
+
const migrated = testOneShot ? await migrator.query(`(${attachGoal}, ${migrateGoal})`) : await (async () => {
|
|
1887
|
+
const attach = await migrator.query(attachGoal);
|
|
1888
|
+
if (!attach.success)
|
|
1889
|
+
throw new Error(attach.error ?? "Failed to attach legacy KB");
|
|
1890
|
+
return migrator.query(migrateGoal);
|
|
1891
|
+
})();
|
|
1892
|
+
if (!migrated.success)
|
|
1893
|
+
throw new Error(migrated.error ?? "Legacy KB migration failed");
|
|
1894
|
+
fsyncJournaledBranchStore(staging);
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
rmSync(staging, { recursive: true, force: true });
|
|
1897
|
+
throw error;
|
|
1898
|
+
} finally {
|
|
1899
|
+
await migrator.terminate();
|
|
1900
|
+
}
|
|
1901
|
+
for (const file of ["rdf", "storage.json", "CURRENT"]) {
|
|
1902
|
+
const source = path4.join(staging, file);
|
|
1903
|
+
if (!existsSync3(source))
|
|
1904
|
+
continue;
|
|
1905
|
+
const target = path4.join(branchPath, file);
|
|
1906
|
+
if (existsSync3(target))
|
|
1907
|
+
rmSync(target, { recursive: true, force: true });
|
|
1908
|
+
renameSync(source, target);
|
|
1909
|
+
}
|
|
1910
|
+
archiveLegacyFiles(branchPath);
|
|
1911
|
+
writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
|
|
1912
|
+
`, { mode: 384 });
|
|
1913
|
+
fsyncJournaledBranchStore(branchPath);
|
|
1914
|
+
rmSync(staging, { recursive: true, force: true });
|
|
1915
|
+
}
|
|
1916
|
+
function archiveLegacyFiles(branchPath) {
|
|
1917
|
+
const legacyDir = path4.join(branchPath, "legacy");
|
|
1918
|
+
mkdirSync2(legacyDir, { recursive: true, mode: 448 });
|
|
1919
|
+
for (const file of ["kb.rdf", "audit.log"]) {
|
|
1920
|
+
const source = path4.join(branchPath, file);
|
|
1921
|
+
if (!existsSync3(source))
|
|
1922
|
+
continue;
|
|
1923
|
+
if (file === "kb.rdf") {
|
|
1924
|
+
const contents = readFileSync2(source, "utf8");
|
|
1925
|
+
if (contents.includes("kibi.rdf-journal.v1"))
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
const destination = path4.join(legacyDir, file);
|
|
1929
|
+
if (existsSync3(destination)) {
|
|
1930
|
+
renameSync(source, path4.join(legacyDir, `${file}.recovery.${Date.now()}`));
|
|
1931
|
+
} else {
|
|
1932
|
+
renameSync(source, destination);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function recoverInterruptedGeneration(branchPath) {
|
|
1937
|
+
let entries = [];
|
|
1938
|
+
try {
|
|
1939
|
+
entries = readdirSync2(branchPath);
|
|
1940
|
+
} catch {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const newest = (prefix) => {
|
|
1944
|
+
const candidates = entries.filter((entry) => entry.startsWith(prefix)).sort().reverse();
|
|
1945
|
+
const candidate = candidates[0];
|
|
1946
|
+
return candidate === undefined ? null : path4.join(branchPath, candidate);
|
|
1947
|
+
};
|
|
1948
|
+
const oldRdf = newest("rdf.old.");
|
|
1949
|
+
const oldCurrent = newest("CURRENT.old.");
|
|
1950
|
+
const rdfPath = path4.join(branchPath, "rdf");
|
|
1951
|
+
const currentPath = path4.join(branchPath, "CURRENT");
|
|
1952
|
+
if ((!existsSync3(rdfPath) || !existsSync3(currentPath)) && oldRdf && oldCurrent) {
|
|
1953
|
+
try {
|
|
1954
|
+
if (existsSync3(rdfPath))
|
|
1955
|
+
rmSync(rdfPath, { recursive: true, force: true });
|
|
1956
|
+
if (existsSync3(currentPath))
|
|
1957
|
+
rmSync(currentPath, { force: true });
|
|
1958
|
+
renameSync(oldRdf, rdfPath);
|
|
1959
|
+
renameSync(oldCurrent, currentPath);
|
|
1960
|
+
return;
|
|
1961
|
+
} catch {}
|
|
1962
|
+
}
|
|
1963
|
+
if (!existsSync3(rdfPath) && oldRdf) {
|
|
1964
|
+
try {
|
|
1965
|
+
renameSync(oldRdf, rdfPath);
|
|
1966
|
+
} catch {}
|
|
1967
|
+
}
|
|
1968
|
+
if (!existsSync3(currentPath) && oldCurrent) {
|
|
1969
|
+
try {
|
|
1970
|
+
renameSync(oldCurrent, currentPath);
|
|
1971
|
+
} catch {}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
async function runEngineDaemon(options) {
|
|
1975
|
+
if (!isValidBranchName(options.branch)) {
|
|
1976
|
+
throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
|
|
1977
|
+
}
|
|
1978
|
+
const branchPath = ensureBranchStoreManifest(options.workspaceRoot, options.branch);
|
|
1979
|
+
await ensureJournaledBranchStoreAsync(branchPath);
|
|
1980
|
+
const prolog = new PrologProcess({ timeout: 120000, oneShot: false });
|
|
1981
|
+
await prolog.start();
|
|
1982
|
+
const attached = await prolog.query(`kb_attach('${quoteProlog(branchPath)}')`);
|
|
1983
|
+
if (!attached.success)
|
|
1984
|
+
throw new Error(attached.error ?? "Failed to attach branch KB");
|
|
1985
|
+
const statusModulePath = path4.join(path4.dirname(resolveKbPlPath()), "status.pl");
|
|
1986
|
+
const statusLoaded = await prolog.query(`use_module('${quoteProlog(statusModulePath.replaceAll("\\", "/"))}')`);
|
|
1987
|
+
if (!statusLoaded.success) {
|
|
1988
|
+
throw new Error(statusLoaded.error ?? "Failed to load Kibi status module");
|
|
1989
|
+
}
|
|
1990
|
+
mkdirSync2(path4.dirname(options.socketPath), { recursive: true, mode: 448 });
|
|
1991
|
+
if (existsSync3(options.socketPath)) {
|
|
1992
|
+
let live = false;
|
|
1993
|
+
try {
|
|
1994
|
+
const existing = await connectSocket(options.socketPath, 100);
|
|
1995
|
+
existing.destroy();
|
|
1996
|
+
live = true;
|
|
1997
|
+
} catch {}
|
|
1998
|
+
if (live) {
|
|
1999
|
+
throw new Error(`A Kibi engine is already listening at ${options.socketPath}`);
|
|
2000
|
+
}
|
|
2001
|
+
try {
|
|
2002
|
+
rmSync(options.socketPath, { force: true });
|
|
2003
|
+
} catch {}
|
|
2004
|
+
}
|
|
2005
|
+
writeFileSync2(enginePidPath(options.workspaceRoot, options.branch), `${process.pid}
|
|
2006
|
+
`, { mode: 384 });
|
|
2007
|
+
const server = net.createServer();
|
|
2008
|
+
let activeClients = 0;
|
|
2009
|
+
let idleTimer = null;
|
|
2010
|
+
let queue = Promise.resolve();
|
|
2011
|
+
let idleCompactionQueued = false;
|
|
2012
|
+
const queryCache = new Map;
|
|
2013
|
+
let freshnessCache = null;
|
|
2014
|
+
const cancelledRequests = new Set;
|
|
2015
|
+
const clients = new Set;
|
|
2016
|
+
let shuttingDown = false;
|
|
2017
|
+
const exactBranchStatus = (result) => {
|
|
2018
|
+
if (!result.success || typeof result.bindings.JsonString !== "string") {
|
|
2019
|
+
return result;
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const decoded = JSON.parse(JSON.parse(result.bindings.JsonString));
|
|
2023
|
+
decoded.branch = options.branch;
|
|
2024
|
+
return {
|
|
2025
|
+
...result,
|
|
2026
|
+
bindings: {
|
|
2027
|
+
...result.bindings,
|
|
2028
|
+
JsonString: JSON.stringify(JSON.stringify(decoded))
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
} catch {
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
const scheduleIdleExit = () => {
|
|
2036
|
+
if (activeClients > 0)
|
|
2037
|
+
return;
|
|
2038
|
+
if (!idleCompactionQueued) {
|
|
2039
|
+
idleCompactionQueued = true;
|
|
2040
|
+
queue = queue.then(async () => {
|
|
2041
|
+
if (activeClients === 0) {
|
|
2042
|
+
await prolog.query("kb_storage_compact_if_needed");
|
|
2043
|
+
}
|
|
2044
|
+
}).catch(() => {
|
|
2045
|
+
return;
|
|
2046
|
+
}).finally(() => {
|
|
2047
|
+
idleCompactionQueued = false;
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
if (idleTimer)
|
|
2051
|
+
clearTimeout(idleTimer);
|
|
2052
|
+
idleTimer = setTimeout(() => {
|
|
2053
|
+
if (activeClients === 0)
|
|
2054
|
+
shutdown();
|
|
2055
|
+
}, engineIdleTimeoutMs());
|
|
2056
|
+
};
|
|
2057
|
+
const handle = async (request) => {
|
|
2058
|
+
if (request.protocolVersion !== ENGINE_PROTOCOL_VERSION) {
|
|
2059
|
+
throw new Error(`Kibi engine protocol mismatch: client=${request.protocolVersion ?? "missing"}, server=${ENGINE_PROTOCOL_VERSION}`);
|
|
2060
|
+
}
|
|
2061
|
+
const expectedPackages = process.env.KIBI_PACKAGE_VERSIONS;
|
|
2062
|
+
if (expectedPackages !== undefined && request.packageVersions !== expectedPackages) {
|
|
2063
|
+
throw new Error(`Kibi engine package-version mismatch: client=${request.packageVersions ?? "missing"}, server=${expectedPackages}`);
|
|
2064
|
+
}
|
|
2065
|
+
if (path4.resolve(request.workspaceRoot ?? "") !== path4.resolve(options.workspaceRoot) || request.branch !== options.branch) {
|
|
2066
|
+
throw new Error("Kibi engine workspace identity mismatch");
|
|
2067
|
+
}
|
|
2068
|
+
switch (request.method) {
|
|
2069
|
+
case "entities": {
|
|
2070
|
+
const limit = request.limit ?? 100;
|
|
2071
|
+
const offset = request.offset ?? 0;
|
|
2072
|
+
if (!Number.isInteger(limit) || !Number.isInteger(offset) || limit < 0 || offset < 0 || limit > 1e5) {
|
|
2073
|
+
throw new Error("entities.limit/offset must be bounded integers");
|
|
2074
|
+
}
|
|
2075
|
+
const type = request.type === undefined ? "none" : `'${quoteProlog(request.type)}'`;
|
|
2076
|
+
const id = request.entityId === undefined ? "none" : `'${quoteProlog(request.entityId)}'`;
|
|
2077
|
+
const tags = `[${(request.tags ?? []).map((tag) => `'${quoteProlog(tag)}'`).join(",")}]`;
|
|
2078
|
+
const source = request.sourceFile === undefined ? "none" : `'${quoteProlog(request.sourceFile)}'`;
|
|
2079
|
+
const result = await prolog.query(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${limit}, ${offset}, Rows, Count)`);
|
|
2080
|
+
if (!result.success) {
|
|
2081
|
+
throw new Error(result.error ?? "Indexed entity query failed");
|
|
2082
|
+
}
|
|
2083
|
+
return result;
|
|
2084
|
+
}
|
|
2085
|
+
case "search": {
|
|
2086
|
+
const limit = request.limit ?? 1e4;
|
|
2087
|
+
const offset = request.offset ?? 0;
|
|
2088
|
+
if (typeof request.searchQuery !== "string" || request.searchQuery.trim().length === 0) {
|
|
2089
|
+
throw new Error("search.searchQuery must be a non-empty string");
|
|
2090
|
+
}
|
|
2091
|
+
if (!Number.isInteger(limit) || !Number.isInteger(offset) || limit < 0 || offset < 0 || limit > 1e5) {
|
|
2092
|
+
throw new Error("search.limit/offset must be bounded integers");
|
|
2093
|
+
}
|
|
2094
|
+
const type = request.type === undefined ? "none" : `'${quoteProlog(request.type)}'`;
|
|
2095
|
+
const result = await prolog.query(`kb_search_entities(${type}, '${quoteProlog(request.searchQuery)}', ${limit}, ${offset}, Rows, Count)`);
|
|
2096
|
+
if (!result.success) {
|
|
2097
|
+
throw new Error(result.error ?? "Indexed search candidate query failed");
|
|
2098
|
+
}
|
|
2099
|
+
return result;
|
|
2100
|
+
}
|
|
2101
|
+
case "command": {
|
|
2102
|
+
const command = request.command;
|
|
2103
|
+
if (command === undefined || command.version !== 1) {
|
|
2104
|
+
throw new Error("engine.command must use version 1");
|
|
2105
|
+
}
|
|
2106
|
+
switch (command.kind) {
|
|
2107
|
+
case "status":
|
|
2108
|
+
return exactBranchStatus(await prolog.query("status:kb_status_json(JsonString)"));
|
|
2109
|
+
case "checkpoint": {
|
|
2110
|
+
queryCache.clear();
|
|
2111
|
+
freshnessCache = null;
|
|
2112
|
+
const result = await prolog.query("kb_sync_checkpoint");
|
|
2113
|
+
if (result.success)
|
|
2114
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2115
|
+
return result;
|
|
2116
|
+
}
|
|
2117
|
+
case "compact": {
|
|
2118
|
+
const result = await prolog.query("kb_storage_compact");
|
|
2119
|
+
if (result.success)
|
|
2120
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2121
|
+
return result;
|
|
2122
|
+
}
|
|
2123
|
+
case "save": {
|
|
2124
|
+
const result = await prolog.query("kb_save");
|
|
2125
|
+
if (result.success)
|
|
2126
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2127
|
+
return result;
|
|
2128
|
+
}
|
|
2129
|
+
case "check": {
|
|
2130
|
+
if (command.rule !== undefined && !/^[a-z][a-z0-9_]*$/.test(command.rule)) {
|
|
2131
|
+
throw new Error("engine.check.rule must be a lowercase rule name");
|
|
2132
|
+
}
|
|
2133
|
+
return prolog.query("checks:check_all_json(JsonString)");
|
|
2134
|
+
}
|
|
2135
|
+
case "relationship": {
|
|
2136
|
+
if (!command.type || !command.from || !command.to) {
|
|
2137
|
+
throw new Error("engine.relationship requires type, from, and to");
|
|
2138
|
+
}
|
|
2139
|
+
const type = quoteProlog(command.type);
|
|
2140
|
+
const from = quoteProlog(command.from);
|
|
2141
|
+
const to = quoteProlog(command.to);
|
|
2142
|
+
const goal = command.action === "assert" ? `kb_assert_relationship('${type}', '${from}', '${to}', [])` : `kb_retract_relationship('${type}', '${from}', '${to}')`;
|
|
2143
|
+
const result = await prolog.query(goal);
|
|
2144
|
+
if (result.success) {
|
|
2145
|
+
queryCache.clear();
|
|
2146
|
+
freshnessCache = null;
|
|
2147
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2148
|
+
}
|
|
2149
|
+
return result;
|
|
2150
|
+
}
|
|
2151
|
+
case "persistence": {
|
|
2152
|
+
const goal = command.action === "checkpoint" ? "kb_sync_checkpoint" : command.action === "compact" ? "kb_storage_compact" : command.action === "save" ? "kb_save" : command.targetDirectory ? `kb_storage_export('${quoteProlog(command.targetDirectory)}')` : null;
|
|
2153
|
+
if (goal === null) {
|
|
2154
|
+
throw new Error("engine.persistence.export requires targetDirectory");
|
|
2155
|
+
}
|
|
2156
|
+
const result = await prolog.query(goal);
|
|
2157
|
+
if (result.success)
|
|
2158
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2159
|
+
return result;
|
|
2160
|
+
}
|
|
2161
|
+
case "lifecycle": {
|
|
2162
|
+
if (command.action === "stop") {
|
|
2163
|
+
setImmediate(() => void shutdown());
|
|
2164
|
+
return { stopped: true };
|
|
2165
|
+
}
|
|
2166
|
+
if (!Number.isInteger(command.requestId)) {
|
|
2167
|
+
throw new Error("engine.lifecycle.cancel requires requestId");
|
|
2168
|
+
}
|
|
2169
|
+
const requestId = command.requestId;
|
|
2170
|
+
cancelledRequests.add(requestId);
|
|
2171
|
+
return { cancelled: requestId };
|
|
2172
|
+
}
|
|
2173
|
+
case "stop":
|
|
2174
|
+
setImmediate(() => void shutdown());
|
|
2175
|
+
return { stopped: true };
|
|
2176
|
+
case "cancel":
|
|
2177
|
+
cancelledRequests.add(command.requestId);
|
|
2178
|
+
return { cancelled: command.requestId };
|
|
2179
|
+
case "entities": {
|
|
2180
|
+
if (!Number.isInteger(command.limit) || !Number.isInteger(command.offset) || command.limit < 0 || command.offset < 0 || command.limit > 1e5) {
|
|
2181
|
+
throw new Error("engine.entities.limit/offset must be bounded integers");
|
|
2182
|
+
}
|
|
2183
|
+
const type = command.type === undefined ? "none" : `'${quoteProlog(command.type)}'`;
|
|
2184
|
+
const id = command.id === undefined ? "none" : `'${quoteProlog(command.id)}'`;
|
|
2185
|
+
const tags = `[${(command.tags ?? []).map((tag) => `'${quoteProlog(tag)}'`).join(",")}]`;
|
|
2186
|
+
const source = command.sourceFile === undefined ? "none" : `'${quoteProlog(command.sourceFile)}'`;
|
|
2187
|
+
return prolog.query(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${command.limit}, ${command.offset}, Rows, Count)`);
|
|
2188
|
+
}
|
|
2189
|
+
case "search": {
|
|
2190
|
+
if (!command.query.trim())
|
|
2191
|
+
throw new Error("engine.search.query must be non-empty");
|
|
2192
|
+
if (!Number.isInteger(command.limit) || !Number.isInteger(command.offset) || command.limit < 0 || command.offset < 0 || command.limit > 1e5) {
|
|
2193
|
+
throw new Error("engine.search.limit/offset must be bounded integers");
|
|
2194
|
+
}
|
|
2195
|
+
const type = command.type === undefined ? "none" : `'${quoteProlog(command.type)}'`;
|
|
2196
|
+
return prolog.query(`kb_search_entities(${type}, '${quoteProlog(command.query)}', ${command.limit}, ${command.offset}, Rows, Count)`);
|
|
2197
|
+
}
|
|
2198
|
+
default:
|
|
2199
|
+
throw new Error("engine.command kind is unsupported");
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
case "kbStatus": {
|
|
2203
|
+
if (freshnessCache !== null && Date.now() - freshnessCache.capturedAt <= ENGINE_FRESHNESS_CACHE_MS) {
|
|
2204
|
+
return freshnessCache.result;
|
|
2205
|
+
}
|
|
2206
|
+
const result = exactBranchStatus(await prolog.query("status:kb_status_json(JsonString)"));
|
|
2207
|
+
if (result.success) {
|
|
2208
|
+
freshnessCache = { capturedAt: Date.now(), result };
|
|
2209
|
+
}
|
|
2210
|
+
return result;
|
|
2211
|
+
}
|
|
2212
|
+
case "query": {
|
|
2213
|
+
if (typeof request.goal !== "string")
|
|
2214
|
+
throw new Error("query.goal must be a string");
|
|
2215
|
+
if (!safeEngineGoal(request.goal)) {
|
|
2216
|
+
throw new Error("Kibi engine accepts only typed Kibi predicates; arbitrary Prolog goals are not exposed");
|
|
2217
|
+
}
|
|
2218
|
+
if (mutatingEngineGoal(request.goal)) {
|
|
2219
|
+
queryCache.clear();
|
|
2220
|
+
freshnessCache = null;
|
|
2221
|
+
}
|
|
2222
|
+
if (cacheableEngineGoal(request.goal)) {
|
|
2223
|
+
const cached = queryCache.get(request.goal);
|
|
2224
|
+
if (cached !== undefined)
|
|
2225
|
+
return cached;
|
|
2226
|
+
}
|
|
2227
|
+
const result = await prolog.query(request.goal);
|
|
2228
|
+
if (result.success && mutatingEngineGoal(request.goal)) {
|
|
2229
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2230
|
+
}
|
|
2231
|
+
if (result.success && cacheableEngineGoal(request.goal) && Buffer.byteLength(JSON.stringify(result), "utf8") <= ENGINE_QUERY_CACHE_MAX_RESULT_BYTES) {
|
|
2232
|
+
if (queryCache.size >= ENGINE_QUERY_CACHE_MAX_ENTRIES) {
|
|
2233
|
+
const oldest = queryCache.keys().next().value;
|
|
2234
|
+
if (typeof oldest === "string")
|
|
2235
|
+
queryCache.delete(oldest);
|
|
2236
|
+
}
|
|
2237
|
+
queryCache.set(request.goal, result);
|
|
2238
|
+
}
|
|
2239
|
+
return result;
|
|
2240
|
+
}
|
|
2241
|
+
case "status":
|
|
2242
|
+
return prolog.query("kb_storage_status(Status), atom_json_dict(Json, Status, [])");
|
|
2243
|
+
case "checkpoint": {
|
|
2244
|
+
queryCache.clear();
|
|
2245
|
+
freshnessCache = null;
|
|
2246
|
+
const result = await prolog.query("kb_sync_checkpoint");
|
|
2247
|
+
if (result.success)
|
|
2248
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2249
|
+
return result;
|
|
2250
|
+
}
|
|
2251
|
+
case "compact": {
|
|
2252
|
+
const result = await prolog.query("kb_storage_compact");
|
|
2253
|
+
if (result.success)
|
|
2254
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2255
|
+
return result;
|
|
2256
|
+
}
|
|
2257
|
+
case "export":
|
|
2258
|
+
if (typeof request.targetDirectory !== "string")
|
|
2259
|
+
throw new Error("export.targetDirectory is required");
|
|
2260
|
+
return prolog.query(`kb_storage_export('${quoteProlog(request.targetDirectory)}')`);
|
|
2261
|
+
case "stop":
|
|
2262
|
+
setImmediate(() => void shutdown());
|
|
2263
|
+
return { stopped: true };
|
|
2264
|
+
case "cancel":
|
|
2265
|
+
if (typeof request.cancelOf === "number") {
|
|
2266
|
+
cancelledRequests.add(request.cancelOf);
|
|
2267
|
+
}
|
|
2268
|
+
return { cancelled: request.cancelOf ?? null };
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
const shutdown = async () => {
|
|
2272
|
+
if (shuttingDown)
|
|
2273
|
+
return;
|
|
2274
|
+
shuttingDown = true;
|
|
2275
|
+
process.off("SIGTERM", requestSignalShutdown);
|
|
2276
|
+
process.off("SIGINT", requestSignalShutdown);
|
|
2277
|
+
if (idleTimer)
|
|
2278
|
+
clearTimeout(idleTimer);
|
|
2279
|
+
const saved = await prolog.query("kb_save").catch(() => null);
|
|
2280
|
+
if (saved?.success) {
|
|
2281
|
+
try {
|
|
2282
|
+
fsyncJournaledBranchStore(branchPath);
|
|
2283
|
+
} catch {}
|
|
2284
|
+
}
|
|
2285
|
+
await prolog.terminate().catch(() => {
|
|
2286
|
+
return;
|
|
2287
|
+
});
|
|
2288
|
+
for (const client of clients)
|
|
2289
|
+
client.destroy();
|
|
2290
|
+
server.close();
|
|
2291
|
+
server.unref();
|
|
2292
|
+
try {
|
|
2293
|
+
if (existsSync3(options.socketPath))
|
|
2294
|
+
rmSync(options.socketPath, { force: true });
|
|
2295
|
+
if (existsSync3(enginePidPath(options.workspaceRoot, options.branch)))
|
|
2296
|
+
rmSync(enginePidPath(options.workspaceRoot, options.branch), {
|
|
2297
|
+
force: true
|
|
2298
|
+
});
|
|
2299
|
+
} catch {}
|
|
2300
|
+
setTimeout(() => process.exit(0), 50).unref();
|
|
2301
|
+
};
|
|
2302
|
+
const requestSignalShutdown = () => {
|
|
2303
|
+
shutdown();
|
|
2304
|
+
};
|
|
2305
|
+
process.once("SIGTERM", requestSignalShutdown);
|
|
2306
|
+
process.once("SIGINT", requestSignalShutdown);
|
|
2307
|
+
server.on("connection", (socket) => {
|
|
2308
|
+
clients.add(socket);
|
|
2309
|
+
activeClients += 1;
|
|
2310
|
+
let buffer = Buffer.alloc(0);
|
|
2311
|
+
socket.on("data", (chunk) => {
|
|
2312
|
+
try {
|
|
2313
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
2314
|
+
buffer = parseFrames(Buffer.concat([buffer, bytes]), (value) => {
|
|
2315
|
+
const request = value;
|
|
2316
|
+
if (request.method === "cancel" && typeof request.cancelOf === "number") {
|
|
2317
|
+
cancelledRequests.add(request.cancelOf);
|
|
2318
|
+
}
|
|
2319
|
+
queue = queue.then(async () => {
|
|
2320
|
+
if (cancelledRequests.delete(request.id)) {
|
|
2321
|
+
writeSocketFrame(socket, {
|
|
2322
|
+
id: request.id,
|
|
2323
|
+
ok: false,
|
|
2324
|
+
error: "Kibi engine request cancelled",
|
|
2325
|
+
serverPackageVersions: ENGINE_PACKAGE_VERSIONS
|
|
2326
|
+
});
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
try {
|
|
2330
|
+
const result = await handle(request);
|
|
2331
|
+
writeSocketFrame(socket, {
|
|
2332
|
+
id: request.id,
|
|
2333
|
+
ok: true,
|
|
2334
|
+
result,
|
|
2335
|
+
serverPackageVersions: ENGINE_PACKAGE_VERSIONS
|
|
2336
|
+
});
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
writeSocketFrame(socket, {
|
|
2339
|
+
id: request.id,
|
|
2340
|
+
ok: false,
|
|
2341
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2342
|
+
serverPackageVersions: ENGINE_PACKAGE_VERSIONS
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
}).catch(() => {
|
|
2346
|
+
return;
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
socket.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
socket.on("close", () => {
|
|
2354
|
+
clients.delete(socket);
|
|
2355
|
+
activeClients -= 1;
|
|
2356
|
+
scheduleIdleExit();
|
|
2357
|
+
});
|
|
2358
|
+
});
|
|
2359
|
+
await new Promise((resolve3, reject) => {
|
|
2360
|
+
server.once("error", reject);
|
|
2361
|
+
server.listen(options.socketPath, () => resolve3());
|
|
2362
|
+
});
|
|
2363
|
+
if (process.platform !== "win32") {
|
|
2364
|
+
try {
|
|
2365
|
+
chmodSync(options.socketPath, 384);
|
|
2366
|
+
} catch {}
|
|
2367
|
+
}
|
|
2368
|
+
scheduleIdleExit();
|
|
2369
|
+
await new Promise((resolve3) => {
|
|
2370
|
+
server.once("close", resolve3);
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// ../cli/src/engine-daemon.ts
|
|
2375
|
+
function requiredArg(args, name) {
|
|
2376
|
+
const index = args.indexOf(name);
|
|
2377
|
+
const value = index >= 0 ? args[index + 1] : undefined;
|
|
2378
|
+
if (value === undefined || value.length === 0) {
|
|
2379
|
+
throw new Error(`Missing ${name} argument`);
|
|
2380
|
+
}
|
|
2381
|
+
return value;
|
|
2382
|
+
}
|
|
2383
|
+
var args = process.argv.slice(2);
|
|
2384
|
+
try {
|
|
2385
|
+
await runEngineDaemon({
|
|
2386
|
+
workspaceRoot: requiredArg(args, "--workspace"),
|
|
2387
|
+
branch: requiredArg(args, "--branch"),
|
|
2388
|
+
socketPath: requiredArg(args, "--socket")
|
|
2389
|
+
});
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2392
|
+
console.error(message);
|
|
2393
|
+
const socketIndex = args.indexOf("--socket");
|
|
2394
|
+
const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
|
|
2395
|
+
if (socketPath) {
|
|
2396
|
+
try {
|
|
2397
|
+
writeFileSync3(`${socketPath}.error`, `${message}
|
|
2398
|
+
`, { mode: 384 });
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}
|
|
2401
|
+
process.exitCode = 1;
|
|
2402
|
+
}
|