amalgm 0.1.245 → 0.1.246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -0,0 +1,486 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SEEDED STORMS over the full detection alphabet — the conclusive
5
+ * detection gate. Random compositions of every ratified operation kind
6
+ * over every entity kind Detect witnesses — files (text and binary),
7
+ * folders, links, and repositories, contests and refusals included —
8
+ * judged by the COMPLETE oracle at every quiet point: model vs
9
+ * disk referee, every field of every six-field record, detector slice,
10
+ * event ledger, channel law, emitted-truth, and the durable registry.
11
+ *
12
+ * Two storms, named for what they prove:
13
+ * - the engine storm drives detection with explicit scans (catch-up
14
+ * mode) and judges the NET of each random batch — burst composition
15
+ * is its territory, because the model and the detector share the
16
+ * same interval by construction;
17
+ * - the live storm rings doorbells through production wiring after
18
+ * start() and never invokes a manual scan — one operation is one
19
+ * interval (ring, drain, judge), because a live observer lawfully
20
+ * settles between operations and witnesses the intermediate
21
+ * continuity a net-state model cannot claim.
22
+ *
23
+ * The alphabet is proven TOTAL by construction: the weight table must
24
+ * name every operation, so a new op cannot ship unstormed. (Managed
25
+ * references sit outside this ground: they are minted by deliberate
26
+ * act, never by detection, so no storm operation can compose one.)
27
+ *
28
+ * Storms are seeded so any failure replays exactly. Every run replays
29
+ * the default seed plus the PERMANENT REGRESSION SEEDS — compositions
30
+ * that convicted the judge during the gate and forced a ratified law.
31
+ * Setting AMALGM_STORM_SEED switches to single-seed replay for
32
+ * diagnosis. A seed lists under the storm that convicted it: the live
33
+ * storm draws its rng from seed + 1, so the same number is a different
34
+ * composition in each storm — moving a seed between lists loses the
35
+ * discovery.
36
+ */
37
+
38
+ const test = require('node:test');
39
+ const assert = require('node:assert/strict');
40
+
41
+ const {
42
+ cleanup, fakeWatchKit, makeKit, sleep,
43
+ } = require('./entity.rig');
44
+
45
+ test.after(cleanup);
46
+
47
+ const DEFAULT_SEED = 20260802;
48
+
49
+ // Permanent regression seeds. Engine trio: 987654321 put two active
50
+ // entities on one slot (forced the slot-based occupant law), 77777 was
51
+ // the last engine conviction standing (the occupant desync under an
52
+ // ancestor rename in the same interval), 123123123 convicted through
53
+ // the parent-at-open / lens-exclusion era. Live seed: 1732050808 landed
54
+ // a rename onto standing divergent ground (forced the standing-ground
55
+ // absorption law; the only known kill of the graph.js standing-ground
56
+ // mutant — the engine storm PASSES that mutant at this seed).
57
+ const ENGINE_SEEDS = [DEFAULT_SEED, 77777, 987654321, 123123123];
58
+ const LIVE_SEEDS = [DEFAULT_SEED, 1732050808];
59
+
60
+ const replay = process.env.AMALGM_STORM_SEED
61
+ ? [Number(process.env.AMALGM_STORM_SEED)] : null;
62
+ const engineSeeds = replay ?? ENGINE_SEEDS;
63
+ const liveSeeds = replay ?? LIVE_SEEDS;
64
+
65
+ function mulberry32(seed) {
66
+ let a = seed >>> 0;
67
+ return () => {
68
+ a = (a + 0x6d2b79f5) >>> 0;
69
+ let t = a;
70
+ t = Math.imul(t ^ (t >>> 15), t | 1);
71
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
72
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
73
+ };
74
+ }
75
+
76
+ /**
77
+ * The storm: every op performs a real filesystem operation through the
78
+ * kit (which records it in the model) and keeps its own bookkeeping of
79
+ * what the DISK holds — never identity; the judges own that. `touched`
80
+ * collects the addresses each op moved, for the live storm's doorbells.
81
+ */
82
+ function makeStorm(rng, kit) {
83
+ const files = []; // relPaths of on-disk regular files
84
+ const links = []; // relPaths of on-disk symlinks
85
+ const dirs = ['']; // on-disk directories ('' = the root)
86
+ const repos = []; // dirs currently holding a .git
87
+ const touched = [];
88
+
89
+ // The name/content counter is LOCAL to the storm, so a seed's
90
+ // composition is a pure function of the seed — the same whether it
91
+ // runs alone under AMALGM_STORM_SEED or after other seeds in the
92
+ // full suite. A shared counter would break exact replay.
93
+ let stamp = 0;
94
+ /** Deterministic, always-distinct content. */
95
+ const bytes = () => `#${(stamp++).toString(36)}-${'x'.repeat(12 + Math.floor(rng() * 400))}`;
96
+
97
+ const pick = (arr) => arr[Math.floor(rng() * arr.length)];
98
+ const drop = (arr, value) => arr.splice(arr.indexOf(value), 1);
99
+ const fresh = (kind) => `${kind}${(stamp++).toString(36)}`;
100
+ const join = (dir, name) => (dir ? `${dir}/${name}` : name);
101
+ const dirname = (rel) => (rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '');
102
+ const touch = (...rels) => touched.push(...rels);
103
+
104
+ /** A dir leaves disk: everything beneath it leaves the books with it. */
105
+ const dropUnder = (relDir) => {
106
+ const gone = (rel) => rel === relDir || rel.startsWith(`${relDir}/`);
107
+ for (const list of [files, links, repos]) {
108
+ for (const rel of list.filter(gone)) drop(list, rel);
109
+ }
110
+ for (const rel of dirs.filter((d) => d && gone(d))) drop(dirs, rel);
111
+ };
112
+ /** A dir moves: everything beneath it re-addresses in the books. */
113
+ const moveUnder = (from, to) => {
114
+ const moved = (rel) => `${to}${rel.slice(from.length)}`;
115
+ for (const list of [files, links, dirs, repos]) {
116
+ for (let i = 0; i < list.length; i += 1) {
117
+ if (list[i] === from || list[i].startsWith(`${from}/`)) list[i] = moved(list[i]);
118
+ }
119
+ }
120
+ };
121
+ const linkTarget = () => pick([
122
+ () => pick(files) ?? 'nowhere/empty',
123
+ () => pick(dirs.filter(Boolean)) ?? 'nowhere/empty',
124
+ () => `nowhere/${fresh('t')}`,
125
+ ])();
126
+
127
+ const ops = {
128
+ // --- file ground -----------------------------------------------------
129
+ fileCreate() {
130
+ const rel = join(pick(dirs), `${fresh('f')}.md`);
131
+ kit.write(rel, bytes());
132
+ files.push(rel);
133
+ touch(rel);
134
+ },
135
+ fileCreateBinary() {
136
+ const rel = join(pick(dirs), `${fresh('b')}.bin`);
137
+ kit.write(rel, Buffer.from([0x00, 0xff, stamp % 256, 0x00, stamp % 251]));
138
+ files.push(rel);
139
+ touch(rel);
140
+ },
141
+ fileEdit() {
142
+ const rel = pick(files);
143
+ kit.write(rel, bytes());
144
+ touch(rel);
145
+ },
146
+ sameSizeEdit() {
147
+ // Stats lie about content: same length, different bytes.
148
+ const rel = pick(files);
149
+ kit.write(rel, `#${(stamp++).toString(36).padStart(6, '0')}-same-size`);
150
+ touch(rel);
151
+ },
152
+ atomicReplace() {
153
+ const rel = pick(files);
154
+ kit.replace(rel, bytes());
155
+ touch(rel);
156
+ },
157
+ touchSame() {
158
+ // mtime moves, bytes do not: must be silence.
159
+ kit.touchSame(pick(files));
160
+ },
161
+ fileRename() {
162
+ const rel = pick(files);
163
+ const to = join(dirname(rel), `${fresh('f')}.md`);
164
+ kit.mv(rel, to);
165
+ drop(files, rel);
166
+ files.push(to);
167
+ touch(rel, to);
168
+ },
169
+ fileMove() {
170
+ const rel = pick(files);
171
+ const to = join(pick(dirs), `${fresh('f')}.md`);
172
+ kit.mv(rel, to);
173
+ drop(files, rel);
174
+ files.push(to);
175
+ touch(rel, to);
176
+ },
177
+ fileOverwriteByMove() {
178
+ // rename(2) atomically replaces the occupant of the same kind.
179
+ const rel = pick(files);
180
+ const onto = pick(files);
181
+ if (rel === onto) return ops.fileRename();
182
+ kit.mv(rel, onto);
183
+ drop(files, rel);
184
+ touch(rel, onto);
185
+ return undefined;
186
+ },
187
+ fileSwap() {
188
+ const [a, b] = [pick(files), pick(files)];
189
+ if (a === b) return ops.fileRename();
190
+ const tmp = join(dirname(a), `${fresh('s')}.md`);
191
+ kit.mv(a, tmp);
192
+ kit.mv(b, a);
193
+ kit.mv(tmp, b);
194
+ touch(a, b, tmp);
195
+ return undefined;
196
+ },
197
+ copyDupe() {
198
+ // Identical bytes, two channels: identity is never derived from bytes.
199
+ const rel = pick(files);
200
+ const to = join(pick(dirs), `${fresh('c')}.md`);
201
+ const data = bytes();
202
+ kit.write(rel, data);
203
+ kit.copy(rel, to, data);
204
+ files.push(to);
205
+ touch(rel, to);
206
+ },
207
+ fileRemove() {
208
+ const rel = pick(files);
209
+ kit.rm(rel);
210
+ drop(files, rel);
211
+ touch(rel);
212
+ },
213
+ fileRecreate() {
214
+ // Death and rebirth inside one interval: the address never
215
+ // observably died.
216
+ const rel = pick(files);
217
+ kit.rm(rel);
218
+ kit.write(rel, bytes());
219
+ touch(rel);
220
+ },
221
+
222
+ // --- folder ground ---------------------------------------------------
223
+ mkdirTree() {
224
+ const a = join(pick(dirs), fresh('d'));
225
+ const b = `${a}/${fresh('d')}`;
226
+ kit.mkdir(b);
227
+ dirs.push(a, b);
228
+ const rel = join(b, `${fresh('f')}.md`);
229
+ kit.write(rel, bytes());
230
+ files.push(rel);
231
+ touch(a, b, rel);
232
+ },
233
+ dirRename() {
234
+ const rel = pick(dirs.filter(Boolean));
235
+ if (!rel || repos.some((r) => rel === r || rel.startsWith(`${r}/`))) return ops.mkdirTree();
236
+ const to = join(dirname(rel), fresh('d'));
237
+ kit.mvDir(rel, to);
238
+ moveUnder(rel, to);
239
+ touch(rel, to);
240
+ return undefined;
241
+ },
242
+ dirRemove() {
243
+ const rel = pick(dirs.filter(Boolean));
244
+ if (!rel) return ops.mkdirTree();
245
+ kit.rmDir(rel);
246
+ dropUnder(rel);
247
+ touch(rel);
248
+ return undefined;
249
+ },
250
+
251
+ // --- link ground -----------------------------------------------------
252
+ linkCreate() {
253
+ const rel = join(pick(dirs), `${fresh('l')}.md`);
254
+ kit.symlink(rel, linkTarget());
255
+ links.push(rel);
256
+ touch(rel);
257
+ },
258
+ linkRetarget() {
259
+ if (links.length === 0) return ops.linkCreate();
260
+ const rel = pick(links);
261
+ kit.retarget(rel, `nowhere/${fresh('t')}`);
262
+ touch(rel);
263
+ return undefined;
264
+ },
265
+ linkRename() {
266
+ if (links.length === 0) return ops.linkCreate();
267
+ const rel = pick(links);
268
+ const to = join(pick(dirs), `${fresh('l')}.md`);
269
+ kit.mvLink(rel, to);
270
+ drop(links, rel);
271
+ links.push(to);
272
+ touch(rel, to);
273
+ return undefined;
274
+ },
275
+ linkSilentRecreate() {
276
+ // A new inode under an old name with an old target: silence.
277
+ if (links.length === 0) return ops.linkCreate();
278
+ const rel = pick(links);
279
+ const target = require('fs').readlinkSync(kit.abs(rel));
280
+ kit.retarget(rel, target);
281
+ touch(rel);
282
+ return undefined;
283
+ },
284
+ linkRemove() {
285
+ if (links.length === 0) return ops.linkCreate();
286
+ const rel = pick(links);
287
+ kit.rmLink(rel);
288
+ drop(links, rel);
289
+ touch(rel);
290
+ return undefined;
291
+ },
292
+
293
+ // --- kinds crossing at a slot ---------------------------------------
294
+ linkOntoFileSlot() {
295
+ const rel = pick(files);
296
+ kit.rm(rel);
297
+ kit.symlink(rel, linkTarget());
298
+ drop(files, rel);
299
+ links.push(rel);
300
+ touch(rel);
301
+ },
302
+ fileOntoLinkSlot() {
303
+ if (links.length === 0) return ops.linkCreate();
304
+ const rel = pick(links);
305
+ kit.rmLink(rel);
306
+ kit.write(rel, bytes());
307
+ drop(links, rel);
308
+ files.push(rel);
309
+ touch(rel);
310
+ return undefined;
311
+ },
312
+
313
+ // --- contests and refusals ------------------------------------------
314
+ contestLand() {
315
+ // The owner's ground leaves and a mover lands on its slot: two
316
+ // identity claims, one address — divergence until the ground moves on.
317
+ const [mover, slot] = [pick(files), pick(files)];
318
+ if (mover === slot) return ops.fileRename();
319
+ kit.rm(slot);
320
+ kit.mv(mover, slot);
321
+ drop(files, mover);
322
+ touch(mover, slot);
323
+ return undefined;
324
+ },
325
+ containerRefusal() {
326
+ // A populated folder swapped for a file: the canonical graph refuses.
327
+ const rel = dirs.filter(Boolean).find((d) => files.some((f) => f.startsWith(`${d}/`))
328
+ && !repos.some((r) => d === r || d.startsWith(`${r}/`)));
329
+ if (!rel) return ops.mkdirTree();
330
+ kit.rmDir(rel);
331
+ dropUnder(rel);
332
+ kit.write(rel, bytes());
333
+ files.push(rel);
334
+ touch(rel);
335
+ return undefined;
336
+ },
337
+
338
+ // --- the git lens ----------------------------------------------------
339
+ repoInit() {
340
+ const rel = dirs.filter(Boolean).find((d) => !repos.some((r) => d === r
341
+ || d.startsWith(`${r}/`) || r.startsWith(`${d}/`)));
342
+ if (!rel) return ops.mkdirTree();
343
+ kit.gitInit(rel);
344
+ repos.push(rel);
345
+ touch(rel);
346
+ return undefined;
347
+ },
348
+ repoLift() {
349
+ if (repos.length === 0) return ops.repoInit();
350
+ const rel = pick(repos);
351
+ kit.gitRemove(rel);
352
+ drop(repos, rel);
353
+ touch(rel);
354
+ return undefined;
355
+ },
356
+
357
+ // --- excluded territory ----------------------------------------------
358
+ excludedChurn() {
359
+ // Must never surface as any event, channel, or record.
360
+ kit.write(`node_modules/junk${(stamp++).toString(36)}.js`, 'x');
361
+ kit.write('.env', `SECRET=${stamp}`);
362
+ touch('node_modules', '.env');
363
+ },
364
+ };
365
+
366
+ const WEIGHTS = {
367
+ fileCreate: 12,
368
+ fileCreateBinary: 4,
369
+ fileEdit: 14,
370
+ sameSizeEdit: 6,
371
+ atomicReplace: 8,
372
+ touchSame: 4,
373
+ fileRename: 7,
374
+ fileMove: 7,
375
+ fileOverwriteByMove: 3,
376
+ fileSwap: 3,
377
+ copyDupe: 3,
378
+ fileRemove: 6,
379
+ fileRecreate: 4,
380
+ mkdirTree: 6,
381
+ dirRename: 5,
382
+ dirRemove: 3,
383
+ linkCreate: 6,
384
+ linkRetarget: 5,
385
+ linkRename: 4,
386
+ linkSilentRecreate: 3,
387
+ linkRemove: 3,
388
+ linkOntoFileSlot: 2,
389
+ fileOntoLinkSlot: 2,
390
+ contestLand: 3,
391
+ containerRefusal: 2,
392
+ repoInit: 2,
393
+ repoLift: 2,
394
+ excludedChurn: 4,
395
+ };
396
+ const table = [];
397
+ for (const [name, weight] of Object.entries(WEIGHTS)) {
398
+ for (let i = 0; i < weight; i += 1) table.push(ops[name]);
399
+ }
400
+
401
+ return {
402
+ ops,
403
+ WEIGHTS,
404
+ step() {
405
+ if (files.length < 6) ops.fileCreate();
406
+ else if (files.length > 70) pick([ops.fileRemove, ops.dirRemove, ops.fileEdit])();
407
+ else pick(table)();
408
+ },
409
+ /** The addresses moved since the last drain — the live storm's bells. */
410
+ drainTouched() {
411
+ return touched.splice(0);
412
+ },
413
+ };
414
+ }
415
+
416
+ // --- totality: the alphabet cannot silently shrink or grow ------------------
417
+
418
+ test('the storm alphabet is total: every operation carries weight', () => {
419
+ const storm = makeStorm(mulberry32(DEFAULT_SEED), makeKit('alphabet'));
420
+ assert.deepEqual(Object.keys(storm.WEIGHTS).sort(), Object.keys(storm.ops).sort(),
421
+ 'the weight table must name exactly the operations — an unweighted op is an unstormed op');
422
+ for (const [name, weight] of Object.entries(storm.WEIGHTS)) {
423
+ assert.ok(weight > 0, `op ${name} has no weight: it would never run`);
424
+ }
425
+ });
426
+
427
+ // --- storm 1: the engine, catch-up mode -------------------------------------
428
+
429
+ for (const seed of engineSeeds) {
430
+ test(`engine storm: seeded random compositions, the whole oracle at every quiet point (seed ${seed})`, () => {
431
+ const rng = mulberry32(seed);
432
+ const kit = makeKit(`engine-storm-${seed}`);
433
+ kit.enroll();
434
+ const storm = makeStorm(rng, kit);
435
+
436
+ for (let batch = 0; batch < 60; batch += 1) {
437
+ const opCount = 3 + Math.floor(rng() * 5);
438
+ for (let i = 0; i < opCount; i += 1) storm.step();
439
+ kit.point();
440
+ }
441
+ });
442
+ }
443
+
444
+ // --- storm 2: live, event-only ----------------------------------------------
445
+
446
+ for (const seed of liveSeeds) {
447
+ test(`live storm: doorbells through production wiring, event-only verdicts, no manual scans (seed ${seed})`, async () => {
448
+ const rng = mulberry32(seed + 1);
449
+ const bells = fakeWatchKit();
450
+ const kit = makeKit(`live-storm-${seed}`, { watch: bells.watch, settleMs: 1 });
451
+ kit.enroll();
452
+ kit.point(); // the catch-up era ends here
453
+ kit.observer.start();
454
+ try {
455
+ const storm = makeStorm(rng, kit);
456
+
457
+ // The chain must drain ON ITS OWN: this waits by looking only — events
458
+ // stopped growing and the registry intake is clean — and never scans,
459
+ // flushes, or repairs. A system that needed help fails at the deadline.
460
+ const drained = async (deadlineMs = 10_000) => {
461
+ const deadline = Date.now() + deadlineMs;
462
+ let seen = kit.events.length;
463
+ let quiet = 0;
464
+ while (quiet < 3 || kit.evidence.dirty()) {
465
+ assert.ok(Date.now() < deadline,
466
+ `storm: the event-driven chain did not drain (seed ${seed + 1})`);
467
+ await sleep(10);
468
+ if (kit.events.length === seen && !kit.evidence.dirty()) quiet += 1;
469
+ else {
470
+ seen = kit.events.length;
471
+ quiet = 0;
472
+ }
473
+ }
474
+ };
475
+
476
+ for (let i = 0; i < 70; i += 1) {
477
+ storm.step();
478
+ for (const rel of storm.drainTouched()) bells.ringAt(kit.abs(rel));
479
+ await drained();
480
+ kit.point({ scan: false });
481
+ }
482
+ } finally {
483
+ kit.observer.stop();
484
+ }
485
+ });
486
+ }
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A deterministic stand-in for fs.watch: captures every doorbell the
5
+ * observer opens and lets an exam deliver exactly the events it means
6
+ * to — down to the lawful worst case, a rename delivering only the new
7
+ * name, which no live OS watcher can be made to pin. Install before
8
+ * `observer.start()`, always `restore()` in a finally.
9
+ */
10
+
11
+ const fs = require('fs');
12
+
13
+ function installFakeWatch() {
14
+ const watchers = [];
15
+ const realWatch = fs.watch;
16
+ fs.watch = (target, _opts, cb) => {
17
+ const entry = {
18
+ target: String(target), cb, closed: false,
19
+ on: () => {}, close: () => { entry.closed = true; },
20
+ };
21
+ watchers.push(entry);
22
+ return entry;
23
+ };
24
+ return {
25
+ /** Ring every open doorbell on `target` with one raw name. */
26
+ deliver(target, name) {
27
+ for (const entry of [...watchers]) {
28
+ if (!entry.closed && entry.target === target) entry.cb('rename', name);
29
+ }
30
+ },
31
+ restore() { fs.watch = realWatch; },
32
+ };
33
+ }
34
+
35
+ module.exports = { installFakeWatch };