@1agh/maude 0.60.6 → 1.0.2
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/apps/studio/acp/index.ts +1 -0
- package/apps/studio/ai-banner.tsx +1 -0
- package/apps/studio/annotations-context-toolbar.tsx +3 -1
- package/apps/studio/annotations-layer.tsx +33 -16
- package/apps/studio/api.ts +108 -18
- package/apps/studio/artboard-guides-overlay.tsx +5 -1
- package/apps/studio/assets-s3.ts +6 -1
- package/apps/studio/bin/_import-figma.mjs +8 -3
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-artifacts.ts +21 -0
- package/apps/studio/canvas-build.ts +14 -9
- package/apps/studio/canvas-comment-mount.tsx +27 -30
- package/apps/studio/canvas-icons.tsx +1 -1
- package/apps/studio/canvas-lib.tsx +94 -5
- package/apps/studio/canvas-list-watch.ts +14 -1
- package/apps/studio/canvas-shell.tsx +8 -0
- package/apps/studio/client/app.jsx +224 -45
- package/apps/studio/client/panels/GitPanel.jsx +121 -13
- package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
- package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
- package/apps/studio/client/panels/SyncPanel.jsx +595 -1
- package/apps/studio/client/styles/3-shell-maude.css +38 -0
- package/apps/studio/clip-ops.ts +8 -1
- package/apps/studio/cloud/endpoints.ts +265 -3
- package/apps/studio/collab/origins.ts +3 -1
- package/apps/studio/comments-overlay.tsx +5 -0
- package/apps/studio/config.schema.json +24 -0
- package/apps/studio/context-menu.tsx +40 -27
- package/apps/studio/context.ts +57 -8
- package/apps/studio/cursors-overlay.tsx +25 -13
- package/apps/studio/dist/client.bundle.js +686 -686
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/jobs.ts +77 -15
- package/apps/studio/exporters/remote.ts +190 -0
- package/apps/studio/exporters/video-encode-lib.ts +10 -4
- package/apps/studio/figma/to-strokes.ts +11 -9
- package/apps/studio/gifenc.d.ts +51 -0
- package/apps/studio/git/log-format.ts +88 -0
- package/apps/studio/git/safe-rel.ts +96 -0
- package/apps/studio/git/service.ts +46 -27
- package/apps/studio/hmr-broadcast.ts +10 -0
- package/apps/studio/http.ts +384 -6
- package/apps/studio/participants-chrome.tsx +1 -0
- package/apps/studio/photo-store.ts +7 -0
- package/apps/studio/react-augment.d.ts +17 -0
- package/apps/studio/runtime-bundle.ts +6 -1
- package/apps/studio/server.ts +43 -8
- package/apps/studio/sync/agent.ts +70 -82
- package/apps/studio/sync/asset-push.ts +28 -6
- package/apps/studio/sync/autocommit.ts +106 -5
- package/apps/studio/sync/cell-file-events.ts +117 -0
- package/apps/studio/sync/cell-pairing.ts +20 -5
- package/apps/studio/sync/cell-write-nudge.ts +244 -0
- package/apps/studio/sync/codec.ts +155 -3
- package/apps/studio/sync/cold-start-apply.ts +211 -0
- package/apps/studio/sync/ctl-heal.ts +253 -0
- package/apps/studio/sync/ctl-provider.ts +217 -0
- package/apps/studio/sync/decide-file.ts +335 -0
- package/apps/studio/sync/file-ledger.ts +581 -0
- package/apps/studio/sync/file-membership.ts +32 -0
- package/apps/studio/sync/file-plane.ts +1400 -0
- package/apps/studio/sync/file-pull.ts +41 -4
- package/apps/studio/sync/hub-link.ts +16 -1
- package/apps/studio/sync/hub-listing.ts +46 -0
- package/apps/studio/sync/hubs-config.ts +16 -0
- package/apps/studio/sync/index.ts +919 -214
- package/apps/studio/sync/journal-client.ts +200 -0
- package/apps/studio/sync/migrate-seed.ts +99 -67
- package/apps/studio/sync/poke.ts +50 -0
- package/apps/studio/sync/projection.ts +13 -0
- package/apps/studio/sync/pull-budget.ts +86 -0
- package/apps/studio/sync/settings.ts +110 -0
- package/apps/studio/sync/status.ts +68 -0
- package/apps/studio/sync/trash.ts +243 -0
- package/apps/studio/sync/untrusted.ts +30 -10
- package/apps/studio/test/_helpers.ts +8 -0
- package/apps/studio/test/canvas-build.test.ts +63 -0
- package/apps/studio/test/canvas-list-watch.test.ts +17 -0
- package/apps/studio/test/canvas-move-api.test.ts +31 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
- package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
- package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
- package/apps/studio/test/cloud-history-posture.test.ts +230 -0
- package/apps/studio/test/cloud-session-role.test.ts +30 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
- package/apps/studio/test/cold-start-apply.test.ts +303 -0
- package/apps/studio/test/collab-stress.test.ts +9 -1
- package/apps/studio/test/export-lane.test.ts +245 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
- package/apps/studio/test/git-log-format.test.ts +95 -0
- package/apps/studio/test/git-safe-rel.test.ts +132 -0
- package/apps/studio/test/hmr-broadcast.test.ts +26 -0
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
- package/apps/studio/test/sync-agent.test.ts +78 -0
- package/apps/studio/test/sync-asset-push.test.ts +90 -16
- package/apps/studio/test/sync-autocommit.test.ts +80 -0
- package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
- package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
- package/apps/studio/test/sync-decide-file.test.ts +420 -0
- package/apps/studio/test/sync-file-ledger.test.ts +334 -0
- package/apps/studio/test/sync-file-membership.test.ts +17 -1
- package/apps/studio/test/sync-file-plane.test.ts +976 -0
- package/apps/studio/test/sync-hub-listing.test.ts +46 -0
- package/apps/studio/test/sync-meta-codec.test.ts +76 -0
- package/apps/studio/test/sync-move-retirement.test.ts +231 -0
- package/apps/studio/test/sync-panel-surface.test.ts +20 -0
- package/apps/studio/test/sync-path-pull.test.ts +67 -1
- package/apps/studio/test/sync-pull-budget.test.ts +169 -0
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
- package/apps/studio/test/sync-settings-routes.test.ts +195 -0
- package/apps/studio/test/sync-settings.test.ts +151 -0
- package/apps/studio/test/sync-status.test.ts +69 -0
- package/apps/studio/test/sync-trash.test.ts +132 -0
- package/apps/studio/test/workspace-containment.test.ts +45 -9
- package/apps/studio/tsconfig.json +9 -10
- package/apps/studio/use-annotation-resize.tsx +14 -3
- package/apps/studio/use-collab.tsx +3 -1
- package/apps/studio/whats-new.json +99 -0
- package/apps/studio/workspace-mode.ts +110 -62
- package/apps/studio/ws.ts +22 -1
- package/cli/bin/claude-design-server.mjs +19 -0
- package/cli/commands/design.mjs +25 -6
- package/cli/commands/hub-workspace.mjs +243 -22
- package/cli/commands/hub-workspace.test.mjs +171 -0
- package/cli/commands/hub.mjs +71 -1
- package/cli/lib/design-link.mjs +186 -1
- package/cli/lib/design-ownership.mjs +330 -0
- package/cli/lib/design-ownership.test.mjs +329 -0
- package/cli/lib/hubs-config.mjs +21 -0
- package/cli/lib/hubs-config.test.mjs +47 -1
- package/cli/lib/workspace-plan.mjs +298 -5
- package/cli/lib/workspace-plan.test.mjs +215 -1
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +43 -2
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/apps/studio/server.mjs +0 -1312
- package/apps/studio/sync/asset-pull.ts +0 -210
- package/apps/studio/sync/asset-push-worker.ts +0 -84
- package/apps/studio/sync/asset-sweep.ts +0 -262
- package/apps/studio/test/sync-asset-pull.test.ts +0 -161
- package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
- package/apps/studio/test/sync-asset-sweep.test.ts +0 -243
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// The ONE file decision — Sync v2 Increment 3 (DDR-226 §4).
|
|
2
|
+
//
|
|
3
|
+
// Every non-CRDT file in a linked project resolves through this function, and
|
|
4
|
+
// through nothing else. It is pure, total, and Y-free, in the shape
|
|
5
|
+
// `cold-start.ts` established: the table is the specification, and the caller
|
|
6
|
+
// executes what it returns.
|
|
7
|
+
//
|
|
8
|
+
// ── Why three inputs, and why the third one matters most ────────────────────
|
|
9
|
+
//
|
|
10
|
+
// Two-way sync (local vs remote) cannot tell "I changed it" from "they changed
|
|
11
|
+
// it" — both look like "the two sides differ", and every answer to that is a
|
|
12
|
+
// coin flip that sometimes erases a person's work. The third input is the
|
|
13
|
+
// ANCESTOR: the hash this machine last reconciled through. With it, the same
|
|
14
|
+
// difference becomes a fact rather than a guess —
|
|
15
|
+
//
|
|
16
|
+
// ancestor == local → they moved, I did not → PULL
|
|
17
|
+
// ancestor == remote → I moved, they did not → PUSH
|
|
18
|
+
// ancestor == neither → we both moved → CONFLICT
|
|
19
|
+
//
|
|
20
|
+
// That is the entire idea, and it is the same one Dropbox, Syncthing and
|
|
21
|
+
// CouchDB converged on. Everything below is the edges.
|
|
22
|
+
//
|
|
23
|
+
// ── The rules that are not negotiable ───────────────────────────────────────
|
|
24
|
+
//
|
|
25
|
+
// **Bytes are never merged.** A conflict parks the local copy aside under a
|
|
26
|
+
// name both ends can see and then adopts the remote — never a silent
|
|
27
|
+
// overwrite, never a three-way text merge of a PNG or a stylesheet.
|
|
28
|
+
//
|
|
29
|
+
// **Absence is never authority** (DDR-076, generalized). A file missing from
|
|
30
|
+
// the hub does not mean "delete yours"; it means the hub does not have it. Only
|
|
31
|
+
// an explicit tombstone row can propagate a deletion, and even then only when
|
|
32
|
+
// the remote is still at the ancestor — an edit beats a delete (the Syncthing
|
|
33
|
+
// rule).
|
|
34
|
+
//
|
|
35
|
+
// **Unstamped emptiness never beats content** (DDR-223, generalized). Emptiness
|
|
36
|
+
// is just a hash here; it wins only where any other hash would, through
|
|
37
|
+
// ancestor equality. There is no branch that treats "nothing" as newer.
|
|
38
|
+
//
|
|
39
|
+
// **A degraded epoch downgrades every overwrite.** When the hub's epoch no
|
|
40
|
+
// longer matches what our ancestors were anchored against, those ancestors
|
|
41
|
+
// describe a log that no longer exists — so they stop being overwrite
|
|
42
|
+
// authority. Every PULL that would land on top of a local change becomes
|
|
43
|
+
// keep-local + park-the-remote-copy + push-local-up.
|
|
44
|
+
|
|
45
|
+
/** What the caller must do with this path. */
|
|
46
|
+
export type FileAction =
|
|
47
|
+
/** Nothing to move. Adopt the ancestor so the next pass is cheap. */
|
|
48
|
+
| 'noop'
|
|
49
|
+
/** Fetch the remote bytes and materialize them. */
|
|
50
|
+
| 'pull'
|
|
51
|
+
/** Upload the local bytes (CAS'd against the head we decided from). */
|
|
52
|
+
| 'push'
|
|
53
|
+
/** Park local aside, then adopt remote. Both ends end up seeing both. */
|
|
54
|
+
| 'conflict-aside'
|
|
55
|
+
/** A tombstone we agree with: quarantine the local copy to `_trash/`. */
|
|
56
|
+
| 'quarantine'
|
|
57
|
+
/** Local is gone and the hub still has what we last saw: state the delete. */
|
|
58
|
+
| 'propagate-delete'
|
|
59
|
+
/** A tombstone we disagree with: keep local and push it back up. */
|
|
60
|
+
| 'revive';
|
|
61
|
+
|
|
62
|
+
export interface FileState {
|
|
63
|
+
/** designRoot-relative path, for the reason string only. */
|
|
64
|
+
path: string;
|
|
65
|
+
/** sha256 of the local file, or null when it is not on disk. */
|
|
66
|
+
local: string | null;
|
|
67
|
+
/** sha256 the hub's latest row carries, or null when it has no row. */
|
|
68
|
+
remote: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* The ancestor: the hash this machine last reconciled through for this path.
|
|
71
|
+
* Null means "never reconciled here" — a first anchor, not an empty file.
|
|
72
|
+
*/
|
|
73
|
+
ancestor: string | null;
|
|
74
|
+
/** The hub's latest row for this path is a tombstone. */
|
|
75
|
+
remoteTombstone?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The hub's epoch differs from the one our ancestors were anchored against,
|
|
78
|
+
* and the journal could not reconstruct the gap. Ancestors stop being
|
|
79
|
+
* overwrite authority — see the header.
|
|
80
|
+
*/
|
|
81
|
+
epochChanged?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* The remote hash equals something this machine has in flight. The "remote
|
|
84
|
+
* change" is our own write coming back to us; adopt it and move on rather
|
|
85
|
+
* than manufacturing a conflict with ourselves.
|
|
86
|
+
*/
|
|
87
|
+
selfInFlight?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Deletion EMISSION is off until Increment 6. When false, a local absence
|
|
90
|
+
* that would propagate a delete is held instead — the row exists in the
|
|
91
|
+
* table from day one so the behaviour is a flag, not a missing branch.
|
|
92
|
+
*/
|
|
93
|
+
propagateDeletes?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FileDecision {
|
|
97
|
+
action: FileAction;
|
|
98
|
+
reason: string;
|
|
99
|
+
/**
|
|
100
|
+
* Park the LOCAL copy aside before anything else happens. Set on
|
|
101
|
+
* `conflict-aside`. If the park fails the caller must refuse the overwrite —
|
|
102
|
+
* DDR-102's fail-closed rule, applied to files.
|
|
103
|
+
*/
|
|
104
|
+
parkLocal?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Park the REMOTE copy aside instead of landing it. Set on the
|
|
107
|
+
* epoch-degraded rows, where the remote is worth keeping a copy of but is
|
|
108
|
+
* not allowed to overwrite anything.
|
|
109
|
+
*/
|
|
110
|
+
parkRemote?: boolean;
|
|
111
|
+
/** The decision moves no bytes; just record the ancestor. */
|
|
112
|
+
adoptAncestor?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The shapes the (local, remote, ancestor, tombstone) space collapses into.
|
|
117
|
+
*
|
|
118
|
+
* Naming the shape BEFORE deciding is what makes the switch below exhaustive
|
|
119
|
+
* with a compile-time `never`: the classification is total by construction, and
|
|
120
|
+
* a new shape cannot be added without the compiler demanding a row for it.
|
|
121
|
+
*/
|
|
122
|
+
type Shape =
|
|
123
|
+
| 'both-absent'
|
|
124
|
+
| 'equal'
|
|
125
|
+
| 'self-echo'
|
|
126
|
+
| 'local-anchored'
|
|
127
|
+
| 'remote-anchored'
|
|
128
|
+
| 'create-down'
|
|
129
|
+
| 'create-up'
|
|
130
|
+
| 'remote-regressed'
|
|
131
|
+
| 'diverged'
|
|
132
|
+
| 'local-deleted-clean'
|
|
133
|
+
| 'local-deleted-but-remote-moved'
|
|
134
|
+
| 'tombstone-agreed'
|
|
135
|
+
| 'tombstone-contested';
|
|
136
|
+
|
|
137
|
+
function classify(s: FileState): Shape {
|
|
138
|
+
const { local, remote, ancestor } = s;
|
|
139
|
+
|
|
140
|
+
// A tombstone is a statement about the path, so it outranks hash comparison.
|
|
141
|
+
if (s.remoteTombstone) {
|
|
142
|
+
if (local === null) return 'tombstone-agreed';
|
|
143
|
+
// We still hold what the tombstone describes ⇒ agree with it.
|
|
144
|
+
if (ancestor !== null && local === ancestor) return 'tombstone-agreed';
|
|
145
|
+
// We hold something the deleter never saw ⇒ an edit beats their delete.
|
|
146
|
+
return 'tombstone-contested';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (local === null && remote === null) return 'both-absent';
|
|
150
|
+
|
|
151
|
+
// Our own write echoing back off the hub. Checked before the difference
|
|
152
|
+
// rows, because to every one of them this looks like a remote change.
|
|
153
|
+
if (s.selfInFlight === true && remote !== null && remote !== local) return 'self-echo';
|
|
154
|
+
|
|
155
|
+
if (local !== null && local === remote) return 'equal';
|
|
156
|
+
|
|
157
|
+
if (local === null) {
|
|
158
|
+
if (ancestor === null) return 'create-down'; // remote !== null here
|
|
159
|
+
// We had it, we reconciled it, and now it is gone from disk.
|
|
160
|
+
return remote === ancestor ? 'local-deleted-clean' : 'local-deleted-but-remote-moved';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (remote === null) {
|
|
164
|
+
if (ancestor === null) return 'create-up';
|
|
165
|
+
// The hub had it and no longer does, with no tombstone to say why.
|
|
166
|
+
// Absence is not authority (DDR-076): put it back.
|
|
167
|
+
return 'remote-regressed';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Both present and different.
|
|
171
|
+
if (ancestor !== null && ancestor === local) return 'local-anchored';
|
|
172
|
+
if (ancestor !== null && ancestor === remote) return 'remote-anchored';
|
|
173
|
+
return 'diverged';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decide what happens to one file. Pure and total.
|
|
178
|
+
*/
|
|
179
|
+
export function decideFile(state: FileState): FileDecision {
|
|
180
|
+
const shape = classify(state);
|
|
181
|
+
const degraded = state.epochChanged === true;
|
|
182
|
+
|
|
183
|
+
switch (shape) {
|
|
184
|
+
case 'both-absent':
|
|
185
|
+
return { action: 'noop', reason: 'neither side has this path', adoptAncestor: true };
|
|
186
|
+
|
|
187
|
+
case 'equal':
|
|
188
|
+
return { action: 'noop', reason: 'local and hub already agree', adoptAncestor: true };
|
|
189
|
+
|
|
190
|
+
case 'self-echo':
|
|
191
|
+
// The hub is telling us about our own in-flight write. Adopting rather
|
|
192
|
+
// than conflicting is what stops a push from fighting its own echo —
|
|
193
|
+
// the reason the old fast lane needed a probe-guard at all.
|
|
194
|
+
return {
|
|
195
|
+
action: 'noop',
|
|
196
|
+
reason: 'the hub row is this machine’s own in-flight write coming back',
|
|
197
|
+
adoptAncestor: true,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
case 'local-anchored':
|
|
201
|
+
// They moved, we did not — the clean download.
|
|
202
|
+
return degraded
|
|
203
|
+
? {
|
|
204
|
+
action: 'noop',
|
|
205
|
+
reason:
|
|
206
|
+
'the hub changed, but the epoch no longer matches our anchor — keeping local and parking their copy rather than overwriting on a stale ancestor',
|
|
207
|
+
parkRemote: true,
|
|
208
|
+
}
|
|
209
|
+
: { action: 'pull', reason: 'the hub moved and this machine did not' };
|
|
210
|
+
|
|
211
|
+
case 'remote-anchored':
|
|
212
|
+
// We moved, they did not — the clean upload.
|
|
213
|
+
return { action: 'push', reason: 'this machine moved and the hub did not' };
|
|
214
|
+
|
|
215
|
+
case 'create-down':
|
|
216
|
+
// Nothing local to lose. Never degraded: there is no local change for a
|
|
217
|
+
// stale ancestor to endanger.
|
|
218
|
+
return { action: 'pull', reason: 'the hub offers a file this machine has never had' };
|
|
219
|
+
|
|
220
|
+
case 'create-up':
|
|
221
|
+
return { action: 'push', reason: 'this machine has a file the hub has never had' };
|
|
222
|
+
|
|
223
|
+
case 'remote-regressed':
|
|
224
|
+
// DDR-076 generalized. A hub that lost a file gets it back; it does not
|
|
225
|
+
// get to take ours.
|
|
226
|
+
return {
|
|
227
|
+
action: 'push',
|
|
228
|
+
reason:
|
|
229
|
+
'the hub no longer has a file it once did, and said no tombstone — absence is not authority',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
case 'diverged':
|
|
233
|
+
// Both moved. Never a merge, never a silent winner: park ours under a
|
|
234
|
+
// name both ends can see, adopt theirs, and let the copy travel too.
|
|
235
|
+
return degraded
|
|
236
|
+
? {
|
|
237
|
+
action: 'noop',
|
|
238
|
+
reason:
|
|
239
|
+
'both sides moved and the epoch is degraded — keeping local, parking the hub copy, and pushing local up',
|
|
240
|
+
parkRemote: true,
|
|
241
|
+
}
|
|
242
|
+
: {
|
|
243
|
+
action: 'conflict-aside',
|
|
244
|
+
reason: 'both sides changed this file since they last agreed',
|
|
245
|
+
parkLocal: true,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
case 'local-deleted-clean':
|
|
249
|
+
// The Syncthing rule. The deletion is real, but EMITTING it is Increment
|
|
250
|
+
// 6's job — until then we hold rather than pretend the file is back.
|
|
251
|
+
return state.propagateDeletes === true
|
|
252
|
+
? {
|
|
253
|
+
action: 'propagate-delete',
|
|
254
|
+
reason: 'deleted here, and the hub still has what we last saw',
|
|
255
|
+
}
|
|
256
|
+
: {
|
|
257
|
+
action: 'noop',
|
|
258
|
+
reason:
|
|
259
|
+
'deleted here, but delete propagation is off — holding rather than resurrecting',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
case 'local-deleted-but-remote-moved':
|
|
263
|
+
// An edit beats a delete: somebody changed the file after we last saw
|
|
264
|
+
// it, so bring it back rather than deleting their work.
|
|
265
|
+
return {
|
|
266
|
+
action: 'pull',
|
|
267
|
+
reason: 'deleted here, but the hub has newer content — an edit beats a delete',
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
case 'tombstone-agreed':
|
|
271
|
+
if (state.local === null) {
|
|
272
|
+
return {
|
|
273
|
+
action: 'noop',
|
|
274
|
+
reason: 'the hub tombstoned a file this machine does not have',
|
|
275
|
+
adoptAncestor: true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// B6 (post-1.0 burn-down) — a degraded epoch damps the deletion row
|
|
279
|
+
// like every overwrite row above it. The "agreement" here is
|
|
280
|
+
// `local === ancestor`, and the degrade just disqualified exactly those
|
|
281
|
+
// ancestors — honouring a tombstone on their strength would let a hub
|
|
282
|
+
// whose log rewound (or a hostile one that answered `reanchor`) delete
|
|
283
|
+
// files using authority it no longer has. Hold; a later pass on a
|
|
284
|
+
// matching epoch re-decides with real ancestors.
|
|
285
|
+
if (degraded) {
|
|
286
|
+
return {
|
|
287
|
+
action: 'noop',
|
|
288
|
+
reason:
|
|
289
|
+
'the hub tombstoned this file, but the epoch is degraded — the ancestors that would justify agreeing were just disqualified; keeping local until a clean pass',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Quarantine, never unlink — the recoverability spine.
|
|
293
|
+
return {
|
|
294
|
+
action: 'quarantine',
|
|
295
|
+
reason: 'the hub deleted a file this machine still holds unchanged',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
case 'tombstone-contested':
|
|
299
|
+
return {
|
|
300
|
+
action: 'revive',
|
|
301
|
+
reason:
|
|
302
|
+
'the hub deleted a file this machine has changed since — keeping local and pushing it back',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
default: {
|
|
306
|
+
// Adding a shape without a row is a compile error, never a silent
|
|
307
|
+
// fallthrough. (This is the mistake `migrate-seed` made with
|
|
308
|
+
// `recover-seed-dup`, and it cost a duplicated canvas body.)
|
|
309
|
+
const never: never = shape;
|
|
310
|
+
throw new Error(`decideFile: unhandled shape ${String(never)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The conflict-copy name. NEVER `*.sync-conflict-*` — see the note below. */
|
|
316
|
+
export function conflictCopyName(rel: string, ts: number, label: string): string {
|
|
317
|
+
// `~/git` runs real Syncthing, whose own conflict artifacts are
|
|
318
|
+
// `*.sync-conflict-*`. Emitting the identical pattern would make a conflict
|
|
319
|
+
// unattributable — you could not tell which program parked the file, and the
|
|
320
|
+
// classifier would happily sync Syncthing's copies as if they were ours.
|
|
321
|
+
const safeLabel = label.replace(/[^A-Za-z0-9-]/g, '').slice(0, 32) || 'peer';
|
|
322
|
+
const stamp = new Date(ts).toISOString().replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
323
|
+
const slash = rel.lastIndexOf('/');
|
|
324
|
+
const dir = slash === -1 ? '' : rel.slice(0, slash + 1);
|
|
325
|
+
const base = slash === -1 ? rel : rel.slice(slash + 1);
|
|
326
|
+
const dot = base.indexOf('.');
|
|
327
|
+
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
328
|
+
const ext = dot === -1 ? '' : base.slice(dot);
|
|
329
|
+
return `${dir}${stem}.maude-conflict-${stamp}-${safeLabel}${ext}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Is this path one of OUR conflict copies? */
|
|
333
|
+
export function isMaudeConflictCopy(rel: string): boolean {
|
|
334
|
+
return /\.maude-conflict-/.test(rel);
|
|
335
|
+
}
|