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
@@ -1,771 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * The one authoritative stream for a code project.
5
- *
6
- * Clients publish deltas. Only an officially numbered cloud mutation changes
7
- * this snapshot. A local outbox row is intent, never authority.
8
- */
9
-
10
- const crypto = require('crypto');
11
- const fs = require('fs');
12
- const { appendStateEvent } = require('../state/events');
13
- const { openLocalDb } = require('../state/db');
14
- const durable = require('./durable');
15
-
16
- const STREAM_CONTRACT = 'code-project-stream@2';
17
- const WALL_CONTRACT = 'code-project@7';
18
- const CARD_CONTRACT = 'repo-card@3';
19
- const CHECKPOINT_CONTRACT = 'repo-checkpoint@11';
20
- const BUNDLE_CONTRACT = 'git-bundle-chunks@2';
21
- const PAYLOAD_CONTRACT = 'opaque-chunks@2';
22
- const LOCAL_RESOURCE_PREFIX = 'code-project:';
23
- const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
24
- const SHA256 = /^[0-9a-f]{64}$/;
25
- const MUTATION_ID = /^[A-Za-z0-9_-]{16,160}$/;
26
- const MAX_CHUNKS = 4096;
27
- const MAX_CHUNK_BYTES = 512 * 1024;
28
- const MAX_BRANCHES = 4096;
29
-
30
- function invalid(message, statusCode = 400, code = null) {
31
- const error = Object.assign(new Error(message), { statusCode });
32
- if (code) error.code = code;
33
- return error;
34
- }
35
-
36
- function exactObject(input, keys, label) {
37
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
38
- throw invalid(`${label} is invalid`, 409);
39
- }
40
- const actual = Object.keys(input).sort();
41
- const expected = [...keys].sort();
42
- if (
43
- actual.length !== expected.length
44
- || actual.some((key, index) => key !== expected[index])
45
- ) throw invalid(`${label} has unexpected fields`, 409);
46
- return input;
47
- }
48
-
49
- function nonEmptyString(value, label) {
50
- if (typeof value !== 'string' || !value) throw invalid(`${label} is invalid`, 409);
51
- return value;
52
- }
53
-
54
- function branchName(value) {
55
- const components = typeof value === 'string' ? value.split('/') : [];
56
- if (
57
- typeof value !== 'string'
58
- || value.length < 1
59
- || value.length > 1024
60
- || value === '@'
61
- || value === 'HEAD'
62
- || value.startsWith('-')
63
- || value.endsWith('.')
64
- || value.includes('..')
65
- || value.includes('@{')
66
- || value.includes('[')
67
- || /[\u0000-\u0020\u007f~^:?*\\]/.test(value)
68
- || components.some((component) => (
69
- !component
70
- || component.startsWith('.')
71
- || component.endsWith('.lock')
72
- ))
73
- ) throw invalid('branch name is invalid', 409);
74
- return value;
75
- }
76
-
77
- function mutationId(value, label) {
78
- if (!MUTATION_ID.test(String(value || ''))) throw invalid(`${label} is invalid`, 409);
79
- return value;
80
- }
81
-
82
- function normalizeChunks(input, resourceId, kind, contract) {
83
- exactObject(
84
- input,
85
- ['kind', 'contract', 'resourceId', 'checksum', 'bytes', 'chunks'],
86
- `${kind} locator`,
87
- );
88
- if (
89
- input.kind !== kind
90
- || input.contract !== contract
91
- || input.resourceId !== resourceId
92
- || !SHA256.test(String(input.checksum || ''))
93
- || !Number.isSafeInteger(input.bytes)
94
- || input.bytes < 1
95
- || !Array.isArray(input.chunks)
96
- || input.chunks.length < 1
97
- || input.chunks.length > MAX_CHUNKS
98
- ) throw invalid(`${kind} locator is invalid`, 409);
99
- const chunks = input.chunks.map((chunk) => {
100
- exactObject(chunk, ['sha256', 'bytes'], `${kind} chunk`);
101
- if (
102
- !SHA256.test(String(chunk.sha256 || ''))
103
- || !Number.isSafeInteger(chunk.bytes)
104
- || chunk.bytes < 1
105
- || chunk.bytes > MAX_CHUNK_BYTES
106
- ) throw invalid(`${kind} chunk is invalid`, 409);
107
- return { sha256: chunk.sha256, bytes: chunk.bytes };
108
- });
109
- if (chunks.reduce((sum, chunk) => sum + chunk.bytes, 0) !== input.bytes) {
110
- throw invalid(`${kind} locator byte count is invalid`, 409);
111
- }
112
- const checksum = crypto.createHash('sha256').update(Buffer.from(JSON.stringify(chunks))).digest('hex');
113
- if (checksum !== input.checksum) throw invalid(`${kind} locator checksum is invalid`, 409);
114
- return {
115
- kind,
116
- contract,
117
- resourceId,
118
- checksum: input.checksum,
119
- bytes: input.bytes,
120
- chunks,
121
- };
122
- }
123
-
124
- function normalizeBundle(input, resourceId, oid) {
125
- exactObject(
126
- input,
127
- ['kind', 'contract', 'resourceId', 'oid', 'ref', 'checksum', 'bytes', 'chunks'],
128
- 'bundle locator',
129
- );
130
- if (
131
- input.kind !== 'bundle'
132
- || input.contract !== BUNDLE_CONTRACT
133
- || input.resourceId !== resourceId
134
- || input.oid !== oid
135
- || input.ref !== `refs/amalgm/exports/${oid}`
136
- ) throw invalid('bundle locator is invalid', 409);
137
- const common = normalizeChunks(
138
- {
139
- kind: 'bundle',
140
- contract: BUNDLE_CONTRACT,
141
- resourceId: input.resourceId,
142
- checksum: input.checksum,
143
- bytes: input.bytes,
144
- chunks: input.chunks,
145
- },
146
- resourceId,
147
- 'bundle',
148
- BUNDLE_CONTRACT,
149
- );
150
- return { ...common, oid, ref: input.ref };
151
- }
152
-
153
- function normalizePayload(input, resourceId) {
154
- return normalizeChunks(input, resourceId, 'chunks', PAYLOAD_CONTRACT);
155
- }
156
-
157
- function normalizeCheckpointValue(input, resourceId, baseOid = null) {
158
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
159
- throw invalid('checkpoint value is invalid', 409);
160
- }
161
- const revision = input.revision;
162
- if (!(revision === null || SHA256.test(String(revision || '')))) {
163
- throw invalid('checkpoint revision is invalid', 409);
164
- }
165
- if (Object.hasOwn(input, 'sections')) {
166
- exactObject(input, ['revision', 'sections'], 'checkpoint value');
167
- exactObject(input.sections, ['index', 'worktree'], 'checkpoint sections');
168
- const normalizeLayer = (layer) => {
169
- if (!Array.isArray(input.sections[layer])) {
170
- throw invalid('checkpoint sections are invalid', 409);
171
- }
172
- return input.sections[layer].map((section) => {
173
- if (Object.hasOwn(section || {}, 'text')) {
174
- exactObject(section, ['text'], 'checkpoint section');
175
- if (typeof section.text !== 'string' || section.text.length === 0) {
176
- throw invalid('checkpoint section is invalid', 409);
177
- }
178
- return { text: section.text };
179
- }
180
- exactObject(section, ['payload'], 'checkpoint section');
181
- return { payload: normalizePayload(section.payload, resourceId) };
182
- });
183
- };
184
- const normalized = {
185
- revision,
186
- sections: {
187
- index: normalizeLayer('index'),
188
- worktree: normalizeLayer('worktree'),
189
- },
190
- };
191
- if (require('./checkpoint').revisionOfValue(baseOid, normalized) !== revision) {
192
- throw invalid('checkpoint revision is invalid', 409);
193
- }
194
- return normalized;
195
- }
196
- exactObject(input, ['revision', 'indexDiff', 'worktreeDiff'], 'checkpoint value');
197
- if (typeof input.indexDiff !== 'string' || typeof input.worktreeDiff !== 'string') {
198
- throw invalid('checkpoint value is invalid', 409);
199
- }
200
- if (
201
- baseOid
202
- && require('./checkpoint').revisionOfValue(baseOid, input) !== revision
203
- ) throw invalid('checkpoint revision is invalid', 409);
204
- return {
205
- revision,
206
- indexDiff: input.indexDiff,
207
- worktreeDiff: input.worktreeDiff,
208
- };
209
- }
210
-
211
- function normalizeHead(input) {
212
- exactObject(input, ['branch', 'oid', 'detached'], 'card head');
213
- if (
214
- !(input.branch === null || branchName(input.branch))
215
- || !SHA.test(String(input.oid || ''))
216
- || typeof input.detached !== 'boolean'
217
- || (input.detached && input.branch !== null)
218
- || (!input.detached && !input.branch)
219
- ) throw invalid('card head is invalid', 409);
220
- return { branch: input.branch, oid: input.oid, detached: input.detached };
221
- }
222
-
223
- function normalizeBranch(input, resourceId) {
224
- exactObject(input, ['name', 'oid', 'objects'], 'card branch');
225
- const name = branchName(input.name);
226
- if (!SHA.test(String(input.oid || ''))) throw invalid('branch oid is invalid', 409);
227
- return {
228
- name,
229
- oid: input.oid,
230
- objects: normalizeBundle(input.objects, resourceId, input.oid),
231
- };
232
- }
233
-
234
- function normalizeCheckpoint(input, resourceId, expectedHead = null) {
235
- exactObject(input, ['checkpoint', 'baseOid', 'branch', 'value'], 'card checkpoint');
236
- if (
237
- input.checkpoint !== CHECKPOINT_CONTRACT
238
- || !SHA.test(String(input.baseOid || ''))
239
- || !(input.branch === null || branchName(input.branch))
240
- || (expectedHead && (
241
- input.baseOid !== expectedHead.oid
242
- || input.branch !== expectedHead.branch
243
- ))
244
- ) throw invalid('card checkpoint is invalid', 409);
245
- return {
246
- checkpoint: CHECKPOINT_CONTRACT,
247
- baseOid: input.baseOid,
248
- branch: input.branch,
249
- value: normalizeCheckpointValue(input.value, resourceId, input.baseOid),
250
- };
251
- }
252
-
253
- function normalizeCardSet(input, resourceId) {
254
- exactObject(input, ['kind', 'card', 'head', 'objects', 'branches', 'checkpoint'], 'card.set');
255
- if (input.kind !== 'card.set' || input.card !== CARD_CONTRACT) {
256
- throw invalid('card.set is invalid', 409);
257
- }
258
- const head = normalizeHead(input.head);
259
- const branches = Array.isArray(input.branches)
260
- ? input.branches.map((branch) => normalizeBranch(branch, resourceId))
261
- : null;
262
- if (!branches) throw invalid('card branches are invalid', 409);
263
- if (branches.length > MAX_BRANCHES) throw invalid('card has too many branches', 409);
264
- const names = new Set();
265
- for (const branch of branches) {
266
- if (names.has(branch.name)) throw invalid('card repeats a branch', 409);
267
- names.add(branch.name);
268
- }
269
- if (!head.detached && !branches.some((branch) => (
270
- branch.name === head.branch && branch.oid === head.oid
271
- ))) throw invalid('card branch map does not contain HEAD', 409);
272
- return {
273
- kind: 'card.set',
274
- card: CARD_CONTRACT,
275
- head,
276
- objects: normalizeBundle(input.objects, resourceId, head.oid),
277
- branches: branches.sort((left, right) => left.name.localeCompare(right.name)),
278
- checkpoint: normalizeCheckpoint(input.checkpoint, resourceId, head),
279
- };
280
- }
281
-
282
- function normalizeCheckpointSet(input, resourceId) {
283
- exactObject(
284
- input,
285
- ['kind', 'checkpoint', 'baseOid', 'branch', 'from', 'to'],
286
- 'checkpoint.set',
287
- );
288
- if (
289
- input.kind !== 'checkpoint.set'
290
- || input.checkpoint !== CHECKPOINT_CONTRACT
291
- || !SHA.test(String(input.baseOid || ''))
292
- || !(input.branch === null || branchName(input.branch))
293
- ) throw invalid('checkpoint.set is invalid', 409);
294
- return {
295
- kind: 'checkpoint.set',
296
- checkpoint: CHECKPOINT_CONTRACT,
297
- baseOid: input.baseOid,
298
- branch: input.branch,
299
- from: normalizeCheckpointValue(input.from, resourceId, input.baseOid),
300
- to: normalizeCheckpointValue(input.to, resourceId, input.baseOid),
301
- };
302
- }
303
-
304
- function normalizeDelta(input, resourceId) {
305
- let normalized;
306
- if (input?.kind === 'card.set') normalized = normalizeCardSet(input, resourceId);
307
- else if (input?.kind === 'checkpoint.set') normalized = normalizeCheckpointSet(input, resourceId);
308
- else throw invalid('code project delta is invalid', 409);
309
- const chunks = new Map();
310
- const visit = (value) => {
311
- if (!value || typeof value !== 'object') return;
312
- if (Array.isArray(value)) {
313
- value.forEach(visit);
314
- return;
315
- }
316
- if (Array.isArray(value.chunks)) {
317
- value.chunks.forEach((chunk) => {
318
- const previous = chunks.get(chunk.sha256);
319
- if (previous !== undefined && previous !== chunk.bytes) {
320
- throw invalid('one code chunk claims two sizes', 409);
321
- }
322
- chunks.set(chunk.sha256, chunk.bytes);
323
- });
324
- }
325
- Object.values(value).forEach(visit);
326
- };
327
- visit(normalized);
328
- if (chunks.size > MAX_CHUNKS) throw invalid('code publication has too many chunks', 409);
329
- return normalized;
330
- }
331
-
332
- function normalizeOperation(input, resourceId) {
333
- exactObject(input, ['kind', 'project', 'delta'], 'code.publish operation');
334
- if (input.kind !== 'code.publish' || input.project !== resourceId) {
335
- throw invalid('code.publish operation is invalid', 409);
336
- }
337
- return {
338
- kind: 'code.publish',
339
- project: resourceId,
340
- delta: normalizeDelta(input.delta, resourceId),
341
- };
342
- }
343
-
344
- function stampCard(delta, stamp) {
345
- return {
346
- card: CARD_CONTRACT,
347
- head: delta.head,
348
- objects: delta.objects,
349
- branches: delta.branches,
350
- ...stamp,
351
- };
352
- }
353
-
354
- function stampCheckpoint(checkpoint, stamp) {
355
- return { ...checkpoint, ...stamp };
356
- }
357
-
358
- function sameValue(left, right) {
359
- return JSON.stringify(left) === JSON.stringify(right);
360
- }
361
-
362
- function fold(snapshotInput, operationInput, stampInput) {
363
- const snapshot = normalizeSnapshot(snapshotInput, operationInput?.project, true);
364
- const operation = normalizeOperation(operationInput, snapshot.project);
365
- const version = Number(stampInput?.version);
366
- const machine = nonEmptyString(stampInput?.machine, 'mutation machine');
367
- const officialMutationId = mutationId(stampInput?.mutationId, 'mutation id');
368
- if (!Number.isSafeInteger(version) || version < 1) {
369
- throw invalid('mutation version is invalid', 409);
370
- }
371
- // Snapshot writes precede cursor writes. Replaying after a crash is silent.
372
- if (snapshot.version >= version) return snapshot;
373
- if (version !== snapshot.version + 1) {
374
- throw invalid(
375
- `code project gap: expected ${snapshot.version + 1}, received ${version}`,
376
- 409,
377
- 'shared_mutation_gap',
378
- );
379
- }
380
- const stamp = { machine, mutationId: officialMutationId, version };
381
- if (operation.delta.kind === 'card.set') {
382
- return {
383
- schemaVersion: 2,
384
- project: snapshot.project,
385
- version,
386
- card: stampCard(operation.delta, stamp),
387
- checkpoint: stampCheckpoint(operation.delta.checkpoint, stamp),
388
- };
389
- }
390
- const delta = operation.delta;
391
- if (
392
- !snapshot.card
393
- || snapshot.card.head.oid !== delta.baseOid
394
- || snapshot.card.head.branch !== delta.branch
395
- || !snapshot.checkpoint
396
- || snapshot.checkpoint.baseOid !== delta.baseOid
397
- || snapshot.checkpoint.branch !== delta.branch
398
- || !sameValue(snapshot.checkpoint.value, delta.from)
399
- ) throw invalid('code_project_stale_base', 409, 'code_project_stale_base');
400
- return {
401
- ...snapshot,
402
- version,
403
- checkpoint: stampCheckpoint({
404
- checkpoint: CHECKPOINT_CONTRACT,
405
- baseOid: delta.baseOid,
406
- branch: delta.branch,
407
- value: delta.to,
408
- }, stamp),
409
- };
410
- }
411
-
412
- function emptySnapshot(resourceId) {
413
- return {
414
- schemaVersion: 2,
415
- project: resourceId,
416
- version: 0,
417
- card: null,
418
- checkpoint: null,
419
- };
420
- }
421
-
422
- function normalizeStampedCard(input, resourceId, snapshotVersion) {
423
- exactObject(
424
- input,
425
- ['card', 'head', 'objects', 'branches', 'machine', 'mutationId', 'version'],
426
- 'official card',
427
- );
428
- const version = Number(input.version);
429
- if (
430
- input.card !== CARD_CONTRACT
431
- || !Number.isSafeInteger(version)
432
- || version < 1
433
- || version > snapshotVersion
434
- ) throw invalid('official card stamp is invalid', 409);
435
- const delta = normalizeCardSet({
436
- kind: 'card.set',
437
- card: input.card,
438
- head: input.head,
439
- objects: input.objects,
440
- branches: input.branches,
441
- checkpoint: {
442
- checkpoint: CHECKPOINT_CONTRACT,
443
- baseOid: input.head.oid,
444
- branch: input.head.branch,
445
- value: { revision: null, indexDiff: '', worktreeDiff: '' },
446
- },
447
- }, resourceId);
448
- return {
449
- card: CARD_CONTRACT,
450
- head: delta.head,
451
- objects: delta.objects,
452
- branches: delta.branches,
453
- machine: nonEmptyString(input.machine, 'official card machine'),
454
- mutationId: mutationId(input.mutationId, 'official card mutation'),
455
- version,
456
- };
457
- }
458
-
459
- function normalizeStampedCheckpoint(input, resourceId, snapshotVersion) {
460
- exactObject(
461
- input,
462
- ['checkpoint', 'baseOid', 'branch', 'value', 'machine', 'mutationId', 'version'],
463
- 'official checkpoint',
464
- );
465
- const version = Number(input.version);
466
- if (
467
- !Number.isSafeInteger(version)
468
- || version < 1
469
- || version > snapshotVersion
470
- ) throw invalid('official checkpoint stamp is invalid', 409);
471
- return {
472
- ...normalizeCheckpoint({
473
- checkpoint: input.checkpoint,
474
- baseOid: input.baseOid,
475
- branch: input.branch,
476
- value: input.value,
477
- }, resourceId),
478
- machine: nonEmptyString(input.machine, 'official checkpoint machine'),
479
- mutationId: mutationId(input.mutationId, 'official checkpoint mutation'),
480
- version,
481
- };
482
- }
483
-
484
- function normalizeSnapshot(input, resourceId = null, official = false) {
485
- exactObject(input, ['schemaVersion', 'project', 'version', 'card', 'checkpoint'], 'code project snapshot');
486
- if (
487
- input.schemaVersion !== 2
488
- || typeof input.project !== 'string'
489
- || !input.project
490
- || !require('./tree/identity').isUuid(input.project)
491
- || (resourceId && input.project !== resourceId)
492
- || !Number.isSafeInteger(Number(input.version))
493
- || Number(input.version) < 0
494
- ) throw invalid('code project snapshot is invalid', 409);
495
- const version = Number(input.version);
496
- if (version === 0) {
497
- if (input.card !== null || input.checkpoint !== null) {
498
- throw invalid('empty code project snapshot is invalid', 409);
499
- }
500
- return emptySnapshot(input.project);
501
- }
502
- if (!official || !input.card || !input.checkpoint) {
503
- throw invalid('official code project snapshot is incomplete', 409);
504
- }
505
- const card = normalizeStampedCard(input.card, input.project, version);
506
- const checkpoint = normalizeStampedCheckpoint(input.checkpoint, input.project, version);
507
- if (
508
- checkpoint.version !== version
509
- || card.version > checkpoint.version
510
- || checkpoint.baseOid !== card.head.oid
511
- || checkpoint.branch !== card.head.branch
512
- ) throw invalid('official checkpoint does not match the card', 409);
513
- return {
514
- schemaVersion: 2,
515
- project: input.project,
516
- version,
517
- card,
518
- checkpoint,
519
- };
520
- }
521
-
522
- function readSnapshot(file, resourceId) {
523
- if (!fs.existsSync(file) || fs.statSync(file).size === 0) return emptySnapshot(resourceId);
524
- try {
525
- return normalizeSnapshot(JSON.parse(fs.readFileSync(file, 'utf8')), resourceId, true);
526
- } catch (error) {
527
- if (error?.statusCode) throw error;
528
- throw invalid('code project snapshot is not JSON', 409);
529
- }
530
- }
531
-
532
- function writeSnapshot(file, snapshot) {
533
- durable.writeJson(file, normalizeSnapshot(snapshot, snapshot.project, true));
534
- }
535
-
536
- function publish(project, deltaInput, mutationIdInput) {
537
- const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
538
- if (!replica || replica.contract !== STREAM_CONTRACT) {
539
- throw invalid('code project stream is not attached', 409);
540
- }
541
- const operation = normalizeOperation({
542
- kind: 'code.publish',
543
- project: project.resourceId,
544
- delta: deltaInput,
545
- }, project.resourceId);
546
- const publicationMutationId = mutationId(mutationIdInput, 'publication mutation');
547
- return require('../state/mutations').commitLocalMutation({
548
- channelId: `resource:${project.resourceId}`,
549
- localResource: `${LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
550
- sharedResourceId: project.resourceId,
551
- mutationId: publicationMutationId,
552
- deviceId: require('./identity').machineId(),
553
- authorityEpoch: replica.authorityEpoch,
554
- baseCloudVersion: replica.appliedVersion,
555
- contract: STREAM_CONTRACT,
556
- schemaVersion: 2,
557
- operationKind: 'code.publish',
558
- operation,
559
- }, {
560
- // Outbound intent deliberately does not mutate the official snapshot.
561
- event: {
562
- resource: `${LOCAL_RESOURCE_PREFIX}${project.resourceId}`,
563
- op: 'update',
564
- id: project.file,
565
- sharedResourceId: project.resourceId,
566
- value: operation.delta,
567
- source: 'code-project:outbox',
568
- },
569
- });
570
- }
571
-
572
- function decodeSnapshot(input) {
573
- if (input?.contract !== STREAM_CONTRACT || Number(input?.schemaVersion) !== 2) {
574
- throw invalid('unsupported code project stream', 409);
575
- }
576
- const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
577
- if (
578
- !bytes.length
579
- || crypto.createHash('sha256').update(bytes).digest('hex') !== input.snapshotChecksum
580
- ) throw invalid('code project snapshot checksum mismatch', 409);
581
- let parsed;
582
- try {
583
- parsed = JSON.parse(bytes.toString('utf8'));
584
- } catch {
585
- throw invalid('code project snapshot must be JSON', 409);
586
- }
587
- const snapshot = normalizeSnapshot(parsed, input.resourceId, true);
588
- if (snapshot.version !== Number(input.snapshotVersion)) {
589
- throw invalid('code project snapshot version mismatch', 409);
590
- }
591
- return { snapshot };
592
- }
593
-
594
- function installSnapshot(file, input, options = {}) {
595
- const database = options.database || openLocalDb();
596
- const replicas = require('../state/replicas');
597
- const envelope = replicas.validateSnapshotEnvelope(input, {
598
- contract: STREAM_CONTRACT,
599
- schemaVersion: 2,
600
- });
601
- const { snapshot } = decodeSnapshot(input);
602
- const existing = replicas.getReplicaByResourceId(input.resourceId, { database });
603
- if (existing && existing.localPath !== file) {
604
- throw invalid('code project stream is bound to another local file', 409);
605
- }
606
- const current = fs.existsSync(file)
607
- ? readSnapshot(file, input.resourceId)
608
- : emptySnapshot(input.resourceId);
609
- if (snapshot.version < current.version) {
610
- const checked = existing
611
- ? replicas.advanceReplicaSnapshot({
612
- resourceId: envelope.resourceId,
613
- snapshotVersion: envelope.snapshotVersion,
614
- snapshotChecksum: input.snapshotChecksum,
615
- authorityEpoch: envelope.authorityEpoch,
616
- }, { database })
617
- : existing;
618
- return { replica: checked, duplicate: true, ignored: true };
619
- }
620
- if (snapshot.version === current.version) {
621
- if (JSON.stringify(snapshot) !== JSON.stringify(current)) {
622
- throw invalid(
623
- 'equal code project snapshot versions contain different state',
624
- 409,
625
- 'code_project_snapshot_mismatch',
626
- );
627
- }
628
- if (existing) {
629
- const replica = replicas.advanceReplicaSnapshot({
630
- resourceId: input.resourceId,
631
- snapshotVersion: envelope.snapshotVersion,
632
- snapshotChecksum: input.snapshotChecksum,
633
- authorityEpoch: envelope.authorityEpoch,
634
- }, { database });
635
- completeInstalledSnapshot(input.resourceId, snapshot.version, database);
636
- return { replica, duplicate: true };
637
- }
638
- }
639
- writeSnapshot(file, snapshot);
640
- const registered = existing
641
- ? {
642
- replica: replicas.advanceReplicaSnapshot({
643
- resourceId: input.resourceId,
644
- snapshotVersion: envelope.snapshotVersion,
645
- snapshotChecksum: input.snapshotChecksum,
646
- authorityEpoch: envelope.authorityEpoch,
647
- }, { database }),
648
- }
649
- : replicas.registerReplica({
650
- resourceId: input.resourceId,
651
- localResource: `${LOCAL_RESOURCE_PREFIX}${input.resourceId}`,
652
- localPath: file,
653
- contract: STREAM_CONTRACT,
654
- schemaVersion: 2,
655
- authorityEpoch: envelope.authorityEpoch,
656
- appliedVersion: envelope.snapshotVersion,
657
- snapshotVersion: envelope.snapshotVersion,
658
- snapshotChecksum: input.snapshotChecksum,
659
- }, { database });
660
- completeInstalledSnapshot(input.resourceId, snapshot.version, database);
661
- appendStateEvent({
662
- resource: `${LOCAL_RESOURCE_PREFIX}${input.resourceId}`,
663
- op: 'update',
664
- id: file,
665
- sharedResourceId: input.resourceId,
666
- resourceVersion: envelope.snapshotVersion,
667
- source: 'code-project:snapshot',
668
- });
669
- return registered;
670
- }
671
-
672
- function completeInstalledSnapshot(resourceId, through, database) {
673
- require('../state/mutations').completeOfficialSnapshotFold(
674
- resourceId,
675
- through,
676
- { database },
677
- );
678
- const project = require('./wall').projectForResource(resourceId);
679
- if (!project) return;
680
- const intents = require('./intent');
681
- const held = intents.readIntent(project);
682
- const acknowledgement = held
683
- ? intents.officialAcknowledgement(project, held)
684
- : null;
685
- if (acknowledgement && acknowledgement.version <= through) {
686
- intents.markPublicationFolded(
687
- project,
688
- acknowledgement.mutationId,
689
- acknowledgement.version,
690
- held.publication.numberedAt,
691
- );
692
- }
693
- }
694
-
695
- function applyCloudMutation(input, options = {}) {
696
- const envelope = input?.envelope || {};
697
- if (
698
- envelope.contract !== STREAM_CONTRACT
699
- || Number(envelope.schemaVersion) !== 2
700
- || envelope.operationKind !== 'code.publish'
701
- ) throw invalid('invalid code project cloud mutation', 409);
702
- const operation = normalizeOperation(envelope.operation, envelope.resourceId);
703
- const database = options.database || openLocalDb();
704
- const replica = require('../state/replicas').getReplicaByResourceId(
705
- envelope.resourceId,
706
- { database },
707
- );
708
- if (!replica || replica.contract !== STREAM_CONTRACT) {
709
- throw invalid('unknown code project stream', 404);
710
- }
711
- return require('../state/mutations').ingestCloudMutation({
712
- ...input,
713
- localResource: `${LOCAL_RESOURCE_PREFIX}${envelope.resourceId}`,
714
- }, {
715
- database,
716
- requireOfficialMaterialization: true,
717
- materialize() {
718
- const next = fold(
719
- readSnapshot(replica.localPath, envelope.resourceId),
720
- operation,
721
- {
722
- machine: envelope.deviceId,
723
- mutationId: envelope.mutationId,
724
- version: Number(input.version),
725
- },
726
- );
727
- writeSnapshot(replica.localPath, next);
728
- const project = require('./wall').projectForResource(envelope.resourceId);
729
- if (project) {
730
- require('./intent').markPublicationFolded(
731
- project,
732
- envelope.mutationId,
733
- Number(input.version),
734
- input.committedAt,
735
- );
736
- }
737
- },
738
- event: {
739
- resource: `${LOCAL_RESOURCE_PREFIX}${envelope.resourceId}`,
740
- op: 'update',
741
- id: replica.localPath,
742
- sharedResourceId: envelope.resourceId,
743
- resourceVersion: Number(input.version),
744
- value: operation.delta,
745
- source: 'code-project:cloud',
746
- },
747
- });
748
- }
749
-
750
- module.exports = {
751
- BUNDLE_CONTRACT,
752
- CARD_CONTRACT,
753
- CHECKPOINT_CONTRACT,
754
- LOCAL_RESOURCE_PREFIX,
755
- PAYLOAD_CONTRACT,
756
- STREAM_CONTRACT,
757
- WALL_CONTRACT,
758
- _test: {
759
- emptySnapshot,
760
- fold,
761
- normalizeDelta,
762
- normalizeOperation,
763
- normalizeSnapshot,
764
- },
765
- applyCloudMutation,
766
- emptySnapshot,
767
- installSnapshot,
768
- publish,
769
- readSnapshot,
770
- writeSnapshot,
771
- };