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