@amalgm/shell 0.1.125 → 0.1.127
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 +21 -7
- package/dist/content-cache-host.d.ts +4 -0
- package/dist/content-cache-host.js +7 -0
- package/dist/content-cache-host.js.map +1 -1
- package/dist/content-direct.d.ts +13 -0
- package/dist/content-direct.js +44 -13
- package/dist/content-direct.js.map +1 -1
- package/dist/registry-graph.d.ts +73 -31
- package/dist/registry-graph.js +283 -80
- package/dist/registry-graph.js.map +1 -1
- package/dist/registry-page-cache.d.ts +14 -0
- package/dist/registry-page-cache.js +32 -0
- package/dist/registry-page-cache.js.map +1 -0
- package/dist/user-ground-host.d.ts +28 -7
- package/dist/user-ground-host.js +331 -112
- package/dist/user-ground-host.js.map +1 -1
- package/dist/workspace-add-staging.d.ts +98 -0
- package/dist/workspace-add-staging.js +306 -0
- package/dist/workspace-add-staging.js.map +1 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
|
|
|
3
3
|
import { open, readFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
|
-
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
|
|
6
|
+
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
|
|
7
7
|
import Database from "better-sqlite3";
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir, ensureUserDir } from "./filesystem.js";
|
|
9
9
|
import { emitFilesStage, } from "./files-observability.js";
|
|
@@ -11,7 +11,8 @@ import { EntityApplyHost, } from "./entity-apply-host.js";
|
|
|
11
11
|
import { EntityRecordSqliteStore } from "./entity-record-store.js";
|
|
12
12
|
import { observeEntityRecordPorts } from "./entity-record-observability.js";
|
|
13
13
|
import { indexRowsByOutermostRoot } from "./ground-root-index.js";
|
|
14
|
-
import { ContentCacheDownloadSet, captureContentFile, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
14
|
+
import { ContentCacheDownloadSet, captureContentFile, hasSealedContent, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
15
|
+
import { StagedContentSet, addStagingEvidenceDir, clearAddStagingEvidence, } from "./workspace-add-staging.js";
|
|
15
16
|
import { DirectContentPlane } from "./content-direct.js";
|
|
16
17
|
import { encodeContentWireBytes } from "./content-wire-codec.js";
|
|
17
18
|
import { exactTextReplay, planGroundDetection, reconcileGroundUUIDs, } from "./detection/portable.js";
|
|
@@ -23,6 +24,7 @@ import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile
|
|
|
23
24
|
import { gitEvidenceFingerprint, inspectGitRegistration, sameGitIdentity, } from "./git-registration-host.js";
|
|
24
25
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
25
26
|
import { graphPageIdentities, registryGraphFromSnapshot, } from "./registry-graph.js";
|
|
27
|
+
import { readVerifiedRegistryPage, writeVerifiedRegistryPage, } from "./registry-page-cache.js";
|
|
26
28
|
import { NodeWatchHost, } from "./watching/index.js";
|
|
27
29
|
import { GroundCoordinator } from "./ground-coordination.js";
|
|
28
30
|
import { adoptSnapshotDeliveryBaseline } from "./snapshot-delivery-baseline.js";
|
|
@@ -34,17 +36,17 @@ const CARGO_SEGMENT_LOAD_CONCURRENCY = 8;
|
|
|
34
36
|
/** Storage-object lanes for one download set. Tiny objects use the wide
|
|
35
37
|
* request window, while the separate byte budget narrows large chunks so
|
|
36
38
|
* request concurrency can never silently become an unbounded memory claim. */
|
|
37
|
-
|
|
39
|
+
/** Admission is sized to the refs batch law so one same-turn wave coalesces
|
|
40
|
+
* into one refs frame and the span fetcher merges each pack's members into a
|
|
41
|
+
* few broad ranged reads. Memory is bounded by the byte laws below, never by
|
|
42
|
+
* this count. */
|
|
43
|
+
const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = CONTENT_REFS_BATCH_OBJECTS;
|
|
38
44
|
// Opening one protected object transiently retains the fetched envelope,
|
|
39
45
|
// WebCrypto's ciphertext-with-tag input, and the plaintext. Bound plaintext
|
|
40
46
|
// to two full blocks so those three representations stay below 96 MiB even
|
|
41
47
|
// on the 1 GiB runtimes used for ordinary Files materialization. Small
|
|
42
48
|
// objects still use the full request-concurrency window.
|
|
43
49
|
const DIRECT_OBJECT_DOWNLOAD_MAX_IN_FLIGHT_BYTES = 2 * CHUNK_BYTES;
|
|
44
|
-
/** Graph pages are at most a few megabytes each and a whole account is a
|
|
45
|
-
* handful of them, so a narrow lane keeps the cold read fast without holding
|
|
46
|
-
* the whole graph twice while it assembles. */
|
|
47
|
-
const GRAPH_PAGE_READ_CONCURRENCY = 8;
|
|
48
50
|
const DETECT_QUIET_MS = 12;
|
|
49
51
|
const DETECT_MAX_DEFERRAL_MS = 75;
|
|
50
52
|
const DETECT_RETRY_MS = 250;
|
|
@@ -94,6 +96,9 @@ export class UserGroundHost {
|
|
|
94
96
|
watchDirty = false;
|
|
95
97
|
cloudState = null;
|
|
96
98
|
converged = false;
|
|
99
|
+
/** Cumulative graph-page reads, reported as diffs on the stages that
|
|
100
|
+
* materialize roots so the ledger shows what each operation actually read. */
|
|
101
|
+
graphPageReads = { fetchedPages: 0, fetchedBytes: 0, cachedPages: 0 };
|
|
97
102
|
syncing = null;
|
|
98
103
|
recordRail = null;
|
|
99
104
|
recordStore = null;
|
|
@@ -159,7 +164,7 @@ export class UserGroundHost {
|
|
|
159
164
|
const local = this.localPort(this.activeIdentity);
|
|
160
165
|
const result = await convergeUserGround(this.activeIdentity, {
|
|
161
166
|
sha256Hex,
|
|
162
|
-
cloud: this.cloudPort(this.activeIdentity),
|
|
167
|
+
cloud: this.cloudPort(this.activeIdentity, { kind: "core" }),
|
|
163
168
|
local,
|
|
164
169
|
});
|
|
165
170
|
await this.resumeWorkspaceAdds(this.activeIdentity);
|
|
@@ -302,13 +307,16 @@ export class UserGroundHost {
|
|
|
302
307
|
});
|
|
303
308
|
const add = {
|
|
304
309
|
sha256Hex,
|
|
305
|
-
lookupRegistry: async () => {
|
|
310
|
+
lookupRegistry: async (workspaceId) => {
|
|
306
311
|
const started = performance.now();
|
|
307
312
|
this.stage({
|
|
308
313
|
primitive: "download", stage: "registry-lookup", status: "started",
|
|
309
314
|
});
|
|
315
|
+
const pagesBefore = { ...this.graphPageReads };
|
|
316
|
+
const planeBefore = this.direct.metrics();
|
|
310
317
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
311
|
-
const cloud = await this.cloudPort(identity)
|
|
318
|
+
const cloud = await this.cloudPort(identity, { kind: "workspace", workspaceId })
|
|
319
|
+
.lookup(resourceId);
|
|
312
320
|
if (!cloud)
|
|
313
321
|
throw new Error("this user has no cloud entity registry");
|
|
314
322
|
// The authority's active snapshot is the compact base; Add's active
|
|
@@ -322,7 +330,7 @@ export class UserGroundHost {
|
|
|
322
330
|
if (deliveryWatermark < cloud.deliveryWatermark) {
|
|
323
331
|
throw new Error("local delivery cursor is behind the active snapshot watermark");
|
|
324
332
|
}
|
|
325
|
-
const snapshot = snapshotFromRecords(
|
|
333
|
+
const snapshot = snapshotFromRecords(await this.foldScopedPrefix(cloud.snapshot.records, readAcceptedResults(this.databasePath(identity), resourceId, cloud.deliveryWatermark, deliveryWatermark), workspaceId));
|
|
326
334
|
const current = {
|
|
327
335
|
...cloud,
|
|
328
336
|
deliveryWatermark,
|
|
@@ -334,6 +342,13 @@ export class UserGroundHost {
|
|
|
334
342
|
stage: "registry-lookup",
|
|
335
343
|
status: "completed",
|
|
336
344
|
durationMs: performance.now() - started,
|
|
345
|
+
measurements: {
|
|
346
|
+
records: snapshot.records.length,
|
|
347
|
+
fetchedPages: this.graphPageReads.fetchedPages - pagesBefore.fetchedPages,
|
|
348
|
+
fetchedPageBytes: this.graphPageReads.fetchedBytes - pagesBefore.fetchedBytes,
|
|
349
|
+
cachedPages: this.graphPageReads.cachedPages - pagesBefore.cachedPages,
|
|
350
|
+
...physicalPlaneDelta(planeBefore, this.direct.metrics()),
|
|
351
|
+
},
|
|
337
352
|
});
|
|
338
353
|
return current;
|
|
339
354
|
},
|
|
@@ -369,7 +384,7 @@ export class UserGroundHost {
|
|
|
369
384
|
destinationParent: input.destinationParent,
|
|
370
385
|
cloudHead: input.cloudHead,
|
|
371
386
|
deliveryWatermark: input.deliveryWatermark,
|
|
372
|
-
readContent: (artifacts) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }),
|
|
387
|
+
readContent: (artifacts, staged) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }, staged),
|
|
373
388
|
adoptMaterialization: (rootUUID) => {
|
|
374
389
|
const settled = this.adoptCloudDeliveryBaseline(identity, input.deliveryWatermark, rootUUID);
|
|
375
390
|
this.applyRail?.recordsAvailable();
|
|
@@ -478,8 +493,9 @@ export class UserGroundHost {
|
|
|
478
493
|
if (stagedAcrossBlindInterval && hasRootRows(database, intent.resourceId, intent.workspaceId)) {
|
|
479
494
|
// Rows prove that staging was verified before the crash, not that its
|
|
480
495
|
// bytes remained unchanged while no watcher existed. Return this
|
|
481
|
-
// private install to the intent-only stage
|
|
482
|
-
//
|
|
496
|
+
// private install to the intent-only stage; the retried install keeps
|
|
497
|
+
// the staged bytes but re-proves every retained chunk against its
|
|
498
|
+
// manifest before anything becomes visible.
|
|
483
499
|
deleteRootRows(database, intent.resourceId, intent.workspaceId);
|
|
484
500
|
}
|
|
485
501
|
await installCloudWorkspace({
|
|
@@ -496,7 +512,7 @@ export class UserGroundHost {
|
|
|
496
512
|
destinationParent: dirname(intent.destinationPath),
|
|
497
513
|
cloudHead: intent.cloudHead,
|
|
498
514
|
deliveryWatermark: intent.deliveryWatermark,
|
|
499
|
-
readContent: (artifacts) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }),
|
|
515
|
+
readContent: (artifacts, staged) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }, staged),
|
|
500
516
|
adoptMaterialization: (rootUUID) => {
|
|
501
517
|
this.adoptCloudDeliveryBaseline(identity, intent.deliveryWatermark, rootUUID);
|
|
502
518
|
this.applyRail?.recordsAvailable();
|
|
@@ -564,12 +580,29 @@ export class UserGroundHost {
|
|
|
564
580
|
throw stageError("Watch coverage", error);
|
|
565
581
|
}
|
|
566
582
|
const database = this.databasePath(identity);
|
|
567
|
-
|
|
568
|
-
|
|
583
|
+
// Both proofs are root-scoped reads: the boundary's own record from
|
|
584
|
+
// whichever root holds it, and its durable reference from the core root
|
|
585
|
+
// — the only root references lawfully live in.
|
|
586
|
+
const hasRegisteredBoundary = async () => {
|
|
587
|
+
const graph = this.cloudState?.graph;
|
|
588
|
+
if (!graph)
|
|
589
|
+
return false;
|
|
590
|
+
const boundaryRoot = await graph.locate(workspaceId);
|
|
591
|
+
if (boundaryRoot === null)
|
|
592
|
+
return false;
|
|
593
|
+
const boundary = (await graph.recordsFor(boundaryRoot))
|
|
594
|
+
.find((record) => record.uuid === workspaceId);
|
|
595
|
+
if (!boundary || boundary.status !== "active")
|
|
596
|
+
return false;
|
|
597
|
+
const coreRoot = await this.coreRootId(identity, graph);
|
|
598
|
+
if (coreRoot === null)
|
|
599
|
+
return false;
|
|
600
|
+
return (await graph.recordsFor(coreRoot)).some((record) => record.type === "reference"
|
|
569
601
|
&& record.status === "active"
|
|
570
602
|
&& record.payloadVersion === workspaceId);
|
|
603
|
+
};
|
|
571
604
|
if (hasRegisteredBoundaryInDatabase(database, workspaceId)
|
|
572
|
-
&& hasRegisteredBoundary(
|
|
605
|
+
&& await hasRegisteredBoundary()) {
|
|
573
606
|
// Already declared — but Register may only return once cargo closure
|
|
574
607
|
// holds under the current content contract. The graph head can be
|
|
575
608
|
// unchanged while a contract cutover moved its cargo namespace, so
|
|
@@ -751,7 +784,7 @@ export class UserGroundHost {
|
|
|
751
784
|
if (!hasRegisteredBoundaryInDatabase(database, workspaceId)) {
|
|
752
785
|
throw stageError("entity registration", new Error(`workspace ${workspaceId} and its durable reference were not recorded`));
|
|
753
786
|
}
|
|
754
|
-
if (!hasRegisteredBoundary(
|
|
787
|
+
if (!(await hasRegisteredBoundary())) {
|
|
755
788
|
throw stageError("cloud graph commit", new Error(`workspace ${workspaceId} or its durable reference is absent from the committed head`));
|
|
756
789
|
}
|
|
757
790
|
return {
|
|
@@ -1128,7 +1161,7 @@ export class UserGroundHost {
|
|
|
1128
1161
|
cacheDir(identity) {
|
|
1129
1162
|
return join(liveMachineStateDir(this.userRoot(identity), identity.deviceId), "content");
|
|
1130
1163
|
}
|
|
1131
|
-
cloudPort(identity) {
|
|
1164
|
+
cloudPort(identity, scope) {
|
|
1132
1165
|
return {
|
|
1133
1166
|
lookup: async (resourceId) => {
|
|
1134
1167
|
await this.publishPendingSnapshotsBeforeLookup(identity, resourceId);
|
|
@@ -1139,7 +1172,8 @@ export class UserGroundHost {
|
|
|
1139
1172
|
}, ["private.resource.ready", "private.resource.missing"]);
|
|
1140
1173
|
if (frame.type === "private.resource.missing")
|
|
1141
1174
|
return null;
|
|
1142
|
-
const
|
|
1175
|
+
const graph = this.materializeRegistry(identity, frame);
|
|
1176
|
+
const ready = readyFromFrame(frame, graph, await this.selectScope(identity, graph, scope));
|
|
1143
1177
|
this.cloudState = ready.state;
|
|
1144
1178
|
return ready.cloud;
|
|
1145
1179
|
},
|
|
@@ -1154,7 +1188,8 @@ export class UserGroundHost {
|
|
|
1154
1188
|
snapshot_checksum: input.checksum,
|
|
1155
1189
|
snapshot_bytes: bytes.length,
|
|
1156
1190
|
}, ["private.resource.ready"]);
|
|
1157
|
-
const
|
|
1191
|
+
const graph = this.materializeRegistry(identity, frame);
|
|
1192
|
+
const ready = readyFromFrame(frame, graph, await this.selectScope(identity, graph, scope));
|
|
1158
1193
|
const current = ready.cloud;
|
|
1159
1194
|
this.cloudState = ready.state;
|
|
1160
1195
|
const created = frame.duplicate === false;
|
|
@@ -1164,60 +1199,103 @@ export class UserGroundHost {
|
|
|
1164
1199
|
},
|
|
1165
1200
|
};
|
|
1166
1201
|
}
|
|
1167
|
-
/** The authority's stored snapshot as this machine's graph. A head
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
|
|
1202
|
+
/** The authority's stored snapshot as this machine's graph. A head is held
|
|
1203
|
+
* lazily — no page is read until an operation asks for a root — and every
|
|
1204
|
+
* page this machine already cached is a local read, so an unchanged root
|
|
1205
|
+
* never crosses the network twice. */
|
|
1206
|
+
materializeRegistry(identity, frame) {
|
|
1171
1207
|
const resourceId = String(frame.resource_id || "");
|
|
1172
|
-
return registryGraphFromSnapshot(Buffer.from(String(frame.snapshot_b64 || ""), "base64"), (
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1208
|
+
return registryGraphFromSnapshot(Buffer.from(String(frame.snapshot_b64 || ""), "base64"), (ref) => this.readGraphPage(identity, resourceId, ref), sha256Hex);
|
|
1209
|
+
}
|
|
1210
|
+
/** The records a lookup hands to Live: every root's own record — the
|
|
1211
|
+
* discovery set that proves root uniqueness — plus the full records of
|
|
1212
|
+
* exactly the roots the operation acts on. A records-shaped snapshot is a
|
|
1213
|
+
* first publication's own small graph and travels whole, keeping its
|
|
1214
|
+
* records-shape checksum law intact. */
|
|
1215
|
+
async selectScope(identity, graph, scope) {
|
|
1216
|
+
if (graph.head === null)
|
|
1217
|
+
return graph.recordsShapedSnapshot().records;
|
|
1218
|
+
if (scope.kind === "none")
|
|
1219
|
+
return [];
|
|
1220
|
+
const selected = new Set();
|
|
1221
|
+
const core = await this.coreRootId(identity, graph);
|
|
1222
|
+
if (core)
|
|
1223
|
+
selected.add(core);
|
|
1224
|
+
if (scope.kind === "workspace") {
|
|
1225
|
+
const boundaryRoot = await graph.locate(scope.workspaceId);
|
|
1226
|
+
if (boundaryRoot)
|
|
1227
|
+
selected.add(boundaryRoot);
|
|
1228
|
+
}
|
|
1229
|
+
const records = new Map((await graph.rootRecords())
|
|
1230
|
+
.map((record) => [record.uuid, record]));
|
|
1231
|
+
for (const rootId of selected) {
|
|
1232
|
+
for (const record of await graph.recordsFor(rootId))
|
|
1233
|
+
records.set(record.uuid, record);
|
|
1234
|
+
}
|
|
1235
|
+
return [...records.values()];
|
|
1236
|
+
}
|
|
1237
|
+
/** The account's core root: the one active top-level workspace named by the
|
|
1238
|
+
* account email. Registered-boundary references live only there. */
|
|
1239
|
+
async coreRootId(identity, graph) {
|
|
1240
|
+
const email = identity.userEmail.toLowerCase();
|
|
1241
|
+
const core = (await graph.rootRecords()).find((record) => record.status === "active"
|
|
1242
|
+
&& record.parentUUID === null
|
|
1243
|
+
&& record.type === "workspace"
|
|
1244
|
+
&& record.name.toLowerCase() === email);
|
|
1245
|
+
return core?.uuid ?? null;
|
|
1246
|
+
}
|
|
1247
|
+
/** Fold the committed delivery tail over a root-scoped base without losing
|
|
1248
|
+
* a subtree the tail moved across roots. A container the folded boundary
|
|
1249
|
+
* contains only through delivered results may still file unchanged
|
|
1250
|
+
* descendants under the root that used to hold it, so that root's records
|
|
1251
|
+
* join the base and the fold reruns until no probe widens it. Leaf results
|
|
1252
|
+
* travel whole and never probe. */
|
|
1253
|
+
async foldScopedPrefix(base, accepted, boundaryId) {
|
|
1254
|
+
if (accepted.length === 0)
|
|
1255
|
+
return base;
|
|
1256
|
+
const graph = this.cloudState?.graph;
|
|
1257
|
+
if (!graph)
|
|
1258
|
+
throw new Error("cloud state is unavailable for a registry lookup");
|
|
1259
|
+
const probed = new Set(base.map((record) => record.uuid));
|
|
1260
|
+
let widened = base;
|
|
1261
|
+
for (;;) {
|
|
1262
|
+
const folded = recordsAtDeliveryPrefix(widened, accepted);
|
|
1263
|
+
const probes = containedRecords(folded, boundaryId)
|
|
1264
|
+
.filter((record) => !probed.has(record.uuid)
|
|
1265
|
+
&& (record.type === "workspace" || record.type === "folder" || record.type === "repo.git"))
|
|
1266
|
+
.map((record) => record.uuid);
|
|
1267
|
+
if (probes.length === 0)
|
|
1268
|
+
return folded;
|
|
1269
|
+
for (const uuid of probes)
|
|
1270
|
+
probed.add(uuid);
|
|
1271
|
+
const located = await graph.membership(new Set(probes));
|
|
1272
|
+
const merged = new Map(widened.map((record) => [record.uuid, record]));
|
|
1273
|
+
for (const rootId of new Set(located.values())) {
|
|
1274
|
+
for (const record of await graph.recordsFor(rootId)) {
|
|
1275
|
+
merged.set(record.uuid, record);
|
|
1276
|
+
}
|
|
1204
1277
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1278
|
+
if (merged.size === widened.length)
|
|
1279
|
+
return folded;
|
|
1280
|
+
widened = [...merged.values()];
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/** Read one graph page by identity: cached pages come off local disk, the
|
|
1284
|
+
* rest are fetched straight from content storage and cached on the way in.
|
|
1285
|
+
* The graph proves every page against its declared identity as it parses,
|
|
1286
|
+
* so this reader only guards the cache against torn copies. */
|
|
1287
|
+
async readGraphPage(identity, resourceId, ref) {
|
|
1288
|
+
const path = join(graphPageCacheDir(this.cacheDir(identity)), `${ref.sha256}.bin`);
|
|
1289
|
+
const cached = readVerifiedRegistryPage(path, ref, sha256Hex);
|
|
1290
|
+
if (cached) {
|
|
1291
|
+
this.graphPageReads.cachedPages += 1;
|
|
1292
|
+
return cached;
|
|
1293
|
+
}
|
|
1294
|
+
const bytes = await retryContent(() => this.direct.fetchObject(resourceId, { kind: "block", sha256: ref.sha256 }), true);
|
|
1295
|
+
writeVerifiedRegistryPage(path, ref, bytes, sha256Hex);
|
|
1296
|
+
this.graphPageReads.fetchedPages += 1;
|
|
1297
|
+
this.graphPageReads.fetchedBytes += bytes.byteLength;
|
|
1298
|
+
return bytes;
|
|
1221
1299
|
}
|
|
1222
1300
|
localPort(identity) {
|
|
1223
1301
|
const userRoot = this.userRoot(identity);
|
|
@@ -1600,15 +1678,18 @@ export class UserGroundHost {
|
|
|
1600
1678
|
* manifests and blocks alike — shares one wide lane bound, so no artifact
|
|
1601
1679
|
* serializes behind another's plan or seal. Each object is still its own
|
|
1602
1680
|
* storage request. A lone artifact is a set of one. */
|
|
1603
|
-
async downloadContentSet(resourceId, cacheDir, wants, retryAttempts = 10, trace) {
|
|
1681
|
+
async downloadContentSet(resourceId, cacheDir, wants, retryAttempts = 10, trace, staged) {
|
|
1604
1682
|
const unique = new Map(wants.map((want) => [want.artifact.contentHash, want]));
|
|
1605
1683
|
const sole = unique.size === 1 ? [...unique.values()][0]?.artifact : undefined;
|
|
1606
1684
|
const started = performance.now();
|
|
1685
|
+
const planeBefore = this.direct.metrics();
|
|
1607
1686
|
let declaredBytes = 0;
|
|
1608
1687
|
let downloadedBytes = 0;
|
|
1609
1688
|
let downloadedChunks = 0;
|
|
1610
1689
|
let reusedChunks = 0;
|
|
1611
1690
|
let requests = 0;
|
|
1691
|
+
let cacheWriteMs = 0;
|
|
1692
|
+
let stagedArtifacts = 0;
|
|
1612
1693
|
const entityId = trace?.entityId ?? sole?.entityId;
|
|
1613
1694
|
const identifiers = {
|
|
1614
1695
|
...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
@@ -1628,6 +1709,7 @@ export class UserGroundHost {
|
|
|
1628
1709
|
},
|
|
1629
1710
|
});
|
|
1630
1711
|
const downloads = new ContentCacheDownloadSet(cacheDir);
|
|
1712
|
+
const stagedSet = staged ? new StagedContentSet(staged.evidenceDir) : null;
|
|
1631
1713
|
const targets = new Map();
|
|
1632
1714
|
const target = (artifact) => {
|
|
1633
1715
|
const found = targets.get(artifact.contentHash);
|
|
@@ -1662,7 +1744,18 @@ export class UserGroundHost {
|
|
|
1662
1744
|
throw new Error(checked.error);
|
|
1663
1745
|
return checked.value;
|
|
1664
1746
|
})();
|
|
1665
|
-
|
|
1747
|
+
// An Add stages ordinary files directly at their hidden final
|
|
1748
|
+
// destination — except content this machine already holds sealed,
|
|
1749
|
+
// which stays on the cache rail so a warm re-Add remains a local
|
|
1750
|
+
// clone instead of a fresh download.
|
|
1751
|
+
const stagedPaths = stagedSet && staged?.destinations.get(artifact.contentHash);
|
|
1752
|
+
if (stagedPaths && !hasSealedContent(cacheDir, artifact.contentHash, manifest.bytes)) {
|
|
1753
|
+
stagedArtifacts += 1;
|
|
1754
|
+
targets.set(artifact.contentHash, stagedSet.target(artifact, manifest, stagedPaths));
|
|
1755
|
+
}
|
|
1756
|
+
else {
|
|
1757
|
+
targets.set(artifact.contentHash, downloads.target(artifact, manifest));
|
|
1758
|
+
}
|
|
1666
1759
|
return manifest;
|
|
1667
1760
|
},
|
|
1668
1761
|
hasChunk: async (artifact, _manifest, index) => target(artifact).hasChunk(index),
|
|
@@ -1673,10 +1766,14 @@ export class UserGroundHost {
|
|
|
1673
1766
|
return fetchObject({ kind: "block", sha256: chunk.sha256 });
|
|
1674
1767
|
},
|
|
1675
1768
|
putChunk: async (artifact, _manifest, index, bytes) => {
|
|
1769
|
+
const writeStarted = performance.now();
|
|
1676
1770
|
await target(artifact).putChunk(index, bytes);
|
|
1771
|
+
cacheWriteMs += performance.now() - writeStarted;
|
|
1677
1772
|
},
|
|
1678
1773
|
sealArtifact: async (artifact) => {
|
|
1774
|
+
const sealStarted = performance.now();
|
|
1679
1775
|
const sealed = await target(artifact).seal();
|
|
1776
|
+
cacheWriteMs += performance.now() - sealStarted;
|
|
1680
1777
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
1681
1778
|
},
|
|
1682
1779
|
}, {
|
|
@@ -1704,7 +1801,15 @@ export class UserGroundHost {
|
|
|
1704
1801
|
downloadedBytes,
|
|
1705
1802
|
downloadedChunks,
|
|
1706
1803
|
reusedChunks,
|
|
1804
|
+
// `requests` is logical object reads; the plane deltas below are
|
|
1805
|
+
// the physical facts — actual storage GETs, sealed bytes on the
|
|
1806
|
+
// wire, refs round-trips, decrypt and local-write CPU.
|
|
1807
|
+
// `cacheWriteMs` is all local placement time, cache and staged
|
|
1808
|
+
// rails alike; `stagedArtifacts` counts single-write placements.
|
|
1707
1809
|
requests,
|
|
1810
|
+
...physicalPlaneDelta(planeBefore, this.direct.metrics()),
|
|
1811
|
+
cacheWriteMs: Math.round(cacheWriteMs),
|
|
1812
|
+
...(stagedArtifacts > 0 ? { stagedArtifacts } : {}),
|
|
1708
1813
|
},
|
|
1709
1814
|
});
|
|
1710
1815
|
return content;
|
|
@@ -1728,6 +1833,7 @@ export class UserGroundHost {
|
|
|
1728
1833
|
finally {
|
|
1729
1834
|
await Promise.all([...targets.values()].map((open) => open.close()));
|
|
1730
1835
|
downloads.close();
|
|
1836
|
+
stagedSet?.close();
|
|
1731
1837
|
}
|
|
1732
1838
|
}
|
|
1733
1839
|
async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace, knownManifest) {
|
|
@@ -2124,7 +2230,8 @@ export class UserGroundHost {
|
|
|
2124
2230
|
// the roots it rebuilt — so the machine re-reads the authority's head
|
|
2125
2231
|
// before deriving the next boundary on top of it.
|
|
2126
2232
|
if (!this.cloudState) {
|
|
2127
|
-
await this.cloudPort(identity
|
|
2233
|
+
await this.cloudPort(identity, { kind: "none" })
|
|
2234
|
+
.lookup(privateEntityResourceId(identity.userId, sha256Hex));
|
|
2128
2235
|
}
|
|
2129
2236
|
const state = this.cloudState;
|
|
2130
2237
|
if (!state)
|
|
@@ -2155,6 +2262,12 @@ export class UserGroundHost {
|
|
|
2155
2262
|
leg[`${name}Ms`] = performance.now() - started;
|
|
2156
2263
|
return value;
|
|
2157
2264
|
};
|
|
2265
|
+
const timedAsync = async (name, work) => {
|
|
2266
|
+
const started = performance.now();
|
|
2267
|
+
const value = await work();
|
|
2268
|
+
leg[`${name}Ms`] = performance.now() - started;
|
|
2269
|
+
return value;
|
|
2270
|
+
};
|
|
2158
2271
|
if (trace?.journey === "register") {
|
|
2159
2272
|
this.stage({
|
|
2160
2273
|
primitive: "register",
|
|
@@ -2167,7 +2280,7 @@ export class UserGroundHost {
|
|
|
2167
2280
|
.map(portableRecord), readGroundRowsForRoots(this.databasePath(identity), "detection_notebook", publicationRootIds)
|
|
2168
2281
|
.map(portableRecord)));
|
|
2169
2282
|
const accepted = timed("officialRows", () => readAcceptedResults(this.databasePath(identity), state.resourceId, state.deliveryWatermark, deliveryWatermark));
|
|
2170
|
-
const rebuilt =
|
|
2283
|
+
const rebuilt = await timedAsync("merge", () => publicationRoots(state.graph, localRecords, accepted));
|
|
2171
2284
|
const projected = timed("project", () => new Map([...rebuilt].map(([rootId, records]) => [rootId, records.length === 0 ? records : travelingRecords(records, sha256Hex)])));
|
|
2172
2285
|
const traveling = [...projected.values()].flat();
|
|
2173
2286
|
const impossibleGitLeaf = traveling.find((record) => (record.type === "file.text" || record.type === "file.binary" || record.type === "link")
|
|
@@ -2179,7 +2292,7 @@ export class UserGroundHost {
|
|
|
2179
2292
|
// ONE canonical serialization per rebuilt root: the page bytes are the
|
|
2180
2293
|
// journal's payload, the upload's payload, and the head's identities.
|
|
2181
2294
|
// Untouched roots are not serialized, hashed, or journaled at all.
|
|
2182
|
-
const publication = timed("pages", () => state.graph.rebuild(projected
|
|
2295
|
+
const publication = timed("pages", () => state.graph.rebuild(projected));
|
|
2183
2296
|
const checksum = publication.checksum;
|
|
2184
2297
|
const localIds = new Set(localRecords.map((record) => record.uuid));
|
|
2185
2298
|
// Graph membership is never proof of sealed cargo: a record in the cloud
|
|
@@ -5177,7 +5290,7 @@ function recordRootResolver(records) {
|
|
|
5177
5290
|
* touched is rebuilt from the committed graph with those results folded in, so
|
|
5178
5291
|
* the published graph never declares a watermark it has not actually applied.
|
|
5179
5292
|
* Every root outside this map keeps its committed pages untouched. */
|
|
5180
|
-
function publicationRoots(graph, localRecords, accepted) {
|
|
5293
|
+
async function publicationRoots(graph, localRecords, accepted) {
|
|
5181
5294
|
const rebuilt = new Map();
|
|
5182
5295
|
const walked = new Map(localRecords
|
|
5183
5296
|
.filter((record) => record.parentUUID === null)
|
|
@@ -5194,9 +5307,16 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5194
5307
|
for (const [rootId, records] of walked)
|
|
5195
5308
|
rebuilt.set(rootId, records);
|
|
5196
5309
|
}
|
|
5310
|
+
// One page-bounded sweep answers every committed-root question this merge
|
|
5311
|
+
// asks: which root holds each accepted result, and which root still files
|
|
5312
|
+
// each record the walk re-rooted.
|
|
5313
|
+
const located = await graph.membership(new Set([
|
|
5314
|
+
...accepted.map((result) => result.uuid),
|
|
5315
|
+
...localRecords.map((record) => record.uuid),
|
|
5316
|
+
]));
|
|
5197
5317
|
const overlays = new Map();
|
|
5198
5318
|
for (const result of accepted) {
|
|
5199
|
-
const rootId =
|
|
5319
|
+
const rootId = located.get(result.uuid) ?? null;
|
|
5200
5320
|
if (rootId === null || walked.has(rootId))
|
|
5201
5321
|
continue;
|
|
5202
5322
|
const overlay = overlays.get(rootId);
|
|
@@ -5206,7 +5326,8 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5206
5326
|
overlays.set(rootId, [result]);
|
|
5207
5327
|
}
|
|
5208
5328
|
for (const [rootId, results] of overlays) {
|
|
5209
|
-
const byUuid = new Map(graph.recordsFor(rootId)
|
|
5329
|
+
const byUuid = new Map((await graph.recordsFor(rootId))
|
|
5330
|
+
.map((record) => [record.uuid, record]));
|
|
5210
5331
|
for (const result of results)
|
|
5211
5332
|
byUuid.set(result.uuid, result);
|
|
5212
5333
|
// Entity channels are independently ordered, so container membership is
|
|
@@ -5219,7 +5340,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5219
5340
|
if (graph.head === null) {
|
|
5220
5341
|
for (const rootId of graph.rootIds()) {
|
|
5221
5342
|
if (!rebuilt.has(rootId))
|
|
5222
|
-
rebuilt.set(rootId, graph.recordsFor(rootId));
|
|
5343
|
+
rebuilt.set(rootId, await graph.recordsFor(rootId));
|
|
5223
5344
|
}
|
|
5224
5345
|
}
|
|
5225
5346
|
// Re-rooting is a move, not a copy: an enclosing Register walks records the
|
|
@@ -5228,7 +5349,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5228
5349
|
// with no records at all — the rebuild then drops it from the graph.
|
|
5229
5350
|
const vacated = new Map();
|
|
5230
5351
|
for (const record of localRecords) {
|
|
5231
|
-
const rootId =
|
|
5352
|
+
const rootId = located.get(record.uuid) ?? null;
|
|
5232
5353
|
if (rootId === null || walked.has(rootId))
|
|
5233
5354
|
continue;
|
|
5234
5355
|
const gone = vacated.get(rootId);
|
|
@@ -5238,7 +5359,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5238
5359
|
vacated.set(rootId, new Set([record.uuid]));
|
|
5239
5360
|
}
|
|
5240
5361
|
for (const [rootId, gone] of vacated) {
|
|
5241
|
-
const held = rebuilt.get(rootId) ?? graph.recordsFor(rootId);
|
|
5362
|
+
const held = rebuilt.get(rootId) ?? await graph.recordsFor(rootId);
|
|
5242
5363
|
rebuilt.set(rootId, held.filter((record) => !gone.has(record.uuid)));
|
|
5243
5364
|
}
|
|
5244
5365
|
return rebuilt;
|
|
@@ -5336,9 +5457,17 @@ function installCloudGround(input) {
|
|
|
5336
5457
|
})();
|
|
5337
5458
|
}
|
|
5338
5459
|
async function materializeRecords(input) {
|
|
5339
|
-
const { records, rootPath, cacheDir, bindingDir, readContent } = input;
|
|
5460
|
+
const { records, rootPath, cacheDir, bindingDir, staged, readContent } = input;
|
|
5340
5461
|
const repositories = [];
|
|
5341
5462
|
const repositoriesByUuid = new Map();
|
|
5463
|
+
const paths = portablePaths(records, input.boundaryUUID ? new Set([input.boundaryUUID]) : new Set());
|
|
5464
|
+
const destinationOf = (relativePath) => {
|
|
5465
|
+
const destination = relativePath ? join(rootPath, ...relativePath.split("/")) : rootPath;
|
|
5466
|
+
if (!pathWithin(rootPath, destination)) {
|
|
5467
|
+
throw new Error(`cloud entity path escapes its workspace: ${relativePath}`);
|
|
5468
|
+
}
|
|
5469
|
+
return destination;
|
|
5470
|
+
};
|
|
5342
5471
|
// One download set covers everything the wire owes this add up front:
|
|
5343
5472
|
// every ordinary file artifact and every repository transport head, side
|
|
5344
5473
|
// by side under one wide object bound. Only transport parents — links a
|
|
@@ -5346,6 +5475,7 @@ async function materializeRecords(input) {
|
|
|
5346
5475
|
// round is again one set across every chain. Disk application below keeps
|
|
5347
5476
|
// its ordered, validated walk.
|
|
5348
5477
|
const wanted = new Map();
|
|
5478
|
+
const stagedDestinations = staged ? new Map() : null;
|
|
5349
5479
|
const repositoryHeads = [];
|
|
5350
5480
|
for (const record of records) {
|
|
5351
5481
|
if (record.type === "repo.git") {
|
|
@@ -5360,11 +5490,30 @@ async function materializeRecords(input) {
|
|
|
5360
5490
|
}
|
|
5361
5491
|
else if (["file.text", "file.binary", "link"].includes(record.type)) {
|
|
5362
5492
|
const artifact = artifactForRecord(record);
|
|
5363
|
-
if (artifact)
|
|
5364
|
-
|
|
5493
|
+
if (!artifact)
|
|
5494
|
+
continue;
|
|
5495
|
+
wanted.set(artifact.contentHash, artifact);
|
|
5496
|
+
// Links and repository transports keep the cache rail: a link's
|
|
5497
|
+
// small payload is verified there and becomes a symlink, and a
|
|
5498
|
+
// transport is never its final worktree. Ordinary file bytes are
|
|
5499
|
+
// the cold Add's mass, and they land exactly once.
|
|
5500
|
+
if (stagedDestinations && record.type !== "link") {
|
|
5501
|
+
const destination = destinationOf(paths.get(record.uuid));
|
|
5502
|
+
const list = stagedDestinations.get(artifact.contentHash) ?? [];
|
|
5503
|
+
list.push(destination);
|
|
5504
|
+
stagedDestinations.set(artifact.contentHash, list);
|
|
5505
|
+
}
|
|
5365
5506
|
}
|
|
5366
5507
|
}
|
|
5367
|
-
|
|
5508
|
+
if (stagedDestinations) {
|
|
5509
|
+
// A deterministic primary destination keeps resume evidence valid: the
|
|
5510
|
+
// same first path receives the stream on every attempt.
|
|
5511
|
+
for (const list of stagedDestinations.values())
|
|
5512
|
+
list.sort();
|
|
5513
|
+
}
|
|
5514
|
+
const fileContent = await readContent([...wanted.values()], staged && stagedDestinations
|
|
5515
|
+
? { evidenceDir: staged.evidenceDir, destinations: stagedDestinations }
|
|
5516
|
+
: undefined);
|
|
5368
5517
|
const repositoryChains = new Map();
|
|
5369
5518
|
const walking = repositoryHeads.map(({ record, artifact }) => {
|
|
5370
5519
|
const local = fileContent.get(artifact.contentHash);
|
|
@@ -5418,7 +5567,6 @@ async function materializeRecords(input) {
|
|
|
5418
5567
|
repositories.push(repository);
|
|
5419
5568
|
repositoriesByUuid.set(record.uuid, repository);
|
|
5420
5569
|
};
|
|
5421
|
-
const paths = portablePaths(records, input.boundaryUUID ? new Set([input.boundaryUUID]) : new Set());
|
|
5422
5570
|
const recordsByUuid = new Map(records.map((record) => [record.uuid, record]));
|
|
5423
5571
|
const repositoryOwner = (record) => {
|
|
5424
5572
|
let parentUUID = record.parentUUID;
|
|
@@ -5456,29 +5604,59 @@ async function materializeRecords(input) {
|
|
|
5456
5604
|
return leftPath.split("/").length - rightPath.split("/").length
|
|
5457
5605
|
|| (leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0);
|
|
5458
5606
|
});
|
|
5607
|
+
// Staged mode walks ground that may be retained from an interrupted Add:
|
|
5608
|
+
// files were already re-proven by the download, directories are lawful
|
|
5609
|
+
// skeleton, and symlinks and repositories are recreated. Existence alone
|
|
5610
|
+
// therefore no longer proves a record collision — the walk's own claims
|
|
5611
|
+
// do, case- and normalization-folded because filesystems merge such
|
|
5612
|
+
// names into one physical path.
|
|
5613
|
+
const claimed = staged ? new Set() : null;
|
|
5614
|
+
const claim = (relativePath) => {
|
|
5615
|
+
if (!claimed || relativePath === "")
|
|
5616
|
+
return;
|
|
5617
|
+
const key = relativePath.normalize("NFC").toLowerCase();
|
|
5618
|
+
if (claimed.has(key)) {
|
|
5619
|
+
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5620
|
+
}
|
|
5621
|
+
claimed.add(key);
|
|
5622
|
+
};
|
|
5459
5623
|
for (const record of ordered) {
|
|
5460
5624
|
const relativePath = paths.get(record.uuid);
|
|
5461
|
-
const destination =
|
|
5462
|
-
|
|
5463
|
-
throw new Error(`cloud entity path escapes its workspace: ${relativePath}`);
|
|
5464
|
-
}
|
|
5625
|
+
const destination = destinationOf(relativePath);
|
|
5626
|
+
claim(relativePath);
|
|
5465
5627
|
if (relativePath === "") {
|
|
5466
|
-
if (record.type === "repo.git")
|
|
5628
|
+
if (record.type === "repo.git") {
|
|
5629
|
+
if (staged && pathExists(destination)) {
|
|
5630
|
+
rmSync(destination, { recursive: true, force: true });
|
|
5631
|
+
}
|
|
5467
5632
|
await materializeRepository(record, destination);
|
|
5468
|
-
|
|
5633
|
+
}
|
|
5634
|
+
else {
|
|
5469
5635
|
ensureUserDir(destination);
|
|
5636
|
+
}
|
|
5470
5637
|
}
|
|
5471
5638
|
else if (record.type === "folder") {
|
|
5472
|
-
if (pathExists(destination)) {
|
|
5639
|
+
if (!pathExists(destination)) {
|
|
5640
|
+
ensureUserDir(destination);
|
|
5641
|
+
}
|
|
5642
|
+
else if (!staged || repositoryOwner(record)) {
|
|
5473
5643
|
assertRepositoryBoundaryPath(record, relativePath, destination);
|
|
5474
5644
|
}
|
|
5475
5645
|
else {
|
|
5476
|
-
|
|
5646
|
+
const stats = lstatSync(destination);
|
|
5647
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
5648
|
+
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5649
|
+
}
|
|
5477
5650
|
}
|
|
5478
5651
|
}
|
|
5479
5652
|
else if (record.type === "repo.git") {
|
|
5480
5653
|
if (pathExists(destination)) {
|
|
5481
|
-
|
|
5654
|
+
if (!staged || repositoryOwner(record)) {
|
|
5655
|
+
assertRepositoryBoundaryPath(record, relativePath, destination);
|
|
5656
|
+
}
|
|
5657
|
+
else {
|
|
5658
|
+
rmSync(destination, { recursive: true, force: true });
|
|
5659
|
+
}
|
|
5482
5660
|
}
|
|
5483
5661
|
await materializeRepository(record, destination);
|
|
5484
5662
|
}
|
|
@@ -5486,8 +5664,11 @@ async function materializeRecords(input) {
|
|
|
5486
5664
|
if (!record.payloadVersion || !UUID.test(record.payloadVersion)) {
|
|
5487
5665
|
throw new Error(`cloud reference ${record.uuid} has no workspace identity`);
|
|
5488
5666
|
}
|
|
5489
|
-
if (
|
|
5667
|
+
if (staged)
|
|
5668
|
+
rmSync(destination, { force: true });
|
|
5669
|
+
else if (pathExists(destination)) {
|
|
5490
5670
|
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5671
|
+
}
|
|
5491
5672
|
ensureUserDir(dirname(destination));
|
|
5492
5673
|
const binding = join(bindingDir, record.payloadVersion.toLowerCase());
|
|
5493
5674
|
symlinkSync(relative(dirname(destination), binding), destination);
|
|
@@ -5495,21 +5676,41 @@ async function materializeRecords(input) {
|
|
|
5495
5676
|
else if (["file.text", "file.binary", "link"].includes(record.type)) {
|
|
5496
5677
|
if (!record.payloadVersion)
|
|
5497
5678
|
throw new Error(`cloud leaf ${record.uuid} has no content head`);
|
|
5498
|
-
if (pathExists(destination))
|
|
5499
|
-
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5500
5679
|
const artifact = artifactForRecord(record);
|
|
5501
5680
|
if (!artifact)
|
|
5502
5681
|
throw new Error(`cloud leaf ${record.uuid} has no transfer artifact`);
|
|
5503
5682
|
const local = fileContent.get(artifact.contentHash);
|
|
5504
5683
|
if (!local)
|
|
5505
5684
|
throw new Error(`cloud leaf ${record.uuid} content was not downloaded`);
|
|
5506
|
-
|
|
5507
|
-
|
|
5685
|
+
if (record.type === "link") {
|
|
5686
|
+
if (staged)
|
|
5687
|
+
rmSync(destination, { force: true });
|
|
5688
|
+
else if (pathExists(destination)) {
|
|
5689
|
+
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5690
|
+
}
|
|
5691
|
+
ensureUserDir(dirname(destination));
|
|
5508
5692
|
symlinkSync(readFileSync(local.file, "utf8"), destination);
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5693
|
+
}
|
|
5694
|
+
else if (staged && pathWithin(rootPath, local.file)) {
|
|
5695
|
+
// The download already wrote, proved, and fsynced these bytes at
|
|
5696
|
+
// their destination — the single-write law. The walk only proves
|
|
5697
|
+
// the placement still stands.
|
|
5698
|
+
const stats = lstatSync(destination);
|
|
5699
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size !== local.bytes) {
|
|
5700
|
+
throw new Error(`cloud content ${local.contentHash} is corrupt`);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
else {
|
|
5704
|
+
if (staged)
|
|
5705
|
+
rmSync(destination, { force: true });
|
|
5706
|
+
else if (pathExists(destination)) {
|
|
5707
|
+
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
5708
|
+
}
|
|
5709
|
+
ensureUserDir(dirname(destination));
|
|
5710
|
+
// Mode is not portable state: a fresh user file gets 0644, the same
|
|
5711
|
+
// default Apply gives a fresh target, never the cache's private 0600.
|
|
5512
5712
|
atomicCopy(local.file, destination, 0o644);
|
|
5713
|
+
}
|
|
5513
5714
|
}
|
|
5514
5715
|
else {
|
|
5515
5716
|
throw new Error(`materialization for ${record.type} requires its dedicated adapter`);
|
|
@@ -5654,6 +5855,7 @@ async function installCloudWorkspace(input) {
|
|
|
5654
5855
|
throw new Error(`local workspace ${workspace.uuid} has unresolved placement`);
|
|
5655
5856
|
}
|
|
5656
5857
|
renameSync(pending.stagingPath, pending.destinationPath);
|
|
5858
|
+
clearAddStagingEvidence(database, workspace.uuid);
|
|
5657
5859
|
revealed = true;
|
|
5658
5860
|
}
|
|
5659
5861
|
else if (pending && pathExists(pending.stagingPath)) {
|
|
@@ -5661,6 +5863,7 @@ async function installCloudWorkspace(input) {
|
|
|
5661
5863
|
}
|
|
5662
5864
|
if (pending && pathExists(root.absolutePath)) {
|
|
5663
5865
|
refreshRevealedRootEvidence(database, workspace.uuid, root.absolutePath);
|
|
5866
|
+
clearAddStagingEvidence(database, workspace.uuid);
|
|
5664
5867
|
}
|
|
5665
5868
|
if (revealed)
|
|
5666
5869
|
await onStage?.("ground-revealed");
|
|
@@ -5762,9 +5965,9 @@ async function installCloudWorkspace(input) {
|
|
|
5762
5965
|
if (pathExists(binding)) {
|
|
5763
5966
|
throw new Error(`workspace ${workspace.uuid} has a binding without committed entity rows`);
|
|
5764
5967
|
}
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5968
|
+
// Staging retained from an interrupted Add is deliberately kept: the
|
|
5969
|
+
// staged download re-proves every retained chunk against its manifest and
|
|
5970
|
+
// downloads only the gaps. Unverified staged bytes are never trusted.
|
|
5768
5971
|
const selection = workspaceAddSelection(intent);
|
|
5769
5972
|
const materialized = await materializeRecords({
|
|
5770
5973
|
records: intent.records,
|
|
@@ -5772,6 +5975,7 @@ async function installCloudWorkspace(input) {
|
|
|
5772
5975
|
boundaryUUID: intent.workspaceId,
|
|
5773
5976
|
cacheDir,
|
|
5774
5977
|
bindingDir,
|
|
5978
|
+
staged: { evidenceDir: addStagingEvidenceDir(database, intent.workspaceId) },
|
|
5775
5979
|
readContent,
|
|
5776
5980
|
});
|
|
5777
5981
|
const rows = projectMaterializedGraph(materialized.rows, materialized.repositories.map((repository) => ({
|
|
@@ -5788,6 +5992,7 @@ async function installCloudWorkspace(input) {
|
|
|
5788
5992
|
input.adoptMaterialization(intent.workspaceId);
|
|
5789
5993
|
await onStage?.("rows-committed");
|
|
5790
5994
|
renameSync(intent.stagingPath, intent.destinationPath);
|
|
5995
|
+
clearAddStagingEvidence(database, intent.workspaceId);
|
|
5791
5996
|
refreshRevealedRootEvidence(database, intent.workspaceId, intent.destinationPath);
|
|
5792
5997
|
await onStage?.("ground-revealed");
|
|
5793
5998
|
ensureWorkspaceBinding({
|
|
@@ -5814,13 +6019,15 @@ async function installCloudWorkspace(input) {
|
|
|
5814
6019
|
alreadyMaterialized: false,
|
|
5815
6020
|
};
|
|
5816
6021
|
}
|
|
5817
|
-
|
|
6022
|
+
/** The authority's ready frame as Live sees it. The snapshot carries only the
|
|
6023
|
+
* records the caller's scope selected — never a flattened whole account. */
|
|
6024
|
+
function readyFromFrame(frame, graph, records) {
|
|
5818
6025
|
const cloud = {
|
|
5819
6026
|
resourceId: String(frame.resource_id || ""),
|
|
5820
6027
|
version: Number(frame.head_version),
|
|
5821
6028
|
deliveryWatermark: Number(frame.delivery_watermark),
|
|
5822
6029
|
checksum: String(frame.snapshot_checksum || ""),
|
|
5823
|
-
snapshot:
|
|
6030
|
+
snapshot: snapshotFromRecords(records),
|
|
5824
6031
|
...(graph.head ? { head: graph.head } : {}),
|
|
5825
6032
|
};
|
|
5826
6033
|
return {
|
|
@@ -5835,6 +6042,18 @@ function readyFromFrame(frame, graph) {
|
|
|
5835
6042
|
},
|
|
5836
6043
|
};
|
|
5837
6044
|
}
|
|
6045
|
+
/** What one leg physically did on the content plane: the counter movement
|
|
6046
|
+
* between its start and end, ms rounded for the ledger. */
|
|
6047
|
+
function physicalPlaneDelta(before, after) {
|
|
6048
|
+
return {
|
|
6049
|
+
rangedGets: after.rangedGets - before.rangedGets,
|
|
6050
|
+
wholeGets: after.wholeGets - before.wholeGets,
|
|
6051
|
+
fetchedSealedBytes: after.fetchedBytes - before.fetchedBytes,
|
|
6052
|
+
refsFrames: after.refsFrames - before.refsFrames,
|
|
6053
|
+
refsMs: Math.round(after.refsMs - before.refsMs),
|
|
6054
|
+
decryptMs: Math.round(after.decryptMs - before.decryptMs),
|
|
6055
|
+
};
|
|
6056
|
+
}
|
|
5838
6057
|
/** Immutable content reads and writes are idempotent. Retry transient wire or
|
|
5839
6058
|
* authority failures; reads additionally tolerate the brief not-found window
|
|
5840
6059
|
* between an immutable object acknowledgement and globally visible R2 state. */
|