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,389 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The conclusive detection test: seeded storms over the full alphabet of
|
|
5
|
+
* file operations, judged three ways.
|
|
6
|
+
*
|
|
7
|
+
* 1. The referee (observer.verify()) re-hashes every byte on disk and
|
|
8
|
+
* compares against the observer's beliefs — zero mismatches or fail.
|
|
9
|
+
* 2. The ledger replay: the emitted events ALONE must reconstruct the
|
|
10
|
+
* exact final disk state — proving nothing was missed and nothing was
|
|
11
|
+
* invented.
|
|
12
|
+
* 3. Silence rules: no event may ever name an excluded path.
|
|
13
|
+
*
|
|
14
|
+
* Every possible change is a composition of the alphabet below; covering the
|
|
15
|
+
* alphabet under random composition is the completeness claim. Storms are
|
|
16
|
+
* seeded (AMALGM_STORM_SEED) so any failure replays exactly.
|
|
17
|
+
*
|
|
18
|
+
* Echo suppression (noteWrite) is deliberately absent here — storms are
|
|
19
|
+
* all-external by construction; noteWrite has its own tests in
|
|
20
|
+
* observer.test.js.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const test = require('node:test');
|
|
24
|
+
const assert = require('node:assert/strict');
|
|
25
|
+
const crypto = require('crypto');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const Database = require('better-sqlite3');
|
|
31
|
+
const { createObserver } = require('../observer');
|
|
32
|
+
const { walkRoot } = require('../observer/scan');
|
|
33
|
+
const inclusion = require('../workspace/tree/inclusion');
|
|
34
|
+
|
|
35
|
+
/** The judge's own hash — deliberately independent of the production
|
|
36
|
+
* primitive, so a broken detector cannot grade its own homework. */
|
|
37
|
+
const hashFile = (absPath) =>
|
|
38
|
+
crypto.createHash('sha256').update(fs.readFileSync(absPath)).digest('hex');
|
|
39
|
+
|
|
40
|
+
const SEED = Number(process.env.AMALGM_STORM_SEED || 20260729);
|
|
41
|
+
|
|
42
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-observer-storm-'));
|
|
43
|
+
test.after(() => {
|
|
44
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
|
|
49
|
+
function mulberry32(seed) {
|
|
50
|
+
let a = seed >>> 0;
|
|
51
|
+
return () => {
|
|
52
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
53
|
+
let t = a;
|
|
54
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
55
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
56
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let stamp = 0;
|
|
61
|
+
|
|
62
|
+
/** Deterministic, always-distinct content of an exact byte length (>= 12). */
|
|
63
|
+
function bytes(size) {
|
|
64
|
+
const head = `#${(stamp++).toString(36)}-`;
|
|
65
|
+
return (head + 'x'.repeat(size)).slice(0, size);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeObserver(options = {}) {
|
|
69
|
+
const events = [];
|
|
70
|
+
const observer = createObserver({
|
|
71
|
+
db: new Database(':memory:'),
|
|
72
|
+
emit: (event) => events.push(event),
|
|
73
|
+
...options,
|
|
74
|
+
});
|
|
75
|
+
return { observer, events };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeStormRoot(name) {
|
|
79
|
+
const dir = fs.mkdtempSync(path.join(tempRoot, `${name}-`));
|
|
80
|
+
const root = fs.realpathSync(dir);
|
|
81
|
+
fs.mkdirSync(path.join(root, 'node_modules'));
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The alphabet. Each op mutates the workspace like a real writer would and
|
|
87
|
+
* keeps its own bookkeeping of what should exist — the judges never use it;
|
|
88
|
+
* only op-picking does.
|
|
89
|
+
*/
|
|
90
|
+
function makeStorm(rng, root, { timestampLies }) {
|
|
91
|
+
const dirs = [''];
|
|
92
|
+
const live = new Set();
|
|
93
|
+
const abs = (rel) => path.join(root, rel);
|
|
94
|
+
const join = (dir, name) => (dir ? `${dir}/${name}` : name);
|
|
95
|
+
const dirname = (rel) => (rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '');
|
|
96
|
+
const pick = (arr) => arr[Math.floor(rng() * arr.length)];
|
|
97
|
+
const pickLive = () => pick([...live]);
|
|
98
|
+
const newName = () => `f${(stamp++).toString(36)}.md`;
|
|
99
|
+
const size = () => 12 + Math.floor(rng() * 1500);
|
|
100
|
+
const write = (rel, n) => fs.writeFileSync(abs(rel), bytes(n));
|
|
101
|
+
|
|
102
|
+
const ops = {
|
|
103
|
+
create() {
|
|
104
|
+
const rel = join(pick(dirs), newName());
|
|
105
|
+
write(rel, size());
|
|
106
|
+
live.add(rel);
|
|
107
|
+
},
|
|
108
|
+
mkdirTree() {
|
|
109
|
+
// New nested dirs with files born instantly inside — the watcher-race case.
|
|
110
|
+
const a = join(pick(dirs), `d${(stamp++).toString(36)}`);
|
|
111
|
+
const b = `${a}/d${(stamp++).toString(36)}`;
|
|
112
|
+
fs.mkdirSync(abs(b), { recursive: true });
|
|
113
|
+
dirs.push(a, b);
|
|
114
|
+
for (let i = 0; i < 2; i += 1) {
|
|
115
|
+
const rel = join(b, newName());
|
|
116
|
+
write(rel, size());
|
|
117
|
+
live.add(rel);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
edit() {
|
|
121
|
+
write(pickLive(), size());
|
|
122
|
+
},
|
|
123
|
+
sameSizeEdit() {
|
|
124
|
+
const rel = pickLive();
|
|
125
|
+
write(rel, fs.statSync(abs(rel)).size); // bytes() is always distinct
|
|
126
|
+
},
|
|
127
|
+
atomicSave() {
|
|
128
|
+
const rel = pickLive();
|
|
129
|
+
const tmp = `${rel}.tmp${(stamp++).toString(36)}`;
|
|
130
|
+
fs.writeFileSync(abs(tmp), bytes(size()));
|
|
131
|
+
fs.renameSync(abs(tmp), abs(rel));
|
|
132
|
+
},
|
|
133
|
+
rename() {
|
|
134
|
+
const rel = pickLive();
|
|
135
|
+
const to = join(dirname(rel), newName());
|
|
136
|
+
fs.renameSync(abs(rel), abs(to));
|
|
137
|
+
live.delete(rel);
|
|
138
|
+
live.add(to);
|
|
139
|
+
},
|
|
140
|
+
move() {
|
|
141
|
+
const rel = pickLive();
|
|
142
|
+
const to = join(pick(dirs), newName());
|
|
143
|
+
fs.renameSync(abs(rel), abs(to));
|
|
144
|
+
live.delete(rel);
|
|
145
|
+
live.add(to);
|
|
146
|
+
},
|
|
147
|
+
swapNames() {
|
|
148
|
+
const [a, b] = [...live];
|
|
149
|
+
if (!a || !b) return ops.create();
|
|
150
|
+
const tmp = join(dirname(a), `swap${(stamp++).toString(36)}.md`);
|
|
151
|
+
fs.renameSync(abs(a), abs(tmp));
|
|
152
|
+
fs.renameSync(abs(b), abs(a));
|
|
153
|
+
fs.renameSync(abs(tmp), abs(b));
|
|
154
|
+
return undefined;
|
|
155
|
+
},
|
|
156
|
+
remove() {
|
|
157
|
+
const rel = pickLive();
|
|
158
|
+
fs.rmSync(abs(rel));
|
|
159
|
+
live.delete(rel);
|
|
160
|
+
},
|
|
161
|
+
recreate() {
|
|
162
|
+
const rel = pickLive();
|
|
163
|
+
fs.rmSync(abs(rel));
|
|
164
|
+
write(rel, size());
|
|
165
|
+
},
|
|
166
|
+
burst() {
|
|
167
|
+
const rel = pickLive();
|
|
168
|
+
const n = fs.statSync(abs(rel)).size;
|
|
169
|
+
for (let i = 0; i < 5; i += 1) write(rel, n); // rapid same-size versions
|
|
170
|
+
},
|
|
171
|
+
excludedChurn() {
|
|
172
|
+
// Must never surface as any event.
|
|
173
|
+
fs.writeFileSync(abs(`node_modules/junk${(stamp++).toString(36)}.js`), 'x');
|
|
174
|
+
fs.writeFileSync(abs('.env'), `SECRET=${stamp}`);
|
|
175
|
+
},
|
|
176
|
+
backdate() {
|
|
177
|
+
// The timestamp lie: same size, same inode, mtime restored. Only the
|
|
178
|
+
// doorbell's word says anything happened.
|
|
179
|
+
const rel = pickLive();
|
|
180
|
+
const before = fs.statSync(abs(rel));
|
|
181
|
+
write(rel, before.size);
|
|
182
|
+
fs.utimesSync(abs(rel), before.atime, before.mtime);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const table = [];
|
|
187
|
+
const weigh = (name, weight) => {
|
|
188
|
+
for (let i = 0; i < weight; i += 1) table.push(ops[name]);
|
|
189
|
+
};
|
|
190
|
+
weigh('create', 15);
|
|
191
|
+
weigh('mkdirTree', 6);
|
|
192
|
+
weigh('edit', 20);
|
|
193
|
+
weigh('sameSizeEdit', 12);
|
|
194
|
+
weigh('atomicSave', 10);
|
|
195
|
+
weigh('rename', 8);
|
|
196
|
+
weigh('move', 8);
|
|
197
|
+
weigh('swapNames', 4);
|
|
198
|
+
weigh('remove', 8);
|
|
199
|
+
weigh('recreate', 5);
|
|
200
|
+
weigh('burst', 6);
|
|
201
|
+
weigh('excludedChurn', 8);
|
|
202
|
+
if (timestampLies) weigh('backdate', 10);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
step() {
|
|
206
|
+
if (live.size < 5) return ops.create();
|
|
207
|
+
return pick(table)();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- the judges -------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/** Judge 2: the ledger alone must rebuild the exact final disk state. */
|
|
215
|
+
function assertLedgerReconstructsDisk(events, root, label) {
|
|
216
|
+
const model = new Map(); // fileId -> { path, hash }
|
|
217
|
+
for (const event of events) {
|
|
218
|
+
if (event.type === 'file.created') {
|
|
219
|
+
model.set(event.fileId, { path: event.path, hash: event.hash });
|
|
220
|
+
} else if (event.type === 'file.renamed') {
|
|
221
|
+
const file = model.get(event.fileId);
|
|
222
|
+
assert.ok(file, `${label}: rename on a channel the ledger never created`);
|
|
223
|
+
assert.equal(file.path, event.from, `${label}: rename .from disagrees with the ledger`);
|
|
224
|
+
file.path = event.to;
|
|
225
|
+
} else if (event.type === 'file.changed') {
|
|
226
|
+
const file = model.get(event.fileId);
|
|
227
|
+
assert.ok(file, `${label}: change on a channel the ledger never created`);
|
|
228
|
+
assert.equal(event.previousHash, file.hash, `${label}: hash chain broken at ${event.path}`);
|
|
229
|
+
file.hash = event.hash;
|
|
230
|
+
file.path = event.path;
|
|
231
|
+
} else if (event.type === 'file.deleted') {
|
|
232
|
+
assert.ok(model.delete(event.fileId), `${label}: delete on a channel the ledger never created`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const fromLedger = new Map();
|
|
237
|
+
for (const file of model.values()) {
|
|
238
|
+
assert.ok(!fromLedger.has(file.path), `${label}: two live channels claim ${file.path}`);
|
|
239
|
+
fromLedger.set(file.path, file.hash);
|
|
240
|
+
}
|
|
241
|
+
const fromDisk = new Map();
|
|
242
|
+
for (const entry of walkRoot(root, inclusion.shouldEnroll).files) {
|
|
243
|
+
fromDisk.set(entry.relPath, hashFile(path.join(root, entry.relPath)));
|
|
244
|
+
}
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
[...fromLedger.entries()].sort(),
|
|
247
|
+
[...fromDisk.entries()].sort(),
|
|
248
|
+
`${label}: replaying the ledger does not reproduce disk (seed ${SEED})`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Judge 3: excluded territory never surfaces. */
|
|
253
|
+
function assertNoExcludedEvents(events, label) {
|
|
254
|
+
for (const event of events) {
|
|
255
|
+
for (const rel of [event.path, event.from, event.to]) {
|
|
256
|
+
if (!rel) continue;
|
|
257
|
+
assert.ok(
|
|
258
|
+
inclusion.shouldEnroll(rel),
|
|
259
|
+
`${label}: event named excluded path "${rel}" (seed ${SEED})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- sanity: the referee itself ----------------------------------------------
|
|
266
|
+
|
|
267
|
+
test('the referee is not blind: unscanned changes are reported, then heal', () => {
|
|
268
|
+
const root = makeStormRoot('referee');
|
|
269
|
+
fs.writeFileSync(path.join(root, 'a.md'), bytes(20));
|
|
270
|
+
const { observer } = makeObserver();
|
|
271
|
+
observer.enrollRoot(root);
|
|
272
|
+
assert.deepEqual(observer.verify(), []);
|
|
273
|
+
|
|
274
|
+
// Three ways to drift, none scanned yet: the referee must see all three.
|
|
275
|
+
fs.writeFileSync(path.join(root, 'a.md'), bytes(20)); // stale
|
|
276
|
+
fs.writeFileSync(path.join(root, 'b.md'), bytes(20)); // unknown
|
|
277
|
+
const problems = observer.verify().map((m) => m.problem.split(':')[0]).sort();
|
|
278
|
+
assert.deepEqual(problems, ['stale', 'unknown']);
|
|
279
|
+
|
|
280
|
+
observer.scanNow();
|
|
281
|
+
fs.rmSync(path.join(root, 'a.md')); // ghost
|
|
282
|
+
assert.deepEqual(observer.verify().map((m) => m.problem.split(':')[0]), ['ghost']);
|
|
283
|
+
|
|
284
|
+
observer.scanNow();
|
|
285
|
+
assert.deepEqual(observer.verify(), []);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// --- storm 1: the compare engine, deterministic -------------------------------
|
|
289
|
+
|
|
290
|
+
test('storm (engine): ~1000 random operations, scans interleaved, judged three ways', () => {
|
|
291
|
+
const rng = mulberry32(SEED);
|
|
292
|
+
const root = makeStormRoot('engine');
|
|
293
|
+
const { observer, events } = makeObserver();
|
|
294
|
+
observer.enrollRoot(root);
|
|
295
|
+
const storm = makeStorm(rng, root, { timestampLies: false });
|
|
296
|
+
|
|
297
|
+
for (let batch = 0; batch < 150; batch += 1) {
|
|
298
|
+
const opCount = 4 + Math.floor(rng() * 6);
|
|
299
|
+
for (let i = 0; i < opCount; i += 1) storm.step();
|
|
300
|
+
if (rng() < 0.85) observer.scanNow(); // sometimes several batches coalesce
|
|
301
|
+
}
|
|
302
|
+
observer.scanNow();
|
|
303
|
+
|
|
304
|
+
assert.deepEqual(observer.verify(), [], `referee found mismatches (seed ${SEED})`);
|
|
305
|
+
assertLedgerReconstructsDisk(events, root, 'engine storm');
|
|
306
|
+
assertNoExcludedEvents(events, 'engine storm');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// --- storm 2: live watchers + timestamp lies -----------------------------------
|
|
310
|
+
|
|
311
|
+
test('storm (live): real watchers, bursts, and backdated timestamps', async () => {
|
|
312
|
+
const rng = mulberry32(SEED + 1);
|
|
313
|
+
const root = makeStormRoot('live');
|
|
314
|
+
const { observer, events } = makeObserver({ settleMs: 25 });
|
|
315
|
+
observer.enrollRoot(root);
|
|
316
|
+
observer.start();
|
|
317
|
+
const storm = makeStorm(rng, root, { timestampLies: true });
|
|
318
|
+
|
|
319
|
+
for (let i = 0; i < 200; i += 1) {
|
|
320
|
+
storm.step();
|
|
321
|
+
if (rng() < 0.3) await sleep(1 + Math.floor(rng() * 8));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Event-only convergence: after start() this test NEVER calls scanNow —
|
|
325
|
+
// the doorbells' own settle scans are the only detection allowed to run.
|
|
326
|
+
// verify() is read-only and repairs nothing; we just watch until the
|
|
327
|
+
// referee is satisfied or the deadline rules a miss.
|
|
328
|
+
const deadline = Date.now() + 15_000;
|
|
329
|
+
let mismatches = observer.verify();
|
|
330
|
+
while (mismatches.length > 0 && Date.now() < deadline) {
|
|
331
|
+
await sleep(100);
|
|
332
|
+
mismatches = observer.verify();
|
|
333
|
+
}
|
|
334
|
+
observer.stop();
|
|
335
|
+
|
|
336
|
+
assert.deepEqual(mismatches, [], `referee found mismatches (seed ${SEED + 1})`);
|
|
337
|
+
assertLedgerReconstructsDisk(events, root, 'live storm');
|
|
338
|
+
assertNoExcludedEvents(events, 'live storm');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// --- performance ----------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
test('performance: costs are one walk per scan and hashes only for suspects', async () => {
|
|
344
|
+
const root = makeStormRoot('perf');
|
|
345
|
+
for (let d = 0; d < 40; d += 1) {
|
|
346
|
+
fs.mkdirSync(path.join(root, `d${d}`));
|
|
347
|
+
for (let f = 0; f < 50; f += 1) {
|
|
348
|
+
fs.writeFileSync(path.join(root, `d${d}`, `f${f}.md`), bytes(1024));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const { observer, events } = makeObserver({ settleMs: 50 });
|
|
352
|
+
const ms = (from) => Number(process.hrtime.bigint() - from) / 1e6;
|
|
353
|
+
|
|
354
|
+
let t = process.hrtime.bigint();
|
|
355
|
+
observer.enrollRoot(root);
|
|
356
|
+
const enrollMs = ms(t);
|
|
357
|
+
assert.equal(events.filter((e) => e.type === 'file.created').length, 2000);
|
|
358
|
+
|
|
359
|
+
events.length = 0;
|
|
360
|
+
t = process.hrtime.bigint();
|
|
361
|
+
observer.scanNow();
|
|
362
|
+
const quietMs = ms(t);
|
|
363
|
+
assert.equal(events.length, 0, 'a quiet scan must be silent');
|
|
364
|
+
|
|
365
|
+
fs.writeFileSync(path.join(root, 'd7', 'f7.md'), bytes(1024));
|
|
366
|
+
t = process.hrtime.bigint();
|
|
367
|
+
observer.scanNow();
|
|
368
|
+
const editMs = ms(t);
|
|
369
|
+
assert.equal(events.length, 1, 'one edit is one event');
|
|
370
|
+
|
|
371
|
+
observer.start();
|
|
372
|
+
events.length = 0;
|
|
373
|
+
t = process.hrtime.bigint();
|
|
374
|
+
fs.writeFileSync(path.join(root, 'd9', 'f9.md'), bytes(1024));
|
|
375
|
+
const deadline = Date.now() + 5_000;
|
|
376
|
+
while (events.length === 0 && Date.now() < deadline) await sleep(5);
|
|
377
|
+
const latencyMs = ms(t);
|
|
378
|
+
observer.stop();
|
|
379
|
+
assert.equal(events.length, 1, 'live edit must arrive as one event');
|
|
380
|
+
|
|
381
|
+
console.log(
|
|
382
|
+
`# perf (2000 files, 1KB): enroll=${enrollMs.toFixed(0)}ms ` +
|
|
383
|
+
`quietScan=${quietMs.toFixed(1)}ms oneEditScan=${editMs.toFixed(1)}ms ` +
|
|
384
|
+
`saveToEventLatency=${latencyMs.toFixed(0)}ms (settle=50ms)`,
|
|
385
|
+
);
|
|
386
|
+
assert.ok(quietMs < 1000, `quiet scan too slow: ${quietMs}ms`);
|
|
387
|
+
assert.ok(editMs < 1000, `one-edit scan too slow: ${editMs}ms`);
|
|
388
|
+
assert.ok(latencyMs < 3000, `save-to-event latency too slow: ${latencyMs}ms`);
|
|
389
|
+
});
|