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,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 };