kibi-cli 0.18.1 → 0.19.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-json-command.d.ts +1 -2
- package/dist/cli-json-command.d.ts.map +1 -1
- package/dist/cli-json-command.js +2 -34
- package/dist/cli-operation-loader.d.ts +3 -0
- package/dist/cli-operation-loader.d.ts.map +1 -0
- package/dist/cli-operation-loader.js +58 -0
- package/dist/cli-operation-metadata.d.ts +82 -0
- package/dist/cli-operation-metadata.d.ts.map +1 -0
- package/dist/cli-operation-metadata.js +106 -0
- package/dist/cli-protocol.d.ts +1 -1
- package/dist/cli-protocol.d.ts.map +1 -1
- package/dist/cli-protocol.js +4 -3
- package/dist/cli-register-foundation.d.ts.map +1 -1
- package/dist/cli-register-foundation.js +14 -17
- package/dist/cli-register-json.d.ts +3 -0
- package/dist/cli-register-json.d.ts.map +1 -0
- package/dist/cli-register-json.js +35 -0
- package/dist/cli-register-maintenance.d.ts.map +1 -1
- package/dist/cli-register-maintenance.js +35 -10
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +13 -14
- package/dist/cli-register-skills.d.ts.map +1 -1
- package/dist/cli-register-skills.js +8 -6
- package/dist/cli.js +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +30 -12
- package/dist/commands/coverage.js +1 -1
- package/dist/commands/discovery-shared.d.ts +2 -1
- package/dist/commands/discovery-shared.d.ts.map +1 -1
- package/dist/commands/discovery-shared.js +35 -13
- package/dist/commands/engine.d.ts +8 -0
- package/dist/commands/engine.d.ts.map +1 -0
- package/dist/commands/engine.js +100 -0
- package/dist/commands/gaps.js +1 -1
- package/dist/commands/graph.js +1 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +3 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/sync/cache.d.ts +10 -0
- package/dist/commands/sync/cache.d.ts.map +1 -1
- package/dist/commands/sync/cache.js +30 -1
- package/dist/commands/sync/persistence.d.ts +10 -1
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +220 -44
- package/dist/commands/sync/staging.d.ts +10 -1
- package/dist/commands/sync/staging.d.ts.map +1 -1
- package/dist/commands/sync/staging.js +84 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +450 -13
- package/dist/engine-daemon.d.ts +3 -0
- package/dist/engine-daemon.d.ts.map +1 -0
- package/dist/engine-daemon.js +34 -0
- package/dist/engine.d.ts +63 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1256 -0
- package/dist/extractors/relationships.d.ts +2 -0
- package/dist/extractors/relationships.d.ts.map +1 -1
- package/dist/extractors/relationships.js +15 -17
- package/dist/prolog.d.ts +8 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +39 -8
- package/dist/public/operations/discovery-entities.d.ts.map +1 -1
- package/dist/public/operations/discovery-entities.js +3 -2
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +40 -3
- package/dist/public/operations/prolog-json.d.ts.map +1 -1
- package/dist/public/operations/prolog-json.js +14 -4
- package/dist/public/operations/runtime-types.d.ts +31 -0
- package/dist/public/operations/runtime-types.d.ts.map +1 -1
- package/dist/public/skills/init-kibi/SKILL.md +2 -2
- package/dist/public/skills/kibi-freshness/SKILL.md +2 -2
- package/dist/public/skills/kibi-traceability/SKILL.md +1 -1
- package/dist/public/skills/kibi-usage/SKILL.md +2 -2
- package/dist/public/skills/kibi-usage/resources/operation-access.md +1 -1
- package/dist/runtime/cli-runtime.d.ts.map +1 -1
- package/dist/runtime/cli-runtime.js +33 -37
- package/package.json +8 -4
- package/src/public/operations/discovery-entities.ts +3 -2
- package/src/public/operations/discovery-executors.ts +44 -3
- package/src/public/operations/prolog-json.ts +15 -3
- package/src/public/operations/runtime-types.ts +39 -0
- package/src/public/skills/init-kibi/SKILL.md +2 -2
- package/src/public/skills/kibi-freshness/SKILL.md +2 -2
- package/src/public/skills/kibi-traceability/SKILL.md +1 -1
- package/src/public/skills/kibi-usage/SKILL.md +2 -2
- package/src/public/skills/kibi-usage/resources/operation-access.md +1 -1
|
@@ -114,6 +114,117 @@ function serializeTypedFactFields(entity) {
|
|
|
114
114
|
}
|
|
115
115
|
return fields;
|
|
116
116
|
}
|
|
117
|
+
function parsePrologList(value) {
|
|
118
|
+
if (typeof value !== "string")
|
|
119
|
+
return [];
|
|
120
|
+
const text = value.trim();
|
|
121
|
+
if (text.length < 2 || !text.startsWith("[") || !text.endsWith("]")) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const body = text.slice(1, -1).trim();
|
|
125
|
+
if (body.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
const values = [];
|
|
128
|
+
// IDs are atoms in the Kibi schema. Keep the parser deliberately narrow so
|
|
129
|
+
// a malformed response cannot turn into a destructive path or goal.
|
|
130
|
+
for (const part of body.split(",")) {
|
|
131
|
+
const valuePart = part.trim();
|
|
132
|
+
if (!valuePart)
|
|
133
|
+
continue;
|
|
134
|
+
if (valuePart.startsWith("'") && valuePart.endsWith("'")) {
|
|
135
|
+
values.push(valuePart.slice(1, -1).replaceAll("''", "'"));
|
|
136
|
+
}
|
|
137
|
+
else if (/^[A-Za-z0-9_.:/@+-]+$/.test(valuePart)) {
|
|
138
|
+
values.push(valuePart);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
143
|
+
async function entityIdsForSource(prolog, sourceFile) {
|
|
144
|
+
const candidates = new Set([
|
|
145
|
+
sourceFile,
|
|
146
|
+
path.relative(process.cwd(), sourceFile),
|
|
147
|
+
path.basename(sourceFile),
|
|
148
|
+
]);
|
|
149
|
+
const ids = new Set();
|
|
150
|
+
for (const candidate of candidates) {
|
|
151
|
+
const result = await prolog.query(`kb_entities_by_source(${toPrologString(candidate)}, Ids)`);
|
|
152
|
+
if (result.success) {
|
|
153
|
+
for (const id of parsePrologList(result.bindings?.Ids))
|
|
154
|
+
ids.add(id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...ids];
|
|
158
|
+
}
|
|
159
|
+
/** Remove entities owned by changed/deleted source files before re-upserting. */
|
|
160
|
+
export async function retractEntitiesForSources(prolog, sourceFiles) {
|
|
161
|
+
const ids = new Set();
|
|
162
|
+
for (const sourceFile of sourceFiles) {
|
|
163
|
+
for (const id of await entityIdsForSource(prolog, sourceFile))
|
|
164
|
+
ids.add(id);
|
|
165
|
+
}
|
|
166
|
+
if (ids.size === 0)
|
|
167
|
+
return 0;
|
|
168
|
+
const goals = [...ids].map((id) => `kb_retract_entity(${toPrologAtom(id)})`);
|
|
169
|
+
const batchRunner = prolog.queryBatch?.bind(prolog);
|
|
170
|
+
if (batchRunner) {
|
|
171
|
+
const result = await batchRunner(goals);
|
|
172
|
+
if (!result.success) {
|
|
173
|
+
throw new Error(result.error ?? "Failed to retract changed source entities");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
for (const goal of goals) {
|
|
178
|
+
const result = await prolog.query(goal);
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
throw new Error(result.error ?? "Failed to retract changed source entity");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return ids.size;
|
|
185
|
+
}
|
|
186
|
+
/** Remove an exact entity delta without scanning every indexed source. */
|
|
187
|
+
export async function retractEntitiesById(prolog, entityIds) {
|
|
188
|
+
const ids = [...new Set(entityIds)];
|
|
189
|
+
if (ids.length === 0)
|
|
190
|
+
return 0;
|
|
191
|
+
const goals = ids.map((id) => `kb_retract_entity(${toPrologAtom(id)})`);
|
|
192
|
+
const batchRunner = prolog.queryBatch?.bind(prolog);
|
|
193
|
+
const result = batchRunner
|
|
194
|
+
? await batchRunner(goals)
|
|
195
|
+
: await prolog.query(`rdf_transaction((${goals.join(", ")}))`);
|
|
196
|
+
if (!result.success) {
|
|
197
|
+
throw new Error(result.error ?? "Failed to retract entity delta");
|
|
198
|
+
}
|
|
199
|
+
return ids.length;
|
|
200
|
+
}
|
|
201
|
+
/** Clear outgoing inline relationships for entities whose payload changed. */
|
|
202
|
+
export async function retractEntityRelationshipsById(prolog, entityIds) {
|
|
203
|
+
const ids = [...new Set(entityIds)];
|
|
204
|
+
if (ids.length === 0)
|
|
205
|
+
return;
|
|
206
|
+
const goals = ids.map((id) => `kb_retract_entity_relationships(${toPrologAtom(id)})`);
|
|
207
|
+
const batchRunner = prolog.queryBatch?.bind(prolog);
|
|
208
|
+
const result = batchRunner
|
|
209
|
+
? await batchRunner(goals)
|
|
210
|
+
: await prolog.query(`rdf_transaction((${goals.join(", ")}))`);
|
|
211
|
+
if (!result.success) {
|
|
212
|
+
throw new Error(result.error ?? "Failed to retract relationship delta");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function retractRelationships(prolog, relationships) {
|
|
216
|
+
if (relationships.length === 0)
|
|
217
|
+
return 0;
|
|
218
|
+
const goals = relationships.map((relationship) => `kb_retract_relationship(${toPrologAtom(relationship.type)}, ${toPrologAtom(relationship.from)}, ${toPrologAtom(relationship.to)})`);
|
|
219
|
+
const batchRunner = prolog.queryBatch?.bind(prolog);
|
|
220
|
+
const result = batchRunner
|
|
221
|
+
? await batchRunner(goals)
|
|
222
|
+
: await prolog.query(`rdf_transaction((${goals.join(", ")}))`);
|
|
223
|
+
if (!result.success) {
|
|
224
|
+
throw new Error(result.error ?? "Failed to retract relationship shard delta");
|
|
225
|
+
}
|
|
226
|
+
return relationships.length;
|
|
227
|
+
}
|
|
117
228
|
function isQueryFailedError(error) {
|
|
118
229
|
const lowered = error.toLowerCase();
|
|
119
230
|
// Only match session-corruption errors that might benefit from a Prolog process restart.
|
|
@@ -140,21 +251,26 @@ async function tryResetPrologProcess(prolog) {
|
|
|
140
251
|
}
|
|
141
252
|
export async function persistEntities(
|
|
142
253
|
// implements REQ-009
|
|
143
|
-
prolog, results, entityIds) {
|
|
254
|
+
prolog, results, entityIds, options = {}) {
|
|
144
255
|
let entityCount = 0;
|
|
145
256
|
let kbModified = false;
|
|
146
257
|
const existingEntityIds = new Set();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
258
|
+
const pendingEntityGoals = [];
|
|
259
|
+
if (options.loadExistingEntityIds !== false) {
|
|
260
|
+
// Generation rebuilds need the complete endpoint inventory for local
|
|
261
|
+
// relationship validation. Journal deltas rely on Prolog's authoritative
|
|
262
|
+
// endpoint checks and must not materialize 10,000 IDs for one upsert.
|
|
263
|
+
const existingIdsResult = await prolog.query("findall(Id, kb_entity(Id, _, _), ExistingIds)");
|
|
264
|
+
if (existingIdsResult.success && existingIdsResult.bindings?.ExistingIds) {
|
|
265
|
+
const raw = existingIdsResult.bindings.ExistingIds;
|
|
266
|
+
const cleaned = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
267
|
+
if (cleaned) {
|
|
268
|
+
for (const atom of cleaned.split(",")) {
|
|
269
|
+
const id = atom.trim().replace(/^'|'$/g, "");
|
|
270
|
+
if (id) {
|
|
271
|
+
entityIds.add(id);
|
|
272
|
+
existingEntityIds.add(id);
|
|
273
|
+
}
|
|
158
274
|
}
|
|
159
275
|
}
|
|
160
276
|
}
|
|
@@ -259,18 +375,56 @@ prolog, results, entityIds) {
|
|
|
259
375
|
}
|
|
260
376
|
const propsList = `[${props.join(", ")}]`;
|
|
261
377
|
const goal = `kb_assert_entity(${entity.type}, ${propsList})`;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
kbModified = true;
|
|
378
|
+
pendingEntityGoals.push({
|
|
379
|
+
entity,
|
|
380
|
+
sourceFile: sourceFile ?? entity.source,
|
|
381
|
+
goal,
|
|
382
|
+
});
|
|
268
383
|
}
|
|
269
384
|
catch (error) {
|
|
270
385
|
const message = error instanceof Error ? error.message : String(error);
|
|
271
386
|
throw new Error(`Failed to upsert entity ${entity.id}: ${message}${semanticEntityContext(entity, sourceFile ?? entity.source)}`);
|
|
272
387
|
}
|
|
273
388
|
}
|
|
389
|
+
// Persist a changed source as a bounded set of RDF edits. This retains the
|
|
390
|
+
// per-entity fallback diagnostics while avoiding one Prolog round-trip and
|
|
391
|
+
// journal flush for every symbol/document in the common case.
|
|
392
|
+
const batchRunner = prolog.queryBatch?.bind(prolog);
|
|
393
|
+
const batchSize = 250;
|
|
394
|
+
for (let offset = 0; offset < pendingEntityGoals.length; offset += batchSize) {
|
|
395
|
+
const chunk = pendingEntityGoals.slice(offset, offset + batchSize);
|
|
396
|
+
const result = batchRunner
|
|
397
|
+
? await batchRunner(chunk.map(({ goal }) => goal))
|
|
398
|
+
: { success: false, error: "batch unsupported" };
|
|
399
|
+
if (result.success) {
|
|
400
|
+
entityCount += chunk.length;
|
|
401
|
+
kbModified = true;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// A malformed entity should retain the precise existing error. The batch
|
|
405
|
+
// transaction has rolled back, so retrying sequentially is safe.
|
|
406
|
+
for (const item of chunk) {
|
|
407
|
+
try {
|
|
408
|
+
const single = await prolog.query(item.goal);
|
|
409
|
+
if (!single.success) {
|
|
410
|
+
const detail = single.error ||
|
|
411
|
+
(batchRunner ? result.error : undefined) ||
|
|
412
|
+
`kb_assert_entity failed for ${item.entity.id}`;
|
|
413
|
+
throw new Error(`Failed to upsert entity ${item.entity.id}: ${detail}${semanticEntityContext(item.entity, item.sourceFile)}`);
|
|
414
|
+
}
|
|
415
|
+
entityCount++;
|
|
416
|
+
kbModified = true;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (error instanceof Error &&
|
|
420
|
+
error.message.startsWith("Failed to upsert entity ")) {
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
424
|
+
throw new Error(`Failed to upsert entity ${item.entity.id}: ${message}${semanticEntityContext(item.entity, item.sourceFile)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
274
428
|
return { entityCount, kbModified };
|
|
275
429
|
}
|
|
276
430
|
function semanticEntityContext(entity, sourceFile) {
|
|
@@ -306,25 +460,14 @@ export async function persistRelationships(prolog, results, shardRelationships)
|
|
|
306
460
|
}
|
|
307
461
|
// Collect all relationships from results and shards
|
|
308
462
|
const failedRelationships = [];
|
|
463
|
+
const pendingRelationships = [];
|
|
309
464
|
for (const { relationships } of results) {
|
|
310
465
|
for (const rel of relationships) {
|
|
311
466
|
try {
|
|
312
467
|
const fromId = idLookup.get(rel.from) || rel.from;
|
|
313
468
|
const toId = idLookup.get(rel.to) || rel.to;
|
|
314
469
|
const goal = `kb_assert_relationship(${toPrologAtom(rel.type)}, ${toPrologAtom(fromId)}, ${toPrologAtom(toId)}, [])`;
|
|
315
|
-
|
|
316
|
-
if (result.success) {
|
|
317
|
-
relCount++;
|
|
318
|
-
kbModified = true;
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
failedRelationships.push({
|
|
322
|
-
rel,
|
|
323
|
-
fromId,
|
|
324
|
-
toId,
|
|
325
|
-
error: result.error || "Unknown error",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
470
|
+
pendingRelationships.push({ rel, fromId, toId, goal });
|
|
328
471
|
}
|
|
329
472
|
catch (error) {
|
|
330
473
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -338,19 +481,12 @@ export async function persistRelationships(prolog, results, shardRelationships)
|
|
|
338
481
|
for (const rel of shardRelationships) {
|
|
339
482
|
try {
|
|
340
483
|
const goal = `kb_assert_relationship(${toPrologAtom(rel.type)}, ${toPrologAtom(rel.from)}, ${toPrologAtom(rel.to)}, [])`;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
failedRelationships.push({
|
|
348
|
-
rel,
|
|
349
|
-
fromId: rel.from,
|
|
350
|
-
toId: rel.to,
|
|
351
|
-
error: result.error || "Unknown error",
|
|
352
|
-
});
|
|
353
|
-
}
|
|
484
|
+
pendingRelationships.push({
|
|
485
|
+
rel,
|
|
486
|
+
fromId: rel.from,
|
|
487
|
+
toId: rel.to,
|
|
488
|
+
goal,
|
|
489
|
+
});
|
|
354
490
|
}
|
|
355
491
|
catch (error) {
|
|
356
492
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -362,6 +498,46 @@ export async function persistRelationships(prolog, results, shardRelationships)
|
|
|
362
498
|
});
|
|
363
499
|
}
|
|
364
500
|
}
|
|
501
|
+
const relationshipBatchRunner = prolog.queryBatch?.bind(prolog);
|
|
502
|
+
const relationshipBatchSize = 250;
|
|
503
|
+
for (let offset = 0; offset < pendingRelationships.length; offset += relationshipBatchSize) {
|
|
504
|
+
const chunk = pendingRelationships.slice(offset, offset + relationshipBatchSize);
|
|
505
|
+
const batch = relationshipBatchRunner
|
|
506
|
+
? await relationshipBatchRunner(chunk.map(({ goal }) => goal))
|
|
507
|
+
: { success: false, error: "batch unsupported" };
|
|
508
|
+
if (batch.success) {
|
|
509
|
+
relCount += chunk.length;
|
|
510
|
+
kbModified = true;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
for (const item of chunk) {
|
|
514
|
+
try {
|
|
515
|
+
const single = await prolog.query(item.goal);
|
|
516
|
+
if (single.success) {
|
|
517
|
+
relCount++;
|
|
518
|
+
kbModified = true;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
failedRelationships.push({
|
|
522
|
+
rel: item.rel,
|
|
523
|
+
fromId: item.fromId,
|
|
524
|
+
toId: item.toId,
|
|
525
|
+
error: single.error ||
|
|
526
|
+
(relationshipBatchRunner ? batch.error : undefined) ||
|
|
527
|
+
"Unknown error",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
failedRelationships.push({
|
|
533
|
+
rel: item.rel,
|
|
534
|
+
fromId: item.fromId,
|
|
535
|
+
toId: item.toId,
|
|
536
|
+
error: error instanceof Error ? error.message : String(error),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
365
541
|
// Retry failed relationships
|
|
366
542
|
const retryCount = 3;
|
|
367
543
|
let resetAttempted = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { copyFileSync } from "node:fs";
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import { copyCleanSnapshot } from "../../utils/branch-resolver.js";
|
|
@@ -13,11 +13,20 @@ interface StagingDeps {
|
|
|
13
13
|
moduleDir: string;
|
|
14
14
|
renameSync: typeof renameSync;
|
|
15
15
|
rmSync: typeof rmSync;
|
|
16
|
+
writeFileSync: typeof writeFileSync;
|
|
16
17
|
}
|
|
17
18
|
export declare function createUniqueStagingPath(currentBranch: string, rootDir: string, pid?: number, now?: number): string;
|
|
18
19
|
export declare function cleanupAbandonedStagingDirectories(stagingPath: string, deps?: Partial<StagingDeps>): Promise<void>;
|
|
19
20
|
export declare function prepareStagingEnvironment(stagingPath: string, livePath: string, rebuild: boolean, deps?: Partial<StagingDeps>): Promise<void>;
|
|
20
21
|
export declare function atomicPublish(stagingPath: string, livePath: string, deps?: Partial<StagingDeps>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Publish a rebuilt journal generation without replacing the branch directory.
|
|
24
|
+
* The legacy backup, sync metadata, and sentinel stay in place; only the
|
|
25
|
+
* journal database and CURRENT pointer move. A short-lived rollback pair is
|
|
26
|
+
* retained until both moves succeed so an interrupted publication can be
|
|
27
|
+
* repaired on the next engine start.
|
|
28
|
+
*/
|
|
29
|
+
export declare function atomicPublishGeneration(stagingPath: string, livePath: string, deps?: Partial<StagingDeps>): void;
|
|
21
30
|
export declare function cleanupStaging(stagingPath: string, deps?: Partial<StagingDeps>): void;
|
|
22
31
|
export {};
|
|
23
32
|
//# sourceMappingURL=staging.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/staging.ts"],"names":[],"mappings":"AAkBA,OAAO,
|
|
1
|
+
{"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/staging.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,UAAU,EACV,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,MAAM,WAAW,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,UAAU,WAAW;IACnB,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,EAAE,EAAE,OAAO,EAAE,CAAC;IACd,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,MAAM,EAAE,OAAO,MAAM,CAAC;IACtB,aAAa,EAAE,OAAO,aAAa,CAAC;CACrC;AA+BD,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,GAAG,SAAc,EACjB,GAAG,SAAa,GACf,MAAM,CAOR;AAGD,wBAAsB,kCAAkC,CACtD,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqDf;AAED,wBAAsB,yBAAyB,CAE7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CA6Cf;AA0CD,wBAAgB,aAAa,CAE3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAeN;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CA6DN;AAED,wBAAgB,cAAc,CAE5B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAKN"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
19
19
|
import { copyFileSync } from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import fg from "fast-glob";
|
|
@@ -42,6 +42,7 @@ function resolveDeps(overrides) {
|
|
|
42
42
|
moduleDir: import.meta.dirname,
|
|
43
43
|
renameSync,
|
|
44
44
|
rmSync,
|
|
45
|
+
writeFileSync,
|
|
45
46
|
...overrides,
|
|
46
47
|
};
|
|
47
48
|
}
|
|
@@ -104,6 +105,22 @@ stagingPath, livePath, rebuild, deps) {
|
|
|
104
105
|
// Start fresh with schema only
|
|
105
106
|
await copySchemaToStaging(stagingPath, resolved);
|
|
106
107
|
}
|
|
108
|
+
// Every new staging generation uses the journaled store format. This also
|
|
109
|
+
// makes `--rebuild` publish a native RDF database instead of recreating the
|
|
110
|
+
// legacy full-snapshot path.
|
|
111
|
+
const markerPath = path.join(stagingPath, "storage.json");
|
|
112
|
+
const canWriteMetadata = deps === undefined || deps.writeFileSync !== undefined;
|
|
113
|
+
if (canWriteMetadata &&
|
|
114
|
+
resolved.existsSync(stagingPath) &&
|
|
115
|
+
!resolved.existsSync(markerPath)) {
|
|
116
|
+
resolved.mkdirSync(path.join(stagingPath, "rdf"), { recursive: true });
|
|
117
|
+
resolved.writeFileSync(markerPath, '{"format":"kibi.rdf-journal.v1","schemaVersion":1}\n', { mode: 0o600 });
|
|
118
|
+
const generation = rebuild
|
|
119
|
+
? `generation-${process.pid}-${Date.now()}`
|
|
120
|
+
: "generation-1";
|
|
121
|
+
resolved.writeFileSync(path.join(stagingPath, "CURRENT"), `${generation}:0\n`, { mode: 0o600 });
|
|
122
|
+
resolved.writeFileSync(path.join(stagingPath, "kb.rdf"), "KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1\n", { mode: 0o600 });
|
|
123
|
+
}
|
|
107
124
|
}
|
|
108
125
|
async function copySchemaToStaging(stagingPath, deps) {
|
|
109
126
|
const possibleSchemaPaths = [
|
|
@@ -154,6 +171,72 @@ stagingPath, livePath, deps) {
|
|
|
154
171
|
resolved.renameSync(stagingPath, livePath);
|
|
155
172
|
}
|
|
156
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Publish a rebuilt journal generation without replacing the branch directory.
|
|
176
|
+
* The legacy backup, sync metadata, and sentinel stay in place; only the
|
|
177
|
+
* journal database and CURRENT pointer move. A short-lived rollback pair is
|
|
178
|
+
* retained until both moves succeed so an interrupted publication can be
|
|
179
|
+
* repaired on the next engine start.
|
|
180
|
+
*/
|
|
181
|
+
export function atomicPublishGeneration(stagingPath, livePath, deps) {
|
|
182
|
+
const resolved = resolveDeps(deps);
|
|
183
|
+
const liveParent = path.dirname(livePath);
|
|
184
|
+
if (!resolved.existsSync(liveParent)) {
|
|
185
|
+
resolved.mkdirSync(liveParent, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
if (!resolved.existsSync(livePath)) {
|
|
188
|
+
resolved.renameSync(stagingPath, livePath);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const generationStamp = `${Date.now()}.${process.pid}`;
|
|
192
|
+
const liveRdf = path.join(livePath, "rdf");
|
|
193
|
+
const stagedRdf = path.join(stagingPath, "rdf");
|
|
194
|
+
const oldRdf = `${liveRdf}.old.${generationStamp}`;
|
|
195
|
+
const liveCurrent = path.join(livePath, "CURRENT");
|
|
196
|
+
const stagedCurrent = path.join(stagingPath, "CURRENT");
|
|
197
|
+
const oldCurrent = `${liveCurrent}.old.${generationStamp}`;
|
|
198
|
+
if (!resolved.existsSync(stagedRdf) || !resolved.existsSync(stagedCurrent)) {
|
|
199
|
+
throw new Error("Rebuilt journal generation is missing rdf or CURRENT");
|
|
200
|
+
}
|
|
201
|
+
let oldRdfMoved = false;
|
|
202
|
+
let oldCurrentMoved = false;
|
|
203
|
+
try {
|
|
204
|
+
if (resolved.existsSync(liveRdf)) {
|
|
205
|
+
resolved.renameSync(liveRdf, oldRdf);
|
|
206
|
+
oldRdfMoved = true;
|
|
207
|
+
}
|
|
208
|
+
resolved.renameSync(stagedRdf, liveRdf);
|
|
209
|
+
if (resolved.existsSync(liveCurrent)) {
|
|
210
|
+
resolved.renameSync(liveCurrent, oldCurrent);
|
|
211
|
+
oldCurrentMoved = true;
|
|
212
|
+
}
|
|
213
|
+
resolved.renameSync(stagedCurrent, liveCurrent);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// Restore the old pointer/database whenever publication fails. If the
|
|
217
|
+
// process itself crashes, ensureJournaledBranchStoreAsync performs the
|
|
218
|
+
// same recovery from the `.old.<stamp>` siblings.
|
|
219
|
+
try {
|
|
220
|
+
if (resolved.existsSync(liveCurrent))
|
|
221
|
+
resolved.rmSync(liveCurrent, { force: true });
|
|
222
|
+
if (oldCurrentMoved && resolved.existsSync(oldCurrent))
|
|
223
|
+
resolved.renameSync(oldCurrent, liveCurrent);
|
|
224
|
+
if (resolved.existsSync(liveRdf))
|
|
225
|
+
resolved.rmSync(liveRdf, { recursive: true, force: true });
|
|
226
|
+
if (oldRdfMoved && resolved.existsSync(oldRdf))
|
|
227
|
+
resolved.renameSync(oldRdf, liveRdf);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Preserve the original publication error; startup recovery will report
|
|
231
|
+
// the remaining sibling paths if the rollback itself was interrupted.
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
if (resolved.existsSync(oldRdf))
|
|
236
|
+
resolved.rmSync(oldRdf, { recursive: true, force: true });
|
|
237
|
+
if (resolved.existsSync(oldCurrent))
|
|
238
|
+
resolved.rmSync(oldCurrent, { force: true });
|
|
239
|
+
}
|
|
157
240
|
export function cleanupStaging(
|
|
158
241
|
// implements REQ-003
|
|
159
242
|
stagingPath, deps) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyBjE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAsC7C,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAoDD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,yBAAyB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,yBAAyB,GAAG;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE,KACzD,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,aAAa,CAAC;CAChE;AAsDD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB,CAAC,EAAE,OAAO,CAAC;CAC/B,EACN,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAy6BrB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
|