@amalgm/shell 0.1.41 → 0.1.42
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/PURPOSE.md +24 -0
- package/dist/detection-host.d.ts +48 -0
- package/dist/detection-host.js +122 -0
- package/dist/detection-host.js.map +1 -0
- package/dist/files-register-host.d.ts +8 -0
- package/dist/files-register-host.js +15 -0
- package/dist/files-register-host.js.map +1 -1
- package/dist/user-ground-host.d.ts +3 -2
- package/dist/user-ground-host.js +440 -107
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -8,7 +8,8 @@ import Database from "better-sqlite3";
|
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
|
|
9
9
|
import { ContentCacheDownload, captureContentFile, hashContentFile, } from "./content-cache-host.js";
|
|
10
10
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
11
|
-
import {
|
|
11
|
+
import { planGroundDetection, reconcileGroundUUIDs, } from "./detection-host.js";
|
|
12
|
+
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceBinding, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, selectKnownRegistrationId, workspaceBindingDir, } from "./files-register-host.js";
|
|
12
13
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
13
14
|
import { inspectGitRegistration, } from "./git-registration-host.js";
|
|
14
15
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
@@ -21,6 +22,12 @@ const PORTABLE_FIELDS = [
|
|
|
21
22
|
const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
|
|
22
23
|
const FILE_CONTENT_DOWNLOAD_CONCURRENCY = 4;
|
|
23
24
|
const FILE_BATCH_DOWNLOAD_CONCURRENCY = 2;
|
|
25
|
+
const GROUND_ROW_COLUMNS = [
|
|
26
|
+
"uuid", "resource_id", "root_uuid", "type", "parent_uuid", "name", "status", "version",
|
|
27
|
+
"payload_version", "transport_version", "relative_path", "absolute_path", "device_number",
|
|
28
|
+
"inode", "byte_size", "modified_time_ms", "changed_time_ms", "filesystem_mode",
|
|
29
|
+
"content_verified_at_ms",
|
|
30
|
+
].join(", ");
|
|
24
31
|
/** The production Files port injected into Core. Portable identity remains in
|
|
25
32
|
* Live; this class owns only Node filesystem, SQLite, Watch, and wire effects. */
|
|
26
33
|
export class UserGroundHost {
|
|
@@ -290,21 +297,21 @@ export class UserGroundHost {
|
|
|
290
297
|
watch,
|
|
291
298
|
};
|
|
292
299
|
}
|
|
293
|
-
/**
|
|
294
|
-
*
|
|
300
|
+
/** Prove one finite observation boundary in SQLite, immutable content
|
|
301
|
+
* storage, and the official cloud head. Rings received during that proof
|
|
302
|
+
* remain pending and already own their next debounced Detect pass. */
|
|
295
303
|
async flush() {
|
|
296
304
|
if (!this.converged || !this.activeIdentity || !this.cloudState)
|
|
297
305
|
return;
|
|
298
306
|
const identity = this.activeIdentity;
|
|
299
307
|
this.watchHost.suspectAll();
|
|
300
|
-
this.
|
|
308
|
+
// The complete suspicion above contains everything known at this cutoff.
|
|
309
|
+
// A native callback after this assignment sets watchDirty again and must
|
|
310
|
+
// remain a later generation; flush never waits for global Watch silence.
|
|
311
|
+
this.watchDirty = false;
|
|
301
312
|
await this.syncNow();
|
|
302
313
|
if (!this.closing)
|
|
303
314
|
this.ensureWatchers(identity);
|
|
304
|
-
await this.flushObservedChanges();
|
|
305
|
-
if (!this.closing)
|
|
306
|
-
this.ensureWatchers(identity);
|
|
307
|
-
await this.flushObservedChanges();
|
|
308
315
|
}
|
|
309
316
|
async syncNow() {
|
|
310
317
|
if (!this.activeIdentity)
|
|
@@ -317,20 +324,14 @@ export class UserGroundHost {
|
|
|
317
324
|
await this.syncing;
|
|
318
325
|
}
|
|
319
326
|
async flushObservedChanges() {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
if (dirty) {
|
|
325
|
-
await this.syncNow();
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (this.syncing) {
|
|
329
|
-
await this.syncing;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
327
|
+
const dirty = this.watchDirty || this.watchHost.hasPending;
|
|
328
|
+
this.watchDirty = false;
|
|
329
|
+
if (dirty) {
|
|
330
|
+
await this.syncNow();
|
|
332
331
|
return;
|
|
333
332
|
}
|
|
333
|
+
if (this.syncing)
|
|
334
|
+
await this.syncing;
|
|
334
335
|
}
|
|
335
336
|
async activateRuntimeTunnel(gatewayPort, runtimeToken) {
|
|
336
337
|
// Files convergence and Watch/Detect own ground currency. Advertising an
|
|
@@ -709,37 +710,62 @@ export class UserGroundHost {
|
|
|
709
710
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
710
711
|
const observations = this.watchHost.observations();
|
|
711
712
|
let localRecords;
|
|
713
|
+
let detection;
|
|
714
|
+
let acceptedNotebookRows;
|
|
715
|
+
let notebookRemovals;
|
|
712
716
|
try {
|
|
713
|
-
|
|
717
|
+
const scanned = await scanAndRegister({
|
|
714
718
|
identity,
|
|
715
719
|
userRoot: this.userRoot(identity),
|
|
716
720
|
database: this.databasePath(identity),
|
|
717
721
|
cacheDir: this.cacheDir(identity),
|
|
718
722
|
onScan: this.options.onDetectScan,
|
|
719
|
-
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion
|
|
723
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
|
|
720
724
|
});
|
|
725
|
+
localRecords = [...scanned.records];
|
|
726
|
+
detection = scanned.detection;
|
|
727
|
+
acceptedNotebookRows = scanned.acceptedNotebookRows;
|
|
728
|
+
notebookRemovals = scanned.notebookRemovals;
|
|
721
729
|
}
|
|
722
730
|
catch (error) {
|
|
723
731
|
throw pipelineStageError("entity registration", error);
|
|
724
732
|
}
|
|
733
|
+
const retry = detection.find((plan) => plan.kind === "retry");
|
|
734
|
+
if (retry?.kind === "retry") {
|
|
735
|
+
throw pipelineStageError("detection", new Error(retry.reasons.join("; ")));
|
|
736
|
+
}
|
|
737
|
+
const detectedRecords = detection.flatMap((plan) => plan.kind === "ready"
|
|
738
|
+
? plan.proposals.map((proposal) => ({ mutationId: randomUUID(), proposal }))
|
|
739
|
+
: []);
|
|
740
|
+
// The notebook is durable identity evidence, not evidence scoped to only
|
|
741
|
+
// this Watch generation. A later no-op flush must not erase ground that an
|
|
742
|
+
// earlier observation proved missing without lifecycle authority.
|
|
743
|
+
localRecords = recordsRetainingMissingEvidence(localRecords, readDetectionNotebook(this.databasePath(identity))
|
|
744
|
+
.filter((row) => !notebookRemovals.includes(row.uuid))
|
|
745
|
+
.map(portableRecord));
|
|
725
746
|
const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
|
|
726
747
|
const checksum = sha256Hex(stableJson(snapshot));
|
|
727
748
|
if (checksum === state.checksum) {
|
|
749
|
+
commitDetectedState(this.databasePath(identity), {
|
|
750
|
+
acceptedNotebookRows,
|
|
751
|
+
notebookRemovals,
|
|
752
|
+
});
|
|
728
753
|
this.watchHost.settle(observations);
|
|
729
754
|
return;
|
|
730
755
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
756
|
+
commitDetectedState(this.databasePath(identity), {
|
|
757
|
+
acceptedNotebookRows,
|
|
758
|
+
notebookRemovals,
|
|
759
|
+
outbox: {
|
|
760
|
+
mutationId: randomUUID(),
|
|
761
|
+
resourceId: state.resourceId,
|
|
762
|
+
authorityEpoch: state.authorityEpoch,
|
|
763
|
+
baseVersion: state.headVersion,
|
|
764
|
+
snapshotChecksum: checksum,
|
|
765
|
+
snapshotJson: stableJson(snapshot),
|
|
766
|
+
detectedRecordsJson: stableJson(detectedRecords),
|
|
767
|
+
},
|
|
768
|
+
});
|
|
743
769
|
await this.drainOutbox(identity);
|
|
744
770
|
this.watchHost.settle(observations);
|
|
745
771
|
}
|
|
@@ -937,6 +963,27 @@ function initializeDatabase(file) {
|
|
|
937
963
|
content_verified_at_ms REAL,
|
|
938
964
|
UNIQUE(resource_id, root_uuid, relative_path)
|
|
939
965
|
);
|
|
966
|
+
CREATE TABLE IF NOT EXISTS detection_notebook (
|
|
967
|
+
uuid TEXT PRIMARY KEY,
|
|
968
|
+
resource_id TEXT NOT NULL,
|
|
969
|
+
root_uuid TEXT NOT NULL,
|
|
970
|
+
type TEXT NOT NULL,
|
|
971
|
+
parent_uuid TEXT,
|
|
972
|
+
name TEXT NOT NULL,
|
|
973
|
+
status TEXT NOT NULL,
|
|
974
|
+
version TEXT NOT NULL,
|
|
975
|
+
payload_version TEXT,
|
|
976
|
+
transport_version TEXT,
|
|
977
|
+
relative_path TEXT NOT NULL,
|
|
978
|
+
absolute_path TEXT NOT NULL,
|
|
979
|
+
device_number INTEGER NOT NULL,
|
|
980
|
+
inode INTEGER NOT NULL,
|
|
981
|
+
byte_size INTEGER,
|
|
982
|
+
modified_time_ms REAL,
|
|
983
|
+
changed_time_ms REAL,
|
|
984
|
+
filesystem_mode INTEGER,
|
|
985
|
+
content_verified_at_ms REAL
|
|
986
|
+
);
|
|
940
987
|
CREATE TABLE IF NOT EXISTS cloud_outbox (
|
|
941
988
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
942
989
|
mutation_id TEXT NOT NULL UNIQUE,
|
|
@@ -945,6 +992,7 @@ function initializeDatabase(file) {
|
|
|
945
992
|
base_version INTEGER NOT NULL,
|
|
946
993
|
snapshot_checksum TEXT NOT NULL,
|
|
947
994
|
snapshot_json TEXT NOT NULL,
|
|
995
|
+
detected_records_json TEXT NOT NULL DEFAULT '[]',
|
|
948
996
|
created_at TEXT NOT NULL
|
|
949
997
|
);
|
|
950
998
|
CREATE TABLE IF NOT EXISTS workspace_add_intents (
|
|
@@ -956,6 +1004,10 @@ function initializeDatabase(file) {
|
|
|
956
1004
|
ON entities(absolute_path);
|
|
957
1005
|
CREATE INDEX IF NOT EXISTS entities_by_physical_identity
|
|
958
1006
|
ON entities(device_number, inode);
|
|
1007
|
+
CREATE INDEX IF NOT EXISTS detection_notebook_by_root_path
|
|
1008
|
+
ON detection_notebook(resource_id, root_uuid, relative_path);
|
|
1009
|
+
CREATE INDEX IF NOT EXISTS detection_notebook_by_physical_identity
|
|
1010
|
+
ON detection_notebook(device_number, inode);
|
|
959
1011
|
`);
|
|
960
1012
|
const currentEntityColumns = new Set(database.prepare("PRAGMA table_info(entities)").all()
|
|
961
1013
|
.map(({ name }) => name));
|
|
@@ -970,9 +1022,20 @@ function initializeDatabase(file) {
|
|
|
970
1022
|
if (!currentEntityColumns.has(name))
|
|
971
1023
|
database.exec(`ALTER TABLE entities ADD COLUMN ${name} ${type}`);
|
|
972
1024
|
}
|
|
1025
|
+
const outboxColumns = new Set(database.prepare("PRAGMA table_info(cloud_outbox)").all()
|
|
1026
|
+
.map(({ name }) => name));
|
|
1027
|
+
if (!outboxColumns.has("detected_records_json")) {
|
|
1028
|
+
database.exec("ALTER TABLE cloud_outbox ADD COLUMN detected_records_json TEXT NOT NULL DEFAULT '[]'");
|
|
1029
|
+
}
|
|
1030
|
+
const notebookCount = database.prepare("SELECT COUNT(*) AS count FROM detection_notebook").get().count;
|
|
1031
|
+
if (notebookCount === 0)
|
|
1032
|
+
database.exec(`
|
|
1033
|
+
INSERT INTO detection_notebook(${GROUND_ROW_COLUMNS})
|
|
1034
|
+
SELECT ${GROUND_ROW_COLUMNS} FROM entities
|
|
1035
|
+
`);
|
|
973
1036
|
return database;
|
|
974
1037
|
}
|
|
975
|
-
function
|
|
1038
|
+
function readGroundRows(file, table) {
|
|
976
1039
|
if (!existsSync(file))
|
|
977
1040
|
return [];
|
|
978
1041
|
const database = initializeDatabase(file);
|
|
@@ -986,13 +1049,19 @@ function readRows(file) {
|
|
|
986
1049
|
byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
|
|
987
1050
|
changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
|
|
988
1051
|
content_verified_at_ms AS contentVerifiedAtMs
|
|
989
|
-
FROM
|
|
1052
|
+
FROM ${table} ORDER BY uuid
|
|
990
1053
|
`).all();
|
|
991
1054
|
}
|
|
992
1055
|
finally {
|
|
993
1056
|
database.close();
|
|
994
1057
|
}
|
|
995
1058
|
}
|
|
1059
|
+
function readRows(file) {
|
|
1060
|
+
return readGroundRows(file, "entities");
|
|
1061
|
+
}
|
|
1062
|
+
function readDetectionNotebook(file) {
|
|
1063
|
+
return readGroundRows(file, "detection_notebook");
|
|
1064
|
+
}
|
|
996
1065
|
function workspaceAddStagingPath(destinationPath, workspaceId) {
|
|
997
1066
|
return join(dirname(destinationPath), `.amalgm-${workspaceId}.adding`);
|
|
998
1067
|
}
|
|
@@ -1107,12 +1176,14 @@ function findKnownEntityId(file, absolutePath, deviceNumber, inode) {
|
|
|
1107
1176
|
const database = initializeDatabase(file);
|
|
1108
1177
|
try {
|
|
1109
1178
|
const exact = database.prepare("SELECT uuid FROM entities WHERE absolute_path = ? LIMIT 2").all(absolutePath);
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (ids.length > 1) {
|
|
1113
|
-
throw new Error(`entity graph has ambiguous identity for ${absolutePath}`);
|
|
1179
|
+
if (exact.length > 0) {
|
|
1180
|
+
return selectKnownRegistrationId(absolutePath, exact.map(({ uuid }) => ({ uuid, absolutePath })));
|
|
1114
1181
|
}
|
|
1115
|
-
|
|
1182
|
+
const physical = database.prepare(`
|
|
1183
|
+
SELECT uuid, absolute_path AS absolutePath
|
|
1184
|
+
FROM entities WHERE device_number = ? AND inode = ?
|
|
1185
|
+
`).all(deviceNumber, inode);
|
|
1186
|
+
return selectKnownRegistrationId(absolutePath, physical);
|
|
1116
1187
|
}
|
|
1117
1188
|
finally {
|
|
1118
1189
|
database.close();
|
|
@@ -1126,7 +1197,8 @@ function readOutbox(file) {
|
|
|
1126
1197
|
return database.prepare(`
|
|
1127
1198
|
SELECT mutation_id AS mutationId, resource_id AS resourceId,
|
|
1128
1199
|
authority_epoch AS authorityEpoch, base_version AS baseVersion,
|
|
1129
|
-
snapshot_checksum AS snapshotChecksum, snapshot_json AS snapshotJson
|
|
1200
|
+
snapshot_checksum AS snapshotChecksum, snapshot_json AS snapshotJson,
|
|
1201
|
+
detected_records_json AS detectedRecordsJson
|
|
1130
1202
|
FROM cloud_outbox ORDER BY sequence
|
|
1131
1203
|
`).all();
|
|
1132
1204
|
}
|
|
@@ -1134,6 +1206,33 @@ function readOutbox(file) {
|
|
|
1134
1206
|
database.close();
|
|
1135
1207
|
}
|
|
1136
1208
|
}
|
|
1209
|
+
/** Record is the SQLite transaction that accepts Detect's exact proposals and
|
|
1210
|
+
* advances the last-verified notebook. Watch may settle only after this
|
|
1211
|
+
* function returns. The existing outbox remains the one journal and queue. */
|
|
1212
|
+
function commitDetectedState(file, input) {
|
|
1213
|
+
const database = initializeDatabase(file);
|
|
1214
|
+
try {
|
|
1215
|
+
database.transaction(() => {
|
|
1216
|
+
if (input.outbox) {
|
|
1217
|
+
database.prepare(`
|
|
1218
|
+
INSERT INTO cloud_outbox(
|
|
1219
|
+
mutation_id, resource_id, authority_epoch, base_version,
|
|
1220
|
+
snapshot_checksum, snapshot_json, detected_records_json, created_at
|
|
1221
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1222
|
+
`).run(input.outbox.mutationId, input.outbox.resourceId, input.outbox.authorityEpoch, input.outbox.baseVersion, input.outbox.snapshotChecksum, input.outbox.snapshotJson, input.outbox.detectedRecordsJson, new Date().toISOString());
|
|
1223
|
+
}
|
|
1224
|
+
const remove = database.prepare("DELETE FROM detection_notebook WHERE uuid = ?");
|
|
1225
|
+
for (const uuid of input.notebookRemovals)
|
|
1226
|
+
remove.run(uuid);
|
|
1227
|
+
const upsertNotebook = prepareGroundUpsert(database, "detection_notebook");
|
|
1228
|
+
for (const row of input.acceptedNotebookRows)
|
|
1229
|
+
upsertNotebook.run(...groundRowValues(row));
|
|
1230
|
+
})();
|
|
1231
|
+
}
|
|
1232
|
+
finally {
|
|
1233
|
+
database.close();
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1137
1236
|
function deleteOutbox(file, mutationId) {
|
|
1138
1237
|
const database = initializeDatabase(file);
|
|
1139
1238
|
try {
|
|
@@ -1149,7 +1248,63 @@ function portableRecord(row) {
|
|
|
1149
1248
|
function portableRecords(file) {
|
|
1150
1249
|
return readRows(file).map(portableRecord);
|
|
1151
1250
|
}
|
|
1152
|
-
function
|
|
1251
|
+
function capturedGroundRow(resourceId, rootUUID, row) {
|
|
1252
|
+
return {
|
|
1253
|
+
resourceId,
|
|
1254
|
+
rootUUID,
|
|
1255
|
+
uuid: row.record.uuid,
|
|
1256
|
+
type: row.record.type,
|
|
1257
|
+
parentUUID: row.record.parentUUID,
|
|
1258
|
+
name: row.record.name,
|
|
1259
|
+
status: "active",
|
|
1260
|
+
version: row.record.version,
|
|
1261
|
+
payloadVersion: row.record.payloadVersion,
|
|
1262
|
+
transportVersion: row.record.transportVersion,
|
|
1263
|
+
relativePath: row.relativePath,
|
|
1264
|
+
absolutePath: row.absolutePath,
|
|
1265
|
+
deviceNumber: row.deviceNumber,
|
|
1266
|
+
inode: row.inode,
|
|
1267
|
+
byteSize: row.byteSize,
|
|
1268
|
+
modifiedTimeMs: row.modifiedTimeMs,
|
|
1269
|
+
changedTimeMs: row.changedTimeMs,
|
|
1270
|
+
filesystemMode: row.filesystemMode,
|
|
1271
|
+
contentVerifiedAtMs: row.contentVerifiedAtMs,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function groundRowValues(row) {
|
|
1275
|
+
return [
|
|
1276
|
+
row.uuid, row.resourceId, row.rootUUID, row.type, row.parentUUID, row.name, row.status,
|
|
1277
|
+
row.version, row.payloadVersion, row.transportVersion, row.relativePath, row.absolutePath,
|
|
1278
|
+
row.deviceNumber, row.inode, row.byteSize, row.modifiedTimeMs, row.changedTimeMs,
|
|
1279
|
+
row.filesystemMode, row.contentVerifiedAtMs,
|
|
1280
|
+
];
|
|
1281
|
+
}
|
|
1282
|
+
function prepareGroundUpsert(database, table) {
|
|
1283
|
+
return database.prepare(`
|
|
1284
|
+
INSERT INTO ${table}(${GROUND_ROW_COLUMNS})
|
|
1285
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1286
|
+
ON CONFLICT(uuid) DO UPDATE SET
|
|
1287
|
+
resource_id = excluded.resource_id,
|
|
1288
|
+
root_uuid = excluded.root_uuid,
|
|
1289
|
+
type = excluded.type,
|
|
1290
|
+
parent_uuid = excluded.parent_uuid,
|
|
1291
|
+
name = excluded.name,
|
|
1292
|
+
status = excluded.status,
|
|
1293
|
+
version = excluded.version,
|
|
1294
|
+
payload_version = excluded.payload_version,
|
|
1295
|
+
transport_version = excluded.transport_version,
|
|
1296
|
+
relative_path = excluded.relative_path,
|
|
1297
|
+
absolute_path = excluded.absolute_path,
|
|
1298
|
+
device_number = excluded.device_number,
|
|
1299
|
+
inode = excluded.inode,
|
|
1300
|
+
byte_size = excluded.byte_size,
|
|
1301
|
+
modified_time_ms = excluded.modified_time_ms,
|
|
1302
|
+
changed_time_ms = excluded.changed_time_ms,
|
|
1303
|
+
filesystem_mode = excluded.filesystem_mode,
|
|
1304
|
+
content_verified_at_ms = excluded.content_verified_at_ms
|
|
1305
|
+
`);
|
|
1306
|
+
}
|
|
1307
|
+
function persistRows(file, identity, resourceId, rootUUID, rows, previousRows = [], options = {}) {
|
|
1153
1308
|
const database = initializeDatabase(file);
|
|
1154
1309
|
try {
|
|
1155
1310
|
const replaceIdentity = database.prepare(`
|
|
@@ -1160,37 +1315,15 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
1160
1315
|
user_email = excluded.user_email,
|
|
1161
1316
|
device_id = excluded.device_id
|
|
1162
1317
|
`);
|
|
1163
|
-
const upsert = database
|
|
1164
|
-
|
|
1165
|
-
uuid, resource_id, root_uuid, type, parent_uuid, name, status, version, payload_version,
|
|
1166
|
-
transport_version, relative_path, absolute_path, device_number, inode
|
|
1167
|
-
, byte_size, modified_time_ms, changed_time_ms, filesystem_mode, content_verified_at_ms
|
|
1168
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1169
|
-
ON CONFLICT(uuid) DO UPDATE SET
|
|
1170
|
-
resource_id = excluded.resource_id,
|
|
1171
|
-
root_uuid = excluded.root_uuid,
|
|
1172
|
-
type = excluded.type,
|
|
1173
|
-
parent_uuid = excluded.parent_uuid,
|
|
1174
|
-
name = excluded.name,
|
|
1175
|
-
status = excluded.status,
|
|
1176
|
-
version = excluded.version,
|
|
1177
|
-
payload_version = excluded.payload_version,
|
|
1178
|
-
transport_version = excluded.transport_version,
|
|
1179
|
-
relative_path = excluded.relative_path,
|
|
1180
|
-
absolute_path = excluded.absolute_path,
|
|
1181
|
-
device_number = excluded.device_number,
|
|
1182
|
-
inode = excluded.inode,
|
|
1183
|
-
byte_size = excluded.byte_size,
|
|
1184
|
-
modified_time_ms = excluded.modified_time_ms,
|
|
1185
|
-
changed_time_ms = excluded.changed_time_ms,
|
|
1186
|
-
filesystem_mode = excluded.filesystem_mode,
|
|
1187
|
-
content_verified_at_ms = excluded.content_verified_at_ms
|
|
1188
|
-
`);
|
|
1318
|
+
const upsert = prepareGroundUpsert(database, "entities");
|
|
1319
|
+
const upsertNotebook = prepareGroundUpsert(database, "detection_notebook");
|
|
1189
1320
|
const remove = database.prepare("DELETE FROM entities WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
|
|
1321
|
+
const removeNotebook = database.prepare("DELETE FROM detection_notebook WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
|
|
1190
1322
|
const previousByUuid = new Map(previousRows.map((row) => [row.uuid, row]));
|
|
1323
|
+
const materializedIds = new Set(database.prepare("SELECT uuid FROM entities").all()
|
|
1324
|
+
.map(({ uuid }) => uuid));
|
|
1191
1325
|
const currentUuids = new Set(rows.map((row) => row.record.uuid));
|
|
1192
|
-
const
|
|
1193
|
-
const old = previousByUuid.get(row.record.uuid);
|
|
1326
|
+
const rowChanged = (row, old) => {
|
|
1194
1327
|
return !old
|
|
1195
1328
|
|| old.resourceId !== resourceId
|
|
1196
1329
|
|| old.rootUUID !== rootUUID
|
|
@@ -1210,15 +1343,27 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
1210
1343
|
|| old.changedTimeMs !== row.changedTimeMs
|
|
1211
1344
|
|| old.filesystemMode !== row.filesystemMode
|
|
1212
1345
|
|| old.contentVerifiedAtMs !== row.contentVerifiedAtMs;
|
|
1213
|
-
}
|
|
1346
|
+
};
|
|
1347
|
+
const changed = rows.filter((row) => !materializedIds.has(row.record.uuid)
|
|
1348
|
+
|| rowChanged(row, previousByUuid.get(row.record.uuid)));
|
|
1349
|
+
const notebookChanged = rows.filter((row) => rowChanged(row, previousByUuid.get(row.record.uuid)));
|
|
1214
1350
|
const replace = database.transaction(() => {
|
|
1215
1351
|
replaceIdentity.run(identity.userId, identity.userEmail, identity.deviceId);
|
|
1216
1352
|
for (const old of previousRows) {
|
|
1217
|
-
if (!currentUuids.has(old.uuid))
|
|
1353
|
+
if (!currentUuids.has(old.uuid)) {
|
|
1218
1354
|
remove.run(old.uuid, resourceId, old.rootUUID);
|
|
1355
|
+
if (options.updateNotebook !== false) {
|
|
1356
|
+
removeNotebook.run(old.uuid, resourceId, old.rootUUID);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1219
1359
|
}
|
|
1220
1360
|
for (const row of changed)
|
|
1221
|
-
upsert.run(
|
|
1361
|
+
upsert.run(...groundRowValues(capturedGroundRow(resourceId, rootUUID, row)));
|
|
1362
|
+
if (options.updateNotebook !== false) {
|
|
1363
|
+
for (const row of notebookChanged) {
|
|
1364
|
+
upsertNotebook.run(...groundRowValues(capturedGroundRow(resourceId, rootUUID, row)));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1222
1367
|
});
|
|
1223
1368
|
replace();
|
|
1224
1369
|
}
|
|
@@ -1233,15 +1378,17 @@ function refreshRevealedRootEvidence(file, workspaceId, absolutePath) {
|
|
|
1233
1378
|
const stats = statFingerprint(lstatSync(absolutePath));
|
|
1234
1379
|
const database = initializeDatabase(file);
|
|
1235
1380
|
try {
|
|
1236
|
-
const
|
|
1237
|
-
UPDATE
|
|
1381
|
+
const refresh = (table) => database.prepare(`
|
|
1382
|
+
UPDATE ${table} SET
|
|
1238
1383
|
device_number = ?, inode = ?, byte_size = ?, modified_time_ms = ?,
|
|
1239
1384
|
changed_time_ms = ?, filesystem_mode = ?
|
|
1240
1385
|
WHERE uuid = ? AND absolute_path = ?
|
|
1241
1386
|
`).run(stats.deviceNumber, stats.inode, stats.byteSize, stats.modifiedTimeMs, stats.changedTimeMs, stats.filesystemMode, workspaceId, absolutePath);
|
|
1387
|
+
const result = refresh("entities");
|
|
1242
1388
|
if (result.changes !== 1) {
|
|
1243
1389
|
throw new Error(`revealed workspace ${workspaceId} has no committed root row`);
|
|
1244
1390
|
}
|
|
1391
|
+
refresh("detection_notebook");
|
|
1245
1392
|
}
|
|
1246
1393
|
finally {
|
|
1247
1394
|
database.close();
|
|
@@ -1364,7 +1511,9 @@ function materializedWorkspaceBindings(bindingDir) {
|
|
|
1364
1511
|
async function scanAndRegister(input) {
|
|
1365
1512
|
const { identity, userRoot, database, cacheDir, suspicions, onScan } = input;
|
|
1366
1513
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
1367
|
-
const
|
|
1514
|
+
const materialized = readRows(database);
|
|
1515
|
+
const notebook = readDetectionNotebook(database);
|
|
1516
|
+
const existing = suspicions ? notebook : materialized;
|
|
1368
1517
|
const ignoreFile = join(userRoot, ".amalgmignore");
|
|
1369
1518
|
const policy = createUserGroundEnrollmentPolicy(existsSync(ignoreFile) ? readFileSync(ignoreFile, "utf8") : "");
|
|
1370
1519
|
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
@@ -1378,8 +1527,9 @@ async function scanAndRegister(input) {
|
|
|
1378
1527
|
const suspicionFor = (root) => {
|
|
1379
1528
|
if (!suspicions || !suspicions.has(root))
|
|
1380
1529
|
return suspicions ? [] : null;
|
|
1381
|
-
return suspicions.get(root);
|
|
1530
|
+
return suspicions.get(root).paths;
|
|
1382
1531
|
};
|
|
1532
|
+
const observationFor = (root) => suspicions?.get(root);
|
|
1383
1533
|
const existingCore = existing.find((row) => row.parentUUID === null && row.absolutePath === userRoot && row.type === "workspace");
|
|
1384
1534
|
const coreUUID = existingCore?.uuid || randomUUID();
|
|
1385
1535
|
const existingReferenceIds = new Set(existing
|
|
@@ -1388,7 +1538,10 @@ async function scanAndRegister(input) {
|
|
|
1388
1538
|
const coreSuspicion = suspicionFor(userRoot);
|
|
1389
1539
|
const missingPortableReference = [...boundRoots.keys()]
|
|
1390
1540
|
.some((workspaceId) => !existingReferenceIds.has(workspaceId));
|
|
1391
|
-
const
|
|
1541
|
+
const acceptedRecords = [];
|
|
1542
|
+
const acceptedNotebookRows = [];
|
|
1543
|
+
const notebookRemovals = new Set();
|
|
1544
|
+
const detection = [];
|
|
1392
1545
|
const scan = async (parameters) => {
|
|
1393
1546
|
const counters = {
|
|
1394
1547
|
entries: 0,
|
|
@@ -1399,6 +1552,14 @@ async function scanAndRegister(input) {
|
|
|
1399
1552
|
};
|
|
1400
1553
|
const started = performance.now();
|
|
1401
1554
|
const result = await scanRoot({ ...parameters, evidence: counters });
|
|
1555
|
+
if (result.detection)
|
|
1556
|
+
detection.push(result.detection);
|
|
1557
|
+
for (const uuid of result.notebookRemovals)
|
|
1558
|
+
notebookRemovals.add(uuid);
|
|
1559
|
+
if (result.transferable) {
|
|
1560
|
+
acceptedRecords.push(...result.records);
|
|
1561
|
+
acceptedNotebookRows.push(...result.rows.map((row) => capturedGroundRow(parameters.resourceId, parameters.rootUUID, row)));
|
|
1562
|
+
}
|
|
1402
1563
|
onScan?.({
|
|
1403
1564
|
rootId: parameters.rootUUID,
|
|
1404
1565
|
directory: parameters.rootPath,
|
|
@@ -1413,7 +1574,7 @@ async function scanAndRegister(input) {
|
|
|
1413
1574
|
await new Promise((resolve) => setImmediate(resolve));
|
|
1414
1575
|
return result;
|
|
1415
1576
|
};
|
|
1416
|
-
|
|
1577
|
+
await scan({
|
|
1417
1578
|
identity,
|
|
1418
1579
|
resourceId,
|
|
1419
1580
|
rootUUID: coreUUID,
|
|
@@ -1428,13 +1589,12 @@ async function scanAndRegister(input) {
|
|
|
1428
1589
|
? [...new Set([...coreSuspicion, "workspaces"])]
|
|
1429
1590
|
: coreSuspicion,
|
|
1430
1591
|
existingRows: existing.filter((row) => row.rootUUID === coreUUID),
|
|
1592
|
+
suspicion: observationFor(userRoot),
|
|
1431
1593
|
});
|
|
1432
|
-
if (!core.transferable)
|
|
1433
|
-
deferredRootIds.add(coreUUID);
|
|
1434
1594
|
for (const { workspaceId, directory: rootPath } of outerBoundRoots) {
|
|
1435
1595
|
const existingRoot = existing.find((row) => row.uuid === workspaceId);
|
|
1436
1596
|
const existingWorkspaceRows = existing.filter((row) => row.resourceId === resourceId && pathWithin(rootPath, row.absolutePath));
|
|
1437
|
-
|
|
1597
|
+
await scan({
|
|
1438
1598
|
identity,
|
|
1439
1599
|
resourceId,
|
|
1440
1600
|
rootUUID: workspaceId,
|
|
@@ -1451,13 +1611,15 @@ async function scanAndRegister(input) {
|
|
|
1451
1611
|
suspects: existingWorkspaceRows.length === 0 ? null : suspicionFor(rootPath),
|
|
1452
1612
|
existingRows: existingWorkspaceRows,
|
|
1453
1613
|
registeredBoundaries,
|
|
1614
|
+
suspicion: observationFor(rootPath),
|
|
1454
1615
|
});
|
|
1455
|
-
if (!scanned.transferable)
|
|
1456
|
-
deferredRootIds.add(workspaceId);
|
|
1457
1616
|
}
|
|
1458
|
-
return
|
|
1459
|
-
|
|
1460
|
-
|
|
1617
|
+
return {
|
|
1618
|
+
records: acceptedRecords,
|
|
1619
|
+
detection,
|
|
1620
|
+
acceptedNotebookRows,
|
|
1621
|
+
notebookRemovals: [...notebookRemovals],
|
|
1622
|
+
};
|
|
1461
1623
|
}
|
|
1462
1624
|
async function scanRoot(input) {
|
|
1463
1625
|
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, } = input;
|
|
@@ -1476,32 +1638,43 @@ async function scanRoot(input) {
|
|
|
1476
1638
|
&& suspects.length === 0
|
|
1477
1639
|
&& existingRows.length > 0
|
|
1478
1640
|
&& completeRepositoryEvidence) {
|
|
1641
|
+
const rows = existingRows.map((row) => ({
|
|
1642
|
+
record: portableRecord(row),
|
|
1643
|
+
relativePath: row.relativePath,
|
|
1644
|
+
absolutePath: row.absolutePath,
|
|
1645
|
+
deviceNumber: row.deviceNumber,
|
|
1646
|
+
inode: row.inode,
|
|
1647
|
+
byteSize: row.byteSize,
|
|
1648
|
+
modifiedTimeMs: row.modifiedTimeMs,
|
|
1649
|
+
changedTimeMs: row.changedTimeMs,
|
|
1650
|
+
filesystemMode: row.filesystemMode,
|
|
1651
|
+
contentVerifiedAtMs: row.contentVerifiedAtMs,
|
|
1652
|
+
}));
|
|
1479
1653
|
return {
|
|
1480
1654
|
records: existingRows.map(portableRecord),
|
|
1481
|
-
rows
|
|
1482
|
-
record: portableRecord(row),
|
|
1483
|
-
relativePath: row.relativePath,
|
|
1484
|
-
absolutePath: row.absolutePath,
|
|
1485
|
-
deviceNumber: row.deviceNumber,
|
|
1486
|
-
inode: row.inode,
|
|
1487
|
-
byteSize: row.byteSize,
|
|
1488
|
-
modifiedTimeMs: row.modifiedTimeMs,
|
|
1489
|
-
changedTimeMs: row.changedTimeMs,
|
|
1490
|
-
filesystemMode: row.filesystemMode,
|
|
1491
|
-
contentVerifiedAtMs: row.contentVerifiedAtMs,
|
|
1492
|
-
})),
|
|
1655
|
+
rows,
|
|
1493
1656
|
referenceWorkspaceIds: existingRows
|
|
1494
1657
|
.filter((row) => row.type === "reference" && row.payloadVersion && UUID.test(row.payloadVersion))
|
|
1495
1658
|
.map((row) => row.payloadVersion),
|
|
1496
1659
|
transferable: true,
|
|
1660
|
+
notebookRemovals: [],
|
|
1661
|
+
detection: input.suspicion ? planGroundDetection({
|
|
1662
|
+
rootId: rootUUID,
|
|
1663
|
+
suspicion: input.suspicion,
|
|
1664
|
+
before: existingRows.map(portableRecord),
|
|
1665
|
+
after: rows.map((row) => ({ record: row.record, relativePath: row.relativePath })),
|
|
1666
|
+
}) : null,
|
|
1497
1667
|
};
|
|
1498
1668
|
}
|
|
1499
1669
|
const entries = [];
|
|
1500
1670
|
const referenceWorkspaceIds = new Set();
|
|
1501
1671
|
const rootStats = lstatSync(rootPath);
|
|
1672
|
+
const priorRootType = existingByPath.get("")?.type;
|
|
1673
|
+
const rootRailChanged = priorRootType !== undefined && priorRootType !== rootType;
|
|
1502
1674
|
evidence && (evidence.entries += 1);
|
|
1503
1675
|
entries.push({
|
|
1504
1676
|
uuid: rootUUID,
|
|
1677
|
+
fixedUUID: rootUUID,
|
|
1505
1678
|
type: rootType,
|
|
1506
1679
|
parentUUID: input.rootParentUUID ?? null,
|
|
1507
1680
|
name: rootName,
|
|
@@ -1538,7 +1711,7 @@ async function scanRoot(input) {
|
|
|
1538
1711
|
};
|
|
1539
1712
|
const visit = async (directory, parentUUID, parentType, base = "", activeRepository = parentType === "repo.git"
|
|
1540
1713
|
? { root: directory, evidencePaths: repositoryEvidencePaths(directory), evidence: null }
|
|
1541
|
-
: null) => {
|
|
1714
|
+
: null, forceObserve = rootRailChanged) => {
|
|
1542
1715
|
const children = readdirSync(directory, { withFileTypes: true })
|
|
1543
1716
|
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1544
1717
|
for (const child of children) {
|
|
@@ -1561,7 +1734,7 @@ async function scanRoot(input) {
|
|
|
1561
1734
|
const stableDuringCompleteCatchUp = suspects === null
|
|
1562
1735
|
&& existing !== undefined
|
|
1563
1736
|
&& reusableDuringCompleteCatchUp(existing, stats);
|
|
1564
|
-
const observe = existing === undefined
|
|
1737
|
+
const observe = forceObserve || existing === undefined
|
|
1565
1738
|
|| (suspects === null ? !stableDuringCompleteCatchUp : touchesSuspicion(relativePath));
|
|
1566
1739
|
if (!observe && suspects === null)
|
|
1567
1740
|
evidence && (evidence.metadataReused += 1);
|
|
@@ -1585,6 +1758,7 @@ async function scanRoot(input) {
|
|
|
1585
1758
|
: undefined;
|
|
1586
1759
|
let type;
|
|
1587
1760
|
let payloadVersion = null;
|
|
1761
|
+
let linkTarget;
|
|
1588
1762
|
let contentVerifiedAtMs = existing?.contentVerifiedAtMs ?? null;
|
|
1589
1763
|
if (!observe && existing && !stats.isDirectory()) {
|
|
1590
1764
|
type = existing.type;
|
|
@@ -1607,7 +1781,8 @@ async function scanRoot(input) {
|
|
|
1607
1781
|
}
|
|
1608
1782
|
else {
|
|
1609
1783
|
evidence && (evidence.contentReads += 1);
|
|
1610
|
-
|
|
1784
|
+
linkTarget = readlinkSync(absolutePath);
|
|
1785
|
+
const bytes = Buffer.from(linkTarget, "utf8");
|
|
1611
1786
|
payloadVersion = sha256Hex(bytes);
|
|
1612
1787
|
if (!activeRepository)
|
|
1613
1788
|
immutableWriteArtifact(cacheDir, {
|
|
@@ -1641,6 +1816,7 @@ async function scanRoot(input) {
|
|
|
1641
1816
|
contentVerifiedAtMs = Date.now();
|
|
1642
1817
|
entries.push({
|
|
1643
1818
|
uuid,
|
|
1819
|
+
...(registeredBoundaryId ? { fixedUUID: registeredBoundaryId } : {}),
|
|
1644
1820
|
type,
|
|
1645
1821
|
parentUUID,
|
|
1646
1822
|
name: child.name,
|
|
@@ -1649,20 +1825,47 @@ async function scanRoot(input) {
|
|
|
1649
1825
|
payloadVersion,
|
|
1650
1826
|
transportVersion: null,
|
|
1651
1827
|
contentVerifiedAtMs,
|
|
1828
|
+
...(linkTarget !== undefined ? { linkTarget } : {}),
|
|
1652
1829
|
...statFingerprint(stats),
|
|
1653
1830
|
});
|
|
1654
1831
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1832
|
+
const childRailChanged = existing !== undefined && existing.type !== type;
|
|
1655
1833
|
await visit(absolutePath, uuid, type, relativePath, type === "repo.git"
|
|
1656
1834
|
? {
|
|
1657
1835
|
root: absolutePath,
|
|
1658
1836
|
evidencePaths: repositoryEvidencePaths(absolutePath),
|
|
1659
1837
|
evidence: null,
|
|
1660
1838
|
}
|
|
1661
|
-
: activeRepository);
|
|
1839
|
+
: activeRepository, forceObserve || childRailChanged);
|
|
1662
1840
|
}
|
|
1663
1841
|
}
|
|
1664
1842
|
};
|
|
1665
1843
|
await visit(rootPath, rootUUID, rootType);
|
|
1844
|
+
const resolvedIdentity = reconcileGroundUUIDs(existingRows.map((row) => ({
|
|
1845
|
+
uuid: row.uuid,
|
|
1846
|
+
type: row.type,
|
|
1847
|
+
relativePath: row.relativePath,
|
|
1848
|
+
deviceNumber: row.deviceNumber,
|
|
1849
|
+
inode: row.inode,
|
|
1850
|
+
})), entries.map((entry) => ({
|
|
1851
|
+
key: entry.relativePath,
|
|
1852
|
+
type: entry.type,
|
|
1853
|
+
relativePath: entry.relativePath,
|
|
1854
|
+
deviceNumber: entry.deviceNumber,
|
|
1855
|
+
inode: entry.inode,
|
|
1856
|
+
...(entry.fixedUUID ? { fixedUUID: entry.fixedUUID } : {}),
|
|
1857
|
+
})));
|
|
1858
|
+
const resolvedByProvisional = new Map();
|
|
1859
|
+
for (const entry of entries) {
|
|
1860
|
+
const resolved = resolvedIdentity.get(entry.relativePath) ?? entry.uuid;
|
|
1861
|
+
resolvedByProvisional.set(entry.uuid, resolved);
|
|
1862
|
+
entry.uuid = resolved;
|
|
1863
|
+
}
|
|
1864
|
+
for (const entry of entries) {
|
|
1865
|
+
if (entry.parentUUID !== null) {
|
|
1866
|
+
entry.parentUUID = resolvedByProvisional.get(entry.parentUUID) ?? entry.parentUUID;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1666
1869
|
const children = new Map();
|
|
1667
1870
|
for (const entry of entries) {
|
|
1668
1871
|
if (entry.parentUUID === null)
|
|
@@ -1706,10 +1909,13 @@ async function scanRoot(input) {
|
|
|
1706
1909
|
});
|
|
1707
1910
|
const persistCurrentRows = () => {
|
|
1708
1911
|
const rows = rowsFromEntries();
|
|
1709
|
-
persistRows(database, identity, resourceId, rootUUID, rows, existingRows
|
|
1912
|
+
persistRows(database, identity, resourceId, rootUUID, rows, existingRows, {
|
|
1913
|
+
updateNotebook: input.suspicion === undefined,
|
|
1914
|
+
});
|
|
1710
1915
|
return rows;
|
|
1711
1916
|
};
|
|
1712
1917
|
const repositories = entries.filter((entry) => entry.type === "repo.git");
|
|
1918
|
+
const replayByUuid = new Map();
|
|
1713
1919
|
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
1714
1920
|
const repositoryOwner = (entry) => repositories
|
|
1715
1921
|
.filter((repository) => repository.uuid !== entry.uuid
|
|
@@ -1734,12 +1940,14 @@ async function scanRoot(input) {
|
|
|
1734
1940
|
repository.transportVersion = priorRow.transportVersion;
|
|
1735
1941
|
}
|
|
1736
1942
|
let prior = null;
|
|
1943
|
+
let priorLayout = null;
|
|
1737
1944
|
if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
|
|
1738
1945
|
const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
|
|
1739
1946
|
if (existsSync(priorPath)) {
|
|
1740
1947
|
try {
|
|
1741
1948
|
const layout = inspectRepositoryTransportFile(priorPath);
|
|
1742
1949
|
if (layout.stateId === priorRow.payloadVersion) {
|
|
1950
|
+
priorLayout = layout;
|
|
1743
1951
|
prior = {
|
|
1744
1952
|
stateId: priorRow.payloadVersion,
|
|
1745
1953
|
transportVersion: priorRow.transportVersion,
|
|
@@ -1789,6 +1997,17 @@ async function scanRoot(input) {
|
|
|
1789
1997
|
contentHash: captured.transportVersion,
|
|
1790
1998
|
}, captured.bytes);
|
|
1791
1999
|
}
|
|
2000
|
+
const currentLayout = inspectRepositoryTransportFile(join(cacheDir, `${captured.transportVersion}.bin`));
|
|
2001
|
+
replayByUuid.set(repository.uuid, {
|
|
2002
|
+
kind: "repo.git",
|
|
2003
|
+
stateId: currentLayout.stateId,
|
|
2004
|
+
cardId: currentLayout.cardId,
|
|
2005
|
+
checkpointId: currentLayout.checkpointId,
|
|
2006
|
+
cardChanged: priorLayout?.cardId !== currentLayout.cardId,
|
|
2007
|
+
checkpointChanged: priorLayout?.checkpointId !== currentLayout.checkpointId,
|
|
2008
|
+
parentTransportVersion: currentLayout.parentTransportVersion,
|
|
2009
|
+
transportVersion: captured.transportVersion,
|
|
2010
|
+
});
|
|
1792
2011
|
}
|
|
1793
2012
|
}
|
|
1794
2013
|
catch {
|
|
@@ -1804,14 +2023,90 @@ async function scanRoot(input) {
|
|
|
1804
2023
|
rows,
|
|
1805
2024
|
referenceWorkspaceIds: [...referenceWorkspaceIds],
|
|
1806
2025
|
transferable: false,
|
|
2026
|
+
notebookRemovals: [],
|
|
2027
|
+
detection: null,
|
|
1807
2028
|
};
|
|
1808
2029
|
}
|
|
1809
2030
|
const rows = persistCurrentRows();
|
|
2031
|
+
const currentUUIDs = new Set(rows.map((row) => row.record.uuid));
|
|
2032
|
+
const notebookRemovals = input.suspicion === undefined ? [] : existingRows
|
|
2033
|
+
.filter((row) => !currentUUIDs.has(row.uuid) && existsSync(row.absolutePath))
|
|
2034
|
+
.map((row) => row.uuid);
|
|
2035
|
+
const before = existingRows.map(portableRecord);
|
|
2036
|
+
const beforeByUuid = new Map(before.map((record) => [record.uuid, record]));
|
|
2037
|
+
const detected = rows.map((row) => {
|
|
2038
|
+
const base = beforeByUuid.get(row.record.uuid) ?? null;
|
|
2039
|
+
const contentChanged = base === null
|
|
2040
|
+
|| base.type !== row.record.type
|
|
2041
|
+
|| base.payloadVersion !== row.record.payloadVersion
|
|
2042
|
+
|| base.transportVersion !== row.record.transportVersion;
|
|
2043
|
+
let replay = { kind: "structure" };
|
|
2044
|
+
if (contentChanged) {
|
|
2045
|
+
switch (row.record.type) {
|
|
2046
|
+
case "file.text":
|
|
2047
|
+
if (!row.record.payloadVersion)
|
|
2048
|
+
throw new Error("detected text file has no content head");
|
|
2049
|
+
replay = {
|
|
2050
|
+
kind: "file.text",
|
|
2051
|
+
mode: "snapshot",
|
|
2052
|
+
basePayloadVersion: base?.payloadVersion && /^[0-9a-f]{64}$/.test(base.payloadVersion)
|
|
2053
|
+
? base.payloadVersion
|
|
2054
|
+
: null,
|
|
2055
|
+
resultPayloadVersion: row.record.payloadVersion,
|
|
2056
|
+
delta: null,
|
|
2057
|
+
};
|
|
2058
|
+
break;
|
|
2059
|
+
case "file.binary": {
|
|
2060
|
+
if (!row.record.payloadVersion)
|
|
2061
|
+
throw new Error("detected binary file has no content head");
|
|
2062
|
+
const manifest = readCachedManifest(cacheDir, row.record.payloadVersion);
|
|
2063
|
+
if (manifest) {
|
|
2064
|
+
replay = {
|
|
2065
|
+
kind: "file.binary",
|
|
2066
|
+
resultPayloadVersion: row.record.payloadVersion,
|
|
2067
|
+
manifest,
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
case "link": {
|
|
2073
|
+
if (!row.record.payloadVersion)
|
|
2074
|
+
throw new Error("detected link has no content head");
|
|
2075
|
+
const target = entries.find((entry) => entry.uuid === row.record.uuid)?.linkTarget;
|
|
2076
|
+
if (target !== undefined) {
|
|
2077
|
+
replay = {
|
|
2078
|
+
kind: "link",
|
|
2079
|
+
resultPayloadVersion: row.record.payloadVersion,
|
|
2080
|
+
target,
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
case "repo.git": {
|
|
2086
|
+
const repositoryReplay = replayByUuid.get(row.record.uuid);
|
|
2087
|
+
if (!repositoryReplay)
|
|
2088
|
+
throw new Error("detected repository has no Card + Checkpoint replay");
|
|
2089
|
+
replay = repositoryReplay;
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
default:
|
|
2093
|
+
break;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return { record: row.record, relativePath: row.relativePath, replay };
|
|
2097
|
+
});
|
|
1810
2098
|
return {
|
|
1811
2099
|
records: rows.map((row) => row.record),
|
|
1812
2100
|
rows,
|
|
1813
2101
|
referenceWorkspaceIds: [...referenceWorkspaceIds],
|
|
1814
2102
|
transferable: true,
|
|
2103
|
+
notebookRemovals,
|
|
2104
|
+
detection: input.suspicion ? planGroundDetection({
|
|
2105
|
+
rootId: rootUUID,
|
|
2106
|
+
suspicion: input.suspicion,
|
|
2107
|
+
before,
|
|
2108
|
+
after: detected,
|
|
2109
|
+
}) : null,
|
|
1815
2110
|
};
|
|
1816
2111
|
}
|
|
1817
2112
|
function portablePaths(records, boundaryUUIDs = new Set()) {
|
|
@@ -1878,6 +2173,40 @@ function mergeMaterializedRoots(cloudRecords, localRecords) {
|
|
|
1878
2173
|
});
|
|
1879
2174
|
return [...preserved, ...localRecords];
|
|
1880
2175
|
}
|
|
2176
|
+
/** Filesystem disappearance is evidence, not cloud lifecycle authority. Keep
|
|
2177
|
+
* the last logical records in the outgoing graph while their materialized
|
|
2178
|
+
* rows remain absent, and derive container membership from that honest union. */
|
|
2179
|
+
function recordsRetainingMissingEvidence(present, missing) {
|
|
2180
|
+
const byUuid = new Map(present.map((record) => [record.uuid, record]));
|
|
2181
|
+
for (const record of missing) {
|
|
2182
|
+
if (!byUuid.has(record.uuid))
|
|
2183
|
+
byUuid.set(record.uuid, record);
|
|
2184
|
+
}
|
|
2185
|
+
const records = [...byUuid.values()];
|
|
2186
|
+
const activeChildren = new Map();
|
|
2187
|
+
for (const record of records) {
|
|
2188
|
+
if (record.status !== "active" || record.parentUUID === null)
|
|
2189
|
+
continue;
|
|
2190
|
+
const children = activeChildren.get(record.parentUUID) ?? [];
|
|
2191
|
+
children.push(record);
|
|
2192
|
+
activeChildren.set(record.parentUUID, children);
|
|
2193
|
+
}
|
|
2194
|
+
return records.map((record) => {
|
|
2195
|
+
if (record.type !== "workspace" && record.type !== "folder")
|
|
2196
|
+
return record;
|
|
2197
|
+
const payloadVersion = membershipHash(activeChildren.get(record.uuid) ?? [], sha256Hex);
|
|
2198
|
+
return {
|
|
2199
|
+
...record,
|
|
2200
|
+
version: canonicalVersion({
|
|
2201
|
+
type: record.type,
|
|
2202
|
+
parentUUID: record.parentUUID,
|
|
2203
|
+
name: record.name,
|
|
2204
|
+
status: record.status,
|
|
2205
|
+
payloadVersion,
|
|
2206
|
+
}, sha256Hex),
|
|
2207
|
+
};
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
1881
2210
|
function clearCoreMaterialization(userRoot) {
|
|
1882
2211
|
ensurePrivateDir(userRoot);
|
|
1883
2212
|
for (const entry of readdirSync(userRoot, { withFileTypes: true })) {
|
|
@@ -1889,8 +2218,12 @@ function clearCoreMaterialization(userRoot) {
|
|
|
1889
2218
|
function deleteRootRows(databasePath, resourceId, rootUUID) {
|
|
1890
2219
|
const database = initializeDatabase(databasePath);
|
|
1891
2220
|
try {
|
|
1892
|
-
database.
|
|
1893
|
-
.
|
|
2221
|
+
database.transaction(() => {
|
|
2222
|
+
database.prepare("DELETE FROM entities WHERE resource_id = ? AND root_uuid = ?")
|
|
2223
|
+
.run(resourceId, rootUUID);
|
|
2224
|
+
database.prepare("DELETE FROM detection_notebook WHERE resource_id = ? AND root_uuid = ?")
|
|
2225
|
+
.run(resourceId, rootUUID);
|
|
2226
|
+
})();
|
|
1894
2227
|
}
|
|
1895
2228
|
finally {
|
|
1896
2229
|
database.close();
|