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