amalgm 0.1.260 → 0.1.262

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/lib/cli.js CHANGED
@@ -1215,10 +1215,16 @@ function selectEntity(status, selector) {
1215
1215
  const uuid = selector && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(selector)
1216
1216
  ? selector.toLowerCase()
1217
1217
  : null;
1218
+ // A UUID is an entity's globally unique address. Do not let rows without
1219
+ // an optional display path turn that exact address into a fuzzy selector.
1220
+ if (uuid) {
1221
+ const match = entities.find((entity) => entity.uuid === uuid);
1222
+ if (!match) throw new Error(`No cloud-backed entity matches ${selector}.`);
1223
+ return match;
1224
+ }
1218
1225
  const localPath = selector && !uuid ? path.resolve(selector) : null;
1219
- const cloudPath = uuid ? null : String(selector).replace(/^\/+|\/+$/g, '');
1220
- const matches = entities.filter((entity) => entity.uuid === uuid
1221
- || entity.localPath === localPath
1226
+ const cloudPath = String(selector).replace(/^\/+|\/+$/g, '');
1227
+ const matches = entities.filter((entity) => entity.localPath === localPath
1222
1228
  || entity.cloudPath === cloudPath
1223
1229
  || entity.name === selector);
1224
1230
  if (matches.length === 0) throw new Error(`No cloud-backed entity matches ${selector}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.260",
3
+ "version": "0.1.262",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Memoized statement preparation for one database handle.
5
+ *
6
+ * better-sqlite3 compiles the SQL text on EVERY `db.prepare()` call — it
7
+ * keeps no statement cache of its own. The stores' row-level verbs run
8
+ * thousands of times per scan (a first census inserts one row per file),
9
+ * so recompiling identical SQL each call was a measured double-digit
10
+ * share of a large registration. Each store builds one preparer over its
11
+ * injected handle and asks it instead; the SQL string is the whole key,
12
+ * so a handful of table-templated queries memoize to a handful of
13
+ * statements, never an unbounded set.
14
+ */
15
+ function preparer(db) {
16
+ const statements = new Map();
17
+ return (sql) => {
18
+ let statement = statements.get(sql);
19
+ if (!statement) {
20
+ statement = db.prepare(sql);
21
+ statements.set(sql, statement);
22
+ }
23
+ return statement;
24
+ };
25
+ }
26
+
27
+ module.exports = { preparer };
@@ -407,8 +407,10 @@ function createObserver(options) {
407
407
  || linkDiff.renames.length || linkDiff.creates.length || linkDiff.deletes.length
408
408
  || fileDiff.renames.length || fileDiff.creates.length || fileDiff.deletes.length;
409
409
  if (structural) return false;
410
- applyLinkDiff(root, linkDiff); // changes and retags: payload-local facts
411
- applyDiff(root, fileDiff); // checks only: content truth on existing channels
410
+ store.transaction(() => {
411
+ applyLinkDiff(root, linkDiff); // changes and retags: payload-local facts
412
+ applyDiff(root, fileDiff); // checks only: content truth on existing channels
413
+ });
412
414
  return true;
413
415
  }
414
416
 
@@ -448,11 +450,17 @@ function createObserver(options) {
448
450
  : { ...diff, deletes: diff.deletes.filter((row) => !underUnsettled(row.relPath)) });
449
451
  if (unsettled.length > 0 && state) armRetry(root, state);
450
452
 
451
- applyDirDiff(root, concluded(diffDirState(store.dirsForRoot(root.rootId), dirs)));
452
- applyLinkDiff(root, concluded(diffLinkState(store.linksForRoot(root.rootId), links)));
453
- const rows = store.filesForRoot(root.rootId);
454
- const mustCheck = checkAll ? new Set(entries.map((entry) => entry.relPath)) : rung;
455
- applyDiff(root, concluded(diffRootState(rows, entries, mustCheck)));
453
+ // One walk's conclusions are one durable transition: a census over
454
+ // thousands of channels commits once, not once per row. Events stay
455
+ // hints — a consumer told early about a rolled-back row re-reads
456
+ // store truth and finds the old state, which concludes nothing.
457
+ store.transaction(() => {
458
+ applyDirDiff(root, concluded(diffDirState(store.dirsForRoot(root.rootId), dirs)));
459
+ applyLinkDiff(root, concluded(diffLinkState(store.linksForRoot(root.rootId), links)));
460
+ const rows = store.filesForRoot(root.rootId);
461
+ const mustCheck = checkAll ? new Set(entries.map((entry) => entry.relPath)) : rung;
462
+ applyDiff(root, concluded(diffRootState(rows, entries, mustCheck)));
463
+ });
456
464
  }
457
465
 
458
466
  /**
@@ -15,6 +15,8 @@
15
15
  * is durable, or its loss is explicit.
16
16
  */
17
17
 
18
+ const { preparer } = require('../lib/prepared');
19
+
18
20
  const SCHEMA = `
19
21
  CREATE TABLE IF NOT EXISTS observer_roots (
20
22
  root_id TEXT PRIMARY KEY,
@@ -141,6 +143,8 @@ function createStore(db) {
141
143
  db.exec(SCHEMA);
142
144
  db.pragma(`user_version = ${GENERATION}`);
143
145
 
146
+ const prepared = preparer(db);
147
+
144
148
  /**
145
149
  * One directory's immediate child rows — the scoped scan's slice of
146
150
  * memory, served by the (root_id, rel_path) unique index. '0' is the
@@ -150,33 +154,43 @@ function createStore(db) {
150
154
  */
151
155
  function immediateRows(table, rootId, relDir) {
152
156
  if (relDir === '') {
153
- return db.prepare(`SELECT * FROM ${table} WHERE root_id = ? AND instr(rel_path, '/') = 0 ORDER BY rel_path`)
157
+ return prepared(`SELECT * FROM ${table} WHERE root_id = ? AND instr(rel_path, '/') = 0 ORDER BY rel_path`)
154
158
  .all(rootId);
155
159
  }
156
160
  const prefix = `${relDir}/`;
157
- return db.prepare(`SELECT * FROM ${table} WHERE root_id = ? AND rel_path >= ? AND rel_path < ? ORDER BY rel_path`)
161
+ return prepared(`SELECT * FROM ${table} WHERE root_id = ? AND rel_path >= ? AND rel_path < ? ORDER BY rel_path`)
158
162
  .all(rootId, prefix, `${relDir}0`)
159
163
  .filter((row) => !row.rel_path.slice(prefix.length).includes('/'));
160
164
  }
161
165
 
162
166
  return {
167
+ /**
168
+ * One scan's whole apply phase commits as one durable transition.
169
+ * Row-level verbs each pay a full journal cycle when run bare, so a
170
+ * census over thousands of channels was thousands of commits; batched,
171
+ * it is one. Nesting is safe — SQLite savepoints — and a crash
172
+ * mid-batch leaves the previous truth whole, exactly like applyDirDiff.
173
+ */
174
+ transaction(fn) {
175
+ return db.transaction(fn)();
176
+ },
163
177
  addRoot({ rootId, path, kind, device = null, inode = null }) {
164
- db.prepare('INSERT INTO observer_roots (root_id, path, kind, device, inode) VALUES (?, ?, ?, ?, ?)')
178
+ prepared('INSERT INTO observer_roots (root_id, path, kind, device, inode) VALUES (?, ?, ?, ?, ?)')
165
179
  .run(rootId, path, kind, device, inode);
166
180
  },
167
181
  rootByPath(path) {
168
- const row = db.prepare('SELECT * FROM observer_roots WHERE path = ?').get(path);
182
+ const row = prepared('SELECT * FROM observer_roots WHERE path = ?').get(path);
169
183
  return row ? rowToRoot(row) : null;
170
184
  },
171
185
  listRoots() {
172
- return db.prepare('SELECT * FROM observer_roots ORDER BY path').all().map(rowToRoot);
186
+ return prepared('SELECT * FROM observer_roots ORDER BY path').all().map(rowToRoot);
173
187
  },
174
188
  setRootKind(rootId, kind) {
175
- db.prepare('UPDATE observer_roots SET kind = ? WHERE root_id = ?').run(kind, rootId);
189
+ prepared('UPDATE observer_roots SET kind = ? WHERE root_id = ?').run(kind, rootId);
176
190
  },
177
191
  /** Identity persists; local presence is separate evidence. */
178
192
  setRootPresent(rootId, present) {
179
- db.prepare('UPDATE observer_roots SET present = ? WHERE root_id = ?').run(present ? 1 : 0, rootId);
193
+ prepared('UPDATE observer_roots SET present = ? WHERE root_id = ?').run(present ? 1 : 0, rootId);
180
194
  },
181
195
  /**
182
196
  * A root's identity is permanent; its address is not — and a family
@@ -186,7 +200,7 @@ function createStore(db) {
186
200
  * Device and inode are continuity itself and never churn on a rename.
187
201
  */
188
202
  moveRoots(moves) {
189
- const move = db.prepare('UPDATE observer_roots SET path = ? WHERE root_id = ?');
203
+ const move = prepared('UPDATE observer_roots SET path = ? WHERE root_id = ?');
190
204
  db.transaction(() => {
191
205
  for (const { rootId, path } of moves) move.run(path, rootId);
192
206
  })();
@@ -195,18 +209,18 @@ function createStore(db) {
195
209
  * new identity at a known address (see enrollRoot) — the one write
196
210
  * where a root's own device:inode may change. */
197
211
  setRootIdentity(rootId, device, inode) {
198
- db.prepare('UPDATE observer_roots SET device = ?, inode = ? WHERE root_id = ?')
212
+ prepared('UPDATE observer_roots SET device = ?, inode = ? WHERE root_id = ?')
199
213
  .run(device, inode, rootId);
200
214
  },
201
215
  removeRoot(rootId) {
202
- db.prepare('DELETE FROM observer_files WHERE root_id = ?').run(rootId);
203
- db.prepare('DELETE FROM observer_dirs WHERE root_id = ?').run(rootId);
204
- db.prepare('DELETE FROM observer_links WHERE root_id = ?').run(rootId);
205
- db.prepare('DELETE FROM observer_roots WHERE root_id = ?').run(rootId);
216
+ prepared('DELETE FROM observer_files WHERE root_id = ?').run(rootId);
217
+ prepared('DELETE FROM observer_dirs WHERE root_id = ?').run(rootId);
218
+ prepared('DELETE FROM observer_links WHERE root_id = ?').run(rootId);
219
+ prepared('DELETE FROM observer_roots WHERE root_id = ?').run(rootId);
206
220
  },
207
221
 
208
222
  filesForRoot(rootId) {
209
- return db.prepare('SELECT * FROM observer_files WHERE root_id = ? ORDER BY rel_path').all(rootId)
223
+ return prepared('SELECT * FROM observer_files WHERE root_id = ? ORDER BY rel_path').all(rootId)
210
224
  .map(rowToFile);
211
225
  },
212
226
  filesInDir(rootId, relDir) {
@@ -220,11 +234,11 @@ function createStore(db) {
220
234
  },
221
235
  fileByPath(rootId, relPath) {
222
236
  return rowToFile(
223
- db.prepare('SELECT * FROM observer_files WHERE root_id = ? AND rel_path = ?').get(rootId, relPath),
237
+ prepared('SELECT * FROM observer_files WHERE root_id = ? AND rel_path = ?').get(rootId, relPath),
224
238
  );
225
239
  },
226
240
  insertFile(file) {
227
- db.prepare(`
241
+ prepared(`
228
242
  INSERT INTO observer_files (file_id, root_id, rel_path, device, inode, size, mtime_ms, content_hash, is_binary, hashed_at_ms)
229
243
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
230
244
  `).run(
@@ -233,18 +247,18 @@ function createStore(db) {
233
247
  );
234
248
  },
235
249
  updateFile(fileId, { relPath, device, inode, size, mtimeMs, contentHash, binary, hashedAtMs }) {
236
- db.prepare(`
250
+ prepared(`
237
251
  UPDATE observer_files
238
252
  SET rel_path = ?, device = ?, inode = ?, size = ?, mtime_ms = ?, content_hash = ?, is_binary = ?, hashed_at_ms = ?
239
253
  WHERE file_id = ?
240
254
  `).run(relPath, device, inode, size, mtimeMs, contentHash, binary ? 1 : 0, hashedAtMs, fileId);
241
255
  },
242
256
  removeFile(fileId) {
243
- db.prepare('DELETE FROM observer_files WHERE file_id = ?').run(fileId);
257
+ prepared('DELETE FROM observer_files WHERE file_id = ?').run(fileId);
244
258
  },
245
259
 
246
260
  dirsForRoot(rootId) {
247
- return db.prepare('SELECT * FROM observer_dirs WHERE root_id = ? ORDER BY rel_path').all(rootId)
261
+ return prepared('SELECT * FROM observer_dirs WHERE root_id = ? ORDER BY rel_path').all(rootId)
248
262
  .map(rowToDir);
249
263
  },
250
264
  /**
@@ -257,9 +271,9 @@ function createStore(db) {
257
271
  * the next scan re-detects the same batch.
258
272
  */
259
273
  applyDirDiff({ deletes, moves, retags, creates }) {
260
- const remove = db.prepare('DELETE FROM observer_dirs WHERE dir_id = ?');
261
- const insert = db.prepare('INSERT INTO observer_dirs (dir_id, root_id, rel_path, device, inode) VALUES (?, ?, ?, ?, ?)');
262
- const retag = db.prepare('UPDATE observer_dirs SET rel_path = ?, device = ?, inode = ? WHERE dir_id = ?');
274
+ const remove = prepared('DELETE FROM observer_dirs WHERE dir_id = ?');
275
+ const insert = prepared('INSERT INTO observer_dirs (dir_id, root_id, rel_path, device, inode) VALUES (?, ?, ?, ?, ?)');
276
+ const retag = prepared('UPDATE observer_dirs SET rel_path = ?, device = ?, inode = ? WHERE dir_id = ?');
263
277
  db.transaction(() => {
264
278
  for (const dirId of deletes) remove.run(dirId);
265
279
  for (const dir of moves) remove.run(dir.dirId);
@@ -269,7 +283,7 @@ function createStore(db) {
269
283
  })();
270
284
  },
271
285
  removeDir(dirId) {
272
- db.prepare('DELETE FROM observer_dirs WHERE dir_id = ?').run(dirId);
286
+ prepared('DELETE FROM observer_dirs WHERE dir_id = ?').run(dirId);
273
287
  },
274
288
 
275
289
  // Link channels: a pointer's whole payload is its target string, read
@@ -277,19 +291,19 @@ function createStore(db) {
277
291
  // a rename only ever targets a row-free slot), so link rows commit
278
292
  // per-row like files — every commit is a valid last-known truth.
279
293
  linksForRoot(rootId) {
280
- return db.prepare('SELECT * FROM observer_links WHERE root_id = ? ORDER BY rel_path').all(rootId)
294
+ return prepared('SELECT * FROM observer_links WHERE root_id = ? ORDER BY rel_path').all(rootId)
281
295
  .map(rowToLink);
282
296
  },
283
297
  insertLink({ linkId, rootId, relPath, device, inode, target }) {
284
- db.prepare('INSERT INTO observer_links (link_id, root_id, rel_path, device, inode, target) VALUES (?, ?, ?, ?, ?, ?)')
298
+ prepared('INSERT INTO observer_links (link_id, root_id, rel_path, device, inode, target) VALUES (?, ?, ?, ?, ?, ?)')
285
299
  .run(linkId, rootId, relPath, device, inode, target);
286
300
  },
287
301
  updateLink(linkId, { relPath, device, inode, target }) {
288
- db.prepare('UPDATE observer_links SET rel_path = ?, device = ?, inode = ?, target = ? WHERE link_id = ?')
302
+ prepared('UPDATE observer_links SET rel_path = ?, device = ?, inode = ?, target = ? WHERE link_id = ?')
289
303
  .run(relPath, device, inode, target, linkId);
290
304
  },
291
305
  removeLink(linkId) {
292
- db.prepare('DELETE FROM observer_links WHERE link_id = ?').run(linkId);
306
+ prepared('DELETE FROM observer_links WHERE link_id = ?').run(linkId);
293
307
  },
294
308
  };
295
309
  }
@@ -124,7 +124,15 @@ function parseSnapshot(bytes) {
124
124
  * no watcher imports: a cloud record may be known on this machine long before
125
125
  * `amalgm add <uuid>` chooses and validates local ground for it.
126
126
  */
127
- function createEntityCloud({ db, userId, deviceId, content = null, repoStates = null, repoTransportFor = null }) {
127
+ function createEntityCloud({
128
+ db,
129
+ userId,
130
+ deviceId,
131
+ content = null,
132
+ repoStates = null,
133
+ repoTransportFor = null,
134
+ recordAt = null,
135
+ }) {
128
136
  if (!db) throw new Error('createEntityCloud requires an identity database');
129
137
  const ownerUserId = typeof userId === 'string' && userId ? userId : null;
130
138
  const localDeviceId = typeof deviceId === 'string' && deviceId ? deviceId : null;
@@ -155,17 +163,62 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
155
163
  return snapshotFromRecords(records.map(cloudRecord));
156
164
  }
157
165
 
158
- function queueContent(input, resourceId, { queue = false } = {}) {
159
- const record = cloudRecord(input);
160
- const head = contentHead(record);
161
- if (!head) return;
162
- if (!content || typeof content.has !== 'function') {
163
- throw new Error('file entity content is unavailable: registration must capture bytes before it records their cloud head');
166
+ /**
167
+ * Select bytes by the root representation that reconstructs them. A
168
+ * repository's Repocard transport contains its worktree, so child file rows
169
+ * retain their UUID/version facts but never create duplicate cloud cargo.
170
+ * Ordinary workspace files still carry their own content, and a nested
171
+ * repository carries its own transport.
172
+ */
173
+ function contentHeads(records, { fallbackRecordAt = recordAt } = {}) {
174
+ const normalized = (records || []).map(cloudRecord);
175
+ const supplied = new Map(normalized.map((record) => [record.uuid, record]));
176
+ const lookup = (uuid) => {
177
+ const known = supplied.get(uuid);
178
+ if (known) return known;
179
+ const fallback = typeof fallbackRecordAt === 'function' ? fallbackRecordAt(uuid) : null;
180
+ return fallback ? cloudRecord(fallback) : null;
181
+ };
182
+
183
+ function enclosedByRepository(record) {
184
+ const seen = new Set([record.uuid]);
185
+ let parentUUID = record.parentUUID;
186
+ while (parentUUID !== null) {
187
+ if (seen.has(parentUUID)) throw new Error(`cloud entity ${record.uuid} has a cyclic parent chain`);
188
+ seen.add(parentUUID);
189
+ const parent = lookup(parentUUID);
190
+ // Missing ancestry retains content rather than risking an ordinary
191
+ // file whose only bytes were silently skipped.
192
+ if (!parent) return false;
193
+ if (parent.type === 'repo.git') return true;
194
+ parentUUID = parent.parentUUID;
195
+ }
196
+ return false;
197
+ }
198
+
199
+ const heads = [];
200
+ const seen = new Set();
201
+ for (const record of normalized) {
202
+ const contentHash = contentHead(record);
203
+ if (!contentHash) continue;
204
+ if (['file.text', 'file.binary'].includes(record.type) && enclosedByRepository(record)) continue;
205
+ if (seen.has(contentHash)) continue;
206
+ seen.add(contentHash);
207
+ heads.push({ record, contentHash });
164
208
  }
165
- if (!content.has({ contentHash: head })) {
166
- throw new Error('file entity bytes were not captured before its cloud head committed');
209
+ return heads;
210
+ }
211
+
212
+ function queueContentHeads(records, resourceId, { queue = false, fallbackRecordAt = recordAt } = {}) {
213
+ for (const { contentHash } of contentHeads(records, { fallbackRecordAt })) {
214
+ if (!content || typeof content.has !== 'function') {
215
+ throw new Error('file entity content is unavailable: registration must capture bytes before it records their cloud head');
216
+ }
217
+ if (!content.has({ contentHash })) {
218
+ throw new Error('file entity bytes were not captured before its cloud head committed');
219
+ }
220
+ if (queue) content.ensureUpload({ resourceId, contentHash });
167
221
  }
168
- if (queue) content.ensureUpload({ resourceId, contentHash: head });
169
222
  }
170
223
 
171
224
  function replica() {
@@ -383,7 +436,7 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
383
436
  // content drain creates the filesystem upload cursor; doing that here
384
437
  // would let a rolled-back identity write upload unreachable bytes.
385
438
  const cloud = cloudRecord(record);
386
- queueContent(cloud, current.resourceId);
439
+ queueContentHeads([cloud], current.resourceId);
387
440
  upsertCatalogRecord({
388
441
  resourceId: current.resourceId,
389
442
  authorityEpoch: current.authorityEpoch,
@@ -423,7 +476,7 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
423
476
  if (bytes > SNAPSHOT_REPLACE_MAX_BYTES) {
424
477
  throw new Error(`registered entity graph is ${bytes} bytes; the atomic cloud registration limit is ${SNAPSHOT_REPLACE_MAX_BYTES} bytes`);
425
478
  }
426
- for (const record of snapshot.records) queueContent(record, current.resourceId, { queue: true });
479
+ queueContentHeads(snapshot.records, current.resourceId, { queue: true });
427
480
  const createdAt = now();
428
481
  db.prepare(`
429
482
  INSERT INTO entity_cloud_mutations (
@@ -448,8 +501,9 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
448
501
  }
449
502
  const insert = db.prepare('INSERT INTO entity_cloud_mutations (resource_id, mutation_id, operation_json, mutation_state, created_at, updated_at) VALUES (?, ?, ?, \'pending\', ?, ?)');
450
503
  const createdAt = now();
451
- for (const record of cloudSnapshot(records).records) {
452
- queueContent(record, current.resourceId, { queue: true });
504
+ const snapshot = cloudSnapshot(records);
505
+ queueContentHeads(snapshot.records, current.resourceId, { queue: true });
506
+ for (const record of snapshot.records) {
453
507
  insert.run(
454
508
  current.resourceId,
455
509
  'ent_' + crypto.randomBytes(18).toString('base64url'),
@@ -632,7 +686,7 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
632
686
  // upload. A joining device must NOT queue remote bytes as uploads; it
633
687
  // downloads them later through a materialization job instead.
634
688
  if (bootstrapping) {
635
- for (const record of snapshot.records) queueContent(record, resourceId, { queue: true });
689
+ queueContentHeads(snapshot.records, resourceId, { queue: true });
636
690
  }
637
691
  replaceCatalog({
638
692
  resourceId,
@@ -706,15 +760,13 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
706
760
  for (const job of pendingMaterializations().filter((entry) => entry.state === 'waiting_for_content')) {
707
761
  if (job.resourceId !== current.resourceId) continue;
708
762
  const tree = catalogTree({ resourceId: job.resourceId, uuid: job.entityUuid });
709
- for (const record of tree) {
710
- if (record.status !== 'active') continue;
711
- const head = contentHead(record);
712
- if (!head) continue;
713
- const part = content.missingPart({ contentHash: head });
763
+ const active = tree.filter((record) => record.status === 'active');
764
+ for (const { contentHash } of contentHeads(active)) {
765
+ const part = content.missingPart({ contentHash });
714
766
  if (!part) continue;
715
767
  return {
716
768
  resourceId: job.resourceId,
717
- contentHash: head,
769
+ contentHash,
718
770
  kind: part.kind,
719
771
  partIndex: part.partIndex,
720
772
  sha256: part.sha256,
@@ -882,10 +934,9 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
882
934
  const records = operation?.kind === SNAPSHOT_REPLACE_OPERATION
883
935
  ? operation?.snapshot?.records
884
936
  : [operation?.record];
885
- if ((records || []).some((record) => {
886
- const head = contentHead(record);
887
- return head && (!content || !content.uploaded({ resourceId: current.resourceId, contentHash: head }));
888
- })) {
937
+ if (contentHeads(records).some(({ contentHash }) => (
938
+ !content || !content.uploaded({ resourceId: current.resourceId, contentHash })
939
+ ))) {
889
940
  return [];
890
941
  }
891
942
  claimRow.run(current.headVersion, leaseUntil, started, row.sequence);
@@ -923,7 +974,7 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
923
974
  const records = operation?.kind === SNAPSHOT_REPLACE_OPERATION
924
975
  ? operation?.snapshot?.records
925
976
  : [operation?.record];
926
- for (const record of records || []) queueContent(record, current.resourceId, { queue: true });
977
+ queueContentHeads(records || [], current.resourceId, { queue: true });
927
978
  contentQueuedSequences.add(row.sequence);
928
979
  }
929
980
  return content.claim({ resourceId: current.resourceId, leaseMs });
@@ -1136,6 +1187,7 @@ function createEntityCloud({ db, userId, deviceId, content = null, repoStates =
1136
1187
  status,
1137
1188
  hasUnsettledLocalMutation,
1138
1189
  catalogTree,
1190
+ contentHeads,
1139
1191
  replica,
1140
1192
  };
1141
1193
  }
@@ -86,6 +86,17 @@ function createRegistrationService({
86
86
 
87
87
  const evidenceDb = new Database(path.join(dir, 'observer.db'));
88
88
  const identityDb = new Database(path.join(dir, 'identity.db'));
89
+ // WAL, decided here because this root OWNS the handles: SQLite's default
90
+ // journal mode pays a create-fsync-delete journal cycle on every commit,
91
+ // which made a large registration ~65% commit ceremony. WAL appends to
92
+ // one log instead, and synchronous=NORMAL keeps every commit durable
93
+ // against process crash (an OS crash can lose the tail, never corrupt —
94
+ // detection re-derives evidence, and identity commits are re-walked by
95
+ // the registration that was still running).
96
+ for (const handle of [evidenceDb, identityDb]) {
97
+ handle.pragma('journal_mode = WAL');
98
+ handle.pragma('synchronous = NORMAL');
99
+ }
89
100
  const entityContent = createEntityContent({ dir });
90
101
  // createRegistration owns the schema migration. Give entity-cloud a stable
91
102
  // lookup facade now, then connect it to that one migrated state store once
@@ -106,6 +117,10 @@ function createRegistrationService({
106
117
  return localState(captured, artifact.contentHash);
107
118
  }
108
119
 
120
+ // Entity-cloud needs only logical ancestry to choose a content plan. It
121
+ // never owns a watcher or address; this facade is available after the
122
+ // registration boundary is constructed below.
123
+ const boundary = { current: null };
109
124
  const entityCloud = createEntityCloud({
110
125
  db: identityDb,
111
126
  userId: cloudUserId,
@@ -113,11 +128,13 @@ function createRegistrationService({
113
128
  content: entityContent,
114
129
  repoStates: repoStateLookup,
115
130
  repoTransportFor: (record) => repoTransportHeads.get(record.payloadVersion) || null,
131
+ recordAt: (uuid) => {
132
+ try { return boundary.current?.registry.syncRecord(uuid) || null; } catch { return null; }
133
+ },
116
134
  });
117
135
 
118
136
  // The observer emits before the boundary exists (its constructor
119
137
  // scans), so the wire reads through a box that the boundary fills.
120
- const boundary = { current: null };
121
138
  const observer = createObserver({
122
139
  db: evidenceDb,
123
140
  emit: (event) => boundary.current?.onEvent(event),
@@ -474,15 +491,8 @@ function createRegistrationService({
474
491
  return state;
475
492
  }
476
493
 
477
- for (const record of active) {
478
- if (['file.text', 'file.binary'].includes(record.type)
479
- && !entityContent.complete({ contentHash: record.payloadVersion })) {
480
- return false;
481
- }
482
- if (record.type === 'repo.git'
483
- && (!record.transportVersion || !entityContent.complete({ contentHash: record.transportVersion }))) {
484
- return false;
485
- }
494
+ for (const { contentHash } of entityCloud.contentHeads(active)) {
495
+ if (!entityContent.complete({ contentHash })) return false;
486
496
  }
487
497
  if (!['workspace', 'repo.git', 'file.text', 'file.binary', 'link'].includes(root.type)) {
488
498
  throw new Error(`cloud root ${root.uuid} is ${root.type}; this materializer supports workspace, repository, file, and link trees only`);
@@ -579,11 +589,8 @@ function createRegistrationService({
579
589
  if (!['workspace', 'repo.git', 'file.text', 'file.binary', 'link'].includes(root.type)) {
580
590
  throw new Error(`cloud root ${root.uuid} is ${root.type}; this live materializer supports workspace, repository, file, and link trees only`);
581
591
  }
582
- for (const record of active) {
583
- if (['file.text', 'file.binary'].includes(record.type)
584
- && !entityContent.complete({ contentHash: record.payloadVersion })) return false;
585
- if (record.type === 'repo.git'
586
- && (!record.transportVersion || !entityContent.complete({ contentHash: record.transportVersion }))) return false;
592
+ for (const { contentHash } of entityCloud.contentHeads(active)) {
593
+ if (!entityContent.complete({ contentHash })) return false;
587
594
  }
588
595
 
589
596
  const parent = fs.realpathSync(job.targetPath);
@@ -877,7 +884,13 @@ function createRegistrationService({
877
884
  /** `amalgm register <path>` — register, then begin Watch + Detect. */
878
885
  register: async (targetPath) => {
879
886
  entityCloud.assertRegistrationReady();
880
- if (estimatedRegistrationRecords(targetPath) < ATOMIC_REGISTRATION_THRESHOLD) {
887
+ // No active cloud replica means no cloud is listening — the journal
888
+ // itself already no-ops there, so a large tree takes the same plain
889
+ // local registration a small one does instead of demanding an atomic
890
+ // replacement that has nowhere to land. Size never changes whether a
891
+ // signed-out registration is allowed.
892
+ const cloudActive = entityCloud.replica()?.state === 'active';
893
+ if (!cloudActive || estimatedRegistrationRecords(targetPath) < ATOMIC_REGISTRATION_THRESHOLD) {
881
894
  return boundary.current.add(targetPath);
882
895
  }
883
896
  const result = boundary.current.registry.withoutJournal(() => boundary.current.add(targetPath));
@@ -890,9 +903,10 @@ function createRegistrationService({
890
903
  * materialize. Small selections retain their ordinary per-entity intent. */
891
904
  registerMany: async (targetPaths) => {
892
905
  entityCloud.assertRegistrationReady();
893
- const needsAtomicReplacement = targetPaths.some((targetPath) => (
894
- estimatedRegistrationRecords(targetPath) >= ATOMIC_REGISTRATION_THRESHOLD
895
- ));
906
+ const needsAtomicReplacement = entityCloud.replica()?.state === 'active'
907
+ && targetPaths.some((targetPath) => (
908
+ estimatedRegistrationRecords(targetPath) >= ATOMIC_REGISTRATION_THRESHOLD
909
+ ));
896
910
  if (!needsAtomicReplacement) return boundary.current.addMany(targetPaths);
897
911
  const results = boundary.current.registry.withoutJournal(() => boundary.current.addMany(targetPaths));
898
912
  entityCloud.replaceSnapshot(boundary.current.registry.syncRecords());
@@ -103,6 +103,8 @@
103
103
  const fs = require('fs');
104
104
  const path = require('path');
105
105
 
106
+ const { preparer } = require('../lib/prepared');
107
+
106
108
  const FILE_TYPES = new Set(['file.text', 'file.binary']);
107
109
  const LEAF_TYPES = new Set(['file.text', 'file.binary', 'link']);
108
110
  // Adoption candidates by ground shape: a dir channel can be an arrived
@@ -132,6 +134,7 @@ const MEMORY_TABLES = ['registry_meta', 'channel_bindings', 'contested_ground'];
132
134
  * Retiring a tree drops its whole scope — bindings and markers together.
133
135
  */
134
136
  function createEvidenceMemory(db) {
137
+ const prepared = preparer(db);
135
138
  const present = db.prepare(
136
139
  `SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${MEMORY_TABLES.map(() => '?').join(', ')})`,
137
140
  ).all(...MEMORY_TABLES).map((row) => row.name);
@@ -145,30 +148,30 @@ function createEvidenceMemory(db) {
145
148
  }
146
149
  return {
147
150
  epoch: () => meta.epoch_uuid,
148
- bindings: (scopeId) => db.prepare('SELECT channel_id, entity_uuid FROM channel_bindings WHERE scope_id = ?')
151
+ bindings: (scopeId) => prepared('SELECT channel_id, entity_uuid FROM channel_bindings WHERE scope_id = ?')
149
152
  .all(scopeId).map((row) => ({ channelId: row.channel_id, uuid: row.entity_uuid })),
150
- bind: (scopeId, channelId, uuid) => db.prepare(
153
+ bind: (scopeId, channelId, uuid) => prepared(
151
154
  'INSERT INTO channel_bindings (channel_id, scope_id, entity_uuid) VALUES (?, ?, ?)'
152
155
  + ' ON CONFLICT(channel_id) DO UPDATE SET scope_id = excluded.scope_id, entity_uuid = excluded.entity_uuid',
153
156
  ).run(channelId, scopeId, uuid),
154
- unbind: (channelId) => db.prepare('DELETE FROM channel_bindings WHERE channel_id = ?').run(channelId),
157
+ unbind: (channelId) => prepared('DELETE FROM channel_bindings WHERE channel_id = ?').run(channelId),
155
158
  /** Across scopes — channel ids are globally unique. */
156
- boundTo: (channelId) => db.prepare('SELECT entity_uuid FROM channel_bindings WHERE channel_id = ?')
159
+ boundTo: (channelId) => prepared('SELECT entity_uuid FROM channel_bindings WHERE channel_id = ?')
157
160
  .get(channelId)?.entity_uuid,
158
- allBindings: () => db.prepare('SELECT channel_id, scope_id, entity_uuid FROM channel_bindings').all()
161
+ allBindings: () => prepared('SELECT channel_id, scope_id, entity_uuid FROM channel_bindings').all()
159
162
  .map((row) => ({ channelId: row.channel_id, scopeId: row.scope_id, uuid: row.entity_uuid })),
160
163
  /** A retired tree's whole evidence namespace, gone as one act. */
161
164
  dropScope: (scopeId) => {
162
- db.prepare('DELETE FROM channel_bindings WHERE scope_id = ?').run(scopeId);
163
- db.prepare('DELETE FROM contested_ground WHERE scope_id = ?').run(scopeId);
165
+ prepared('DELETE FROM channel_bindings WHERE scope_id = ?').run(scopeId);
166
+ prepared('DELETE FROM contested_ground WHERE scope_id = ?').run(scopeId);
164
167
  },
165
- contests: (scopeId) => db.prepare('SELECT rel_path, claimant_uuid, kind FROM contested_ground WHERE scope_id = ?')
168
+ contests: (scopeId) => prepared('SELECT rel_path, claimant_uuid, kind FROM contested_ground WHERE scope_id = ?')
166
169
  .all(scopeId).map((row) => ({ relPath: row.rel_path, uuid: row.claimant_uuid, kind: row.kind })),
167
- contest: (scopeId, relPath, uuid, kind) => db.prepare(
170
+ contest: (scopeId, relPath, uuid, kind) => prepared(
168
171
  'INSERT INTO contested_ground (scope_id, rel_path, claimant_uuid, kind) VALUES (?, ?, ?, ?)'
169
172
  + ' ON CONFLICT(scope_id, rel_path) DO UPDATE SET claimant_uuid = excluded.claimant_uuid, kind = excluded.kind',
170
173
  ).run(scopeId, relPath, uuid, kind),
171
- clearContest: (scopeId, relPath) => db.prepare('DELETE FROM contested_ground WHERE scope_id = ? AND rel_path = ?')
174
+ clearContest: (scopeId, relPath) => prepared('DELETE FROM contested_ground WHERE scope_id = ? AND rel_path = ?')
172
175
  .run(scopeId, relPath),
173
176
  };
174
177
  }
@@ -359,6 +362,27 @@ function createEvidence(options) {
359
362
  function reconcile() {
360
363
  dirty = false; // an event landing mid-reconcile re-dirties honestly
361
364
  try {
365
+ // One reconciliation is ONE durable truth transition: a first census
366
+ // over thousands of channels lands as one commit, not one per record.
367
+ // The registry's own transaction defers every bell to the commit and
368
+ // discards them all on rollback, so a failed pass publishes nothing.
369
+ registry.transaction(pass);
370
+ } catch (error) {
371
+ // The transaction rolled every durable write back; the in-memory
372
+ // caches may still hold entries from the rolled-back half. Durable
373
+ // truth first, cache second — so the caches reload from the tables
374
+ // before the retry, never believing a write that no longer exists.
375
+ channelToUuid.clear();
376
+ for (const row of memory.bindings(rootId)) channelToUuid.set(row.channelId, row.uuid);
377
+ contested.clear();
378
+ for (const row of memory.contests(rootId)) contested.set(row.relPath, { uuid: row.uuid, kind: row.kind });
379
+ dirty = true;
380
+ throw error;
381
+ }
382
+ }
383
+
384
+ function pass() {
385
+ {
362
386
  const rootRow = observer.listRoots().find((row) => row.rootId === rootId);
363
387
  if (!rootRow) return; // not enrolled yet — nothing is claimed
364
388
  groundPath = rootRow.path; // the address is derived, never the lens's identity
@@ -771,9 +795,6 @@ function createEvidence(options) {
771
795
  : null);
772
796
  }
773
797
  for (const [relPath, hash] of nowDivergent) divergent.set(relPath, hash);
774
- } catch (error) {
775
- dirty = true;
776
- throw error;
777
798
  }
778
799
  }
779
800
 
@@ -26,6 +26,8 @@
26
26
  * (address), never by name, so two trees may share a basename.
27
27
  */
28
28
 
29
+ const { preparer } = require('../lib/prepared');
30
+
29
31
  const SCHEMA = `
30
32
  CREATE TABLE IF NOT EXISTS registry_meta (
31
33
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
@@ -246,56 +248,58 @@ function createStore(db) {
246
248
  db.exec(SCHEMA);
247
249
  db.pragma(`user_version = ${GENERATION}`);
248
250
 
251
+ const prepared = preparer(db);
252
+
249
253
  return {
250
254
  meta() {
251
- const row = db.prepare('SELECT * FROM registry_meta WHERE singleton = 1').get();
255
+ const row = prepared('SELECT * FROM registry_meta WHERE singleton = 1').get();
252
256
  return row ? { epochUuid: row.epoch_uuid, createdAtMs: row.created_at_ms } : null;
253
257
  },
254
258
  initMeta({ epochUuid, createdAtMs }) {
255
- db.prepare('INSERT INTO registry_meta (singleton, epoch_uuid, created_at_ms) VALUES (1, ?, ?)')
259
+ prepared('INSERT INTO registry_meta (singleton, epoch_uuid, created_at_ms) VALUES (1, ?, ?)')
256
260
  .run(epochUuid, createdAtMs);
257
261
  },
258
262
 
259
263
  byUuid(uuid) {
260
- return rowToEntity(db.prepare('SELECT * FROM entities WHERE uuid = ?').get(uuid));
264
+ return rowToEntity(prepared('SELECT * FROM entities WHERE uuid = ?').get(uuid));
261
265
  },
262
266
  /** Every registered tree's top record: the active parentless rows. */
263
267
  activeRoots() {
264
- return db.prepare("SELECT * FROM entities WHERE parent_uuid IS NULL AND status = 'active' ORDER BY uuid")
268
+ return prepared("SELECT * FROM entities WHERE parent_uuid IS NULL AND status = 'active' ORDER BY uuid")
265
269
  .all().map(rowToEntity);
266
270
  },
267
271
  activeAtSlot(parentUuid, name) {
268
272
  return rowToEntity(
269
- db.prepare("SELECT * FROM entities WHERE parent_uuid IS ? AND name = ? AND status = 'active'")
273
+ prepared("SELECT * FROM entities WHERE parent_uuid IS ? AND name = ? AND status = 'active'")
270
274
  .get(parentUuid, name),
271
275
  );
272
276
  },
273
277
  childrenOf(parentUuid) {
274
- return db.prepare('SELECT * FROM entities WHERE parent_uuid = ? ORDER BY uuid').all(parentUuid)
278
+ return prepared('SELECT * FROM entities WHERE parent_uuid = ? ORDER BY uuid').all(parentUuid)
275
279
  .map(rowToEntity);
276
280
  },
277
281
  activeChildrenOf(parentUuid) {
278
- return db.prepare("SELECT * FROM entities WHERE parent_uuid = ? AND status = 'active' ORDER BY uuid")
282
+ return prepared("SELECT * FROM entities WHERE parent_uuid = ? AND status = 'active' ORDER BY uuid")
279
283
  .all(parentUuid).map(rowToEntity);
280
284
  },
281
285
  activeByType(type) {
282
- return db.prepare("SELECT * FROM entities WHERE type = ? AND status = 'active' ORDER BY uuid")
286
+ return prepared("SELECT * FROM entities WHERE type = ? AND status = 'active' ORDER BY uuid")
283
287
  .all(type).map(rowToEntity);
284
288
  },
285
289
  /** Logical graph rows for the cloud registry. This deliberately excludes
286
290
  * observer bindings and every machine-local table. */
287
291
  allEntities() {
288
- return db.prepare('SELECT * FROM entities ORDER BY uuid').all().map(rowToEntity);
292
+ return prepared('SELECT * FROM entities ORDER BY uuid').all().map(rowToEntity);
289
293
  },
290
294
 
291
295
  insert(entity) {
292
- db.prepare(`
296
+ prepared(`
293
297
  INSERT INTO entities (uuid, type, parent_uuid, name, status, payload_version)
294
298
  VALUES (?, ?, ?, ?, ?, ?)
295
299
  `).run(entity.uuid, entity.type, entity.parentUuid, entity.name, entity.status, entity.payloadVersion);
296
300
  },
297
301
  update(uuid, { type, parentUuid, name, status, payloadVersion }) {
298
- db.prepare(`
302
+ prepared(`
299
303
  UPDATE entities SET type = ?, parent_uuid = ?, name = ?, status = ?, payload_version = ?
300
304
  WHERE uuid = ?
301
305
  `).run(type, parentUuid, name, status, payloadVersion, uuid);
@@ -313,17 +317,17 @@ function createStore(db) {
313
317
  */
314
318
  rebindIdentities(mappings) {
315
319
  const retired = mappings.filter((mapping) => mapping.oldUuid !== mapping.newUuid);
316
- const retire = db.prepare("UPDATE entities SET status = 'trashed' WHERE uuid = ?");
317
- const insert = db.prepare(`
320
+ const retire = prepared("UPDATE entities SET status = 'trashed' WHERE uuid = ?");
321
+ const insert = prepared(`
318
322
  INSERT INTO entities (uuid, type, parent_uuid, name, status, payload_version)
319
323
  VALUES (?, ?, ?, ?, ?, ?)
320
324
  `);
321
- const reparent = db.prepare('UPDATE entities SET parent_uuid = ? WHERE parent_uuid = ?');
322
- const rebindChannels = db.prepare('UPDATE channel_bindings SET entity_uuid = ? WHERE entity_uuid = ?');
323
- const rebindClaims = db.prepare('UPDATE contested_ground SET claimant_uuid = ? WHERE claimant_uuid = ?');
324
- const rebindRepoState = db.prepare('UPDATE repo_states SET entity_uuid = ? WHERE entity_uuid = ?');
325
- const rebindReference = db.prepare("UPDATE entities SET payload_version = ? WHERE type = 'reference' AND payload_version = ?");
326
- const remove = db.prepare('DELETE FROM entities WHERE uuid = ?');
325
+ const reparent = prepared('UPDATE entities SET parent_uuid = ? WHERE parent_uuid = ?');
326
+ const rebindChannels = prepared('UPDATE channel_bindings SET entity_uuid = ? WHERE entity_uuid = ?');
327
+ const rebindClaims = prepared('UPDATE contested_ground SET claimant_uuid = ? WHERE claimant_uuid = ?');
328
+ const rebindRepoState = prepared('UPDATE repo_states SET entity_uuid = ? WHERE entity_uuid = ?');
329
+ const rebindReference = prepared("UPDATE entities SET payload_version = ? WHERE type = 'reference' AND payload_version = ?");
330
+ const remove = prepared('DELETE FROM entities WHERE uuid = ?');
327
331
 
328
332
  db.transaction(() => {
329
333
  for (const mapping of retired) retire.run(mapping.oldUuid);
@@ -365,10 +369,10 @@ function createStore(db) {
365
369
  * for an exchange. One transaction: the in-between never exists.
366
370
  */
367
371
  applyMoves(moves) {
368
- const toTemp = db.prepare('UPDATE entities SET name = ? WHERE uuid = ?');
372
+ const toTemp = prepared('UPDATE entities SET name = ? WHERE uuid = ?');
369
373
  // An optional type rides the same transition (adoption lands a
370
374
  // workspace as a folder) — a separate write could tear.
371
- const toFinal = db.prepare('UPDATE entities SET parent_uuid = ?, name = ?, type = COALESCE(?, type) WHERE uuid = ?');
375
+ const toFinal = prepared('UPDATE entities SET parent_uuid = ?, name = ?, type = COALESCE(?, type) WHERE uuid = ?');
372
376
  db.transaction(() => {
373
377
  for (const move of moves) toTemp.run(`\0${move.uuid}`, move.uuid);
374
378
  for (const move of moves) toFinal.run(move.parentUuid, move.name, move.type ?? null, move.uuid);
@@ -376,7 +380,7 @@ function createStore(db) {
376
380
  },
377
381
 
378
382
  cloudReplica() {
379
- const row = db.prepare('SELECT * FROM entity_cloud_replica WHERE singleton = 1').get();
383
+ const row = prepared('SELECT * FROM entity_cloud_replica WHERE singleton = 1').get();
380
384
  if (!row) return null;
381
385
  return {
382
386
  resourceId: row.resource_id,
@@ -389,7 +393,7 @@ function createStore(db) {
389
393
  };
390
394
  },
391
395
  saveCloudReplica(replica) {
392
- db.prepare(`
396
+ prepared(`
393
397
  INSERT INTO entity_cloud_replica (
394
398
  singleton, resource_id, device_id, authority_epoch, head_version,
395
399
  state, snapshot_cutoff, updated_at
@@ -413,11 +417,11 @@ function createStore(db) {
413
417
  );
414
418
  },
415
419
  cloudMutationCutoff() {
416
- const row = db.prepare('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM entity_cloud_mutations').get();
420
+ const row = prepared('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM entity_cloud_mutations').get();
417
421
  return Number(row.sequence);
418
422
  },
419
423
  enqueueCloudMutation(mutation) {
420
- db.prepare(`
424
+ prepared(`
421
425
  INSERT INTO entity_cloud_mutations (
422
426
  resource_id, mutation_id, operation_json, mutation_state,
423
427
  created_at, updated_at
@@ -431,7 +435,7 @@ function createStore(db) {
431
435
  );
432
436
  },
433
437
  listCloudMutations() {
434
- return db.prepare('SELECT * FROM entity_cloud_mutations ORDER BY sequence').all();
438
+ return prepared('SELECT * FROM entity_cloud_mutations ORDER BY sequence').all();
435
439
  },
436
440
  };
437
441
  }
@@ -369,20 +369,35 @@ test('add recreates an actual Git repository from verified Repocard cargo, then
369
369
  snapshotChecksum: sha256(snapshotBytes),
370
370
  });
371
371
 
372
- const heads = new Set(records.flatMap((record) => {
373
- if (['file.text', 'file.binary'].includes(record.type) && record.payloadVersion) return [record.payloadVersion];
374
- if (record.type === 'repo.git' && record.transportVersion) return [record.transportVersion];
375
- return [];
376
- }));
377
- for (const head of heads) {
378
- const parts = uploadedParts(source.entityContent, bootstrap.resourceId, head);
379
- for (const part of [...parts.filter((entry) => entry.kind === 'manifest'), ...parts.filter((entry) => entry.kind === 'chunk')]) {
380
- target.receiveCloudContent({
381
- resourceId: bootstrap.resourceId, contentHash: head, kind: part.kind,
382
- partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
383
- sha256: part.sha256, dataBase64: part.dataBase64,
384
- });
385
- }
372
+ const planned = source.entityCloud.contentHeads(records);
373
+ assert.deepEqual(
374
+ planned.map(({ contentHash }) => contentHash),
375
+ [repoCloudRecord.transportVersion],
376
+ 'a repository register uploads its one Repocard transport, never duplicate child file bytes',
377
+ );
378
+ const uploaded = [];
379
+ while (true) {
380
+ const part = source.entityCloud.claimContent();
381
+ if (!part) break;
382
+ uploaded.push(part);
383
+ source.entityCloud.acknowledgeContent({
384
+ resourceId: bootstrap.resourceId,
385
+ contentHash: part.contentHash,
386
+ kind: part.kind,
387
+ partIndex: part.partIndex,
388
+ });
389
+ }
390
+ assert.deepEqual(
391
+ [...new Set(uploaded.map((part) => part.contentHash))],
392
+ [repoCloudRecord.transportVersion],
393
+ 'the source content pump sends only the repository transport',
394
+ );
395
+ for (const part of [...uploaded.filter((entry) => entry.kind === 'manifest'), ...uploaded.filter((entry) => entry.kind === 'chunk')]) {
396
+ target.receiveCloudContent({
397
+ resourceId: bootstrap.resourceId, contentHash: part.contentHash, kind: part.kind,
398
+ partIndex: part.kind === 'manifest' ? 0 : part.partIndex,
399
+ sha256: part.sha256, dataBase64: part.dataBase64,
400
+ });
386
401
  }
387
402
 
388
403
  const job = await target.add({ uuid: registered.record.uuid, targetPath: targetParent });
@@ -544,6 +544,27 @@ test('register registers the named ground and every file beneath it', async () =
544
544
  assert.equal(nested[0].type, 'file.binary', 'and it distinguishes bytes from text');
545
545
  });
546
546
 
547
+ test('a signed-out installation registers a large tree the same way it registers a small one', async () => {
548
+ // Size routes a SIGNED-IN registration to the atomic cloud replacement;
549
+ // it must never decide whether a signed-out one is allowed at all. The
550
+ // journal already no-ops without an active replica, so a large tree takes
551
+ // the same plain local registration a small tree does.
552
+ const service = makeService('add-large-signed-out');
553
+ const ground = makeGround('add-large-signed-out');
554
+ for (let dir = 0; dir < 30; dir++) {
555
+ const nested = path.join(ground, `dir${dir}`);
556
+ fs.mkdirSync(nested);
557
+ for (let file = 0; file < 20; file++) {
558
+ fs.writeFileSync(path.join(nested, `f${file}.md`), `${dir}/${file}`);
559
+ }
560
+ }
561
+
562
+ const { record } = await service.register(ground);
563
+ assert.ok(record.uuid, 'the large tree registered without demanding a cloud handshake');
564
+ const registry = service.registration.tree(ground).registry;
565
+ assert.equal(registry.children(record.uuid).length, 30, 'every directory is registered');
566
+ });
567
+
547
568
  test('registerMany registers every selected file or folder through the one registration boundary', async () => {
548
569
  const service = makeService('add-many');
549
570
  const folder = makeGround('add-many-folder');