@gmickel/gno 1.34.5 → 1.35.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/README.md +22 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/spec/cli.md +43 -0
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +565 -108
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
- package/src/serve/watch-reconciliation-fallback.ts +434 -0
- package/src/serve/watch-reconciliation-shared.ts +348 -0
- package/src/serve/watch-reconciliation.ts +129 -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 +443 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +221 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +125 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +266 -306
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +365 -0
- package/src/serve/watch-snapshot-libc.ts +510 -0
- package/src/serve/watch-snapshot-ops.ts +541 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +300 -0
- package/src/serve/watch-snapshot-types.ts +392 -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/src/ingestion/sync.ts
CHANGED
|
@@ -69,6 +69,19 @@ import {
|
|
|
69
69
|
} from "./frontmatter";
|
|
70
70
|
import { buildLineOffsets } from "./position";
|
|
71
71
|
import { processRecordContainer } from "./record-container";
|
|
72
|
+
import {
|
|
73
|
+
createDirectoryAvailability,
|
|
74
|
+
createSourceContentReader,
|
|
75
|
+
findUnprovenAvailabilityPrefix,
|
|
76
|
+
isSourceAvailabilitySkip,
|
|
77
|
+
isUnprovenAbsenceCode,
|
|
78
|
+
memoizeDirectoryAvailability,
|
|
79
|
+
relPathUnderAnyPrefix,
|
|
80
|
+
resolveSourceAvailability,
|
|
81
|
+
type DirectoryAvailabilityPort,
|
|
82
|
+
type SourceContentReaderPort,
|
|
83
|
+
type SourceReadFailure,
|
|
84
|
+
} from "./source-availability";
|
|
72
85
|
import { getExcludedRanges } from "./strip";
|
|
73
86
|
import { collectionToWalkConfig, DEFAULT_CHUNK_PARAMS } from "./types";
|
|
74
87
|
import { defaultWalker } from "./walker";
|
|
@@ -616,6 +629,31 @@ class Semaphore {
|
|
|
616
629
|
}
|
|
617
630
|
}
|
|
618
631
|
|
|
632
|
+
function summarizePathResults(
|
|
633
|
+
collection: string,
|
|
634
|
+
results: FileSyncResult[],
|
|
635
|
+
markedInactive: number,
|
|
636
|
+
startedAt: number,
|
|
637
|
+
errors: CollectionSyncResult["errors"]
|
|
638
|
+
): CollectionSyncResult {
|
|
639
|
+
return {
|
|
640
|
+
collection,
|
|
641
|
+
filesProcessed: results.length,
|
|
642
|
+
filesAdded: results.filter((result) => result.status === "added").length,
|
|
643
|
+
filesUpdated: results.filter((result) => result.status === "updated")
|
|
644
|
+
.length,
|
|
645
|
+
filesUnchanged: results.filter((result) => result.status === "unchanged")
|
|
646
|
+
.length,
|
|
647
|
+
filesErrored: results.filter((result) => result.status === "error").length,
|
|
648
|
+
filesSkipped: results.filter((result) => result.status === "skipped")
|
|
649
|
+
.length,
|
|
650
|
+
filesMarkedInactive: markedInactive,
|
|
651
|
+
durationMs: Date.now() - startedAt,
|
|
652
|
+
files: results,
|
|
653
|
+
errors,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
619
657
|
/**
|
|
620
658
|
* Sync service implementation.
|
|
621
659
|
*/
|
|
@@ -624,17 +662,32 @@ export class SyncService {
|
|
|
624
662
|
private readonly chunker: ChunkerPort;
|
|
625
663
|
private readonly mimeDetector: MimeDetector;
|
|
626
664
|
private readonly pipeline: ConversionPipeline;
|
|
665
|
+
private readonly sourceReaderFactory: (
|
|
666
|
+
mode: "any" | "local"
|
|
667
|
+
) => SourceContentReaderPort;
|
|
668
|
+
private readonly directoryAvailabilityFactory: (
|
|
669
|
+
mode: "any" | "local"
|
|
670
|
+
) => DirectoryAvailabilityPort;
|
|
627
671
|
|
|
628
672
|
constructor(
|
|
629
673
|
walker?: WalkerPort,
|
|
630
674
|
chunker?: ChunkerPort,
|
|
631
675
|
mimeDetector?: MimeDetector,
|
|
632
|
-
pipeline?: ConversionPipeline
|
|
676
|
+
pipeline?: ConversionPipeline,
|
|
677
|
+
sourceReaderFactory?: (mode: "any" | "local") => SourceContentReaderPort,
|
|
678
|
+
directoryAvailabilityFactory?: (
|
|
679
|
+
mode: "any" | "local"
|
|
680
|
+
) => DirectoryAvailabilityPort
|
|
633
681
|
) {
|
|
634
682
|
this.walker = walker ?? defaultWalker;
|
|
635
683
|
this.chunker = chunker ?? defaultChunker;
|
|
636
684
|
this.mimeDetector = mimeDetector ?? getDefaultMimeDetector();
|
|
637
685
|
this.pipeline = pipeline ?? getDefaultPipeline();
|
|
686
|
+
this.sourceReaderFactory =
|
|
687
|
+
sourceReaderFactory ?? ((mode) => createSourceContentReader(mode));
|
|
688
|
+
this.directoryAvailabilityFactory =
|
|
689
|
+
directoryAvailabilityFactory ??
|
|
690
|
+
((mode) => createDirectoryAvailability(mode));
|
|
638
691
|
}
|
|
639
692
|
|
|
640
693
|
private async selectRecordAdapter(
|
|
@@ -741,6 +794,28 @@ export class SyncService {
|
|
|
741
794
|
const contentTypeRulesFingerprint =
|
|
742
795
|
options.contentTypeRulesFingerprint ??
|
|
743
796
|
fingerprintContentTypeMetadataRules(contentTypeRules);
|
|
797
|
+
|
|
798
|
+
// 2. Local mode performs one guarded content-boundary read. `any` keeps
|
|
799
|
+
// the legacy record-stream and sniff/read paths byte-for-byte unchanged.
|
|
800
|
+
// No availability syscall is added during discovery.
|
|
801
|
+
const availabilityMode = resolveSourceAvailability(collection, options);
|
|
802
|
+
let guardedBytes: Uint8Array | undefined;
|
|
803
|
+
if (availabilityMode === "local") {
|
|
804
|
+
const sourceRead = await this.sourceReaderFactory("local").readAll(
|
|
805
|
+
entry.absPath,
|
|
806
|
+
sourceSize
|
|
807
|
+
);
|
|
808
|
+
if (!sourceRead.ok) {
|
|
809
|
+
return await this.finishSourceAvailabilityFailure(
|
|
810
|
+
collection,
|
|
811
|
+
entry,
|
|
812
|
+
store,
|
|
813
|
+
sourceRead
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
guardedBytes = sourceRead.bytes;
|
|
817
|
+
}
|
|
818
|
+
|
|
744
819
|
if (recordAdapter) {
|
|
745
820
|
return await processRecordContainer({
|
|
746
821
|
adapter: recordAdapter,
|
|
@@ -757,13 +832,16 @@ export class SyncService {
|
|
|
757
832
|
sourceCtime,
|
|
758
833
|
sourceMtime,
|
|
759
834
|
sourceSize,
|
|
835
|
+
sourceBytes: guardedBytes,
|
|
760
836
|
store,
|
|
761
837
|
});
|
|
762
838
|
}
|
|
763
839
|
|
|
764
|
-
const sniffBytes =
|
|
765
|
-
|
|
766
|
-
|
|
840
|
+
const sniffBytes = guardedBytes
|
|
841
|
+
? guardedBytes.subarray(0, Math.min(512, guardedBytes.byteLength))
|
|
842
|
+
: new Uint8Array(
|
|
843
|
+
await Bun.file(entry.absPath).slice(0, 512).arrayBuffer()
|
|
844
|
+
);
|
|
767
845
|
const mime = this.mimeDetector.detect(entry.relPath, sniffBytes);
|
|
768
846
|
|
|
769
847
|
const priorRecordDocuments = mustOk(
|
|
@@ -772,10 +850,9 @@ export class SyncService {
|
|
|
772
850
|
{ collection: collection.name, relPath: entry.relPath }
|
|
773
851
|
);
|
|
774
852
|
|
|
775
|
-
|
|
776
|
-
const bytes = await Bun.file(entry.absPath).bytes();
|
|
853
|
+
const bytes = guardedBytes ?? (await Bun.file(entry.absPath).bytes());
|
|
777
854
|
|
|
778
|
-
// 3. Compute sourceHash
|
|
855
|
+
// 3. Compute sourceHash from source bytes. Local mode cannot reopen.
|
|
779
856
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
780
857
|
hasher.update(bytes);
|
|
781
858
|
const sourceHash = hasher.digest("hex");
|
|
@@ -1127,6 +1204,38 @@ export class SyncService {
|
|
|
1127
1204
|
}
|
|
1128
1205
|
}
|
|
1129
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* Translate guarded-read failures into skip (cloud placeholder/partial) or
|
|
1209
|
+
* fail-closed error outcomes. Never surfaces materialization refusal as a
|
|
1210
|
+
* conversion error.
|
|
1211
|
+
*/
|
|
1212
|
+
private async finishSourceAvailabilityFailure(
|
|
1213
|
+
collection: Collection,
|
|
1214
|
+
entry: WalkEntry,
|
|
1215
|
+
store: StorePort,
|
|
1216
|
+
failure: SourceReadFailure
|
|
1217
|
+
): Promise<FileSyncResult> {
|
|
1218
|
+
const status = isSourceAvailabilitySkip(failure.code) ? "skipped" : "error";
|
|
1219
|
+
await store
|
|
1220
|
+
.recordError({
|
|
1221
|
+
collection: collection.name,
|
|
1222
|
+
relPath: entry.relPath,
|
|
1223
|
+
code: failure.code,
|
|
1224
|
+
message: failure.message,
|
|
1225
|
+
details:
|
|
1226
|
+
failure.errno === undefined || failure.errno === null
|
|
1227
|
+
? undefined
|
|
1228
|
+
: { errno: failure.errno },
|
|
1229
|
+
})
|
|
1230
|
+
.catch(() => undefined);
|
|
1231
|
+
return {
|
|
1232
|
+
relPath: entry.relPath,
|
|
1233
|
+
status,
|
|
1234
|
+
errorCode: failure.code,
|
|
1235
|
+
errorMessage: failure.message,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1130
1239
|
private async readPreviousStructure(
|
|
1131
1240
|
store: StorePort,
|
|
1132
1241
|
existing: DocumentRow | null
|
|
@@ -1161,6 +1270,54 @@ export class SyncService {
|
|
|
1161
1270
|
return result.files ?? [];
|
|
1162
1271
|
}
|
|
1163
1272
|
|
|
1273
|
+
/**
|
|
1274
|
+
* Inactivate proven-absent index sources without requiring disk absence.
|
|
1275
|
+
* Used when classification proves a path is no longer an eligible file
|
|
1276
|
+
* source even if a directory/FIFO/device now occupies the path.
|
|
1277
|
+
* Preserves record-container fan-out and typed-edge projection.
|
|
1278
|
+
*/
|
|
1279
|
+
async inactivateAbsentSources(
|
|
1280
|
+
collection: Collection,
|
|
1281
|
+
store: StorePort,
|
|
1282
|
+
relPaths: string[],
|
|
1283
|
+
options: SyncOptions = {}
|
|
1284
|
+
): Promise<CollectionSyncResult> {
|
|
1285
|
+
const startedAt = Date.now();
|
|
1286
|
+
const syncOptions: SyncOptions = {
|
|
1287
|
+
...options,
|
|
1288
|
+
contentTypeRules: options.contentTypeRules ?? [],
|
|
1289
|
+
contentTypeRulesFingerprint:
|
|
1290
|
+
options.contentTypeRulesFingerprint ??
|
|
1291
|
+
fingerprintContentTypeMetadataRules(options.contentTypeRules ?? []),
|
|
1292
|
+
};
|
|
1293
|
+
const results: FileSyncResult[] = [];
|
|
1294
|
+
const projectionSourceIds = new Set<number>();
|
|
1295
|
+
let markedInactive = 0;
|
|
1296
|
+
|
|
1297
|
+
for (const relPath of relPaths) {
|
|
1298
|
+
const outcome = await this.inactivateOneAbsentSource(
|
|
1299
|
+
collection,
|
|
1300
|
+
store,
|
|
1301
|
+
relPath,
|
|
1302
|
+
projectionSourceIds
|
|
1303
|
+
);
|
|
1304
|
+
results.push(outcome.result);
|
|
1305
|
+
markedInactive += outcome.markedInactive;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const errors =
|
|
1309
|
+
syncOptions.projectTypedEdges === false
|
|
1310
|
+
? []
|
|
1311
|
+
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1312
|
+
return summarizePathResults(
|
|
1313
|
+
collection.name,
|
|
1314
|
+
results,
|
|
1315
|
+
markedInactive,
|
|
1316
|
+
startedAt,
|
|
1317
|
+
errors
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1164
1321
|
async syncPaths(
|
|
1165
1322
|
collection: Collection,
|
|
1166
1323
|
store: StorePort,
|
|
@@ -1178,6 +1335,10 @@ export class SyncService {
|
|
|
1178
1335
|
const results: FileSyncResult[] = [];
|
|
1179
1336
|
const projectionSourceIds = new Set<number>();
|
|
1180
1337
|
let markedInactive = 0;
|
|
1338
|
+
const availabilityMode = resolveSourceAvailability(collection, syncOptions);
|
|
1339
|
+
const directoryAvailability = memoizeDirectoryAvailability(
|
|
1340
|
+
this.directoryAvailabilityFactory(availabilityMode)
|
|
1341
|
+
);
|
|
1181
1342
|
|
|
1182
1343
|
for (const relPath of relPaths) {
|
|
1183
1344
|
const recordDocumentsResult = await store.listRecordDocuments(
|
|
@@ -1195,20 +1356,79 @@ export class SyncService {
|
|
|
1195
1356
|
}
|
|
1196
1357
|
const recordDocuments = recordDocumentsResult.value;
|
|
1197
1358
|
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1198
|
-
|
|
1359
|
+
if (!existingResult.ok) {
|
|
1360
|
+
results.push({
|
|
1361
|
+
relPath,
|
|
1362
|
+
status: "error",
|
|
1363
|
+
errorCode: existingResult.error.code,
|
|
1364
|
+
errorMessage: existingResult.error.message,
|
|
1365
|
+
});
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const existingDoc = existingResult.value;
|
|
1199
1369
|
if (existingDoc) {
|
|
1200
|
-
await this.collectProjectionSourceIds(
|
|
1370
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1201
1371
|
store,
|
|
1202
1372
|
existingDoc.id,
|
|
1203
1373
|
projectionSourceIds
|
|
1204
1374
|
);
|
|
1375
|
+
if (collectError) {
|
|
1376
|
+
results.push({
|
|
1377
|
+
relPath,
|
|
1378
|
+
status: "error",
|
|
1379
|
+
errorCode: collectError.code,
|
|
1380
|
+
errorMessage: collectError.message,
|
|
1381
|
+
});
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1205
1384
|
}
|
|
1385
|
+
let recordCollectFailed: FileSyncResult | null = null;
|
|
1206
1386
|
for (const recordDocument of recordDocuments) {
|
|
1207
|
-
await this.collectProjectionSourceIds(
|
|
1387
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1208
1388
|
store,
|
|
1209
1389
|
recordDocument.id,
|
|
1210
1390
|
projectionSourceIds
|
|
1211
1391
|
);
|
|
1392
|
+
if (collectError) {
|
|
1393
|
+
recordCollectFailed = {
|
|
1394
|
+
relPath,
|
|
1395
|
+
status: "error",
|
|
1396
|
+
errorCode: collectError.code,
|
|
1397
|
+
errorMessage: collectError.message,
|
|
1398
|
+
};
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (recordCollectFailed) {
|
|
1403
|
+
results.push(recordCollectFailed);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Direct syncPaths must not bypass directory-availability guards.
|
|
1408
|
+
const unprovenPrefix = await findUnprovenAvailabilityPrefix(
|
|
1409
|
+
collection.path,
|
|
1410
|
+
relPath,
|
|
1411
|
+
directoryAvailability
|
|
1412
|
+
);
|
|
1413
|
+
if (unprovenPrefix) {
|
|
1414
|
+
const status = isSourceAvailabilitySkip(unprovenPrefix.code)
|
|
1415
|
+
? "skipped"
|
|
1416
|
+
: "error";
|
|
1417
|
+
await store
|
|
1418
|
+
.recordError({
|
|
1419
|
+
collection: collection.name,
|
|
1420
|
+
relPath: unprovenPrefix.relPath || relPath,
|
|
1421
|
+
code: unprovenPrefix.code,
|
|
1422
|
+
message: unprovenPrefix.message,
|
|
1423
|
+
})
|
|
1424
|
+
.catch(() => undefined);
|
|
1425
|
+
results.push({
|
|
1426
|
+
relPath,
|
|
1427
|
+
status,
|
|
1428
|
+
errorCode: unprovenPrefix.code,
|
|
1429
|
+
errorMessage: unprovenPrefix.message,
|
|
1430
|
+
});
|
|
1431
|
+
continue;
|
|
1212
1432
|
}
|
|
1213
1433
|
|
|
1214
1434
|
const absPath = join(collection.path, relPath);
|
|
@@ -1230,39 +1450,15 @@ export class SyncService {
|
|
|
1230
1450
|
});
|
|
1231
1451
|
continue;
|
|
1232
1452
|
}
|
|
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({
|
|
1453
|
+
const inactive = await this.inactivateOneAbsentSource(
|
|
1454
|
+
collection,
|
|
1455
|
+
store,
|
|
1262
1456
|
relPath,
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1457
|
+
projectionSourceIds,
|
|
1458
|
+
{ existingDoc, recordDocuments }
|
|
1459
|
+
);
|
|
1460
|
+
results.push(inactive.result);
|
|
1461
|
+
markedInactive += inactive.markedInactive;
|
|
1266
1462
|
continue;
|
|
1267
1463
|
}
|
|
1268
1464
|
|
|
@@ -1322,28 +1518,67 @@ export class SyncService {
|
|
|
1322
1518
|
syncOptions
|
|
1323
1519
|
);
|
|
1324
1520
|
results.push(result);
|
|
1521
|
+
if (result.status === "error" || result.status === "skipped") {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
// Post-write projection identity must succeed so typed-edge cleanup can run.
|
|
1325
1525
|
const currentResult = await store.getDocument(collection.name, relPath);
|
|
1326
|
-
|
|
1526
|
+
if (!currentResult.ok) {
|
|
1527
|
+
results[results.length - 1] = {
|
|
1528
|
+
relPath,
|
|
1529
|
+
status: "error",
|
|
1530
|
+
errorCode: currentResult.error.code,
|
|
1531
|
+
errorMessage: currentResult.error.message,
|
|
1532
|
+
};
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const currentDoc = currentResult.value;
|
|
1327
1536
|
if (currentDoc?.active) {
|
|
1328
|
-
await this.collectProjectionSourceIds(
|
|
1537
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1329
1538
|
store,
|
|
1330
1539
|
currentDoc.id,
|
|
1331
1540
|
projectionSourceIds
|
|
1332
1541
|
);
|
|
1542
|
+
if (collectError) {
|
|
1543
|
+
results[results.length - 1] = {
|
|
1544
|
+
relPath,
|
|
1545
|
+
status: "error",
|
|
1546
|
+
errorCode: collectError.code,
|
|
1547
|
+
errorMessage: collectError.message,
|
|
1548
|
+
};
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1333
1551
|
}
|
|
1334
1552
|
const currentDocuments = await store.listRecordDocuments(
|
|
1335
1553
|
collection.name,
|
|
1336
1554
|
relPath
|
|
1337
1555
|
);
|
|
1338
|
-
if (currentDocuments.ok) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1556
|
+
if (!currentDocuments.ok) {
|
|
1557
|
+
results[results.length - 1] = {
|
|
1558
|
+
relPath,
|
|
1559
|
+
status: "error",
|
|
1560
|
+
errorCode: currentDocuments.error.code,
|
|
1561
|
+
errorMessage: currentDocuments.error.message,
|
|
1562
|
+
};
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
for (const recordDocument of currentDocuments.value) {
|
|
1566
|
+
if (!recordDocument.active) {
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1570
|
+
store,
|
|
1571
|
+
recordDocument.id,
|
|
1572
|
+
projectionSourceIds
|
|
1573
|
+
);
|
|
1574
|
+
if (collectError) {
|
|
1575
|
+
results[results.length - 1] = {
|
|
1576
|
+
relPath,
|
|
1577
|
+
status: "error",
|
|
1578
|
+
errorCode: collectError.code,
|
|
1579
|
+
errorMessage: collectError.message,
|
|
1580
|
+
};
|
|
1581
|
+
break;
|
|
1347
1582
|
}
|
|
1348
1583
|
}
|
|
1349
1584
|
}
|
|
@@ -1352,55 +1587,184 @@ export class SyncService {
|
|
|
1352
1587
|
syncOptions.projectTypedEdges === false
|
|
1353
1588
|
? []
|
|
1354
1589
|
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1590
|
+
return summarizePathResults(
|
|
1591
|
+
collection.name,
|
|
1592
|
+
results,
|
|
1593
|
+
markedInactive,
|
|
1594
|
+
startedAt,
|
|
1595
|
+
errors
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Soft-delete active docs/record-container children for a proven-absent source.
|
|
1601
|
+
* Does not inspect disk — callers prove absence of an indexable file source.
|
|
1602
|
+
* Store lookup / backlink / mark failures surface as per-file errors (never
|
|
1603
|
+
* silent null/skip) so callers cannot advance snapshot authority.
|
|
1604
|
+
*/
|
|
1605
|
+
private async inactivateOneAbsentSource(
|
|
1606
|
+
collection: Collection,
|
|
1607
|
+
store: StorePort,
|
|
1608
|
+
relPath: string,
|
|
1609
|
+
projectionSourceIds: Set<number>,
|
|
1610
|
+
preloaded?: {
|
|
1611
|
+
existingDoc: { id: number; active: boolean; docid?: string } | null;
|
|
1612
|
+
recordDocuments: Array<{ id: number; active: boolean; relPath: string }>;
|
|
1613
|
+
}
|
|
1614
|
+
): Promise<{ result: FileSyncResult; markedInactive: number }> {
|
|
1615
|
+
let existingDoc = preloaded?.existingDoc ?? null;
|
|
1616
|
+
let recordDocuments = preloaded?.recordDocuments;
|
|
1368
1617
|
|
|
1618
|
+
if (!preloaded) {
|
|
1619
|
+
const recordDocumentsResult = await store.listRecordDocuments(
|
|
1620
|
+
collection.name,
|
|
1621
|
+
relPath
|
|
1622
|
+
);
|
|
1623
|
+
if (!recordDocumentsResult.ok) {
|
|
1624
|
+
return {
|
|
1625
|
+
result: {
|
|
1626
|
+
relPath,
|
|
1627
|
+
status: "error",
|
|
1628
|
+
errorCode: recordDocumentsResult.error.code,
|
|
1629
|
+
errorMessage: recordDocumentsResult.error.message,
|
|
1630
|
+
},
|
|
1631
|
+
markedInactive: 0,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
recordDocuments = recordDocumentsResult.value;
|
|
1635
|
+
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1636
|
+
if (!existingResult.ok) {
|
|
1637
|
+
// ok:false is a hard per-file error — never treat as missing/skip.
|
|
1638
|
+
return {
|
|
1639
|
+
result: {
|
|
1640
|
+
relPath,
|
|
1641
|
+
status: "error",
|
|
1642
|
+
errorCode: existingResult.error.code,
|
|
1643
|
+
errorMessage: existingResult.error.message,
|
|
1644
|
+
},
|
|
1645
|
+
markedInactive: 0,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
existingDoc = existingResult.value;
|
|
1649
|
+
if (existingDoc) {
|
|
1650
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1651
|
+
store,
|
|
1652
|
+
existingDoc.id,
|
|
1653
|
+
projectionSourceIds
|
|
1654
|
+
);
|
|
1655
|
+
if (collectError) {
|
|
1656
|
+
return {
|
|
1657
|
+
result: {
|
|
1658
|
+
relPath,
|
|
1659
|
+
status: "error",
|
|
1660
|
+
errorCode: collectError.code,
|
|
1661
|
+
errorMessage: collectError.message,
|
|
1662
|
+
},
|
|
1663
|
+
markedInactive: 0,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
for (const recordDocument of recordDocuments) {
|
|
1668
|
+
const collectError = await this.collectProjectionSourceIds(
|
|
1669
|
+
store,
|
|
1670
|
+
recordDocument.id,
|
|
1671
|
+
projectionSourceIds
|
|
1672
|
+
);
|
|
1673
|
+
if (collectError) {
|
|
1674
|
+
return {
|
|
1675
|
+
result: {
|
|
1676
|
+
relPath,
|
|
1677
|
+
status: "error",
|
|
1678
|
+
errorCode: collectError.code,
|
|
1679
|
+
errorMessage: collectError.message,
|
|
1680
|
+
},
|
|
1681
|
+
markedInactive: 0,
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const docs = recordDocuments ?? [];
|
|
1688
|
+
// Physical source path may own multiple logical docs (record containers).
|
|
1689
|
+
// Deduplicate while preserving the primary source path first.
|
|
1690
|
+
const activePathSet = new Set<string>();
|
|
1691
|
+
if (existingDoc?.active) {
|
|
1692
|
+
activePathSet.add(relPath);
|
|
1693
|
+
}
|
|
1694
|
+
for (const document of docs) {
|
|
1695
|
+
if (document.active) {
|
|
1696
|
+
activePathSet.add(document.relPath);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const activePaths = [...activePathSet];
|
|
1700
|
+
if (activePaths.length > 0) {
|
|
1701
|
+
const inactiveResult = await store.markInactive(
|
|
1702
|
+
collection.name,
|
|
1703
|
+
activePaths
|
|
1704
|
+
);
|
|
1705
|
+
if (!inactiveResult.ok) {
|
|
1706
|
+
return {
|
|
1707
|
+
result: {
|
|
1708
|
+
relPath,
|
|
1709
|
+
status: "error",
|
|
1710
|
+
errorCode: inactiveResult.error.code,
|
|
1711
|
+
errorMessage: inactiveResult.error.message,
|
|
1712
|
+
},
|
|
1713
|
+
markedInactive: 0,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
result: {
|
|
1718
|
+
relPath,
|
|
1719
|
+
status: "updated",
|
|
1720
|
+
docid: existingDoc?.docid,
|
|
1721
|
+
},
|
|
1722
|
+
markedInactive: inactiveResult.value,
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1369
1725
|
return {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
filesSkipped: skipped,
|
|
1377
|
-
filesMarkedInactive: markedInactive,
|
|
1378
|
-
durationMs: Date.now() - startedAt,
|
|
1379
|
-
files: results,
|
|
1380
|
-
errors,
|
|
1726
|
+
result: {
|
|
1727
|
+
relPath,
|
|
1728
|
+
status: existingDoc ? "unchanged" : "skipped",
|
|
1729
|
+
docid: existingDoc?.docid,
|
|
1730
|
+
},
|
|
1731
|
+
markedInactive: 0,
|
|
1381
1732
|
};
|
|
1382
1733
|
}
|
|
1383
1734
|
|
|
1735
|
+
/**
|
|
1736
|
+
* Collect source doc ids that need typed-edge re-projection after a change.
|
|
1737
|
+
* Backlink lookup failures are fatal for the caller (not silently ignored).
|
|
1738
|
+
*/
|
|
1384
1739
|
private async collectProjectionSourceIds(
|
|
1385
1740
|
store: StorePort,
|
|
1386
1741
|
documentId: number,
|
|
1387
1742
|
sourceIds: Set<number>
|
|
1388
|
-
): Promise<
|
|
1743
|
+
): Promise<{ code: string; message: string } | null> {
|
|
1389
1744
|
sourceIds.add(documentId);
|
|
1390
1745
|
const [linkBacklinks, edgeBacklinks] = await Promise.all([
|
|
1391
1746
|
store.getBacklinksForDoc(documentId),
|
|
1392
1747
|
store.getEdgeBacklinksForDoc(documentId),
|
|
1393
1748
|
]);
|
|
1394
|
-
if (linkBacklinks.ok) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1749
|
+
if (!linkBacklinks.ok) {
|
|
1750
|
+
return {
|
|
1751
|
+
code: linkBacklinks.error.code,
|
|
1752
|
+
message: linkBacklinks.error.message,
|
|
1753
|
+
};
|
|
1398
1754
|
}
|
|
1399
|
-
if (edgeBacklinks.ok) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1755
|
+
if (!edgeBacklinks.ok) {
|
|
1756
|
+
return {
|
|
1757
|
+
code: edgeBacklinks.error.code,
|
|
1758
|
+
message: edgeBacklinks.error.message,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
for (const backlink of linkBacklinks.value) {
|
|
1762
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1403
1763
|
}
|
|
1764
|
+
for (const backlink of edgeBacklinks.value) {
|
|
1765
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1766
|
+
}
|
|
1767
|
+
return null;
|
|
1404
1768
|
}
|
|
1405
1769
|
|
|
1406
1770
|
private async projectTypedEdges(
|
|
@@ -1441,7 +1805,19 @@ export class SyncService {
|
|
|
1441
1805
|
if (selectedSourceIds) {
|
|
1442
1806
|
for (const documentId of selectedSourceIds) {
|
|
1443
1807
|
if (!activeIds.has(documentId)) {
|
|
1444
|
-
|
|
1808
|
+
// Clear typed edges for inactivated projection sources.
|
|
1809
|
+
const clearResult = await store.setDocEdges(
|
|
1810
|
+
documentId,
|
|
1811
|
+
[],
|
|
1812
|
+
"frontmatter-relation"
|
|
1813
|
+
);
|
|
1814
|
+
if (!clearResult.ok) {
|
|
1815
|
+
errors.push({
|
|
1816
|
+
relPath: `(doc:${documentId})`,
|
|
1817
|
+
code: clearResult.error.code,
|
|
1818
|
+
message: clearResult.error.message,
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1445
1821
|
}
|
|
1446
1822
|
}
|
|
1447
1823
|
}
|
|
@@ -1620,23 +1996,44 @@ export class SyncService {
|
|
|
1620
1996
|
await gitPull(collection.path);
|
|
1621
1997
|
}
|
|
1622
1998
|
|
|
1623
|
-
// 2. Walk collection
|
|
1999
|
+
// 2. Walk collection (local mode refuses dataless/unproven directory descent)
|
|
1624
2000
|
const maxBytes = options.limits?.maxBytes ?? DEFAULT_LIMITS.maxBytes;
|
|
1625
|
-
const
|
|
2001
|
+
const availabilityMode = resolveSourceAvailability(collection, syncOptions);
|
|
2002
|
+
const directoryAvailability = memoizeDirectoryAvailability(
|
|
2003
|
+
this.directoryAvailabilityFactory(availabilityMode)
|
|
2004
|
+
);
|
|
2005
|
+
const walkConfig = {
|
|
2006
|
+
...collectionToWalkConfig(collection, maxBytes, syncOptions),
|
|
2007
|
+
sourceAvailability: availabilityMode,
|
|
2008
|
+
directoryAvailability,
|
|
2009
|
+
};
|
|
1626
2010
|
const { entries, skipped } = await this.walker.walk(walkConfig);
|
|
1627
2011
|
|
|
1628
2012
|
// Track seen paths for marking inactive
|
|
1629
2013
|
// Only include TOO_LARGE files (they exist but are unprocessable)
|
|
1630
2014
|
// EXCLUDED files should NOT be in seenPaths - if config changes to exclude
|
|
1631
2015
|
// a previously-included file, that doc SHOULD be marked inactive
|
|
2016
|
+
// Unproven prefixes (dataless / availability-unknown dirs) preserve
|
|
2017
|
+
// previously indexed descendants — never treat them as proven deletions.
|
|
1632
2018
|
const seenPaths = new Set<string>();
|
|
2019
|
+
const unprovenPrefixes: string[] = [];
|
|
1633
2020
|
for (const skip of skipped) {
|
|
1634
2021
|
if (skip.reason === "TOO_LARGE") {
|
|
1635
2022
|
seenPaths.add(skip.relPath);
|
|
1636
2023
|
}
|
|
2024
|
+
if (skip.unprovenPrefix || isUnprovenAbsenceCode(skip.reason)) {
|
|
2025
|
+
unprovenPrefixes.push(skip.relPath);
|
|
2026
|
+
}
|
|
1637
2027
|
}
|
|
1638
2028
|
|
|
1639
|
-
|
|
2029
|
+
let added = 0;
|
|
2030
|
+
let updated = 0;
|
|
2031
|
+
let unchanged = 0;
|
|
2032
|
+
let errored = 0;
|
|
2033
|
+
let dynamicSkipped = 0;
|
|
2034
|
+
const fileResults: FileSyncResult[] = [];
|
|
2035
|
+
|
|
2036
|
+
// 3. Record TOO_LARGE / availability-prefix outcomes for receipts
|
|
1640
2037
|
for (const skip of skipped) {
|
|
1641
2038
|
if (skip.reason === "TOO_LARGE") {
|
|
1642
2039
|
const recordResult = await store.recordError({
|
|
@@ -1658,6 +2055,35 @@ export class SyncService {
|
|
|
1658
2055
|
code: "TOO_LARGE",
|
|
1659
2056
|
message: `File size ${skip.size} exceeds limit ${maxBytes}`,
|
|
1660
2057
|
});
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
if (skip.unprovenPrefix || isUnprovenAbsenceCode(skip.reason)) {
|
|
2061
|
+
const message =
|
|
2062
|
+
skip.message ??
|
|
2063
|
+
`Directory availability refused descent (${skip.reason})`;
|
|
2064
|
+
await store
|
|
2065
|
+
.recordError({
|
|
2066
|
+
collection: collection.name,
|
|
2067
|
+
relPath: skip.relPath,
|
|
2068
|
+
code: skip.reason,
|
|
2069
|
+
message,
|
|
2070
|
+
})
|
|
2071
|
+
.catch(() => undefined);
|
|
2072
|
+
errors.push({
|
|
2073
|
+
relPath: skip.relPath,
|
|
2074
|
+
code: skip.reason,
|
|
2075
|
+
message,
|
|
2076
|
+
});
|
|
2077
|
+
const isSkip = isSourceAvailabilitySkip(skip.reason);
|
|
2078
|
+
fileResults.push({
|
|
2079
|
+
relPath: skip.relPath === "" ? "." : skip.relPath,
|
|
2080
|
+
status: isSkip ? "skipped" : "error",
|
|
2081
|
+
errorCode: skip.reason,
|
|
2082
|
+
errorMessage: message,
|
|
2083
|
+
});
|
|
2084
|
+
if (!isSkip) {
|
|
2085
|
+
errored += 1;
|
|
2086
|
+
}
|
|
1661
2087
|
}
|
|
1662
2088
|
}
|
|
1663
2089
|
|
|
@@ -1666,14 +2092,6 @@ export class SyncService {
|
|
|
1666
2092
|
1,
|
|
1667
2093
|
Math.min(MAX_CONCURRENCY, options.concurrency ?? DEFAULT_CONCURRENCY)
|
|
1668
2094
|
);
|
|
1669
|
-
|
|
1670
|
-
let added = 0;
|
|
1671
|
-
let updated = 0;
|
|
1672
|
-
let unchanged = 0;
|
|
1673
|
-
let errored = 0;
|
|
1674
|
-
let dynamicSkipped = 0;
|
|
1675
|
-
const fileResults: FileSyncResult[] = [];
|
|
1676
|
-
|
|
1677
2095
|
if (concurrency === 1) {
|
|
1678
2096
|
// Sequential processing with batched transactions (Windows perf)
|
|
1679
2097
|
for (let i = 0; i < entries.length; i += TX_BATCH_SIZE) {
|
|
@@ -1798,16 +2216,33 @@ export class SyncService {
|
|
|
1798
2216
|
}
|
|
1799
2217
|
}
|
|
1800
2218
|
|
|
1801
|
-
// 5. Mark missing files as inactive
|
|
2219
|
+
// 5. Mark missing files as inactive (inventory failures are hard errors).
|
|
1802
2220
|
let markedInactive = 0;
|
|
2221
|
+
let inventoryErrored = 0;
|
|
1803
2222
|
const existingDocsResult = await store.listDocuments(collection.name);
|
|
1804
|
-
if (existingDocsResult.ok) {
|
|
2223
|
+
if (!existingDocsResult.ok) {
|
|
2224
|
+
inventoryErrored += 1;
|
|
2225
|
+
errors.push({
|
|
2226
|
+
relPath: "",
|
|
2227
|
+
code: existingDocsResult.error.code,
|
|
2228
|
+
message: existingDocsResult.error.message,
|
|
2229
|
+
});
|
|
2230
|
+
} else {
|
|
1805
2231
|
const missingPaths = existingDocsResult.value
|
|
1806
|
-
.filter(
|
|
1807
|
-
(document)
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2232
|
+
.filter((document) => {
|
|
2233
|
+
if (!document.active) {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
const sourcePath = document.recordSourcePath ?? document.relPath;
|
|
2237
|
+
if (seenPaths.has(sourcePath)) {
|
|
2238
|
+
return false;
|
|
2239
|
+
}
|
|
2240
|
+
// Absence under an unenumerated/unproven prefix is not proven deletion.
|
|
2241
|
+
if (relPathUnderAnyPrefix(sourcePath, unprovenPrefixes)) {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
return true;
|
|
2245
|
+
})
|
|
1811
2246
|
.map((d) => d.relPath);
|
|
1812
2247
|
|
|
1813
2248
|
if (missingPaths.length > 0) {
|
|
@@ -1817,6 +2252,21 @@ export class SyncService {
|
|
|
1817
2252
|
);
|
|
1818
2253
|
if (markResult.ok) {
|
|
1819
2254
|
markedInactive = markResult.value;
|
|
2255
|
+
} else {
|
|
2256
|
+
inventoryErrored += missingPaths.length;
|
|
2257
|
+
errors.push({
|
|
2258
|
+
relPath: missingPaths[0] ?? "",
|
|
2259
|
+
code: markResult.error.code,
|
|
2260
|
+
message: markResult.error.message,
|
|
2261
|
+
});
|
|
2262
|
+
for (const missingPath of missingPaths) {
|
|
2263
|
+
fileResults.push({
|
|
2264
|
+
relPath: missingPath,
|
|
2265
|
+
status: "error",
|
|
2266
|
+
errorCode: markResult.error.code,
|
|
2267
|
+
errorMessage: markResult.error.message,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
1820
2270
|
}
|
|
1821
2271
|
}
|
|
1822
2272
|
}
|
|
@@ -1825,14 +2275,21 @@ export class SyncService {
|
|
|
1825
2275
|
errors.push(...(await this.projectTypedEdges(store, syncOptions)));
|
|
1826
2276
|
}
|
|
1827
2277
|
|
|
2278
|
+
const walkerSkippedCount = skipped.filter(
|
|
2279
|
+
(skip) =>
|
|
2280
|
+
skip.reason === "TOO_LARGE" ||
|
|
2281
|
+
skip.reason === "EXCLUDED" ||
|
|
2282
|
+
isSourceAvailabilitySkip(skip.reason)
|
|
2283
|
+
).length;
|
|
2284
|
+
|
|
1828
2285
|
return {
|
|
1829
2286
|
collection: collection.name,
|
|
1830
|
-
filesProcessed: entries.length,
|
|
2287
|
+
filesProcessed: entries.length + inventoryErrored,
|
|
1831
2288
|
filesAdded: added,
|
|
1832
2289
|
filesUpdated: updated,
|
|
1833
2290
|
filesUnchanged: unchanged,
|
|
1834
|
-
filesErrored: errored,
|
|
1835
|
-
filesSkipped:
|
|
2291
|
+
filesErrored: errored + inventoryErrored,
|
|
2292
|
+
filesSkipped: walkerSkippedCount + dynamicSkipped,
|
|
1836
2293
|
filesMarkedInactive: markedInactive,
|
|
1837
2294
|
durationMs: Date.now() - startTime,
|
|
1838
2295
|
files: fileResults,
|