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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- 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 };
|