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
@@ -13,10 +13,7 @@
13
13
  * real local edit or revert is one new transition.
14
14
  *
15
15
  * Checkpoint files below are local capture cache only. Their operations ride
16
- * inside the project stream, so the entire repo has one ordered rail.
17
- *
18
- * Axiom 4: only an exact local observation may create or update intent.
19
- * Proven by workspace-local-intent.test.js.
16
+ * inside the machine wall, so the entire repo has one ordered stream.
20
17
  */
21
18
 
22
19
  const crypto = require('crypto');
@@ -26,10 +23,9 @@ const { AMALGM_DIR } = require('../config');
26
23
  const { appendStateEvent, subscribeStateEvents } = require('../state/events');
27
24
  const { machineId } = require('./identity');
28
25
  const { listRepoCards } = require('./cards');
29
- const durable = require('./durable');
30
- const { describeBytes, manifestChecksum, sha256 } = require('./content');
26
+ const { captureCheckpoint } = require('./git');
31
27
 
32
- const CHECKPOINT_CONTRACT = 'repo-checkpoint@11';
28
+ const CHECKPOINT_CONTRACT = 'repo-checkpoint@8';
33
29
  const MAX_INLINE_DIFF_BYTES = 512 * 1024;
34
30
  const PUBLISH_QUIET_MS = 25;
35
31
  const RECONCILE_INTERVAL_MS = 30_000;
@@ -58,75 +54,15 @@ function checkpointFile(machine, repoKey) {
58
54
  .update(`${CHECKPOINT_CONTRACT}|${machine}|${repoKey}`)
59
55
  .digest('hex')
60
56
  .slice(0, 24);
61
- return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v11', `${digest}.json`);
62
- }
63
-
64
- /**
65
- * Git can print the same tree in different file order depending on whether
66
- * a path is tracked or still untracked. File order is transport formatting,
67
- * not content identity, so sort complete file patches before hashing them.
68
- */
69
- function patchSections(diff) {
70
- if (!diff) return [];
71
- if (!diff.startsWith('diff --git ')) return [diff];
72
- const sections = [];
73
- let start = 0;
74
- for (;;) {
75
- const next = diff.indexOf('\ndiff --git ', start + 1);
76
- if (next === -1) {
77
- sections.push(diff.slice(start));
78
- break;
79
- }
80
- sections.push(diff.slice(start, next + 1));
81
- start = next + 1;
82
- }
83
- return sections.sort((left, right) => (
84
- left < right ? -1 : left > right ? 1 : 0
85
- ));
86
- }
87
-
88
- function canonicalPatch(diff) {
89
- return patchSections(diff).join('');
90
- }
91
-
92
- function sectionIdentity(text) {
93
- return manifestChecksum(describeBytes(Buffer.from(text, 'utf8')));
94
- }
95
-
96
- function revisionOfSections(baseOid, indexSections, worktreeSections) {
97
- const index = [...indexSections].sort();
98
- const worktree = [...worktreeSections].sort();
99
- if (index.length === 0 && worktree.length === 0) return null;
100
- return sha256(Buffer.from(JSON.stringify({ baseOid, index, worktree }), 'utf8'));
57
+ return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v8', `${digest}.json`);
101
58
  }
102
59
 
103
60
  function revisionOf(baseOid, indexDiff, worktreeDiff = '') {
104
- return revisionOfSections(
105
- baseOid,
106
- patchSections(indexDiff).map(sectionIdentity),
107
- patchSections(worktreeDiff).map(sectionIdentity),
108
- );
109
- }
110
-
111
- function revisionOfValue(baseOid, value) {
112
- if (
113
- typeof value?.indexDiff === 'string'
114
- && typeof value?.worktreeDiff === 'string'
115
- ) return revisionOf(baseOid, value.indexDiff, value.worktreeDiff);
116
- if (!value?.sections) throw new Error('checkpoint value has no content');
117
- const identities = (layer) => {
118
- if (!Array.isArray(value.sections[layer])) throw new Error('checkpoint sections are invalid');
119
- return value.sections[layer].map((section) => {
120
- if (typeof section?.text === 'string' && section.text.length > 0) {
121
- return sectionIdentity(section.text);
122
- }
123
- if (/^[0-9a-f]{64}$/.test(String(section?.payload?.checksum || ''))) {
124
- return section.payload.checksum;
125
- }
126
- throw new Error('checkpoint section has no content identity');
127
- });
128
- };
129
- return revisionOfSections(baseOid, identities('index'), identities('worktree'));
61
+ return indexDiff === '' && worktreeDiff === ''
62
+ ? null
63
+ : crypto.createHash('sha256')
64
+ .update(`${baseOid}\nindex\n${indexDiff}\nworktree\n${worktreeDiff}`, 'utf8')
65
+ .digest('hex');
130
66
  }
131
67
 
132
68
  function parseCheckpoint(text) {
@@ -153,16 +89,6 @@ function readCheckpoint(machine, repoKey) {
153
89
  }
154
90
  }
155
91
 
156
- function stampCheckpointMutation(machine, repoKey, checkpoint, mutationId) {
157
- if (!mutationId) return false;
158
- const existing = readCheckpoint(machine, repoKey);
159
- if (!existing || !sameTransition(existing, checkpoint)) return false;
160
- if (existing.mutationId === mutationId) return true;
161
- const file = checkpointFile(machine, repoKey);
162
- durable.writeJson(file, { ...existing, mutationId });
163
- return true;
164
- }
165
-
166
92
  function valueIdentity(value) {
167
93
  return JSON.stringify({
168
94
  revision: value?.revision ?? null,
@@ -170,502 +96,81 @@ function valueIdentity(value) {
170
96
  });
171
97
  }
172
98
 
99
+ function parkedIdentity(parked) {
100
+ return JSON.stringify((parked || []).map((entry) => {
101
+ const stable = { ...entry };
102
+ delete stable.version;
103
+ return stable;
104
+ }));
105
+ }
106
+
173
107
  function sameTransition(left, right) {
174
108
  return left
175
109
  && left.baseOid === right.baseOid
176
110
  && left.branch === right.branch
177
111
  && valueIdentity(left.from) === valueIdentity(right.from)
178
- && valueIdentity(left.to) === valueIdentity(right.to);
179
- }
180
-
181
- function sectionEntry(machine, repoKey, baseOid, layer, text, external) {
182
- if (!external) return { text };
183
- const digest = crypto.createHash('sha256').update(text, 'utf8').digest('hex');
184
- return {
185
- payload: require('./objects').publishPayload(
186
- `${CHECKPOINT_CONTRACT}\n${machine}\n${repoKey}\n${baseOid}\n${layer}\n${digest}`,
187
- Buffer.from(text, 'utf8'),
188
- repoKey,
189
- ),
190
- };
112
+ && valueIdentity(left.to) === valueIdentity(right.to)
113
+ && parkedIdentity(left.parked) === parkedIdentity(right.parked);
191
114
  }
192
115
 
193
116
  function checkpointValue(machine, repoKey, baseOid, indexDiff, worktreeDiff) {
194
117
  const revision = revisionOf(baseOid, indexDiff, worktreeDiff);
195
- const canonicalIndex = canonicalPatch(indexDiff);
196
- const canonicalWorktree = canonicalPatch(worktreeDiff);
197
- const bytes = Buffer.from(
198
- JSON.stringify({ indexDiff: canonicalIndex, worktreeDiff: canonicalWorktree }),
199
- 'utf8',
200
- );
201
- if (bytes.length <= MAX_INLINE_DIFF_BYTES) {
202
- return { revision, indexDiff: canonicalIndex, worktreeDiff: canonicalWorktree };
203
- }
204
-
205
- const sections = [
206
- ...patchSections(canonicalIndex).map((text, index) => ({
207
- key: `index:${index}`,
208
- layer: 'index',
209
- text,
210
- bytes: Buffer.byteLength(text, 'utf8'),
211
- })),
212
- ...patchSections(canonicalWorktree).map((text, index) => ({
213
- key: `worktree:${index}`,
214
- layer: 'worktree',
215
- text,
216
- bytes: Buffer.byteLength(text, 'utf8'),
217
- })),
218
- ];
219
- const external = new Set(
220
- sections.filter((section) => section.bytes > MAX_INLINE_DIFF_BYTES)
221
- .map((section) => section.key),
222
- );
223
- let inlineBytes = sections
224
- .filter((section) => !external.has(section.key))
225
- .reduce((total, section) => total + section.bytes, 0);
226
- for (const section of sections
227
- .filter((candidate) => !external.has(candidate.key))
228
- .sort((left, right) => right.bytes - left.bytes || left.key.localeCompare(right.key))) {
229
- if (inlineBytes <= MAX_INLINE_DIFF_BYTES) break;
230
- external.add(section.key);
231
- inlineBytes -= section.bytes;
232
- }
233
-
118
+ const bytes = Buffer.from(JSON.stringify({ indexDiff, worktreeDiff }), 'utf8');
119
+ if (bytes.length <= MAX_INLINE_DIFF_BYTES) return { revision, indexDiff, worktreeDiff };
234
120
  return {
235
121
  revision,
236
- sections: {
237
- index: sections
238
- .filter((section) => section.layer === 'index')
239
- .map((section) => sectionEntry(
240
- machine,
241
- repoKey,
242
- baseOid,
243
- section.layer,
244
- section.text,
245
- external.has(section.key),
246
- )),
247
- worktree: sections
248
- .filter((section) => section.layer === 'worktree')
249
- .map((section) => sectionEntry(
250
- machine,
251
- repoKey,
252
- baseOid,
253
- section.layer,
254
- section.text,
255
- external.has(section.key),
256
- )),
257
- },
122
+ payload: require('./objects').publishPayload(
123
+ `${CHECKPOINT_CONTRACT}\n${machine}\n${repoKey}\n${baseOid}\n${revision || 'clean'}`,
124
+ bytes,
125
+ repoKey,
126
+ ),
258
127
  };
259
128
  }
260
129
 
261
130
  function checkpointValueReady(value) {
262
- if (value?.sections) {
263
- return ['index', 'worktree'].every((layer) => (
264
- Array.isArray(value.sections[layer])
265
- && value.sections[layer].every((section) => (
266
- typeof section?.text === 'string'
267
- || section?.payload?.kind === 'chunks'
268
- ))
269
- ));
270
- }
271
131
  return !value?.payload || value.payload.kind === 'chunks';
272
132
  }
273
133
 
274
- function exactCard(card) {
275
- return {
276
- head: {
277
- branch: card.head.branch ?? null,
278
- oid: card.head.oid,
279
- detached: Boolean(card.head.detached),
280
- },
281
- objects: card.objects,
282
- branches: (card.refs || card.branches || []).map((branch) => ({
283
- name: branch.name,
284
- oid: branch.oid,
285
- objects: branch.objects,
286
- })).sort((left, right) => left.name.localeCompare(right.name)),
287
- };
288
- }
289
-
290
- function intentBase(state) {
291
- if (
292
- state?.verificationContract !== 'verified-repo-state@1'
293
- || state.materialized !== true
294
- || !Number.isInteger(state.appliedThrough)
295
- || state.appliedThrough < 0
296
- || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.targetCard?.oid || ''))
297
- || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.heldIndexTree || ''))
298
- || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(String(state.heldTree || ''))
299
- || !Array.isArray(state.branches)
300
- ) return null;
301
- return {
302
- through: state.appliedThrough,
303
- card: {
304
- head: {
305
- branch: state.targetCard.branch ?? null,
306
- oid: state.targetCard.oid,
307
- detached: !state.targetCard.branch,
308
- },
309
- branches: state.branches.map((branch) => ({
310
- name: branch.name,
311
- oid: branch.oid,
312
- })).sort((left, right) => left.name.localeCompare(right.name)),
313
- },
314
- indexTree: state.heldIndexTree,
315
- worktreeTree: state.heldTree,
316
- };
317
- }
318
-
319
- function hasIntentEffect(base, local) {
320
- const card = require('./intent')._test.cardState;
321
- return JSON.stringify(card(base.card)) !== JSON.stringify(card(local.card))
322
- || base.indexTree !== local.indexTree
323
- || base.worktreeTree !== local.worktreeTree;
324
- }
325
-
326
- async function initialIntentBase(project, card) {
327
- if (project.ownerUserId !== require('./identity').userId()) return null;
328
- const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
329
- if (!replica || replica.appliedVersion !== 0) return null;
330
- const snapshot = require('./project-stream').readSnapshot(project.file, project.resourceId);
331
- if (snapshot.version !== 0 || snapshot.card !== null || snapshot.checkpoint !== null) return null;
332
- const emptyTree = await require('./transition').emptyTree(card.path);
333
- return {
334
- through: 0,
335
- card: null,
336
- indexTree: emptyTree,
337
- worktreeTree: emptyTree,
338
- };
339
- }
340
-
341
- async function gitlinkPaths(card, observed) {
342
- const merge = require('./merge');
343
- const trees = new Set([
344
- observed.value.indexTree,
345
- observed.value.worktreeTree,
346
- card.head.oid,
347
- ...(card.refs || card.branches || []).map((branch) => branch.oid),
348
- ].filter(Boolean));
349
- const paths = new Set();
350
- for (const tree of trees) {
351
- for (const entry of await merge.gitlinksInTree(card.path, tree)) paths.add(entry);
134
+ function checkpointForSharedState(existing, {
135
+ machine,
136
+ repoKey,
137
+ branch,
138
+ baseOid,
139
+ sharedValue,
140
+ parked,
141
+ capturedAt = new Date().toISOString(),
142
+ }) {
143
+ if (existing?.baseOid === baseOid && existing.branch === branch) {
144
+ return { ...existing, parked };
352
145
  }
353
- return [...paths].sort();
354
- }
355
-
356
- /**
357
- * Capture is the only place physical state may become local authorship.
358
- * It requires a verified baseline (or an already-owned intent) and runs only
359
- * outside the reducer's materialization section.
360
- */
361
- async function recordObservedIntent(
362
- project,
363
- card,
364
- checkpoint,
365
- base,
366
- observed,
367
- options = {},
368
- ) {
369
- const intents = require('./intent');
370
- const local = {
371
- card: exactCard(card),
372
- checkpoint: {
373
- checkpoint: CHECKPOINT_CONTRACT,
374
- baseOid: card.head.oid,
375
- branch: card.head.branch ?? null,
376
- value: checkpoint.to,
377
- },
378
- indexTree: observed.value.indexTree,
379
- worktreeTree: observed.value.worktreeTree,
380
- physicalDigest: observed.digest,
381
- };
382
- const links = await gitlinkPaths(card, observed);
383
- const terminal = links.length > 0
384
- ? {
385
- code: 'unsupported_gitlink',
386
- message: `unsupported gitlink/submodule at ${links[0]}`,
387
- }
388
- : null;
389
- const previous = intents.readIntent(project);
390
- const previousSuccessor = previous?.successor || null;
391
- if (previous?.terminal && observed.digest === previous.local.physicalDigest) {
392
- return {
393
- state: 'rejected',
394
- reason: previous.terminal.code,
395
- intent: previous,
396
- unchanged: true,
397
- };
398
- }
399
- if (
400
- previousSuccessor?.terminal
401
- && observed.digest === previousSuccessor.local.physicalDigest
402
- ) {
403
- return {
404
- state: 'rejected',
405
- reason: previousSuccessor.terminal.code,
406
- intent: previous,
407
- successor: previousSuccessor,
408
- unchanged: true,
409
- };
410
- }
411
- const nextSemanticDigest = intents._test.semanticDigest({ local });
412
- const acknowledgement = previous
413
- ? intents.officialAcknowledgement(project, previous)
414
- : null;
415
- if (
416
- acknowledgement
417
- && previous.semanticDigest === nextSemanticDigest
418
- && base.through <= acknowledgement.version
419
- ) {
420
- if (previousSuccessor) intents.clearSuccessor(project, previous);
421
- return { state: 'silent', reason: 'officially-acknowledged' };
422
- }
423
- const mayReplaceAnchored = Boolean(
424
- previous?.publication
425
- && (
426
- previous.semanticDigest === options.replaceAnchoredIntent
427
- || (acknowledgement && acknowledgement.version <= base.through)
428
- || intents.publicationMayRebase(project, previous, base.through)
429
- )
430
- );
431
- if (
432
- previous?.publication
433
- && previous.semanticDigest !== nextSemanticDigest
434
- && !mayReplaceAnchored
435
- ) {
436
- const successor = intents.writeSuccessor(project, previous, {
437
- local,
438
- terminal,
439
- });
440
- if (!successor) {
441
- return {
442
- state: 'waiting',
443
- reason: 'local-intent-moving',
444
- intent: previous,
445
- };
446
- }
447
- return {
448
- state: terminal ? 'rejected' : 'captured',
449
- reason: terminal ? terminal.code : 'successor-captured',
450
- intent: previous,
451
- successor,
452
- mayMaterialize: true,
453
- };
454
- }
455
- if (!terminal && !hasIntentEffect(base, local)) {
456
- if (
457
- options.preserveNoEffectIntent === true
458
- && previous
459
- && !previous.publication
460
- ) {
461
- const saved = intents.writeIntent(project, {
462
- base,
463
- local,
464
- terminal: null,
465
- publication: null,
466
- birthMutationId: previous.birthMutationId,
467
- preserveBirthMutationId: true,
468
- });
469
- return {
470
- state: 'captured',
471
- reason: 'identity-confirmation',
472
- intent: saved,
473
- };
474
- }
475
- if (!previous?.publication || mayReplaceAnchored) {
476
- intents.clearIntent(project);
477
- return { state: 'silent', reason: 'no-local-effect' };
478
- }
479
- if (acknowledgement && acknowledgement.version <= base.through) {
480
- return { state: 'silent', reason: 'officially-acknowledged' };
481
- }
482
- }
483
- const candidate = {
484
- base,
485
- local,
486
- terminal,
487
- successor: null,
488
- publication: (
489
- previous?.semanticDigest === nextSemanticDigest
490
- && previous.publication?.baseThrough === base.through
491
- )
492
- ? previous.publication
493
- : null,
494
- birthMutationId: options.preserveBirthMutationId === true
495
- ? previous?.birthMutationId
496
- : mayReplaceAnchored
497
- ? previousSuccessor?.birthMutationId
498
- : null,
499
- preserveBirthMutationId: options.preserveBirthMutationId === true,
500
- };
501
- const saved = intents.writeIntent(project, candidate);
502
- return terminal
503
- ? { state: 'rejected', reason: terminal.code, intent: saved }
504
- : { state: 'captured', intent: saved };
505
- }
506
-
507
- async function captureIntentObservationWithinTurn(project, card, checkpoint) {
508
- const reducer = require('./reducer');
509
- const state = reducer.readRepoState(project.repoKey, project.resourceId);
510
- const base = intentBase(state) || await initialIntentBase(project, card);
511
- if (!base) {
512
- return { state: 'silent', reason: 'no-verified-baseline' };
513
- }
514
- const observed = await reducer.observePhysicalRepo(card.path);
515
- if (observed.state !== 'ready') return { state: 'waiting', reason: 'local-state-moving' };
516
- const observedRevision = revisionOf(
517
- observed.captured.baseOid,
518
- observed.captured.indexDiff,
519
- observed.captured.worktreeDiff,
520
- );
521
- const observedCard = {
522
- head: {
523
- branch: observed.value.head.branch ?? null,
524
- oid: observed.value.head.oid,
525
- detached: !observed.value.head.branch,
526
- },
527
- branches: observed.value.branches,
528
- };
529
- const intents = require('./intent');
530
- if (
531
- observed.captured.baseOid !== card.head.oid
532
- || (
533
- checkpoint
534
- && (
535
- checkpoint.to?.revision !== observedRevision
536
- || checkpoint.baseOid !== observed.captured.baseOid
537
- || (checkpoint.branch ?? null) !== (observed.value.head.branch ?? null)
538
- )
539
- )
540
- || JSON.stringify(intents._test.cardState(exactCard(card)))
541
- !== JSON.stringify(intents._test.cardState(observedCard))
542
- ) {
543
- return { state: 'waiting', reason: 'local-state-moving' };
544
- }
545
- const to = checkpointValue(
546
- machineId(),
547
- project.repoKey,
548
- observed.captured.baseOid,
549
- observed.captured.indexDiff,
550
- observed.captured.worktreeDiff,
551
- );
552
- if (!checkpointValueReady(to)) {
553
- return { state: 'waiting', reason: 'checkpoint-payload-pending' };
554
- }
555
- const capturedCheckpoint = {
146
+ if (parked.length === 0) return null;
147
+ return {
556
148
  checkpoint: CHECKPOINT_CONTRACT,
557
- machine: machineId(),
558
- repoKey: project.repoKey,
559
- branch: observed.value.head.branch ?? null,
560
- baseOid: observed.captured.baseOid,
561
- from: checkpoint?.from || null,
562
- to,
563
- capturedAt: new Date().toISOString(),
149
+ machine,
150
+ repoKey,
151
+ branch,
152
+ baseOid,
153
+ from: sharedValue,
154
+ to: sharedValue,
155
+ parked,
156
+ capturedAt,
564
157
  };
565
- if (!intents.readIntent(project) && observed.digest === state?.observedPhysicalDigest) {
566
- return {
567
- state: 'silent',
568
- reason: 'verified-state',
569
- checkpoint: capturedCheckpoint,
570
- };
571
- }
572
- const result = await recordObservedIntent(
573
- project,
574
- card,
575
- capturedCheckpoint,
576
- base,
577
- observed,
578
- );
579
- return { ...result, checkpoint: capturedCheckpoint };
580
- }
581
-
582
- function captureIntentObservation(project, card, checkpoint) {
583
- return require('./reducer').withRepoTurn(
584
- project.repoKey,
585
- 'capture',
586
- () => captureIntentObservationWithinTurn(project, card, checkpoint),
587
- );
588
- }
589
-
590
- async function captureLocalIntent(project, card, checkpoint) {
591
- const { checkpoint: capturedCheckpoint, ...outcome } = await captureIntentObservation(
592
- project,
593
- card,
594
- checkpoint,
595
- );
596
- return outcome;
597
- }
598
-
599
- /**
600
- * A valid apply WAL is also an exact baseline. Recovery first resolves every
601
- * source/target component to target while retaining values that equal
602
- * neither. Only then does this function durably name the retained state as
603
- * local intent over the official WAL target.
604
- */
605
- async function captureIntentAgainst(project, card, base, observed, options = {}) {
606
- if (!base || observed?.state !== 'ready' || !observed.captured) {
607
- return { state: 'waiting', reason: 'local-state-moving' };
608
- }
609
- const to = checkpointValue(
610
- machineId(),
611
- project.repoKey,
612
- card.head.oid,
613
- observed.captured.indexDiff,
614
- observed.captured.worktreeDiff,
615
- );
616
- if (!checkpointValueReady(to)) {
617
- return { state: 'waiting', reason: 'checkpoint-payload-pending' };
618
- }
619
- return recordObservedIntent(project, card, { to }, base, observed, options);
620
158
  }
621
159
 
622
- /**
623
- * Observe one card and turn a real local change into durable intent.
624
- *
625
- * The reducer calls this before ordered apply so an inbound event cannot beat
626
- * the debounced filesystem watcher. It still obeys the same capture rule:
627
- * without a verified baseline, the physical checkout is not authorship.
628
- */
629
- async function captureIntentForCard(project, card) {
630
- if (!project || !card?.head?.oid) {
631
- return { state: 'silent', reason: 'no-registered-card' };
632
- }
633
- return captureLocalIntent(project, card, null);
160
+ function localParkedEntries(existing, reduced, card) {
161
+ const held = existing?.baseOid === card.head?.oid
162
+ && existing.branch === card.head?.branch
163
+ ? existing.parked || []
164
+ : [];
165
+ const unpublished = (reduced?.parked || []).filter((entry) => (
166
+ !Number.isInteger(entry.version) || entry.version < 1
167
+ ));
168
+ return [...new Map([...held, ...unpublished].map((entry) => [entry.oid, entry])).values()];
634
169
  }
635
170
 
636
171
  function readCheckpointValue(baseOid, value) {
637
172
  let indexDiff = typeof value?.indexDiff === 'string' ? value.indexDiff : '';
638
173
  let worktreeDiff = typeof value?.worktreeDiff === 'string' ? value.worktreeDiff : '';
639
- if (value?.sections) {
640
- const readSections = (layer) => {
641
- if (!Array.isArray(value.sections[layer])) return { state: 'payload-invalid', text: null };
642
- const parts = [];
643
- for (const section of value.sections[layer]) {
644
- if (typeof section?.text === 'string') {
645
- parts.push(section.text);
646
- continue;
647
- }
648
- if (section?.payload?.kind === 'pending') {
649
- return { state: 'payload-pending', text: null };
650
- }
651
- if (!section?.payload) return { state: 'payload-invalid', text: null };
652
- const payload = require('./objects').readPayload(section.payload);
653
- if (payload.state !== 'ready') return { state: payload.state, text: null };
654
- parts.push(payload.bytes.toString('utf8'));
655
- }
656
- return { state: 'ready', text: parts.join('') };
657
- };
658
- const index = readSections('index');
659
- if (index.state !== 'ready') {
660
- return { state: index.state, indexDiff: null, worktreeDiff: null };
661
- }
662
- const worktree = readSections('worktree');
663
- if (worktree.state !== 'ready') {
664
- return { state: worktree.state, indexDiff: null, worktreeDiff: null };
665
- }
666
- indexDiff = index.text;
667
- worktreeDiff = worktree.text;
668
- }
669
174
  if (value?.payload) {
670
175
  if (value.payload.kind === 'pending') {
671
176
  return { state: 'payload-pending', indexDiff: null, worktreeDiff: null };
@@ -692,6 +197,17 @@ function readCheckpointValue(baseOid, value) {
692
197
  return { state: 'ready', indexDiff, worktreeDiff };
693
198
  }
694
199
 
200
+ function checkpointBaseReady(card, reduced, publication) {
201
+ return Boolean(
202
+ reduced
203
+ && publication?.current
204
+ && reduced.baseOid === card.head?.oid
205
+ && reduced.branch === card.head?.branch
206
+ && typeof reduced.indexDiff === 'string'
207
+ && typeof reduced.worktreeDiff === 'string'
208
+ );
209
+ }
210
+
695
211
  function checkpointMayPublish(repoKey) {
696
212
  return !require('./reducer').isMaterializing(repoKey);
697
213
  }
@@ -702,62 +218,108 @@ async function publishPass(replicaId = null) {
702
218
  for (const card of listRepoCards()) {
703
219
  if (replicaId && card.replica !== replicaId) continue;
704
220
  if (card.health !== 'healthy' || card.kind !== 'primary' || card.operation !== 'none') continue;
705
- const wall = require('./wall');
706
- const project = wall.projectForPath(card.path);
707
- const repoKey = project?.repoKey;
221
+ const repoKey = repoKeyOfCard(card);
708
222
  if (!repoKey || !card.head?.oid) continue;
709
223
  if (!checkpointMayPublish(repoKey)) {
710
224
  run.waiting += 1;
711
225
  continue;
712
226
  }
713
227
  try {
714
- const intent = await captureIntentObservation(project, card, null);
715
- if (intent.state === 'waiting') {
228
+ const reduced = require('./reducer').readRepoState(repoKey);
229
+ const wall = require('./wall');
230
+ if (!checkpointBaseReady(card, reduced, wall.publicationStatus(repoKey, reduced))) {
716
231
  run.waiting += 1;
717
232
  continue;
718
233
  }
719
- if (intent.state === 'rejected') {
720
- if (intent.unchanged) run.skips += 1;
721
- else run.failures += 1;
722
- continue;
723
- }
724
- if (!intent.checkpoint) {
725
- run.skips += 1;
234
+
235
+ const captured = await captureCheckpoint(card.path);
236
+ if (captured.indexDiff === null || captured.baseOid !== card.head.oid) {
237
+ run.waiting += 1;
726
238
  continue;
727
239
  }
240
+ const sharedRevision = revisionOf(card.head.oid, reduced.indexDiff, reduced.worktreeDiff);
241
+ const localRevision = revisionOf(card.head.oid, captured.indexDiff, captured.worktreeDiff);
728
242
  const file = checkpointFile(machine, repoKey);
729
243
  const existing = readCheckpoint(machine, repoKey);
730
- const to = intent.checkpoint.to;
731
- const snapshot = project && fs.existsSync(project.file)
732
- ? require('./project-stream').readSnapshot(project.file, project.resourceId)
733
- : null;
734
- const onOfficialCard = snapshot?.card
735
- && wall._test.sameCardState(card, snapshot.card);
736
- const from = onOfficialCard
737
- ? snapshot.checkpoint.value
738
- : { revision: null, indexDiff: '', worktreeDiff: '' };
739
- const envelope = {
740
- ...intent.checkpoint,
741
- from,
742
- };
743
- if (onOfficialCard && valueIdentity(from) === valueIdentity(to)) {
744
- if (existing) {
745
- fs.rmSync(file, { force: true });
244
+ const parked = await Promise.all(localParkedEntries(existing, reduced, card).map(async (entry) => ({
245
+ ...entry,
246
+ objects: await require('./objects').bundleSourceFor(card.path, repoKey, entry.oid),
247
+ })));
248
+ if (!wall.publicationStatus(repoKey, reduced).current) {
249
+ run.waiting += 1;
250
+ continue;
251
+ }
252
+
253
+ // Exact equality means the working tree merely materialized the shared
254
+ // checkpoint. Keep the writer's last transition because it remains part
255
+ // of the reducer input. A newly parked commit still rides as metadata on
256
+ // a no-op transition; the working tree itself is never echoed.
257
+ if (sharedRevision === localRevision) {
258
+ const refreshed = checkpointForSharedState(existing, {
259
+ machine,
260
+ repoKey,
261
+ branch: card.head.branch,
262
+ baseOid: card.head.oid,
263
+ sharedValue: checkpointValue(
264
+ machine,
265
+ repoKey,
266
+ card.head.oid,
267
+ reduced.indexDiff,
268
+ reduced.worktreeDiff,
269
+ ),
270
+ parked,
271
+ });
272
+ if (refreshed && !sameTransition(existing, refreshed)) {
273
+ fs.writeFileSync(file, `${JSON.stringify(refreshed)}\n`);
746
274
  appendStateEvent({
747
275
  resource: 'repo-checkpoints',
748
- op: 'delete',
276
+ op: 'update',
749
277
  id: card.replica,
750
- source: 'repo-checkpoint:covered',
278
+ value: refreshed,
279
+ source: 'repo-checkpoint:parked-objects',
751
280
  });
281
+ run.published += 1;
282
+ continue;
752
283
  }
753
284
  run.skips += 1;
754
285
  continue;
755
286
  }
287
+
288
+ const from = checkpointValue(
289
+ machine,
290
+ repoKey,
291
+ card.head.oid,
292
+ reduced.indexDiff,
293
+ reduced.worktreeDiff,
294
+ );
295
+ const to = checkpointValue(
296
+ machine,
297
+ repoKey,
298
+ card.head.oid,
299
+ captured.indexDiff,
300
+ captured.worktreeDiff,
301
+ );
302
+ if (!checkpointValueReady(from) || !checkpointValueReady(to)) {
303
+ run.waiting += 1;
304
+ continue;
305
+ }
306
+ const envelope = {
307
+ checkpoint: CHECKPOINT_CONTRACT,
308
+ machine,
309
+ repoKey,
310
+ branch: card.head.branch,
311
+ baseOid: card.head.oid,
312
+ from,
313
+ to,
314
+ parked,
315
+ capturedAt: new Date().toISOString(),
316
+ };
756
317
  if (sameTransition(existing, envelope)) {
757
318
  run.skips += 1;
758
319
  continue;
759
320
  }
760
- durable.writeJson(file, envelope);
321
+ fs.mkdirSync(path.dirname(file), { recursive: true });
322
+ fs.writeFileSync(file, `${JSON.stringify(envelope)}\n`);
761
323
  appendStateEvent({
762
324
  resource: 'repo-checkpoints',
763
325
  op: existing ? 'update' : 'insert',
@@ -845,7 +407,10 @@ function checkpointStats() {
845
407
 
846
408
  module.exports = {
847
409
  _test: {
410
+ checkpointBaseReady,
411
+ checkpointForSharedState,
848
412
  checkpointMayPublish,
413
+ localParkedEntries,
849
414
  },
850
415
  CHECKPOINT_CONTRACT,
851
416
  MAX_INLINE_DIFF_BYTES,
@@ -853,18 +418,13 @@ module.exports = {
853
418
  checkpointStats,
854
419
  checkpointValue,
855
420
  checkpointValueReady,
856
- captureIntentAgainst,
857
- captureIntentForCard,
858
- captureLocalIntent,
859
421
  parseCheckpoint,
860
422
  publishCheckpoints,
861
423
  readCheckpoint,
862
424
  readCheckpointValue,
863
425
  repoKeyOfCard,
864
426
  revisionOf,
865
- revisionOfValue,
866
427
  sameTransition,
867
- stampCheckpointMutation,
868
428
  startCheckpointService,
869
429
  stopCheckpointService,
870
430
  };