amalgm 0.1.239 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +6 -1
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +8 -4
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -13,7 +13,10 @@
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 machine wall, so the entire repo has one ordered stream.
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.
17
20
  */
18
21
 
19
22
  const crypto = require('crypto');
@@ -23,9 +26,10 @@ const { AMALGM_DIR } = require('../config');
23
26
  const { appendStateEvent, subscribeStateEvents } = require('../state/events');
24
27
  const { machineId } = require('./identity');
25
28
  const { listRepoCards } = require('./cards');
26
- const { captureCheckpoint } = require('./git');
29
+ const durable = require('./durable');
30
+ const { describeBytes, manifestChecksum, sha256 } = require('./content');
27
31
 
28
- const CHECKPOINT_CONTRACT = 'repo-checkpoint@9';
32
+ const CHECKPOINT_CONTRACT = 'repo-checkpoint@11';
29
33
  const MAX_INLINE_DIFF_BYTES = 512 * 1024;
30
34
  const PUBLISH_QUIET_MS = 25;
31
35
  const RECONCILE_INTERVAL_MS = 30_000;
@@ -54,7 +58,7 @@ function checkpointFile(machine, repoKey) {
54
58
  .update(`${CHECKPOINT_CONTRACT}|${machine}|${repoKey}`)
55
59
  .digest('hex')
56
60
  .slice(0, 24);
57
- return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v9', `${digest}.json`);
61
+ return path.join(AMALGM_DIR, 'state', 'repo-checkpoints-v11', `${digest}.json`);
58
62
  }
59
63
 
60
64
  /**
@@ -85,15 +89,44 @@ function canonicalPatch(diff) {
85
89
  return patchSections(diff).join('');
86
90
  }
87
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'));
101
+ }
102
+
88
103
  function revisionOf(baseOid, indexDiff, worktreeDiff = '') {
89
- return indexDiff === '' && worktreeDiff === ''
90
- ? null
91
- : crypto.createHash('sha256')
92
- .update(
93
- `${baseOid}\nindex\n${canonicalPatch(indexDiff)}\nworktree\n${canonicalPatch(worktreeDiff)}`,
94
- 'utf8',
95
- )
96
- .digest('hex');
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'));
97
130
  }
98
131
 
99
132
  function parseCheckpoint(text) {
@@ -126,8 +159,7 @@ function stampCheckpointMutation(machine, repoKey, checkpoint, mutationId) {
126
159
  if (!existing || !sameTransition(existing, checkpoint)) return false;
127
160
  if (existing.mutationId === mutationId) return true;
128
161
  const file = checkpointFile(machine, repoKey);
129
- fs.mkdirSync(path.dirname(file), { recursive: true });
130
- fs.writeFileSync(file, `${JSON.stringify({ ...existing, mutationId })}\n`);
162
+ durable.writeJson(file, { ...existing, mutationId });
131
163
  return true;
132
164
  }
133
165
 
@@ -138,21 +170,12 @@ function valueIdentity(value) {
138
170
  });
139
171
  }
140
172
 
141
- function parkedIdentity(parked) {
142
- return JSON.stringify((parked || []).map((entry) => {
143
- const stable = { ...entry };
144
- delete stable.version;
145
- return stable;
146
- }));
147
- }
148
-
149
173
  function sameTransition(left, right) {
150
174
  return left
151
175
  && left.baseOid === right.baseOid
152
176
  && left.branch === right.branch
153
177
  && valueIdentity(left.from) === valueIdentity(right.from)
154
- && valueIdentity(left.to) === valueIdentity(right.to)
155
- && parkedIdentity(left.parked) === parkedIdentity(right.parked);
178
+ && valueIdentity(left.to) === valueIdentity(right.to);
156
179
  }
157
180
 
158
181
  function sectionEntry(machine, repoKey, baseOid, layer, text, external) {
@@ -248,41 +271,366 @@ function checkpointValueReady(value) {
248
271
  return !value?.payload || value.payload.kind === 'chunks';
249
272
  }
250
273
 
251
- function checkpointForSharedState({
252
- machine,
253
- repoKey,
254
- branch,
255
- baseOid,
256
- sharedValue,
257
- parked,
258
- capturedAt = new Date().toISOString(),
259
- }) {
260
- if (parked.length === 0) return null;
274
+ function exactCard(card) {
261
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);
352
+ }
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 = {
262
556
  checkpoint: CHECKPOINT_CONTRACT,
263
- machine,
264
- repoKey,
265
- branch,
266
- baseOid,
267
- from: sharedValue,
268
- to: sharedValue,
269
- parked,
270
- capturedAt,
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(),
271
564
  };
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 };
272
580
  }
273
581
 
274
- function localParkedEntries(existing, reduced, card) {
275
- const published = new Set((reduced?.parked || []).filter((entry) => (
276
- Number.isInteger(entry.version) && entry.version > 0
277
- )).map((entry) => entry.oid));
278
- const held = existing?.baseOid === card.head?.oid
279
- && existing.branch === card.head?.branch
280
- ? (existing.parked || []).filter((entry) => !published.has(entry.oid))
281
- : [];
282
- const unpublished = (reduced?.parked || []).filter((entry) => (
283
- !Number.isInteger(entry.version) || entry.version < 1
284
- ));
285
- return [...new Map([...held, ...unpublished].map((entry) => [entry.oid, entry])).values()];
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
+ }
621
+
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);
286
634
  }
287
635
 
288
636
  function readCheckpointValue(baseOid, value) {
@@ -344,28 +692,6 @@ function readCheckpointValue(baseOid, value) {
344
692
  return { state: 'ready', indexDiff, worktreeDiff };
345
693
  }
346
694
 
347
- function checkpointBaseReady(card, reduced, publication) {
348
- return Boolean(
349
- reduced
350
- && publication?.current
351
- && reduced.baseOid === card.head?.oid
352
- && reduced.branch === card.head?.branch
353
- && typeof reduced.indexDiff === 'string'
354
- && typeof reduced.worktreeDiff === 'string'
355
- );
356
- }
357
-
358
- function checkpointWasAcknowledged(checkpoint, card, localRevision, version) {
359
- return Boolean(
360
- checkpoint
361
- && Number.isInteger(version)
362
- && version > 0
363
- && checkpoint.baseOid === card.head?.oid
364
- && checkpoint.branch === card.head?.branch
365
- && checkpoint.to?.revision === localRevision
366
- );
367
- }
368
-
369
695
  function checkpointMayPublish(repoKey) {
370
696
  return !require('./reducer').isMaterializing(repoKey);
371
697
  }
@@ -376,125 +702,62 @@ async function publishPass(replicaId = null) {
376
702
  for (const card of listRepoCards()) {
377
703
  if (replicaId && card.replica !== replicaId) continue;
378
704
  if (card.health !== 'healthy' || card.kind !== 'primary' || card.operation !== 'none') continue;
379
- const repoKey = repoKeyOfCard(card);
705
+ const wall = require('./wall');
706
+ const project = wall.projectForPath(card.path);
707
+ const repoKey = project?.repoKey;
380
708
  if (!repoKey || !card.head?.oid) continue;
381
709
  if (!checkpointMayPublish(repoKey)) {
382
710
  run.waiting += 1;
383
711
  continue;
384
712
  }
385
713
  try {
386
- const wall = require('./wall');
387
- const reduced = require('./reducer').readRepoState(
388
- repoKey,
389
- wall.projectForRepo(repoKey)?.resourceId,
390
- );
391
- if (!checkpointBaseReady(card, reduced, wall.publicationStatus(repoKey, reduced))) {
714
+ const intent = await captureIntentObservation(project, card, null);
715
+ if (intent.state === 'waiting') {
392
716
  run.waiting += 1;
393
717
  continue;
394
718
  }
395
-
396
- const captured = await captureCheckpoint(card.path);
397
- if (captured.indexDiff === null || captured.baseOid !== card.head.oid) {
398
- run.waiting += 1;
719
+ if (intent.state === 'rejected') {
720
+ if (intent.unchanged) run.skips += 1;
721
+ else run.failures += 1;
399
722
  continue;
400
723
  }
401
- const sharedRevision = revisionOf(card.head.oid, reduced.indexDiff, reduced.worktreeDiff);
402
- const localRevision = revisionOf(card.head.oid, captured.indexDiff, captured.worktreeDiff);
403
- const file = checkpointFile(machine, repoKey);
404
- const existing = readCheckpoint(machine, repoKey);
405
- const parked = await Promise.all(localParkedEntries(existing, reduced, card).map(async (entry) => ({
406
- ...entry,
407
- objects: await require('./objects').bundleSourceFor(card.path, repoKey, entry.oid),
408
- })));
409
- if (!wall.publicationStatus(repoKey, reduced).current) {
410
- run.waiting += 1;
724
+ if (!intent.checkpoint) {
725
+ run.skips += 1;
411
726
  continue;
412
727
  }
413
-
414
- // Exact equality means the working tree has no pending file write.
415
- // Remove an accepted local transition. A newly archived commit still
416
- // rides as metadata on one no-op transition until the rail accepts it.
417
- if (sharedRevision === localRevision) {
418
- const refreshed = checkpointForSharedState({
419
- machine,
420
- repoKey,
421
- branch: card.head.branch,
422
- baseOid: card.head.oid,
423
- sharedValue: checkpointValue(
424
- machine,
425
- repoKey,
426
- card.head.oid,
427
- reduced.indexDiff,
428
- reduced.worktreeDiff,
429
- ),
430
- parked,
431
- });
432
- if (!refreshed) {
433
- const project = wall.projectForRepo(repoKey);
434
- const version = wall.mutationVersion(
435
- project?.resourceId,
436
- existing?.mutationId,
437
- repoKey,
438
- );
439
- if (checkpointWasAcknowledged(existing, card, localRevision, version)) {
440
- run.skips += 1;
441
- continue;
442
- }
728
+ const file = checkpointFile(machine, repoKey);
729
+ 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) {
443
745
  fs.rmSync(file, { force: true });
444
- run.skips += 1;
445
- continue;
446
- }
447
- if (refreshed && !sameTransition(existing, refreshed)) {
448
- fs.writeFileSync(file, `${JSON.stringify(refreshed)}\n`);
449
746
  appendStateEvent({
450
747
  resource: 'repo-checkpoints',
451
- op: 'update',
748
+ op: 'delete',
452
749
  id: card.replica,
453
- value: refreshed,
454
- source: 'repo-checkpoint:parked-objects',
750
+ source: 'repo-checkpoint:covered',
455
751
  });
456
- run.published += 1;
457
- continue;
458
752
  }
459
753
  run.skips += 1;
460
754
  continue;
461
755
  }
462
-
463
- const from = checkpointValue(
464
- machine,
465
- repoKey,
466
- card.head.oid,
467
- reduced.indexDiff,
468
- reduced.worktreeDiff,
469
- );
470
- const to = checkpointValue(
471
- machine,
472
- repoKey,
473
- card.head.oid,
474
- captured.indexDiff,
475
- captured.worktreeDiff,
476
- );
477
- if (!checkpointValueReady(from) || !checkpointValueReady(to)) {
478
- run.waiting += 1;
479
- continue;
480
- }
481
- const envelope = {
482
- checkpoint: CHECKPOINT_CONTRACT,
483
- machine,
484
- repoKey,
485
- branch: card.head.branch,
486
- baseOid: card.head.oid,
487
- from,
488
- to,
489
- parked,
490
- capturedAt: new Date().toISOString(),
491
- };
492
756
  if (sameTransition(existing, envelope)) {
493
757
  run.skips += 1;
494
758
  continue;
495
759
  }
496
- fs.mkdirSync(path.dirname(file), { recursive: true });
497
- fs.writeFileSync(file, `${JSON.stringify(envelope)}\n`);
760
+ durable.writeJson(file, envelope);
498
761
  appendStateEvent({
499
762
  resource: 'repo-checkpoints',
500
763
  op: existing ? 'update' : 'insert',
@@ -582,11 +845,7 @@ function checkpointStats() {
582
845
 
583
846
  module.exports = {
584
847
  _test: {
585
- checkpointBaseReady,
586
- checkpointForSharedState,
587
848
  checkpointMayPublish,
588
- checkpointWasAcknowledged,
589
- localParkedEntries,
590
849
  },
591
850
  CHECKPOINT_CONTRACT,
592
851
  MAX_INLINE_DIFF_BYTES,
@@ -594,12 +853,16 @@ module.exports = {
594
853
  checkpointStats,
595
854
  checkpointValue,
596
855
  checkpointValueReady,
856
+ captureIntentAgainst,
857
+ captureIntentForCard,
858
+ captureLocalIntent,
597
859
  parseCheckpoint,
598
860
  publishCheckpoints,
599
861
  readCheckpoint,
600
862
  readCheckpointValue,
601
863
  repoKeyOfCard,
602
864
  revisionOf,
865
+ revisionOfValue,
603
866
  sameTransition,
604
867
  stampCheckpointMutation,
605
868
  startCheckpointService,