@gmickel/gno 1.34.5 → 1.34.6
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/README.md +10 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.34.6.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/spec/cli.md +6 -0
- package/src/ingestion/sync.ts +368 -84
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +220 -0
- package/src/serve/watch-reconciliation-fallback.ts +404 -0
- package/src/serve/watch-reconciliation-shared.ts +343 -0
- package/src/serve/watch-reconciliation.ts +122 -0
- package/src/serve/watch-service-events.ts +261 -0
- package/src/serve/watch-service-flush-generation.ts +140 -0
- package/src/serve/watch-service-flush-helpers.ts +147 -0
- package/src/serve/watch-service-flush.ts +433 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +219 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +101 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +265 -306
- package/src/serve/watch-snapshot-handles.ts +285 -0
- package/src/serve/watch-snapshot-libc.ts +391 -0
- package/src/serve/watch-snapshot-ops.ts +399 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +297 -0
- package/src/serve/watch-snapshot-types.ts +350 -0
- package/src/serve/watch-snapshot.ts +51 -0
- package/src/store/index.ts +1 -1
- package/src/store/sqlite/adapter.ts +191 -0
- package/src/store/types.ts +66 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.5.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -332,6 +332,11 @@ gno daemon --detach # background (macOS/Linux); use --status / --stop to manag
|
|
|
332
332
|
detached child and exits 0; `gno daemon --status` and `gno daemon --stop` give
|
|
333
333
|
you lifecycle control without `nohup`, `launchd`, or `systemd` units.
|
|
334
334
|
|
|
335
|
+
Exact eligible file events always go through content hashing. Atomic-save temp
|
|
336
|
+
files, directory events, missing filenames, and recursive deletions are
|
|
337
|
+
reconciled from bounded filesystem/index evidence, so final files appear and
|
|
338
|
+
proven removals disappear without routinely resyncing untouched siblings.
|
|
339
|
+
|
|
335
340
|
See also: [docs/DAEMON.md](./docs/DAEMON.md)
|
|
336
341
|
|
|
337
342
|
### Connect to AI Agents
|
|
@@ -396,6 +401,11 @@ headless. `--detach` / `--status` / `--stop` give you symmetric lifecycle
|
|
|
396
401
|
controls so you don't need `nohup`, `launchd`, or `systemd` units. The same
|
|
397
402
|
flag set is available on `gno serve`.
|
|
398
403
|
|
|
404
|
+
The shared watcher preserves exact content-hash decisions while reconciling
|
|
405
|
+
ambiguous atomic replacements and directory changes with bounded work and
|
|
406
|
+
durable retries. Tested guarantees cover supported local filesystems; network,
|
|
407
|
+
removable, and coarse-timestamp filesystems are not universally claimed.
|
|
408
|
+
|
|
399
409
|
[Daemon guide →](https://gno.sh/features/daemon-mode)
|
|
400
410
|
|
|
401
411
|
---
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
76e1e560efa69f780e5e61130008e863cf8288a8ffe5cd82e16b48608c6c8578 gno-browser-clipper-v1.34.6.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.6",
|
|
4
4
|
"description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"embeddings",
|
|
@@ -82,6 +82,9 @@
|
|
|
82
82
|
"eval:hybrid:delta": "bun scripts/hybrid-benchmark.ts --delta",
|
|
83
83
|
"bench:ast-chunking": "bun scripts/ast-chunking-benchmark.ts",
|
|
84
84
|
"smoke:serve-shutdown": "bun scripts/serve-shutdown-smoke.ts",
|
|
85
|
+
"test:watcher:filesystem": "bun test test/serve/watch-service-filesystem.test.ts test/serve/resident-runtime.test.ts",
|
|
86
|
+
"bench:watcher-reconciliation": "bun scripts/watcher-reconciliation-benchmark.ts",
|
|
87
|
+
"smoke:watcher-reconciliation": "bun scripts/watcher-reconciliation-smoke.ts",
|
|
85
88
|
"bench:code-embeddings": "bun scripts/code-embedding-benchmark.ts",
|
|
86
89
|
"bench:code-embeddings:write": "bun scripts/code-embedding-benchmark.ts --write",
|
|
87
90
|
"bench:general-embeddings": "bun scripts/general-embedding-benchmark.ts",
|
package/spec/cli.md
CHANGED
|
@@ -3369,6 +3369,12 @@ is blocked.
|
|
|
3369
3369
|
- Opens DB once at startup
|
|
3370
3370
|
- Loads config and requires at least one configured collection
|
|
3371
3371
|
- Starts the same watcher + embed scheduler used by `gno serve`
|
|
3372
|
+
- Exact contained eligible paths always use targeted content-hash sync.
|
|
3373
|
+
Ambiguous temp/directory/missing-name/recursive-delete events use bounded,
|
|
3374
|
+
failure-safe filesystem/index reconciliation with durable retry.
|
|
3375
|
+
- Bounded overflow or unavailable native anchored handles escalates to full
|
|
3376
|
+
collection sync. This does not claim universal network/removable filesystem
|
|
3377
|
+
watcher behavior.
|
|
3372
3378
|
- Runs an initial sync by default
|
|
3373
3379
|
- Triggers embedding after initial sync completes
|
|
3374
3380
|
- Runs in the foreground until `SIGINT` / `SIGTERM`
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -616,6 +616,31 @@ class Semaphore {
|
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
+
function summarizePathResults(
|
|
620
|
+
collection: string,
|
|
621
|
+
results: FileSyncResult[],
|
|
622
|
+
markedInactive: number,
|
|
623
|
+
startedAt: number,
|
|
624
|
+
errors: CollectionSyncResult["errors"]
|
|
625
|
+
): CollectionSyncResult {
|
|
626
|
+
return {
|
|
627
|
+
collection,
|
|
628
|
+
filesProcessed: results.length,
|
|
629
|
+
filesAdded: results.filter((result) => result.status === "added").length,
|
|
630
|
+
filesUpdated: results.filter((result) => result.status === "updated")
|
|
631
|
+
.length,
|
|
632
|
+
filesUnchanged: results.filter((result) => result.status === "unchanged")
|
|
633
|
+
.length,
|
|
634
|
+
filesErrored: results.filter((result) => result.status === "error").length,
|
|
635
|
+
filesSkipped: results.filter((result) => result.status === "skipped")
|
|
636
|
+
.length,
|
|
637
|
+
filesMarkedInactive: markedInactive,
|
|
638
|
+
durationMs: Date.now() - startedAt,
|
|
639
|
+
files: results,
|
|
640
|
+
errors,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
619
644
|
/**
|
|
620
645
|
* Sync service implementation.
|
|
621
646
|
*/
|
|
@@ -1161,6 +1186,54 @@ export class SyncService {
|
|
|
1161
1186
|
return result.files ?? [];
|
|
1162
1187
|
}
|
|
1163
1188
|
|
|
1189
|
+
/**
|
|
1190
|
+
* Inactivate proven-absent index sources without requiring disk absence.
|
|
1191
|
+
* Used when classification proves a path is no longer an eligible file
|
|
1192
|
+
* source even if a directory/FIFO/device now occupies the path.
|
|
1193
|
+
* Preserves record-container fan-out and typed-edge projection.
|
|
1194
|
+
*/
|
|
1195
|
+
async inactivateAbsentSources(
|
|
1196
|
+
collection: Collection,
|
|
1197
|
+
store: StorePort,
|
|
1198
|
+
relPaths: string[],
|
|
1199
|
+
options: SyncOptions = {}
|
|
1200
|
+
): Promise<CollectionSyncResult> {
|
|
1201
|
+
const startedAt = Date.now();
|
|
1202
|
+
const syncOptions: SyncOptions = {
|
|
1203
|
+
...options,
|
|
1204
|
+
contentTypeRules: options.contentTypeRules ?? [],
|
|
1205
|
+
contentTypeRulesFingerprint:
|
|
1206
|
+
options.contentTypeRulesFingerprint ??
|
|
1207
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1208
|
+
};
|
|
1209
|
+
const results: FileSyncResult[] = [];
|
|
1210
|
+
const projectionSourceIds = new Set<number>();
|
|
1211
|
+
let markedInactive = 0;
|
|
1212
|
+
|
|
1213
|
+
for (const relPath of relPaths) {
|
|
1214
|
+
const outcome = await this.inactivateOneAbsentSource(
|
|
1215
|
+
collection,
|
|
1216
|
+
store,
|
|
1217
|
+
relPath,
|
|
1218
|
+
projectionSourceIds
|
|
1219
|
+
);
|
|
1220
|
+
results.push(outcome.result);
|
|
1221
|
+
markedInactive += outcome.markedInactive;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const errors =
|
|
1225
|
+
syncOptions.projectTypedEdges === false
|
|
1226
|
+
? []
|
|
1227
|
+
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1228
|
+
return summarizePathResults(
|
|
1229
|
+
collection.name,
|
|
1230
|
+
results,
|
|
1231
|
+
markedInactive,
|
|
1232
|
+
startedAt,
|
|
1233
|
+
errors
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1164
1237
|
async syncPaths(
|
|
1165
1238
|
collection: Collection,
|
|
1166
1239
|
store: StorePort,
|
|
@@ -1195,20 +1268,52 @@ export class SyncService {
|
|
|
1195
1268
|
}
|
|
1196
1269
|
const recordDocuments = recordDocumentsResult.value;
|
|
1197
1270
|
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1198
|
-
|
|
1271
|
+
if (!existingResult.ok) {
|
|
1272
|
+
results.push({
|
|
1273
|
+
relPath,
|
|
1274
|
+
status: "error",
|
|
1275
|
+
errorCode: existingResult.error.code,
|
|
1276
|
+
errorMessage: existingResult.error.message,
|
|
1277
|
+
});
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
const existingDoc = existingResult.value;
|
|
1199
1281
|
if (existingDoc) {
|
|
1200
|
-
await this.collectProjectionSourceIds(
|
|
1282
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1201
1283
|
store,
|
|
1202
1284
|
existingDoc.id,
|
|
1203
1285
|
projectionSourceIds
|
|
1204
1286
|
);
|
|
1287
|
+
if (collectError) {
|
|
1288
|
+
results.push({
|
|
1289
|
+
relPath,
|
|
1290
|
+
status: "error",
|
|
1291
|
+
errorCode: collectError.code,
|
|
1292
|
+
errorMessage: collectError.message,
|
|
1293
|
+
});
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1205
1296
|
}
|
|
1297
|
+
let recordCollectFailed: FileSyncResult | null = null;
|
|
1206
1298
|
for (const recordDocument of recordDocuments) {
|
|
1207
|
-
await this.collectProjectionSourceIds(
|
|
1299
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1208
1300
|
store,
|
|
1209
1301
|
recordDocument.id,
|
|
1210
1302
|
projectionSourceIds
|
|
1211
1303
|
);
|
|
1304
|
+
if (collectError) {
|
|
1305
|
+
recordCollectFailed = {
|
|
1306
|
+
relPath,
|
|
1307
|
+
status: "error",
|
|
1308
|
+
errorCode: collectError.code,
|
|
1309
|
+
errorMessage: collectError.message,
|
|
1310
|
+
};
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (recordCollectFailed) {
|
|
1315
|
+
results.push(recordCollectFailed);
|
|
1316
|
+
continue;
|
|
1212
1317
|
}
|
|
1213
1318
|
|
|
1214
1319
|
const absPath = join(collection.path, relPath);
|
|
@@ -1230,39 +1335,15 @@ export class SyncService {
|
|
|
1230
1335
|
});
|
|
1231
1336
|
continue;
|
|
1232
1337
|
}
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
.filter((document) => document.active)
|
|
1237
|
-
.map((document) => document.relPath),
|
|
1238
|
-
];
|
|
1239
|
-
if (activePaths.length > 0) {
|
|
1240
|
-
const inactiveResult = await store.markInactive(
|
|
1241
|
-
collection.name,
|
|
1242
|
-
activePaths
|
|
1243
|
-
);
|
|
1244
|
-
if (!inactiveResult.ok) {
|
|
1245
|
-
results.push({
|
|
1246
|
-
relPath,
|
|
1247
|
-
status: "error",
|
|
1248
|
-
errorCode: inactiveResult.error.code,
|
|
1249
|
-
errorMessage: inactiveResult.error.message,
|
|
1250
|
-
});
|
|
1251
|
-
continue;
|
|
1252
|
-
}
|
|
1253
|
-
markedInactive += inactiveResult.value;
|
|
1254
|
-
results.push({
|
|
1255
|
-
relPath,
|
|
1256
|
-
status: "updated",
|
|
1257
|
-
docid: existingDoc?.docid,
|
|
1258
|
-
});
|
|
1259
|
-
continue;
|
|
1260
|
-
}
|
|
1261
|
-
results.push({
|
|
1338
|
+
const inactive = await this.inactivateOneAbsentSource(
|
|
1339
|
+
collection,
|
|
1340
|
+
store,
|
|
1262
1341
|
relPath,
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1342
|
+
projectionSourceIds,
|
|
1343
|
+
{ existingDoc, recordDocuments }
|
|
1344
|
+
);
|
|
1345
|
+
results.push(inactive.result);
|
|
1346
|
+
markedInactive += inactive.markedInactive;
|
|
1266
1347
|
continue;
|
|
1267
1348
|
}
|
|
1268
1349
|
|
|
@@ -1322,28 +1403,67 @@ export class SyncService {
|
|
|
1322
1403
|
syncOptions
|
|
1323
1404
|
);
|
|
1324
1405
|
results.push(result);
|
|
1406
|
+
if (result.status === "error" || result.status === "skipped") {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
// Post-write projection identity must succeed so typed-edge cleanup can run.
|
|
1325
1410
|
const currentResult = await store.getDocument(collection.name, relPath);
|
|
1326
|
-
|
|
1411
|
+
if (!currentResult.ok) {
|
|
1412
|
+
results[results.length - 1] = {
|
|
1413
|
+
relPath,
|
|
1414
|
+
status: "error",
|
|
1415
|
+
errorCode: currentResult.error.code,
|
|
1416
|
+
errorMessage: currentResult.error.message,
|
|
1417
|
+
};
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
const currentDoc = currentResult.value;
|
|
1327
1421
|
if (currentDoc?.active) {
|
|
1328
|
-
await this.collectProjectionSourceIds(
|
|
1422
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1329
1423
|
store,
|
|
1330
1424
|
currentDoc.id,
|
|
1331
1425
|
projectionSourceIds
|
|
1332
1426
|
);
|
|
1427
|
+
if (collectError) {
|
|
1428
|
+
results[results.length - 1] = {
|
|
1429
|
+
relPath,
|
|
1430
|
+
status: "error",
|
|
1431
|
+
errorCode: collectError.code,
|
|
1432
|
+
errorMessage: collectError.message,
|
|
1433
|
+
};
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1333
1436
|
}
|
|
1334
1437
|
const currentDocuments = await store.listRecordDocuments(
|
|
1335
1438
|
collection.name,
|
|
1336
1439
|
relPath
|
|
1337
1440
|
);
|
|
1338
|
-
if (currentDocuments.ok) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1441
|
+
if (!currentDocuments.ok) {
|
|
1442
|
+
results[results.length - 1] = {
|
|
1443
|
+
relPath,
|
|
1444
|
+
status: "error",
|
|
1445
|
+
errorCode: currentDocuments.error.code,
|
|
1446
|
+
errorMessage: currentDocuments.error.message,
|
|
1447
|
+
};
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
for (const recordDocument of currentDocuments.value) {
|
|
1451
|
+
if (!recordDocument.active) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1455
|
+
store,
|
|
1456
|
+
recordDocument.id,
|
|
1457
|
+
projectionSourceIds
|
|
1458
|
+
);
|
|
1459
|
+
if (collectError) {
|
|
1460
|
+
results[results.length - 1] = {
|
|
1461
|
+
relPath,
|
|
1462
|
+
status: "error",
|
|
1463
|
+
errorCode: collectError.code,
|
|
1464
|
+
errorMessage: collectError.message,
|
|
1465
|
+
};
|
|
1466
|
+
break;
|
|
1347
1467
|
}
|
|
1348
1468
|
}
|
|
1349
1469
|
}
|
|
@@ -1352,55 +1472,184 @@ export class SyncService {
|
|
|
1352
1472
|
syncOptions.projectTypedEdges === false
|
|
1353
1473
|
? []
|
|
1354
1474
|
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1475
|
+
return summarizePathResults(
|
|
1476
|
+
collection.name,
|
|
1477
|
+
results,
|
|
1478
|
+
markedInactive,
|
|
1479
|
+
startedAt,
|
|
1480
|
+
errors
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Soft-delete active docs/record-container children for a proven-absent source.
|
|
1486
|
+
* Does not inspect disk — callers prove absence of an indexable file source.
|
|
1487
|
+
* Store lookup / backlink / mark failures surface as per-file errors (never
|
|
1488
|
+
* silent null/skip) so callers cannot advance snapshot authority.
|
|
1489
|
+
*/
|
|
1490
|
+
private async inactivateOneAbsentSource(
|
|
1491
|
+
collection: Collection,
|
|
1492
|
+
store: StorePort,
|
|
1493
|
+
relPath: string,
|
|
1494
|
+
projectionSourceIds: Set<number>,
|
|
1495
|
+
preloaded?: {
|
|
1496
|
+
existingDoc: { id: number; active: boolean; docid?: string } | null;
|
|
1497
|
+
recordDocuments: Array<{ id: number; active: boolean; relPath: string }>;
|
|
1498
|
+
}
|
|
1499
|
+
): Promise<{ result: FileSyncResult; markedInactive: number }> {
|
|
1500
|
+
let existingDoc = preloaded?.existingDoc ?? null;
|
|
1501
|
+
let recordDocuments = preloaded?.recordDocuments;
|
|
1502
|
+
|
|
1503
|
+
if (!preloaded) {
|
|
1504
|
+
const recordDocumentsResult = await store.listRecordDocuments(
|
|
1505
|
+
collection.name,
|
|
1506
|
+
relPath
|
|
1507
|
+
);
|
|
1508
|
+
if (!recordDocumentsResult.ok) {
|
|
1509
|
+
return {
|
|
1510
|
+
result: {
|
|
1511
|
+
relPath,
|
|
1512
|
+
status: "error",
|
|
1513
|
+
errorCode: recordDocumentsResult.error.code,
|
|
1514
|
+
errorMessage: recordDocumentsResult.error.message,
|
|
1515
|
+
},
|
|
1516
|
+
markedInactive: 0,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
recordDocuments = recordDocumentsResult.value;
|
|
1520
|
+
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1521
|
+
if (!existingResult.ok) {
|
|
1522
|
+
// ok:false is a hard per-file error — never treat as missing/skip.
|
|
1523
|
+
return {
|
|
1524
|
+
result: {
|
|
1525
|
+
relPath,
|
|
1526
|
+
status: "error",
|
|
1527
|
+
errorCode: existingResult.error.code,
|
|
1528
|
+
errorMessage: existingResult.error.message,
|
|
1529
|
+
},
|
|
1530
|
+
markedInactive: 0,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
existingDoc = existingResult.value;
|
|
1534
|
+
if (existingDoc) {
|
|
1535
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1536
|
+
store,
|
|
1537
|
+
existingDoc.id,
|
|
1538
|
+
projectionSourceIds
|
|
1539
|
+
);
|
|
1540
|
+
if (collectError) {
|
|
1541
|
+
return {
|
|
1542
|
+
result: {
|
|
1543
|
+
relPath,
|
|
1544
|
+
status: "error",
|
|
1545
|
+
errorCode: collectError.code,
|
|
1546
|
+
errorMessage: collectError.message,
|
|
1547
|
+
},
|
|
1548
|
+
markedInactive: 0,
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
for (const recordDocument of recordDocuments) {
|
|
1553
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1554
|
+
store,
|
|
1555
|
+
recordDocument.id,
|
|
1556
|
+
projectionSourceIds
|
|
1557
|
+
);
|
|
1558
|
+
if (collectError) {
|
|
1559
|
+
return {
|
|
1560
|
+
result: {
|
|
1561
|
+
relPath,
|
|
1562
|
+
status: "error",
|
|
1563
|
+
errorCode: collectError.code,
|
|
1564
|
+
errorMessage: collectError.message,
|
|
1565
|
+
},
|
|
1566
|
+
markedInactive: 0,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1368
1571
|
|
|
1572
|
+
const docs = recordDocuments ?? [];
|
|
1573
|
+
// Physical source path may own multiple logical docs (record containers).
|
|
1574
|
+
// Deduplicate while preserving the primary source path first.
|
|
1575
|
+
const activePathSet = new Set<string>();
|
|
1576
|
+
if (existingDoc?.active) {
|
|
1577
|
+
activePathSet.add(relPath);
|
|
1578
|
+
}
|
|
1579
|
+
for (const document of docs) {
|
|
1580
|
+
if (document.active) {
|
|
1581
|
+
activePathSet.add(document.relPath);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const activePaths = [...activePathSet];
|
|
1585
|
+
if (activePaths.length > 0) {
|
|
1586
|
+
const inactiveResult = await store.markInactive(
|
|
1587
|
+
collection.name,
|
|
1588
|
+
activePaths
|
|
1589
|
+
);
|
|
1590
|
+
if (!inactiveResult.ok) {
|
|
1591
|
+
return {
|
|
1592
|
+
result: {
|
|
1593
|
+
relPath,
|
|
1594
|
+
status: "error",
|
|
1595
|
+
errorCode: inactiveResult.error.code,
|
|
1596
|
+
errorMessage: inactiveResult.error.message,
|
|
1597
|
+
},
|
|
1598
|
+
markedInactive: 0,
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
result: {
|
|
1603
|
+
relPath,
|
|
1604
|
+
status: "updated",
|
|
1605
|
+
docid: existingDoc?.docid,
|
|
1606
|
+
},
|
|
1607
|
+
markedInactive: inactiveResult.value,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1369
1610
|
return {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
filesSkipped: skipped,
|
|
1377
|
-
filesMarkedInactive: markedInactive,
|
|
1378
|
-
durationMs: Date.now() - startedAt,
|
|
1379
|
-
files: results,
|
|
1380
|
-
errors,
|
|
1611
|
+
result: {
|
|
1612
|
+
relPath,
|
|
1613
|
+
status: existingDoc ? "unchanged" : "skipped",
|
|
1614
|
+
docid: existingDoc?.docid,
|
|
1615
|
+
},
|
|
1616
|
+
markedInactive: 0,
|
|
1381
1617
|
};
|
|
1382
1618
|
}
|
|
1383
1619
|
|
|
1620
|
+
/**
|
|
1621
|
+
* Collect source doc ids that need typed-edge re-projection after a change.
|
|
1622
|
+
* Backlink lookup failures are fatal for the caller (not silently ignored).
|
|
1623
|
+
*/
|
|
1384
1624
|
private async collectProjectionSourceIds(
|
|
1385
1625
|
store: StorePort,
|
|
1386
1626
|
documentId: number,
|
|
1387
1627
|
sourceIds: Set<number>
|
|
1388
|
-
): Promise<
|
|
1628
|
+
): Promise<{ code: string; message: string } | null> {
|
|
1389
1629
|
sourceIds.add(documentId);
|
|
1390
1630
|
const [linkBacklinks, edgeBacklinks] = await Promise.all([
|
|
1391
1631
|
store.getBacklinksForDoc(documentId),
|
|
1392
1632
|
store.getEdgeBacklinksForDoc(documentId),
|
|
1393
1633
|
]);
|
|
1394
|
-
if (linkBacklinks.ok) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1634
|
+
if (!linkBacklinks.ok) {
|
|
1635
|
+
return {
|
|
1636
|
+
code: linkBacklinks.error.code,
|
|
1637
|
+
message: linkBacklinks.error.message,
|
|
1638
|
+
};
|
|
1398
1639
|
}
|
|
1399
|
-
if (edgeBacklinks.ok) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1640
|
+
if (!edgeBacklinks.ok) {
|
|
1641
|
+
return {
|
|
1642
|
+
code: edgeBacklinks.error.code,
|
|
1643
|
+
message: edgeBacklinks.error.message,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
for (const backlink of linkBacklinks.value) {
|
|
1647
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1648
|
+
}
|
|
1649
|
+
for (const backlink of edgeBacklinks.value) {
|
|
1650
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1403
1651
|
}
|
|
1652
|
+
return null;
|
|
1404
1653
|
}
|
|
1405
1654
|
|
|
1406
1655
|
private async projectTypedEdges(
|
|
@@ -1441,7 +1690,19 @@ export class SyncService {
|
|
|
1441
1690
|
if (selectedSourceIds) {
|
|
1442
1691
|
for (const documentId of selectedSourceIds) {
|
|
1443
1692
|
if (!activeIds.has(documentId)) {
|
|
1444
|
-
|
|
1693
|
+
// Clear typed edges for inactivated projection sources.
|
|
1694
|
+
const clearResult = await store.setDocEdges(
|
|
1695
|
+
documentId,
|
|
1696
|
+
[],
|
|
1697
|
+
"frontmatter-relation"
|
|
1698
|
+
);
|
|
1699
|
+
if (!clearResult.ok) {
|
|
1700
|
+
errors.push({
|
|
1701
|
+
relPath: `(doc:${documentId})`,
|
|
1702
|
+
code: clearResult.error.code,
|
|
1703
|
+
message: clearResult.error.message,
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1445
1706
|
}
|
|
1446
1707
|
}
|
|
1447
1708
|
}
|
|
@@ -1798,10 +2059,18 @@ export class SyncService {
|
|
|
1798
2059
|
}
|
|
1799
2060
|
}
|
|
1800
2061
|
|
|
1801
|
-
// 5. Mark missing files as inactive
|
|
2062
|
+
// 5. Mark missing files as inactive (inventory failures are hard errors).
|
|
1802
2063
|
let markedInactive = 0;
|
|
2064
|
+
let inventoryErrored = 0;
|
|
1803
2065
|
const existingDocsResult = await store.listDocuments(collection.name);
|
|
1804
|
-
if (existingDocsResult.ok) {
|
|
2066
|
+
if (!existingDocsResult.ok) {
|
|
2067
|
+
inventoryErrored += 1;
|
|
2068
|
+
errors.push({
|
|
2069
|
+
relPath: "",
|
|
2070
|
+
code: existingDocsResult.error.code,
|
|
2071
|
+
message: existingDocsResult.error.message,
|
|
2072
|
+
});
|
|
2073
|
+
} else {
|
|
1805
2074
|
const missingPaths = existingDocsResult.value
|
|
1806
2075
|
.filter(
|
|
1807
2076
|
(document) =>
|
|
@@ -1817,6 +2086,21 @@ export class SyncService {
|
|
|
1817
2086
|
);
|
|
1818
2087
|
if (markResult.ok) {
|
|
1819
2088
|
markedInactive = markResult.value;
|
|
2089
|
+
} else {
|
|
2090
|
+
inventoryErrored += missingPaths.length;
|
|
2091
|
+
errors.push({
|
|
2092
|
+
relPath: missingPaths[0] ?? "",
|
|
2093
|
+
code: markResult.error.code,
|
|
2094
|
+
message: markResult.error.message,
|
|
2095
|
+
});
|
|
2096
|
+
for (const missingPath of missingPaths) {
|
|
2097
|
+
fileResults.push({
|
|
2098
|
+
relPath: missingPath,
|
|
2099
|
+
status: "error",
|
|
2100
|
+
errorCode: markResult.error.code,
|
|
2101
|
+
errorMessage: markResult.error.message,
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
1820
2104
|
}
|
|
1821
2105
|
}
|
|
1822
2106
|
}
|
|
@@ -1827,11 +2111,11 @@ export class SyncService {
|
|
|
1827
2111
|
|
|
1828
2112
|
return {
|
|
1829
2113
|
collection: collection.name,
|
|
1830
|
-
filesProcessed: entries.length,
|
|
2114
|
+
filesProcessed: entries.length + inventoryErrored,
|
|
1831
2115
|
filesAdded: added,
|
|
1832
2116
|
filesUpdated: updated,
|
|
1833
2117
|
filesUnchanged: unchanged,
|
|
1834
|
-
filesErrored: errored,
|
|
2118
|
+
filesErrored: errored + inventoryErrored,
|
|
1835
2119
|
filesSkipped: skipped.length + dynamicSkipped,
|
|
1836
2120
|
filesMarkedInactive: markedInactive,
|
|
1837
2121
|
durationMs: Date.now() - startTime,
|