@amalgm/shell 0.1.54 → 0.1.56

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.
@@ -0,0 +1,661 @@
1
+ /** Node filesystem, immutable-cache, Git, and Verify effects for Live Apply. */
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, copyFileSync, existsSync, lstatSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, } from "node:fs";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { applyRepositoryIdentity, buildEntityApplyEffects, createUserGroundEnrollmentPolicy, indexRepositoryTerritory, repositoryIdentityHash, } from "@amalgm/live";
6
+ import Database from "better-sqlite3";
7
+ import { ensurePrivateDir } from "./filesystem.js";
8
+ import { applyRepositoryFiles, captureRepository, hasGitMarker, inspectRepositoryTransportFile, } from "./git-repository-host.js";
9
+ import { hashContentFile } from "./content-cache-host.js";
10
+ import { projectMaterializedGraph } from "./materialized-graph.js";
11
+ import { EntityApplySqliteStore } from "./entity-apply-store.js";
12
+ const ROW_SELECT = `
13
+ SELECT resource_id AS resourceId, root_uuid AS rootUUID,
14
+ uuid, type, parent_uuid AS parentUUID, name, status, version,
15
+ payload_version AS payloadVersion, transport_version AS transportVersion,
16
+ relative_path AS relativePath, absolute_path AS absolutePath,
17
+ device_number AS deviceNumber, inode,
18
+ byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
19
+ changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
20
+ content_verified_at_ms AS contentVerifiedAtMs
21
+ FROM detection_notebook`;
22
+ const sha256Hex = (input) => createHash("sha256").update(input).digest("hex");
23
+ const portable = (row) => ({
24
+ uuid: row.uuid,
25
+ type: row.type,
26
+ parentUUID: row.parentUUID,
27
+ name: row.name,
28
+ status: row.status,
29
+ version: row.version,
30
+ payloadVersion: row.payloadVersion,
31
+ transportVersion: row.transportVersion,
32
+ });
33
+ const tryLstat = (path) => {
34
+ try {
35
+ return lstatSync(path);
36
+ }
37
+ catch (error) {
38
+ const code = error.code;
39
+ if (code === "ENOENT" || code === "ENOTDIR")
40
+ return null;
41
+ throw error;
42
+ }
43
+ };
44
+ const exactRecord = (left, right) => left.uuid === right.uuid
45
+ && left.type === right.type
46
+ && left.parentUUID === right.parentUUID
47
+ && left.name === right.name
48
+ && left.status === right.status
49
+ && left.version === right.version
50
+ && left.payloadVersion === right.payloadVersion
51
+ && left.transportVersion === right.transportVersion;
52
+ /** One persistent machine-effect owner for the Apply rail lifetime. */
53
+ export class EntityApplyHost {
54
+ store;
55
+ effects;
56
+ #database;
57
+ #userRoot;
58
+ #cacheDir;
59
+ #bindingDir;
60
+ #readContent;
61
+ #acquireGround;
62
+ #rowByUuid;
63
+ #entityByUuid;
64
+ #rowsUnderRoot;
65
+ #prepared = new Map();
66
+ #groundReleases = new Map();
67
+ constructor(options) {
68
+ this.#database = new Database(options.databaseFile);
69
+ this.#userRoot = options.userRoot;
70
+ this.store = new EntityApplySqliteStore(this.#database, {
71
+ now: options.now,
72
+ onIntentFinished: (intent) => this.#releaseGround(intent),
73
+ isLocallyEligible: (target) => this.#isLocallyEligible(target),
74
+ });
75
+ this.#cacheDir = options.cacheDir;
76
+ this.#bindingDir = options.bindingDir;
77
+ this.#readContent = options.readContent;
78
+ this.#acquireGround = options.acquireGround;
79
+ this.#rowByUuid = this.#database.prepare(`${ROW_SELECT} WHERE uuid = ? LIMIT 1`);
80
+ this.#entityByUuid = this.#database.prepare(`
81
+ SELECT resource_id AS resourceId, root_uuid AS rootUUID,
82
+ uuid, type, parent_uuid AS parentUUID, name, status, version,
83
+ payload_version AS payloadVersion, transport_version AS transportVersion,
84
+ relative_path AS relativePath, absolute_path AS absolutePath,
85
+ device_number AS deviceNumber, inode,
86
+ byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
87
+ changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
88
+ content_verified_at_ms AS contentVerifiedAtMs
89
+ FROM entities WHERE uuid = ? LIMIT 1
90
+ `);
91
+ this.#rowsUnderRoot = this.#database.prepare(`
92
+ SELECT resource_id AS resourceId, root_uuid AS rootUUID,
93
+ uuid, type, parent_uuid AS parentUUID, name, status, version,
94
+ payload_version AS payloadVersion, transport_version AS transportVersion,
95
+ relative_path AS relativePath, absolute_path AS absolutePath,
96
+ device_number AS deviceNumber, inode,
97
+ byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
98
+ changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
99
+ content_verified_at_ms AS contentVerifiedAtMs
100
+ FROM entities WHERE root_uuid = ? ORDER BY relative_path
101
+ `);
102
+ const adapters = [
103
+ "workspace", "folder", "repo.git", "file.text", "file.binary", "link", "reference",
104
+ ].map((type) => this.#adapter(type));
105
+ this.effects = buildEntityApplyEffects(adapters, options.captureLocal);
106
+ }
107
+ close() {
108
+ for (const release of this.#groundReleases.values())
109
+ release();
110
+ this.#groundReleases.clear();
111
+ this.store.close();
112
+ }
113
+ #adapter(type) {
114
+ const apply = Object.freeze({
115
+ prepare: (intent, plan) => this.#prepare(intent, plan),
116
+ guard: (intent, plan) => this.#guard(intent, plan),
117
+ reveal: (intent, plan) => this.#reveal(intent, plan),
118
+ discard: (intent, plan) => this.#discard(intent, plan),
119
+ });
120
+ const verify = Object.freeze({
121
+ verify: (intent, plan) => this.#verify(intent, plan),
122
+ });
123
+ return Object.freeze({
124
+ name: `${type}-apply`,
125
+ types: Object.freeze({ [type]: `${type} truth is reread from its native filesystem representation` }),
126
+ rails: Object.freeze({ apply, verify }),
127
+ });
128
+ }
129
+ /** The portable Inbox contains cloud truth for every entity. Shell alone
130
+ * decides whether that accepted entity belongs on this machine. External
131
+ * selected roots are fully materialized; core-ground descendants obey the
132
+ * portable `.amalgmignore` policy before any Apply intent or disk write. */
133
+ #isLocallyEligible(target) {
134
+ const context = this.#context(target);
135
+ const boundary = context.current ?? context.parent;
136
+ if (!boundary)
137
+ return false;
138
+ const root = this.#current(boundary.rootUUID);
139
+ if (!root || root.absolutePath !== this.#userRoot)
140
+ return true;
141
+ const relativePath = relative(this.#userRoot, context.targetPath).split("\\").join("/");
142
+ if (!relativePath)
143
+ return true;
144
+ const ignore = join(this.#userRoot, ".amalgmignore");
145
+ const policy = createUserGroundEnrollmentPolicy(existsSync(ignore) ? readFileSync(ignore, "utf8") : "");
146
+ return policy(relativePath);
147
+ }
148
+ async #prepare(intent, plan) {
149
+ await this.#ensurePrepared(intent, plan);
150
+ }
151
+ async #ensurePrepared(intent, plan) {
152
+ const key = this.#key(intent);
153
+ const existing = this.#prepared.get(key);
154
+ if (existing)
155
+ return existing;
156
+ let prepared = {};
157
+ if (plan.kind === "file.text" || plan.kind === "file.binary" || plan.kind === "link") {
158
+ prepared = { artifact: await this.#readContent(plan.artifact) };
159
+ }
160
+ else if (plan.kind === "structure" && plan.repairArtifact) {
161
+ prepared = { artifact: await this.#readContent(plan.repairArtifact) };
162
+ }
163
+ else if (plan.kind === "repo.git") {
164
+ prepared = { repository: await this.#repositoryChain(plan.artifact) };
165
+ }
166
+ this.#prepared.set(key, prepared);
167
+ return prepared;
168
+ }
169
+ async #guard(intent, plan) {
170
+ await this.#holdGround(intent);
171
+ try {
172
+ const context = this.#context(plan.target);
173
+ const current = context.current;
174
+ if (current
175
+ && exactRecord(portable(current), plan.target)
176
+ && await this.#matches(plan.target, context.targetPath, plan, current)) {
177
+ return { kind: "already-target" };
178
+ }
179
+ // A rail transition can change an entity's registered payload witness
180
+ // without changing its bytes: clean repo leaves use Git object ids,
181
+ // while the ordinary file rail uses SHA-256. At the same durable UUID
182
+ // and address, exact target bytes prove that no write is required; Verify
183
+ // can advance the row directly. Equal bytes at a different address are
184
+ // deliberately not identity evidence.
185
+ if (current?.absolutePath === context.targetPath
186
+ && await this.#matches(plan.target, context.targetPath, plan)) {
187
+ return { kind: "already-target" };
188
+ }
189
+ if (!current) {
190
+ if (!tryLstat(context.targetPath)) {
191
+ if (intent.phase === "declared")
192
+ this.#releaseGround(intent);
193
+ return { kind: "safe" };
194
+ }
195
+ const result = await this.#matches(plan.target, context.targetPath, plan)
196
+ ? { kind: "already-target" }
197
+ : { kind: "local-change" };
198
+ if (result.kind === "local-change")
199
+ this.#releaseGround(intent);
200
+ return result;
201
+ }
202
+ // A move/rename may never erase a different thing already occupying the
203
+ // accepted destination. Detect must first give that ground identity.
204
+ if (current.absolutePath !== context.targetPath && tryLstat(context.targetPath)) {
205
+ this.#releaseGround(intent);
206
+ return { kind: "local-change" };
207
+ }
208
+ if (await this.#matches(portable(current), current.absolutePath, plan, current)) {
209
+ if (intent.phase === "declared")
210
+ this.#releaseGround(intent);
211
+ return { kind: "safe" };
212
+ }
213
+ const displaced = this.#displacedPath(intent, context);
214
+ if (!tryLstat(current.absolutePath)
215
+ && tryLstat(displaced)
216
+ && await this.#matches(portable(current), displaced, plan, current)) {
217
+ if (intent.phase === "declared")
218
+ this.#releaseGround(intent);
219
+ return { kind: "safe" };
220
+ }
221
+ this.#releaseGround(intent);
222
+ return { kind: "local-change" };
223
+ }
224
+ catch (error) {
225
+ this.#releaseGround(intent);
226
+ return { kind: "retry", reason: error instanceof Error ? error.message : String(error) };
227
+ }
228
+ }
229
+ async #reveal(intent, plan) {
230
+ try {
231
+ const context = this.#context(plan.target);
232
+ const prepared = await this.#ensurePrepared(intent, plan);
233
+ if (plan.kind === "lifecycle") {
234
+ this.#removeWithRecovery(intent, context);
235
+ return;
236
+ }
237
+ if (plan.kind === "repo.git") {
238
+ if (context.current && context.current.absolutePath !== context.targetPath) {
239
+ this.#moveWithRecovery(intent, context.current.absolutePath, context.targetPath, context);
240
+ }
241
+ ensurePrivateDir(context.targetPath);
242
+ const repository = prepared.repository;
243
+ if (!repository)
244
+ throw new Error("repository Apply has no prepared transport chain");
245
+ await applyRepositoryFiles(context.targetPath, repository.chain);
246
+ return;
247
+ }
248
+ if (plan.kind === "structure") {
249
+ if (context.current && context.current.absolutePath !== context.targetPath) {
250
+ this.#moveWithRecovery(intent, context.current.absolutePath, context.targetPath, context);
251
+ }
252
+ else if (!tryLstat(context.targetPath)) {
253
+ if (plan.target.type === "workspace" || plan.target.type === "folder") {
254
+ ensurePrivateDir(context.targetPath);
255
+ }
256
+ else if (prepared.artifact) {
257
+ this.#installArtifact(intent, context, plan.target, prepared.artifact);
258
+ }
259
+ else {
260
+ throw new Error("structural leaf repair has no immutable artifact");
261
+ }
262
+ }
263
+ if ((plan.target.type === "workspace" || plan.target.type === "folder")
264
+ && hasGitMarker(context.targetPath)) {
265
+ rmSync(join(context.targetPath, ".git"), { recursive: true, force: true });
266
+ }
267
+ return;
268
+ }
269
+ if (plan.kind === "reference") {
270
+ this.#installLink(intent, context, relative(dirname(context.targetPath), join(this.#bindingDir, plan.targetEntityId)));
271
+ return;
272
+ }
273
+ if (plan.kind === "link") {
274
+ this.#installLink(intent, context, plan.targetText);
275
+ return;
276
+ }
277
+ const artifact = prepared.artifact;
278
+ if (!artifact)
279
+ throw new Error(`${plan.kind} Apply has no prepared immutable artifact`);
280
+ this.#installArtifact(intent, context, plan.target, artifact);
281
+ }
282
+ catch (error) {
283
+ this.#releaseGround(intent);
284
+ throw error;
285
+ }
286
+ }
287
+ async #verify(intent, plan) {
288
+ try {
289
+ const context = this.#context(plan.target);
290
+ if (await this.#restoreRacedLocal(intent, plan, context)) {
291
+ this.#releaseGround(intent);
292
+ return { kind: "local-change" };
293
+ }
294
+ if (!await this.#matches(plan.target, context.targetPath, plan)) {
295
+ this.#releaseGround(intent);
296
+ return { kind: "local-change" };
297
+ }
298
+ if (plan.target.type === "repo.git" && plan.target.status === "active") {
299
+ const repository = await this.#repositoryRows(intent, plan, context);
300
+ this.store.stageVerification(intent, repository.rows, repository.removals);
301
+ }
302
+ else {
303
+ this.store.stageVerification(intent, [this.#verifiedRow(plan.target, context)]);
304
+ }
305
+ return { kind: "match" };
306
+ }
307
+ catch (error) {
308
+ this.#releaseGround(intent);
309
+ return { kind: "retry", reason: error instanceof Error ? error.message : String(error) };
310
+ }
311
+ }
312
+ async #discard(intent, _plan) {
313
+ try {
314
+ this.#prepared.delete(this.#key(intent));
315
+ const current = this.#current(intent.entityId);
316
+ if (!current)
317
+ return;
318
+ rmSync(this.#controlDirectory(intent, {
319
+ current,
320
+ parent: current.parentUUID ? this.#current(current.parentUUID) : null,
321
+ }), { recursive: true, force: true });
322
+ }
323
+ catch (error) {
324
+ this.#releaseGround(intent);
325
+ throw error;
326
+ }
327
+ }
328
+ async #holdGround(intent) {
329
+ const key = this.#key(intent);
330
+ if (this.#groundReleases.has(key))
331
+ return;
332
+ this.#groundReleases.set(key, await this.#acquireGround());
333
+ }
334
+ #releaseGround(intent) {
335
+ const key = this.#key(intent);
336
+ const release = this.#groundReleases.get(key);
337
+ if (!release)
338
+ return;
339
+ this.#groundReleases.delete(key);
340
+ release();
341
+ }
342
+ /**
343
+ * A save can land after the final read guard but before the replacement
344
+ * syscall. Replacement keeps the displaced bytes private; Verify compares
345
+ * those bytes with the last notebook observation. A difference is restored
346
+ * so Live's ordinary Detect + Record capture path can make it durable.
347
+ */
348
+ async #restoreRacedLocal(intent, plan, context) {
349
+ const current = context.current;
350
+ if (!current)
351
+ return false;
352
+ const displaced = this.#displacedPath(intent, context);
353
+ if (!tryLstat(displaced))
354
+ return false;
355
+ if (await this.#matches(portable(current), displaced, plan, current))
356
+ return false;
357
+ const applied = join(this.#controlDirectory(intent, context), "applied");
358
+ rmSync(applied, { recursive: true, force: true });
359
+ if (tryLstat(context.targetPath))
360
+ renameSync(context.targetPath, applied);
361
+ ensurePrivateDir(dirname(current.absolutePath));
362
+ if (tryLstat(current.absolutePath))
363
+ rmSync(current.absolutePath, { recursive: true, force: true });
364
+ renameSync(displaced, current.absolutePath);
365
+ return true;
366
+ }
367
+ #installArtifact(intent, context, target, artifact) {
368
+ const control = this.#controlDirectory(intent, context);
369
+ ensurePrivateDir(control);
370
+ const staged = join(control, "prepared");
371
+ rmSync(staged, { recursive: true, force: true });
372
+ copyFileSync(artifact.file, staged);
373
+ const currentMode = context.current && tryLstat(context.current.absolutePath)?.mode;
374
+ chmodSync(staged, currentMode ? currentMode & 0o777 : 0o644);
375
+ this.#replacePrepared(intent, context, staged);
376
+ }
377
+ #installLink(intent, context, targetText) {
378
+ const control = this.#controlDirectory(intent, context);
379
+ ensurePrivateDir(control);
380
+ const staged = join(control, "prepared");
381
+ rmSync(staged, { recursive: true, force: true });
382
+ symlinkSync(targetText, staged);
383
+ this.#replacePrepared(intent, context, staged);
384
+ }
385
+ #replacePrepared(intent, context, staged) {
386
+ ensurePrivateDir(dirname(context.targetPath));
387
+ const displaced = this.#displacedPath(intent, context);
388
+ if (tryLstat(context.targetPath)) {
389
+ rmSync(displaced, { recursive: true, force: true });
390
+ renameSync(context.targetPath, displaced);
391
+ }
392
+ try {
393
+ renameSync(staged, context.targetPath);
394
+ }
395
+ catch (error) {
396
+ if (!tryLstat(context.targetPath) && tryLstat(displaced)) {
397
+ renameSync(displaced, context.targetPath);
398
+ }
399
+ throw error;
400
+ }
401
+ }
402
+ #moveWithRecovery(intent, source, destination, context) {
403
+ ensurePrivateDir(dirname(destination));
404
+ if (tryLstat(destination)) {
405
+ const displaced = this.#displacedPath(intent, context);
406
+ rmSync(displaced, { recursive: true, force: true });
407
+ renameSync(destination, displaced);
408
+ }
409
+ renameSync(source, destination);
410
+ }
411
+ #removeWithRecovery(intent, context) {
412
+ const source = context.current?.absolutePath ?? context.targetPath;
413
+ if (!tryLstat(source))
414
+ return;
415
+ const displaced = this.#displacedPath(intent, context);
416
+ rmSync(displaced, { recursive: true, force: true });
417
+ ensurePrivateDir(dirname(displaced));
418
+ renameSync(source, displaced);
419
+ }
420
+ async #matches(record, path, plan, stored) {
421
+ const stats = tryLstat(path);
422
+ if (record.status !== "active")
423
+ return stats === null;
424
+ if (!stats)
425
+ return false;
426
+ if (record.type === "file.text" || record.type === "file.binary") {
427
+ return stats.isFile() && !stats.isSymbolicLink()
428
+ && record.payloadVersion !== null
429
+ && await this.#hashFile(path) === record.payloadVersion;
430
+ }
431
+ if (record.type === "link") {
432
+ return stats.isSymbolicLink()
433
+ && record.payloadVersion !== null
434
+ && sha256Hex(Buffer.from(readlinkSync(path), "utf8")) === record.payloadVersion;
435
+ }
436
+ if (record.type === "reference") {
437
+ if (!stats.isSymbolicLink() || !record.payloadVersion)
438
+ return false;
439
+ return readlinkSync(path) === relative(dirname(path), join(this.#bindingDir, record.payloadVersion));
440
+ }
441
+ if (record.type === "repo.git") {
442
+ if (!stats.isDirectory() || !hasGitMarker(path)
443
+ || !record.payloadVersion || !record.transportVersion) {
444
+ return false;
445
+ }
446
+ const identity = plan.kind === "repo.git"
447
+ && (!stored || stored.transportVersion !== record.transportVersion)
448
+ ? (await this.#repositoryChain(plan.artifact)).identity
449
+ : this.#repositoryIdentity(stored ?? this.#current(record.uuid));
450
+ const file = join(this.#cacheDir, `${record.transportVersion}.bin`);
451
+ if (!existsSync(file))
452
+ return false;
453
+ const layout = inspectRepositoryTransportFile(file);
454
+ const prior = {
455
+ stateId: record.payloadVersion,
456
+ transportVersion: record.transportVersion,
457
+ card: layout.card,
458
+ identityHash: layout.identityHash,
459
+ };
460
+ const captured = captureRepository(path, identity, prior, identity);
461
+ return captured.bytes === null
462
+ && captured.stateId === record.payloadVersion
463
+ && captured.transportVersion === record.transportVersion;
464
+ }
465
+ if (!stats.isDirectory() || stats.isSymbolicLink())
466
+ return false;
467
+ if (hasGitMarker(path))
468
+ return false;
469
+ // Containers do not ship or overwrite a child list. Their physical Apply
470
+ // proof is only the accepted address/type/lifecycle result; independently
471
+ // ordered child UUIDs establish membership as they materialize.
472
+ return true;
473
+ }
474
+ #verifiedRow(target, context) {
475
+ const prior = context.current ?? context.parent;
476
+ if (!prior)
477
+ throw new Error(`entity ${target.uuid} has no local materialization boundary`);
478
+ const stats = tryLstat(context.targetPath);
479
+ if (target.status === "active" && !stats)
480
+ throw new Error(`entity ${target.uuid} vanished before Verify`);
481
+ const evidence = stats ?? prior;
482
+ return {
483
+ resourceId: prior.resourceId,
484
+ rootUUID: prior.rootUUID,
485
+ record: target,
486
+ relativePath: target.parentUUID === null
487
+ ? prior.relativePath
488
+ : [context.parent?.relativePath, target.name].filter(Boolean).join("/"),
489
+ absolutePath: context.targetPath,
490
+ deviceNumber: "dev" in evidence ? evidence.dev : evidence.deviceNumber,
491
+ inode: "ino" in evidence ? evidence.ino : evidence.inode,
492
+ byteSize: "size" in evidence ? evidence.size : evidence.byteSize,
493
+ modifiedTimeMs: "mtimeMs" in evidence ? evidence.mtimeMs : evidence.modifiedTimeMs,
494
+ changedTimeMs: "ctimeMs" in evidence ? evidence.ctimeMs : evidence.changedTimeMs,
495
+ filesystemMode: "mode" in evidence ? evidence.mode : evidence.filesystemMode,
496
+ contentVerifiedAtMs: ["file.text", "file.binary", "link", "reference"]
497
+ .includes(target.type) ? Date.now() : null,
498
+ };
499
+ }
500
+ async #repositoryRows(intent, plan, context) {
501
+ if (plan.kind !== "repo.git")
502
+ throw new Error("repository Verify received a non-repository plan");
503
+ const prepared = await this.#ensurePrepared(intent, plan);
504
+ const repository = prepared.repository;
505
+ if (!repository)
506
+ throw new Error("repository Verify has no transport identity");
507
+ const root = this.#verifiedRow(plan.target, context);
508
+ const existing = this.#rowsUnderRoot.all(root.rootUUID);
509
+ const repositoryPrefix = root.relativePath;
510
+ const identityByPath = new Map(repository.identity.map((entry) => [entry.path, entry]));
511
+ const nestedRepositoryPaths = repository.identity
512
+ .filter((entry) => entry.type === "repo.git")
513
+ .map((entry) => entry.path);
514
+ const localRepositoryPath = (row) => {
515
+ if (row.uuid === plan.target.uuid)
516
+ return "";
517
+ if (!repositoryPrefix)
518
+ return row.relativePath;
519
+ return row.relativePath.startsWith(`${repositoryPrefix}/`)
520
+ ? row.relativePath.slice(repositoryPrefix.length + 1)
521
+ : null;
522
+ };
523
+ const removals = existing.filter((row) => {
524
+ const path = localRepositoryPath(row);
525
+ if (path === null || path === "")
526
+ return false;
527
+ if (nestedRepositoryPaths.some((nested) => path.startsWith(`${nested}/`)))
528
+ return false;
529
+ return identityByPath.get(path)?.uuid !== row.uuid;
530
+ }).map((row) => row.uuid);
531
+ const removed = new Set(removals);
532
+ const base = existing.filter((row) => !removed.has(row.uuid)).map((row) => ({
533
+ record: row.uuid === plan.target.uuid ? plan.target : portable(row),
534
+ relativePath: row.uuid === plan.target.uuid ? root.relativePath : row.relativePath,
535
+ absolutePath: row.uuid === plan.target.uuid ? root.absolutePath : row.absolutePath,
536
+ deviceNumber: row.uuid === plan.target.uuid ? root.deviceNumber : row.deviceNumber,
537
+ inode: row.uuid === plan.target.uuid ? root.inode : row.inode,
538
+ byteSize: row.uuid === plan.target.uuid ? root.byteSize ?? 0 : row.byteSize ?? 0,
539
+ modifiedTimeMs: row.uuid === plan.target.uuid ? root.modifiedTimeMs ?? 0 : row.modifiedTimeMs ?? 0,
540
+ changedTimeMs: row.uuid === plan.target.uuid ? root.changedTimeMs ?? 0 : row.changedTimeMs ?? 0,
541
+ filesystemMode: row.uuid === plan.target.uuid ? root.filesystemMode ?? 0 : row.filesystemMode ?? 0,
542
+ contentVerifiedAtMs: row.uuid === plan.target.uuid
543
+ ? root.contentVerifiedAtMs : row.contentVerifiedAtMs,
544
+ }));
545
+ if (!base.some((row) => row.record.uuid === plan.target.uuid))
546
+ base.push({
547
+ record: root.record,
548
+ relativePath: root.relativePath,
549
+ absolutePath: root.absolutePath,
550
+ deviceNumber: root.deviceNumber,
551
+ inode: root.inode,
552
+ byteSize: root.byteSize ?? 0,
553
+ modifiedTimeMs: root.modifiedTimeMs ?? 0,
554
+ changedTimeMs: root.changedTimeMs ?? 0,
555
+ filesystemMode: root.filesystemMode ?? 0,
556
+ contentVerifiedAtMs: root.contentVerifiedAtMs,
557
+ });
558
+ const rows = projectMaterializedGraph(base, [{
559
+ record: plan.target,
560
+ identity: repository.identity,
561
+ }], sha256Hex).map((row) => ({
562
+ resourceId: root.resourceId,
563
+ rootUUID: root.rootUUID,
564
+ ...row,
565
+ }));
566
+ const projectedIdentity = indexRepositoryTerritory(rows.map((row) => ({
567
+ ...row,
568
+ uuid: row.record.uuid,
569
+ type: row.record.type,
570
+ parentUUID: row.record.parentUUID,
571
+ }))).ownedBy(plan.target.uuid)
572
+ .map((row) => ({
573
+ path: root.relativePath
574
+ ? row.relativePath.slice(root.relativePath.length + 1)
575
+ : row.relativePath,
576
+ uuid: row.record.uuid,
577
+ type: row.record.type,
578
+ payloadVersion: row.record.payloadVersion,
579
+ }))
580
+ .sort((left, right) => left.path.localeCompare(right.path));
581
+ if (repositoryIdentityHash(projectedIdentity, sha256Hex)
582
+ !== repositoryIdentityHash(repository.identity, sha256Hex)) {
583
+ throw new Error("repository Apply projection does not reproduce its transport identity map");
584
+ }
585
+ return { rows, removals };
586
+ }
587
+ async #repositoryChain(artifact) {
588
+ const reversed = [];
589
+ const versions = new Set();
590
+ let version = artifact.contentHash;
591
+ while (version) {
592
+ if (versions.has(version))
593
+ throw new Error("repository Apply transport chain has a cycle");
594
+ versions.add(version);
595
+ const local = await this.#readContent({ ...artifact, contentHash: version });
596
+ if (local.contentHash !== version)
597
+ throw new Error("repository Apply cache returned wrong transport");
598
+ reversed.push({ file: local.file, transportVersion: version });
599
+ version = inspectRepositoryTransportFile(local.file).parentTransportVersion;
600
+ }
601
+ const chain = reversed.reverse();
602
+ let identity = null;
603
+ for (const sealed of chain) {
604
+ identity = applyRepositoryIdentity(identity, inspectRepositoryTransportFile(sealed.file), sha256Hex);
605
+ }
606
+ return { chain, identity: identity ?? [] };
607
+ }
608
+ #repositoryIdentity(repository) {
609
+ if (!repository)
610
+ return [];
611
+ const rows = this.#rowsUnderRoot.all(repository.rootUUID);
612
+ return indexRepositoryTerritory(rows).ownedBy(repository.uuid)
613
+ .map((row) => ({
614
+ path: repository.relativePath
615
+ ? row.relativePath.slice(repository.relativePath.length + 1)
616
+ : row.relativePath,
617
+ uuid: row.uuid,
618
+ type: row.type,
619
+ payloadVersion: row.payloadVersion,
620
+ }))
621
+ .sort((left, right) => left.path.localeCompare(right.path));
622
+ }
623
+ #context(target) {
624
+ const current = this.#current(target.uuid);
625
+ const parent = target.parentUUID ? this.#current(target.parentUUID) : null;
626
+ if (target.parentUUID !== null && !parent) {
627
+ throw new Error(`entity ${target.uuid} waits for materialized parent ${target.parentUUID}`);
628
+ }
629
+ const targetPath = parent ? join(parent.absolutePath, target.name) : current?.absolutePath;
630
+ if (!targetPath)
631
+ throw new Error(`root entity ${target.uuid} has no machine binding`);
632
+ return { current, parent, targetPath };
633
+ }
634
+ #current(entityId) {
635
+ return this.#rowByUuid.get(entityId)
636
+ ?? this.#entityByUuid.get(entityId)
637
+ ?? null;
638
+ }
639
+ #root(current) {
640
+ const root = this.#current(current.rootUUID);
641
+ if (!root)
642
+ throw new Error(`entity ${current.uuid} has no materialized root ${current.rootUUID}`);
643
+ return root;
644
+ }
645
+ #controlDirectory(intent, context) {
646
+ const anchor = context.current ?? context.parent;
647
+ if (!anchor)
648
+ throw new Error(`entity ${intent.entityId} has no Apply control root`);
649
+ return join(this.#root(anchor).absolutePath, ".amalgm", "apply", `${intent.entityId}-${intent.throughSequence}`);
650
+ }
651
+ #displacedPath(intent, context) {
652
+ return join(this.#controlDirectory(intent, context), "displaced");
653
+ }
654
+ #key(intent) {
655
+ return `${intent.entityId}:${intent.throughSequence}`;
656
+ }
657
+ async #hashFile(file) {
658
+ return hashContentFile(file);
659
+ }
660
+ }
661
+ //# sourceMappingURL=entity-apply-host.js.map