kibi-cli 0.1.7 → 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.
Files changed (41) hide show
  1. package/dist/cli.js +4 -2
  2. package/dist/commands/branch.d.ts +4 -1
  3. package/dist/commands/branch.d.ts.map +1 -1
  4. package/dist/commands/branch.js +62 -12
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +27 -3
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +57 -0
  9. package/dist/commands/gc.d.ts.map +1 -1
  10. package/dist/commands/gc.js +24 -1
  11. package/dist/commands/init-helpers.d.ts.map +1 -1
  12. package/dist/commands/init-helpers.js +46 -28
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +23 -2
  15. package/dist/commands/query.d.ts.map +1 -1
  16. package/dist/commands/query.js +19 -10
  17. package/dist/commands/sync.d.ts +3 -1
  18. package/dist/commands/sync.d.ts.map +1 -1
  19. package/dist/commands/sync.js +348 -203
  20. package/dist/diagnostics.d.ts +61 -0
  21. package/dist/diagnostics.d.ts.map +1 -0
  22. package/dist/diagnostics.js +114 -0
  23. package/dist/extractors/markdown.d.ts +1 -0
  24. package/dist/extractors/markdown.d.ts.map +1 -1
  25. package/dist/extractors/markdown.js +47 -0
  26. package/dist/public/branch-resolver.d.ts +2 -0
  27. package/dist/public/branch-resolver.d.ts.map +1 -0
  28. package/dist/public/branch-resolver.js +1 -0
  29. package/dist/traceability/git-staged.d.ts.map +1 -1
  30. package/dist/traceability/git-staged.js +13 -3
  31. package/dist/traceability/markdown-validate.d.ts +7 -0
  32. package/dist/traceability/markdown-validate.d.ts.map +1 -0
  33. package/dist/traceability/markdown-validate.js +35 -0
  34. package/dist/utils/branch-resolver.d.ts +79 -0
  35. package/dist/utils/branch-resolver.d.ts.map +1 -0
  36. package/dist/utils/branch-resolver.js +311 -0
  37. package/dist/utils/config.d.ts +47 -0
  38. package/dist/utils/config.d.ts.map +1 -0
  39. package/dist/utils/config.js +105 -0
  40. package/package.json +13 -1
  41. package/src/public/branch-resolver.ts +1 -0
@@ -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
- try {
136
- // Detect current branch early (needed for cache and KB paths)
137
- let currentBranch = "main";
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
- const { execSync } = await import("node:child_process");
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
- currentBranch = "main";
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 (fall back to defaults if missing)
159
- const DEFAULT_CONFIG = {
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 = await fg(paths.symbols, {
217
- cwd: process.cwd(),
218
- absolute: true,
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
- if (expired || syncCache.hashes[key] !== hash || validateOnly) {
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
- // Connect to KB
330
- const prolog = new PrologProcess({ timeout: 120000 });
331
- await prolog.start();
332
- const kbPath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
333
- const mainPath = path.join(process.cwd(), ".kb/branches/main");
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 attachResult = await prolog.query(`kb_attach('${kbPath}')`);
362
- if (!attachResult.success) {
363
- await prolog.terminate();
364
- throw new SyncError(`Failed to attach KB: ${attachResult.error || "Unknown error"}`);
365
- }
366
- // Upsert entities
367
- let entityCount = 0;
368
- let kbModified = false;
369
- const simplePrologAtom = /^[a-z][a-zA-Z0-9_]*$/;
370
- const prologAtom = (value) => simplePrologAtom.test(value) ? value : `'${value.replace(/'/g, "''")}'`;
371
- for (const { entity } of results) {
372
- try {
373
- const props = [
374
- `id='${entity.id}'`,
375
- `title="${entity.title.replace(/"/g, '\\"')}"`,
376
- `status=${prologAtom(entity.status)}`,
377
- `created_at="${entity.created_at}"`,
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
- if (entity.owner)
386
- props.push(`owner=${prologAtom(entity.owner)}`);
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
- catch (error) {
402
- const message = error instanceof Error ? error.message : String(error);
403
- console.warn(`Warning: Failed to upsert entity ${entity.id}: ${message}`);
458
+ else {
459
+ await copySchemaToStaging(stagingPath);
404
460
  }
405
- }
406
- // Build ID lookup map: filename -> entity ID
407
- const idLookup = new Map();
408
- for (const { entity } of results) {
409
- const filename = path.basename(entity.source, ".md");
410
- idLookup.set(filename, entity.id);
411
- idLookup.set(entity.id, entity.id);
412
- }
413
- // Assert relationships - two-pass approach to handle targets that don't exist yet
414
- let relCount = 0;
415
- const failedRelationships = [];
416
- // First pass: try all relationships
417
- for (const { relationships } of results) {
418
- for (const rel of relationships) {
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 fromId = idLookup.get(rel.from) || rel.from;
421
- const toId = idLookup.get(rel.to) || rel.to;
422
- const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
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
- relCount++;
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
- const fromId = idLookup.get(rel.from) || rel.from;
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
- // Second pass: retry failed relationships (targets may have been created in first pass)
441
- const retryCount = 3;
442
- for (let pass = 0; pass < retryCount && failedRelationships.length > 0; pass++) {
443
- const remainingFailed = [];
444
- for (const { rel, fromId, toId, error } of failedRelationships) {
445
- try {
446
- const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
447
- const result = await prolog.query(goal);
448
- if (result.success) {
449
- relCount++;
450
- kbModified = true;
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
- else {
453
- remainingFailed.push({ rel, fromId, toId, error: result.error || "Unknown error" });
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
- catch (err) {
457
- const message = err instanceof Error ? err.message : String(err);
458
- remainingFailed.push({ rel, fromId, toId, error: message });
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 = 0;
462
- failedRelationships.push(...remainingFailed);
463
- }
464
- // Report remaining failed relationships after all passes
465
- if (failedRelationships.length > 0) {
466
- console.warn(`\nWarning: ${failedRelationships.length} relationship(s) failed to sync:`);
467
- const seen = new Set();
468
- for (const { rel, fromId, toId, error } of failedRelationships) {
469
- const key = `${rel.type}:${fromId}->${toId}`;
470
- if (!seen.has(key)) {
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
- console.warn("\nTip: Ensure target entities exist before creating relationships.");
477
- }
478
- if (kbModified) {
479
- prolog.invalidateCache();
480
- }
481
- // Save KB and detach
482
- await prolog.query("kb_save");
483
- await prolog.query("kb_detach");
484
- await prolog.terminate();
485
- const evictedHashes = {};
486
- const evictedSeenAt = {};
487
- for (const [key, hash] of Object.entries(nextHashes)) {
488
- if (failedCacheKeys.has(key)) {
489
- continue;
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
- evictedHashes[key] = hash;
492
- evictedSeenAt[key] = nextSeenAt[key] ?? nowIso;
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
- if (error instanceof SyncError) {
504
- console.error(`Error: ${error.message}`);
505
- }
506
- else if (error instanceof Error) {
507
- console.error(`Error: ${error.message}`);
508
- }
509
- else {
510
- console.error(`Error: ${String(error)}`);
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
- process.exit(1);
657
+ throw error;
513
658
  }
514
659
  }
515
660
  async function refreshManifestCoordinates(manifestPath, workspaceRoot) {