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,497 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The quiet-point verdict — every assertion the oracle makes, in one place.
5
+ *
6
+ * Four witnesses must tell one story at every proven quiet point:
7
+ * the operation model (graph.js) — what the test actually did,
8
+ * the disk referee (referee.js) — what disk actually holds,
9
+ * the detector slice (adapter.js) — what the observer believes,
10
+ * the event ledger (ledger.js) — what the observer ever claimed.
11
+ *
12
+ * Model vs referee catches a drifting test. The records block judges every
13
+ * field of every six-field record the model holds — unique identity, one
14
+ * parentless root (the workspace, or the repo the lens makes of it),
15
+ * addresses that re-derive through the parent chain, departed records that
16
+ * persist untouched, and versions (membership payloads and canonical
17
+ * envelopes alike) that recompute from the records + evidence alone.
18
+ * Lifecycle is EXPLICIT registry law and detection never writes it: every
19
+ * record here must be `active`, and a status that moved is a conviction.
20
+ * The git lens is derived independently — covered when any ancestor is a
21
+ * repo, never a stored rollup — and the exposed slice is exactly the
22
+ * present, uncovered files. An active entity owns its (parent, name) slot,
23
+ * so at most one active child per name — reappearing ground reconnects,
24
+ * never doubles. Only containers (workspace, folder, repo.git) hold active
25
+ * children, so ground the canonical graph refuses — a file swapped onto an
26
+ * occupied folder's slot, or CONTESTED ground carrying two identity claims
27
+ * at once — must be accounted as entity-less DIVERGENCE: reported, claimed
28
+ * by nobody, records unmoved. Slice vs referee catches a blind detector.
29
+ * Ledger vs slice catches a lying event stream. The channel binder follows
30
+ * the channel law's four sentences: one channel claims at most one entity;
31
+ * an entity may temporarily hold several channels; only one stands as its
32
+ * canonical ground; and every other live claim must be reported contest or
33
+ * an honestly closed channel — whose id never rises again. Emitted-truth
34
+ * is judged per entity — or at the address the channel claimed, which is
35
+ * where contested ground and never-settled transients answer. The closing
36
+ * re-scan proves catch-up convergence: a settled detector, asked again,
37
+ * says nothing.
38
+ *
39
+ * Two modes, named for what they prove:
40
+ * { scan: true } — catch-up: the verdict drives a scan and the registry
41
+ * intake's reconciliation, then judges.
42
+ * { scan: false } — event-only: detection must already be true; the
43
+ * referee stays read-only, no scan is invoked, and the
44
+ * doorbell-driven registry intake must already have
45
+ * converged — the verdict never repairs what it judges.
46
+ */
47
+
48
+ const assert = require('node:assert/strict');
49
+ const crypto = require('node:crypto');
50
+ const path = require('path');
51
+
52
+ const { detectorSlice, registrySlice } = require('./adapter');
53
+ const { allBoundaries, readGround } = require('./referee');
54
+
55
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
56
+
57
+ const FILE_TYPES = new Set(['file.text', 'file.binary']);
58
+
59
+ /**
60
+ * Judge the model's records on their own terms, so a drifted model cannot
61
+ * hide behind the narrower slice today's detector claims. Returns the
62
+ * derived view (address + lens coverage per uuid) and the stored-active
63
+ * membership pairs per container, for the identity and version checks
64
+ * downstream.
65
+ */
66
+ function assertRecords(snap) {
67
+ const byUuid = new Map();
68
+ for (const record of snap.records) {
69
+ assert.ok(!byUuid.has(record.uuid), `oracle: two records share the uuid ${record.uuid}`);
70
+ // Lifecycle (active | trashed | deleted) moves only by explicit
71
+ // registry action — trash, restore, empty-trash. Detection performs
72
+ // none, these tests perform none, so a non-active status IS the
73
+ // conviction: something inferred lifecycle from disk.
74
+ assert.equal(record.status, 'active',
75
+ `oracle: record ${record.uuid} is "${record.status}" — detection never writes lifecycle`);
76
+ assert.ok(snap.payloads.has(record.uuid),
77
+ `oracle: record ${record.uuid} has no payload evidence`);
78
+ if (record.type === 'repo.git') {
79
+ assert.equal(snap.payloads.get(record.uuid), null,
80
+ 'oracle: repo.git payload is null-gated until card/checkpoint evidence is adapted');
81
+ }
82
+ byUuid.set(record.uuid, record);
83
+ }
84
+
85
+ const roots = snap.records.filter((record) => record.parentUUID === null);
86
+ assert.equal(roots.length, 1, 'oracle: exactly one parentless record must root the graph');
87
+ assert.ok(roots[0].type === 'workspace' || roots[0].type === 'repo.git',
88
+ 'oracle: the root record is the workspace — or the repo the lens makes of it');
89
+
90
+ // One parent may have at most one ACTIVE child with a given name: an
91
+ // active entity owns its slot, present or absent, so reappearing ground
92
+ // must reconnect — a second active claimant is a conviction. (Every
93
+ // record here is active; when registry actions exist, this filters.)
94
+ // And only CONTAINERS hold active children: an active child hanging
95
+ // from a file-typed parent means an invalid type transition was
96
+ // canonicalized.
97
+ const CONTAINERS = new Set(['workspace', 'folder', 'repo.git']);
98
+ const slots = new Set();
99
+ for (const record of snap.records) {
100
+ if (record.parentUUID === null || record.status !== 'active') continue;
101
+ const parent = byUuid.get(record.parentUUID);
102
+ assert.ok(parent && CONTAINERS.has(parent.type),
103
+ `oracle: active entity ${record.uuid} hangs from a non-container — only workspace, folder, and repo.git may hold active children`);
104
+ const slot = `${record.parentUUID}\0${record.name}`;
105
+ assert.ok(!slots.has(slot),
106
+ `oracle: two active entities claim one slot — "${record.name}" under ${record.parentUUID}`);
107
+ slots.add(slot);
108
+ }
109
+
110
+ // Walk each record to the root once: address and lens coverage both
111
+ // derive from the same chain — never from a stored rollup.
112
+ const derived = new Map(); // uuid -> { address, covered }
113
+ const derive = (record) => {
114
+ if (derived.has(record.uuid)) return derived.get(record.uuid);
115
+ const names = [];
116
+ let covered = false;
117
+ let current = record;
118
+ while (current.parentUUID !== null) {
119
+ names.unshift(current.name);
120
+ assert.ok(names.length <= byUuid.size, 'oracle: the parent graph has a cycle');
121
+ const parent = byUuid.get(current.parentUUID);
122
+ assert.ok(parent, `oracle: record ${current.uuid} names a parent with no record`);
123
+ if (parent.type === 'repo.git') covered = true;
124
+ current = parent;
125
+ }
126
+ const view = { address: names.join('/'), covered };
127
+ derived.set(record.uuid, view);
128
+ return view;
129
+ };
130
+ for (const record of snap.records) derive(record);
131
+
132
+ // The exposed slice ⇔ exactly the locally-present, uncovered files.
133
+ // Presence is evidence beside the records: a departed entity's record is
134
+ // untouched, it simply is not here.
135
+ const exposed = snap.records.filter((record) => {
136
+ if (!FILE_TYPES.has(record.type)) return false;
137
+ return snap.present.has(record.uuid) && !derived.get(record.uuid).covered;
138
+ });
139
+ assert.equal(exposed.length, snap.files.size,
140
+ 'oracle: exposed file records and the model file slice disagree in count');
141
+ for (const record of exposed) {
142
+ const rel = derived.get(record.uuid).address;
143
+ const slot = snap.files.get(rel);
144
+ assert.ok(slot, `oracle: record ${record.uuid} derives address ${rel}, which the model does not hold`);
145
+ assert.equal(slot.uuid, record.uuid, `oracle: record and file slice disagree on who owns ${rel}`);
146
+ assert.equal(snap.payloads.get(record.uuid), slot.hash,
147
+ `oracle: payload evidence drifted from content at ${rel}`);
148
+ }
149
+ // And the same for pointer ground: exposed link records ⇔ the link slice,
150
+ // payload the written target.
151
+ const exposedLinks = snap.records.filter((record) => record.type === 'link'
152
+ && snap.present.has(record.uuid) && !derived.get(record.uuid).covered);
153
+ assert.equal(exposedLinks.length, snap.links.size,
154
+ 'oracle: exposed link records and the model link slice disagree in count');
155
+ for (const record of exposedLinks) {
156
+ const rel = derived.get(record.uuid).address;
157
+ const slot = snap.links.get(rel);
158
+ assert.ok(slot, `oracle: link record ${record.uuid} derives address ${rel}, which the model does not hold`);
159
+ assert.equal(slot.uuid, record.uuid, `oracle: record and link slice disagree on who owns ${rel}`);
160
+ assert.equal(snap.payloads.get(record.uuid), slot.target,
161
+ `oracle: payload evidence drifted from the written target at ${rel}`);
162
+ }
163
+
164
+ // Both halves of the version formula recompute from records + evidence.
165
+ // Membership counts stored-active children — a locally-absent child is
166
+ // still a child, which is why a disappearance moves no record.
167
+ const memberships = new Map();
168
+ for (const record of snap.records) {
169
+ if (record.status !== 'active' || record.parentUUID === null) continue;
170
+ if (!memberships.has(record.parentUUID)) memberships.set(record.parentUUID, []);
171
+ memberships.get(record.parentUUID).push([record.uuid, record.name]);
172
+ }
173
+ for (const record of snap.records) {
174
+ if (record.type === 'folder' || record.type === 'workspace') {
175
+ assert.equal(snap.payloads.get(record.uuid),
176
+ sha256(JSON.stringify((memberships.get(record.uuid) ?? []).sort())),
177
+ `oracle: membership version of ${record.uuid} does not recompute from its stored-active children`);
178
+ }
179
+ assert.equal(record.version,
180
+ sha256(JSON.stringify(
181
+ [record.type, record.parentUUID, record.name, record.status, snap.payloads.get(record.uuid)])),
182
+ `oracle: canonical version of ${record.uuid} does not recompute from its fields + evidence`);
183
+ }
184
+
185
+ return { derived, memberships };
186
+ }
187
+
188
+ function assertQuietPoint(kit, { scan = true } = {}) {
189
+ const { observer, events, model, ledger, binder, rootPath, rootId, policy, cursor } = kit;
190
+
191
+ if (scan) observer.scanNow(rootId);
192
+ // A quiet point is a consumer lifecycle moment — but only CATCH-UP mode
193
+ // may reconcile here (the ratified startup/flush comparison). Event-only
194
+ // mode judges without helping: the doorbell-driven intake must already
195
+ // have converged, or a missed observer→registry doorbell would be
196
+ // silently repaired by the referee and never convicted.
197
+ if (kit.evidence) {
198
+ if (scan) kit.evidence.reconcile();
199
+ else {
200
+ assert.equal(kit.evidence.dirty(), false,
201
+ 'oracle: the event-driven intake had not converged — its scheduled reconciliation failed and no one may repair it here');
202
+ }
203
+ }
204
+
205
+ const snap = model.settle();
206
+ const truth = readGround(rootPath, policy);
207
+ const boundaries = allBoundaries(rootPath, policy);
208
+
209
+ // --- the two independent truths must agree, or the test itself is wrong.
210
+ // Disk = entity ground ∪ divergence: every on-disk file is either an
211
+ // entity's ground or reported entity-less divergence — never unaccounted.
212
+ for (const [relPath, hash] of truth.files) {
213
+ const expected = snap.files.get(relPath);
214
+ if (expected) {
215
+ assert.equal(expected.hash, hash, `oracle: model hash for ${relPath} drifted from disk`);
216
+ } else {
217
+ assert.equal(snap.divergent.get(relPath), hash,
218
+ `oracle: model missing ${relPath} — neither an entity's ground nor reported divergence`);
219
+ }
220
+ }
221
+ for (const relPath of snap.files.keys()) {
222
+ assert.ok(truth.files.has(relPath), `oracle: model believes in ${relPath}, disk does not`);
223
+ }
224
+ // Pointer ground under the same accounting: every on-disk symlink is a
225
+ // link entity's ground or reported divergence, never unaccounted.
226
+ for (const [relPath, target] of truth.links) {
227
+ const expected = snap.links.get(relPath);
228
+ if (expected) {
229
+ assert.equal(expected.target, target, `oracle: model target for ${relPath} drifted from disk`);
230
+ } else {
231
+ assert.equal(snap.divergent.get(relPath), target,
232
+ `oracle: model missing link ${relPath} — neither an entity's ground nor reported divergence`);
233
+ }
234
+ }
235
+ for (const relPath of snap.links.keys()) {
236
+ assert.ok(truth.links.has(relPath), `oracle: model believes in link ${relPath}, disk does not`);
237
+ }
238
+ // Directories are ground too: every on-disk directory is an entity's
239
+ // ground or reported divergence, never unaccounted — and never a ghost.
240
+ for (const relDir of truth.dirs) {
241
+ assert.ok(snap.dirs.has(relDir) || snap.divergent.has(relDir),
242
+ `oracle: model missing directory ${relDir} — neither an entity's ground nor reported divergence`);
243
+ }
244
+ for (const relDir of snap.dirs.keys()) {
245
+ assert.ok(truth.dirs.has(relDir), `oracle: model believes in directory ${relDir}, disk does not`);
246
+ }
247
+ for (const relPath of snap.divergent.keys()) {
248
+ assert.ok(truth.files.has(relPath) || truth.links.has(relPath) || truth.dirs.has(relPath),
249
+ `oracle: model claims divergent ground at ${relPath}, disk holds nothing`);
250
+ assert.ok(!snap.files.has(relPath) && !snap.links.has(relPath) && !snap.dirs.has(relPath),
251
+ `oracle: ${relPath} is claimed as entity ground AND divergence at once`);
252
+ }
253
+ const modelBounds = [...snap.territories]
254
+ .map((rel) => (rel === '' ? rootPath : path.join(rootPath, rel)))
255
+ .sort();
256
+ assert.deepEqual(modelBounds, boundaries, 'oracle: model territories drifted from disk');
257
+
258
+ // --- every field of every record is judged; lifecycle + lens derive here
259
+ const { derived, memberships } = assertRecords(snap);
260
+
261
+ // --- detector slice vs disk truth
262
+ const slice = detectorSlice(observer, rootId, rootPath);
263
+ assert.ok(slice.root, 'oracle: the enrolled root vanished from the store');
264
+ for (const [relPath, hash] of truth.files) {
265
+ const claimed = slice.files.get(relPath);
266
+ assert.ok(claimed, `oracle: detector never saw ${relPath} (missing channel)`);
267
+ assert.equal(claimed.version, hash, `oracle: detector is stale at ${relPath}`);
268
+ }
269
+ for (const relPath of slice.files.keys()) {
270
+ assert.ok(truth.files.has(relPath), `oracle: detector believes a ghost at ${relPath}`);
271
+ }
272
+ for (const [relPath, target] of truth.links) {
273
+ const claimed = slice.links.get(relPath);
274
+ assert.ok(claimed, `oracle: detector never saw link ${relPath} (missing channel)`);
275
+ assert.equal(claimed.target, target, `oracle: detector is stale at link ${relPath}`);
276
+ }
277
+ for (const relPath of slice.links.keys()) {
278
+ assert.ok(truth.links.has(relPath), `oracle: detector believes a ghost link at ${relPath}`);
279
+ }
280
+ for (const relDir of truth.dirs) {
281
+ assert.ok(slice.dirs.has(relDir), `oracle: detector never saw directory ${relDir} (missing channel)`);
282
+ }
283
+ for (const relDir of slice.dirs.keys()) {
284
+ assert.ok(truth.dirs.has(relDir), `oracle: detector believes a ghost directory at ${relDir}`);
285
+ }
286
+ assert.deepEqual(slice.territories, boundaries,
287
+ 'oracle: detector territory set diverges from the census referee');
288
+
289
+ // --- the ledger replays the fresh events
290
+ const fresh = events.slice(cursor.value);
291
+ ledger.ingest(fresh);
292
+ cursor.value = events.length;
293
+
294
+ // --- the event stream and the store must tell one story
295
+ const believed = ledger.liveFiles(rootId);
296
+ for (const [relPath, { fileId, hash }] of believed) {
297
+ const claimed = slice.files.get(relPath);
298
+ assert.ok(claimed, `oracle: events say ${relPath} lives, the store does not`);
299
+ assert.equal(claimed.uuid, fileId, `oracle: events and store disagree on the identity at ${relPath}`);
300
+ assert.equal(claimed.version, hash, `oracle: events and store disagree on the version at ${relPath}`);
301
+ }
302
+ for (const relPath of slice.files.keys()) {
303
+ assert.ok(believed.has(relPath), `oracle: the store believes ${relPath} without events to justify it`);
304
+ }
305
+ const believedLinks = ledger.liveLinks(rootId);
306
+ for (const [relPath, { linkId, target }] of believedLinks) {
307
+ const claimed = slice.links.get(relPath);
308
+ assert.ok(claimed, `oracle: events say link ${relPath} lives, the store does not`);
309
+ assert.equal(claimed.uuid, linkId, `oracle: events and store disagree on the identity at link ${relPath}`);
310
+ assert.equal(claimed.target, target, `oracle: events and store disagree on the target at ${relPath}`);
311
+ }
312
+ for (const relPath of slice.links.keys()) {
313
+ assert.ok(believedLinks.has(relPath), `oracle: the store believes link ${relPath} without events to justify it`);
314
+ }
315
+ const believedDirs = ledger.liveDirs(rootId);
316
+ for (const [relPath, { dirId }] of believedDirs) {
317
+ const claimed = slice.dirs.get(relPath);
318
+ assert.ok(claimed, `oracle: events say directory ${relPath} lives, the store does not`);
319
+ assert.equal(claimed.uuid, dirId, `oracle: events and store disagree on the identity at directory ${relPath}`);
320
+ }
321
+ for (const relPath of slice.dirs.keys()) {
322
+ assert.ok(believedDirs.has(relPath), `oracle: the store believes directory ${relPath} without events to justify it`);
323
+ }
324
+ const ledgerBounds = [...ledger.liveTerritories().keys()]
325
+ .filter((abs) => abs === rootPath || abs.startsWith(`${rootPath}/`))
326
+ .sort();
327
+ assert.deepEqual(ledgerBounds, boundaries, 'oracle: event-stream territories diverge from disk');
328
+
329
+ // --- identity: the channel law's accounting, kind-blind. Canonical
330
+ // claims are observed from the exposed slices; then EVERY live claim
331
+ // must be exactly one of — canonical ground at its entity's own address,
332
+ // contested ground the model attributes to that same entity, or a
333
+ // channel that closed honestly (and is retired forever). Channels are
334
+ // evidence; entities are identity — a record never moves for any of this.
335
+ for (const [relPath, { uuid }] of snap.files) {
336
+ binder.observe(uuid, slice.files.get(relPath).uuid, ` at ${relPath}`);
337
+ }
338
+ for (const [relPath, { uuid }] of snap.links) {
339
+ binder.observe(uuid, slice.links.get(relPath).uuid, ` at link ${relPath}`);
340
+ }
341
+ for (const [relPath, { uuid }] of snap.dirs) {
342
+ binder.observe(uuid, slice.dirs.get(relPath).uuid, ` at directory ${relPath}`);
343
+ }
344
+ const channelPlace = new Map(); // live channel -> its address in the store
345
+ for (const [relPath, claimed] of slice.files) channelPlace.set(claimed.uuid, relPath);
346
+ for (const [relPath, claimed] of slice.links) channelPlace.set(claimed.uuid, relPath);
347
+ for (const [relPath, claimed] of slice.dirs) channelPlace.set(claimed.uuid, relPath);
348
+ for (const [channelId, nodeUuid] of binder.claims()) {
349
+ const at = channelPlace.get(channelId);
350
+ if (at !== undefined
351
+ && (snap.files.get(at)?.uuid === nodeUuid
352
+ || snap.links.get(at)?.uuid === nodeUuid
353
+ || snap.dirs.get(at)?.uuid === nodeUuid)) continue; // canonical ground
354
+ if (at !== undefined && snap.contested.get(at) === nodeUuid) continue; // reported contest — resolves nothing
355
+ assert.ok(ledger.isDeleted(channelId),
356
+ `oracle: channel ${channelId} of entity ${nodeUuid} is neither canonical ground, nor reported contest, nor honestly closed`);
357
+ binder.retire(channelId);
358
+ }
359
+ const retired = binder.retiredIds();
360
+ for (const [relPath, claimed] of slice.files) {
361
+ assert.ok(!retired.has(claimed.uuid),
362
+ `oracle: retired identity ${claimed.uuid} still lives in the store at ${relPath}`);
363
+ }
364
+ for (const [relPath, claimed] of slice.links) {
365
+ assert.ok(!retired.has(claimed.uuid),
366
+ `oracle: retired identity ${claimed.uuid} still lives in the store at link ${relPath}`);
367
+ }
368
+ for (const [relPath, claimed] of slice.dirs) {
369
+ assert.ok(!retired.has(claimed.uuid),
370
+ `oracle: retired identity ${claimed.uuid} still lives in the store at directory ${relPath}`);
371
+ }
372
+
373
+ // --- emitted-truth: a version claimed for a channel must have been true
374
+ // for THAT channel's entity. The address answers instead only where the
375
+ // entity has no history to answer with: contested ground the model
376
+ // attributes to this same claimant at either boundary of the just-closed
377
+ // interval — a contest moves with its ground mid-interval, and an honest
378
+ // event at the old address answers with the boundary it fired under —
379
+ // and a channel born and dead inside one interval, never settled, never
380
+ // bound. An uncontested bound channel gets no address escape: bytes true
381
+ // at an address it claimed but under someone else's tenure are still a
382
+ // lie about this entity.
383
+ for (const event of fresh) {
384
+ if (event.type === 'file.created' || event.type === 'file.changed') {
385
+ const nodeUuid = binder.nodeOf(event.fileId);
386
+ if (nodeUuid !== undefined) {
387
+ assert.ok(
388
+ model.nodeEverTrue(nodeUuid, event.hash)
389
+ || (model.addressContestedFor(event.path, nodeUuid) && model.addressEverTrue(event.path, event.hash)),
390
+ `oracle: ${event.path} emitted a version never true for that entity nor at its contested ground`);
391
+ } else {
392
+ assert.ok(model.addressEverTrue(event.path, event.hash),
393
+ `oracle: ${event.path} emitted a version never true at that address — short-lived channels may not lie either`);
394
+ }
395
+ } else if (event.type === 'link.created' || event.type === 'link.changed') {
396
+ // The same law for pointer ground: a claimed target must have been
397
+ // written for THAT channel's entity — or at the address, where the
398
+ // entity has no history to answer with.
399
+ const nodeUuid = binder.nodeOf(event.linkId);
400
+ if (nodeUuid !== undefined) {
401
+ assert.ok(
402
+ model.nodeEverTrue(nodeUuid, event.target)
403
+ || (model.addressContestedFor(event.path, nodeUuid) && model.addressLinkEverTrue(event.path, event.target)),
404
+ `oracle: link ${event.path} emitted a target never written for that entity nor at its contested ground`);
405
+ } else {
406
+ assert.ok(model.addressLinkEverTrue(event.path, event.target),
407
+ `oracle: link ${event.path} emitted a target never written at that address — short-lived channels may not lie either`);
408
+ }
409
+ }
410
+ }
411
+
412
+ // --- the registry: durable records judged against model truth — root
413
+ // identity, file AND folder records, repo.git territory transitions,
414
+ // and divergence. Folder detection put folder identity inside the
415
+ // slice: a folder record answers for its own uuid across directory
416
+ // renames, and its membership version recomputes from the model's
417
+ // stored-active pairs translated through the identity binding. Model
418
+ // uuid ↔ registry uuid rides an observe-only binder: records are
419
+ // permanent identity, so a registry rebind — a reconnect that minted a
420
+ // second entity — is a conviction.
421
+ if (kit.registry) {
422
+ const reg = registrySlice(kit.registry, kit.evidence);
423
+ const modelRoot = snap.records.find((record) => record.parentUUID === null);
424
+ assert.ok(reg.root, 'oracle: the registry holds no root record');
425
+ kit.registryBinder.observe(modelRoot.uuid, reg.root.uuid, ' at the enrolled root');
426
+ assert.equal(reg.root.type, modelRoot.type,
427
+ 'oracle: the registry root must ride the lens on its own uuid — workspace ↔ repo.git with the model');
428
+ assert.equal(reg.root.status, 'active',
429
+ 'oracle: the registry wrote lifecycle on the root — detection never does');
430
+ // Identity first, judgment second: every compared record is bound
431
+ // before any container's membership recomputes, because membership
432
+ // pairs translate model uuids through this very binding.
433
+ const compared = [];
434
+ for (const record of snap.records) {
435
+ if (record.parentUUID === null) continue;
436
+ if (!FILE_TYPES.has(record.type) && record.type !== 'repo.git'
437
+ && record.type !== 'folder' && record.type !== 'link') continue;
438
+ const view = derived.get(record.uuid);
439
+ if (view.covered) continue; // under the lens: card evidence is not adapted yet
440
+ const regRec = reg.recordAtPath(view.address);
441
+ assert.ok(regRec, `oracle: the registry holds no record at ${view.address}`);
442
+ kit.registryBinder.observe(record.uuid, regRec.uuid, ` at ${view.address}`);
443
+ compared.push({ record, regRec, view });
444
+ }
445
+ const translated = (modelUuid, regUuid) => {
446
+ const pairs = (memberships.get(modelUuid) ?? []).map(([childUuid, name]) => {
447
+ const bound = kit.registryBinder.boundOf(childUuid);
448
+ assert.ok(bound !== undefined,
449
+ `oracle: container ${modelUuid} counts child ${childUuid} that no registry record answered for`);
450
+ return [bound, name];
451
+ });
452
+ // Registry children no model node answers for are records born of
453
+ // observation FINER than the model's settle points: a transient that
454
+ // lived and died inside one interval was truthfully observed and
455
+ // birthed, and explicit-only lifecycle keeps its record forever.
456
+ // They join the expected membership as the registry's own pairs —
457
+ // extras, never replacements: a registry record standing in for a
458
+ // model-known entity is a rebind the identity binder convicts, and
459
+ // a missing or misparented model child still breaks this recompute.
460
+ for (const [childUuid, name] of reg.activeChildren(regUuid)) {
461
+ if (kit.registryBinder.nodeOfBound(childUuid) === undefined) pairs.push([childUuid, name]);
462
+ }
463
+ return sha256(JSON.stringify(pairs.sort()));
464
+ };
465
+ for (const { record, regRec, view } of compared) {
466
+ assert.equal(regRec.type, record.type,
467
+ `oracle: registry type drifted from the model at ${view.address}`);
468
+ assert.equal(regRec.status, 'active',
469
+ `oracle: the registry wrote lifecycle at ${view.address} — detection never does`);
470
+ const payload = record.type === 'folder' ? translated(record.uuid, regRec.uuid) : snap.payloads.get(record.uuid);
471
+ assert.equal(regRec.version, sha256(JSON.stringify(
472
+ [regRec.type, regRec.parentUUID, regRec.name, regRec.status, payload])),
473
+ `oracle: registry version at ${view.address} does not recompute from its record + the model's payload evidence`);
474
+ }
475
+ const rootPayload = modelRoot.type === 'workspace' ? translated(modelRoot.uuid, reg.root.uuid) : null;
476
+ assert.equal(reg.root.version, sha256(JSON.stringify(
477
+ [reg.root.type, null, reg.root.name, reg.root.status, rootPayload])),
478
+ 'oracle: registry root version does not recompute from its record + translated membership');
479
+ assert.deepEqual([...reg.divergence].sort(), [...snap.divergent].sort(),
480
+ 'oracle: registry divergence and model divergence must be one report');
481
+ }
482
+
483
+ // --- catch-up convergence: a settled detector, asked again, says
484
+ // nothing. (repo.changed is a raw catch-up doorbell, not a state claim.)
485
+ if (scan) {
486
+ const before = events.length;
487
+ observer.scanNow(rootId);
488
+ const noise = events.slice(before).filter((event) => event.type !== 'repo.changed');
489
+ assert.deepEqual(noise, [], 'oracle: the detector had not converged — a second scan still spoke');
490
+ ledger.ingest(events.slice(before));
491
+ cursor.value = events.length;
492
+ }
493
+
494
+ return snap;
495
+ }
496
+
497
+ module.exports = { assertQuietPoint };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Binding — the identity half of the oracle's verdict. Two different laws,
5
+ * so two binders, neither carrying exceptions:
6
+ *
7
+ * `createBinder` is PERMANENT identity, strictly one-to-one: model entity ↔
8
+ * registry uuid, bound at first observation, forever. A bound pair that
9
+ * changes on either side is a conviction — a registry that minted a second
10
+ * record for one entity, or reused one record for two.
11
+ *
12
+ * `createChannelBinder` is EVIDENCE, and follows the channel law's four
13
+ * sentences: one channel claims at most one entity, ever. An entity may
14
+ * temporarily hold several evidence channels. Only one channel may stand
15
+ * as its canonical ground. Every other live claim must be accounted as
16
+ * reported divergence — or the channel must have closed honestly. The
17
+ * binder holds the claims; the judge runs that accounting at every quiet
18
+ * point and retires channels it proves closed. A retired id surfacing
19
+ * anywhere again is a resurrection, which the axioms forbid.
20
+ */
21
+
22
+ const assert = require('node:assert/strict');
23
+
24
+ /** Permanent one-to-one identity (model record ↔ registry record). */
25
+ function createBinder() {
26
+ const forward = new Map(); // nodeId -> boundId
27
+ const reverse = new Map();
28
+
29
+ return {
30
+ observe(nodeId, boundId, context = '') {
31
+ const bound = forward.get(nodeId);
32
+ if (bound !== undefined) {
33
+ assert.equal(boundId, bound,
34
+ `identity broke${context}: node ${nodeId} was ${bound}, now claimed as ${boundId}`);
35
+ return;
36
+ }
37
+ const previous = reverse.get(boundId);
38
+ assert.equal(previous, undefined,
39
+ `identity stolen${context}: ${boundId} already belongs to node ${previous}`);
40
+ forward.set(nodeId, boundId);
41
+ reverse.set(boundId, nodeId);
42
+ },
43
+
44
+ /** The bound identity for a node (undefined if never observed) — lets
45
+ * the judge translate model uuids in derived payloads (container
46
+ * membership) into the bound side's uuids. */
47
+ boundOf: (nodeId) => forward.get(nodeId),
48
+
49
+ /** The node a bound identity answers for (undefined if unbound) — how
50
+ * the judge tells a record the model knows from one born of finer
51
+ * observation than the model's settle points. */
52
+ nodeOfBound: (boundId) => reverse.get(boundId),
53
+ };
54
+ }
55
+
56
+ /** Channel claims (detector fileId → model entity), many-to-one by law. */
57
+ function createChannelBinder() {
58
+ const claims = new Map(); // fileId -> nodeId
59
+ const retired = new Set();
60
+
61
+ return {
62
+ /** A channel observed standing for an entity's ground. */
63
+ observe(nodeId, fileId, context = '') {
64
+ assert.ok(!retired.has(fileId),
65
+ `a tombstoned identity rose again${context}: ${fileId}`);
66
+ const prior = claims.get(fileId);
67
+ if (prior !== undefined) {
68
+ assert.equal(nodeId, prior,
69
+ `identity broke${context}: channel ${fileId} claimed ${prior}, detector now says ${nodeId}`);
70
+ return;
71
+ }
72
+ claims.set(fileId, nodeId);
73
+ },
74
+
75
+ /** Every live claim, for the judge's per-point accounting. */
76
+ claims: () => [...claims],
77
+
78
+ /** The judge proved this channel closed: it must never rise again. */
79
+ retire(fileId) {
80
+ claims.delete(fileId);
81
+ retired.add(fileId);
82
+ },
83
+
84
+ /** Which entity this channel claims (undefined if never claimed). */
85
+ nodeOf: (fileId) => claims.get(fileId),
86
+
87
+ retiredIds: () => new Set(retired),
88
+ };
89
+ }
90
+
91
+ module.exports = { createBinder, createChannelBinder };