kibi-cli 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -2
- package/dist/commands/aggregated-checks.d.ts.map +1 -1
- package/dist/commands/aggregated-checks.js +16 -41
- package/dist/commands/branch.d.ts +4 -1
- package/dist/commands/branch.d.ts.map +1 -1
- package/dist/commands/branch.js +62 -12
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +42 -23
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +57 -0
- package/dist/commands/gc.d.ts.map +1 -1
- package/dist/commands/gc.js +24 -1
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +46 -28
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +23 -2
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +19 -10
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +348 -203
- package/dist/diagnostics.d.ts +61 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +114 -0
- package/dist/extractors/markdown.d.ts +1 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +47 -0
- package/dist/public/branch-resolver.d.ts +2 -0
- package/dist/public/branch-resolver.d.ts.map +1 -0
- package/dist/public/branch-resolver.js +1 -0
- package/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +13 -3
- package/dist/traceability/markdown-validate.d.ts +7 -0
- package/dist/traceability/markdown-validate.d.ts.map +1 -0
- package/dist/traceability/markdown-validate.js +35 -0
- package/dist/utils/branch-resolver.d.ts +79 -0
- package/dist/utils/branch-resolver.d.ts.map +1 -0
- package/dist/utils/branch-resolver.js +311 -0
- package/dist/utils/config.d.ts +47 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +105 -0
- package/package.json +15 -9
- package/src/public/branch-resolver.ts +1 -0
package/dist/commands/sync.js
CHANGED
|
@@ -42,16 +42,20 @@
|
|
|
42
42
|
fi
|
|
43
43
|
done
|
|
44
44
|
*/
|
|
45
|
+
import { execSync } from "node:child_process";
|
|
45
46
|
import { createHash } from "node:crypto";
|
|
46
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
47
|
-
import * as fs from "node:fs";
|
|
47
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
48
48
|
import * as path from "node:path";
|
|
49
49
|
import fg from "fast-glob";
|
|
50
50
|
import { dump as dumpYAML, load as parseYAML } from "js-yaml";
|
|
51
51
|
import { extractFromManifest } from "../extractors/manifest.js";
|
|
52
|
-
import { extractFromMarkdown, } from "../extractors/markdown.js";
|
|
52
|
+
import { FrontmatterError, extractFromMarkdown, } from "../extractors/markdown.js";
|
|
53
|
+
import { copyFileSync } from "node:fs";
|
|
54
|
+
import { branchErrorToDiagnostic, createDocsNotIndexedDiagnostic, createInvalidAuthoringDiagnostic, createKbMissingDiagnostic, formatSyncSummary, } from "../diagnostics.js";
|
|
53
55
|
import { enrichSymbolCoordinates, } from "../extractors/symbols-coordinator.js";
|
|
54
56
|
import { PrologProcess } from "../prolog.js";
|
|
57
|
+
import { copyCleanSnapshot, getBranchDiagnostic, resolveActiveBranch, resolveDefaultBranch, } from "../utils/branch-resolver.js";
|
|
58
|
+
import { loadSyncConfig } from "../utils/config.js";
|
|
55
59
|
export class SyncError extends Error {
|
|
56
60
|
constructor(message) {
|
|
57
61
|
super(message);
|
|
@@ -130,24 +134,117 @@ function writeSyncCache(cachePath, cache) {
|
|
|
130
134
|
writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}
|
|
131
135
|
`, "utf8");
|
|
132
136
|
}
|
|
137
|
+
function copySyncCache(livePath, stagingPath) {
|
|
138
|
+
const liveCachePath = path.join(livePath, "sync-cache.json");
|
|
139
|
+
const stagingCachePath = path.join(stagingPath, "sync-cache.json");
|
|
140
|
+
if (existsSync(liveCachePath)) {
|
|
141
|
+
const cacheContent = readFileSync(liveCachePath, "utf8");
|
|
142
|
+
writeFileSync(stagingCachePath, cacheContent, "utf8");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function copySchemaToStaging(stagingPath) {
|
|
146
|
+
const possibleSchemaPaths = [
|
|
147
|
+
path.resolve(process.cwd(), "node_modules", "kibi-cli", "schema"),
|
|
148
|
+
path.resolve(process.cwd(), "..", "..", "schema"),
|
|
149
|
+
path.resolve(import.meta.dirname || __dirname, "..", "..", "schema"),
|
|
150
|
+
path.resolve(process.cwd(), "packages", "cli", "schema"),
|
|
151
|
+
];
|
|
152
|
+
let schemaSourceDir = null;
|
|
153
|
+
for (const p of possibleSchemaPaths) {
|
|
154
|
+
if (existsSync(p)) {
|
|
155
|
+
schemaSourceDir = p;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!schemaSourceDir) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const schemaFiles = await fg("*.pl", {
|
|
163
|
+
cwd: schemaSourceDir,
|
|
164
|
+
absolute: false,
|
|
165
|
+
});
|
|
166
|
+
const schemaDestDir = path.join(stagingPath, "schema");
|
|
167
|
+
if (!existsSync(schemaDestDir)) {
|
|
168
|
+
mkdirSync(schemaDestDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
for (const file of schemaFiles) {
|
|
171
|
+
const sourcePath = path.join(schemaSourceDir, file);
|
|
172
|
+
const destPath = path.join(schemaDestDir, file);
|
|
173
|
+
copyFileSync(sourcePath, destPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function validateStagingKB(stagingPath) {
|
|
177
|
+
const prolog = new PrologProcess({ timeout: 60000 });
|
|
178
|
+
await prolog.start();
|
|
179
|
+
try {
|
|
180
|
+
const attachResult = await prolog.query(`kb_attach('${stagingPath}')`);
|
|
181
|
+
if (!attachResult.success) {
|
|
182
|
+
console.error(`Failed to attach to staging KB: ${attachResult.error}`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
await prolog.query("kb_detach");
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
console.error(`Validation error: ${message}`);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await prolog.terminate();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function atomicPublish(stagingPath, livePath) {
|
|
198
|
+
const liveParent = path.dirname(livePath);
|
|
199
|
+
if (!existsSync(liveParent)) {
|
|
200
|
+
mkdirSync(liveParent, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
if (existsSync(livePath)) {
|
|
203
|
+
const tempPath = `${livePath}.old.${Date.now()}`;
|
|
204
|
+
renameSync(livePath, tempPath);
|
|
205
|
+
renameSync(stagingPath, livePath);
|
|
206
|
+
rmSync(tempPath, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
renameSync(stagingPath, livePath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function cleanupStaging(stagingPath) {
|
|
213
|
+
if (existsSync(stagingPath)) {
|
|
214
|
+
rmSync(stagingPath, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
133
217
|
export async function syncCommand(options = {}) {
|
|
134
218
|
const validateOnly = options.validateOnly ?? false;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
219
|
+
const rebuild = options.rebuild ?? false;
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
const diagnostics = [];
|
|
222
|
+
const entityCounts = {};
|
|
223
|
+
const relationshipCount = 0;
|
|
224
|
+
let published = false;
|
|
225
|
+
let currentBranch;
|
|
226
|
+
const getCurrentCommit = () => {
|
|
138
227
|
try {
|
|
139
|
-
|
|
140
|
-
const branch = execSync("git branch --show-current", {
|
|
228
|
+
return execSync("git rev-parse HEAD", {
|
|
141
229
|
cwd: process.cwd(),
|
|
142
230
|
encoding: "utf8",
|
|
231
|
+
timeout: 5000,
|
|
232
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
143
233
|
}).trim();
|
|
144
|
-
if (branch && branch !== "master") {
|
|
145
|
-
currentBranch = branch;
|
|
146
|
-
}
|
|
147
234
|
}
|
|
148
235
|
catch {
|
|
149
|
-
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
try {
|
|
240
|
+
const branchResult = resolveActiveBranch(process.cwd());
|
|
241
|
+
if ("error" in branchResult) {
|
|
242
|
+
const diagnostic = branchErrorToDiagnostic(branchResult.code, branchResult.error);
|
|
243
|
+
diagnostics.push(diagnostic);
|
|
244
|
+
console.error(getBranchDiagnostic(undefined, branchResult.error));
|
|
245
|
+
throw new SyncError(`Failed to resolve active branch: ${branchResult.error}`);
|
|
150
246
|
}
|
|
247
|
+
currentBranch = branchResult.branch;
|
|
151
248
|
if (process.env.KIBI_DEBUG) {
|
|
152
249
|
try {
|
|
153
250
|
// eslint-disable-next-line no-console
|
|
@@ -155,33 +252,8 @@ export async function syncCommand(options = {}) {
|
|
|
155
252
|
}
|
|
156
253
|
catch { }
|
|
157
254
|
}
|
|
158
|
-
// Load config
|
|
159
|
-
const
|
|
160
|
-
paths: {
|
|
161
|
-
requirements: "requirements/**/*.md",
|
|
162
|
-
scenarios: "scenarios/**/*.md",
|
|
163
|
-
tests: "tests/**/*.md",
|
|
164
|
-
adr: "adr/**/*.md",
|
|
165
|
-
flags: "flags/**/*.md",
|
|
166
|
-
events: "events/**/*.md",
|
|
167
|
-
facts: "facts/**/*.md",
|
|
168
|
-
symbols: "symbols.yaml",
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
const configPath = path.join(process.cwd(), ".kb/config.json");
|
|
172
|
-
let config;
|
|
173
|
-
try {
|
|
174
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
175
|
-
config = {
|
|
176
|
-
paths: {
|
|
177
|
-
...DEFAULT_CONFIG.paths,
|
|
178
|
-
...(parsed.paths ?? {}),
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
config = DEFAULT_CONFIG;
|
|
184
|
-
}
|
|
255
|
+
// Load config using shared loader
|
|
256
|
+
const config = loadSyncConfig(process.cwd());
|
|
185
257
|
const paths = config.paths;
|
|
186
258
|
// Discover files - construct glob patterns from directory paths
|
|
187
259
|
const normalizeMarkdownPath = (pattern) => {
|
|
@@ -213,10 +285,12 @@ export async function syncCommand(options = {}) {
|
|
|
213
285
|
}
|
|
214
286
|
catch { }
|
|
215
287
|
}
|
|
216
|
-
const manifestFiles =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
288
|
+
const manifestFiles = paths.symbols
|
|
289
|
+
? await fg(paths.symbols, {
|
|
290
|
+
cwd: process.cwd(),
|
|
291
|
+
absolute: true,
|
|
292
|
+
})
|
|
293
|
+
: [];
|
|
220
294
|
const sourceFiles = [...markdownFiles, ...manifestFiles].sort();
|
|
221
295
|
// Use branch-specific cache to handle branch isolation correctly
|
|
222
296
|
const cachePath = path.join(process.cwd(), `.kb/branches/${currentBranch}/sync-cache.json`);
|
|
@@ -227,6 +301,12 @@ export async function syncCommand(options = {}) {
|
|
|
227
301
|
const nextSeenAt = {};
|
|
228
302
|
const changedMarkdownFiles = [];
|
|
229
303
|
const changedManifestFiles = [];
|
|
304
|
+
if (process.env.KIBI_DEBUG) {
|
|
305
|
+
// eslint-disable-next-line no-console
|
|
306
|
+
console.log("[kibi-debug] sourceFiles:", sourceFiles);
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.log("[kibi-debug] syncCache.hashes:", syncCache.hashes);
|
|
309
|
+
}
|
|
230
310
|
for (const file of sourceFiles) {
|
|
231
311
|
try {
|
|
232
312
|
const key = toCacheKey(file);
|
|
@@ -238,7 +318,12 @@ export async function syncCommand(options = {}) {
|
|
|
238
318
|
: nowMs - lastSeenMs > SYNC_CACHE_TTL_MS;
|
|
239
319
|
nextHashes[key] = hash;
|
|
240
320
|
nextSeenAt[key] = nowIso;
|
|
241
|
-
|
|
321
|
+
const isChanged = expired || syncCache.hashes[key] !== hash || validateOnly;
|
|
322
|
+
if (process.env.KIBI_DEBUG) {
|
|
323
|
+
// eslint-disable-next-line no-console
|
|
324
|
+
console.log(`[kibi-debug] ${key}: cached=${syncCache.hashes[key]}, current=${hash}, changed=${isChanged}`);
|
|
325
|
+
}
|
|
326
|
+
if (isChanged) {
|
|
242
327
|
if (markdownFiles.includes(file)) {
|
|
243
328
|
changedMarkdownFiles.push(file);
|
|
244
329
|
}
|
|
@@ -252,6 +337,10 @@ export async function syncCommand(options = {}) {
|
|
|
252
337
|
console.warn(`Warning: Failed to hash ${file}: ${message}`);
|
|
253
338
|
}
|
|
254
339
|
}
|
|
340
|
+
if (process.env.KIBI_DEBUG) {
|
|
341
|
+
// eslint-disable-next-line no-console
|
|
342
|
+
console.log("[kibi-debug] changedMarkdownFiles:", changedMarkdownFiles);
|
|
343
|
+
}
|
|
255
344
|
const results = [];
|
|
256
345
|
const failedCacheKeys = new Set();
|
|
257
346
|
const errors = [];
|
|
@@ -261,6 +350,18 @@ export async function syncCommand(options = {}) {
|
|
|
261
350
|
}
|
|
262
351
|
catch (error) {
|
|
263
352
|
const message = error instanceof Error ? error.message : String(error);
|
|
353
|
+
// Handle INVALID_AUTHORING diagnostics for embedded entities
|
|
354
|
+
if (error instanceof FrontmatterError &&
|
|
355
|
+
error.classification === "Embedded Entity Violation") {
|
|
356
|
+
const embeddedTypes = message.includes("scenario") && message.includes("test")
|
|
357
|
+
? ["scenario", "test"]
|
|
358
|
+
: message.includes("scenario")
|
|
359
|
+
? ["scenario"]
|
|
360
|
+
: message.includes("test")
|
|
361
|
+
? ["test"]
|
|
362
|
+
: ["entity"];
|
|
363
|
+
diagnostics.push(createInvalidAuthoringDiagnostic(file, embeddedTypes));
|
|
364
|
+
}
|
|
264
365
|
if (validateOnly) {
|
|
265
366
|
errors.push({ file, message });
|
|
266
367
|
}
|
|
@@ -308,7 +409,7 @@ export async function syncCommand(options = {}) {
|
|
|
308
409
|
console.warn(`Warning: Failed to refresh symbol coordinates in ${file}: ${message}`);
|
|
309
410
|
}
|
|
310
411
|
}
|
|
311
|
-
if (results.length === 0) {
|
|
412
|
+
if (results.length === 0 && !rebuild) {
|
|
312
413
|
const evictedHashes = {};
|
|
313
414
|
const evictedSeenAt = {};
|
|
314
415
|
for (const [key, hash] of Object.entries(nextHashes)) {
|
|
@@ -323,193 +424,237 @@ export async function syncCommand(options = {}) {
|
|
|
323
424
|
hashes: evictedHashes,
|
|
324
425
|
seenAt: evictedSeenAt,
|
|
325
426
|
});
|
|
326
|
-
console.log("✓ Imported 0 entities, 0 relationships");
|
|
427
|
+
console.log("✓ Imported 0 entities, 0 relationships (no changes)");
|
|
327
428
|
process.exit(0);
|
|
328
429
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// If branch KB doesn't exist but main does, copy from main (copy-on-write)
|
|
335
|
-
// Skip for orphan branches (branches with no commits yet)
|
|
336
|
-
if (!existsSync(kbPath) && existsSync(mainPath)) {
|
|
337
|
-
const hasCommits = (() => {
|
|
338
|
-
try {
|
|
339
|
-
const { execSync } = require("node:child_process");
|
|
340
|
-
execSync("git rev-parse HEAD", { cwd: process.cwd(), stdio: "pipe" });
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
})();
|
|
347
|
-
if (hasCommits) {
|
|
348
|
-
fs.cpSync(mainPath, kbPath, { recursive: true });
|
|
349
|
-
// Remove copied sync cache to avoid cross-branch cache pollution
|
|
350
|
-
try {
|
|
351
|
-
const copiedCache = path.join(kbPath, "sync-cache.json");
|
|
352
|
-
if (existsSync(copiedCache)) {
|
|
353
|
-
fs.rmSync(copiedCache);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
// ignore errors cleaning up cache
|
|
358
|
-
}
|
|
359
|
-
}
|
|
430
|
+
const livePath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
|
|
431
|
+
// Check if KB exists (for diagnostic purposes)
|
|
432
|
+
const kbExists = existsSync(livePath);
|
|
433
|
+
if (!kbExists && !rebuild) {
|
|
434
|
+
diagnostics.push(createKbMissingDiagnostic(currentBranch, livePath));
|
|
360
435
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
`updated_at="${entity.updated_at}"`,
|
|
379
|
-
`source="${entity.source.replace(/"/g, '\\"')}"`,
|
|
380
|
-
];
|
|
381
|
-
if (entity.tags && entity.tags.length > 0) {
|
|
382
|
-
const tagsList = entity.tags.map(prologAtom).join(",");
|
|
383
|
-
props.push(`tags=[${tagsList}]`);
|
|
436
|
+
const stagingPath = path.join(process.cwd(), `.kb/branches/${currentBranch}.staging`);
|
|
437
|
+
cleanupStaging(stagingPath);
|
|
438
|
+
mkdirSync(stagingPath, { recursive: true });
|
|
439
|
+
try {
|
|
440
|
+
if (!rebuild) {
|
|
441
|
+
const config = loadSyncConfig(process.cwd());
|
|
442
|
+
const defaultBranchResult = resolveDefaultBranch(process.cwd(), config);
|
|
443
|
+
const defaultBranch = "branch" in defaultBranchResult ? defaultBranchResult.branch : "main";
|
|
444
|
+
const defaultBranchPath = path.join(process.cwd(), `.kb/branches/${defaultBranch}`);
|
|
445
|
+
const sourcePath = existsSync(livePath)
|
|
446
|
+
? livePath
|
|
447
|
+
: existsSync(defaultBranchPath) && currentBranch !== defaultBranch
|
|
448
|
+
? defaultBranchPath
|
|
449
|
+
: null;
|
|
450
|
+
if (sourcePath) {
|
|
451
|
+
copyCleanSnapshot(sourcePath, stagingPath);
|
|
452
|
+
copySyncCache(sourcePath, stagingPath);
|
|
384
453
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (entity.priority)
|
|
388
|
-
props.push(`priority=${prologAtom(entity.priority)}`);
|
|
389
|
-
if (entity.severity)
|
|
390
|
-
props.push(`severity=${prologAtom(entity.severity)}`);
|
|
391
|
-
if (entity.text_ref)
|
|
392
|
-
props.push(`text_ref="${entity.text_ref}"`);
|
|
393
|
-
const propsList = `[${props.join(", ")}]`;
|
|
394
|
-
const goal = `kb_assert_entity(${entity.type}, ${propsList})`;
|
|
395
|
-
const result = await prolog.query(goal);
|
|
396
|
-
if (result.success) {
|
|
397
|
-
entityCount++;
|
|
398
|
-
kbModified = true;
|
|
454
|
+
else {
|
|
455
|
+
await copySchemaToStaging(stagingPath);
|
|
399
456
|
}
|
|
400
457
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
console.warn(`Warning: Failed to upsert entity ${entity.id}: ${message}`);
|
|
458
|
+
else {
|
|
459
|
+
await copySchemaToStaging(stagingPath);
|
|
404
460
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
461
|
+
const prolog = new PrologProcess({ timeout: 120000 });
|
|
462
|
+
await prolog.start();
|
|
463
|
+
const attachResult = await prolog.query(`kb_attach('${stagingPath}')`);
|
|
464
|
+
if (!attachResult.success) {
|
|
465
|
+
await prolog.terminate();
|
|
466
|
+
throw new SyncError(`Failed to attach to staging KB: ${attachResult.error || "Unknown error"}`);
|
|
467
|
+
}
|
|
468
|
+
let entityCount = 0;
|
|
469
|
+
let kbModified = false;
|
|
470
|
+
// Track entity counts by type
|
|
471
|
+
for (const { entity } of results) {
|
|
472
|
+
entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
|
|
473
|
+
}
|
|
474
|
+
const simplePrologAtom = /^[a-z][a-zA-Z0-9_]*$/;
|
|
475
|
+
const prologAtom = (value) => simplePrologAtom.test(value) ? value : `'${value.replace(/'/g, "''")}'`;
|
|
476
|
+
for (const { entity } of results) {
|
|
419
477
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
478
|
+
const props = [
|
|
479
|
+
`id='${entity.id}'`,
|
|
480
|
+
`title="${entity.title.replace(/"/g, '\\"')}"`,
|
|
481
|
+
`status=${prologAtom(entity.status)}`,
|
|
482
|
+
`created_at="${entity.created_at}"`,
|
|
483
|
+
`updated_at="${entity.updated_at}"`,
|
|
484
|
+
`source="${entity.source.replace(/"/g, '\\"')}"`,
|
|
485
|
+
];
|
|
486
|
+
if (entity.tags && entity.tags.length > 0) {
|
|
487
|
+
const tagsList = entity.tags.map(prologAtom).join(",");
|
|
488
|
+
props.push(`tags=[${tagsList}]`);
|
|
489
|
+
}
|
|
490
|
+
if (entity.owner)
|
|
491
|
+
props.push(`owner=${prologAtom(entity.owner)}`);
|
|
492
|
+
if (entity.priority)
|
|
493
|
+
props.push(`priority=${prologAtom(entity.priority)}`);
|
|
494
|
+
if (entity.severity)
|
|
495
|
+
props.push(`severity=${prologAtom(entity.severity)}`);
|
|
496
|
+
if (entity.text_ref)
|
|
497
|
+
props.push(`text_ref="${entity.text_ref}"`);
|
|
498
|
+
const propsList = `[${props.join(", ")}]`;
|
|
499
|
+
const goal = `kb_assert_entity(${entity.type}, ${propsList})`;
|
|
423
500
|
const result = await prolog.query(goal);
|
|
424
501
|
if (result.success) {
|
|
425
|
-
|
|
502
|
+
entityCount++;
|
|
426
503
|
kbModified = true;
|
|
427
504
|
}
|
|
428
|
-
else {
|
|
429
|
-
failedRelationships.push({ rel, fromId, toId, error: result.error || "Unknown error" });
|
|
430
|
-
}
|
|
431
505
|
}
|
|
432
506
|
catch (error) {
|
|
433
507
|
const message = error instanceof Error ? error.message : String(error);
|
|
434
|
-
|
|
435
|
-
const toId = idLookup.get(rel.to) || rel.to;
|
|
436
|
-
failedRelationships.push({ rel, fromId, toId, error: message });
|
|
508
|
+
console.warn(`Warning: Failed to upsert entity ${entity.id}: ${message}`);
|
|
437
509
|
}
|
|
438
510
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
511
|
+
const idLookup = new Map();
|
|
512
|
+
for (const { entity } of results) {
|
|
513
|
+
const filename = path.basename(entity.source, ".md");
|
|
514
|
+
idLookup.set(filename, entity.id);
|
|
515
|
+
idLookup.set(entity.id, entity.id);
|
|
516
|
+
}
|
|
517
|
+
let relCount = 0;
|
|
518
|
+
const failedRelationships = [];
|
|
519
|
+
for (const { relationships } of results) {
|
|
520
|
+
for (const rel of relationships) {
|
|
521
|
+
try {
|
|
522
|
+
const fromId = idLookup.get(rel.from) || rel.from;
|
|
523
|
+
const toId = idLookup.get(rel.to) || rel.to;
|
|
524
|
+
const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
|
|
525
|
+
const result = await prolog.query(goal);
|
|
526
|
+
if (result.success) {
|
|
527
|
+
relCount++;
|
|
528
|
+
kbModified = true;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
failedRelationships.push({
|
|
532
|
+
rel,
|
|
533
|
+
fromId,
|
|
534
|
+
toId,
|
|
535
|
+
error: result.error || "Unknown error",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
451
538
|
}
|
|
452
|
-
|
|
453
|
-
|
|
539
|
+
catch (error) {
|
|
540
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
+
const fromId = idLookup.get(rel.from) || rel.from;
|
|
542
|
+
const toId = idLookup.get(rel.to) || rel.to;
|
|
543
|
+
failedRelationships.push({ rel, fromId, toId, error: message });
|
|
454
544
|
}
|
|
455
545
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
546
|
+
}
|
|
547
|
+
const retryCount = 3;
|
|
548
|
+
for (let pass = 0; pass < retryCount && failedRelationships.length > 0; pass++) {
|
|
549
|
+
const remainingFailed = [];
|
|
550
|
+
for (const { rel, fromId, toId } of failedRelationships) {
|
|
551
|
+
try {
|
|
552
|
+
const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
|
|
553
|
+
const result = await prolog.query(goal);
|
|
554
|
+
if (result.success) {
|
|
555
|
+
relCount++;
|
|
556
|
+
kbModified = true;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
remainingFailed.push({
|
|
560
|
+
rel,
|
|
561
|
+
fromId,
|
|
562
|
+
toId,
|
|
563
|
+
error: result.error || "Unknown error",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
569
|
+
remainingFailed.push({ rel, fromId, toId, error: message });
|
|
570
|
+
}
|
|
459
571
|
}
|
|
572
|
+
failedRelationships.length = 0;
|
|
573
|
+
failedRelationships.push(...remainingFailed);
|
|
460
574
|
}
|
|
461
|
-
failedRelationships.length
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
seen.add(key);
|
|
472
|
-
console.warn(` - ${rel.type}: ${fromId} -> ${toId}`);
|
|
473
|
-
console.warn(` Error: ${error}`);
|
|
575
|
+
if (failedRelationships.length > 0) {
|
|
576
|
+
console.warn(`\nWarning: ${failedRelationships.length} relationship(s) failed to sync:`);
|
|
577
|
+
const seen = new Set();
|
|
578
|
+
for (const { rel, fromId, toId, error } of failedRelationships) {
|
|
579
|
+
const key = `${rel.type}:${fromId}->${toId}`;
|
|
580
|
+
if (!seen.has(key)) {
|
|
581
|
+
seen.add(key);
|
|
582
|
+
console.warn(` - ${rel.type}: ${fromId} -> ${toId}`);
|
|
583
|
+
console.warn(` Error: ${error}`);
|
|
584
|
+
}
|
|
474
585
|
}
|
|
586
|
+
console.warn("\nTip: Ensure target entities exist before creating relationships.");
|
|
475
587
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
prolog.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
588
|
+
if (kbModified) {
|
|
589
|
+
prolog.invalidateCache();
|
|
590
|
+
}
|
|
591
|
+
await prolog.query("kb_save");
|
|
592
|
+
await prolog.query("kb_detach");
|
|
593
|
+
await prolog.terminate();
|
|
594
|
+
atomicPublish(stagingPath, livePath);
|
|
595
|
+
const evictedHashes = {};
|
|
596
|
+
const evictedSeenAt = {};
|
|
597
|
+
for (const [key, hash] of Object.entries(nextHashes)) {
|
|
598
|
+
if (failedCacheKeys.has(key)) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
evictedHashes[key] = hash;
|
|
602
|
+
evictedSeenAt[key] = nextSeenAt[key] ?? nowIso;
|
|
490
603
|
}
|
|
491
|
-
|
|
492
|
-
|
|
604
|
+
const liveCachePath = path.join(livePath, "sync-cache.json");
|
|
605
|
+
writeSyncCache(liveCachePath, {
|
|
606
|
+
version: SYNC_CACHE_VERSION,
|
|
607
|
+
hashes: evictedHashes,
|
|
608
|
+
seenAt: evictedSeenAt,
|
|
609
|
+
});
|
|
610
|
+
published = true;
|
|
611
|
+
if (markdownFiles.length > 0 && entityCount < markdownFiles.length) {
|
|
612
|
+
diagnostics.push(createDocsNotIndexedDiagnostic(markdownFiles.length, entityCount));
|
|
613
|
+
}
|
|
614
|
+
console.log(`✓ Imported ${entityCount} entities, ${relCount} relationships`);
|
|
615
|
+
const commit = getCurrentCommit();
|
|
616
|
+
const summary = {
|
|
617
|
+
branch: currentBranch,
|
|
618
|
+
commit,
|
|
619
|
+
timestamp: new Date().toISOString(),
|
|
620
|
+
entityCounts,
|
|
621
|
+
relationshipCount: relCount,
|
|
622
|
+
success: true,
|
|
623
|
+
published,
|
|
624
|
+
failures: diagnostics,
|
|
625
|
+
durationMs: Date.now() - startTime,
|
|
626
|
+
};
|
|
627
|
+
console.log(formatSyncSummary(summary));
|
|
628
|
+
return summary;
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
cleanupStaging(stagingPath);
|
|
632
|
+
throw error;
|
|
493
633
|
}
|
|
494
|
-
writeSyncCache(cachePath, {
|
|
495
|
-
version: SYNC_CACHE_VERSION,
|
|
496
|
-
hashes: evictedHashes,
|
|
497
|
-
seenAt: evictedSeenAt,
|
|
498
|
-
});
|
|
499
|
-
console.log(`✓ Imported ${entityCount} entities, ${relCount} relationships`);
|
|
500
|
-
process.exit(0);
|
|
501
634
|
}
|
|
502
635
|
catch (error) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
636
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
637
|
+
console.error(`Error: ${errorMessage}`);
|
|
638
|
+
// Return failure summary
|
|
639
|
+
const commit = getCurrentCommit();
|
|
640
|
+
const summary = {
|
|
641
|
+
branch: currentBranch || "unknown",
|
|
642
|
+
commit,
|
|
643
|
+
timestamp: new Date().toISOString(),
|
|
644
|
+
entityCounts,
|
|
645
|
+
relationshipCount,
|
|
646
|
+
success: false,
|
|
647
|
+
published: false,
|
|
648
|
+
failures: diagnostics,
|
|
649
|
+
durationMs: Date.now() - startTime,
|
|
650
|
+
};
|
|
651
|
+
if (diagnostics.length > 0) {
|
|
652
|
+
console.log("\nDiagnostics:");
|
|
653
|
+
for (const d of diagnostics) {
|
|
654
|
+
console.log(` [${d.category}] ${d.severity}: ${d.message}`);
|
|
655
|
+
}
|
|
511
656
|
}
|
|
512
|
-
|
|
657
|
+
throw error;
|
|
513
658
|
}
|
|
514
659
|
}
|
|
515
660
|
async function refreshManifestCoordinates(manifestPath, workspaceRoot) {
|