amalgm 0.1.238 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -1,13 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * One ordered card+checkpoint stream per code project.
4
+ * Local adapter between a checkout and its one canonical project stream.
5
5
  *
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.
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.
11
9
  */
12
10
 
13
11
  const crypto = require('crypto');
@@ -15,21 +13,20 @@ const fs = require('fs');
15
13
  const path = require('path');
16
14
  const { AMALGM_DIR } = require('../config');
17
15
  const { subscribeStateEvents } = require('../state/events');
16
+ const durable = require('./durable');
18
17
  const { machineId, userId } = require('./identity');
19
18
  const { listRepoCards } = require('./cards');
20
19
  const {
21
20
  CHECKPOINT_CONTRACT,
22
- checkpointValue,
23
21
  checkpointValueReady,
24
22
  readCheckpoint,
25
23
  repoKeyOfCard,
26
- sameTransition,
27
24
  stampCheckpointMutation,
28
25
  } = require('./checkpoint');
29
26
  const projectStream = require('./project-stream');
30
27
 
31
28
  const { WALL_CONTRACT } = projectStream;
32
- const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
29
+ const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects-v2');
33
30
  const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
34
31
  const PUBLISH_QUIET_MS = 25;
35
32
  const ENSURE_STEP_TIMEOUT_MS = 20_000;
@@ -57,16 +54,7 @@ function readRegistry() {
57
54
  }
58
55
 
59
56
  function writeRegistry(registry) {
60
- fs.mkdirSync(PROJECT_DIR, { recursive: true });
61
- fs.writeFileSync(REGISTRY_FILE, `${JSON.stringify(registry, null, 2)}\n`);
62
- }
63
-
64
- function projectResourceId(repoKey, ownerUserId = userId()) {
65
- if (!repoKey || !ownerUserId) return null;
66
- const digest = crypto.createHash('sha256')
67
- .update(`amalgm-code-project@2|${ownerUserId}|${repoKey}`)
68
- .digest('base64url');
69
- return `shr_cp1${digest.slice(0, 40)}`;
57
+ durable.writeJson(REGISTRY_FILE, registry, { pretty: true });
70
58
  }
71
59
 
72
60
  function projectFile(resourceId) {
@@ -74,66 +62,105 @@ function projectFile(resourceId) {
74
62
  return path.join(PROJECT_DIR, `${digest}.json`);
75
63
  }
76
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);
76
+ }
77
+
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;
99
+ }
100
+
77
101
  function projectForRepo(repoKey) {
78
102
  if (!repoKey) return null;
79
- const bound = readRegistry()[repoKey];
80
- const ownerUserId = bound?.ownerUserId || userId();
81
- const resourceId = bound?.resourceId || projectResourceId(repoKey, ownerUserId);
82
- return resourceId ? {
83
- repoKey,
84
- resourceId,
85
- ownerUserId,
86
- path: bound?.path || null,
87
- file: projectFile(resourceId),
88
- } : 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);
89
107
  }
90
108
 
91
- function bindProject(repoKey, resourceId, ownerUserId = null, localPath = null) {
92
- if (!repoKey || typeof repoKey !== 'string') throw new Error('repoKey is required');
93
- if (!/^shr_[A-Za-z0-9_-]{16,128}$/.test(String(resourceId || ''))) {
94
- throw new Error('resourceId must be an opaque shared resource id');
109
+ function projectForResource(resourceId) {
110
+ return projects().find((project) => project.resourceId === resourceId) || null;
111
+ }
112
+
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');
117
+ }
118
+ if (!identity.isUuid(String(ownerUserId || ''))) {
119
+ throw new Error('ownerUserId must be a user UUID');
95
120
  }
96
- if (localPath !== null && (typeof localPath !== 'string' || !path.isAbsolute(localPath))) {
121
+ if (typeof localPath !== 'string' || !path.isAbsolute(localPath)) {
97
122
  throw new Error('path must be an absolute folder path');
98
123
  }
99
124
  const registry = readRegistry();
100
- const previous = registry[repoKey];
101
- registry[repoKey] = {
102
- resourceId,
103
- ownerUserId: ownerUserId || null,
104
- path: localPath || previous?.path || null,
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,
105
145
  joinedAt: new Date().toISOString(),
106
146
  };
107
147
  writeRegistry(registry);
108
- if (localPath) {
109
- require('./store').upsertWorkspace({
110
- path: localPath,
111
- name: path.basename(localPath),
112
- source: 'computer',
113
- managed: false,
114
- }, { source: 'workspace:code-project-bind' });
115
- }
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]);
116
155
  if (service.started) {
117
- ensureProjectBound(projectForRepo(repoKey))
156
+ ensureProjectBound(project)
118
157
  .then(() => {
119
158
  schedulePublish();
120
159
  return require('./reducer').reduceNow();
121
160
  })
122
161
  .catch(() => {});
123
162
  }
124
- return projectForRepo(repoKey);
125
- }
126
-
127
- function projects() {
128
- const byRepo = new Map();
129
- for (const card of listRepoCards()) {
130
- const repoKey = repoKeyOfCard(card);
131
- if (repoKey && !byRepo.has(repoKey)) byRepo.set(repoKey, projectForRepo(repoKey));
132
- }
133
- for (const repoKey of Object.keys(readRegistry())) {
134
- if (!byRepo.has(repoKey)) byRepo.set(repoKey, projectForRepo(repoKey));
135
- }
136
- return [...byRepo.values()].filter(Boolean);
163
+ return project;
137
164
  }
138
165
 
139
166
  function wallReplica(project) {
@@ -141,61 +168,111 @@ function wallReplica(project) {
141
168
  return replica?.contract === projectStream.STREAM_CONTRACT ? replica : null;
142
169
  }
143
170
 
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
+ };
179
+ }
180
+
144
181
  function publicationStatus(repoKey, reduced = undefined) {
145
182
  const project = projectForRepo(repoKey);
146
183
  const replica = project ? wallReplica(project) : null;
147
184
  const receivedThrough = replica?.appliedVersion ?? null;
185
+ if (project && Number.isInteger(receivedThrough)) {
186
+ require('../state/mutations').retireStaleCodeMutations(
187
+ project.resourceId,
188
+ receivedThrough,
189
+ );
190
+ }
148
191
  const baseline = reduced === undefined
149
192
  ? require('./reducer').readRepoState(repoKey, project?.resourceId)
150
193
  : reduced;
151
194
  const appliedThrough = Number.isInteger(baseline?.appliedThrough)
152
195
  ? baseline.appliedThrough
153
196
  : receivedThrough === 0 && !baseline ? 0 : null;
154
- const pending = project
155
- ? require('../state/mutations').listPendingCloudMutations(project.resourceId).length
156
- : 0;
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);
157
205
  return {
158
206
  receivedThrough,
159
207
  appliedThrough,
160
- pending,
208
+ ...outgoing,
209
+ awaitingOfficial: officialRows.length,
161
210
  current: receivedThrough !== null
162
211
  && appliedThrough === receivedThrough
163
- && pending === 0,
212
+ && outgoing.pending === 0
213
+ && officialRows.length === 0,
164
214
  };
165
215
  }
166
216
 
167
- function sealCardVersions(project, card, record = null) {
168
- const versionFor = (mutationId) => (
169
- mutationId === record?.mutationId && validVersion(record?.version)
170
- ? record.version
171
- : officialVersion(project.resourceId, mutationId)
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 }
172
222
  );
173
- return {
174
- ...card,
175
- positionVersion: validVersion(card.positionVersion)
176
- ? card.positionVersion
177
- : versionFor(card.positionMutationId),
178
- branches: (card.branches || []).map((ref) => ({
179
- ...ref,
180
- version: validVersion(ref.version)
181
- ? ref.version
182
- : versionFor(ref.mutationId),
183
- })),
184
- checkpoints: (card.checkpoints || []).map((checkpoint) => ({
185
- ...checkpoint,
186
- version: validVersion(checkpoint.version)
187
- ? checkpoint.version
188
- : versionFor(checkpoint.mutationId),
189
- })),
190
- };
191
223
  }
192
224
 
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
+ */
193
230
  function readProjectWall(project) {
194
- return projectStream.records(project).map((entry) => ({
195
- ...entry,
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,
196
238
  resourceId: project.resourceId,
197
- cards: entry.cards.map((card) => sealCardVersions(project, card, entry)),
198
- }));
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
276
  }
200
277
 
201
278
  function readWall(repoKey = null) {
@@ -203,356 +280,120 @@ function readWall(repoKey = null) {
203
280
  return selected.flatMap((project) => readProjectWall(project));
204
281
  }
205
282
 
206
- function publishedCardFor(card) {
207
- const repoKey = repoKeyOfCard(card);
208
- if (!repoKey) return null;
209
- const own = readWall(repoKey).find((entry) => entry.machine === machineId());
210
- const published = own?.cards?.find((candidate) => candidate.replica === card.replica);
211
- if (
212
- !published
213
- || !Number.isInteger(published.positionVersion)
214
- || published.positionVersion < 1
215
- || published.head?.branch !== card.head?.branch
216
- || published.head?.oid !== card.head?.oid
217
- || Boolean(published.head?.detached) !== Boolean(card.head?.detached)
218
- ) return null;
219
- return published;
220
- }
221
-
222
283
  function samePosition(left, right) {
223
284
  return left?.head?.branch === right?.head?.branch
224
285
  && left?.head?.oid === right?.head?.oid
225
286
  && Boolean(left?.head?.detached) === Boolean(right?.head?.detached);
226
287
  }
227
288
 
228
- function oidMap(refs) {
229
- return new Map((refs || []).map((ref) => [ref.name, ref.oid ?? null]));
230
- }
231
-
232
- /**
233
- * Publishing is a transition from the last shared state this checkout
234
- * materialized. A local value equal to either that baseline or the current
235
- * rail is not new work. Anything else is a local contribution.
236
- */
237
- function hasLocalContribution(card, plan, baseline, checkpoint = null, previousCard = null) {
238
- if (!plan?.card) return true;
239
- const onRail = samePosition(card, plan.card);
240
- const atBaseline = baseline
241
- && card.head?.branch === baseline.branch
242
- && card.head?.oid === baseline.baseOid
243
- && !baseline.pendingCard;
244
- const alreadyPublished = validVersion(previousCard?.positionVersion)
245
- && samePosition(card, previousCard);
246
- const reversesPublishedSharedPosition = previousCard
247
- && validVersion(previousCard.positionVersion)
248
- && samePosition(previousCard, plan.card)
249
- && !alreadyPublished;
250
- if ((!onRail && !atBaseline && !alreadyPublished) || reversesPublishedSharedPosition) return true;
251
-
252
- const currentRefs = oidMap(card.refs);
253
- const railRefs = oidMap(plan.branches);
254
- const baselineRefs = oidMap(baseline?.branches);
255
- const previousRefs = new Map(
256
- (previousCard?.branches || previousCard?.refs || []).map((ref) => [ref.name, ref]),
257
- );
258
- for (const name of new Set([
259
- ...currentRefs.keys(),
260
- ...baselineRefs.keys(),
261
- ...previousRefs.keys(),
262
- ])) {
263
- const current = currentRefs.get(name) ?? null;
264
- const shared = railRefs.get(name) ?? null;
265
- const previous = baselineRefs.get(name) ?? null;
266
- const publishedRef = previousRefs.get(name);
267
- const published = publishedRef?.oid ?? null;
268
- const matchesBaseline = baselineRefs.has(name) && current === previous;
269
- const matchesPublished = validVersion(publishedRef?.version) && current === published;
270
- const reversesPublishedSharedValue = validVersion(publishedRef?.version)
271
- && published === shared
272
- && current !== published;
273
- if (
274
- current !== shared
275
- && ((!matchesBaseline && !matchesPublished) || reversesPublishedSharedValue)
276
- ) return true;
277
- }
278
-
279
- if (
280
- onRail
281
- && checkpoint
282
- && checkpoint.baseOid === card.head?.oid
283
- && checkpoint.branch === card.head?.branch
284
- ) {
285
- const latest = [...(plan.transitions || [])]
286
- .reverse()
287
- .find((entry) => entry.machine === checkpoint.machine);
288
- if (!sameTransition(latest, checkpoint)) return true;
289
- }
290
- return false;
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)));
291
293
  }
292
294
 
293
- function hasPendingContributionObjects(card, plan) {
294
- if (!plan?.card || !samePosition(card, plan.card)) {
295
- return card.objects?.kind === 'pending';
296
- }
297
- const shared = new Map((plan.branches || []).map((ref) => [ref.name, ref]));
298
- return (card.refs || []).some((ref) => {
299
- const accepted = shared.get(ref.name);
300
- const alreadyAvailable = accepted
301
- && validVersion(accepted.version)
302
- && (accepted.oid ?? null) === (ref.oid ?? null);
303
- return !alreadyAvailable && ref.objects?.kind === 'pending';
304
- });
295
+ function sameCardState(localCard, officialCard) {
296
+ return samePosition(localCard, officialCard)
297
+ && branchIdentity(localCard?.refs || localCard?.branches)
298
+ === branchIdentity(officialCard?.refs || officialCard?.branches);
305
299
  }
306
300
 
307
- function checkpointBaseFor(card, baseline, project, shared = null) {
308
- if (
309
- !baseline
310
- || baseline.baseOid !== card.head?.oid
311
- || baseline.branch !== card.head?.branch
312
- || !Number.isInteger(baseline.appliedThrough)
313
- || baseline.appliedThrough < 0
314
- ) return null;
315
- const value = checkpointValue(
316
- machineId(),
317
- project.repoKey,
318
- card.head.oid,
319
- baseline.indexDiff || '',
320
- baseline.worktreeDiff || '',
321
- );
322
- if (!checkpointValueReady(value)) return null;
323
- const reusableThrough = shared
324
- && shared.baseOid === card.head.oid
325
- && shared.branch === card.head.branch
326
- && shared.value?.revision === value.revision
327
- && Number.isInteger(shared.through)
328
- && shared.through >= 0;
329
- return {
330
- checkpoint: CHECKPOINT_CONTRACT,
331
- baseOid: card.head.oid,
332
- branch: card.head.branch,
333
- through: reusableThrough ? shared.through : baseline.appliedThrough,
334
- value,
335
- };
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;
336
306
  }
337
307
 
338
- function validVersion(value) {
339
- return Number.isInteger(value) && value > 0;
308
+ function isCommittedCard(card) {
309
+ return typeof card?.head?.oid === 'string'
310
+ && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(card.head.oid);
340
311
  }
341
312
 
342
- function knownMutation(resourceId, mutationId) {
343
- if (!mutationId) return false;
344
- return Boolean(require('../state/mutations').getMutation(`resource:${resourceId}`, mutationId));
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
+ ));
345
318
  }
346
319
 
347
- function officialVersion(resourceId, mutationId) {
348
- if (!mutationId) return null;
349
- const mutation = require('../state/mutations').getMutation(`resource:${resourceId}`, mutationId);
350
- return Number.isInteger(mutation?.officialCloudVersion) ? mutation.officialCloudVersion : null;
320
+ function hasPendingContributionObjects(card) {
321
+ return !cardPayloadsReady(card);
351
322
  }
352
323
 
353
- function durableMutationVersion(project, mutationId) {
354
- if (!project || !mutationId) return null;
355
- for (const record of projectStream.records(project)) {
356
- if (record.mutationId === mutationId && validVersion(record.version)) return record.version;
357
- for (const card of record.cards) {
358
- if (card.positionMutationId === mutationId) {
359
- return validVersion(card.positionVersion) ? card.positionVersion : null;
360
- }
361
- const nested = [
362
- ...(card.branches || []),
363
- ...(card.checkpoints || []),
364
- ].find((entry) => entry.mutationId === mutationId);
365
- if (nested) return validVersion(nested.version) ? nested.version : null;
366
- }
367
- }
368
- return null;
369
- }
370
-
371
- function mutationVersion(resourceId, mutationId, repoKey = null) {
372
- if (!resourceId) return null;
373
- return officialVersion(resourceId, mutationId)
374
- || durableMutationVersion(repoKey ? projectForRepo(repoKey) : null, mutationId);
375
- }
376
-
377
- function sealedVersion(resourceId, mutationId, ...versions) {
378
- return versions.find(validVersion)
379
- || officialVersion(resourceId, mutationId)
380
- || null;
381
- }
382
-
383
- function stampBranches(
384
- refs,
385
- previousBranches,
386
- sharedBranches,
387
- baselineBranches,
388
- mutationId,
389
- resourceId,
390
- ) {
391
- const current = new Map((refs || []).map((ref) => [ref.name, ref]));
392
- const previous = new Map((previousBranches || []).map((ref) => [ref.name, ref]));
393
- const shared = new Map((sharedBranches || []).map((ref) => [ref.name, ref]));
394
- const baseline = new Map((baselineBranches || []).map((ref) => [ref.name, ref]));
395
- const names = [...new Set([...current.keys(), ...previous.keys()])].sort();
396
- return names.map((name) => {
397
- const observed = current.get(name);
398
- const before = previous.get(name);
399
- const onRail = shared.get(name);
400
- const materialized = baseline.get(name);
401
- const oid = observed?.oid ?? null;
402
- const unchanged = before && (before.oid ?? null) === oid;
403
- const previousReusable = unchanged
404
- && before.mutationId
405
- && (validVersion(before.version) || knownMutation(resourceId, before.mutationId));
406
- const railReusable = onRail
407
- && (onRail.oid ?? null) === oid
408
- && onRail.mutationId
409
- && validVersion(onRail.version);
410
- const baselineReusable = materialized
411
- && (materialized.oid ?? null) === oid
412
- && materialized.mutationId
413
- && validVersion(materialized.version);
414
- const mutation = previousReusable
415
- ? before.mutationId
416
- : railReusable
417
- ? onRail.mutationId
418
- : baselineReusable ? materialized.mutationId : mutationId;
419
- return {
420
- name,
421
- oid,
422
- ...(observed?.objects ? { objects: observed.objects } : {}),
423
- mutationId: mutation,
424
- version: sealedVersion(
425
- resourceId,
426
- mutation,
427
- mutation === before?.mutationId ? before.version : null,
428
- mutation === onRail?.mutationId ? onRail.version : null,
429
- mutation === materialized?.mutationId ? materialized.version : null,
430
- ),
431
- };
432
- });
324
+ function checkpointForCard(card, repoKey) {
325
+ const checkpoint = readCheckpoint(machineId(), repoKey);
326
+ if (
327
+ checkpoint
328
+ && checkpoint.checkpoint === CHECKPOINT_CONTRACT
329
+ && checkpoint.baseOid === card.head?.oid
330
+ && 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 ? {
337
+ checkpoint: CHECKPOINT_CONTRACT,
338
+ baseOid: card.head.oid,
339
+ branch: card.head.branch,
340
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
341
+ to: { revision: null, indexDiff: '', worktreeDiff: '' },
342
+ } : null;
433
343
  }
434
344
 
435
- function sharedCard(card) {
345
+ function cardSet(card, checkpoint) {
436
346
  return {
437
- card: card.card,
438
- machine: card.machine,
439
- replica: card.replica,
440
- repo: card.repo,
441
- objects: card.objects,
442
- kind: card.kind,
347
+ kind: 'card.set',
348
+ card: projectStream.CARD_CONTRACT,
443
349
  head: {
444
- branch: card.head?.branch ?? null,
445
- oid: card.head?.oid ?? null,
446
- detached: Boolean(card.head?.detached),
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,
447
365
  },
448
- health: card.health,
449
366
  };
450
367
  }
451
368
 
452
- function isCommittedCard(card) {
453
- return typeof card?.head?.oid === 'string' && /^[0-9a-f]{40,64}$/.test(card.head.oid);
454
- }
455
-
456
- function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, project) {
457
- const previousByReplica = new Map(
458
- (previousCards || []).filter((card) => card?.replica).map((card) => [card.replica, card]),
459
- );
460
- return cards.map((card) => {
461
- const previous = previousByReplica.get(card.replica);
462
- const reusablePosition = samePosition(previous, card)
463
- && previous?.positionMutationId
464
- && (
465
- validVersion(previous.positionVersion)
466
- || knownMutation(project.resourceId, previous.positionMutationId)
467
- );
468
- const reusableRailPosition = samePosition(sharedPlan?.card, card)
469
- && sharedPlan.card.positionMutationId
470
- && validVersion(sharedPlan.card.positionVersion);
471
- const reusableBaselinePosition = baseline
472
- && card.head?.branch === baseline.branch
473
- && card.head?.oid === baseline.baseOid
474
- && baseline.cardMutationId
475
- && validVersion(baseline.cardVersion);
476
- const positionMutationId = reusablePosition
477
- ? previous.positionMutationId
478
- : reusableRailPosition
479
- ? sharedPlan.card.positionMutationId
480
- : reusableBaselinePosition ? baseline.cardMutationId : mutationId;
481
- const positionVersion = sealedVersion(
482
- project.resourceId,
483
- positionMutationId,
484
- positionMutationId === previous?.positionMutationId ? previous.positionVersion : null,
485
- positionMutationId === sharedPlan?.card?.positionMutationId
486
- ? sharedPlan.card.positionVersion
487
- : null,
488
- positionMutationId === baseline?.cardMutationId ? baseline.cardVersion : null,
489
- );
490
- const checkpoint = readCheckpoint(machineId(), project.repoKey);
491
- const currentCheckpoint = checkpoint
492
- && checkpoint.baseOid === card.head?.oid
493
- && checkpoint.branch === card.head?.branch
494
- ? checkpoint
495
- : null;
496
- const checkpointBase = checkpointBaseFor(
497
- card,
498
- baseline,
499
- project,
500
- sharedPlan?.card?.checkpointBase,
501
- );
502
- const sameBase = previous?.head?.oid === card.head?.oid
503
- && previous?.head?.branch === card.head?.branch;
504
- const history = sameBase && Array.isArray(previous?.checkpoints)
505
- ? previous.checkpoints.map((entry) => ({
506
- ...entry,
507
- version: sealedVersion(
508
- project.resourceId,
509
- entry.mutationId,
510
- entry.version,
511
- ),
512
- })).filter((entry) => (
513
- validVersion(entry.version)
514
- && (!checkpointBase || entry.version > checkpointBase.through)
515
- ))
516
- : [];
517
- const last = history.at(-1);
518
- const checkpoints = currentCheckpoint
519
- ? (sameTransition(last, currentCheckpoint)
520
- ? history
521
- : [...history, { ...currentCheckpoint, mutationId }])
522
- : history;
523
- return {
524
- ...sharedCard(card),
525
- positionMutationId,
526
- positionVersion,
527
- branches: stampBranches(
528
- card.refs,
529
- previous?.branches,
530
- sharedPlan?.branches,
531
- baseline?.branches,
532
- mutationId,
533
- project.resourceId,
534
- ),
535
- ...(checkpointBase ? { checkpointBase } : {}),
536
- ...(checkpoints.length ? { checkpoints } : {}),
537
- };
538
- });
539
- }
540
-
541
- function cardsUseMutation(cards, mutationId) {
542
- return cards.some((card) => (
543
- card.positionMutationId === mutationId
544
- || (card.branches || []).some((ref) => ref.mutationId === mutationId)
545
- || (card.checkpoints || []).some((checkpoint) => checkpoint.mutationId === mutationId)
546
- ));
369
+ function checkpointSet(card, checkpoint, officialCheckpoint) {
370
+ 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,
377
+ };
547
378
  }
548
379
 
549
- function samePublishedRecord(left, right) {
550
- if (!left || !right) return false;
551
- const stripTransport = ({ publishedAt, mutationId, version, ...record }) => record;
552
- return JSON.stringify(stripTransport(left)) === JSON.stringify(stripTransport(right));
380
+ function hasLocalContribution(card, plan) {
381
+ return !plan?.card || !sameCardState(card, plan.card);
553
382
  }
554
383
 
555
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;
389
+ return {
390
+ published: false,
391
+ reason: 'local-intent-rejected',
392
+ terminalError: intent.terminal.message,
393
+ terminalCode: intent.terminal.code,
394
+ repoKey: project.repoKey,
395
+ };
396
+ }
556
397
  const replica = wallReplica(project);
557
398
  if (!replica) {
558
399
  service.counters.publishSkips += 1;
@@ -562,75 +403,73 @@ function publishProject(project, rawCards) {
562
403
  service.counters.publishSkips += 1;
563
404
  return { published: false, reason: 'repo-materializing', repoKey: project.repoKey };
564
405
  }
565
- const previousEntry = projectStream.records(project)
566
- .find((entry) => entry.machine === machineId()) || null;
567
-
568
- const reducer = require('./reducer');
569
- const sharedPlan = reducer.reduceWall(readProjectWall(project)).get(project.repoKey);
570
- const baseline = reducer.readRepoState(project.repoKey, project.resourceId);
571
- const previousCards = (previousEntry?.cards || [])
572
- .map((card) => sealCardVersions(project, card, previousEntry));
573
- const previousReplicas = new Set(previousCards.map((card) => card.replica));
574
- const currentReplicas = new Set(rawCards.map((card) => card.replica));
575
- const removed = [...previousReplicas].some((replica) => !currentReplicas.has(replica));
576
- const contributes = rawCards.some((card) => {
577
- const checkpoint = readCheckpoint(machineId(), project.repoKey);
578
- const previousCard = previousCards.find((entry) => entry.replica === card.replica);
579
- return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard);
580
- });
581
- if (contributes && rawCards.some((card) => hasPendingContributionObjects(card, sharedPlan))) {
406
+ const status = publicationStatus(project.repoKey);
407
+ if (!status.current) {
582
408
  service.counters.publishSkips += 1;
583
- return { published: false, reason: 'objects-preparing', repoKey: project.repoKey };
409
+ return {
410
+ published: false,
411
+ reason: status.terminalError ? 'publication-rejected' : 'project-not-current',
412
+ mayMaterialize: true,
413
+ repoKey: project.repoKey,
414
+ ...status,
415
+ };
584
416
  }
585
- if (!removed && !contributes) {
417
+ if (!intent) {
586
418
  service.counters.publishSkips += 1;
587
- return { published: false, reason: 'shared-state-covered', repoKey: project.repoKey };
419
+ return {
420
+ published: false,
421
+ reason: 'shared-state-covered',
422
+ mayMaterialize: true,
423
+ repoKey: project.repoKey,
424
+ };
588
425
  }
589
- const mutationId = `mut_project_${crypto.randomBytes(16).toString('base64url')}`;
590
- const cards = stampCards(
591
- rawCards,
592
- previousCards,
593
- sharedPlan,
594
- baseline,
595
- mutationId,
596
- project,
597
- );
598
- const mine = {
599
- wall: WALL_CONTRACT,
600
- project: project.repoKey,
601
- machine: machineId(),
602
- mutationId,
603
- publishedAt: new Date().toISOString(),
604
- cards,
605
- };
606
- if (samePublishedRecord(previousEntry, mine)) {
426
+ if (intent.base.through !== status.appliedThrough) {
607
427
  service.counters.publishSkips += 1;
608
- return { published: false, reason: 'unchanged', repoKey: project.repoKey };
428
+ return {
429
+ published: false,
430
+ reason: 'local-intent-refreshing',
431
+ mayMaterialize: true,
432
+ repoKey: project.repoKey,
433
+ };
609
434
  }
610
- if (!cardsUseMutation(cards, mutationId)) {
611
- service.counters.publishSkips += 1;
612
- return { published: false, reason: 'no-content-change', repoKey: project.repoKey };
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 };
449
+ }
450
+ delta = cardSet(card, checkpoint);
451
+ } else if (checkpoint.to.revision !== snapshot.checkpoint?.value?.revision) {
452
+ delta = checkpointSet(card, checkpoint, snapshot.checkpoint);
613
453
  }
614
- const status = publicationStatus(project.repoKey, baseline);
615
- if (!status.current) {
454
+ if (!delta) {
455
+ delta = checkpointSet(card, checkpoint, snapshot.checkpoint);
456
+ }
457
+ const reserved = intents.reservePublication(project, intent);
458
+ if (!reserved?.publication?.mutationId) {
616
459
  service.counters.publishSkips += 1;
617
460
  return {
618
461
  published: false,
619
- reason: 'project-not-current',
462
+ reason: 'local-intent-refreshing',
463
+ mayMaterialize: true,
620
464
  repoKey: project.repoKey,
621
- ...status,
622
465
  };
623
466
  }
624
-
625
- for (const card of cards) {
626
- const checkpoint = (card.checkpoints || []).find((entry) => (
627
- entry.machine === machineId() && entry.mutationId === mutationId
628
- ));
629
- if (checkpoint) {
630
- stampCheckpointMutation(machineId(), project.repoKey, checkpoint, mutationId);
631
- }
632
- }
633
- const result = projectStream.publish(project, mine);
467
+ const result = projectStream.publish(
468
+ project,
469
+ delta,
470
+ reserved.publication.mutationId,
471
+ );
472
+ stampCheckpointMutation(machineId(), project.repoKey, checkpoint, result.mutation.mutationId);
634
473
  service.counters.publishes += 1;
635
474
  service.lastPublishedAt = new Date().toISOString();
636
475
  return {
@@ -638,26 +477,36 @@ function publishProject(project, rawCards) {
638
477
  repoKey: project.repoKey,
639
478
  resourceId: project.resourceId,
640
479
  mutationId: result.mutation.mutationId,
641
- cards: cards.length,
480
+ delta: delta.kind,
642
481
  };
643
482
  }
644
483
 
645
484
  function publishSelected(selected, allCards, publisher = publishProject) {
646
485
  return selected.map((project) => {
647
486
  try {
648
- return publisher(
649
- project,
650
- allCards.filter((card) => (
651
- repoKeyOfCard(card) === project.repoKey
652
- && isCommittedCard(card)
653
- )),
654
- );
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);
655
503
  } catch (error) {
656
504
  service.counters.publishFailures += 1;
657
505
  console.warn(`[CodeProject] ${project.repoKey} publish failed:`, error.message);
658
506
  return {
659
507
  published: false,
660
508
  reason: 'publish-failed',
509
+ terminalError: error.message,
661
510
  repoKey: project.repoKey,
662
511
  error: error.message,
663
512
  };
@@ -705,9 +554,11 @@ async function ensureProject(project) {
705
554
  const attachments = require('../state/attachments');
706
555
  const promotions = require('../state/promotions');
707
556
  fs.mkdirSync(path.dirname(project.file), { recursive: true });
708
- if (!fs.existsSync(project.file)) fs.writeFileSync(project.file, '');
557
+ if (!fs.existsSync(project.file)) {
558
+ projectStream.writeSnapshot(project.file, projectStream.emptySnapshot(project.resourceId));
559
+ }
709
560
 
710
- let request = attachments.getAttachmentByResource(project.resourceId);
561
+ const request = attachments.getAttachmentByResource(project.resourceId);
711
562
  if (!request) {
712
563
  await callLocal(shared.handleAttachmentStart, {
713
564
  resourceId: project.resourceId,
@@ -715,21 +566,26 @@ async function ensureProject(project) {
715
566
  contract: projectStream.STREAM_CONTRACT,
716
567
  });
717
568
  } else if (request.state === 'rejected') {
569
+ let promotion = promotions.getPromotionByResource(project.resourceId);
718
570
  if (
719
571
  ATTACH_REJECTED.test(request.lastError || '')
720
- && project.resourceId === projectResourceId(project.repoKey, userId())
721
- && !promotions.getPromotionByResource(project.resourceId)
572
+ && project.ownerUserId === userId()
573
+ && !promotion
722
574
  ) {
723
575
  await callLocal(shared.handlePromotionStart, {
724
576
  resourceId: project.resourceId,
725
577
  path: project.file,
726
- project: project.repoKey,
727
578
  contract: projectStream.STREAM_CONTRACT,
728
- schemaVersion: 1,
579
+ schemaVersion: 2,
729
580
  });
730
- await waitForSettled(project, () => promotions.getPromotionByResource(project.resourceId));
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);
731
588
  }
732
- if (!wallReplica(project)) attachments.retryAttachment(project.resourceId);
733
589
  }
734
590
  await waitForSettled(project, () => attachments.getAttachmentByResource(project.resourceId));
735
591
  if (wallReplica(project)) schedulePublish();
@@ -770,17 +626,12 @@ function startWallService() {
770
626
  if (service.started) return;
771
627
  service.started = true;
772
628
  service.unsubscribe = subscribeStateEvents((event) => {
773
- const acknowledgedProject = event.resource === 'shared-mutations'
774
- ? projects().find((project) => project.resourceId === event.sharedResourceId)
775
- : null;
776
- if (acknowledgedProject) {
777
- projectStream.seal(acknowledgedProject, event.mutationId || event.id, event.resourceVersion);
778
- }
779
629
  if (
780
630
  event.resource === 'repo-cards'
781
631
  || event.resource === 'repo-checkpoints'
782
632
  || event.resource === 'repo-objects'
783
- || acknowledgedProject
633
+ || event.resource === 'shared-mutations'
634
+ || String(event.resource || '').startsWith(projectStream.LOCAL_RESOURCE_PREFIX)
784
635
  ) {
785
636
  ensureProjects().catch(() => {});
786
637
  schedulePublish();
@@ -809,6 +660,7 @@ function stopWallService() {
809
660
  function wallStats() {
810
661
  const rows = projects().map((project) => {
811
662
  const replica = wallReplica(project);
663
+ const snapshot = projectStream.readSnapshot(project.file, project.resourceId);
812
664
  return {
813
665
  repoKey: project.repoKey,
814
666
  resourceId: project.resourceId,
@@ -822,7 +674,7 @@ function wallStats() {
822
674
  lastError: replica.lastError,
823
675
  updatedAt: replica.updatedAt,
824
676
  },
825
- machines: readProjectWall(project).length,
677
+ version: snapshot.version,
826
678
  };
827
679
  });
828
680
  return {
@@ -838,24 +690,27 @@ function wallStats() {
838
690
 
839
691
  module.exports = {
840
692
  _test: {
841
- cardsUseMutation,
693
+ cardPayloadsReady,
694
+ checkpointForCard,
842
695
  hasPendingContributionObjects,
696
+ ensureProject,
697
+ outboundState,
698
+ publishProject,
843
699
  publishSelected,
844
- samePublishedRecord,
700
+ sameCardState,
845
701
  },
846
702
  WALL_CONTRACT,
847
703
  bindProject,
848
704
  hasLocalContribution,
849
705
  isCommittedCard,
850
- mutationVersion,
851
706
  projectFile,
707
+ projectForPath,
852
708
  projectForRepo,
853
- projectResourceId,
709
+ projectForResource,
854
710
  publicationStatus,
855
711
  publishedCardFor,
856
712
  publishWall,
857
713
  readWall,
858
- stampCards,
859
714
  startWallService,
860
715
  stopWallService,
861
716
  wallStats,