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
@@ -18,7 +18,6 @@
18
18
  */
19
19
 
20
20
  const fs = require('fs');
21
- const os = require('os');
22
21
  const path = require('path');
23
22
  const { execFile } = require('child_process');
24
23
  const { promisify } = require('util');
@@ -47,15 +46,14 @@ class GitError extends Error {
47
46
  }
48
47
  }
49
48
 
50
- async function git(args, cwd, { okCodes = [0], env = {} } = {}) {
49
+ async function git(args, cwd, { okCodes = [0] } = {}) {
51
50
  try {
52
- const options = {
51
+ const { stdout } = await execFileAsync('git', args, {
53
52
  cwd,
54
- env: { ...process.env, ...GIT_ENV, ...env },
53
+ env: { ...process.env, ...GIT_ENV },
55
54
  timeout: 15_000,
56
55
  maxBuffer: 128 * 1024 * 1024,
57
- };
58
- const { stdout } = await execFileAsync('git', args, options);
56
+ });
59
57
  return { stdout, exitCode: 0 };
60
58
  } catch (error) {
61
59
  const exitCode = typeof error.code === 'number' ? error.code : 1;
@@ -66,189 +64,6 @@ async function git(args, cwd, { okCodes = [0], env = {} } = {}) {
66
64
  }
67
65
  }
68
66
 
69
- function changeIdentity(file) {
70
- try {
71
- const stat = fs.lstatSync(file, { bigint: true });
72
- return {
73
- dev: stat.dev.toString(),
74
- ino: stat.ino.toString(),
75
- mode: stat.mode.toString(),
76
- size: stat.size.toString(),
77
- mtimeNs: stat.mtimeNs.toString(),
78
- ctimeNs: stat.ctimeNs.toString(),
79
- };
80
- } catch (error) {
81
- if (error?.code === 'ENOENT') return null;
82
- throw error;
83
- }
84
- }
85
-
86
- function sameIdentity(left, right) {
87
- return JSON.stringify(left) === JSON.stringify(right);
88
- }
89
-
90
- async function indexPath(worktreeRoot) {
91
- const { stdout } = await git(
92
- ['-C', worktreeRoot, 'rev-parse', '--git-path', 'index'],
93
- worktreeRoot,
94
- );
95
- const value = stdout.trim();
96
- return path.isAbsolute(value) ? value : path.resolve(worktreeRoot, value);
97
- }
98
-
99
- function worktreePathIdentity(worktreeRoot, relativePath) {
100
- const parts = relativePath.split('/');
101
- let current = worktreeRoot;
102
- for (let index = 0; index < parts.length; index += 1) {
103
- current = path.join(current, parts[index]);
104
- let stat;
105
- try {
106
- stat = fs.lstatSync(current, { bigint: true });
107
- } catch (error) {
108
- if (error?.code === 'ENOENT') return { kind: 'absent' };
109
- throw error;
110
- }
111
- if (index < parts.length - 1 && !stat.isDirectory()) {
112
- return { kind: 'unsafe-parent' };
113
- }
114
- if (index === parts.length - 1) {
115
- return {
116
- kind: stat.isSymbolicLink()
117
- ? 'symlink'
118
- : stat.isFile()
119
- ? 'file'
120
- : 'other',
121
- dev: stat.dev.toString(),
122
- ino: stat.ino.toString(),
123
- mode: stat.mode.toString(),
124
- size: stat.size.toString(),
125
- mtimeNs: stat.mtimeNs.toString(),
126
- ctimeNs: stat.ctimeNs.toString(),
127
- ...(stat.isSymbolicLink() ? { target: fs.readlinkSync(current) } : {}),
128
- };
129
- }
130
- }
131
- return { kind: 'absent' };
132
- }
133
-
134
- function worktreeIdentity(worktreeRoot, paths) {
135
- return Object.fromEntries(
136
- paths.map((relativePath) => [
137
- relativePath,
138
- worktreePathIdentity(worktreeRoot, relativePath),
139
- ]),
140
- );
141
- }
142
-
143
- async function ignoredNamespacePaths(worktreeRoot) {
144
- const { stdout } = await git(
145
- [
146
- '-C', worktreeRoot,
147
- 'ls-files',
148
- '--others',
149
- '--ignored',
150
- '--exclude-standard',
151
- '--directory',
152
- '-z',
153
- ],
154
- worktreeRoot,
155
- );
156
- return stdout.split('\0').filter(Boolean).map((entry) => (
157
- entry.endsWith('/') ? entry.slice(0, -1) : entry
158
- ));
159
- }
160
-
161
- function namespacePathIgnored(relativePath, ignoredPaths) {
162
- return ignoredPaths.some((ignored) => (
163
- relativePath === ignored || relativePath.startsWith(`${ignored}/`)
164
- ));
165
- }
166
-
167
- function directoryIdentity(absolutePath) {
168
- const stat = fs.lstatSync(absolutePath, { bigint: true });
169
- if (!stat.isDirectory()) return null;
170
- return {
171
- dev: stat.dev.toString(),
172
- ino: stat.ino.toString(),
173
- mode: stat.mode.toString(),
174
- mtimeNs: stat.mtimeNs.toString(),
175
- ctimeNs: stat.ctimeNs.toString(),
176
- };
177
- }
178
-
179
- /**
180
- * Git's untracked listing names files, not the namespace in which new files
181
- * can appear. Capture the complete non-ignored directory membership before
182
- * and after the byte reads so delete/list/recreate ABA cannot hide a file.
183
- * Ignored subtrees are pruned at their boundary, so dependency churn never
184
- * blocks publication.
185
- */
186
- async function visibleNamespaceIdentity(worktreeRoot) {
187
- const ignored = await ignoredNamespacePaths(worktreeRoot);
188
- const identity = {};
189
- let directories = [{ absolute: worktreeRoot, relative: '' }];
190
- while (directories.length > 0) {
191
- const next = [];
192
- for (const directory of directories) {
193
- const entries = [];
194
- try {
195
- const before = directoryIdentity(directory.absolute);
196
- if (!before) return { state: 'moving' };
197
- for (const dirent of fs.readdirSync(directory.absolute, { withFileTypes: true })) {
198
- if (directory.relative === '' && dirent.name === '.git') continue;
199
- const relative = directory.relative
200
- ? `${directory.relative}/${dirent.name}`
201
- : dirent.name;
202
- if (namespacePathIgnored(relative, ignored)) continue;
203
- const absolute = path.join(directory.absolute, dirent.name);
204
- const stat = fs.lstatSync(absolute, { bigint: true });
205
- const isDirectory = stat.isDirectory();
206
- const kind = isDirectory
207
- ? 'directory'
208
- : stat.isSymbolicLink()
209
- ? 'symlink'
210
- : stat.isFile()
211
- ? 'file'
212
- : 'other';
213
- entries.push({
214
- name: dirent.name,
215
- kind,
216
- dev: stat.dev.toString(),
217
- ino: stat.ino.toString(),
218
- ...(isDirectory ? {
219
- mode: stat.mode.toString(),
220
- mtimeNs: stat.mtimeNs.toString(),
221
- ctimeNs: stat.ctimeNs.toString(),
222
- } : {
223
- mode: stat.mode.toString(),
224
- size: stat.size.toString(),
225
- mtimeNs: stat.mtimeNs.toString(),
226
- ctimeNs: stat.ctimeNs.toString(),
227
- ...(stat.isSymbolicLink() ? { target: fs.readlinkSync(absolute) } : {}),
228
- }),
229
- });
230
- if (isDirectory && !fs.existsSync(path.join(absolute, '.git'))) {
231
- next.push({ absolute, relative });
232
- }
233
- }
234
- const after = directoryIdentity(directory.absolute);
235
- if (!after || !sameIdentity(before, after)) return { state: 'moving' };
236
- identity[directory.relative] = {
237
- directory: after,
238
- entries: entries.sort((left, right) => left.name.localeCompare(right.name)),
239
- };
240
- } catch (error) {
241
- if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
242
- return { state: 'moving' };
243
- }
244
- throw error;
245
- }
246
- }
247
- directories = next.sort((left, right) => left.relative.localeCompare(right.relative));
248
- }
249
- return { state: 'ready', value: identity };
250
- }
251
-
252
67
  const NOT_A_REPO_RE = /not a git repository/i;
253
68
 
254
69
  /**
@@ -524,16 +339,7 @@ async function captureWorktreeDiff(worktreeRoot, { maxBytes = Infinity } = {}) {
524
339
  ['-C', worktreeRoot, 'ls-files', '--others', '--exclude-standard', '-z'],
525
340
  worktreeRoot,
526
341
  );
527
- const untrackedPaths = untracked.stdout.split('\0').filter(Boolean).sort();
528
- if (untrackedPaths.some((file) => file.endsWith('/'))) {
529
- return {
530
- baseOid,
531
- diff: null,
532
- reason: 'unsupported-nested-repository',
533
- bytes,
534
- };
535
- }
536
- for (const file of untrackedPaths) {
342
+ for (const file of untracked.stdout.split('\0').filter(Boolean).sort()) {
537
343
  if (bytes > maxBytes) break;
538
344
  const creation = await git(
539
345
  ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', '--no-index', '--binary', '--full-index', '--', '/dev/null', file],
@@ -554,224 +360,46 @@ async function captureWorktreeDiff(worktreeRoot, { maxBytes = Infinity } = {}) {
554
360
  * Untracked, non-ignored files belong to the working-tree layer. Keeping
555
361
  * these deltas separate preserves what the user deliberately staged.
556
362
  */
557
- function checkpointCaptureResult({
558
- baseOid,
559
- observedOid,
560
- indexDiff,
561
- worktreeDiff,
562
- bytes,
563
- maxBytes,
564
- }) {
565
- if (observedOid !== baseOid) {
566
- return { baseOid, indexDiff: null, worktreeDiff: null, reason: 'head-moved', bytes };
567
- }
568
- if (bytes > maxBytes) {
569
- return { baseOid, indexDiff: null, worktreeDiff: null, reason: 'too-large', bytes };
570
- }
571
- return { baseOid, indexDiff, worktreeDiff, bytes };
572
- }
573
-
574
- async function captureCheckpointOnce(worktreeRoot, { maxBytes = Infinity } = {}) {
363
+ async function captureCheckpoint(worktreeRoot, { maxBytes = Infinity } = {}) {
575
364
  const baseOid = await headOid(worktreeRoot);
576
365
  if (!baseOid) return { baseOid: null, indexDiff: null, worktreeDiff: null, reason: 'unborn' };
577
- const sourceIndex = await indexPath(worktreeRoot);
578
- const snapshotRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-index-snapshot-'));
579
- const snapshotIndex = path.join(snapshotRoot, 'index');
580
- try {
581
- const initialIndexIdentity = changeIdentity(sourceIndex);
582
- if (!initialIndexIdentity) {
583
- return {
584
- baseOid,
585
- indexDiff: null,
586
- worktreeDiff: null,
587
- reason: 'index-missing',
588
- bytes: 0,
589
- };
590
- }
591
- fs.copyFileSync(sourceIndex, snapshotIndex);
592
- if (!sameIdentity(initialIndexIdentity, changeIdentity(sourceIndex))) {
593
- return {
594
- baseOid,
595
- indexDiff: null,
596
- worktreeDiff: null,
597
- reason: 'observation-moved',
598
- bytes: 0,
599
- };
600
- }
601
- const snapshotEnv = { GIT_INDEX_FILE: snapshotIndex };
602
- let stagedTree;
603
- try {
604
- stagedTree = (await git(
605
- ['-C', worktreeRoot, 'write-tree'],
606
- worktreeRoot,
607
- { env: snapshotEnv },
608
- )).stdout.trim();
609
- } catch (error) {
610
- if (
611
- error instanceof GitError
612
- && /unmerged|not uptodate|cannot write.*tree/i.test(error.stderr)
613
- ) {
614
- return {
615
- baseOid,
616
- indexDiff: null,
617
- worktreeDiff: null,
618
- reason: 'index-unmerged',
619
- bytes: 0,
620
- };
621
- }
622
- throw error;
623
- }
624
- const normalizedIndex = path.join(snapshotRoot, 'normalized-index');
625
- const normalizedEnv = { GIT_INDEX_FILE: normalizedIndex };
626
- await git(
627
- ['-C', worktreeRoot, 'read-tree', stagedTree],
628
- worktreeRoot,
629
- { env: normalizedEnv },
630
- );
631
- const tracked = await git(
632
- ['-C', worktreeRoot, 'ls-files', '-z'],
633
- worktreeRoot,
634
- { env: normalizedEnv },
635
- );
636
- const trackedPaths = tracked.stdout.split('\0').filter(Boolean);
637
- const initialNamespace = await visibleNamespaceIdentity(worktreeRoot);
638
- if (initialNamespace.state !== 'ready') {
639
- return {
640
- baseOid,
641
- indexDiff: null,
642
- worktreeDiff: null,
643
- reason: 'observation-moved',
644
- bytes: 0,
645
- };
646
- }
647
- const untracked = await git(
648
- ['-C', worktreeRoot, 'ls-files', '--others', '--exclude-standard', '-z'],
649
- worktreeRoot,
650
- { env: normalizedEnv },
651
- );
652
- const untrackedPaths = untracked.stdout.split('\0').filter(Boolean).sort();
653
- if (untrackedPaths.some((file) => file.endsWith('/'))) {
654
- return {
655
- baseOid,
656
- indexDiff: null,
657
- worktreeDiff: null,
658
- reason: 'unsupported-nested-repository',
659
- bytes: 0,
660
- };
661
- }
662
- const observedPaths = [...new Set([...trackedPaths, ...untrackedPaths])].sort();
663
- const initialWorktreeIdentity = worktreeIdentity(worktreeRoot, observedPaths);
664
366
 
665
- const staged = await git(
666
- ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', 'HEAD', stagedTree, '--binary', '--full-index'],
667
- worktreeRoot,
668
- { env: normalizedEnv },
669
- );
670
- const unstaged = await git(
671
- ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', '--binary', '--full-index'],
672
- worktreeRoot,
673
- { env: normalizedEnv },
674
- );
675
- const worktreeParts = [unstaged.stdout];
676
- let bytes = Buffer.byteLength(staged.stdout) + Buffer.byteLength(unstaged.stdout);
677
- for (const file of untrackedPaths) {
678
- if (bytes > maxBytes) break;
679
- const creation = await git(
680
- [
681
- '-C', worktreeRoot, ...PATCH_ARGS,
682
- 'diff', '--no-index', '--binary', '--full-index', '--', '/dev/null', file,
683
- ],
684
- worktreeRoot,
685
- { okCodes: [0, 1], env: normalizedEnv },
686
- );
687
- worktreeParts.push(creation.stdout);
688
- bytes += Buffer.byteLength(creation.stdout);
689
- }
690
- const observedOid = await headOid(worktreeRoot);
691
- const finalWorktreeIdentity = worktreeIdentity(worktreeRoot, observedPaths);
692
- const finalUntracked = await git(
693
- ['-C', worktreeRoot, 'ls-files', '--others', '--exclude-standard', '-z'],
367
+ const staged = await git(
368
+ ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', '--cached', 'HEAD', '--binary', '--full-index'],
369
+ worktreeRoot,
370
+ );
371
+ const unstaged = await git(
372
+ ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', '--binary', '--full-index'],
373
+ worktreeRoot,
374
+ );
375
+ const worktreeParts = [unstaged.stdout];
376
+ let bytes = Buffer.byteLength(staged.stdout) + Buffer.byteLength(unstaged.stdout);
377
+ const untracked = await git(
378
+ ['-C', worktreeRoot, 'ls-files', '--others', '--exclude-standard', '-z'],
379
+ worktreeRoot,
380
+ );
381
+ for (const file of untracked.stdout.split('\0').filter(Boolean).sort()) {
382
+ if (bytes > maxBytes) break;
383
+ const creation = await git(
384
+ ['-C', worktreeRoot, ...PATCH_ARGS, 'diff', '--no-index', '--binary', '--full-index', '--', '/dev/null', file],
694
385
  worktreeRoot,
695
- { env: normalizedEnv },
386
+ { okCodes: [0, 1] },
696
387
  );
697
- const finalNamespace = await visibleNamespaceIdentity(worktreeRoot);
698
- if (
699
- !sameIdentity(initialIndexIdentity, changeIdentity(sourceIndex))
700
- || !sameIdentity(initialWorktreeIdentity, finalWorktreeIdentity)
701
- || untracked.stdout !== finalUntracked.stdout
702
- || finalNamespace.state !== 'ready'
703
- || !sameIdentity(initialNamespace.value, finalNamespace.value)
704
- ) {
705
- return {
706
- baseOid,
707
- indexDiff: null,
708
- worktreeDiff: null,
709
- reason: 'observation-moved',
710
- bytes,
711
- };
712
- }
713
- return checkpointCaptureResult({
714
- baseOid,
715
- observedOid,
716
- indexDiff: staged.stdout,
717
- worktreeDiff: worktreeParts.join(''),
718
- bytes,
719
- maxBytes,
720
- });
721
- } finally {
722
- fs.rmSync(snapshotRoot, { recursive: true, force: true });
388
+ worktreeParts.push(creation.stdout);
389
+ bytes += Buffer.byteLength(creation.stdout);
390
+ }
391
+ if (bytes > maxBytes) {
392
+ return { baseOid, indexDiff: null, worktreeDiff: null, reason: 'too-large', bytes };
723
393
  }
724
- }
725
-
726
- function stableCheckpointCapture(first, second) {
727
- if (JSON.stringify(first) === JSON.stringify(second)) return second;
728
394
  return {
729
- baseOid: second?.baseOid ?? first?.baseOid ?? null,
730
- indexDiff: null,
731
- worktreeDiff: null,
732
- reason: 'observation-moved',
733
- bytes: Math.max(
734
- Number.isFinite(first?.bytes) ? first.bytes : 0,
735
- Number.isFinite(second?.bytes) ? second.bytes : 0,
736
- ),
395
+ baseOid,
396
+ indexDiff: staged.stdout,
397
+ worktreeDiff: worktreeParts.join(''),
398
+ bytes,
737
399
  };
738
400
  }
739
401
 
740
- async function captureStableCheckpoint(read, attempts = 4) {
741
- if (!Number.isInteger(attempts) || attempts < 2) {
742
- throw new Error('checkpoint capture requires at least two observations');
743
- }
744
- let previous = await read();
745
- let compared = previous;
746
- for (let attempt = 1; attempt < attempts; attempt += 1) {
747
- const current = await read();
748
- compared = stableCheckpointCapture(previous, current);
749
- if (compared.reason !== 'observation-moved') return compared;
750
- previous = current;
751
- }
752
- return compared;
753
- }
754
-
755
- /**
756
- * A checkpoint is a fact about HEAD, index, and worktree at one stable
757
- * instant. Individual Git reads cannot lock all three layers together, so a
758
- * capture is usable only after two consecutive complete captures are
759
- * byte-identical. A bounded retry absorbs filesystem metadata settling after
760
- * a save; alternating or continuously moving state still returns "moving."
761
- * Mixed layers are never promoted to provenance.
762
- */
763
- async function captureCheckpoint(worktreeRoot, options = {}) {
764
- return captureStableCheckpoint(
765
- () => captureCheckpointOnce(worktreeRoot, options),
766
- );
767
- }
768
-
769
402
  module.exports = {
770
- _test: {
771
- captureStableCheckpoint,
772
- checkpointCaptureResult,
773
- stableCheckpointCapture,
774
- },
775
403
  GitError,
776
404
  captureCheckpoint,
777
405
  captureWorktreeDiff,