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,197 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The adapter contract: what it means to serve an entity type.
5
+ *
6
+ * An adapter is how a KIND of entity rides the seven realtime rails —
7
+ * watch, detect, record, send, receive, apply, verify
8
+ * (docs/realtime-rails.md §2). The entity's `type` selects its adapter;
9
+ * the rails themselves never change shape per type.
10
+ *
11
+ * Axioms:
12
+ *
13
+ * 1. ONE ADAPTER PER TYPE, EVERY TYPE ASSIGNED. The alphabet belongs
14
+ * to the registry (`ENTITY_TYPES`); the adapter map covers it
15
+ * exactly — no type unassigned, no type claimed twice. Adapters
16
+ * read the alphabet, never restate it, so the map cannot drift
17
+ * from the registry that enforces it. Assigned is not yet
18
+ * operational: operational is earned rail by rail, and absence is
19
+ * honest.
20
+ *
21
+ * 2. ADAPTERS OWN BEHAVIOR; THE REGISTRY OWNS IDENTITY. An adapter
22
+ * decides how truth is read and applied — never which UUID it
23
+ * belongs to. A lawful type transition that crosses adapters
24
+ * (folder becoming repo.git) is a handoff on the same UUID, never
25
+ * a rebirth; and the kinds that transition into one another on a
26
+ * single piece of ground (file ↔ link) live inside ONE adapter,
27
+ * so their transitions never even change hands.
28
+ *
29
+ * 3. ONE SOCKET PER RAIL. A rail's callable surfaces — their names
30
+ * and their shapes — are ratified HERE, once, for every adapter
31
+ * (`SOCKETS`). A generic caller performs the same conceptual
32
+ * operation through the same call on every type; it never branches
33
+ * on an adapter-specific function name. An adapter binds a rail's
34
+ * whole socket or binds the rail not at all; a rail whose socket
35
+ * is not yet ratified cannot be bound by anyone. The sockets are
36
+ * derived from the real consumer — the registration boundary and
37
+ * its evidence lenses — never invented ahead of one, and never for
38
+ * symmetry (axiom 7 decides which rails earn one at all).
39
+ *
40
+ * 4. A TRUTH READ NEVER LIES. Every assigned type names its source of
41
+ * truth in one sentence, and reading it produces exactly one of
42
+ * three honest answers: PRESENT (the complete truth behind a
43
+ * version — equal versions mean equal truth, and equal truth is
44
+ * silence), ABSENT (the ground genuinely is not there — a fact,
45
+ * not an error), or UNSETTLED (changing or unreadable right now;
46
+ * read again). Partial truth is never passed off as complete, and
47
+ * nothing is silently omitted — a read is complete or it is
48
+ * unsettled.
49
+ *
50
+ * 5. RAILS ARE HONEST. A rail an adapter binds is built and proven;
51
+ * an unbound rail is absent — never stubbed, never defaulted, so
52
+ * nothing downstream can mistake a placeholder for a capability.
53
+ *
54
+ * 6. THE CONTRACT IS FROZEN GROUND. A descriptor is immutable from
55
+ * the moment it loads; behavior changes by shipping a new adapter,
56
+ * never by mutating a live one.
57
+ *
58
+ * 7. THE BASE ORCHESTRATES; THE ADAPTER SUPPLIES TYPE BEHAVIOR. The
59
+ * universal machinery owns orchestration and identity: what is
60
+ * enrolled, when work happens, retries, routing, and which UUID
61
+ * owns the result. An adapter supplies only the type-specific
62
+ * behavior a real rail requires. A rail therefore earns a socket
63
+ * only when a real consumer proves that behavior differs by type —
64
+ * never for symmetry. Behavior that survives an adapter handoff
65
+ * unchanged was never that adapter's behavior.
66
+ */
67
+
68
+ const RAILS = Object.freeze([
69
+ 'watch', 'detect', 'record', 'send', 'receive', 'apply', 'verify',
70
+ ]);
71
+
72
+ /**
73
+ * The ratified sockets — for each rail that has one, the exact surfaces
74
+ * every binding adapter implements. Same names, same shapes, every
75
+ * type. ONE socket is ratified today, derived from what production
76
+ * detection actually asks of a type (the observer's content decision,
77
+ * the registration boundary's lenses): read the current truth and
78
+ * compare — equal truth is silence.
79
+ *
80
+ * detect.truth(path) →
81
+ * { truth: 'present', version, state } the complete current
82
+ * truth, fingerprinted — equal versions mean equal truth
83
+ * { truth: 'absent' } the ground genuinely is
84
+ * not there — a fact, not an error
85
+ * { truth: 'unsettled', reason } changing or unreadable
86
+ * right now; read again — never a partial answer
87
+ *
88
+ * No other rail has a socket, and WATCH AND RECORD EARN NONE AT THIS
89
+ * BAR — adjudicated 2026-08-03 under axiom 7, on the filesystem
90
+ * alphabet we actually serve. The observer's doorbell is chosen by
91
+ * physical shape (one piece of ground watches its parent; a tree
92
+ * watches itself): identical for a folder and a repo, which are
93
+ * different adapters; different inside one adapter's own types; and
94
+ * untouched when a live root crosses `files` ↔ `repo` on the same
95
+ * UUID. It survives the adapter handoff, so it was never adapter
96
+ * behavior. Record is the registry's by axiom 2 — identity,
97
+ * hierarchy, versions, lifecycle. A type whose truth is NOT filesystem
98
+ * ground — an API, a database — would be new evidence, and axiom 7
99
+ * decides it then, on that consumer, exactly as it decided this one.
100
+ *
101
+ * Apply and verify remain what they are today: proven MODULE surfaces
102
+ * (repocard's materialize-and-prove and re-read-and-compare), joining
103
+ * the contract only when a real multi-type consumer — the
104
+ * Send/Receive rails — exists to derive their socket from, by the same
105
+ * law that made detect's. Send is where the next socket is expected,
106
+ * because there the cargo itself differs by type: bytes, membership,
107
+ * written target, repository state. A socket is ratified from a real
108
+ * consumer or it is not ratified at all.
109
+ */
110
+ const SOCKETS = Object.freeze({
111
+ detect: Object.freeze(['truth']),
112
+ });
113
+
114
+ /**
115
+ * Convict a malformed adapter descriptor loudly; return it unchanged
116
+ * when lawful. The shape is deliberately tiny:
117
+ *
118
+ * name — the adapter's one name
119
+ * types — { entityType: 'one-sentence source of truth', ... }
120
+ * rails — { railName: { socket surfaces, complete }, ... } — only
121
+ * rails whose ratified socket the adapter fully implements
122
+ */
123
+ function assertAdapter(adapter) {
124
+ const where = adapter && adapter.name ? `adapter ${JSON.stringify(adapter.name)}` : 'adapter';
125
+ if (!adapter || typeof adapter !== 'object' || !Object.isFrozen(adapter)) {
126
+ throw new Error(`${where}: a descriptor is a frozen object`);
127
+ }
128
+ if (typeof adapter.name !== 'string' || adapter.name.length === 0) {
129
+ throw new Error('adapter: name is a non-empty string');
130
+ }
131
+ const { types, rails } = adapter;
132
+ if (!types || typeof types !== 'object' || !Object.isFrozen(types) || Object.keys(types).length === 0) {
133
+ throw new Error(`${where}: types is a frozen, non-empty map of entity type → declared truth`);
134
+ }
135
+ for (const [type, truth] of Object.entries(types)) {
136
+ if (typeof truth !== 'string' || truth.length === 0) {
137
+ throw new Error(`${where}: type ${JSON.stringify(type)} must declare its source of truth in one sentence`);
138
+ }
139
+ }
140
+ if (!rails || typeof rails !== 'object' || !Object.isFrozen(rails)) {
141
+ throw new Error(`${where}: rails is a frozen map (empty is honest; a stub is not)`);
142
+ }
143
+ for (const [rail, surfaces] of Object.entries(rails)) {
144
+ if (!RAILS.includes(rail)) {
145
+ throw new Error(`${where}: ${JSON.stringify(rail)} is not a rail — the rails are ${RAILS.join(', ')}`);
146
+ }
147
+ const socket = SOCKETS[rail];
148
+ if (!socket) {
149
+ throw new Error(`${where}: rail ${rail} has no ratified socket yet — a socket is ratified in the contract before anyone binds it`);
150
+ }
151
+ if (!surfaces || typeof surfaces !== 'object' || !Object.isFrozen(surfaces)) {
152
+ throw new Error(`${where}: rail ${rail} must bind a frozen map of its socket's surfaces`);
153
+ }
154
+ const bound = Object.keys(surfaces).sort();
155
+ if (bound.join(',') !== [...socket].sort().join(',')) {
156
+ throw new Error(`${where}: rail ${rail} binds [${bound.join(', ')}] but its socket is [${socket.join(', ')}] — an adapter implements a rail's whole socket or binds the rail not at all`);
157
+ }
158
+ for (const [surface, fn] of Object.entries(surfaces)) {
159
+ if (typeof fn !== 'function') {
160
+ throw new Error(`${where}: ${rail}.${surface} must be a function — an unbuilt surface is left absent, never stubbed`);
161
+ }
162
+ }
163
+ }
164
+ return adapter;
165
+ }
166
+
167
+ /**
168
+ * Build the one type → adapter map, convicting every violation of
169
+ * axiom 1: an adapter claiming a type outside the alphabet, two
170
+ * adapters claiming one type, or an alphabet type left unassigned.
171
+ * Pure — callers own where the alphabet and adapters come from.
172
+ */
173
+ function buildTypeMap(adapters, alphabet) {
174
+ const known = new Set(alphabet);
175
+ const byType = new Map();
176
+ for (const adapter of adapters) {
177
+ assertAdapter(adapter);
178
+ for (const type of Object.keys(adapter.types)) {
179
+ if (!known.has(type)) {
180
+ throw new Error(`adapter ${adapter.name} claims ${JSON.stringify(type)}, which is not in the registry's alphabet`);
181
+ }
182
+ const holder = byType.get(type);
183
+ if (holder) {
184
+ throw new Error(`entity type ${JSON.stringify(type)} is claimed by both ${holder.name} and ${adapter.name} — one adapter per type`);
185
+ }
186
+ byType.set(type, adapter);
187
+ }
188
+ }
189
+ for (const type of known) {
190
+ if (!byType.has(type)) {
191
+ throw new Error(`entity type ${JSON.stringify(type)} has no adapter — every type is assigned`);
192
+ }
193
+ }
194
+ return byType;
195
+ }
196
+
197
+ module.exports = { RAILS, SOCKETS, assertAdapter, buildTypeMap };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The filesystem adapter: every kind whose truth IS ordinary filesystem
5
+ * ground — folders, files, symlinks — served by ONE adapter on purpose.
6
+ * These kinds transition into one another on the same UUID (a file
7
+ * becomes a link on the same slot, text becomes binary, a workspace
8
+ * sheds to folder on adoption), and one adapter means those transitions
9
+ * never cross a behavioral boundary where identity could be fumbled.
10
+ *
11
+ * Constructs — agents, apps, automations, tools, skills — are NOT
12
+ * types here or anywhere: physically they are folders and files with a
13
+ * known structure, served by this adapter like any other ground, and
14
+ * their construct meaning is layered above the physical type. If one
15
+ * ever truly travels or applies differently, promoting it is a
16
+ * deliberate future ratification — never a default.
17
+ *
18
+ * One rail bound: detect, through the ratified socket, and production
19
+ * consumes it — the observer's content reads (observer/index.js
20
+ * scanContent and safeContent) speak through this exact function. The
21
+ * content primitive lives HERE: one detector, owned by the adapter, and
22
+ * the dependency points one way — the observer reads truth through the
23
+ * socket, never the other way around. Watch and record stay
24
+ * orchestrated in the observer until their sockets are ratified from
25
+ * that seam; apply and verify for filesystem ground ride the cloud
26
+ * rails. All honestly absent.
27
+ */
28
+
29
+ const crypto = require('crypto');
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const { lookFailed } = require('./truth');
34
+
35
+ /**
36
+ * A file's bytes, read ONCE, answering both questions they support:
37
+ * their sha256 and whether any of them is a NUL. A hash and a type that
38
+ * came from different reads can disagree — the file may change between
39
+ * them — and a type is a claim ABOUT the bytes behind the hash, so the
40
+ * two are one answer or they are not honest. Streaming, so large files
41
+ * never load whole into memory; the NUL is looked for chunk by chunk.
42
+ */
43
+ function readFile(absPath) {
44
+ const hash = crypto.createHash('sha256');
45
+ let binary = false;
46
+ const fd = fs.openSync(absPath, 'r');
47
+ try {
48
+ const buffer = Buffer.allocUnsafe(1 << 20);
49
+ let read;
50
+ while ((read = fs.readSync(fd, buffer, 0, buffer.length)) > 0) {
51
+ const chunk = buffer.subarray(0, read);
52
+ hash.update(chunk);
53
+ if (!binary && chunk.includes(0)) binary = true;
54
+ }
55
+ } finally {
56
+ fs.closeSync(fd);
57
+ }
58
+ return { sha256: hash.digest('hex'), binary };
59
+ }
60
+
61
+ const present = (parts, state) => ({
62
+ truth: 'present',
63
+ version: crypto.createHash('sha256').update(JSON.stringify(parts)).digest('hex'),
64
+ state,
65
+ });
66
+
67
+ const KIND = (stat) => (stat.isSymbolicLink() ? 'link' : stat.isDirectory() ? 'dir' : 'file');
68
+
69
+ /**
70
+ * The detect socket: the ground's whole current truth, by its physical
71
+ * kind — the caller never says which kind it expects, disk decides
72
+ * (exactly how a file becomes a link on the same slot without anyone
73
+ * branching).
74
+ *
75
+ * link → its written target text, exactly as disk holds it
76
+ * dir → its direct membership: sorted names with their kinds
77
+ * (folder truth is membership; content belongs to members)
78
+ * file → its bytes, as a streaming sha256, plus whether any of
79
+ * those same bytes is a NUL — one read, so the fingerprint
80
+ * and the text/bytes claim can never describe different ground
81
+ *
82
+ * Three honest answers, never a fourth: ground that is not there is
83
+ * ABSENT — a fact, not an error — and ground that moved between looks
84
+ * or refuses to be read completely is UNSETTLED: the whole read
85
+ * succeeds or the whole read says "read again"; a member is never
86
+ * silently dropped from a membership that claims to be complete.
87
+ *
88
+ * Physical truth only: enrollment policy — what deserves to be shared —
89
+ * is the caller's injected question, asked before this one.
90
+ */
91
+ function truth(absPath) {
92
+ let stat;
93
+ try {
94
+ stat = fs.lstatSync(absPath);
95
+ } catch (error) {
96
+ return lookFailed(error);
97
+ }
98
+ const kind = KIND(stat);
99
+ try {
100
+ if (kind === 'link') {
101
+ const target = fs.readlinkSync(absPath);
102
+ return present(['link', target], { kind, target });
103
+ }
104
+ if (kind === 'dir') {
105
+ const members = fs.readdirSync(absPath).sort().map((name) => ({
106
+ name, kind: KIND(fs.lstatSync(path.join(absPath, name))),
107
+ }));
108
+ return present(['dir', members], { kind, members });
109
+ }
110
+ // `binary` rides the state but NOT the version: it is derived from
111
+ // the very bytes the sha256 fingerprints, so it can never vary
112
+ // while the hash holds. Adding it would fingerprint the same fact twice.
113
+ const { sha256, binary } = readFile(absPath);
114
+ return present(['file', sha256], { kind, sha256, binary });
115
+ } catch (error) {
116
+ // The ground changed between looks: a complete read was impossible.
117
+ return { truth: 'unsettled', reason: error.code ?? 'changed-mid-read' };
118
+ }
119
+ }
120
+
121
+ module.exports = Object.freeze({
122
+ name: 'filesystem',
123
+
124
+ types: Object.freeze({
125
+ workspace: 'its active direct membership — folder truth; the collaboration boundary it marks is authority meaning, layered above detection',
126
+ folder: 'its active direct membership',
127
+ 'file.text': 'its bytes',
128
+ 'file.binary': 'its bytes',
129
+ link: 'its written target text — the literal address, never what it resolves to',
130
+ }),
131
+
132
+ rails: Object.freeze({
133
+ detect: Object.freeze({ truth }),
134
+ }),
135
+ });
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The Git adapter: a repository is ONE entity — its committed state
5
+ * (the card) plus its working checkpoint — never thousands of unrelated
6
+ * files. One logical operation (a checkout, a rebase) is one change on
7
+ * one UUID, not meaningless bulk churn.
8
+ *
9
+ * One rail bound: detect, through the ratified socket. Truth
10
+ * translates repocard's proven capture into the three-state answer:
11
+ * ground with no repository on it is ABSENT (a vanished .git is how a
12
+ * repository leaves an address — the territory law), a repo caught
13
+ * mid-operation is UNSETTLED (capture's busy markers — MERGE_HEAD, an
14
+ * active rebase — mean the truth is changing under the read), and a
15
+ * present truth's version is the stateId: one fingerprint over cardId
16
+ * and checkpointId. Detection's truth is deliberately bundle-free:
17
+ * comparing is constant work and packing is neither cheap nor
18
+ * deterministic, so the TRANSPORTED head — card with its bundle of
19
+ * objects — is the Send rail's packaging act, not detect's.
20
+ *
21
+ * Repocard's apply and verify remain proven MODULE surfaces
22
+ * (materialize-and-prove, re-read-and-compare); they join the contract
23
+ * when a real multi-type Apply consumer — the Send/Receive rails —
24
+ * exists to derive their socket from. Watching a repo (the follower,
25
+ * the capture worker) stays orchestrated by the observer until the
26
+ * watch socket is ratified from that seam.
27
+ */
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+
32
+ const { capture } = require('../repocard');
33
+ const { lookFailed } = require('./truth');
34
+
35
+ /** The detect socket over capture: same call, same shape as every type. */
36
+ function truth(repoPath) {
37
+ try {
38
+ fs.lstatSync(repoPath);
39
+ } catch (error) {
40
+ return lookFailed(error);
41
+ }
42
+ // No .git (directory or worktree file) means no repository holds this
43
+ // ground: the REPOSITORY is absent, whatever else stands at the path.
44
+ // The look is error-preserving: a .git that cannot be inspected still
45
+ // exists — that read is unsettled, never absence.
46
+ try {
47
+ fs.lstatSync(path.join(repoPath, '.git'));
48
+ } catch (error) {
49
+ return lookFailed(error);
50
+ }
51
+ let captured;
52
+ try {
53
+ captured = capture(repoPath, { withBundle: false });
54
+ } catch (error) {
55
+ // A git subprocess that failed is a repository that could not be
56
+ // inspected — unsettled, never a fourth answer. Only failures git
57
+ // itself reported are classified; a programming error propagates.
58
+ if (!error.gitFailed) throw error;
59
+ return { truth: 'unsettled', reason: error.gitFailed };
60
+ }
61
+ if (captured.busy) return { truth: 'unsettled', reason: captured.busy };
62
+ return {
63
+ truth: 'present',
64
+ version: captured.stateId,
65
+ state: {
66
+ card: captured.card,
67
+ checkpoint: captured.checkpoint,
68
+ cardId: captured.cardId,
69
+ checkpointId: captured.checkpointId,
70
+ },
71
+ };
72
+ }
73
+
74
+ module.exports = Object.freeze({
75
+ name: 'git',
76
+
77
+ types: Object.freeze({
78
+ 'repo.git': 'its card — the committed state — plus its working checkpoint',
79
+ }),
80
+
81
+ rails: Object.freeze({
82
+ detect: Object.freeze({ truth }),
83
+ }),
84
+ });
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The adapter layer's one door. Three adapters cover the registry's
5
+ * whole physical alphabet:
6
+ *
7
+ * filesystem — workspace, folder, file.text, file.binary, link:
8
+ * ordinary filesystem ground, one adapter so same-ground
9
+ * type transitions never change hands (constructs —
10
+ * agents, automations, and kin — are this ground too,
11
+ * their meaning layered above the physical type)
12
+ * git — repo.git: card + checkpoint as one entity
13
+ * reference — reference: a UUID-targeting managed link (assigned,
14
+ * not yet operational)
15
+ *
16
+ * The map is built against the registry's published alphabet at load —
17
+ * an unassigned type, a double claim, or a type the registry doesn't
18
+ * know refuses to load at all, so a drifted map cannot exist at
19
+ * runtime. A generic caller selects with `adapterFor(type)` and speaks
20
+ * only the ratified sockets — the same calls for every type, never an
21
+ * adapter-specific name. Every surface — SDK, CLI, MCP, REST — reaches
22
+ * type behavior through this door; none re-implements it.
23
+ */
24
+
25
+ const { ENTITY_TYPES } = require('../registry');
26
+ const { RAILS, SOCKETS, assertAdapter, buildTypeMap } = require('./contract');
27
+ const filesystem = require('./filesystem');
28
+ const git = require('./git');
29
+ const reference = require('./reference');
30
+
31
+ const ADAPTERS = Object.freeze([filesystem, git, reference]);
32
+ const byType = buildTypeMap(ADAPTERS, ENTITY_TYPES);
33
+
34
+ /** The one selection: entity type → its adapter. Unknown types convict. */
35
+ function adapterFor(type) {
36
+ const adapter = byType.get(type);
37
+ if (!adapter) {
38
+ throw new Error(`no adapter serves entity type ${JSON.stringify(type)} — the alphabet is ${ENTITY_TYPES.join(', ')}`);
39
+ }
40
+ return adapter;
41
+ }
42
+
43
+ module.exports = { ADAPTERS, RAILS, SOCKETS, adapterFor, assertAdapter, buildTypeMap };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The managed-reference adapter: Amalgm's OWN link kind, never conflated
5
+ * with a filesystem symlink. Its payload is the target entity's UUID —
6
+ * identity, not an address — and its local rendering as an ordinary
7
+ * symlink is DERIVED state: canonical-truth-projected on every boundary
8
+ * pass, missing renderings recreated, drifted ones rewritten, real
9
+ * ground on the slot never overwritten, dangling honestly while the
10
+ * target is unavailable.
11
+ *
12
+ * ASSIGNED, NOT YET OPERATIONAL — and that absence is the honest
13
+ * truth: no rail is bound because the projection lives inside the
14
+ * registration boundary today (registration/index.js maintains
15
+ * renderings as part of every pass), proven by the wall but not yet
16
+ * standing as socket surfaces. A reference's truth is registry
17
+ * identity, not disk — its detect socket is ratified from that seam
18
+ * when the boundary's projection is extracted, never invented ahead
19
+ * of it. Send-side resolution of the target UUID on other machines
20
+ * rides the cloud rails.
21
+ */
22
+
23
+ module.exports = Object.freeze({
24
+ name: 'reference',
25
+
26
+ types: Object.freeze({
27
+ reference: "its target entity's UUID — identity, not an address; the rendered symlink is derived state",
28
+ }),
29
+
30
+ rails: Object.freeze({}),
31
+ });
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The three-state vocabulary's one shared judgment: what a FAILED look
5
+ * at ground means. ENOENT and ENOTDIR are the filesystem itself saying
6
+ * "not there" — honest ABSENCE, a fact. Every other failure means the
7
+ * LOOK failed, not the ground — UNSETTLED: no conclusion, read again.
8
+ *
9
+ * Every truth reader classifies through this one function so no reader
10
+ * can invent its own idea of absence: mistaking "could not look" for
11
+ * "not there" is the exact lie the axiom forbids. Only present or
12
+ * absent may produce a conclusion.
13
+ */
14
+ const lookFailed = (error) =>
15
+ error.code === 'ENOENT' || error.code === 'ENOTDIR'
16
+ ? { truth: 'absent' }
17
+ : { truth: 'unsettled', reason: error.code };
18
+
19
+ module.exports = { lookFailed };
@@ -599,26 +599,18 @@ function getCookieCloudBootstrap(options = {}) {
599
599
 
600
600
  function installCookieCloudBootstrap(input = {}, options = {}) {
601
601
  const database = options.database || require('../state/db').openLocalDb();
602
- const replicas = require('../state/replicas');
603
- const envelope = replicas.validateSnapshotEnvelope(input, {
604
- contract: COOKIE_CLOUD_CONTRACT,
605
- schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
606
- });
607
- const expectedResourceId = privateCookieResourceId(
608
- require('../workspace/identity').userId(),
609
- );
610
- if (!expectedResourceId || envelope.resourceId !== expectedResourceId) {
602
+ const replica = cookieCloudReplica({ database });
603
+ if (!replica || input.resourceId !== replica.resourceId) {
611
604
  throw new Error('Private cookie resource does not belong to this runtime user');
612
605
  }
606
+ if (input.contract !== COOKIE_CLOUD_CONTRACT || Number(input.schemaVersion) !== COOKIE_JAR_SCHEMA_VERSION) {
607
+ throw new Error('Private cookie resource contract changed');
608
+ }
613
609
  const bytes = Buffer.from(String(input.snapshotBase64 || ''), 'base64');
614
610
  const checksum = crypto.createHash('sha256').update(bytes).digest('hex');
615
611
  if (checksum !== input.snapshotChecksum) throw new Error('Private cookie snapshot checksum mismatch');
616
612
  const local = loadInternalCookieJar();
617
613
  const remote = normalizeCloudSnapshot(JSON.parse(bytes.toString('utf8')));
618
- if (remote.version !== envelope.snapshotVersion) {
619
- throw new Error('Private cookie snapshot version mismatch');
620
- }
621
- const replica = cookieCloudReplica({ database });
622
614
  const pending = require('../state/mutations').listPendingCloudMutations(replica.resourceId, { database });
623
615
  const pendingKeys = new Set(pending.map((mutation) => mutation.operation?.key).filter(Boolean));
624
616
  // A base snapshot behind the official head is not the current cloud jar.
@@ -626,62 +618,28 @@ function installCookieCloudBootstrap(input = {}, options = {}) {
626
618
  // as pre-cloud work and create an echo loop on every reconnect. The gateway
627
619
  // normally compacts private resources to head before bootstrap; this guard
628
620
  // makes the runtime fail closed if an older gateway sends a stale base.
629
- const snapshotIsCurrent = envelope.snapshotVersion === envelope.headVersion;
621
+ const snapshotIsCurrent = Number(input.snapshotVersion) >= Number(input.headVersion);
630
622
  const bootstrapOperations = snapshotIsCurrent
631
623
  ? localBootstrapOperations(local, remote, pendingKeys)
632
624
  : [];
633
625
 
634
- if (!snapshotIsCurrent) {
635
- // A stale prefix is useful only as an identity check for a prefix this
636
- // machine has already applied. Advancing to a newer stale prefix without
637
- // installing its bytes would make the cursor lie; installing it would
638
- // overwrite local legacy state before a head snapshot can classify it.
639
- const currentReplica = replicas.refreshReplicaAuthority({
640
- resourceId: replica.resourceId,
641
- authorityEpoch: envelope.authorityEpoch,
642
- headVersion: envelope.headVersion,
643
- contract: COOKIE_CLOUD_CONTRACT,
644
- schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
645
- }, { database });
646
- return {
647
- replica: currentReplica,
648
- jar: readCookieJar(),
649
- bootstrappedMutations: 0,
650
- bootstrapDeferred: true,
651
- };
652
- }
653
-
654
- // Legacy local values become durable mutations before the official
655
- // snapshot can replace the physical jar. If the process dies after the
656
- // replacement, restart can replay these rows; in-memory bootstrapOperations
657
- // are never the only copy of user state.
658
- let currentReplica = replica;
659
- for (const operation of bootstrapOperations) {
660
- commitCookieOperation(operation, currentReplica, { database });
661
- currentReplica = cookieCloudReplica({ database });
662
- }
663
- const durablePending = require('../state/mutations')
664
- .listPendingCloudMutations(replica.resourceId, { database });
665
-
666
626
  persistCloudSnapshot({
667
627
  schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
668
628
  version: remote.version,
669
629
  records: [...remote.records.values()],
670
630
  tombstones: [...remote.tombstones.values()],
671
631
  });
672
- if (options.testHooks?.crashAfterSnapshotPersist === true) {
673
- throw new Error('simulated cookie bootstrap crash after snapshot persist');
674
- }
675
- replicas.advanceReplicaSnapshot({
676
- resourceId: replica.resourceId,
677
- snapshotVersion: envelope.snapshotVersion,
678
- snapshotChecksum: checksum,
679
- authorityEpoch: envelope.authorityEpoch,
632
+ const { advanceReplicaSnapshot, refreshReplicaAuthority } = require('../state/replicas');
633
+ advanceReplicaSnapshot({
634
+ resourceId: replica.resourceId,
635
+ snapshotVersion: Number(input.snapshotVersion) || 0,
636
+ snapshotChecksum: checksum,
637
+ authorityEpoch: Number(input.authorityEpoch) || 1,
680
638
  }, { database });
681
- replicas.refreshReplicaAuthority({
639
+ refreshReplicaAuthority({
682
640
  resourceId: replica.resourceId,
683
- authorityEpoch: envelope.authorityEpoch,
684
- headVersion: envelope.headVersion,
641
+ authorityEpoch: Number(input.authorityEpoch) || 1,
642
+ headVersion: Number(input.headVersion) || 0,
685
643
  contract: COOKIE_CLOUD_CONTRACT,
686
644
  schemaVersion: COOKIE_JAR_SCHEMA_VERSION,
687
645
  }, { database });
@@ -689,18 +647,12 @@ function installCookieCloudBootstrap(input = {}, options = {}) {
689
647
  // Snapshot installation never discards local work. Already-journaled edits
690
648
  // replay from their durable rows; pre-cloud legacy entries become ordinary
691
649
  // new mutations through the same commit path.
692
- for (const mutation of durablePending) {
693
- materializeCookieOperation(mutation.operation);
694
- if (mutation.state === 'saving-local' && mutation.materializedAt === null) {
695
- require('../state/mutations').completeMaterializedMutation(
696
- mutation.channelId,
697
- mutation.mutationId,
698
- cookieEvent(mutation.operation, mutation.mutationId),
699
- { database },
700
- );
701
- }
650
+ for (const mutation of pending) materializeCookieOperation(mutation.operation);
651
+ let currentReplica = cookieCloudReplica({ database });
652
+ for (const operation of bootstrapOperations) {
653
+ commitCookieOperation(operation, currentReplica, { database });
654
+ currentReplica = cookieCloudReplica({ database });
702
655
  }
703
- currentReplica = cookieCloudReplica({ database });
704
656
  return {
705
657
  replica: currentReplica,
706
658
  jar: readCookieJar(),
@@ -127,6 +127,10 @@ async function boot() {
127
127
  ensureAppsDirs();
128
128
  startProjectContextService();
129
129
 
130
+ // The authenticated tunnel coordinates user-home bootstrap after it asks
131
+ // the gateway whether this user already has a cloud UUID graph. Runtime
132
+ // startup must not mint a competing graph before that answer arrives.
133
+
130
134
  require('./state/ports').startPortsBridge();
131
135
  require('./workspace/cards').startRepoCardService();
132
136
  require('./workspace/tree-store').startTreeEnrollmentService();