amalgm 0.1.262 → 0.1.264

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.
@@ -44,6 +44,57 @@ function isRepository(dirPath) {
44
44
  return gitDirTruth(dirPath) === 'present';
45
45
  }
46
46
 
47
+ /**
48
+ * Assign every enrolled worktree entity its permanent UUID BEFORE the
49
+ * repo's transport is packed, so the transport's identity map and the
50
+ * records this walk then creates name the exact same UUIDs. Repo children
51
+ * never travel as individual cloud records (the repo travel law); this
52
+ * map, riding inside the one state parcel, is how a receiving machine
53
+ * binds the same child identities instead of minting competitors.
54
+ *
55
+ * The walk mirrors registerDirectory's classification exactly — same
56
+ * readdir order, same enrollment gate, same lstat shape — and stops at a
57
+ * nested repository boundary: that repo's own capture owns its own map.
58
+ */
59
+ function collectWorktreeIdentity(repoPath, { shouldEnroll, identityFor }) {
60
+ const assignment = new Map(); // repo-relative path -> { uuid, type }
61
+ const walk = (dirPath, relativePath) => {
62
+ let entries;
63
+ try {
64
+ entries = fs.readdirSync(dirPath, { withFileTypes: true })
65
+ .sort((a, b) => a.name.localeCompare(b.name));
66
+ } catch (error) {
67
+ if (error.code === 'EACCES' || error.code === 'EPERM') return;
68
+ throw error;
69
+ }
70
+ for (const entry of entries) {
71
+ const entryPath = path.join(dirPath, entry.name);
72
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
73
+ if (!shouldEnroll(entryPath)) continue;
74
+ let stat;
75
+ try {
76
+ stat = fs.lstatSync(entryPath);
77
+ } catch {
78
+ continue; // raced away between readdir and look; the create-walk decides
79
+ }
80
+ let type;
81
+ if (stat.isSymbolicLink()) type = 'link';
82
+ else if (stat.isDirectory()) type = isRepository(entryPath) ? 'repo.git' : 'folder';
83
+ else if (stat.isFile()) type = 'file.text';
84
+ else continue;
85
+ const uuid = identityFor?.(entryRelativePath) || crypto.randomUUID();
86
+ assignment.set(entryRelativePath, { uuid, type });
87
+ if (type === 'folder') walk(entryPath, entryRelativePath);
88
+ }
89
+ };
90
+ walk(repoPath, '');
91
+ return assignment;
92
+ }
93
+
94
+ function transportIdentity(assignment) {
95
+ return [...assignment].map(([entryPath, { uuid, type }]) => ({ path: entryPath, uuid, type }));
96
+ }
97
+
47
98
  /**
48
99
  * Create the root record, its witness, and every entity below ordinary
49
100
  * directory ground. The caller supplies a root that is already known to the
@@ -90,47 +141,58 @@ function registerTree({ root, registry, memory, shouldEnroll, captureRepo, repoS
90
141
  /** An imported repository already has a verified Repocard artifact in the
91
142
  * local state table before it is bound to Watch. Reuse that exact state;
92
143
  * recapturing would waste a second Git bundle and could race the import.
93
- * Ordinary local registration has no seeded UUID and captures fresh truth. */
94
- function repoStateFor(identity, repoPath) {
144
+ * Ordinary local registration has no seeded UUID and captures fresh truth:
145
+ * the worktree's UUIDs are assigned FIRST so the packed transport's
146
+ * identity map and the created child records agree exactly. */
147
+ function repoStateFor(identity, repoPath, relativePath) {
95
148
  if (identity.uuid) {
96
149
  const imported = repoStateAt?.(identity.uuid);
97
- if (imported?.stateId === identity.payloadVersion) return imported;
150
+ if (imported?.stateId === identity.payloadVersion) return { state: imported, assignment: null };
98
151
  const saved = repoStates.get(identity.uuid);
99
- if (saved?.stateId === identity.payloadVersion) return saved;
152
+ if (saved?.stateId === identity.payloadVersion) return { state: saved, assignment: null };
100
153
  }
101
- return captureInitialRepo(repoPath, captureRepo);
154
+ const assignment = collectWorktreeIdentity(repoPath, {
155
+ shouldEnroll,
156
+ identityFor: (repoRelativePath) => identityAt?.(
157
+ relativePath ? `${relativePath}/${repoRelativePath}` : repoRelativePath,
158
+ )?.uuid || null,
159
+ });
160
+ return {
161
+ state: captureInitialRepo(repoPath, captureRepo, transportIdentity(assignment)),
162
+ assignment,
163
+ };
102
164
  }
103
165
 
104
166
  return registry.transaction(() => {
105
167
  const rootIdentity = seeded('', {
106
168
  type: rootType(root.path, root.kind), parentUUID: null, name: path.basename(root.path), root: true,
107
169
  });
108
- const initialState = root.kind === 'repo' ? repoStateFor(rootIdentity, root.path) : null;
170
+ const initial = root.kind === 'repo' ? repoStateFor(rootIdentity, root.path, '') : null;
109
171
  // A repo's immutable transport head is already held by the injected
110
172
  // cloud-cargo lookup before its entity journal runs. Allocate the UUID,
111
173
  // create the FK parent, then persist its bundle-free state in this same
112
174
  // transaction. A rollback removes both durable facts together.
113
- const rootUuid = rootIdentity.uuid || (initialState ? crypto.randomUUID() : undefined);
175
+ const rootUuid = rootIdentity.uuid || (initial ? crypto.randomUUID() : undefined);
114
176
  const record = registry.enrollRoot({
115
177
  type: rootIdentity.type,
116
178
  name: path.basename(root.path),
117
- payloadVersion: initialState?.stateId ?? rootIdentity.payloadVersion ?? null,
179
+ payloadVersion: initial?.state.stateId ?? rootIdentity.payloadVersion ?? null,
118
180
  uuid: rootUuid,
119
181
  });
120
182
  memory.bind(root.rootId, root.rootId, record.uuid);
121
- if (initialState) repoStates.put(record.uuid, initialState);
183
+ if (initial) repoStates.put(record.uuid, initial.state);
122
184
 
123
185
  if (root.kind === 'files' || root.kind === 'repo') {
124
186
  registerDirectory(root.path, record.uuid, {
125
187
  registry, shouldEnroll, captureRepo, repoStates, identityAt, repoStateAt, repoStateFor, seeded, relativePath: '',
126
- });
188
+ }, initial?.assignment ? { base: '', byPath: initial.assignment } : null);
127
189
  }
128
190
  return record;
129
191
  });
130
192
  }
131
193
 
132
- function captureInitialRepo(repoPath, captureRepo) {
133
- const state = captureRepo(repoPath);
194
+ function captureInitialRepo(repoPath, captureRepo, identityEntries = null) {
195
+ const state = captureRepo(repoPath, { identity: identityEntries });
134
196
  if (state?.busy) {
135
197
  throw new Error(`cannot register ${repoPath}: Git is unsettled (${state.busy})`);
136
198
  }
@@ -142,7 +204,7 @@ function captureInitialRepo(repoPath, captureRepo) {
142
204
 
143
205
  function registerDirectory(dirPath, parentUUID, {
144
206
  registry, shouldEnroll, captureRepo, repoStates, identityAt, repoStateAt, repoStateFor, seeded, relativePath,
145
- }) {
207
+ }, assigned = null) {
146
208
  let entries;
147
209
  try {
148
210
  entries = fs.readdirSync(dirPath, { withFileTypes: true })
@@ -155,6 +217,20 @@ function registerDirectory(dirPath, parentUUID, {
155
217
  throw error;
156
218
  }
157
219
 
220
+ // The UUID a pre-capture assignment reserved for this exact ground. The
221
+ // transport's identity map was packed from these, so the created record
222
+ // must wear the same UUID — unless the ground changed shape between the
223
+ // assignment walk and this one, in which case the walk's truth wins and
224
+ // the map entry is honestly stale.
225
+ const assignedFor = (entryRelativePath, type) => {
226
+ if (!assigned) return undefined;
227
+ const repoRelativePath = assigned.base
228
+ ? entryRelativePath.slice(assigned.base.length + 1)
229
+ : entryRelativePath;
230
+ const seat = assigned.byPath.get(repoRelativePath);
231
+ return seat && seat.type === type ? seat.uuid : undefined;
232
+ };
233
+
158
234
  for (const entry of entries) {
159
235
  const entryPath = path.join(dirPath, entry.name);
160
236
  const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
@@ -173,7 +249,7 @@ function registerDirectory(dirPath, parentUUID, {
173
249
  // Preserve it through the structural bind; normal local registration
174
250
  // still leaves this null until Watch observes the filesystem.
175
251
  payloadVersion: identity.payloadVersion ?? null,
176
- uuid: identity.uuid,
252
+ uuid: identity.uuid ?? assignedFor(entryRelativePath, 'link'),
177
253
  });
178
254
  continue;
179
255
  }
@@ -182,19 +258,27 @@ function registerDirectory(dirPath, parentUUID, {
182
258
  const identity = seeded(entryRelativePath, {
183
259
  type, parentUUID, name: entry.name, root: false,
184
260
  });
185
- const state = type === 'repo.git' ? repoStateFor(identity, entryPath) : null;
186
- const uuid = identity.uuid || (state ? crypto.randomUUID() : undefined);
261
+ const initial = type === 'repo.git' ? repoStateFor(identity, entryPath, entryRelativePath) : null;
262
+ const uuid = identity.uuid
263
+ ?? assignedFor(entryRelativePath, type)
264
+ ?? (initial ? crypto.randomUUID() : undefined);
187
265
  const record = registry.create({
188
266
  type: identity.type,
189
267
  parentUUID,
190
268
  name: entry.name,
191
- payloadVersion: state?.stateId ?? identity.payloadVersion ?? null,
269
+ payloadVersion: initial?.state.stateId ?? identity.payloadVersion ?? null,
192
270
  uuid,
193
271
  });
194
- if (state) repoStates.put(record.uuid, state);
272
+ if (initial) repoStates.put(record.uuid, initial.state);
273
+ // A repository starts its own assignment scope: its transport's map
274
+ // covers exactly its worktree, so its children draw from it — never
275
+ // from an enclosing repo's map.
276
+ const childAssigned = type === 'repo.git'
277
+ ? (initial?.assignment ? { base: entryRelativePath, byPath: initial.assignment } : null)
278
+ : assigned;
195
279
  registerDirectory(entryPath, record.uuid, {
196
280
  registry, shouldEnroll, captureRepo, repoStates, identityAt, repoStateAt, repoStateFor, seeded, relativePath: entryRelativePath,
197
- });
281
+ }, childAssigned);
198
282
  continue;
199
283
  }
200
284
  if (stat.isFile()) {
@@ -206,7 +290,7 @@ function registerDirectory(dirPath, parentUUID, {
206
290
  parentUUID,
207
291
  name: entry.name,
208
292
  payloadVersion: identity.payloadVersion ?? null,
209
- uuid: identity.uuid,
293
+ uuid: identity.uuid ?? assignedFor(entryRelativePath, 'file.text'),
210
294
  });
211
295
  }
212
296
  }
@@ -80,7 +80,8 @@
80
80
  * the folder record and every descendant's derived address follows,
81
81
  * which is why folders reconcile before files, parents before
82
82
  * children;
83
- * - repo territories as repo.git type transitions on the slot's record;
83
+ * - repo territories as observer truth, with an explicitly registered
84
+ * repository boundary supplying the corresponding repo.git record;
84
85
  * - ADOPTION, the channel law completing across trees: unclaimed
85
86
  * ground wearing the physical identity of an orphaned top-level tree
86
87
  * is that tree's arrival — one lawful parent update on its
@@ -604,23 +605,24 @@ function createEvidence(options) {
604
605
  };
605
606
  settleClaims(claimsOf(desiredDirs, 'dir'), 'dir');
606
607
 
607
- // Territories after folder continuity, before file ground: a
608
- // boundary is a repo.git type transition on the slot's record, and
609
- // the slot must already be where its folder channel says it is.
608
+ // Territories after folder continuity, before file ground. A Git
609
+ // boundary is NOT ordinary filesystem evidence: every repo.git record
610
+ // needs a Card + Checkpoint state and immutable transport before it can
611
+ // travel. This generic lens has neither, so it may only recognize a
612
+ // boundary that the registration adapter already made whole. An
613
+ // unregistered repository remains a local observer territory until
614
+ // `amalgm register <repo>` admits it through that adapter.
615
+ //
616
+ // This is deliberately not a convenience gate. Letting this layer
617
+ // mint or promote repo.git would create an identity record whose cloud
618
+ // head names no state — the exact invalid half-repository the cloud
619
+ // contract rejects. Folders and files are evidence-shaped; Git is a
620
+ // type-specific state carrier.
610
621
  for (const territory of [...territories].sort()) {
611
622
  const parentUuid = ensureDir(path.dirname(territory) === '.' ? '' : path.dirname(territory), root);
612
623
  const name = path.basename(territory);
613
624
  const at = registry.activeAt(parentUuid, name);
614
- if (!at) {
615
- // Adoption before birth here too: a registered top-level repo
616
- // TREE that moved in arrives as its own record, never a fresh
617
- // territory record. The enclosed observer root's identity is
618
- // the evidence — the same physical dir the orphaned tree owns.
619
- const enclosed = observer.listRoots().find((row) => row.path === path.join(groundPath, territory));
620
- const arrivedRecord = enclosed ? arrivedTree(enclosed.device, enclosed.inode, REPO_TREE) : null;
621
- if (arrivedRecord) registry.move(arrivedRecord.uuid, { parentUUID: parentUuid, name });
622
- else registry.create({ type: 'repo.git', parentUUID: parentUuid, name });
623
- } else if (at.type !== 'repo.git') registry.changeType(at.uuid, 'repo.git');
625
+ if (at?.type !== 'repo.git') continue;
624
626
  }
625
627
  // A lifted lens is the reverse transition on the same record.
626
628
  for (const { record, rel } of walkActive(root.uuid, '', [])) {
@@ -62,13 +62,50 @@ function remoteList(repoPath) {
62
62
  }).sort((a, b) => (a.name < b.name ? -1 : 1));
63
63
  }
64
64
 
65
- function untrackedFiles(repoPath) {
65
+ /** Is `rel` named by the suspect list — itself, or under a named directory? */
66
+ function makeSuspectTest(suspects) {
67
+ const named = new Set(suspects);
68
+ return (rel) => {
69
+ if (named.has(rel)) return true;
70
+ let prefix = rel;
71
+ for (;;) {
72
+ const cut = prefix.lastIndexOf('/');
73
+ if (cut < 0) return false;
74
+ prefix = prefix.slice(0, cut);
75
+ if (named.has(prefix)) return true;
76
+ }
77
+ };
78
+ }
79
+
80
+ /**
81
+ * The warm half of the checkpoint. Git keeps the Card's ledger itself and
82
+ * its index makes the tracked diffs cheap — the one cost with no cache was
83
+ * untracked files, re-read in full on every capture. With `suspects` (the
84
+ * doorbell's named paths since the last settled state) and the previous
85
+ * state's untracked entries, an unsuspected file's (mode, sha256) is
86
+ * REUSED without opening it: silence from the doorbell is the proof the
87
+ * written answer is still true. Everything else stays a fresh read — a
88
+ * suspect, a new path, or any capture without both inputs (suspects null
89
+ * means "everything is suspect": first capture, flush, catch-up, a command
90
+ * ending). Reused entries carry `content: null`; ids derive from
91
+ * (path, mode, sha256) alone, so a scoped capture's ids are byte-identical
92
+ * to a from-scratch one over the same ground.
93
+ */
94
+ function untrackedFiles(repoPath, { suspects = null, previousUntracked = null } = {}) {
95
+ const reusable = suspects !== null && previousUntracked
96
+ ? new Map(previousUntracked.map((file) => [file.path, file]))
97
+ : null;
98
+ const suspect = reusable ? makeSuspectTest(suspects) : null;
66
99
  const out = runGit(repoPath, ['ls-files', '--others', '--exclude-standard', '-z']).toString('utf8');
67
100
  return out.split('\0').filter(Boolean).sort().flatMap((rel) => {
68
101
  // A trailing slash is git marking a repo boundary it will not enter — a
69
102
  // nested repository or linked worktree. That is its own territory with
70
103
  // its own card; the parent's checkpoint must not contain it.
71
104
  if (rel.endsWith('/')) return [];
105
+ if (reusable && !suspect(rel)) {
106
+ const prev = reusable.get(rel);
107
+ if (prev) return [{ path: rel, mode: prev.mode, content: null, sha256: prev.sha256, reused: true }];
108
+ }
72
109
  const abs = path.join(repoPath, rel);
73
110
  try {
74
111
  const stat = fs.lstatSync(abs);
@@ -123,9 +160,14 @@ function deriveCheckpointId(checkpoint) {
123
160
  }));
124
161
  }
125
162
 
126
- function capture(repoPath, { withBundle = true } = {}) {
163
+ function capture(repoPath, { withBundle = true, suspects = null, previousUntracked = null } = {}) {
127
164
  const busy = busyReason(repoPath);
128
165
  if (busy) return { busy };
166
+ // Untracked reuse is bundle-free (local) captures only: cloud cargo
167
+ // (prepareRepo) consumes untracked CONTENT, and a reused entry carries
168
+ // none. Teaching the cargo path to dedupe by sha256 is the named next
169
+ // step; until then a withBundle capture always reads every byte.
170
+ const scoped = withBundle ? {} : { suspects, previousUntracked };
129
171
 
130
172
  const branch = tryGitText(repoPath, ['symbolic-ref', '-q', '--short', 'HEAD']); // null = detached
131
173
  const commit = tryGitText(repoPath, ['rev-parse', '-q', '--verify', 'HEAD']); // null = unborn
@@ -146,7 +188,7 @@ function capture(repoPath, { withBundle = true } = {}) {
146
188
  // raw non-UTF-8 bytes, and a string conversion would corrupt it silently.
147
189
  stagedPatch: runGit(repoPath, ['diff', '--cached', '--binary', '--full-index', commit ? 'HEAD' : EMPTY_TREE]),
148
190
  unstagedPatch: runGit(repoPath, ['diff', '--binary', '--full-index']),
149
- untracked: untrackedFiles(repoPath),
191
+ untracked: untrackedFiles(repoPath, scoped),
150
192
  };
151
193
 
152
194
  const cardId = deriveCardId(card);
@@ -95,12 +95,24 @@ function createFollower(options) {
95
95
  last = null, // resume seed: { cardId, checkpointId }
96
96
  // May return a promise (e.g. a capture worker): the follower awaits it,
97
97
  // and any doorbell that lands mid-pair re-arms a retry (`pendingWake`).
98
- captureFn = (repo) => capture(repo, { withBundle: false }),
98
+ // Called with { suspects }: the worktree paths rung since the last
99
+ // settled state, or null when everything is suspect.
100
+ captureFn = (repo, { suspects } = {}) => capture(repo, { withBundle: false, suspects }),
99
101
  } = options;
100
102
  if (!repoPath) throw new Error('createFollower requires { repoPath }');
101
103
  if (typeof emit !== 'function') throw new Error('createFollower requires { emit }');
102
104
 
103
105
  let known = last ? { cardId: last.cardId, checkpointId: last.checkpointId } : null;
106
+ // The warm-checkpoint scope: which worktree paths the doorbell has named
107
+ // since the last settled state. Each entry remembers the GENERATION of
108
+ // the ring that named it, so a path rung again mid-pair keeps its
109
+ // suspicion when the pair's success consumes the older hints. A nameless
110
+ // ring — or any entrance that may follow missed doorbells (birth, flush,
111
+ // a command ending) — makes EVERYTHING suspect until a pair settles.
112
+ const SCOPE_CAP = 512;
113
+ const scope = new Map(); // repo-relative path -> generation
114
+ let scopeGen = 0;
115
+ let scopeAllGen = 0; // >0: everything is suspect (set at birth, flush, command end, nameless ring)
104
116
  let owned = 0; // depth of amalgm-owned commands in flight
105
117
  let blocked = null; // lock name while the last capture attempt met a held lock
106
118
  let blockedPath = null;
@@ -210,12 +222,15 @@ function createFollower(options) {
210
222
 
211
223
  // The torn-read guard: two consecutive reads must agree before a state
212
224
  // may travel — a repo mid-mutation cannot produce the same ids twice.
225
+ // Both halves of the pair read the SAME scope: the paths rung so far.
226
+ const genNow = scopeGen;
227
+ const suspects = scopeAllGen > 0 ? null : [...scope.keys()];
213
228
  let first;
214
229
  let second;
215
230
  try {
216
- first = await captureFn(repoPath);
231
+ first = await captureFn(repoPath, { suspects });
217
232
  if (closed || owned > 0) return; // the pair is stale before it finished
218
- second = first && !first.busy ? await captureFn(repoPath) : null;
233
+ second = first && !first.busy ? await captureFn(repoPath, { suspects }) : null;
219
234
  } catch (error) {
220
235
  if (closed) return;
221
236
  // A read tripping over a mid-flight writer (a file vanishing between
@@ -234,11 +249,34 @@ function createFollower(options) {
234
249
  retry(settleMs);
235
250
  return;
236
251
  }
237
- if (!speak(second)) retry(capMs); // consumer failure: patient, loud, never lost
252
+ if (!speak(second)) {
253
+ retry(capMs); // consumer failure: patient, loud, never lost
254
+ return;
255
+ }
256
+ // The settled pair proved the ground the scope described: hints from
257
+ // BEFORE the pair began are consumed; anything rung mid-pair keeps its
258
+ // newer generation and stays suspect for the pendingWake retry.
259
+ if (scopeAllGen > 0 && scopeAllGen <= genNow) scopeAllGen = 0;
260
+ for (const [rel, gen] of scope) {
261
+ if (gen <= genNow) scope.delete(rel);
262
+ }
238
263
  }
239
264
 
240
- function ring() {
265
+ function ring(hint) {
241
266
  if (closed || owned > 0) return; // the command's end is the boundary
267
+ // Accumulate the warm-checkpoint scope. A .git-area hint never touches
268
+ // untracked ground, so it schedules a capture without widening the
269
+ // scope; a nameless hint (or an overflowing scope) suspects everything.
270
+ scopeGen += 1;
271
+ if (hint === null || hint === undefined) {
272
+ scopeAllGen = scopeGen;
273
+ } else if (hint !== '.git' && !hint.startsWith('.git/')) {
274
+ scope.set(hint, scopeGen);
275
+ if (scope.size > SCOPE_CAP) {
276
+ scope.clear();
277
+ scopeAllGen = scopeGen;
278
+ }
279
+ }
242
280
  if (settleTimer) clearTimeout(settleTimer);
243
281
  settleTimer = setTimeout(() => {
244
282
  settleTimer = null;
@@ -262,7 +300,13 @@ function createFollower(options) {
262
300
 
263
301
  function endCommand() {
264
302
  owned = Math.max(0, owned - 1);
265
- if (owned === 0 && !closed) fire();
303
+ // Rings were suppressed while the command ran: its writes are nobody's
304
+ // hints, so the capture at the boundary suspects everything.
305
+ if (owned === 0 && !closed) {
306
+ scopeGen += 1;
307
+ scopeAllGen = scopeGen;
308
+ fire();
309
+ }
266
310
  }
267
311
 
268
312
  /** Run an amalgm-owned command: no captures while it runs, one at its end. */
@@ -284,10 +328,21 @@ function createFollower(options) {
284
328
 
285
329
  return {
286
330
  ring,
287
- /** Birth: capture once, immediately. With a seeded `last`, an unchanged repo is silence. */
288
- start: () => fire(),
289
- /** Consumer-requested capture — the "scanNow" of this layer. */
290
- flush: () => fire(),
331
+ /** Birth: capture once, immediately, suspecting everything — rings
332
+ * before birth were nobody's. With a seeded `last`, an unchanged
333
+ * repo is silence. */
334
+ start: () => {
335
+ scopeGen += 1;
336
+ scopeAllGen = scopeGen;
337
+ return fire();
338
+ },
339
+ /** Consumer-requested capture — the "scanNow" of this layer. Its whole
340
+ * purpose is catching a LOST doorbell, so it never trusts the scope. */
341
+ flush: () => {
342
+ scopeGen += 1;
343
+ scopeAllGen = scopeGen;
344
+ return fire();
345
+ },
291
346
  beginCommand,
292
347
  endCommand,
293
348
  command,
@@ -371,3 +371,93 @@ test('add follows in-order mirrored mutations and refuses after a version gap un
371
371
  fs.rmSync(targetDir, { recursive: true, force: true });
372
372
  }
373
373
  });
374
+
375
+ // --- the repo travel law -----------------------------------------------------
376
+
377
+ function activeCloud({ content = { has: () => true, ensureUpload: () => {} }, repoTransportFor = null } = {}) {
378
+ const db = new Database(':memory:');
379
+ let registry = null;
380
+ const cloud = createEntityCloud({
381
+ db,
382
+ userId: 'user-1',
383
+ deviceId: 'device-1',
384
+ content,
385
+ repoTransportFor,
386
+ recordAt: (uuid) => {
387
+ try { return registry.syncRecord(uuid); } catch { return null; }
388
+ },
389
+ });
390
+ registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
391
+ const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
392
+ const bootstrap = cloud.bootstrap(registry.syncRecords());
393
+ cloud.install({
394
+ registry,
395
+ resourceId: bootstrap.resourceId,
396
+ authorityEpoch: 1,
397
+ headVersion: 0,
398
+ snapshotBase64: bootstrap.snapshotBase64,
399
+ snapshotChecksum: bootstrap.snapshotChecksum,
400
+ });
401
+ return { db, cloud, registry, root, resourceId: bootstrap.resourceId };
402
+ }
403
+
404
+ test('a record enclosed by a repository never journals its own cloud mutation — the repo state parcel is the carrier', () => {
405
+ const transportHead = 'a'.repeat(64);
406
+ const { db, registry, root, resourceId } = activeCloud({
407
+ repoTransportFor: () => transportHead,
408
+ });
409
+
410
+ const repo = registry.create({ type: 'repo.git', parentUUID: root.uuid, name: 'engine', payloadVersion: 'state-1' });
411
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
412
+ 'the repository record itself travels');
413
+
414
+ const folder = registry.create({ type: 'folder', parentUUID: repo.uuid, name: 'src' });
415
+ registry.create({ type: 'file.text', parentUUID: folder.uuid, name: 'index.js', payloadVersion: 'b'.repeat(64) });
416
+ registry.create({ type: 'link', parentUUID: repo.uuid, name: 'current', payloadVersion: 'src' });
417
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
418
+ 'worktree children mail no postcards: a register or a rebase is one parcel on the repo entity');
419
+
420
+ const catalog = db.prepare('SELECT entity_uuid FROM entity_cloud_catalog WHERE resource_id = ? ORDER BY entity_uuid').all(resourceId)
421
+ .map((row) => row.entity_uuid).sort();
422
+ assert.deepEqual(catalog, [root.uuid, repo.uuid].sort(),
423
+ 'the cloud catalog holds the traveling graph only — no rows for repo-enclosed children');
424
+
425
+ // A later Detect correction on the child (type/payload) stays local too.
426
+ const inner = registry.activeAt(folder.uuid, 'index.js');
427
+ registry.attest(inner.uuid, { type: 'file.binary', payloadVersion: 'c'.repeat(64) });
428
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 1,
429
+ 'the census drip is dead: child corrections never reach the outbox');
430
+ });
431
+
432
+ test('an atomic snapshot replacement excludes repo-enclosed records and never demands their bytes', () => {
433
+ const transportHead = 'd'.repeat(64);
434
+ const childHead = 'e'.repeat(64);
435
+ const { db, cloud, registry, root, resourceId } = activeCloud({
436
+ repoTransportFor: () => transportHead,
437
+ content: {
438
+ ensureUpload: () => {},
439
+ has: ({ contentHash }) => {
440
+ assert.notEqual(contentHash, childHead, 'an enclosed child head must never be demanded as separate cloud cargo');
441
+ return true;
442
+ },
443
+ },
444
+ });
445
+
446
+ registry.withoutJournal(() => {
447
+ const repo = registry.create({ type: 'repo.git', parentUUID: root.uuid, name: 'engine', payloadVersion: 'state-1' });
448
+ registry.create({ type: 'file.text', parentUUID: repo.uuid, name: 'inner.md', payloadVersion: childHead });
449
+ registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'loose.md', payloadVersion: 'f'.repeat(64) });
450
+ });
451
+
452
+ cloud.replaceSnapshot(registry.syncRecords());
453
+ const operation = JSON.parse(db.prepare(`
454
+ SELECT operation_json FROM entity_cloud_mutations
455
+ WHERE resource_id = ? ORDER BY sequence DESC LIMIT 1
456
+ `).get(resourceId).operation_json);
457
+ assert.equal(operation.kind, 'entity.snapshot.replace');
458
+ const types = operation.snapshot.records.map((record) => record.type).sort();
459
+ assert.deepEqual(types, ['file.text', 'repo.git', 'workspace'],
460
+ 'the atomic graph carries the workspace, the loose file, and ONE record per repository');
461
+ assert.ok(!operation.snapshot.records.some((record) => record.name === 'inner.md'),
462
+ 'repo children stay out of the published graph');
463
+ });