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.
- package/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* One ordered card+checkpoint stream per code project.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
26
|
+
sameTransition,
|
|
25
27
|
} = require('./checkpoint');
|
|
26
|
-
const projectStream = require('./project-stream');
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
-
const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects
|
|
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
|
-
|
|
63
|
+
fs.mkdirSync(PROJECT_DIR, { recursive: true });
|
|
64
|
+
fs.writeFileSync(REGISTRY_FILE, `${JSON.stringify(registry, null, 2)}\n`);
|
|
58
65
|
}
|
|
59
66
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
79
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
if (
|
|
116
|
-
throw new Error('resourceId must be
|
|
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 (
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
|
119
|
+
return projectForRepo(repoKey);
|
|
164
120
|
}
|
|
165
121
|
|
|
166
|
-
function
|
|
167
|
-
const
|
|
168
|
-
|
|
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
|
|
172
|
-
|
|
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
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
209
|
-
awaitingOfficial: officialRows.length,
|
|
154
|
+
pending,
|
|
210
155
|
current: receivedThrough !== null
|
|
211
156
|
&& appliedThrough === receivedThrough
|
|
212
|
-
&&
|
|
213
|
-
&& officialRows.length === 0,
|
|
157
|
+
&& pending === 0,
|
|
214
158
|
};
|
|
215
159
|
}
|
|
216
160
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
290
|
-
return
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
328
|
-
&& checkpoint
|
|
294
|
+
onRail
|
|
295
|
+
&& checkpoint
|
|
329
296
|
&& checkpoint.baseOid === card.head?.oid
|
|
330
297
|
&& checkpoint.branch === card.head?.branch
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
346
|
+
through: reusableThrough ? shared.through : baseline.appliedThrough,
|
|
347
|
+
value,
|
|
348
|
+
};
|
|
343
349
|
}
|
|
344
350
|
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
381
|
-
return
|
|
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
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
431
|
-
mayMaterialize: true,
|
|
646
|
+
reason: 'project-not-current',
|
|
432
647
|
repoKey: project.repoKey,
|
|
648
|
+
...status,
|
|
433
649
|
};
|
|
434
650
|
}
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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.
|
|
480
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
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.
|
|
573
|
-
&& !
|
|
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
|
-
|
|
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
|
-
||
|
|
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
|
-
|
|
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
|
-
|
|
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,
|