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
package/dist/commands/sync.js
CHANGED
|
@@ -16,28 +16,103 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import { execSync } from "node:child_process";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
19
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { branchErrorToDiagnostic, createDocsNotIndexedDiagnostic, createInvalidAuthoringDiagnostic, createKbMissingDiagnostic, formatSyncSummary, } from "../diagnostics.js";
|
|
22
|
+
import { EngineClient, engineSocketPath, ensureJournaledBranchStoreAsync, fsyncJournaledBranchStore, } from "../engine.js";
|
|
22
23
|
import { isCliDebugEnabled } from "../env.js";
|
|
23
|
-
import {
|
|
24
|
+
import { extractRelationshipShard, validateRelationships, } from "../extractors/relationships.js";
|
|
24
25
|
import { analyzeSemanticAdvisorInput } from "../operations/semantic-advisor/analyze-prose.js";
|
|
25
26
|
import { validateSemanticInventoryBoundary } from "../operations/semantic-advisor/ingestion-boundary.js";
|
|
26
27
|
import { PrologProcess } from "../prolog.js";
|
|
27
28
|
import { resolveActiveBranch, } from "../utils/branch-resolver.js";
|
|
28
29
|
import { loadSyncConfig } from "../utils/config.js";
|
|
29
|
-
import { SYNC_CACHE_TTL_MS, SYNC_CACHE_VERSION, hashFile, readSyncCache, toCacheKey, writeSyncCache, } from "./sync/cache.js";
|
|
30
|
+
import { SYNC_CACHE_TTL_MS, SYNC_CACHE_VERSION, hashFile, hashNormalized, readSyncCache, toCacheKey, writeSyncCache, } from "./sync/cache.js";
|
|
30
31
|
import { discoverSourceFiles, } from "./sync/discovery.js";
|
|
31
32
|
import { processExtractions } from "./sync/extraction.js";
|
|
32
33
|
import { refreshManifestCoordinates } from "./sync/manifest.js";
|
|
33
|
-
import { persistEntities, persistRelationships } from "./sync/persistence.js";
|
|
34
|
-
import { atomicPublish, cleanupStaging, createUniqueStagingPath, prepareStagingEnvironment, } from "./sync/staging.js";
|
|
34
|
+
import { persistEntities, persistRelationships, retractEntitiesById, retractEntitiesForSources, retractEntityRelationshipsById, retractRelationships, } from "./sync/persistence.js";
|
|
35
|
+
import { atomicPublish, atomicPublishGeneration, cleanupStaging, createUniqueStagingPath, prepareStagingEnvironment, } from "./sync/staging.js";
|
|
35
36
|
export class SyncError extends Error {
|
|
36
37
|
constructor(message) {
|
|
37
38
|
super(message);
|
|
38
39
|
this.name = "SyncError";
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
function relationshipKey(relationship) {
|
|
43
|
+
return `${relationship.type}\0${relationship.from}\0${relationship.to}`;
|
|
44
|
+
}
|
|
45
|
+
function relationshipFromKey(key) {
|
|
46
|
+
const [type, from, to] = key.split("\0");
|
|
47
|
+
if (type === undefined || from === undefined || to === undefined) {
|
|
48
|
+
throw new SyncError(`Invalid cached relationship key: ${key}`);
|
|
49
|
+
}
|
|
50
|
+
return { type, from, to };
|
|
51
|
+
}
|
|
52
|
+
function normalizedEntityHash(result) {
|
|
53
|
+
const entity = { ...result.entity };
|
|
54
|
+
// Manifest defaults are generated at extraction time. They must not turn a
|
|
55
|
+
// one-record edit into 10,000 false-positive symbol updates.
|
|
56
|
+
entity.created_at = undefined;
|
|
57
|
+
entity.updated_at = undefined;
|
|
58
|
+
return hashNormalized({
|
|
59
|
+
entity,
|
|
60
|
+
sourceFile: result.sourceFile,
|
|
61
|
+
relationships: result.relationships
|
|
62
|
+
.map(relationshipKey)
|
|
63
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function checkpointNoopSync(workspaceRoot, branch) {
|
|
67
|
+
const engine = new EngineClient({
|
|
68
|
+
workspaceRoot,
|
|
69
|
+
branch,
|
|
70
|
+
timeout: 120_000,
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await engine.start();
|
|
74
|
+
const checkpoint = await engine.checkpoint();
|
|
75
|
+
if (!checkpoint.success) {
|
|
76
|
+
throw new SyncError(`Failed to publish the no-op sync checkpoint: ${checkpoint.error ?? "Unknown error"}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await engine.terminate();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function compilerCacheHasEntityDelta(cache) {
|
|
84
|
+
return (cache.entityHashes !== undefined && cache.sourceEntityIds !== undefined);
|
|
85
|
+
}
|
|
86
|
+
function compilerCacheIsFresh(cache, sourceFiles, nowMs) {
|
|
87
|
+
const sourceKeys = sourceFiles.map(toCacheKey);
|
|
88
|
+
if (Object.keys(cache.hashes).some((key) => !sourceKeys.includes(key))) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return sourceKeys.every((key) => {
|
|
92
|
+
const lastSeen = cache.seenAt[key];
|
|
93
|
+
const lastSeenMs = lastSeen ? Date.parse(lastSeen) : Number.NaN;
|
|
94
|
+
return (typeof cache.hashes[key] === "string" &&
|
|
95
|
+
lastSeen !== undefined &&
|
|
96
|
+
!Number.isNaN(lastSeenMs) &&
|
|
97
|
+
nowMs - lastSeenMs <= SYNC_CACHE_TTL_MS);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function compilerCacheFilesMatch(cache, sourceFiles, relationshipFiles) {
|
|
101
|
+
for (const file of sourceFiles) {
|
|
102
|
+
if (cache.hashes[toCacheKey(file)] !== hashFile(file))
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const relationshipKeys = new Set(relationshipFiles.map(toCacheKey));
|
|
106
|
+
if (Object.keys(cache.relationshipHashes ?? {}).some((key) => !relationshipKeys.has(key))) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
for (const file of relationshipFiles) {
|
|
110
|
+
if (cache.relationshipHashes?.[toCacheKey(file)] !== hashFile(file)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
41
116
|
// implements REQ-003, REQ-007
|
|
42
117
|
export async function syncCommand(options = {}, runtime = {}) {
|
|
43
118
|
const validateOnly = options.validateOnly ?? false;
|
|
@@ -72,6 +147,24 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
72
147
|
throw new SyncError(`Failed to resolve active branch: ${branchResult.error}`);
|
|
73
148
|
}
|
|
74
149
|
currentBranch = branchResult.branch;
|
|
150
|
+
// Cut over legacy branches before creating a staging copy. If another
|
|
151
|
+
// CLI/MCP operation has an engine attached, stop that single writer so the
|
|
152
|
+
// atomic publish below cannot race its RDF lock.
|
|
153
|
+
const livePathForEngine = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
|
|
154
|
+
if (!validateOnly) {
|
|
155
|
+
await ensureJournaledBranchStoreAsync(livePathForEngine);
|
|
156
|
+
const existingEngine = new EngineClient({
|
|
157
|
+
workspaceRoot: process.cwd(),
|
|
158
|
+
branch: currentBranch,
|
|
159
|
+
timeout: 2_000,
|
|
160
|
+
});
|
|
161
|
+
const needsExclusiveGenerationPublish = rebuild || runtime.createProlog !== undefined;
|
|
162
|
+
if (needsExclusiveGenerationPublish &&
|
|
163
|
+
existsSync(engineSocketPath(process.cwd(), currentBranch))) {
|
|
164
|
+
await existingEngine.stop(false).catch(() => undefined);
|
|
165
|
+
await existingEngine.terminate();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
75
168
|
if (isCliDebugEnabled()) {
|
|
76
169
|
// eslint-disable-next-line no-console
|
|
77
170
|
console.log("[kibi-debug] currentBranch:", currentBranch);
|
|
@@ -90,6 +183,44 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
90
183
|
const syncCache = readSyncCache(cachePath);
|
|
91
184
|
const nowIso = new Date().toISOString();
|
|
92
185
|
const nowMs = Date.now();
|
|
186
|
+
const currentSourceKeys = new Set(sourceFiles.map(toCacheKey));
|
|
187
|
+
const relationshipShardFiles = existsSync(relationshipsDir)
|
|
188
|
+
? readdirSync(relationshipsDir)
|
|
189
|
+
.filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
|
|
190
|
+
.map((file) => path.join(relationshipsDir, file))
|
|
191
|
+
.sort()
|
|
192
|
+
: [];
|
|
193
|
+
// No-op is a first-class compiler result. Hashing the configured inputs is
|
|
194
|
+
// sufficient when normalized entity/shard inventories are already fresh;
|
|
195
|
+
// avoid rebuilding maps and rediscovering RDF state for the common path.
|
|
196
|
+
if (!validateOnly &&
|
|
197
|
+
!rebuild &&
|
|
198
|
+
!options.refreshSymbolCoordinates &&
|
|
199
|
+
runtime.createProlog === undefined &&
|
|
200
|
+
compilerCacheHasEntityDelta(syncCache) &&
|
|
201
|
+
compilerCacheIsFresh(syncCache, sourceFiles, nowMs) &&
|
|
202
|
+
compilerCacheFilesMatch(syncCache, sourceFiles, relationshipShardFiles)) {
|
|
203
|
+
await checkpointNoopSync(process.cwd(), currentBranch);
|
|
204
|
+
console.log("✓ Imported 0 entities, 0 relationships (no changes)");
|
|
205
|
+
return withOptionalCommit({
|
|
206
|
+
branch: currentBranch,
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
entityCounts,
|
|
209
|
+
relationshipCount: 0,
|
|
210
|
+
success: true,
|
|
211
|
+
published: false,
|
|
212
|
+
failures: diagnostics,
|
|
213
|
+
durationMs: Date.now() - startTime,
|
|
214
|
+
exitCode: 0,
|
|
215
|
+
}, getCurrentCommit());
|
|
216
|
+
}
|
|
217
|
+
const changedSourceHashes = new Map();
|
|
218
|
+
for (const file of sourceFiles) {
|
|
219
|
+
const key = toCacheKey(file);
|
|
220
|
+
const hash = hashFile(file);
|
|
221
|
+
if (syncCache.hashes[key] !== hash)
|
|
222
|
+
changedSourceHashes.set(key, hash);
|
|
223
|
+
}
|
|
93
224
|
const nextHashes = {};
|
|
94
225
|
const nextSeenAt = {};
|
|
95
226
|
const nextSemanticHashes = {
|
|
@@ -98,15 +229,74 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
98
229
|
const nextSemanticContracts = {
|
|
99
230
|
...syncCache.semanticContracts,
|
|
100
231
|
};
|
|
232
|
+
const nextRelationshipHashes = {};
|
|
233
|
+
const nextShardRelationships = {};
|
|
234
|
+
const addedShardRelationships = [];
|
|
235
|
+
const removedShardRelationships = [];
|
|
101
236
|
const initialSemanticBaseline = Object.keys(syncCache.hashes).length === 0;
|
|
102
|
-
const shardResults = extractFromRelationshipShards(relationshipsDir);
|
|
103
|
-
const allRelationships = flattenRelationships(shardResults);
|
|
104
237
|
const changedMarkdownFiles = [];
|
|
105
238
|
const changedManifestFiles = [];
|
|
239
|
+
const forceEntitySourceKeys = new Set();
|
|
240
|
+
// A removed source has no file left to extract, but its entities must be
|
|
241
|
+
// retracted from the branch. Keep this separate from failed extraction:
|
|
242
|
+
// malformed current files remain authoritative until a later successful
|
|
243
|
+
// sync rather than deleting their last known-good entities.
|
|
244
|
+
const deletedSourceFiles = Object.keys(syncCache.hashes)
|
|
245
|
+
.filter((key) => !currentSourceKeys.has(key))
|
|
246
|
+
.map((key) => path.resolve(process.cwd(), key));
|
|
247
|
+
const currentRelationshipKeys = new Set();
|
|
248
|
+
for (const shardPath of relationshipShardFiles) {
|
|
249
|
+
const key = toCacheKey(shardPath);
|
|
250
|
+
currentRelationshipKeys.add(key);
|
|
251
|
+
let hash;
|
|
252
|
+
try {
|
|
253
|
+
hash = hashFile(shardPath);
|
|
254
|
+
nextRelationshipHashes[key] = hash;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
console.warn(`Warning: Failed to hash relationship shard ${shardPath}: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
const previousKeys = syncCache.shardRelationships?.[key] ?? [];
|
|
261
|
+
const shardChanged = syncCache.shardRelationships === undefined ||
|
|
262
|
+
hash === undefined ||
|
|
263
|
+
syncCache.relationshipHashes?.[key] !== hash;
|
|
264
|
+
const currentKeys = shardChanged
|
|
265
|
+
? extractRelationshipShard(shardPath)
|
|
266
|
+
.relationships.map(relationshipKey)
|
|
267
|
+
.sort((left, right) => left.localeCompare(right))
|
|
268
|
+
: previousKeys;
|
|
269
|
+
nextShardRelationships[key] = currentKeys;
|
|
270
|
+
if (shardChanged) {
|
|
271
|
+
const previous = new Set(previousKeys);
|
|
272
|
+
const current = new Set(currentKeys);
|
|
273
|
+
for (const edge of previous) {
|
|
274
|
+
if (!current.has(edge)) {
|
|
275
|
+
removedShardRelationships.push(relationshipFromKey(edge));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const edge of current) {
|
|
279
|
+
if (!previous.has(edge)) {
|
|
280
|
+
addedShardRelationships.push(relationshipFromKey(edge));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [key, previousKeys] of Object.entries(syncCache.shardRelationships ?? {})) {
|
|
286
|
+
if (currentRelationshipKeys.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
for (const edge of previousKeys) {
|
|
289
|
+
removedShardRelationships.push(relationshipFromKey(edge));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const relationshipChanged = addedShardRelationships.length > 0 ||
|
|
293
|
+
removedShardRelationships.length > 0;
|
|
106
294
|
for (const file of sourceFiles) {
|
|
107
295
|
try {
|
|
108
296
|
const key = toCacheKey(file);
|
|
109
|
-
const hash =
|
|
297
|
+
const hash = changedSourceHashes.get(key) ?? syncCache.hashes[key];
|
|
298
|
+
if (hash === undefined)
|
|
299
|
+
continue;
|
|
110
300
|
const lastSeen = syncCache.seenAt[key];
|
|
111
301
|
const lastSeenMs = lastSeen ? Date.parse(lastSeen) : Number.NaN;
|
|
112
302
|
const expired = !lastSeen ||
|
|
@@ -114,8 +304,10 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
114
304
|
nowMs - lastSeenMs > SYNC_CACHE_TTL_MS;
|
|
115
305
|
nextHashes[key] = hash;
|
|
116
306
|
nextSeenAt[key] = nowIso;
|
|
117
|
-
const isChanged = expired ||
|
|
307
|
+
const isChanged = expired || changedSourceHashes.has(key) || validateOnly || rebuild;
|
|
118
308
|
if (isChanged) {
|
|
309
|
+
if (expired)
|
|
310
|
+
forceEntitySourceKeys.add(key);
|
|
119
311
|
if (markdownFiles.includes(file)) {
|
|
120
312
|
changedMarkdownFiles.push(file);
|
|
121
313
|
}
|
|
@@ -129,6 +321,19 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
129
321
|
console.warn(`Warning: Failed to hash ${file}: ${message}`);
|
|
130
322
|
}
|
|
131
323
|
}
|
|
324
|
+
// A v1 cache has only whole-file hashes. Perform one compiler-metadata
|
|
325
|
+
// backfill so subsequent edits can be reduced to normalized entity deltas.
|
|
326
|
+
if (syncCache.entityHashes === undefined ||
|
|
327
|
+
syncCache.sourceEntityIds === undefined) {
|
|
328
|
+
for (const file of markdownFiles) {
|
|
329
|
+
if (!changedMarkdownFiles.includes(file))
|
|
330
|
+
changedMarkdownFiles.push(file);
|
|
331
|
+
}
|
|
332
|
+
for (const file of manifestFiles) {
|
|
333
|
+
if (!changedManifestFiles.includes(file))
|
|
334
|
+
changedManifestFiles.push(file);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
132
337
|
// Coordinate refresh must precede extraction so the manifest overlay that
|
|
133
338
|
// is persisted into RDF observes the newly generated coordinates. Force
|
|
134
339
|
// each refreshed manifest through extraction even when its authored YAML
|
|
@@ -151,8 +356,73 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
151
356
|
}
|
|
152
357
|
const performedFullReindex = changedMarkdownFiles.length === markdownFiles.length &&
|
|
153
358
|
changedManifestFiles.length === manifestFiles.length;
|
|
154
|
-
const
|
|
155
|
-
|
|
359
|
+
const extraction = await processExtractions(changedMarkdownFiles, changedManifestFiles, validateOnly);
|
|
360
|
+
const { failedCacheKeys, errors } = extraction;
|
|
361
|
+
const extractedResults = extraction.results;
|
|
362
|
+
const nextEntityHashes = {
|
|
363
|
+
...(syncCache.entityHashes ?? {}),
|
|
364
|
+
};
|
|
365
|
+
const nextSourceEntityIds = {
|
|
366
|
+
...(syncCache.sourceEntityIds ?? {}),
|
|
367
|
+
};
|
|
368
|
+
const removedEntityIds = new Set();
|
|
369
|
+
const changedEntityIds = new Set();
|
|
370
|
+
const supportsEntityDelta = syncCache.entityHashes !== undefined &&
|
|
371
|
+
syncCache.sourceEntityIds !== undefined;
|
|
372
|
+
const useEntityDelta = supportsEntityDelta &&
|
|
373
|
+
!validateOnly &&
|
|
374
|
+
!rebuild &&
|
|
375
|
+
runtime.createProlog === undefined;
|
|
376
|
+
const materializeAllRelationships = !useEntityDelta || changedMarkdownFiles.length > 0;
|
|
377
|
+
const allRelationships = materializeAllRelationships
|
|
378
|
+
? Object.values(nextShardRelationships).flat().map(relationshipFromKey)
|
|
379
|
+
: [...addedShardRelationships, ...removedShardRelationships];
|
|
380
|
+
for (const deletedSource of deletedSourceFiles) {
|
|
381
|
+
const sourceKey = toCacheKey(deletedSource);
|
|
382
|
+
for (const id of nextSourceEntityIds[sourceKey] ?? []) {
|
|
383
|
+
removedEntityIds.add(id);
|
|
384
|
+
delete nextEntityHashes[id];
|
|
385
|
+
}
|
|
386
|
+
delete nextSourceEntityIds[sourceKey];
|
|
387
|
+
}
|
|
388
|
+
const resultsBySource = new Map();
|
|
389
|
+
for (const result of extractedResults) {
|
|
390
|
+
const sourceKey = toCacheKey(result.entity.source);
|
|
391
|
+
const sourceResults = resultsBySource.get(sourceKey) ?? [];
|
|
392
|
+
sourceResults.push(result);
|
|
393
|
+
resultsBySource.set(sourceKey, sourceResults);
|
|
394
|
+
}
|
|
395
|
+
for (const changedSource of [
|
|
396
|
+
...changedMarkdownFiles,
|
|
397
|
+
...changedManifestFiles,
|
|
398
|
+
]) {
|
|
399
|
+
const sourceKey = toCacheKey(changedSource);
|
|
400
|
+
if (failedCacheKeys.has(sourceKey))
|
|
401
|
+
continue;
|
|
402
|
+
const sourceResults = resultsBySource.get(sourceKey) ?? [];
|
|
403
|
+
const previousIds = new Set(nextSourceEntityIds[sourceKey] ?? []);
|
|
404
|
+
const currentIds = sourceResults.map(({ entity }) => entity.id).sort();
|
|
405
|
+
const currentIdSet = new Set(currentIds);
|
|
406
|
+
for (const previousId of previousIds) {
|
|
407
|
+
if (!currentIdSet.has(previousId)) {
|
|
408
|
+
removedEntityIds.add(previousId);
|
|
409
|
+
delete nextEntityHashes[previousId];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const result of sourceResults) {
|
|
413
|
+
const hash = normalizedEntityHash(result);
|
|
414
|
+
if (forceEntitySourceKeys.has(sourceKey) ||
|
|
415
|
+
nextEntityHashes[result.entity.id] !== hash) {
|
|
416
|
+
changedEntityIds.add(result.entity.id);
|
|
417
|
+
}
|
|
418
|
+
nextEntityHashes[result.entity.id] = hash;
|
|
419
|
+
}
|
|
420
|
+
nextSourceEntityIds[sourceKey] = currentIds;
|
|
421
|
+
}
|
|
422
|
+
const results = useEntityDelta
|
|
423
|
+
? extractedResults.filter(({ entity }) => changedEntityIds.has(entity.id))
|
|
424
|
+
: extractedResults;
|
|
425
|
+
for (const result of extractedResults) {
|
|
156
426
|
if (result.entity.type !== "req")
|
|
157
427
|
continue;
|
|
158
428
|
const key = toCacheKey(result.entity.source);
|
|
@@ -212,7 +482,13 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
212
482
|
exitCode: 1,
|
|
213
483
|
}, getCurrentCommit());
|
|
214
484
|
}
|
|
215
|
-
if (results.length === 0 &&
|
|
485
|
+
if (results.length === 0 &&
|
|
486
|
+
removedEntityIds.size === 0 &&
|
|
487
|
+
!relationshipChanged &&
|
|
488
|
+
(useEntityDelta || changedMarkdownFiles.length === 0) &&
|
|
489
|
+
(useEntityDelta || changedManifestFiles.length === 0) &&
|
|
490
|
+
deletedSourceFiles.length === 0 &&
|
|
491
|
+
!rebuild) {
|
|
216
492
|
const evictedHashes = {};
|
|
217
493
|
const evictedSeenAt = {};
|
|
218
494
|
const evictedSemanticHashes = {};
|
|
@@ -231,10 +507,17 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
231
507
|
writeSyncCache(cachePath, {
|
|
232
508
|
version: SYNC_CACHE_VERSION,
|
|
233
509
|
hashes: evictedHashes,
|
|
510
|
+
relationshipHashes: nextRelationshipHashes,
|
|
511
|
+
entityHashes: nextEntityHashes,
|
|
512
|
+
sourceEntityIds: nextSourceEntityIds,
|
|
513
|
+
shardRelationships: nextShardRelationships,
|
|
234
514
|
seenAt: evictedSeenAt,
|
|
235
515
|
semanticHashes: evictedSemanticHashes,
|
|
236
516
|
semanticContracts: evictedSemanticContracts,
|
|
237
517
|
});
|
|
518
|
+
if (runtime.createProlog === undefined) {
|
|
519
|
+
await checkpointNoopSync(process.cwd(), currentBranch);
|
|
520
|
+
}
|
|
238
521
|
console.log("✓ Imported 0 entities, 0 relationships (no changes)");
|
|
239
522
|
return withOptionalCommit({
|
|
240
523
|
branch: currentBranch,
|
|
@@ -253,6 +536,135 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
253
536
|
if (!kbExists && !rebuild) {
|
|
254
537
|
diagnostics.push(createKbMissingDiagnostic(currentBranch, livePath));
|
|
255
538
|
}
|
|
539
|
+
// implements REQ-core-journaled-engine-delta-sync
|
|
540
|
+
// Normal syncs are compiled directly into the long-lived single-writer
|
|
541
|
+
// engine. Rebuilds retain generation replacement semantics, and injected
|
|
542
|
+
// Prolog runtimes keep the staging path used by the contract fixtures.
|
|
543
|
+
// This is the cutover that avoids copying and rewriting a complete branch
|
|
544
|
+
// for a one-symbol or relationship-only change.
|
|
545
|
+
if (!validateOnly && !rebuild && runtime.createProlog === undefined) {
|
|
546
|
+
const engine = new EngineClient({
|
|
547
|
+
workspaceRoot: process.cwd(),
|
|
548
|
+
branch: currentBranch,
|
|
549
|
+
timeout: 120_000,
|
|
550
|
+
});
|
|
551
|
+
const engineProlog = engine;
|
|
552
|
+
try {
|
|
553
|
+
await engine.start();
|
|
554
|
+
const successfulChangedSources = [
|
|
555
|
+
...changedMarkdownFiles,
|
|
556
|
+
...changedManifestFiles,
|
|
557
|
+
].filter((file) => !failedCacheKeys.has(toCacheKey(file)));
|
|
558
|
+
const removedCount = useEntityDelta
|
|
559
|
+
? await retractEntitiesById(engineProlog, [...removedEntityIds])
|
|
560
|
+
: await retractEntitiesForSources(engineProlog, [
|
|
561
|
+
...successfulChangedSources,
|
|
562
|
+
...deletedSourceFiles,
|
|
563
|
+
]);
|
|
564
|
+
if (useEntityDelta) {
|
|
565
|
+
await retractEntityRelationshipsById(engineProlog, [
|
|
566
|
+
...changedEntityIds,
|
|
567
|
+
]);
|
|
568
|
+
const exactRemovedEdges = removedShardRelationships.filter((relationship) => !removedEntityIds.has(relationship.from) &&
|
|
569
|
+
!removedEntityIds.has(relationship.to) &&
|
|
570
|
+
!changedEntityIds.has(relationship.from));
|
|
571
|
+
await retractRelationships(engineProlog, exactRemovedEdges);
|
|
572
|
+
}
|
|
573
|
+
else if (relationshipChanged) {
|
|
574
|
+
const cleared = await engine.query("kb_retract_all_relationships");
|
|
575
|
+
if (!cleared.success) {
|
|
576
|
+
throw new SyncError(`Failed to clear changed relationship shards: ${cleared.error || "Unknown error"}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const entityIds = new Set();
|
|
580
|
+
for (const { entity } of results) {
|
|
581
|
+
entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
|
|
582
|
+
}
|
|
583
|
+
const { entityCount, kbModified: entitiesModified } = await persistEntities(engineProlog, results, entityIds, {
|
|
584
|
+
loadExistingEntityIds: !useEntityDelta,
|
|
585
|
+
});
|
|
586
|
+
const validationErrors = useEntityDelta
|
|
587
|
+
? []
|
|
588
|
+
: validateRelationships(allRelationships, entityIds);
|
|
589
|
+
if (validationErrors.length > 0) {
|
|
590
|
+
console.warn(`Warning: ${validationErrors.length} dangling relationship(s) found`);
|
|
591
|
+
}
|
|
592
|
+
const danglingKeys = new Set(validationErrors.map(({ relationship }) => `${relationship.type}|${relationship.from}|${relationship.to}`));
|
|
593
|
+
const shardDeltaByKey = new Map();
|
|
594
|
+
const shardCandidates = useEntityDelta
|
|
595
|
+
? [
|
|
596
|
+
...addedShardRelationships,
|
|
597
|
+
...allRelationships.filter((relationship) => changedEntityIds.has(relationship.from)),
|
|
598
|
+
]
|
|
599
|
+
: allRelationships;
|
|
600
|
+
for (const relationship of shardCandidates) {
|
|
601
|
+
if (!danglingKeys.has(`${relationship.type}|${relationship.from}|${relationship.to}`)) {
|
|
602
|
+
shardDeltaByKey.set(relationshipKey(relationship), relationship);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const { relationshipCount, kbModified: relationshipsModified } = await persistRelationships(engineProlog, results, [
|
|
606
|
+
...shardDeltaByKey.values(),
|
|
607
|
+
]);
|
|
608
|
+
const kbModified = removedCount > 0 ||
|
|
609
|
+
relationshipChanged ||
|
|
610
|
+
entitiesModified ||
|
|
611
|
+
relationshipsModified;
|
|
612
|
+
if (kbModified)
|
|
613
|
+
engine.invalidateCache();
|
|
614
|
+
const saveResult = await engine.save();
|
|
615
|
+
if (!saveResult.success) {
|
|
616
|
+
throw new SyncError(`Failed to save journaled KB: ${saveResult.error || "Unknown error"}`);
|
|
617
|
+
}
|
|
618
|
+
const evictedHashes = {};
|
|
619
|
+
const evictedSeenAt = {};
|
|
620
|
+
const evictedSemanticHashes = {};
|
|
621
|
+
const evictedSemanticContracts = {};
|
|
622
|
+
for (const [key, hash] of Object.entries(nextHashes)) {
|
|
623
|
+
if (failedCacheKeys.has(key))
|
|
624
|
+
continue;
|
|
625
|
+
evictedHashes[key] = hash;
|
|
626
|
+
evictedSeenAt[key] = nextSeenAt[key] ?? nowIso;
|
|
627
|
+
if (nextSemanticHashes[key] !== undefined) {
|
|
628
|
+
evictedSemanticHashes[key] = nextSemanticHashes[key];
|
|
629
|
+
evictedSemanticContracts[key] = nextSemanticContracts[key] === true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
writeSyncCache(path.join(livePath, "sync-cache.json"), {
|
|
633
|
+
version: SYNC_CACHE_VERSION,
|
|
634
|
+
hashes: evictedHashes,
|
|
635
|
+
relationshipHashes: nextRelationshipHashes,
|
|
636
|
+
entityHashes: nextEntityHashes,
|
|
637
|
+
sourceEntityIds: nextSourceEntityIds,
|
|
638
|
+
shardRelationships: nextShardRelationships,
|
|
639
|
+
seenAt: evictedSeenAt,
|
|
640
|
+
semanticHashes: evictedSemanticHashes,
|
|
641
|
+
semanticContracts: evictedSemanticContracts,
|
|
642
|
+
});
|
|
643
|
+
published = true;
|
|
644
|
+
if (performedFullReindex &&
|
|
645
|
+
markdownFiles.length > 0 &&
|
|
646
|
+
entityCount < markdownFiles.length) {
|
|
647
|
+
diagnostics.push(createDocsNotIndexedDiagnostic(markdownFiles.length, entityCount));
|
|
648
|
+
}
|
|
649
|
+
console.log(`✓ Imported ${entityCount} entities, ${relationshipCount} relationships (removed ${removedCount} entities)`);
|
|
650
|
+
const commit = getCurrentCommit();
|
|
651
|
+
const summary = withOptionalCommit({
|
|
652
|
+
branch: currentBranch,
|
|
653
|
+
timestamp: new Date().toISOString(),
|
|
654
|
+
entityCounts,
|
|
655
|
+
relationshipCount,
|
|
656
|
+
success: true,
|
|
657
|
+
published,
|
|
658
|
+
failures: diagnostics,
|
|
659
|
+
durationMs: Date.now() - startTime,
|
|
660
|
+
}, commit);
|
|
661
|
+
console.log(formatSyncSummary(summary));
|
|
662
|
+
return { ...summary, exitCode: 0 };
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
await engine.terminate();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
256
668
|
stagingPath = createUniqueStagingPath(currentBranch, process.cwd());
|
|
257
669
|
const runtimeContext = {
|
|
258
670
|
currentBranch,
|
|
@@ -276,6 +688,20 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
276
688
|
for (const { entity } of results) {
|
|
277
689
|
entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
|
|
278
690
|
}
|
|
691
|
+
// A copied generation retains entities from old source contents. Remove
|
|
692
|
+
// only successfully extracted/deleted sources before applying the new
|
|
693
|
+
// entities; failed extraction deliberately leaves the previous data.
|
|
694
|
+
await retractEntitiesForSources(prolog, [
|
|
695
|
+
...changedMarkdownFiles,
|
|
696
|
+
...changedManifestFiles,
|
|
697
|
+
...deletedSourceFiles,
|
|
698
|
+
].filter((file) => !failedCacheKeys.has(toCacheKey(file))));
|
|
699
|
+
if (relationshipChanged) {
|
|
700
|
+
const clearRelationships = await prolog.query("kb_retract_all_relationships");
|
|
701
|
+
if (!clearRelationships.success) {
|
|
702
|
+
throw new SyncError(`Failed to clear changed relationship shards: ${clearRelationships.error || "Unknown error"}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
279
705
|
const { entityCount, kbModified: entitiesModified } = await persistEntities(prolog, results, entityIds);
|
|
280
706
|
const validationErrors = validateRelationships(allRelationships, entityIds);
|
|
281
707
|
if (validationErrors.length > 0) {
|
|
@@ -333,7 +759,14 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
333
759
|
}
|
|
334
760
|
await prolog.query("kb_detach");
|
|
335
761
|
await prolog.terminate();
|
|
336
|
-
|
|
762
|
+
const journaledLive = existsSync(path.join(livePath, "storage.json"));
|
|
763
|
+
if (rebuild && journaledLive) {
|
|
764
|
+
atomicPublishGeneration(stagingPath, livePath);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
atomicPublish(stagingPath, livePath);
|
|
768
|
+
}
|
|
769
|
+
fsyncJournaledBranchStore(livePath);
|
|
337
770
|
cleanupStaging(stagingPath);
|
|
338
771
|
const evictedHashes = {};
|
|
339
772
|
const evictedSeenAt = {};
|
|
@@ -354,6 +787,10 @@ export async function syncCommand(options = {}, runtime = {}) {
|
|
|
354
787
|
writeSyncCache(liveCachePath, {
|
|
355
788
|
version: SYNC_CACHE_VERSION,
|
|
356
789
|
hashes: evictedHashes,
|
|
790
|
+
relationshipHashes: nextRelationshipHashes,
|
|
791
|
+
entityHashes: nextEntityHashes,
|
|
792
|
+
sourceEntityIds: nextSourceEntityIds,
|
|
793
|
+
shardRelationships: nextShardRelationships,
|
|
357
794
|
seenAt: evictedSeenAt,
|
|
358
795
|
semanticHashes: evictedSemanticHashes,
|
|
359
796
|
semanticContracts: evictedSemanticContracts,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-daemon.d.ts","sourceRoot":"","sources":["../src/engine-daemon.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { runEngineDaemon } from "./engine.js";
|
|
4
|
+
function requiredArg(args, name) {
|
|
5
|
+
const index = args.indexOf(name);
|
|
6
|
+
const value = index >= 0 ? args[index + 1] : undefined;
|
|
7
|
+
if (value === undefined || value.length === 0) {
|
|
8
|
+
throw new Error(`Missing ${name} argument`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
try {
|
|
14
|
+
await runEngineDaemon({
|
|
15
|
+
workspaceRoot: requiredArg(args, "--workspace"),
|
|
16
|
+
branch: requiredArg(args, "--branch"),
|
|
17
|
+
socketPath: requiredArg(args, "--socket"),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
console.error(message);
|
|
23
|
+
const socketIndex = args.indexOf("--socket");
|
|
24
|
+
const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
|
|
25
|
+
if (socketPath) {
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(`${socketPath}.error`, `${message}\n`, { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Preserve the process exit code even if the diagnostic path is gone.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|