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,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A REFUSAL: the boundary understood the request and declined it.
|
|
5
|
+
*
|
|
6
|
+
* Two things can go wrong when a caller asks for something, and they
|
|
7
|
+
* mean opposite things:
|
|
8
|
+
*
|
|
9
|
+
* a refusal the request named ground amalgm will not take — excluded
|
|
10
|
+
* by enrollment policy, already owned by another tree, not
|
|
11
|
+
* there at all. Nothing is broken. The message IS the
|
|
12
|
+
* reason and is meant to be shown to whoever asked.
|
|
13
|
+
* a fault the boundary could not do its job — a torn identity
|
|
14
|
+
* store, an unusable database, a bug. The request was
|
|
15
|
+
* fine; the installation is not.
|
|
16
|
+
*
|
|
17
|
+
* A refusal is NAMED WHERE IT IS RAISED, by the code that decided to
|
|
18
|
+
* refuse. It is never guessed later from an error's wording: matching on
|
|
19
|
+
* message text would quietly reclassify every fault as the caller's
|
|
20
|
+
* mistake the moment a message changed, and "your path is bad" is the
|
|
21
|
+
* worst possible thing to say on the day the identity store tears.
|
|
22
|
+
*
|
|
23
|
+
* Callers that must answer differently — an HTTP status, an exit code —
|
|
24
|
+
* ask `isRefusal`. Everything else stays an ordinary Error and travels
|
|
25
|
+
* to whatever handles faults.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const REFUSAL = Symbol.for('amalgm.registration.refusal');
|
|
29
|
+
|
|
30
|
+
/** Decline a request, with the reason the caller should be shown. */
|
|
31
|
+
function refusal(message) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error[REFUSAL] = true;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True only for an error this boundary deliberately raised as a refusal. */
|
|
38
|
+
function isRefusal(error) {
|
|
39
|
+
return Boolean(error && error[REFUSAL]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { refusal, isRefusal };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The repository half of the registration boundary.
|
|
5
|
+
*
|
|
6
|
+
* Purpose: one `repo.git` entity travels as one Repocard state. The
|
|
7
|
+
* filesystem observer supplies noisy "something changed" doorbells; the
|
|
8
|
+
* Repocard follower turns them into settled Card + Checkpoint facts, and the
|
|
9
|
+
* registry records their stateId on the repo's existing UUID, then the
|
|
10
|
+
* settled state authorizes one child-tree reconciliation.
|
|
11
|
+
*
|
|
12
|
+
* Axioms:
|
|
13
|
+
* 1. One live repository observer root owns one follower.
|
|
14
|
+
* 2. Only a follower may turn `repo.changed` into a repo state update.
|
|
15
|
+
* 3. A settled state updates the existing repo UUID and is the only
|
|
16
|
+
* authority that may reconcile its registered worktree children.
|
|
17
|
+
* 4. A follower resumes from registration's persisted Card + Checkpoint;
|
|
18
|
+
* a restart never invents a second initial state.
|
|
19
|
+
*
|
|
20
|
+
* Card and checkpoint documents are the future Send rail's cargo. Local
|
|
21
|
+
* Record persists the complete bundle-free document now; the entity row
|
|
22
|
+
* carries its stateId, exactly as every other entity records its payload
|
|
23
|
+
* fingerprint.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const { createFollower, createCaptureWorker } = require('../repocard');
|
|
27
|
+
|
|
28
|
+
function createRepoFollowers({ observer, registry, repoStates, recordForRoot, onSettled }) {
|
|
29
|
+
if (!observer || !registry || !repoStates || typeof recordForRoot !== 'function'
|
|
30
|
+
|| typeof onSettled !== 'function') {
|
|
31
|
+
throw new Error('createRepoFollowers requires { observer, registry, repoStates, recordForRoot, onSettled }');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const worker = createCaptureWorker();
|
|
35
|
+
const followers = new Map(); // observer rootId -> { path, uuid, follower }
|
|
36
|
+
let stopped = false;
|
|
37
|
+
|
|
38
|
+
function close(rootId) {
|
|
39
|
+
const entry = followers.get(rootId);
|
|
40
|
+
if (!entry) return;
|
|
41
|
+
entry.follower.close();
|
|
42
|
+
followers.delete(rootId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Align followers with the observer's currently live repo roots. A root can
|
|
47
|
+
* move, be adopted, or cease being a repository; in every case the old
|
|
48
|
+
* follower is closed before a replacement can speak.
|
|
49
|
+
*/
|
|
50
|
+
function sync() {
|
|
51
|
+
if (stopped) return;
|
|
52
|
+
const roots = observer.listRoots().filter((root) => root.kind === 'repo');
|
|
53
|
+
const live = new Set(roots.map((root) => root.rootId));
|
|
54
|
+
for (const rootId of [...followers.keys()]) {
|
|
55
|
+
if (!live.has(rootId)) close(rootId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const root of roots) {
|
|
59
|
+
const record = recordForRoot(root);
|
|
60
|
+
if (!record || record.type !== 'repo.git') {
|
|
61
|
+
// A territory can appear before its enclosing lens has registered the
|
|
62
|
+
// boundary. `sync()` runs again after that lens reconciles.
|
|
63
|
+
close(root.rootId);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const existing = followers.get(root.rootId);
|
|
67
|
+
if (existing && existing.path === root.path && existing.uuid === record.uuid) continue;
|
|
68
|
+
close(root.rootId);
|
|
69
|
+
const baseline = repoStates.get(record.uuid);
|
|
70
|
+
|
|
71
|
+
const follower = createFollower({
|
|
72
|
+
repoPath: root.path,
|
|
73
|
+
last: baseline && { cardId: baseline.cardId, checkpointId: baseline.checkpointId },
|
|
74
|
+
captureFn: (repoPath) => worker.capture(repoPath, { withBundle: false }),
|
|
75
|
+
emit: (next) => {
|
|
76
|
+
// ONE settled Card + Checkpoint state is ONE registry transition on
|
|
77
|
+
// the repo entity. Raw filesystem events never write Git state.
|
|
78
|
+
const previous = repoStates.get(record.uuid);
|
|
79
|
+
registry.transaction(() => {
|
|
80
|
+
repoStates.put(record.uuid, next);
|
|
81
|
+
registry.attest(record.uuid, { type: 'repo.git', payloadVersion: next.stateId });
|
|
82
|
+
});
|
|
83
|
+
// The normal structural comparer may now inspect the worktree.
|
|
84
|
+
// It never runs from a raw doorbell, only this settled state.
|
|
85
|
+
onSettled({ root, record, previous, state: next });
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
followers.set(root.rootId, { path: root.path, uuid: record.uuid, follower });
|
|
89
|
+
// Birth captures the initial Card + Checkpoint. The follower owns its
|
|
90
|
+
// retry policy; registration must not invent an alternate Git scanner.
|
|
91
|
+
void follower.start();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Route exactly the observer's repo lifecycle signals into its follower. */
|
|
96
|
+
function onObserverEvent(event) {
|
|
97
|
+
if (stopped) return;
|
|
98
|
+
if (event.type === 'repo.removed') {
|
|
99
|
+
// A territory that ceased to be Git cannot keep a capture loop alive
|
|
100
|
+
// over an ordinary folder (or a vanished address).
|
|
101
|
+
close(event.rootId);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (event.type === 'root.moved') {
|
|
105
|
+
// The follower's capture path is its repo root's address. Rebind it
|
|
106
|
+
// before an old in-flight capture can mistake a normal move for a Git
|
|
107
|
+
// failure; `sync()` closes the old follower and starts one at the new
|
|
108
|
+
// address on the same repo UUID.
|
|
109
|
+
sync();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (event.type === 'repo.changed') followers.get(event.rootId)?.follower.ring();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stop() {
|
|
116
|
+
if (stopped) return;
|
|
117
|
+
stopped = true;
|
|
118
|
+
for (const rootId of [...followers.keys()]) close(rootId);
|
|
119
|
+
// `close()` rejects any in-flight worker capture. Followers are closed
|
|
120
|
+
// first, so no rejected capture can schedule another retry.
|
|
121
|
+
void worker.close();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { sync, onObserverEvent, stop };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { createRepoFollowers };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The durable current state of each registered Git repository.
|
|
5
|
+
*
|
|
6
|
+
* Purpose: a `repo.git` entity needs a complete Card + Checkpoint baseline
|
|
7
|
+
* before Watch begins. The identity registry owns the UUID and stateId;
|
|
8
|
+
* this module keeps the exact bundle-free documents that stateId names so a
|
|
9
|
+
* follower can resume after a restart and reconciliation can compare a new
|
|
10
|
+
* settled state with the state registration actually recorded.
|
|
11
|
+
*
|
|
12
|
+
* Axioms:
|
|
13
|
+
* 1. One active repo UUID has at most one current Card + Checkpoint state.
|
|
14
|
+
* 2. The state document and the entity's stateId commit in the same
|
|
15
|
+
* identity-database transaction.
|
|
16
|
+
* 3. Detect stores bundle-free state. Send constructs a transport bundle
|
|
17
|
+
* later; Detect never pays transport's cost.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function encodeBytes(bytes) {
|
|
21
|
+
return Buffer.from(bytes || []).toString('base64');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decodeBytes(encoded) {
|
|
25
|
+
return Buffer.from(encoded || '', 'base64');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pack(state) {
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
stateId: state.stateId,
|
|
31
|
+
cardId: state.cardId,
|
|
32
|
+
checkpointId: state.checkpointId,
|
|
33
|
+
card: {
|
|
34
|
+
...state.card,
|
|
35
|
+
bundle: state.card.bundle && {
|
|
36
|
+
...state.card.bundle,
|
|
37
|
+
bytes: encodeBytes(state.card.bundle.bytes),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
checkpoint: {
|
|
41
|
+
...state.checkpoint,
|
|
42
|
+
stagedPatch: encodeBytes(state.checkpoint.stagedPatch),
|
|
43
|
+
unstagedPatch: encodeBytes(state.checkpoint.unstagedPatch),
|
|
44
|
+
untracked: state.checkpoint.untracked.map((file) => ({
|
|
45
|
+
...file,
|
|
46
|
+
content: encodeBytes(file.content),
|
|
47
|
+
})),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function unpack(text) {
|
|
53
|
+
const state = JSON.parse(text);
|
|
54
|
+
return {
|
|
55
|
+
...state,
|
|
56
|
+
card: {
|
|
57
|
+
...state.card,
|
|
58
|
+
bundle: state.card.bundle && {
|
|
59
|
+
...state.card.bundle,
|
|
60
|
+
bytes: decodeBytes(state.card.bundle.bytes),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
checkpoint: {
|
|
64
|
+
...state.checkpoint,
|
|
65
|
+
stagedPatch: decodeBytes(state.checkpoint.stagedPatch),
|
|
66
|
+
unstagedPatch: decodeBytes(state.checkpoint.unstagedPatch),
|
|
67
|
+
untracked: state.checkpoint.untracked.map((file) => ({
|
|
68
|
+
...file,
|
|
69
|
+
content: decodeBytes(file.content),
|
|
70
|
+
})),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createRepoStates(db) {
|
|
76
|
+
if (!db) throw new Error('createRepoStates requires the identity database');
|
|
77
|
+
const read = db.prepare('SELECT state_json FROM repo_states WHERE entity_uuid = ?');
|
|
78
|
+
const write = db.prepare(`
|
|
79
|
+
INSERT INTO repo_states (entity_uuid, state_id, card_id, checkpoint_id, state_json)
|
|
80
|
+
VALUES (@entityUuid, @stateId, @cardId, @checkpointId, @stateJson)
|
|
81
|
+
ON CONFLICT(entity_uuid) DO UPDATE SET
|
|
82
|
+
state_id = excluded.state_id,
|
|
83
|
+
card_id = excluded.card_id,
|
|
84
|
+
checkpoint_id = excluded.checkpoint_id,
|
|
85
|
+
state_json = excluded.state_json
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
get(entityUuid) {
|
|
90
|
+
const row = read.get(entityUuid);
|
|
91
|
+
return row ? unpack(row.state_json) : null;
|
|
92
|
+
},
|
|
93
|
+
put(entityUuid, state) {
|
|
94
|
+
if (!entityUuid || !state?.stateId || !state?.cardId || !state?.checkpointId) {
|
|
95
|
+
throw new Error('a repo state needs entityUuid, stateId, cardId, and checkpointId');
|
|
96
|
+
}
|
|
97
|
+
write.run({
|
|
98
|
+
entityUuid,
|
|
99
|
+
stateId: state.stateId,
|
|
100
|
+
cardId: state.cardId,
|
|
101
|
+
checkpointId: state.checkpointId,
|
|
102
|
+
stateJson: pack(state),
|
|
103
|
+
});
|
|
104
|
+
return state;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { createRepoStates };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The installation's ONE live registration boundary — the production
|
|
5
|
+
* composition root for the realtime rails that exist today:
|
|
6
|
+
*
|
|
7
|
+
* Watch the observer's doorbells, armed by start()
|
|
8
|
+
* Detect the observer's scans, published as events
|
|
9
|
+
* Record registration.onEvent → the evidence lenses → the registry
|
|
10
|
+
*
|
|
11
|
+
* This file's only job is COMPOSITION AND LIFETIME: which policies are
|
|
12
|
+
* injected, where the two databases live, how the observer's events
|
|
13
|
+
* reach the boundary, and how all of it is given back. It contains no
|
|
14
|
+
* detection logic and no laws — those are `observer/`, `registry/`, and
|
|
15
|
+
* `registration/index.js`. Nothing is built until something asks: the
|
|
16
|
+
* runtime pays nothing for this until a caller registers ground, which
|
|
17
|
+
* is what keeps the cutover shadow-only.
|
|
18
|
+
*
|
|
19
|
+
* Because this root OPENS the handles, this root CLOSES them: `close()`
|
|
20
|
+
* is the exact inverse of construction, and it is complete or it is a
|
|
21
|
+
* leak wearing a shutdown's name.
|
|
22
|
+
*
|
|
23
|
+
* TWO DATABASES, one directory, because they answer different questions
|
|
24
|
+
* and fail independently: `observer.db` holds EVIDENCE (what disk looks
|
|
25
|
+
* like right now, rebuildable by rescanning), `identity.db` holds
|
|
26
|
+
* IDENTITY (permanent UUIDs, never rebuildable) guarded by its epoch
|
|
27
|
+
* sentinel — if identity is lost while the sentinel survives, the
|
|
28
|
+
* registry refuses to mint replacement identities rather than quietly
|
|
29
|
+
* re-inventing everyone.
|
|
30
|
+
*
|
|
31
|
+
* The event wire is the Detect → Record rail and is NOT optional: the
|
|
32
|
+
* observer publishes into the boundary, which reconciles its lenses.
|
|
33
|
+
* The boundary's OWN events (`entity.committed` and kin) are discarded
|
|
34
|
+
* here on purpose — fanning them out to the app, the cloud, or the
|
|
35
|
+
* legacy consumers is the next cutover slice, and a shadow that
|
|
36
|
+
* publishes is not a shadow. User-home bootstrap is intentionally not
|
|
37
|
+
* another registrar: it calls this boundary's registration operation once for
|
|
38
|
+
* every declared safe root (docs/user-cloud-bootstrap.md).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const path = require('path');
|
|
42
|
+
|
|
43
|
+
const Database = require('better-sqlite3');
|
|
44
|
+
|
|
45
|
+
const { STORAGE_DIR } = require('../config');
|
|
46
|
+
const { ensureDir } = require('../lib/storage');
|
|
47
|
+
const { ensureUserHome } = require('../lib/layout');
|
|
48
|
+
const { createObserver } = require('../observer');
|
|
49
|
+
const { shouldEnroll } = require('../workspace/tree/inclusion');
|
|
50
|
+
const { classifyFile } = require('./classify');
|
|
51
|
+
const { createEntityCloud } = require('./entity-cloud');
|
|
52
|
+
const { createEntityContent } = require('./entity-content');
|
|
53
|
+
const { createRegistration } = require('./index');
|
|
54
|
+
const { machineId, userId } = require('../workspace/identity');
|
|
55
|
+
|
|
56
|
+
const ENTITIES_DIR = path.join(STORAGE_DIR, 'entities');
|
|
57
|
+
const USER_HOME_CLOUD_CONTRACT = 'amalgm-user-home@1';
|
|
58
|
+
const USER_HOME_CLOUD_SCHEMA_VERSION = 1;
|
|
59
|
+
|
|
60
|
+
let live = null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build the whole stack over one directory. Exported for tests, which
|
|
64
|
+
* pass a temporary directory; production always uses the singleton
|
|
65
|
+
* below, so an installation can never hold two boundaries over one
|
|
66
|
+
* identity database.
|
|
67
|
+
*/
|
|
68
|
+
function createRegistrationService({
|
|
69
|
+
dir = ENTITIES_DIR,
|
|
70
|
+
homeDir = STORAGE_DIR,
|
|
71
|
+
// Tests may supply a signed-in identity without mutating process-global
|
|
72
|
+
// runtime configuration. Production uses the enrolled computer record.
|
|
73
|
+
cloudUserId = userId(),
|
|
74
|
+
cloudDeviceId = machineId(),
|
|
75
|
+
} = {}) {
|
|
76
|
+
ensureDir(dir);
|
|
77
|
+
|
|
78
|
+
const evidenceDb = new Database(path.join(dir, 'observer.db'));
|
|
79
|
+
const identityDb = new Database(path.join(dir, 'identity.db'));
|
|
80
|
+
const entityContent = createEntityContent({ dir });
|
|
81
|
+
const entityCloud = createEntityCloud({
|
|
82
|
+
db: identityDb,
|
|
83
|
+
userId: cloudUserId,
|
|
84
|
+
deviceId: cloudDeviceId,
|
|
85
|
+
content: entityContent,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The observer emits before the boundary exists (its constructor
|
|
89
|
+
// scans), so the wire reads through a box that the boundary fills.
|
|
90
|
+
const boundary = { current: null };
|
|
91
|
+
const observer = createObserver({
|
|
92
|
+
db: evidenceDb,
|
|
93
|
+
emit: (event) => boundary.current?.onEvent(event),
|
|
94
|
+
shouldEnroll,
|
|
95
|
+
captureContent: entityContent.captureFile,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
boundary.current = createRegistration({
|
|
99
|
+
observer,
|
|
100
|
+
openIdentity: () => ({
|
|
101
|
+
db: identityDb,
|
|
102
|
+
sentinelPath: path.join(dir, 'identity.epoch'),
|
|
103
|
+
journal: entityCloud.journal,
|
|
104
|
+
}),
|
|
105
|
+
classify: classifyFile,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let closed = false;
|
|
109
|
+
|
|
110
|
+
const publicRecord = (record) => ({
|
|
111
|
+
uuid: record.uuid,
|
|
112
|
+
type: record.type,
|
|
113
|
+
parentUUID: record.parentUUID,
|
|
114
|
+
name: record.name,
|
|
115
|
+
version: record.version,
|
|
116
|
+
status: record.status,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function identitySeeds(snapshot, declaration) {
|
|
120
|
+
if (snapshot === null || snapshot === undefined) return null;
|
|
121
|
+
if (!snapshot || snapshot.schemaVersion !== USER_HOME_CLOUD_SCHEMA_VERSION
|
|
122
|
+
|| snapshot.kind !== 'amalgm-user-home'
|
|
123
|
+
|| !Array.isArray(snapshot.roots) || !Array.isArray(snapshot.exactManaged)) {
|
|
124
|
+
throw new Error('cloud user-home snapshot has an unsupported shape');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rootEntries = new Map(snapshot.roots.map((entry) => [entry?.key, entry]));
|
|
128
|
+
const exactEntries = new Map(snapshot.exactManaged.map((entry) => [entry?.key, entry]));
|
|
129
|
+
if (rootEntries.size !== declaration.roots.length || exactEntries.size !== declaration.exactManaged.length) {
|
|
130
|
+
throw new Error('cloud user-home snapshot does not declare this user-home schema');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const roots = new Map();
|
|
134
|
+
for (const root of declaration.roots) {
|
|
135
|
+
const entry = rootEntries.get(root.key);
|
|
136
|
+
const record = entry?.record;
|
|
137
|
+
if (!entry || !record || record.parentUUID !== null || record.name !== path.basename(root.path)
|
|
138
|
+
|| record.status !== 'active') {
|
|
139
|
+
throw new Error(`cloud user-home root ${root.key} is invalid`);
|
|
140
|
+
}
|
|
141
|
+
roots.set(root.key, record);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const managed = new Map();
|
|
145
|
+
for (const target of declaration.exactManaged) {
|
|
146
|
+
const entry = exactEntries.get(target.key);
|
|
147
|
+
const record = entry?.record;
|
|
148
|
+
const root = roots.get(target.rootKey);
|
|
149
|
+
const isRoot = target.relativePath === target.rootKey;
|
|
150
|
+
const parentUUID = isRoot ? null : root?.uuid;
|
|
151
|
+
if (!entry || entry.scope !== 'exact' || entry.relativePath !== target.relativePath
|
|
152
|
+
|| !record || !root || record.parentUUID !== parentUUID
|
|
153
|
+
|| record.name !== path.basename(target.relativePath)
|
|
154
|
+
|| record.status !== 'active'
|
|
155
|
+
|| (isRoot && record.uuid !== root.uuid)) {
|
|
156
|
+
throw new Error(`cloud user-home managed target ${target.key} is invalid`);
|
|
157
|
+
}
|
|
158
|
+
managed.set(target.key, record);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
forRoot(key) {
|
|
163
|
+
return (relativePath) => {
|
|
164
|
+
if (relativePath === '') return roots.get(key) || null;
|
|
165
|
+
const target = declaration.exactManaged.find((entry) => (
|
|
166
|
+
entry.rootKey === key
|
|
167
|
+
&& entry.relativePath.startsWith(`${key}/`)
|
|
168
|
+
&& entry.relativePath.slice(key.length + 1) === relativePath
|
|
169
|
+
));
|
|
170
|
+
return target ? managed.get(target.key) || null : null;
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
root: (key) => roots.get(key),
|
|
174
|
+
managed: (key) => managed.get(key),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Bootstrap a logged-in person's declared Amalgm home through the SAME
|
|
180
|
+
* operation as `amalgm register`. `ensureUserHome` only creates the safe shape;
|
|
181
|
+
* this function is the sole point that assigns UUIDs and begins Watch.
|
|
182
|
+
*
|
|
183
|
+
* The result deliberately exposes the exact platform-managed UUIDs rather
|
|
184
|
+
* than inferring authority from `system/`. The gateway will persist those
|
|
185
|
+
* exact facts beside ordinary inheriting human permissions in the Send
|
|
186
|
+
* slice. Until then this operation is local identity + watch only.
|
|
187
|
+
*/
|
|
188
|
+
async function bootstrapUserHome({ identitySnapshot = null } = {}) {
|
|
189
|
+
const declaration = ensureUserHome(homeDir);
|
|
190
|
+
const seeds = identitySeeds(identitySnapshot, declaration);
|
|
191
|
+
const roots = [];
|
|
192
|
+
const addedRoots = boundary.current.addMany(declaration.roots.map((entry) => ({
|
|
193
|
+
targetPath: entry.path,
|
|
194
|
+
identityAt: seeds?.forRoot(entry.key) ?? null,
|
|
195
|
+
})));
|
|
196
|
+
for (let index = 0; index < declaration.roots.length; index += 1) {
|
|
197
|
+
const entry = declaration.roots[index];
|
|
198
|
+
const added = addedRoots[index];
|
|
199
|
+
if (seeds && added.record.uuid !== seeds.root(entry.key).uuid) {
|
|
200
|
+
throw new Error(`local user-home root ${entry.key} does not match its cloud UUID`);
|
|
201
|
+
}
|
|
202
|
+
roots.push({
|
|
203
|
+
key: entry.key,
|
|
204
|
+
path: added.rootPath,
|
|
205
|
+
record: added.record,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const system = roots.find((entry) => entry.key === 'system');
|
|
210
|
+
if (!system) throw new Error('declared user home has no system root');
|
|
211
|
+
const tree = boundary.current.tree(system.path);
|
|
212
|
+
if (!tree?.root()) throw new Error(`declared system root was not registered: ${system.path}`);
|
|
213
|
+
|
|
214
|
+
const recordAt = (relativePath) => {
|
|
215
|
+
let record = tree.root();
|
|
216
|
+
for (const name of relativePath.split('/')) {
|
|
217
|
+
record = tree.registry.activeAt(record.uuid, name);
|
|
218
|
+
if (!record) throw new Error(`declared user-home target was not registered: ${relativePath}`);
|
|
219
|
+
}
|
|
220
|
+
return record;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const exactManaged = declaration.exactManaged.map((entry) => {
|
|
224
|
+
const root = roots.find((candidate) => candidate.key === entry.rootKey);
|
|
225
|
+
const isRoot = entry.relativePath === entry.rootKey;
|
|
226
|
+
const relativePath = isRoot ? '' : entry.relativePath.slice(entry.rootKey.length + 1);
|
|
227
|
+
const record = isRoot ? root?.record : recordAt(relativePath);
|
|
228
|
+
if (!record) throw new Error(`declared user-home managed target was not registered: ${entry.relativePath}`);
|
|
229
|
+
if (seeds && record.uuid !== seeds.managed(entry.key).uuid) {
|
|
230
|
+
throw new Error(`local user-home managed target ${entry.key} does not match its cloud UUID`);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
key: entry.key,
|
|
234
|
+
rootKey: entry.rootKey,
|
|
235
|
+
relativePath: entry.relativePath,
|
|
236
|
+
path: entry.path,
|
|
237
|
+
scope: entry.scope,
|
|
238
|
+
record,
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const cloudSnapshot = {
|
|
243
|
+
schemaVersion: USER_HOME_CLOUD_SCHEMA_VERSION,
|
|
244
|
+
kind: 'amalgm-user-home',
|
|
245
|
+
roots: roots.map(({ key, record }) => ({ key, record: publicRecord(record) })),
|
|
246
|
+
exactManaged: exactManaged.map(({ key, relativePath, scope, record }) => ({
|
|
247
|
+
key, relativePath, scope, record: publicRecord(record),
|
|
248
|
+
})),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
schemaVersion: declaration.schemaVersion,
|
|
253
|
+
homeDir: declaration.userDir,
|
|
254
|
+
roots,
|
|
255
|
+
exactManaged,
|
|
256
|
+
cloudSnapshot,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
dir,
|
|
262
|
+
homeDir,
|
|
263
|
+
observer,
|
|
264
|
+
registration: boundary.current,
|
|
265
|
+
/** `amalgm register <path>` — register, then begin Watch + Detect. */
|
|
266
|
+
register: async (targetPath) => {
|
|
267
|
+
entityCloud.assertRegistrationReady();
|
|
268
|
+
return boundary.current.add(targetPath);
|
|
269
|
+
},
|
|
270
|
+
/** `amalgm register <path>...` — the same primitive for each selected root. */
|
|
271
|
+
registerMany: async (targetPaths) => {
|
|
272
|
+
entityCloud.assertRegistrationReady();
|
|
273
|
+
return boundary.current.addMany(targetPaths);
|
|
274
|
+
},
|
|
275
|
+
bootstrapUserHome,
|
|
276
|
+
entityCloud,
|
|
277
|
+
entityContent,
|
|
278
|
+
/**
|
|
279
|
+
* Give back everything this root took, in the order it took it:
|
|
280
|
+
* watchers and their timers first (the observer's own `stop`), then
|
|
281
|
+
* the two databases. Whoever opens a handle closes it — a stack that
|
|
282
|
+
* only stops watching is still holding the files it opened, which is
|
|
283
|
+
* why a "restart" over a half-closed service is two live stacks over
|
|
284
|
+
* one database rather than the fresh process it claims to be.
|
|
285
|
+
* Idempotent: shutdown is often reached from more than one direction.
|
|
286
|
+
*/
|
|
287
|
+
close() {
|
|
288
|
+
if (closed) return;
|
|
289
|
+
closed = true;
|
|
290
|
+
observer.stop(); // doorbells and their timers
|
|
291
|
+
boundary.current.stop(); // and the pass they may already have scheduled
|
|
292
|
+
evidenceDb.close();
|
|
293
|
+
identityDb.close();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The installation's one boundary, built on first use. */
|
|
299
|
+
function registrationService() {
|
|
300
|
+
if (!live) live = createRegistrationService();
|
|
301
|
+
return live;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
ENTITIES_DIR,
|
|
306
|
+
USER_HOME_CLOUD_CONTRACT,
|
|
307
|
+
USER_HOME_CLOUD_SCHEMA_VERSION,
|
|
308
|
+
createRegistrationService,
|
|
309
|
+
registrationService,
|
|
310
|
+
};
|