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,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The shared rig for the doorbell grammar matrix suites. Habitats and
|
|
5
|
+
* the transcript judge only — the grids themselves live in
|
|
6
|
+
* doorbell.matrix.life.test.js and doorbell.matrix.watch.test.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const Database = require('better-sqlite3');
|
|
16
|
+
const { createObserver } = require('../observer');
|
|
17
|
+
|
|
18
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-matrix-'));
|
|
19
|
+
|
|
20
|
+
function cleanup() {
|
|
21
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeDir(name) {
|
|
25
|
+
return fs.realpathSync(fs.mkdtempSync(path.join(tempRoot, `${name}-`)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function gitInit(dir) {
|
|
29
|
+
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeObserver(options = {}) {
|
|
33
|
+
const events = [];
|
|
34
|
+
const observer = createObserver({
|
|
35
|
+
db: new Database(':memory:'),
|
|
36
|
+
emit: (event) => events.push(event),
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
return { observer, events };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A files root over freshly laid ground, marked after enrollment. */
|
|
43
|
+
function filesRoot(lay = () => {}) {
|
|
44
|
+
const dir = makeDir('cell');
|
|
45
|
+
lay(dir);
|
|
46
|
+
const { observer, events } = makeObserver();
|
|
47
|
+
const root = observer.enrollRoot(dir);
|
|
48
|
+
return { dir, events, observer, root, mark: events.length };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The restart column's shape: enroll, let the first observer die,
|
|
53
|
+
* mutate while nothing runs, and return a fresh observer's voice over
|
|
54
|
+
* the same store — converged by start() ALONE, the real startup
|
|
55
|
+
* entrance, never an assisted scan.
|
|
56
|
+
*/
|
|
57
|
+
function afterRestart(lay, mutate) {
|
|
58
|
+
const dir = makeDir('cell');
|
|
59
|
+
lay(dir);
|
|
60
|
+
const db = new Database(':memory:');
|
|
61
|
+
createObserver({ db, emit: () => {} }).enrollRoot(dir);
|
|
62
|
+
mutate(dir);
|
|
63
|
+
const events = [];
|
|
64
|
+
const observer = createObserver({ db, emit: (event) => events.push(event) });
|
|
65
|
+
observer.start(); // no scanNow: starting IS the catch-up event
|
|
66
|
+
observer.stop();
|
|
67
|
+
return { dir, events, observer, root: observer.listRoots()[0], mark: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What was spoken since the mark, reduced to the grammar under exam:
|
|
72
|
+
* the event type and its addresses. Order is not part of the grammar
|
|
73
|
+
* (scans batch), so both sides are sorted; WHAT is spoken, exactly
|
|
74
|
+
* once each, is the whole claim.
|
|
75
|
+
*/
|
|
76
|
+
const VOICE = ['path', 'from', 'to'];
|
|
77
|
+
function said(spoken) {
|
|
78
|
+
return spoken
|
|
79
|
+
.map((event) => {
|
|
80
|
+
const fields = {};
|
|
81
|
+
for (const key of VOICE) if (event[key] !== undefined) fields[key] = event[key];
|
|
82
|
+
return [event.type, fields];
|
|
83
|
+
})
|
|
84
|
+
.sort((a, b) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1));
|
|
85
|
+
}
|
|
86
|
+
function speaks(rig, expected, note) {
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
said(rig.events.slice(rig.mark)),
|
|
89
|
+
said(expected.map(([type, fields]) => ({ type, ...fields }))),
|
|
90
|
+
note);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clean(rig) {
|
|
94
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
|
+
async function until(arrived, deadlineMs = 5_000) {
|
|
99
|
+
const deadline = Date.now() + deadlineMs;
|
|
100
|
+
while (!arrived() && Date.now() < deadline) await sleep(20);
|
|
101
|
+
return arrived();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
afterRestart, clean, cleanup, filesRoot, gitInit, makeDir, makeObserver,
|
|
106
|
+
said, sleep, speaks, until,
|
|
107
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE WORDING GRID + THE WATCHER ENTRANCES — the live half of the
|
|
5
|
+
* doorbell grammar matrix.
|
|
6
|
+
*
|
|
7
|
+
* The wording grid: one canonical mutation, delivered under every
|
|
8
|
+
* lawful doorbell wording, down to the worst cases the OS contract
|
|
9
|
+
* permits (a rename naming only the new name, a bare null, no doorbell
|
|
10
|
+
* at all). The conviction must be IDENTICAL under every wording: a
|
|
11
|
+
* doorbell is only a doorbell; disk decides. Proven total by
|
|
12
|
+
* construction against the ratified wording list.
|
|
13
|
+
*
|
|
14
|
+
* The watcher entrances: the observer opens exactly four genuinely
|
|
15
|
+
* distinct doorbell surfaces — the files-root recursive watcher, the
|
|
16
|
+
* repo-root recursive watcher, the single-file root's parent-directory
|
|
17
|
+
* doorbell, and every top-level root's address doorbell. One
|
|
18
|
+
* event-driven exam each proves the delivered ring converges without
|
|
19
|
+
* an assisted scan. No cross product: the life grid already proves the
|
|
20
|
+
* detection grammar these entrances feed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const test = require('node:test');
|
|
24
|
+
const assert = require('node:assert/strict');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const { installFakeWatch } = require('./fake-watch');
|
|
29
|
+
const {
|
|
30
|
+
cleanup, gitInit, makeDir, makeObserver, sleep, speaks, until,
|
|
31
|
+
} = require('./doorbell.matrix.rig');
|
|
32
|
+
|
|
33
|
+
test.after(cleanup);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A live observer over fresh ground, ringing through the fake watch so
|
|
37
|
+
* an exam can deliver exactly the wording it means to. Always torn
|
|
38
|
+
* down, watchers and fake alike.
|
|
39
|
+
*/
|
|
40
|
+
async function live(lay, run) {
|
|
41
|
+
const fake = installFakeWatch();
|
|
42
|
+
try {
|
|
43
|
+
const dir = makeDir('live');
|
|
44
|
+
lay(dir);
|
|
45
|
+
const { observer, events } = makeObserver({ settleMs: 25 });
|
|
46
|
+
const root = observer.enrollRoot(dir);
|
|
47
|
+
observer.start();
|
|
48
|
+
try {
|
|
49
|
+
await run({ fake, dir, observer, events, root, mark: events.length });
|
|
50
|
+
} finally {
|
|
51
|
+
observer.stop();
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
fake.restore();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- the wording grid -----------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const WORDINGS = Object.freeze([
|
|
61
|
+
'the old name only',
|
|
62
|
+
'the new name only',
|
|
63
|
+
'both names',
|
|
64
|
+
'no filename',
|
|
65
|
+
'the ancestor directory only',
|
|
66
|
+
'the same name twice',
|
|
67
|
+
'a burst of unrelated names',
|
|
68
|
+
'a name for unchanged ground',
|
|
69
|
+
'nothing at all',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The canonical mutation: `a/x.md` renamed to `a/y.md` behind a live
|
|
74
|
+
* observer. Whatever the doorbell says, the conviction must be this
|
|
75
|
+
* one identical sentence.
|
|
76
|
+
*/
|
|
77
|
+
const layCanon = (dir) => {
|
|
78
|
+
fs.mkdirSync(path.join(dir, 'a'));
|
|
79
|
+
fs.writeFileSync(path.join(dir, 'a', 'x.md'), 'steady');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function convictionUnder(deliver) {
|
|
83
|
+
return live(layCanon, async (rig) => {
|
|
84
|
+
fs.renameSync(path.join(rig.dir, 'a', 'x.md'), path.join(rig.dir, 'a', 'y.md'));
|
|
85
|
+
deliver(rig.fake, rig.dir);
|
|
86
|
+
assert.ok(await until(() => rig.events.slice(rig.mark).some((event) => event.type === 'file.renamed')),
|
|
87
|
+
'the wording was swallowed: no conviction arrived');
|
|
88
|
+
await sleep(100); // let any echo speak before judging the transcript
|
|
89
|
+
speaks(rig, [['file.renamed', { from: 'a/x.md', to: 'a/y.md' }]],
|
|
90
|
+
'one conviction, identical under every wording');
|
|
91
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const WORDING_GRID = {
|
|
96
|
+
'the old name only': () => convictionUnder((fake, dir) => fake.deliver(dir, 'a/x.md')),
|
|
97
|
+
'the new name only': () => convictionUnder((fake, dir) => fake.deliver(dir, 'a/y.md')),
|
|
98
|
+
'both names': () => convictionUnder((fake, dir) => {
|
|
99
|
+
fake.deliver(dir, 'a/x.md');
|
|
100
|
+
fake.deliver(dir, 'a/y.md');
|
|
101
|
+
}),
|
|
102
|
+
'no filename': () => convictionUnder((fake, dir) => fake.deliver(dir, null)),
|
|
103
|
+
'the ancestor directory only': () => convictionUnder((fake, dir) => fake.deliver(dir, 'a')),
|
|
104
|
+
'the same name twice': () => convictionUnder((fake, dir) => {
|
|
105
|
+
fake.deliver(dir, 'a/y.md');
|
|
106
|
+
fake.deliver(dir, 'a/y.md');
|
|
107
|
+
}),
|
|
108
|
+
'a burst of unrelated names': () => convictionUnder((fake, dir) => {
|
|
109
|
+
for (const name of ['lore.md', 'a/zzz.png', 'a/x.md', 'b', 'a/y.md']) fake.deliver(dir, name);
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
'a name for unchanged ground': () => live(layCanon, async (rig) => {
|
|
113
|
+
rig.fake.deliver(rig.dir, 'a/ghost.md');
|
|
114
|
+
await sleep(150);
|
|
115
|
+
assert.equal(rig.events.length, rig.mark,
|
|
116
|
+
'a doorbell cannot create belief: equal truth is silence');
|
|
117
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
'nothing at all': () => live(layCanon, async (rig) => {
|
|
121
|
+
fs.renameSync(path.join(rig.dir, 'a', 'x.md'), path.join(rig.dir, 'a', 'y.md'));
|
|
122
|
+
await sleep(150);
|
|
123
|
+
assert.equal(rig.events.slice(rig.mark).filter((event) => event.type === 'file.renamed').length, 0,
|
|
124
|
+
'no doorbell, no conviction: the observer needs evidence, not luck');
|
|
125
|
+
assert.ok(rig.observer.verify(rig.root.rootId).length > 0,
|
|
126
|
+
'but the referee is not blind: unscanned truth is reported');
|
|
127
|
+
rig.observer.scanNow(rig.root.rootId);
|
|
128
|
+
speaks(rig, [['file.renamed', { from: 'a/x.md', to: 'a/y.md' }]],
|
|
129
|
+
'and the next look convicts identically');
|
|
130
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
test('the wording grid is total: every lawful doorbell wording is faced', () => {
|
|
135
|
+
assert.deepEqual(Object.keys(WORDING_GRID), [...WORDINGS]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const [wording, cell] of Object.entries(WORDING_GRID)) {
|
|
139
|
+
test(`wording grid — the doorbell says ${wording}`, cell);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- the watcher entrances ------------------------------------------------
|
|
143
|
+
|
|
144
|
+
test('entrance — the files-root recursive watcher: a delivered ring convicts a member edit unassisted', () =>
|
|
145
|
+
live((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'v1'), async (rig) => {
|
|
146
|
+
fs.writeFileSync(path.join(rig.dir, 'notes.md'), 'v2');
|
|
147
|
+
rig.fake.deliver(rig.dir, 'notes.md');
|
|
148
|
+
assert.ok(await until(() => rig.events.slice(rig.mark).some((event) => event.type === 'file.changed')),
|
|
149
|
+
'the member doorbell never convicted');
|
|
150
|
+
await sleep(100);
|
|
151
|
+
speaks(rig, [['file.changed', { path: 'notes.md' }]]);
|
|
152
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
test('entrance — the repo-root recursive watcher: a delivered ring forwards as one raw repo doorbell', () =>
|
|
156
|
+
live((dir) => {
|
|
157
|
+
gitInit(dir);
|
|
158
|
+
fs.writeFileSync(path.join(dir, 'a.md'), 'a');
|
|
159
|
+
}, async (rig) => {
|
|
160
|
+
assert.equal(rig.root.kind, 'repo', 'territory is decided by disk');
|
|
161
|
+
fs.writeFileSync(path.join(rig.dir, 'b.md'), 'b');
|
|
162
|
+
rig.fake.deliver(rig.dir, 'b.md');
|
|
163
|
+
assert.ok(await until(() => rig.events.slice(rig.mark).some((event) => event.type === 'repo.changed')),
|
|
164
|
+
'the repo doorbell never convicted');
|
|
165
|
+
await sleep(100);
|
|
166
|
+
speaks(rig, [['repo.changed', {}]],
|
|
167
|
+
'one raw repo doorbell, zero file events — classification into card or checkpoint is the follower\'s layer');
|
|
168
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
test('entrance — the single-file root\'s parent doorbell: the parent hears what the file\'s own inode cannot', async () => {
|
|
172
|
+
const fake = installFakeWatch();
|
|
173
|
+
try {
|
|
174
|
+
const holder = makeDir('single');
|
|
175
|
+
const file = path.join(holder, 'solo.md');
|
|
176
|
+
fs.writeFileSync(file, 'v1');
|
|
177
|
+
const { observer, events } = makeObserver({ settleMs: 25 });
|
|
178
|
+
const root = observer.enrollRoot(file);
|
|
179
|
+
assert.equal(root.kind, 'file', 'the smallest root: one channel');
|
|
180
|
+
observer.start();
|
|
181
|
+
try {
|
|
182
|
+
const mark = events.length;
|
|
183
|
+
fs.writeFileSync(file, 'v2');
|
|
184
|
+
fake.deliver(holder, 'solo.md');
|
|
185
|
+
assert.ok(await until(() => events.slice(mark).some((event) => event.type === 'file.changed')),
|
|
186
|
+
'the parent doorbell never convicted');
|
|
187
|
+
await sleep(100);
|
|
188
|
+
speaks({ events, mark }, [['file.changed', { path: '' }]],
|
|
189
|
+
'the smallest root is its own address: the one channel lives at the empty relative path');
|
|
190
|
+
assert.deepEqual(observer.verify(root.rootId), []);
|
|
191
|
+
} finally {
|
|
192
|
+
observer.stop();
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
fake.restore();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('entrance — the address doorbell: the parent observes a root rename and the follow rings unassisted', () =>
|
|
200
|
+
live((dir) => fs.writeFileSync(path.join(dir, 'notes.md'), 'n'), async (rig) => {
|
|
201
|
+
const renamed = `${rig.dir}-w2`;
|
|
202
|
+
fs.renameSync(rig.dir, renamed);
|
|
203
|
+
// The lawful worst case: only the NEW name is delivered, to the
|
|
204
|
+
// PARENT — the root's own recursive watcher heard nothing.
|
|
205
|
+
rig.fake.deliver(path.dirname(rig.dir), path.basename(renamed));
|
|
206
|
+
assert.ok(await until(() => rig.events.slice(rig.mark).some((event) => event.type === 'root.moved')),
|
|
207
|
+
'the address doorbell never convicted');
|
|
208
|
+
await sleep(100);
|
|
209
|
+
speaks(rig, [['root.moved', { from: rig.dir, to: renamed }]],
|
|
210
|
+
'same identity, new address — no unavailable flicker');
|
|
211
|
+
assert.equal(rig.observer.listRoots()[0].path, renamed);
|
|
212
|
+
assert.deepEqual(rig.observer.verify(rig.root.rootId), []);
|
|
213
|
+
}));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The boundary adapter — the ONLY oracle file that knows what today's
|
|
5
|
+
* detection stack actually claims.
|
|
6
|
+
*
|
|
7
|
+
* Two slices, one per layer:
|
|
8
|
+
*
|
|
9
|
+
* `detectorSlice` reads the OBSERVER's claim: channel identity (fileId /
|
|
10
|
+
* linkId / dirId), address (rel_path), version (content_hash for a file;
|
|
11
|
+
* the written target for a link; a directory has none — existence is its
|
|
12
|
+
* whole claim), and repo-territory membership — channel evidence, judged
|
|
13
|
+
* against disk and the event ledger.
|
|
14
|
+
*
|
|
15
|
+
* `registrySlice` reads the REGISTRY's claim through its public API: the
|
|
16
|
+
* root record, active records by derived address, and the evidence layer's
|
|
17
|
+
* divergence report. The claimed slice: root identity, file AND folder
|
|
18
|
+
* records (uuid, slot, status, type, version — container membership
|
|
19
|
+
* translated through the identity binding), repo.git territory
|
|
20
|
+
* transitions, and divergence.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
function detectorSlice(observer, rootId, rootPath) {
|
|
24
|
+
const root = observer.listRoots().find((row) => row.rootId === rootId) ?? null;
|
|
25
|
+
const rows = root && root.kind === 'files' ? observer.filesForRoot(rootId) : [];
|
|
26
|
+
const files = new Map();
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
files.set(row.relPath, { uuid: row.fileId, version: row.contentHash });
|
|
29
|
+
}
|
|
30
|
+
const links = new Map();
|
|
31
|
+
for (const row of (root && root.kind === 'files' ? observer.linksForRoot(rootId) : [])) {
|
|
32
|
+
links.set(row.relPath, { uuid: row.linkId, target: row.target });
|
|
33
|
+
}
|
|
34
|
+
const dirs = new Map();
|
|
35
|
+
for (const row of (root && root.kind === 'files' ? observer.dirsForRoot(rootId) : [])) {
|
|
36
|
+
dirs.set(row.relPath, { uuid: row.dirId });
|
|
37
|
+
}
|
|
38
|
+
const territories = observer.listRoots()
|
|
39
|
+
.filter((row) => row.kind === 'repo'
|
|
40
|
+
&& (row.path === rootPath || row.path.startsWith(`${rootPath}/`)))
|
|
41
|
+
.map((row) => row.path)
|
|
42
|
+
.sort();
|
|
43
|
+
return { root, files, links, dirs, territories };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registrySlice(registry, evidence) {
|
|
47
|
+
// The oracle harness registers exactly one tree per registry, so its
|
|
48
|
+
// top record is the one parentless active.
|
|
49
|
+
const root = registry.roots()[0] ?? null;
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
divergence: evidence.divergence(),
|
|
53
|
+
/** Walk a derived address slot by slot — the registry's own chain answers. */
|
|
54
|
+
recordAtPath(address) {
|
|
55
|
+
let cursor = root;
|
|
56
|
+
if (address === '') return cursor;
|
|
57
|
+
for (const name of address.split('/')) {
|
|
58
|
+
if (!cursor) return null;
|
|
59
|
+
cursor = registry.activeAt(cursor.uuid, name);
|
|
60
|
+
}
|
|
61
|
+
return cursor;
|
|
62
|
+
},
|
|
63
|
+
/** The registry's own stored-active membership pairs for one container. */
|
|
64
|
+
activeChildren(uuid) {
|
|
65
|
+
return registry.children(uuid)
|
|
66
|
+
.filter((child) => child.status === 'active')
|
|
67
|
+
.map((child) => [child.uuid, child.name]);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { detectorSlice, registrySlice };
|