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
@@ -1,11 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Local adapter between a checkout and its one canonical project stream.
4
+ * One ordered card+checkpoint stream per code project.
5
5
  *
6
- * repoKey is only a local lookup alias. resourceId is the sole cloud key.
7
- * The cloud stores exactly one card and one checkpoint; this file derives
8
- * one card.set or checkpoint.set proposal from the current checkout.
6
+ * A project is the authorization pool for one repository. Same-user
7
+ * machines derive the same resource from (owner, repo identity). A
8
+ * collaborator binds that repo identity to the owner's resource after the
9
+ * owner grants membership. Every authorized machine is then an ordinary
10
+ * writer in the same stream.
9
11
  */
10
12
 
11
13
  const crypto = require('crypto');
@@ -13,22 +15,22 @@ const fs = require('fs');
13
15
  const path = require('path');
14
16
  const { AMALGM_DIR } = require('../config');
15
17
  const { subscribeStateEvents } = require('../state/events');
16
- const durable = require('./durable');
17
18
  const { machineId, userId } = require('./identity');
18
19
  const { listRepoCards } = require('./cards');
19
20
  const {
20
21
  CHECKPOINT_CONTRACT,
22
+ checkpointValue,
21
23
  checkpointValueReady,
22
24
  readCheckpoint,
23
25
  repoKeyOfCard,
24
- stampCheckpointMutation,
26
+ sameTransition,
25
27
  } = require('./checkpoint');
26
- const projectStream = require('./project-stream');
27
28
 
28
- const { WALL_CONTRACT } = projectStream;
29
- const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects-v2');
29
+ const WALL_CONTRACT = 'code-project@4';
30
+ const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
30
31
  const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
31
32
  const PUBLISH_QUIET_MS = 25;
33
+ const COMPACT_AT_BYTES = 1024 * 1024;
32
34
  const ENSURE_STEP_TIMEOUT_MS = 20_000;
33
35
  const ENSURE_INTERVAL_MS = 15_000;
34
36
  const ATTACH_REJECTED = /shared_resource_read_forbidden/;
@@ -44,6 +46,10 @@ const service = {
44
46
  lastPublishedAt: null,
45
47
  };
46
48
 
49
+ function sha256(text) {
50
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
51
+ }
52
+
47
53
  function readRegistry() {
48
54
  try {
49
55
  const parsed = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
@@ -54,225 +60,155 @@ function readRegistry() {
54
60
  }
55
61
 
56
62
  function writeRegistry(registry) {
57
- durable.writeJson(REGISTRY_FILE, registry, { pretty: true });
63
+ fs.mkdirSync(PROJECT_DIR, { recursive: true });
64
+ fs.writeFileSync(REGISTRY_FILE, `${JSON.stringify(registry, null, 2)}\n`);
58
65
  }
59
66
 
60
- function projectFile(resourceId) {
61
- const digest = crypto.createHash('sha256').update(resourceId).digest('hex').slice(0, 24);
62
- return path.join(PROJECT_DIR, `${digest}.json`);
63
- }
64
-
65
- function canonicalPath(localPath) {
66
- const resolved = path.resolve(localPath);
67
- const tail = [];
68
- let existing = resolved;
69
- while (!fs.existsSync(existing)) {
70
- const parent = path.dirname(existing);
71
- if (parent === existing) return resolved;
72
- tail.unshift(path.basename(existing));
73
- existing = parent;
74
- }
75
- return path.join(fs.realpathSync.native(existing), ...tail);
67
+ function projectResourceId(repoKey, ownerUserId = userId()) {
68
+ if (!repoKey || !ownerUserId) return null;
69
+ const digest = crypto.createHash('sha256')
70
+ .update(`amalgm-code-project@1|${ownerUserId}|${repoKey}`)
71
+ .digest('base64url');
72
+ return `shr_cp1${digest.slice(0, 40)}`;
76
73
  }
77
74
 
78
- function projectRecord(bound) {
79
- if (!bound?.resourceId) return null;
80
- return {
81
- repoKey: `code-folder:${bound.resourceId}`,
82
- resourceId: bound.resourceId,
83
- ownerUserId: bound.ownerUserId || null,
84
- path: bound?.path || null,
85
- file: projectFile(bound.resourceId),
86
- };
87
- }
88
-
89
- function projects() {
90
- return Object.values(readRegistry()).map(projectRecord).filter(Boolean);
91
- }
92
-
93
- function projectForPath(localPath) {
94
- if (!localPath) return null;
95
- const resolved = canonicalPath(localPath);
96
- return projects().find((project) => (
97
- project.path && canonicalPath(project.path) === resolved
98
- )) || null;
75
+ function projectFile(resourceId) {
76
+ const digest = crypto.createHash('sha256').update(resourceId).digest('hex').slice(0, 24);
77
+ return path.join(PROJECT_DIR, `${digest}.jsonl`);
99
78
  }
100
79
 
101
80
  function projectForRepo(repoKey) {
102
81
  if (!repoKey) return null;
103
- const direct = projects().find((project) => project.repoKey === repoKey);
104
- if (direct) return direct;
105
- const card = listRepoCards().find((candidate) => repoKeyOfCard(candidate) === repoKey);
106
- return projectForPath(card?.path);
107
- }
108
-
109
- function projectForResource(resourceId) {
110
- return projects().find((project) => project.resourceId === resourceId) || null;
82
+ const bound = readRegistry()[repoKey];
83
+ const ownerUserId = bound?.ownerUserId || userId();
84
+ const resourceId = bound?.resourceId || projectResourceId(repoKey, ownerUserId);
85
+ return resourceId ? {
86
+ repoKey,
87
+ resourceId,
88
+ ownerUserId,
89
+ path: bound?.path || null,
90
+ file: projectFile(resourceId),
91
+ } : null;
111
92
  }
112
93
 
113
- function bindProject(resourceId, ownerUserId = null, localPath = null) {
114
- const identity = require('./tree/identity');
115
- if (!identity.isUuid(String(resourceId || ''))) {
116
- throw new Error('resourceId must be the code-folder UUID');
94
+ function bindProject(repoKey, resourceId, ownerUserId = null, localPath = null) {
95
+ if (!repoKey || typeof repoKey !== 'string') throw new Error('repoKey is required');
96
+ if (!/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(resourceId || ''))) {
97
+ throw new Error('resourceId must be an opaque shared resource id');
117
98
  }
118
- if (!identity.isUuid(String(ownerUserId || ''))) {
119
- throw new Error('ownerUserId must be a user UUID');
120
- }
121
- if (typeof localPath !== 'string' || !path.isAbsolute(localPath)) {
99
+ if (localPath !== null && (typeof localPath !== 'string' || !path.isAbsolute(localPath))) {
122
100
  throw new Error('path must be an absolute folder path');
123
101
  }
124
102
  const registry = readRegistry();
125
- const key = resourceId.toLowerCase();
126
- const owner = ownerUserId.toLowerCase();
127
- const boundPath = canonicalPath(localPath);
128
- const existing = registry[key];
129
- if (existing) {
130
- if (
131
- existing.resourceId === key
132
- && existing.ownerUserId === owner
133
- && canonicalPath(existing.path) === boundPath
134
- ) return projectRecord(existing);
135
- throw new Error('that code-folder UUID is already bound differently');
136
- }
137
- const pathOwner = Object.values(registry).find((bound) => (
138
- bound?.path && canonicalPath(bound.path) === boundPath
139
- ));
140
- if (pathOwner) throw new Error('that folder path is already bound to another code-folder UUID');
141
- registry[key] = {
142
- resourceId: key,
143
- ownerUserId: owner,
144
- path: boundPath,
103
+ const previous = registry[repoKey];
104
+ registry[repoKey] = {
105
+ resourceId,
106
+ ownerUserId: ownerUserId || null,
107
+ path: localPath || previous?.path || null,
145
108
  joinedAt: new Date().toISOString(),
146
109
  };
147
110
  writeRegistry(registry);
148
- require('./store').upsertWorkspace({
149
- path: boundPath,
150
- name: path.basename(boundPath),
151
- source: 'computer',
152
- managed: false,
153
- }, { source: 'workspace:code-project-bind' });
154
- const project = projectRecord(registry[key]);
155
111
  if (service.started) {
156
- ensureProjectBound(project)
112
+ ensureProjectBound(projectForRepo(repoKey))
157
113
  .then(() => {
158
114
  schedulePublish();
159
115
  return require('./reducer').reduceNow();
160
116
  })
161
117
  .catch(() => {});
162
118
  }
163
- return project;
119
+ return projectForRepo(repoKey);
164
120
  }
165
121
 
166
- function wallReplica(project) {
167
- const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
168
- return replica?.contract === projectStream.STREAM_CONTRACT ? replica : null;
122
+ function projects() {
123
+ const byRepo = new Map();
124
+ for (const card of listRepoCards()) {
125
+ const repoKey = repoKeyOfCard(card);
126
+ if (repoKey && !byRepo.has(repoKey)) byRepo.set(repoKey, projectForRepo(repoKey));
127
+ }
128
+ for (const repoKey of Object.keys(readRegistry())) {
129
+ if (!byRepo.has(repoKey)) byRepo.set(repoKey, projectForRepo(repoKey));
130
+ }
131
+ return [...byRepo.values()].filter(Boolean);
169
132
  }
170
133
 
171
- function outboundState(rows) {
172
- const terminal = rows.find((row) => row.state === 'rejected' || row.state === 'conflicted');
173
- return {
174
- pending: rows.length,
175
- terminalError: terminal
176
- ? terminal.lastError || `code publication ${terminal.mutationId} was ${terminal.state}`
177
- : null,
178
- };
134
+ function wallReplica(project) {
135
+ return require('../state/replicas').getReplicaByResourceId(project.resourceId);
179
136
  }
180
137
 
181
138
  function publicationStatus(repoKey, reduced = undefined) {
182
139
  const project = projectForRepo(repoKey);
183
140
  const replica = project ? wallReplica(project) : null;
184
141
  const receivedThrough = replica?.appliedVersion ?? null;
185
- if (project && Number.isInteger(receivedThrough)) {
186
- require('../state/mutations').retireStaleCodeMutations(
187
- project.resourceId,
188
- receivedThrough,
189
- );
190
- }
191
142
  const baseline = reduced === undefined
192
- ? require('./reducer').readRepoState(repoKey, project?.resourceId)
143
+ ? require('./reducer').readRepoState(repoKey)
193
144
  : reduced;
194
145
  const appliedThrough = Number.isInteger(baseline?.appliedThrough)
195
146
  ? baseline.appliedThrough
196
147
  : receivedThrough === 0 && !baseline ? 0 : null;
197
- const mutations = require('../state/mutations');
198
- const pendingRows = project
199
- ? mutations.listPendingCloudMutations(project.resourceId)
200
- : [];
201
- const officialRows = project
202
- ? mutations.listUnappliedOfficialMutations(project.resourceId)
203
- : [];
204
- const outgoing = outboundState(pendingRows);
148
+ const pending = project
149
+ ? require('../state/mutations').listPendingCloudMutations(project.resourceId).length
150
+ : 0;
205
151
  return {
206
152
  receivedThrough,
207
153
  appliedThrough,
208
- ...outgoing,
209
- awaitingOfficial: officialRows.length,
154
+ pending,
210
155
  current: receivedThrough !== null
211
156
  && appliedThrough === receivedThrough
212
- && outgoing.pending === 0
213
- && officialRows.length === 0,
157
+ && pending === 0,
214
158
  };
215
159
  }
216
160
 
217
- function localRepo(card, repoKey) {
218
- return card?.repo || (
219
- repoKey.startsWith('root:')
220
- ? { remote: null, rootCommit: repoKey.slice(5) }
221
- : { remote: repoKey, rootCommit: null }
222
- );
161
+ function parseWallLine(line) {
162
+ try {
163
+ const entry = JSON.parse(line);
164
+ if (
165
+ entry?.wall !== WALL_CONTRACT
166
+ || !entry.project
167
+ || !entry.machine
168
+ || !entry.mutationId
169
+ || !Array.isArray(entry.cards)
170
+ ) return null;
171
+ return entry;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ function sealCardVersions(project, card) {
178
+ return {
179
+ ...card,
180
+ positionVersion: validVersion(card.positionVersion)
181
+ ? card.positionVersion
182
+ : officialVersion(project.resourceId, card.positionMutationId),
183
+ branches: (card.branches || []).map((ref) => ({
184
+ ...ref,
185
+ version: validVersion(ref.version)
186
+ ? ref.version
187
+ : officialVersion(project.resourceId, ref.mutationId),
188
+ })),
189
+ checkpoints: (card.checkpoints || []).map((checkpoint) => ({
190
+ ...checkpoint,
191
+ version: validVersion(checkpoint.version)
192
+ ? checkpoint.version
193
+ : officialVersion(project.resourceId, checkpoint.mutationId),
194
+ })),
195
+ };
223
196
  }
224
197
 
225
- /**
226
- * The reducer still consumes its local card-shaped projection. This adapter
227
- * is not shared state: it injects the local repo alias and derives one exact
228
- * clean→current checkpoint transition from the canonical two values.
229
- */
230
198
  function readProjectWall(project) {
231
- const snapshot = projectStream.readSnapshot(project.file, project.resourceId);
232
- if (!snapshot.card || !snapshot.checkpoint) return [];
233
- const card = snapshot.card;
234
- const checkpoint = snapshot.checkpoint;
235
- return [{
236
- wall: WALL_CONTRACT,
237
- project: project.resourceId,
238
- resourceId: project.resourceId,
239
- machine: card.machine,
240
- mutationId: card.mutationId,
241
- version: snapshot.version,
242
- cards: [{
243
- card: projectStream.CARD_CONTRACT,
244
- machine: card.machine,
245
- replica: `official:${project.resourceId}`,
246
- repo: localRepo(null, project.repoKey),
247
- kind: 'primary',
248
- health: 'healthy',
249
- head: card.head,
250
- objects: card.objects,
251
- positionMachine: card.machine,
252
- positionMutationId: card.mutationId,
253
- positionVersion: card.version,
254
- branches: card.branches.map((branch) => ({
255
- ...branch,
256
- machine: card.machine,
257
- mutationId: card.mutationId,
258
- version: card.version,
259
- })),
260
- checkpoints: [{
261
- checkpoint: CHECKPOINT_CONTRACT,
262
- machine: checkpoint.machine,
263
- baseOid: checkpoint.baseOid,
264
- branch: checkpoint.branch,
265
- from: {
266
- revision: null,
267
- indexDiff: '',
268
- worktreeDiff: '',
269
- },
270
- to: checkpoint.value,
271
- mutationId: checkpoint.mutationId,
272
- version: checkpoint.version,
273
- }],
274
- }],
275
- }];
199
+ if (!fs.existsSync(project.file)) return [];
200
+ const entries = new Map();
201
+ for (const line of require('../state/docs').getDocText(project.file).split('\n')) {
202
+ if (!line) continue;
203
+ const entry = parseWallLine(line);
204
+ if (entry?.project !== project.repoKey) continue;
205
+ entries.set(entry.machine, {
206
+ ...entry,
207
+ resourceId: project.resourceId,
208
+ cards: entry.cards.map((card) => sealCardVersions(project, card)),
209
+ });
210
+ }
211
+ return [...entries.values()];
276
212
  }
277
213
 
278
214
  function readWall(repoKey = null) {
@@ -280,120 +216,365 @@ function readWall(repoKey = null) {
280
216
  return selected.flatMap((project) => readProjectWall(project));
281
217
  }
282
218
 
219
+ function publishedCardFor(card) {
220
+ const repoKey = repoKeyOfCard(card);
221
+ if (!repoKey) return null;
222
+ const own = readWall(repoKey).find((entry) => entry.machine === machineId());
223
+ const published = own?.cards?.find((candidate) => candidate.replica === card.replica);
224
+ if (
225
+ !published
226
+ || !Number.isInteger(published.positionVersion)
227
+ || published.positionVersion < 1
228
+ || published.head?.branch !== card.head?.branch
229
+ || published.head?.oid !== card.head?.oid
230
+ || Boolean(published.head?.detached) !== Boolean(card.head?.detached)
231
+ || hasObjectUpgrade(card, { card: published, branches: published.branches })
232
+ ) return null;
233
+ return published;
234
+ }
235
+
283
236
  function samePosition(left, right) {
284
237
  return left?.head?.branch === right?.head?.branch
285
238
  && left?.head?.oid === right?.head?.oid
286
239
  && Boolean(left?.head?.detached) === Boolean(right?.head?.detached);
287
240
  }
288
241
 
289
- function branchIdentity(refs) {
290
- return JSON.stringify((refs || [])
291
- .map((ref) => ({ name: ref.name, oid: ref.oid }))
292
- .sort((left, right) => left.name.localeCompare(right.name)));
293
- }
294
-
295
- function sameCardState(localCard, officialCard) {
296
- return samePosition(localCard, officialCard)
297
- && branchIdentity(localCard?.refs || localCard?.branches)
298
- === branchIdentity(officialCard?.refs || officialCard?.branches);
299
- }
300
-
301
- function publishedCardFor(card) {
302
- const project = projectForPath(card?.path);
303
- if (!project) return null;
304
- const projected = readProjectWall(project)[0]?.cards?.[0] || null;
305
- return projected && sameCardState(card, projected) ? projected : null;
306
- }
307
-
308
- function isCommittedCard(card) {
309
- return typeof card?.head?.oid === 'string'
310
- && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(card.head.oid);
311
- }
312
-
313
- function cardPayloadsReady(card) {
314
- if (card.head?.oid && card.objects?.kind !== 'bundle') return false;
315
- return (card.refs || card.branches || []).every((ref) => (
316
- !ref.oid || ref.objects?.kind === 'bundle'
317
- ));
242
+ function oidMap(refs) {
243
+ return new Map((refs || []).map((ref) => [ref.name, ref.oid ?? null]));
318
244
  }
319
245
 
320
- function hasPendingContributionObjects(card) {
321
- return !cardPayloadsReady(card);
322
- }
246
+ /**
247
+ * Publishing is a transition from the last shared state this checkout
248
+ * materialized. A local value equal to either that baseline or the current
249
+ * rail is not new work. Anything else is a local contribution.
250
+ */
251
+ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousCard = null) {
252
+ if (!plan?.card) return true;
253
+ const onRail = samePosition(card, plan.card);
254
+ const atBaseline = baseline
255
+ && card.head?.branch === baseline.branch
256
+ && card.head?.oid === baseline.baseOid
257
+ && !baseline.pendingCard;
258
+ const alreadyPublished = validVersion(previousCard?.positionVersion)
259
+ && samePosition(card, previousCard);
260
+ const reversesPublishedSharedPosition = previousCard
261
+ && validVersion(previousCard.positionVersion)
262
+ && samePosition(previousCard, plan.card)
263
+ && !alreadyPublished;
264
+ if ((!onRail && !atBaseline && !alreadyPublished) || reversesPublishedSharedPosition) return true;
265
+
266
+ const currentRefs = oidMap(card.refs);
267
+ const railRefs = oidMap(plan.branches);
268
+ const baselineRefs = oidMap(baseline?.branches);
269
+ const previousRefs = new Map(
270
+ (previousCard?.branches || previousCard?.refs || []).map((ref) => [ref.name, ref]),
271
+ );
272
+ for (const name of new Set([
273
+ ...currentRefs.keys(),
274
+ ...baselineRefs.keys(),
275
+ ...previousRefs.keys(),
276
+ ])) {
277
+ const current = currentRefs.get(name) ?? null;
278
+ const shared = railRefs.get(name) ?? null;
279
+ const previous = baselineRefs.get(name) ?? null;
280
+ const publishedRef = previousRefs.get(name);
281
+ const published = publishedRef?.oid ?? null;
282
+ const matchesBaseline = baselineRefs.has(name) && current === previous;
283
+ const matchesPublished = validVersion(publishedRef?.version) && current === published;
284
+ const reversesPublishedSharedValue = validVersion(publishedRef?.version)
285
+ && published === shared
286
+ && current !== published;
287
+ if (
288
+ current !== shared
289
+ && ((!matchesBaseline && !matchesPublished) || reversesPublishedSharedValue)
290
+ ) return true;
291
+ }
323
292
 
324
- function checkpointForCard(card, repoKey) {
325
- const checkpoint = readCheckpoint(machineId(), repoKey);
326
293
  if (
327
- checkpoint
328
- && checkpoint.checkpoint === CHECKPOINT_CONTRACT
294
+ onRail
295
+ && checkpoint
329
296
  && checkpoint.baseOid === card.head?.oid
330
297
  && checkpoint.branch === card.head?.branch
331
- && checkpointValueReady(checkpoint.to)
332
- ) return checkpoint;
333
- const dirty = card.dirty || {};
334
- const clean = ['staged', 'unstaged', 'untracked', 'conflicted', 'submodulesChanged']
335
- .every((key) => Number(dirty[key] || 0) === 0);
336
- return clean ? {
298
+ ) {
299
+ const latest = [...(plan.transitions || [])]
300
+ .reverse()
301
+ .find((entry) => entry.machine === checkpoint.machine);
302
+ if (!sameTransition(latest, checkpoint)) return true;
303
+ }
304
+ return false;
305
+ }
306
+
307
+ function hasObjectUpgrade(card, plan) {
308
+ if (!plan?.card || !samePosition(card, plan.card)) return false;
309
+ if (card.objects?.kind === 'bundle' && plan.card.objects?.kind !== 'bundle') return true;
310
+ const shared = new Map((plan.branches || []).map((ref) => [ref.name, ref]));
311
+ return (card.refs || []).some((ref) => {
312
+ const onRail = shared.get(ref.name);
313
+ return onRail
314
+ && onRail.oid === ref.oid
315
+ && ref.objects?.kind === 'bundle'
316
+ && onRail.objects?.kind !== 'bundle';
317
+ });
318
+ }
319
+
320
+ function checkpointBaseFor(card, baseline, project, shared = null) {
321
+ if (
322
+ !baseline
323
+ || baseline.baseOid !== card.head?.oid
324
+ || baseline.branch !== card.head?.branch
325
+ || !Number.isInteger(baseline.appliedThrough)
326
+ || baseline.appliedThrough < 0
327
+ ) return null;
328
+ const value = checkpointValue(
329
+ machineId(),
330
+ project.repoKey,
331
+ card.head.oid,
332
+ baseline.indexDiff || '',
333
+ baseline.worktreeDiff || '',
334
+ );
335
+ if (!checkpointValueReady(value)) return null;
336
+ const reusableThrough = shared
337
+ && shared.baseOid === card.head.oid
338
+ && shared.branch === card.head.branch
339
+ && shared.value?.revision === value.revision
340
+ && Number.isInteger(shared.through)
341
+ && shared.through >= 0;
342
+ return {
337
343
  checkpoint: CHECKPOINT_CONTRACT,
338
344
  baseOid: card.head.oid,
339
345
  branch: card.head.branch,
340
- from: { revision: null, indexDiff: '', worktreeDiff: '' },
341
- to: { revision: null, indexDiff: '', worktreeDiff: '' },
342
- } : null;
346
+ through: reusableThrough ? shared.through : baseline.appliedThrough,
347
+ value,
348
+ };
343
349
  }
344
350
 
345
- function cardSet(card, checkpoint) {
346
- return {
347
- kind: 'card.set',
348
- card: projectStream.CARD_CONTRACT,
349
- head: {
350
- branch: card.head.branch ?? null,
351
- oid: card.head.oid,
352
- detached: Boolean(card.head.detached),
353
- },
354
- objects: card.objects,
355
- branches: (card.refs || card.branches || []).map((ref) => ({
356
- name: ref.name,
357
- oid: ref.oid,
358
- objects: ref.objects,
359
- })),
360
- checkpoint: {
361
- checkpoint: CHECKPOINT_CONTRACT,
362
- baseOid: card.head.oid,
363
- branch: card.head.branch ?? null,
364
- value: checkpoint.to,
365
- },
366
- };
351
+ function hasCheckpointBaseUpgrade(card, plan, baseline, project) {
352
+ const shared = plan?.card?.checkpointBase;
353
+ const candidate = checkpointBaseFor(card, baseline, project, shared);
354
+ return Boolean(shared && candidate) && (
355
+ candidate.through === shared.through
356
+ && candidate.value?.revision === shared.value?.revision
357
+ && JSON.stringify(candidate.value) !== JSON.stringify(shared.value)
358
+ );
367
359
  }
368
360
 
369
- function checkpointSet(card, checkpoint, officialCheckpoint) {
361
+ function validVersion(value) {
362
+ return Number.isInteger(value) && value > 0;
363
+ }
364
+
365
+ function knownMutation(resourceId, mutationId) {
366
+ if (!mutationId) return false;
367
+ return Boolean(require('../state/mutations').getMutation(`resource:${resourceId}`, mutationId));
368
+ }
369
+
370
+ function officialVersion(resourceId, mutationId) {
371
+ if (!mutationId) return null;
372
+ const mutation = require('../state/mutations').getMutation(`resource:${resourceId}`, mutationId);
373
+ return Number.isInteger(mutation?.officialCloudVersion) ? mutation.officialCloudVersion : null;
374
+ }
375
+
376
+ function sealedVersion(resourceId, mutationId, ...versions) {
377
+ return versions.find(validVersion)
378
+ || officialVersion(resourceId, mutationId)
379
+ || null;
380
+ }
381
+
382
+ function stampBranches(
383
+ refs,
384
+ previousBranches,
385
+ sharedBranches,
386
+ baselineBranches,
387
+ mutationId,
388
+ resourceId,
389
+ ) {
390
+ const current = new Map((refs || []).map((ref) => [ref.name, ref]));
391
+ const previous = new Map((previousBranches || []).map((ref) => [ref.name, ref]));
392
+ const shared = new Map((sharedBranches || []).map((ref) => [ref.name, ref]));
393
+ const baseline = new Map((baselineBranches || []).map((ref) => [ref.name, ref]));
394
+ const names = [...new Set([...current.keys(), ...previous.keys()])].sort();
395
+ return names.map((name) => {
396
+ const observed = current.get(name);
397
+ const before = previous.get(name);
398
+ const onRail = shared.get(name);
399
+ const materialized = baseline.get(name);
400
+ const oid = observed?.oid ?? null;
401
+ const unchanged = before && (before.oid ?? null) === oid;
402
+ const previousReusable = unchanged
403
+ && before.mutationId
404
+ && (validVersion(before.version) || knownMutation(resourceId, before.mutationId));
405
+ const railReusable = onRail
406
+ && (onRail.oid ?? null) === oid
407
+ && onRail.mutationId
408
+ && validVersion(onRail.version);
409
+ const baselineReusable = materialized
410
+ && (materialized.oid ?? null) === oid
411
+ && materialized.mutationId
412
+ && validVersion(materialized.version);
413
+ const mutation = previousReusable
414
+ ? before.mutationId
415
+ : railReusable
416
+ ? onRail.mutationId
417
+ : baselineReusable ? materialized.mutationId : mutationId;
418
+ return {
419
+ name,
420
+ oid,
421
+ ...(observed?.objects ? { objects: observed.objects } : {}),
422
+ mutationId: mutation,
423
+ version: sealedVersion(
424
+ resourceId,
425
+ mutation,
426
+ mutation === before?.mutationId ? before.version : null,
427
+ mutation === onRail?.mutationId ? onRail.version : null,
428
+ mutation === materialized?.mutationId ? materialized.version : null,
429
+ ),
430
+ };
431
+ });
432
+ }
433
+
434
+ function sharedCard(card) {
370
435
  return {
371
- kind: 'checkpoint.set',
372
- checkpoint: CHECKPOINT_CONTRACT,
373
- baseOid: card.head.oid,
374
- branch: card.head.branch ?? null,
375
- from: officialCheckpoint.value,
376
- to: checkpoint.to,
436
+ card: card.card,
437
+ machine: card.machine,
438
+ replica: card.replica,
439
+ repo: card.repo,
440
+ objects: card.objects,
441
+ kind: card.kind,
442
+ head: {
443
+ branch: card.head?.branch ?? null,
444
+ oid: card.head?.oid ?? null,
445
+ detached: Boolean(card.head?.detached),
446
+ },
447
+ health: card.health,
377
448
  };
378
449
  }
379
450
 
380
- function hasLocalContribution(card, plan) {
381
- return !plan?.card || !sameCardState(card, plan.card);
451
+ function isCommittedCard(card) {
452
+ return typeof card?.head?.oid === 'string' && /^[0-9a-f]{40,64}$/.test(card.head.oid);
382
453
  }
383
454
 
384
- function publishProject(project, rawCards) {
385
- const intents = require('./intent');
386
- const intent = intents.readOwnedIntent(project);
387
- if (intent?.terminal) {
388
- service.counters.publishSkips += 1;
455
+ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, project) {
456
+ const previousByReplica = new Map(
457
+ (previousCards || []).filter((card) => card?.replica).map((card) => [card.replica, card]),
458
+ );
459
+ return cards.map((card) => {
460
+ const previous = previousByReplica.get(card.replica);
461
+ const reusablePosition = samePosition(previous, card)
462
+ && previous?.positionMutationId
463
+ && (
464
+ validVersion(previous.positionVersion)
465
+ || knownMutation(project.resourceId, previous.positionMutationId)
466
+ );
467
+ const reusableRailPosition = samePosition(sharedPlan?.card, card)
468
+ && sharedPlan.card.positionMutationId
469
+ && validVersion(sharedPlan.card.positionVersion);
470
+ const reusableBaselinePosition = baseline
471
+ && card.head?.branch === baseline.branch
472
+ && card.head?.oid === baseline.baseOid
473
+ && baseline.cardMutationId
474
+ && validVersion(baseline.cardVersion);
475
+ const positionMutationId = reusablePosition
476
+ ? previous.positionMutationId
477
+ : reusableRailPosition
478
+ ? sharedPlan.card.positionMutationId
479
+ : reusableBaselinePosition ? baseline.cardMutationId : mutationId;
480
+ const positionVersion = sealedVersion(
481
+ project.resourceId,
482
+ positionMutationId,
483
+ positionMutationId === previous?.positionMutationId ? previous.positionVersion : null,
484
+ positionMutationId === sharedPlan?.card?.positionMutationId
485
+ ? sharedPlan.card.positionVersion
486
+ : null,
487
+ positionMutationId === baseline?.cardMutationId ? baseline.cardVersion : null,
488
+ );
489
+ const checkpoint = readCheckpoint(machineId(), project.repoKey);
490
+ const currentCheckpoint = checkpoint
491
+ && checkpoint.baseOid === card.head?.oid
492
+ && checkpoint.branch === card.head?.branch
493
+ ? checkpoint
494
+ : null;
495
+ const checkpointBase = checkpointBaseFor(
496
+ card,
497
+ baseline,
498
+ project,
499
+ sharedPlan?.card?.checkpointBase,
500
+ );
501
+ const sameBase = previous?.head?.oid === card.head?.oid
502
+ && previous?.head?.branch === card.head?.branch;
503
+ const history = sameBase && Array.isArray(previous?.checkpoints)
504
+ ? previous.checkpoints.map((entry) => ({
505
+ ...entry,
506
+ version: sealedVersion(
507
+ project.resourceId,
508
+ entry.mutationId,
509
+ entry.version,
510
+ ),
511
+ })).filter((entry) => (
512
+ validVersion(entry.version)
513
+ && (!checkpointBase || entry.version > checkpointBase.through)
514
+ ))
515
+ : [];
516
+ const last = history.at(-1);
517
+ const checkpoints = currentCheckpoint
518
+ ? (sameTransition(last, currentCheckpoint)
519
+ ? history
520
+ : [...history, { ...currentCheckpoint, mutationId }])
521
+ : history;
389
522
  return {
390
- published: false,
391
- reason: 'local-intent-rejected',
392
- terminalError: intent.terminal.message,
393
- terminalCode: intent.terminal.code,
394
- repoKey: project.repoKey,
523
+ ...sharedCard(card),
524
+ positionMutationId,
525
+ positionVersion,
526
+ branches: stampBranches(
527
+ card.refs,
528
+ previous?.branches,
529
+ sharedPlan?.branches,
530
+ baseline?.branches,
531
+ mutationId,
532
+ project.resourceId,
533
+ ),
534
+ ...(checkpointBase ? { checkpointBase } : {}),
535
+ ...(checkpoints.length ? { checkpoints } : {}),
395
536
  };
537
+ });
538
+ }
539
+
540
+ function withoutPublishedAt(line) {
541
+ const entry = parseWallLine(line);
542
+ if (!entry) return line;
543
+ const { publishedAt, mutationId, ...rest } = entry;
544
+ return JSON.stringify(rest);
545
+ }
546
+
547
+ /**
548
+ * Keep every machine's newest complete record and delete the largest span
549
+ * containing none of them. Repeating this as the wall grows removes all
550
+ * obsolete history, even when a retired machine's last record is near the
551
+ * beginning. Each pass is one deletion, so concurrent appends commute in Yjs.
552
+ */
553
+ function compactWallText(current, repoKey) {
554
+ const latest = new Map();
555
+ const lines = current.split('\n');
556
+ let offset = 0;
557
+ for (let index = 0; index < lines.length; index += 1) {
558
+ const line = lines[index];
559
+ const entry = line ? parseWallLine(line) : null;
560
+ const end = offset + line.length + (index < lines.length - 1 ? 1 : 0);
561
+ if (entry?.project === repoKey) latest.set(entry.machine, { start: offset, end });
562
+ offset = end;
563
+ }
564
+ if (latest.size === 0) return current;
565
+ const records = [...latest.values()].sort((left, right) => left.start - right.start);
566
+ const gaps = [{ start: 0, end: records[0].start }];
567
+ for (let index = 0; index < records.length - 1; index += 1) {
568
+ gaps.push({ start: records[index].end, end: records[index + 1].start });
396
569
  }
570
+ const gap = gaps.reduce((largest, candidate) => (
571
+ candidate.end - candidate.start > largest.end - largest.start ? candidate : largest
572
+ ));
573
+ if (gap.end <= gap.start) return current;
574
+ return `${current.slice(0, gap.start)}${current.slice(gap.end)}`;
575
+ }
576
+
577
+ function publishProject(project, rawCards) {
397
578
  const replica = wallReplica(project);
398
579
  if (!replica) {
399
580
  service.counters.publishSkips += 1;
@@ -403,110 +584,116 @@ function publishProject(project, rawCards) {
403
584
  service.counters.publishSkips += 1;
404
585
  return { published: false, reason: 'repo-materializing', repoKey: project.repoKey };
405
586
  }
406
- const status = publicationStatus(project.repoKey);
407
- if (!status.current) {
587
+ const docs = require('../state/docs');
588
+ let current = docs.getDocText(project.file);
589
+ let previous = null;
590
+ let previousEntry = null;
591
+ for (const line of current.split('\n')) {
592
+ if (!line) continue;
593
+ const entry = parseWallLine(line);
594
+ if (entry?.project === project.repoKey && entry.machine === machineId()) {
595
+ previous = line;
596
+ previousEntry = entry;
597
+ }
598
+ }
599
+
600
+ const reducer = require('./reducer');
601
+ const sharedPlan = reducer.reduceWall(readProjectWall(project)).get(project.repoKey);
602
+ const baseline = reducer.readRepoState(project.repoKey);
603
+ const previousCards = (previousEntry?.cards || []).map((card) => sealCardVersions(project, card));
604
+ const previousReplicas = new Set(previousCards.map((card) => card.replica));
605
+ const currentReplicas = new Set(rawCards.map((card) => card.replica));
606
+ const removed = [...previousReplicas].some((replica) => !currentReplicas.has(replica));
607
+ const contributes = rawCards.some((card) => {
608
+ const checkpoint = readCheckpoint(machineId(), project.repoKey);
609
+ const previousCard = previousCards.find((entry) => entry.replica === card.replica);
610
+ return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
611
+ || hasObjectUpgrade(card, sharedPlan)
612
+ || hasCheckpointBaseUpgrade(card, sharedPlan, baseline, project);
613
+ });
614
+ if (!removed && !contributes) {
408
615
  service.counters.publishSkips += 1;
409
- return {
410
- published: false,
411
- reason: status.terminalError ? 'publication-rejected' : 'project-not-current',
412
- mayMaterialize: true,
413
- repoKey: project.repoKey,
414
- ...status,
415
- };
616
+ return { published: false, reason: 'shared-state-covered', repoKey: project.repoKey };
416
617
  }
417
- if (!intent) {
618
+ const mutationId = `mut_project_${crypto.randomBytes(16).toString('base64url')}`;
619
+ const cards = stampCards(
620
+ rawCards,
621
+ previousCards,
622
+ sharedPlan,
623
+ baseline,
624
+ mutationId,
625
+ project,
626
+ );
627
+ const mine = JSON.stringify({
628
+ wall: WALL_CONTRACT,
629
+ project: project.repoKey,
630
+ machine: machineId(),
631
+ mutationId,
632
+ publishedAt: new Date().toISOString(),
633
+ cards,
634
+ });
635
+ const unchanged = previous !== null
636
+ && withoutPublishedAt(previous) === withoutPublishedAt(mine);
637
+ if (unchanged) {
418
638
  service.counters.publishSkips += 1;
419
- return {
420
- published: false,
421
- reason: 'shared-state-covered',
422
- mayMaterialize: true,
423
- repoKey: project.repoKey,
424
- };
639
+ return { published: false, reason: 'unchanged', repoKey: project.repoKey };
425
640
  }
426
- if (intent.base.through !== status.appliedThrough) {
641
+ const status = publicationStatus(project.repoKey, baseline);
642
+ if (!status.current) {
427
643
  service.counters.publishSkips += 1;
428
644
  return {
429
645
  published: false,
430
- reason: 'local-intent-refreshing',
431
- mayMaterialize: true,
646
+ reason: 'project-not-current',
432
647
  repoKey: project.repoKey,
648
+ ...status,
433
649
  };
434
650
  }
435
- const card = intent.local.card;
436
- const checkpoint = {
437
- checkpoint: CHECKPOINT_CONTRACT,
438
- baseOid: intent.local.checkpoint.baseOid,
439
- branch: intent.local.checkpoint.branch,
440
- from: null,
441
- to: intent.local.checkpoint.value,
442
- };
443
- const snapshot = projectStream.readSnapshot(project.file, project.resourceId);
444
- let delta = null;
445
- if (!snapshot.card || !sameCardState(card, snapshot.card)) {
446
- if (!cardPayloadsReady(card)) {
447
- service.counters.publishSkips += 1;
448
- return { published: false, reason: 'objects-preparing', repoKey: project.repoKey };
651
+
652
+ const separator = current && !current.endsWith('\n') ? '\n' : '';
653
+ if (Buffer.byteLength(`${current}${separator}${mine}\n`, 'utf8') > COMPACT_AT_BYTES) {
654
+ const compacted = compactWallText(current, project.repoKey);
655
+ if (compacted !== current) {
656
+ docs.writeDocText(project.file, compacted, {
657
+ baseHash: sha256(current),
658
+ mutationId: `mut_project_compact_${crypto.randomBytes(16).toString('base64url')}`,
659
+ });
660
+ current = docs.getDocText(project.file);
449
661
  }
450
- delta = cardSet(card, checkpoint);
451
- } else if (checkpoint.to.revision !== snapshot.checkpoint?.value?.revision) {
452
- delta = checkpointSet(card, checkpoint, snapshot.checkpoint);
453
- }
454
- if (!delta) {
455
- delta = checkpointSet(card, checkpoint, snapshot.checkpoint);
456
- }
457
- const reserved = intents.reservePublication(project, intent);
458
- if (!reserved?.publication?.mutationId) {
459
- service.counters.publishSkips += 1;
460
- return {
461
- published: false,
462
- reason: 'local-intent-refreshing',
463
- mayMaterialize: true,
464
- repoKey: project.repoKey,
465
- };
466
662
  }
467
- const result = projectStream.publish(
468
- project,
469
- delta,
470
- reserved.publication.mutationId,
663
+ const nextSeparator = current && !current.endsWith('\n') ? '\n' : '';
664
+ const result = docs.writeDocText(
665
+ project.file,
666
+ `${current}${nextSeparator}${mine}\n`,
667
+ { baseHash: sha256(current), mutationId },
471
668
  );
472
- stampCheckpointMutation(machineId(), project.repoKey, checkpoint, result.mutation.mutationId);
473
669
  service.counters.publishes += 1;
474
670
  service.lastPublishedAt = new Date().toISOString();
475
671
  return {
476
672
  published: true,
477
673
  repoKey: project.repoKey,
478
674
  resourceId: project.resourceId,
479
- mutationId: result.mutation.mutationId,
480
- delta: delta.kind,
675
+ mutationId: result.mutationId,
676
+ merge: result.merge,
677
+ cards: cards.length,
481
678
  };
482
679
  }
483
680
 
484
681
  function publishSelected(selected, allCards, publisher = publishProject) {
485
682
  return selected.map((project) => {
486
683
  try {
487
- const cards = allCards.filter((card) => (
488
- projectForPath(card.path)?.resourceId === project.resourceId
489
- && isCommittedCard(card)
490
- && card.kind === 'primary'
491
- && card.health === 'healthy'
492
- && card.operation === 'none'
493
- ));
494
- if (cards.length === 0) {
495
- service.counters.publishSkips += 1;
496
- return {
497
- published: false,
498
- reason: 'local-checkout-unavailable',
499
- repoKey: project.repoKey,
500
- };
501
- }
502
- return publisher(project, cards);
684
+ return publisher(
685
+ project,
686
+ allCards.filter((card) => (
687
+ repoKeyOfCard(card) === project.repoKey
688
+ && isCommittedCard(card)
689
+ )),
690
+ );
503
691
  } catch (error) {
504
692
  service.counters.publishFailures += 1;
505
693
  console.warn(`[CodeProject] ${project.repoKey} publish failed:`, error.message);
506
694
  return {
507
695
  published: false,
508
696
  reason: 'publish-failed',
509
- terminalError: error.message,
510
697
  repoKey: project.repoKey,
511
698
  error: error.message,
512
699
  };
@@ -554,38 +741,27 @@ async function ensureProject(project) {
554
741
  const attachments = require('../state/attachments');
555
742
  const promotions = require('../state/promotions');
556
743
  fs.mkdirSync(path.dirname(project.file), { recursive: true });
557
- if (!fs.existsSync(project.file)) {
558
- projectStream.writeSnapshot(project.file, projectStream.emptySnapshot(project.resourceId));
559
- }
744
+ if (!fs.existsSync(project.file)) fs.writeFileSync(project.file, '');
560
745
 
561
- const request = attachments.getAttachmentByResource(project.resourceId);
746
+ let request = attachments.getAttachmentByResource(project.resourceId);
562
747
  if (!request) {
563
748
  await callLocal(shared.handleAttachmentStart, {
564
749
  resourceId: project.resourceId,
565
750
  path: project.file,
566
- contract: projectStream.STREAM_CONTRACT,
567
751
  });
568
752
  } else if (request.state === 'rejected') {
569
- let promotion = promotions.getPromotionByResource(project.resourceId);
570
753
  if (
571
754
  ATTACH_REJECTED.test(request.lastError || '')
572
- && project.ownerUserId === userId()
573
- && !promotion
755
+ && project.resourceId === projectResourceId(project.repoKey, userId())
756
+ && !promotions.getPromotionByResource(project.resourceId)
574
757
  ) {
575
758
  await callLocal(shared.handlePromotionStart, {
576
759
  resourceId: project.resourceId,
577
760
  path: project.file,
578
- contract: projectStream.STREAM_CONTRACT,
579
- schemaVersion: 2,
580
761
  });
581
- promotion = await waitForSettled(
582
- project,
583
- () => promotions.getPromotionByResource(project.resourceId),
584
- );
585
- }
586
- if (!wallReplica(project) && promotion?.state === 'promoted') {
587
- attachments.retryAttachment(project.resourceId);
762
+ await waitForSettled(project, () => promotions.getPromotionByResource(project.resourceId));
588
763
  }
764
+ if (!wallReplica(project)) attachments.retryAttachment(project.resourceId);
589
765
  }
590
766
  await waitForSettled(project, () => attachments.getAttachmentByResource(project.resourceId));
591
767
  if (wallReplica(project)) schedulePublish();
@@ -626,12 +802,13 @@ function startWallService() {
626
802
  if (service.started) return;
627
803
  service.started = true;
628
804
  service.unsubscribe = subscribeStateEvents((event) => {
805
+ const acknowledged = event.resource === 'shared-mutations'
806
+ && projects().some((project) => project.resourceId === event.sharedResourceId);
629
807
  if (
630
808
  event.resource === 'repo-cards'
631
809
  || event.resource === 'repo-checkpoints'
632
810
  || event.resource === 'repo-objects'
633
- || event.resource === 'shared-mutations'
634
- || String(event.resource || '').startsWith(projectStream.LOCAL_RESOURCE_PREFIX)
811
+ || acknowledged
635
812
  ) {
636
813
  ensureProjects().catch(() => {});
637
814
  schedulePublish();
@@ -660,7 +837,6 @@ function stopWallService() {
660
837
  function wallStats() {
661
838
  const rows = projects().map((project) => {
662
839
  const replica = wallReplica(project);
663
- const snapshot = projectStream.readSnapshot(project.file, project.resourceId);
664
840
  return {
665
841
  repoKey: project.repoKey,
666
842
  resourceId: project.resourceId,
@@ -674,7 +850,7 @@ function wallStats() {
674
850
  lastError: replica.lastError,
675
851
  updatedAt: replica.updatedAt,
676
852
  },
677
- version: snapshot.version,
853
+ machines: readProjectWall(project).length,
678
854
  };
679
855
  });
680
856
  return {
@@ -689,28 +865,20 @@ function wallStats() {
689
865
  }
690
866
 
691
867
  module.exports = {
692
- _test: {
693
- cardPayloadsReady,
694
- checkpointForCard,
695
- hasPendingContributionObjects,
696
- ensureProject,
697
- outboundState,
698
- publishProject,
699
- publishSelected,
700
- sameCardState,
701
- },
868
+ _test: { publishSelected },
702
869
  WALL_CONTRACT,
703
870
  bindProject,
871
+ compactWallText,
704
872
  hasLocalContribution,
705
873
  isCommittedCard,
706
874
  projectFile,
707
- projectForPath,
708
875
  projectForRepo,
709
- projectForResource,
876
+ projectResourceId,
710
877
  publicationStatus,
711
878
  publishedCardFor,
712
879
  publishWall,
713
880
  readWall,
881
+ stampCards,
714
882
  startWallService,
715
883
  stopWallService,
716
884
  wallStats,