@amalgm/shell 0.1.29 → 0.1.30

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.
@@ -1,17 +1,18 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, watch, writeFileSync, } from "node:fs";
2
+ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
3
3
  import { open } from "node:fs/promises";
4
4
  import { basename, dirname, extname, 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, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, convergeUserGround, createSuspicionScope, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, download as downloadArtifact, deriveUploadManifest, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
6
+ import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, download as downloadArtifact, deriveUploadManifest, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
7
7
  import Database from "better-sqlite3";
8
8
  import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
9
9
  import { ContentCacheDownload, hashContentFile } from "./content-cache-host.js";
10
10
  import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
11
11
  import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
12
- import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, isRepository, } from "./git-repository-host.js";
12
+ import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
13
13
  import { inspectGitRegistration, } from "./git-registration-host.js";
14
14
  import { projectMaterializedGraph } from "./materialized-graph.js";
15
+ import { NodeWatchHost, } from "./watching/index.js";
15
16
  import { WireClient, WireRequestError } from "./wire-client.js";
16
17
  const PORTABLE_FIELDS = [
17
18
  "uuid", "type", "parentUUID", "name", "status", "version",
@@ -25,7 +26,7 @@ const FILE_BATCH_DOWNLOAD_CONCURRENCY = 2;
25
26
  export class UserGroundHost {
26
27
  options;
27
28
  wire;
28
- watchers = new Map();
29
+ watchHost;
29
30
  activeIdentity = null;
30
31
  rescanTimer = null;
31
32
  watchDirty = false;
@@ -41,6 +42,11 @@ export class UserGroundHost {
41
42
  token: options.tunnelToken,
42
43
  runtimeVersion: options.runtimeVersion,
43
44
  });
45
+ this.watchHost = new NodeWatchHost({
46
+ open: options.openWatch,
47
+ onSignal: () => this.scheduleRescan(),
48
+ onHealth: options.onWatchHealth,
49
+ });
44
50
  this.port = {
45
51
  converge: async (identity) => {
46
52
  this.converged = false;
@@ -73,18 +79,30 @@ export class UserGroundHost {
73
79
  rows(identity) {
74
80
  return readRows(this.databasePath(identity));
75
81
  }
76
- /** Open only the authenticated Files-command effects over an already-ready
77
- * local projection. This deliberately does not run login convergence or
78
- * Watch registration before a cold download. */
79
- openFilesCommand(identity) {
80
- const normalized = normalizedIdentity(identity);
81
- const database = this.databasePath(normalized);
82
- if (!existsSync(database) || readRows(database).length === 0) {
83
- throw new Error("Files commands require a materialized local user ground");
82
+ watchHealth() {
83
+ return this.watchHost.health();
84
+ }
85
+ watchEvidence() {
86
+ return this.watchHost.evidence();
87
+ }
88
+ coverWorkspaceEvidence(workspaceId) {
89
+ if (!this.activeIdentity || !this.converged) {
90
+ throw new Error("Watch coverage requires a running authenticated runtime");
84
91
  }
85
- assertDatabaseIdentity(database, normalized);
86
- this.activeIdentity = normalized;
87
- this.converged = true;
92
+ const evidence = this.ensureWatchers(this.activeIdentity);
93
+ const watch = this.watchHost.evidence();
94
+ if (evidence.state !== "healthy") {
95
+ throw new Error(`Watch coverage is degraded: ${evidence.reason ?? "unknown failure"}`);
96
+ }
97
+ assertHealthyWatch(watch, workspaceId);
98
+ return watch;
99
+ }
100
+ /** Add already projects authoritative rows; this proves that the same
101
+ * long-lived runtime now owns their healthy ongoing coverage. */
102
+ addedWorkspaceEvidence(workspaceId) {
103
+ const watch = this.watchHost.evidence();
104
+ assertHealthyWatch(watch, workspaceId);
105
+ return watch;
88
106
  }
89
107
  /** Host effects for Live-owned Files commands. The shell CLI receives these
90
108
  * ports but never decides Files behavior itself. */
@@ -120,8 +138,8 @@ export class UserGroundHost {
120
138
  readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
121
139
  }),
122
140
  coverWorkspace: async () => {
123
- this.ensureWatchers(identity);
124
- return { active: true, roots: this.watchers.size };
141
+ const health = this.ensureWatchers(identity);
142
+ return { active: true, roots: health.contentHandles };
125
143
  },
126
144
  };
127
145
  return { register, add };
@@ -132,8 +150,7 @@ export class UserGroundHost {
132
150
  if (!this.converged || !this.activeIdentity || !this.cloudState)
133
151
  return;
134
152
  const identity = this.activeIdentity;
135
- for (const watcher of this.watchers.values())
136
- watcher.suspicion.suspectAll();
153
+ this.watchHost.suspectAll();
137
154
  this.watchDirty = true;
138
155
  await this.syncNow();
139
156
  if (!this.closing)
@@ -156,7 +173,7 @@ export class UserGroundHost {
156
173
  async flushObservedChanges() {
157
174
  while (true) {
158
175
  const dirty = this.watchDirty
159
- || [...this.watchers.values()].some((watcher) => watcher.suspicion.size > 0);
176
+ || this.watchHost.hasPending;
160
177
  this.watchDirty = false;
161
178
  if (dirty) {
162
179
  await this.syncNow();
@@ -184,25 +201,20 @@ export class UserGroundHost {
184
201
  if (this.rescanTimer)
185
202
  clearTimeout(this.rescanTimer);
186
203
  this.rescanTimer = null;
187
- for (const watcher of this.watchers.values())
188
- watcher.handle.close();
189
- this.watchers.clear();
204
+ this.watchHost.close({ catchUp: false });
190
205
  await this.wire.close();
191
206
  return;
192
207
  }
193
208
  // Command shutdown is the last catch-up boundary. A filesystem callback
194
209
  // may still be queued, so watcher silence cannot be used as evidence yet.
195
- for (const watcher of this.watchers.values())
196
- watcher.suspicion.suspectAll();
197
- this.watchDirty = this.watchers.size > 0 || this.watchDirty;
210
+ this.watchHost.suspectAll();
211
+ this.watchDirty = this.watchHost.health().logicalRoots > 0 || this.watchDirty;
198
212
  this.closing = true;
199
213
  if (this.rescanTimer)
200
214
  clearTimeout(this.rescanTimer);
201
215
  this.rescanTimer = null;
202
216
  await this.flushObservedChanges();
203
- for (const watcher of this.watchers.values())
204
- watcher.handle.close();
205
- this.watchers.clear();
217
+ this.watchHost.close();
206
218
  if (this.rescanTimer)
207
219
  clearTimeout(this.rescanTimer);
208
220
  this.rescanTimer = null;
@@ -285,8 +297,8 @@ export class UserGroundHost {
285
297
  return localValue(identity, userRoot, database);
286
298
  },
287
299
  watch: async () => {
288
- this.ensureWatchers(identity);
289
- return { active: true, roots: this.watchers.size };
300
+ const health = this.ensureWatchers(identity);
301
+ return { active: true, roots: health.contentHandles };
290
302
  },
291
303
  };
292
304
  }
@@ -497,64 +509,57 @@ export class UserGroundHost {
497
509
  }
498
510
  ensureWatchers(identity) {
499
511
  if (this.closing)
500
- return;
512
+ return this.watchHost.health();
501
513
  const root = this.userRoot(identity);
502
- const ignoreFile = join(root, ".amalgmignore");
503
- const policy = createUserGroundEnrollmentPolicy(existsSync(ignoreFile) ? readFileSync(ignoreFile, "utf8") : "");
504
- const wanted = new Map([[root, policy]]);
514
+ const wanted = new Map([[root, {
515
+ rootId: `user:${identity.userId}`,
516
+ directory: root,
517
+ materialized: directoryExists(root),
518
+ excludedSubtrees: [join(root, ".amalgm")],
519
+ }]]);
520
+ const bindings = workspaceBindingDir(root, identity.deviceId);
521
+ if (existsSync(bindings)) {
522
+ for (const entry of readdirSync(bindings, { withFileTypes: true })) {
523
+ if (!entry.isSymbolicLink() || !UUID.test(entry.name))
524
+ continue;
525
+ const binding = join(bindings, entry.name);
526
+ try {
527
+ const directory = realpathSync(binding);
528
+ if (!directoryExists(directory))
529
+ continue;
530
+ wanted.set(directory, {
531
+ rootId: entry.name.toLowerCase(),
532
+ directory,
533
+ materialized: true,
534
+ });
535
+ }
536
+ catch {
537
+ // A missing external target has no trustworthy address until Detect
538
+ // rescues or rebinds it; its prior entity row remains below.
539
+ }
540
+ }
541
+ }
505
542
  for (const row of readRows(this.databasePath(identity))) {
506
543
  if (row.parentUUID !== null || row.absolutePath === root)
507
544
  continue;
508
- try {
509
- if (statSync(row.absolutePath).isDirectory())
510
- wanted.set(row.absolutePath, () => true);
511
- }
512
- catch {
513
- // Unresolved ground keeps its rows but cannot hold a watcher handle.
545
+ const directory = directoryExists(row.absolutePath)
546
+ ? realpathSync(row.absolutePath)
547
+ : resolve(row.absolutePath);
548
+ const prior = wanted.get(directory);
549
+ if (prior && prior.rootId !== row.rootUUID) {
550
+ throw new Error(`two logical roots claim the same Watch ground: ${directory}`);
514
551
  }
515
- }
516
- for (const [directory, watcher] of this.watchers) {
517
- if (!wanted.has(directory)) {
518
- watcher.handle.close();
519
- this.watchers.delete(directory);
520
- }
521
- }
522
- for (const [directory, enrollment] of wanted) {
523
- if (this.watchers.has(directory))
524
- continue;
525
- const suspicion = createSuspicionScope();
526
- const handle = watch(directory, { recursive: true }, (_event, file) => {
527
- const relativePath = String(file || "").split(sep).join("/");
528
- if (relativePath.split("/").includes(".amalgm"))
529
- return;
530
- if (relativePath && !enrollment(relativePath))
531
- return;
532
- const segments = relativePath.split("/");
533
- const git = segments.indexOf(".git");
534
- if (!relativePath || relativePath === ".amalgmignore"
535
- || (git === 0 && segments[1] === "index")) {
536
- suspicion.suspectAll();
537
- }
538
- else if (git > 0 && segments[git + 1] === "index") {
539
- suspicion.ring(segments.slice(0, git).join("/"));
540
- }
541
- else {
542
- suspicion.ring(relativePath);
543
- }
544
- this.scheduleRescan();
552
+ wanted.set(directory, {
553
+ rootId: row.rootUUID,
554
+ directory,
555
+ materialized: directoryExists(directory),
545
556
  });
546
- handle.on("error", () => {
547
- suspicion.suspectAll();
548
- handle.close();
549
- this.watchers.delete(directory);
550
- this.scheduleRescan();
551
- });
552
- this.watchers.set(directory, { handle, suspicion });
553
- // Coverage began after the preceding observation. One catch-up proves
554
- // the unobserved interval before watcher silence can be trusted.
555
- suspicion.suspectAll();
556
- this.scheduleRescan();
557
557
  }
558
+ const health = this.watchHost.reconcile([...wanted.values()]);
559
+ if (health.state !== "healthy" || health.contentHandles < 1) {
560
+ throw new Error(`Watch coverage is degraded: ${health.reason ?? "no content coverage"}`);
561
+ }
562
+ return health;
558
563
  }
559
564
  scheduleRescan() {
560
565
  if (this.closing)
@@ -581,23 +586,18 @@ export class UserGroundHost {
581
586
  const state = this.cloudState;
582
587
  if (!state)
583
588
  throw new Error("cloud state is unavailable for user-ground Watch");
584
- const observations = new Map();
585
- for (const [directory, watcher] of this.watchers) {
586
- observations.set(directory, watcher.suspicion.snapshot());
587
- }
589
+ const observations = this.watchHost.observations();
588
590
  const localRecords = scanAndRegister({
589
591
  identity,
590
592
  userRoot: this.userRoot(identity),
591
593
  database: this.databasePath(identity),
592
594
  cacheDir: this.cacheDir(identity),
593
- suspicions: new Map([...observations].map(([directory, snapshot]) => [directory, snapshot.paths])),
595
+ suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
594
596
  });
595
597
  const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
596
598
  const checksum = sha256Hex(stableJson(snapshot));
597
599
  if (checksum === state.checksum) {
598
- for (const [directory, snapshot] of observations) {
599
- this.watchers.get(directory)?.suspicion.settle(snapshot);
600
- }
600
+ this.watchHost.settle(observations);
601
601
  return;
602
602
  }
603
603
  const database = initializeDatabase(this.databasePath(identity));
@@ -613,9 +613,7 @@ export class UserGroundHost {
613
613
  database.close();
614
614
  }
615
615
  await this.drainOutbox(identity);
616
- for (const [directory, snapshot] of observations) {
617
- this.watchers.get(directory)?.suspicion.settle(snapshot);
618
- }
616
+ this.watchHost.settle(observations);
619
617
  }
620
618
  async drainOutboxBeforeLookup(identity, resourceId) {
621
619
  const pending = readOutbox(this.databasePath(identity));
@@ -686,6 +684,23 @@ function normalizedIdentity(identity) {
686
684
  deviceId: String(identity.deviceId).trim(),
687
685
  };
688
686
  }
687
+ function assertHealthyWatch(evidence, rootId) {
688
+ const root = evidence.roots.find((candidate) => candidate.rootId === rootId);
689
+ const content = root?.contentOwner
690
+ ? evidence.content.find((candidate) => candidate.directory === root.contentOwner)
691
+ : undefined;
692
+ const address = root?.contentOwner
693
+ ? evidence.addresses.find((candidate) => candidate.rootIds.includes(rootId))
694
+ : undefined;
695
+ if (evidence.health.state !== "healthy"
696
+ || evidence.health.contentHandles !== evidence.content.length
697
+ || evidence.health.addressHandles !== evidence.addresses.length
698
+ || !root?.materialized
699
+ || !content?.rootIds.includes(rootId)
700
+ || !address) {
701
+ throw new Error(`Watch coverage is not healthy for workspace ${rootId}: ${evidence.health.reason ?? "incomplete handle evidence"}`);
702
+ }
703
+ }
689
704
  function immutableWrite(file, bytes) {
690
705
  ensurePrivateDir(dirname(file));
691
706
  try {
@@ -929,6 +944,14 @@ function fileType(bytes) {
929
944
  function slashPath(path) {
930
945
  return path.split(sep).join("/");
931
946
  }
947
+ function directoryExists(path) {
948
+ try {
949
+ return statSync(path).isDirectory();
950
+ }
951
+ catch {
952
+ return false;
953
+ }
954
+ }
932
955
  function scanAndRegister(input) {
933
956
  const { identity, userRoot, database, cacheDir, suspicions } = input;
934
957
  const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
@@ -984,7 +1007,7 @@ function scanAndRegister(input) {
984
1007
  rootUUID: workspaceId,
985
1008
  rootName: existingRoot?.name || basename(rootPath),
986
1009
  rootPath,
987
- rootType: classifyRegisteredRoot({ repository: isRepository(rootPath) }),
1010
+ rootType: classifyRegisteredRoot({ repository: hasGitMarker(rootPath) }),
988
1011
  database,
989
1012
  cacheDir,
990
1013
  policy: () => true,
@@ -1074,7 +1097,15 @@ function scanRoot(input) {
1074
1097
  ? slashPath(relative(activeRepository.root, absolutePath))
1075
1098
  : null;
1076
1099
  if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
1077
- activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
1100
+ try {
1101
+ activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
1102
+ }
1103
+ catch {
1104
+ // Classification already settled from the marker. If Git cannot
1105
+ // provide clean-index evidence, read current worktree bytes; Card +
1106
+ // Checkpoint capture below remains the transfer retry boundary.
1107
+ activeRepository.evidence = { cleanLeaves: new Map() };
1108
+ }
1078
1109
  }
1079
1110
  const indexed = observe && repositoryPath
1080
1111
  ? activeRepository?.evidence?.cleanLeaves.get(repositoryPath)
@@ -1114,7 +1145,7 @@ function scanRoot(input) {
1114
1145
  }
1115
1146
  }
1116
1147
  else if (stats.isDirectory()) {
1117
- type = classifyDirectory({ repository: isRepository(absolutePath) });
1148
+ type = classifyDirectory({ repository: hasGitMarker(absolutePath) });
1118
1149
  }
1119
1150
  else if (stats.isFile()) {
1120
1151
  if (indexed && indexed.type !== "link") {
@@ -1161,81 +1192,6 @@ function scanRoot(input) {
1161
1192
  }
1162
1193
  };
1163
1194
  visit(rootPath, rootUUID, rootType);
1164
- const repositories = entries.filter((entry) => entry.type === "repo.git");
1165
- const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
1166
- const repositoryOwner = (entry) => repositories
1167
- .filter((repository) => repository.uuid !== entry.uuid
1168
- && properDescendantOf(entry.relativePath, repository.relativePath))
1169
- .sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
1170
- for (const repository of repositories) {
1171
- const repoIdentity = entries
1172
- .filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
1173
- .map((entry) => ({
1174
- path: repository.relativePath
1175
- ? entry.relativePath.slice(repository.relativePath.length + 1)
1176
- : entry.relativePath,
1177
- uuid: entry.uuid,
1178
- type: entry.type,
1179
- payloadVersion: entry.payloadVersion,
1180
- }))
1181
- .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
1182
- const priorRow = existingByPath.get(repository.relativePath);
1183
- let prior = null;
1184
- if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
1185
- const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
1186
- if (existsSync(priorPath)) {
1187
- try {
1188
- const layout = inspectRepositoryTransportFile(priorPath);
1189
- if (layout.stateId === priorRow.payloadVersion) {
1190
- prior = {
1191
- stateId: priorRow.payloadVersion,
1192
- transportVersion: priorRow.transportVersion,
1193
- card: layout.card,
1194
- identityHash: layout.identityHash,
1195
- };
1196
- }
1197
- else {
1198
- rmSync(priorPath, { force: true });
1199
- }
1200
- }
1201
- catch {
1202
- // Content-addressed cache bytes are evidence, not ground. A file
1203
- // that cannot prove the current envelope is a cache miss.
1204
- rmSync(priorPath, { force: true });
1205
- }
1206
- }
1207
- }
1208
- const previousRepositories = [...existingByPath.values()]
1209
- .filter((row) => row.type === "repo.git");
1210
- const previousOwner = (row) => previousRepositories
1211
- .filter((candidate) => candidate.uuid !== row.uuid
1212
- && properDescendantOf(row.relativePath, candidate.relativePath))
1213
- .sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
1214
- const previousIdentity = prior && priorRow
1215
- ? [...existingByPath.values()]
1216
- .filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
1217
- .map((row) => ({
1218
- path: priorRow.relativePath
1219
- ? row.relativePath.slice(priorRow.relativePath.length + 1)
1220
- : row.relativePath,
1221
- uuid: row.uuid,
1222
- type: row.type,
1223
- payloadVersion: row.payloadVersion,
1224
- }))
1225
- .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
1226
- : null;
1227
- const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
1228
- repository.payloadVersion = captured.stateId;
1229
- repository.transportVersion = captured.transportVersion;
1230
- if (captured.bytes) {
1231
- immutableWriteArtifact(cacheDir, {
1232
- entityId: repository.uuid,
1233
- entityType: "repo.git",
1234
- kind: "git",
1235
- contentHash: captured.transportVersion,
1236
- }, captured.bytes);
1237
- }
1238
- }
1239
1195
  const children = new Map();
1240
1196
  for (const entry of entries) {
1241
1197
  if (entry.parentUUID === null)
@@ -1244,7 +1200,7 @@ function scanRoot(input) {
1244
1200
  group.push({ uuid: entry.uuid, name: entry.name });
1245
1201
  children.set(entry.parentUUID, group);
1246
1202
  }
1247
- const rows = entries.map((entry) => {
1203
+ const rowsFromEntries = () => entries.map((entry) => {
1248
1204
  const versionPayload = entry.type === "workspace" || entry.type === "folder"
1249
1205
  ? membershipHash(children.get(entry.uuid) ?? [], sha256Hex)
1250
1206
  : entry.payloadVersion;
@@ -1272,7 +1228,100 @@ function scanRoot(input) {
1272
1228
  inode: entry.inode,
1273
1229
  };
1274
1230
  });
1275
- persistRows(database, identity, resourceId, rootUUID, rows, existingRows);
1231
+ const persistCurrentRows = () => {
1232
+ const rows = rowsFromEntries();
1233
+ persistRows(database, identity, resourceId, rootUUID, rows, existingRows);
1234
+ return rows;
1235
+ };
1236
+ const repositories = entries.filter((entry) => entry.type === "repo.git");
1237
+ const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
1238
+ const repositoryOwner = (entry) => repositories
1239
+ .filter((repository) => repository.uuid !== entry.uuid
1240
+ && properDescendantOf(entry.relativePath, repository.relativePath))
1241
+ .sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
1242
+ try {
1243
+ for (const repository of repositories) {
1244
+ const repoIdentity = entries
1245
+ .filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
1246
+ .map((entry) => ({
1247
+ path: repository.relativePath
1248
+ ? entry.relativePath.slice(repository.relativePath.length + 1)
1249
+ : entry.relativePath,
1250
+ uuid: entry.uuid,
1251
+ type: entry.type,
1252
+ payloadVersion: entry.payloadVersion,
1253
+ }))
1254
+ .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
1255
+ const priorRow = existingByPath.get(repository.relativePath);
1256
+ if (priorRow?.type === "repo.git") {
1257
+ repository.payloadVersion = priorRow.payloadVersion;
1258
+ repository.transportVersion = priorRow.transportVersion;
1259
+ }
1260
+ let prior = null;
1261
+ if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
1262
+ const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
1263
+ if (existsSync(priorPath)) {
1264
+ try {
1265
+ const layout = inspectRepositoryTransportFile(priorPath);
1266
+ if (layout.stateId === priorRow.payloadVersion) {
1267
+ prior = {
1268
+ stateId: priorRow.payloadVersion,
1269
+ transportVersion: priorRow.transportVersion,
1270
+ card: layout.card,
1271
+ identityHash: layout.identityHash,
1272
+ };
1273
+ }
1274
+ else {
1275
+ rmSync(priorPath, { force: true });
1276
+ }
1277
+ }
1278
+ catch {
1279
+ // Content-addressed cache bytes are evidence, not ground. A file
1280
+ // that cannot prove the current envelope is a cache miss.
1281
+ rmSync(priorPath, { force: true });
1282
+ }
1283
+ }
1284
+ }
1285
+ const previousRepositories = [...existingByPath.values()]
1286
+ .filter((row) => row.type === "repo.git");
1287
+ const previousOwner = (row) => previousRepositories
1288
+ .filter((candidate) => candidate.uuid !== row.uuid
1289
+ && properDescendantOf(row.relativePath, candidate.relativePath))
1290
+ .sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
1291
+ const previousIdentity = prior && priorRow
1292
+ ? [...existingByPath.values()]
1293
+ .filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
1294
+ .map((row) => ({
1295
+ path: priorRow.relativePath
1296
+ ? row.relativePath.slice(priorRow.relativePath.length + 1)
1297
+ : row.relativePath,
1298
+ uuid: row.uuid,
1299
+ type: row.type,
1300
+ payloadVersion: row.payloadVersion,
1301
+ }))
1302
+ .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
1303
+ : null;
1304
+ const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
1305
+ repository.payloadVersion = captured.stateId;
1306
+ repository.transportVersion = captured.transportVersion;
1307
+ if (captured.bytes) {
1308
+ immutableWriteArtifact(cacheDir, {
1309
+ entityId: repository.uuid,
1310
+ entityType: "repo.git",
1311
+ kind: "git",
1312
+ contentHash: captured.transportVersion,
1313
+ }, captured.bytes);
1314
+ }
1315
+ }
1316
+ }
1317
+ catch (error) {
1318
+ // Type and identity are filesystem facts, so they settle locally even
1319
+ // when the selected repository rail cannot yet capture its artifact.
1320
+ // Cloud publication remains commit-last because this failure propagates.
1321
+ persistCurrentRows();
1322
+ throw error;
1323
+ }
1324
+ const rows = persistCurrentRows();
1276
1325
  return { records: rows.map((row) => row.record), rows, referenceWorkspaceIds: [...referenceWorkspaceIds] };
1277
1326
  }
1278
1327
  function portablePaths(records) {