amalgm 0.1.245 → 0.1.246

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.
Files changed (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -0,0 +1,345 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Immutable local cache and upload ledger for file entity content.
5
+ *
6
+ * The entity registry owns UUID facts. This module owns only the bytes named
7
+ * by a file record's payloadVersion: capture the exact observed bytes once,
8
+ * split them into bounded content-addressed chunks, and retain a durable
9
+ * upload cursor until the private cloud resource acknowledges the manifest.
10
+ * It deliberately knows no local entity path or watcher binding.
11
+ */
12
+
13
+ const crypto = require('crypto');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const CONTENT_CONTRACT = 'amalgm-entity-content@1';
18
+ const CHUNK_BYTES = 256 * 1024;
19
+ const HASH = /^[0-9a-f]{64}$/;
20
+
21
+ function digest(bytes) {
22
+ return crypto.createHash('sha256').update(bytes).digest('hex');
23
+ }
24
+
25
+ function now() {
26
+ return new Date().toISOString();
27
+ }
28
+
29
+ function assertHash(value, name = 'content hash') {
30
+ if (typeof value !== 'string' || !HASH.test(value)) throw new Error(`invalid ${name}`);
31
+ return value;
32
+ }
33
+
34
+ function safeName(value) {
35
+ return crypto.createHash('sha256').update(String(value)).digest('hex');
36
+ }
37
+
38
+ function createEntityContent({ dir }) {
39
+ if (typeof dir !== 'string' || !dir) throw new Error('createEntityContent requires an entity directory');
40
+ const root = path.join(dir, 'content');
41
+ const chunksDir = path.join(root, 'chunks');
42
+ const manifestsDir = path.join(root, 'manifests');
43
+ const uploadsDir = path.join(root, 'uploads');
44
+
45
+ function ensureDirs() {
46
+ for (const target of [chunksDir, manifestsDir, uploadsDir]) {
47
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
48
+ }
49
+ }
50
+
51
+ function chunkFile(checksum) {
52
+ return path.join(chunksDir, `${assertHash(checksum, 'chunk checksum')}.bin`);
53
+ }
54
+
55
+ function manifestFile(contentHash) {
56
+ return path.join(manifestsDir, `${assertHash(contentHash)}.json`);
57
+ }
58
+
59
+ function uploadFile(resourceId, contentHash) {
60
+ assertHash(contentHash);
61
+ return path.join(uploadsDir, safeName(resourceId), `${contentHash}.json`);
62
+ }
63
+
64
+ function readJson(file) {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function writeJson(file, value) {
73
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
74
+ const temporary = `${file}.${crypto.randomBytes(12).toString('hex')}.tmp`;
75
+ try {
76
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
77
+ // Every writer for this file derived the same manifest from the same
78
+ // content hash. Atomic replacement is therefore safe across local
79
+ // processes and works on filesystems that do not support hard links.
80
+ fs.renameSync(temporary, file);
81
+ } finally {
82
+ fs.rmSync(temporary, { force: true });
83
+ }
84
+ }
85
+
86
+ function validateManifest(value, expectedHash = null) {
87
+ if (!value || value.contract !== CONTENT_CONTRACT
88
+ || typeof value.contentHash !== 'string' || !HASH.test(value.contentHash)
89
+ || (expectedHash !== null && value.contentHash !== expectedHash)
90
+ || !Number.isSafeInteger(value.bytes) || value.bytes < 0
91
+ || !Array.isArray(value.chunks)
92
+ || value.chunks.some((chunk) => (
93
+ !chunk || !HASH.test(String(chunk.sha256 || ''))
94
+ || !Number.isSafeInteger(chunk.bytes) || chunk.bytes < 1 || chunk.bytes > CHUNK_BYTES
95
+ ))) {
96
+ throw new Error('entity content manifest is invalid');
97
+ }
98
+ if (value.chunks.reduce((total, chunk) => total + chunk.bytes, 0) !== value.bytes) {
99
+ throw new Error('entity content manifest byte count is invalid');
100
+ }
101
+ if (value.bytes === 0 && value.chunks.length !== 0) {
102
+ throw new Error('empty entity content must have no chunks');
103
+ }
104
+ return {
105
+ contract: CONTENT_CONTRACT,
106
+ contentHash: value.contentHash,
107
+ bytes: value.bytes,
108
+ chunks: value.chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
109
+ };
110
+ }
111
+
112
+ function manifest(contentHash) {
113
+ ensureDirs();
114
+ const value = readJson(manifestFile(contentHash));
115
+ return value ? validateManifest(value, contentHash) : null;
116
+ }
117
+
118
+ function writeChunk(checksum, bytes) {
119
+ const file = chunkFile(checksum);
120
+ try {
121
+ fs.writeFileSync(file, bytes, { mode: 0o600, flag: 'wx' });
122
+ } catch (error) {
123
+ if (error.code !== 'EEXIST') throw error;
124
+ const existing = fs.readFileSync(file);
125
+ if (existing.length !== bytes.length || digest(existing) !== checksum) {
126
+ throw new Error(`local content chunk ${checksum} is corrupt`);
127
+ }
128
+ }
129
+ }
130
+
131
+ /** Capture exactly the bytes that produced `contentHash`, never a later
132
+ * version of the file. A changed source is a retryable refusal: no
133
+ * manifest exists for the wrong head, and the watcher will observe again. */
134
+ function captureFile({ filePath, contentHash }) {
135
+ assertHash(contentHash);
136
+ if (typeof filePath !== 'string' || !path.isAbsolute(filePath)) {
137
+ throw new Error('entity content capture requires an absolute file path');
138
+ }
139
+ ensureDirs();
140
+ const existing = manifest(contentHash);
141
+ if (existing) return existing;
142
+
143
+ const full = crypto.createHash('sha256');
144
+ const chunks = [];
145
+ let bytes = 0;
146
+ const fd = fs.openSync(filePath, 'r');
147
+ try {
148
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
149
+ let read;
150
+ while ((read = fs.readSync(fd, buffer, 0, buffer.length)) > 0) {
151
+ const part = Buffer.from(buffer.subarray(0, read));
152
+ const checksum = digest(part);
153
+ full.update(part);
154
+ writeChunk(checksum, part);
155
+ chunks.push({ sha256: checksum, bytes: part.length });
156
+ bytes += part.length;
157
+ }
158
+ } finally {
159
+ fs.closeSync(fd);
160
+ }
161
+ if (full.digest('hex') !== contentHash) {
162
+ throw new Error('file changed while its content was being captured');
163
+ }
164
+ const captured = { contract: CONTENT_CONTRACT, contentHash, bytes, chunks };
165
+ writeJson(manifestFile(contentHash), captured);
166
+ const installed = manifest(contentHash);
167
+ if (!installed) throw new Error('captured entity content manifest did not persist');
168
+ return installed;
169
+ }
170
+
171
+ function initialUpload(resourceId, contentHash) {
172
+ return {
173
+ contract: CONTENT_CONTRACT,
174
+ resourceId,
175
+ contentHash,
176
+ nextPart: 0,
177
+ manifestUploaded: false,
178
+ leaseUntil: null,
179
+ updatedAt: now(),
180
+ };
181
+ }
182
+
183
+ function validateUpload(value, resourceId, contentHash, content) {
184
+ if (!value || value.contract !== CONTENT_CONTRACT
185
+ || value.resourceId !== resourceId || value.contentHash !== contentHash
186
+ || !Number.isSafeInteger(value.nextPart) || value.nextPart < 0 || value.nextPart > content.chunks.length
187
+ || typeof value.manifestUploaded !== 'boolean') {
188
+ throw new Error('entity content upload ledger is invalid');
189
+ }
190
+ return value;
191
+ }
192
+
193
+ function readUpload(resourceId, contentHash, content) {
194
+ const file = uploadFile(resourceId, contentHash);
195
+ const value = readJson(file);
196
+ return value ? validateUpload(value, resourceId, contentHash, content) : null;
197
+ }
198
+
199
+ function saveUpload(value) {
200
+ const file = uploadFile(value.resourceId, value.contentHash);
201
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
202
+ const temporary = `${file}.${crypto.randomBytes(12).toString('hex')}.tmp`;
203
+ try {
204
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' });
205
+ fs.renameSync(temporary, file);
206
+ } finally {
207
+ fs.rmSync(temporary, { force: true });
208
+ }
209
+ }
210
+
211
+ function ensureUpload({ resourceId, contentHash }) {
212
+ if (typeof resourceId !== 'string' || !resourceId) throw new Error('entity content upload requires a resource id');
213
+ const content = manifest(contentHash);
214
+ if (!content) throw new Error(`content ${contentHash} was not captured before its entity record committed`);
215
+ const current = readUpload(resourceId, contentHash, content);
216
+ if (current) return current;
217
+ const created = initialUpload(resourceId, contentHash);
218
+ saveUpload(created);
219
+ return created;
220
+ }
221
+
222
+ function queueRecords({ resourceId, records }) {
223
+ if (!Array.isArray(records)) throw new Error('entity content queue requires records');
224
+ for (const record of records) {
225
+ if (!['file.text', 'file.binary'].includes(record?.type) || record.payloadVersion === null) continue;
226
+ ensureUpload({ resourceId, contentHash: record.payloadVersion });
227
+ }
228
+ }
229
+
230
+ function uploadEntries(resourceId) {
231
+ const directory = path.join(uploadsDir, safeName(resourceId));
232
+ try {
233
+ return fs.readdirSync(directory).filter((name) => name.endsWith('.json')).sort()
234
+ .map((name) => name.slice(0, -'.json'.length));
235
+ } catch (error) {
236
+ if (error.code === 'ENOENT') return [];
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ function claim({ resourceId, leaseMs = 30_000 }) {
242
+ ensureDirs();
243
+ const at = Date.now();
244
+ for (const contentHash of uploadEntries(resourceId)) {
245
+ const content = manifest(contentHash);
246
+ if (!content) throw new Error(`entity content ${contentHash} disappeared from the local cache`);
247
+ const upload = readUpload(resourceId, contentHash, content);
248
+ if (!upload || upload.manifestUploaded) continue;
249
+ if (upload.leaseUntil && Date.parse(upload.leaseUntil) > at) continue;
250
+ const next = { ...upload, leaseUntil: new Date(at + Math.max(1_000, Number(leaseMs) || 30_000)).toISOString(), updatedAt: now() };
251
+ if (next.nextPart < content.chunks.length) {
252
+ const index = next.nextPart;
253
+ const chunk = content.chunks[index];
254
+ const bytes = fs.readFileSync(chunkFile(chunk.sha256));
255
+ if (bytes.length !== chunk.bytes || digest(bytes) !== chunk.sha256) {
256
+ throw new Error(`local entity content chunk ${chunk.sha256} is corrupt`);
257
+ }
258
+ saveUpload(next);
259
+ return {
260
+ resourceId,
261
+ contentHash,
262
+ kind: 'chunk',
263
+ partIndex: index,
264
+ partCount: content.chunks.length,
265
+ sha256: chunk.sha256,
266
+ bytes: bytes.length,
267
+ dataBase64: bytes.toString('base64'),
268
+ };
269
+ }
270
+ const bytes = Buffer.from(JSON.stringify(content), 'utf8');
271
+ saveUpload(next);
272
+ return {
273
+ resourceId,
274
+ contentHash,
275
+ kind: 'manifest',
276
+ partIndex: content.chunks.length,
277
+ partCount: content.chunks.length,
278
+ sha256: digest(bytes),
279
+ bytes: bytes.length,
280
+ dataBase64: bytes.toString('base64'),
281
+ };
282
+ }
283
+ return null;
284
+ }
285
+
286
+ function acknowledge({ resourceId, contentHash, kind, partIndex }) {
287
+ const content = manifest(contentHash);
288
+ if (!content) throw new Error(`entity content ${contentHash} is unavailable`);
289
+ const current = readUpload(resourceId, contentHash, content);
290
+ if (!current) throw new Error(`entity content ${contentHash} was never queued`);
291
+ if (current.manifestUploaded) return { duplicate: true, uploaded: true };
292
+ if (kind === 'chunk') {
293
+ if (partIndex !== current.nextPart) {
294
+ throw new Error(`entity content chunk acknowledgement is out of order for ${contentHash}`);
295
+ }
296
+ current.nextPart += 1;
297
+ } else if (kind === 'manifest') {
298
+ if (current.nextPart !== content.chunks.length) {
299
+ throw new Error(`entity content manifest acknowledged before all chunks for ${contentHash}`);
300
+ }
301
+ current.manifestUploaded = true;
302
+ } else {
303
+ throw new Error('entity content acknowledgement has an unknown kind');
304
+ }
305
+ current.leaseUntil = null;
306
+ current.updatedAt = now();
307
+ saveUpload(current);
308
+ return { duplicate: false, uploaded: current.manifestUploaded };
309
+ }
310
+
311
+ function fail({ resourceId, contentHash, message }) {
312
+ const content = manifest(contentHash);
313
+ if (!content) throw new Error(`entity content ${contentHash} is unavailable`);
314
+ const current = readUpload(resourceId, contentHash, content);
315
+ if (!current || current.manifestUploaded) return { duplicate: Boolean(current?.manifestUploaded) };
316
+ current.leaseUntil = null;
317
+ current.lastError = String(message || 'entity content upload failed').slice(0, 4000);
318
+ current.updatedAt = now();
319
+ saveUpload(current);
320
+ return { duplicate: false };
321
+ }
322
+
323
+ function uploaded({ resourceId, contentHash }) {
324
+ const content = manifest(contentHash);
325
+ if (!content) return false;
326
+ return Boolean(readUpload(resourceId, contentHash, content)?.manifestUploaded);
327
+ }
328
+
329
+ function has({ contentHash }) {
330
+ return manifest(contentHash) !== null;
331
+ }
332
+
333
+ return {
334
+ captureFile,
335
+ ensureUpload,
336
+ queueRecords,
337
+ claim,
338
+ acknowledge,
339
+ fail,
340
+ uploaded,
341
+ has,
342
+ };
343
+ }
344
+
345
+ module.exports = { CHUNK_BYTES, CONTENT_CONTRACT, createEntityContent };