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,234 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The event ledger — replays the detector's published events into a
5
+ * believed entity state and refuses every stream a correct detector could
6
+ * not have produced:
7
+ *
8
+ * - a created fileId or dirId that was ever used before (fresh identity
9
+ * is fresh),
10
+ * - a change whose previousHash breaks the hash chain,
11
+ * - a change to an equal hash (equal versions are silence),
12
+ * - any event on a deleted or unknown channel,
13
+ * - a rename whose `from` is not where the channel lives,
14
+ * - a territory discovered twice, or removed while unknown.
15
+ *
16
+ * Directory channels follow the same grammar minus content: created,
17
+ * renamed, deleted — there is no dir.changed, because a directory has no
18
+ * bytes of its own. Link channels follow the FILE grammar with the written
19
+ * target as the whole payload: the previousTarget chain must hold, and an
20
+ * equal target is silence. `repo.changed` is a raw doorbell, not a state
21
+ * claim, and is ignored — quieting it is the follower's job.
22
+ */
23
+
24
+ const assert = require('node:assert/strict');
25
+
26
+ function createLedger() {
27
+ const files = new Map(); // fileId -> { rootId, path, hash, deleted }
28
+ const links = new Map(); // linkId -> { rootId, path, target, deleted }
29
+ const dirs = new Map(); // dirId -> { rootId, path, deleted }
30
+ const usedFileIds = new Set();
31
+ const usedLinkIds = new Set();
32
+ const usedDirIds = new Set();
33
+ const territories = new Map(); // absolute path -> rootId
34
+
35
+ function alive(event) {
36
+ const believed = files.get(event.fileId);
37
+ assert.ok(believed, `ledger: ${event.type} for unknown fileId ${event.fileId}`);
38
+ assert.ok(!believed.deleted, `ledger: ${event.type} after deletion of ${event.fileId}`);
39
+ return believed;
40
+ }
41
+
42
+ function aliveLink(event) {
43
+ const believed = links.get(event.linkId);
44
+ assert.ok(believed, `ledger: ${event.type} for unknown linkId ${event.linkId}`);
45
+ assert.ok(!believed.deleted, `ledger: ${event.type} after deletion of ${event.linkId}`);
46
+ return believed;
47
+ }
48
+
49
+ function aliveDir(event) {
50
+ const believed = dirs.get(event.dirId);
51
+ assert.ok(believed, `ledger: ${event.type} for unknown dirId ${event.dirId}`);
52
+ assert.ok(!believed.deleted, `ledger: ${event.type} after deletion of ${event.dirId}`);
53
+ return believed;
54
+ }
55
+
56
+ return {
57
+ ingest(events) {
58
+ for (const event of events) {
59
+ switch (event.type) {
60
+ case 'file.created': {
61
+ assert.ok(!usedFileIds.has(event.fileId),
62
+ `ledger: created fileId ${event.fileId} was used before — a fresh identity must be fresh`);
63
+ for (const believed of files.values()) {
64
+ if (believed.deleted) continue;
65
+ assert.ok(!(believed.rootId === event.rootId && believed.path === event.path),
66
+ `ledger: two live channels claim ${event.path}`);
67
+ }
68
+ usedFileIds.add(event.fileId);
69
+ files.set(event.fileId, {
70
+ rootId: event.rootId, path: event.path, hash: event.hash, deleted: false,
71
+ });
72
+ break;
73
+ }
74
+ case 'file.changed': {
75
+ const believed = alive(event);
76
+ assert.equal(event.path, believed.path,
77
+ `ledger: change names ${event.path} but the channel lives at ${believed.path}`);
78
+ assert.equal(event.previousHash, believed.hash,
79
+ `ledger: hash chain broken at ${event.path}`);
80
+ assert.notEqual(event.hash, event.previousHash,
81
+ `ledger: equal versions must be silence (${event.path})`);
82
+ believed.hash = event.hash;
83
+ break;
84
+ }
85
+ case 'file.renamed': {
86
+ const believed = alive(event);
87
+ assert.equal(event.from, believed.path,
88
+ `ledger: rename from ${event.from} but the channel lives at ${believed.path}`);
89
+ believed.path = event.to;
90
+ break;
91
+ }
92
+ case 'file.deleted': {
93
+ const believed = alive(event);
94
+ assert.equal(event.path, believed.path,
95
+ `ledger: delete names ${event.path} but the channel lives at ${believed.path}`);
96
+ believed.deleted = true;
97
+ break;
98
+ }
99
+ case 'link.created': {
100
+ assert.ok(!usedLinkIds.has(event.linkId),
101
+ `ledger: created linkId ${event.linkId} was used before — a fresh identity must be fresh`);
102
+ for (const believed of links.values()) {
103
+ if (believed.deleted) continue;
104
+ assert.ok(!(believed.rootId === event.rootId && believed.path === event.path),
105
+ `ledger: two live link channels claim ${event.path}`);
106
+ }
107
+ usedLinkIds.add(event.linkId);
108
+ links.set(event.linkId, {
109
+ rootId: event.rootId, path: event.path, target: event.target, deleted: false,
110
+ });
111
+ break;
112
+ }
113
+ case 'link.changed': {
114
+ const believed = aliveLink(event);
115
+ assert.equal(event.path, believed.path,
116
+ `ledger: link change names ${event.path} but the channel lives at ${believed.path}`);
117
+ assert.equal(event.previousTarget, believed.target,
118
+ `ledger: target chain broken at ${event.path}`);
119
+ assert.notEqual(event.target, event.previousTarget,
120
+ `ledger: equal targets must be silence (${event.path})`);
121
+ believed.target = event.target;
122
+ break;
123
+ }
124
+ case 'link.renamed': {
125
+ const believed = aliveLink(event);
126
+ assert.equal(event.from, believed.path,
127
+ `ledger: link rename from ${event.from} but the channel lives at ${believed.path}`);
128
+ believed.path = event.to;
129
+ break;
130
+ }
131
+ case 'link.deleted': {
132
+ const believed = aliveLink(event);
133
+ assert.equal(event.path, believed.path,
134
+ `ledger: link delete names ${event.path} but the channel lives at ${believed.path}`);
135
+ believed.deleted = true;
136
+ break;
137
+ }
138
+ case 'dir.created': {
139
+ assert.ok(!usedDirIds.has(event.dirId),
140
+ `ledger: created dirId ${event.dirId} was used before — a fresh identity must be fresh`);
141
+ for (const believed of dirs.values()) {
142
+ if (believed.deleted) continue;
143
+ assert.ok(!(believed.rootId === event.rootId && believed.path === event.path),
144
+ `ledger: two live dir channels claim ${event.path}`);
145
+ }
146
+ usedDirIds.add(event.dirId);
147
+ dirs.set(event.dirId, { rootId: event.rootId, path: event.path, deleted: false });
148
+ break;
149
+ }
150
+ case 'dir.renamed': {
151
+ const believed = aliveDir(event);
152
+ assert.equal(event.from, believed.path,
153
+ `ledger: dir rename from ${event.from} but the channel lives at ${believed.path}`);
154
+ believed.path = event.to;
155
+ break;
156
+ }
157
+ case 'dir.deleted': {
158
+ const believed = aliveDir(event);
159
+ assert.equal(event.path, believed.path,
160
+ `ledger: dir delete names ${event.path} but the channel lives at ${believed.path}`);
161
+ believed.deleted = true;
162
+ break;
163
+ }
164
+ case 'repo.discovered': {
165
+ assert.ok(!territories.has(event.path),
166
+ `ledger: territory ${event.path} discovered twice without removal`);
167
+ territories.set(event.path, event.rootId);
168
+ break;
169
+ }
170
+ case 'repo.removed': {
171
+ assert.ok(territories.has(event.path),
172
+ `ledger: territory ${event.path} removed while unknown`);
173
+ territories.delete(event.path);
174
+ break;
175
+ }
176
+ case 'repo.changed':
177
+ break; // raw doorbell, never a state claim
178
+ default:
179
+ assert.fail(`ledger: unknown event type ${event.type}`);
180
+ }
181
+ }
182
+ },
183
+
184
+ /** Live believed files, optionally for one root: path -> { fileId, hash }. */
185
+ liveFiles(rootId) {
186
+ const live = new Map();
187
+ for (const [fileId, believed] of files) {
188
+ if (believed.deleted) continue;
189
+ if (rootId && believed.rootId !== rootId) continue;
190
+ live.set(believed.path, { fileId, hash: believed.hash });
191
+ }
192
+ return live;
193
+ },
194
+
195
+ /** Live believed links, optionally for one root: path -> { linkId, target }. */
196
+ liveLinks(rootId) {
197
+ const live = new Map();
198
+ for (const [linkId, believed] of links) {
199
+ if (believed.deleted) continue;
200
+ if (rootId && believed.rootId !== rootId) continue;
201
+ live.set(believed.path, { linkId, target: believed.target });
202
+ }
203
+ return live;
204
+ },
205
+
206
+ /** Live believed directories, optionally for one root: path -> { dirId }. */
207
+ liveDirs(rootId) {
208
+ const live = new Map();
209
+ for (const [dirId, believed] of dirs) {
210
+ if (believed.deleted) continue;
211
+ if (rootId && believed.rootId !== rootId) continue;
212
+ live.set(believed.path, { dirId });
213
+ }
214
+ return live;
215
+ },
216
+
217
+ liveTerritories() {
218
+ return new Map(territories);
219
+ },
220
+
221
+ /** Channel-kind blind: a closed channel is closed, file, link, or dir. */
222
+ isDeleted(channelId) {
223
+ return files.get(channelId)?.deleted === true
224
+ || links.get(channelId)?.deleted === true
225
+ || dirs.get(channelId)?.deleted === true;
226
+ },
227
+
228
+ knows(fileId) {
229
+ return files.has(fileId);
230
+ },
231
+ };
232
+ }
233
+
234
+ module.exports = { createLedger };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The independent disk referee — the oracle's second truth.
5
+ *
6
+ * A deliberately simple test-only reader: plain recursive readdir, plain
7
+ * whole-buffer sha256, its own .git-boundary and policy handling. It shares
8
+ * no traversal or comparison code with the detector (walkRoot,
9
+ * censusBoundaries, diffRootState are all forbidden imports — enforced by a
10
+ * meta-test), so the detector and its referee cannot host the same bug.
11
+ *
12
+ * Read-only by contract: nothing here writes, touches, or scans.
13
+ */
14
+
15
+ const crypto = require('crypto');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ function hashBytes(absPath) {
20
+ return crypto.createHash('sha256').update(fs.readFileSync(absPath)).digest('hex');
21
+ }
22
+
23
+ /**
24
+ * Ground truth for one enrolled root: every enrollable regular file outside
25
+ * repo territory, hashed, every enrollable directory outside repo territory
26
+ * (a directory is ground with no bytes — existence is its whole claim),
27
+ * every enrollable symlink as pointer ground — its target exactly as
28
+ * written, never followed — plus the minimal repo boundaries the walk
29
+ * stops at ('' when the root itself is one). Policy prunes at
30
+ * directories, exactly as the enrollment contract states.
31
+ */
32
+ function readGround(rootPath, shouldEnroll) {
33
+ const files = new Map(); // relPath -> hash
34
+ const links = new Map(); // relPath -> target, exactly as written
35
+ const dirs = new Set(); // relDir, outside repo territory ('' never listed)
36
+ const repoDirs = [];
37
+
38
+ (function walk(relDir) {
39
+ const absDir = relDir ? path.join(rootPath, relDir) : rootPath;
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
43
+ } catch {
44
+ return; // vanished mid-read; the settled truth is the next read's
45
+ }
46
+ if (entries.some((entry) => entry.name === '.git')) {
47
+ repoDirs.push(relDir);
48
+ return;
49
+ }
50
+ if (relDir) dirs.add(relDir);
51
+ for (const entry of entries) {
52
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
53
+ if (!shouldEnroll(relPath)) continue;
54
+ if (entry.isSymbolicLink()) {
55
+ try {
56
+ links.set(relPath, fs.readlinkSync(path.join(rootPath, relPath)));
57
+ } catch {
58
+ /* vanished mid-read */
59
+ }
60
+ } else if (entry.isDirectory()) walk(relPath);
61
+ else if (entry.isFile()) {
62
+ try {
63
+ files.set(relPath, hashBytes(path.join(rootPath, relPath)));
64
+ } catch {
65
+ /* vanished mid-read */
66
+ }
67
+ }
68
+ }
69
+ })('');
70
+
71
+ return { files, links, dirs, repoDirs };
72
+ }
73
+
74
+ /**
75
+ * Every repo boundary under rootPath at any depth, as absolute paths —
76
+ * recursing INTO each boundary the way each root runs its own census, with
77
+ * the enrollment policy re-applied relative to that boundary.
78
+ */
79
+ function allBoundaries(rootPath, shouldEnroll) {
80
+ const found = [];
81
+
82
+ function census(baseAbs) {
83
+ const pending = [''];
84
+ while (pending.length > 0) {
85
+ const relDir = pending.pop();
86
+ const absDir = relDir ? path.join(baseAbs, relDir) : baseAbs;
87
+ let entries;
88
+ try {
89
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
90
+ } catch {
91
+ continue;
92
+ }
93
+ if (relDir && entries.some((entry) => entry.name === '.git')) {
94
+ found.push(absDir);
95
+ census(absDir); // a child's children are the child's own census
96
+ continue;
97
+ }
98
+ for (const entry of entries) {
99
+ if (!entry.isDirectory() || entry.name === '.git') continue;
100
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
101
+ if (!shouldEnroll(rel)) continue;
102
+ pending.push(rel);
103
+ }
104
+ }
105
+ }
106
+
107
+ census(rootPath);
108
+ let hasRootGit = false;
109
+ try {
110
+ hasRootGit = fs.existsSync(path.join(rootPath, '.git'));
111
+ } catch {
112
+ /* unreadable root: no boundary claim */
113
+ }
114
+ if (hasRootGit) found.push(rootPath);
115
+ return found.sort();
116
+ }
117
+
118
+ module.exports = { allBoundaries, hashBytes, readGround };