@1agh/maude 0.60.2 → 0.60.4
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/api.ts +14 -0
- package/apps/studio/client/app.jsx +44 -9
- package/apps/studio/client/panels/SyncPanel.jsx +53 -11
- package/apps/studio/context.ts +9 -0
- package/apps/studio/dist/client.bundle.js +525 -525
- package/apps/studio/http.ts +48 -7
- package/apps/studio/server.ts +11 -0
- package/apps/studio/sync/asset-pull.ts +210 -0
- package/apps/studio/sync/asset-push.ts +118 -75
- package/apps/studio/sync/connection-state.ts +23 -0
- package/apps/studio/sync/discovery.ts +139 -0
- package/apps/studio/sync/file-membership.ts +290 -0
- package/apps/studio/sync/file-pull.ts +330 -0
- package/apps/studio/sync/index.ts +995 -166
- package/apps/studio/sync/remote-docs.ts +98 -4
- package/apps/studio/sync/status.ts +25 -0
- package/apps/studio/sync/tombstone-apply.ts +131 -0
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +90 -0
- package/apps/studio/test/exporters/jobs.test.ts +10 -4
- package/apps/studio/test/git-cloud-posture.test.ts +11 -2
- package/apps/studio/test/sync-asset-pull.test.ts +161 -0
- package/apps/studio/test/sync-asset-push.test.ts +144 -3
- package/apps/studio/test/sync-attach-incremental.test.ts +527 -0
- package/apps/studio/test/sync-file-membership.test.ts +331 -0
- package/apps/studio/test/sync-file-pull.test.ts +333 -0
- package/apps/studio/test/sync-fresh-link-parity.test.ts +267 -0
- package/apps/studio/test/sync-panel-surface.test.ts +10 -0
- package/apps/studio/test/sync-remote-docs.test.ts +118 -8
- package/apps/studio/test/sync-resync-routes.test.ts +43 -0
- package/apps/studio/test/sync-status.test.ts +18 -0
- package/apps/studio/test/sync-supervisor.test.ts +4 -0
- package/apps/studio/test/sync-tombstone-apply.test.ts +111 -0
- package/apps/studio/test/sync-two-peer-discovery.test.ts +343 -0
- package/apps/studio/whats-new.json +18 -0
- package/cli/commands/doctor.mjs +141 -6
- package/cli/lib/gitignore-drift.mjs +149 -0
- package/cli/lib/gitignore-drift.test.mjs +156 -0
- package/package.json +8 -8
package/apps/studio/api.ts
CHANGED
|
@@ -2577,6 +2577,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2577
2577
|
// via git "Get latest" — the file travels through git, this event is only a
|
|
2578
2578
|
// "refresh your list" nudge for online local tabs (loopback inspector WS).
|
|
2579
2579
|
ctx.bus.emit('canvas-list-update', { action: 'added', rel, slug });
|
|
2580
|
+
// The inverse of `canvas-deleted` (see deleteCanvas for why this is its own
|
|
2581
|
+
// event). Creating a name the project buried earlier has to lift the
|
|
2582
|
+
// tombstone, or every peer would dutifully trash the new canvas for as long
|
|
2583
|
+
// as the gravestone lives.
|
|
2584
|
+
ctx.bus.emit('canvas-created', { slug });
|
|
2580
2585
|
// designRel-prefixed path — matches the file-tree `file.path` shape so the
|
|
2581
2586
|
// client can open it directly after reloadTree().
|
|
2582
2587
|
return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
|
|
@@ -2690,6 +2695,15 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2690
2695
|
|
|
2691
2696
|
// Phase 30 — live tree refresh for other local tabs (see createCanvas).
|
|
2692
2697
|
ctx.bus.emit('canvas-list-update', { action: 'removed', rel, slug });
|
|
2698
|
+
// A SEPARATE EVENT, DELIBERATELY. `canvas-list-update` is emitted by this
|
|
2699
|
+
// API *and* by the filesystem watcher, so its payload can describe a file
|
|
2700
|
+
// an agent or a `git checkout` produced — which is why discovery treats it
|
|
2701
|
+
// as a nudge and re-derives the truth itself. This one is only ever emitted
|
|
2702
|
+
// HERE, from a privileged route the canvas origin cannot reach, so it
|
|
2703
|
+
// carries something the watcher's version cannot: the user meant it. Sync
|
|
2704
|
+
// needs exactly that, because "gone from this disk" and "deleted from the
|
|
2705
|
+
// project" are different claims and only the second may travel.
|
|
2706
|
+
ctx.bus.emit('canvas-deleted', { slug });
|
|
2693
2707
|
return {
|
|
2694
2708
|
ok: true,
|
|
2695
2709
|
rel,
|
|
@@ -4704,6 +4704,11 @@ function StatusBar({
|
|
|
4704
4704
|
onOpenSync,
|
|
4705
4705
|
changesCount = 0,
|
|
4706
4706
|
unpushed = 0,
|
|
4707
|
+
// Somebody else commits this project (a cloud cell, or the cell behind a
|
|
4708
|
+
// linked+credentialed desktop repo — DDR-218). The chip then names the
|
|
4709
|
+
// mechanism that IS saving rather than reporting a working-tree count the
|
|
4710
|
+
// user has no reason to act on. Default false = today's local-first chip.
|
|
4711
|
+
savingIsManaged = false,
|
|
4707
4712
|
changesOpen = false,
|
|
4708
4713
|
onOpenChanges,
|
|
4709
4714
|
version,
|
|
@@ -4811,11 +4816,17 @@ function StatusBar({
|
|
|
4811
4816
|
<span className="st-sb-changes-dot" aria-hidden="true" />
|
|
4812
4817
|
<span className="lbl">changes</span>
|
|
4813
4818
|
<span className="val">
|
|
4814
|
-
{
|
|
4815
|
-
?
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
+
{savingIsManaged
|
|
4820
|
+
? // Not "all saved": assets and project config are written into the
|
|
4821
|
+
// cell's checkout but never committed there, so asserting a clean
|
|
4822
|
+
// save would overstate what the cloud actually holds. Naming the
|
|
4823
|
+
// mechanism is both honest and the thing the user needs to know.
|
|
4824
|
+
'cloud saving'
|
|
4825
|
+
: changesCount > 0
|
|
4826
|
+
? `${changesCount} unsaved`
|
|
4827
|
+
: unpushed > 0
|
|
4828
|
+
? `${unpushed} to publish`
|
|
4829
|
+
: 'all saved'}
|
|
4819
4830
|
</span>
|
|
4820
4831
|
</button>
|
|
4821
4832
|
)}
|
|
@@ -11383,7 +11394,26 @@ function App() {
|
|
|
11383
11394
|
for (const f of gitStatus?.files || []) m.set(f.path, KIND[f.status]);
|
|
11384
11395
|
return m;
|
|
11385
11396
|
}, [gitStatus]);
|
|
11386
|
-
|
|
11397
|
+
// WHO IS SAVING THIS PROJECT — one expression, read by every surface that
|
|
11398
|
+
// would otherwise offer to save it (DDR-218 + the cell's `historyOnly`).
|
|
11399
|
+
//
|
|
11400
|
+
// These two flags were previously derived at the GitPanel call site alone, so
|
|
11401
|
+
// the panel withdrew correctly while the toolbar menu and the status-bar chip
|
|
11402
|
+
// kept rendering a raw dirty count from `gitStatus.files.length` — the very
|
|
11403
|
+
// "lie about work that is already saved" the withdrawal exists to delete. The
|
|
11404
|
+
// count is the claim, wherever it is drawn; hoisting the rule here is what
|
|
11405
|
+
// keeps a third surface from re-introducing it.
|
|
11406
|
+
//
|
|
11407
|
+
// PRESENTATION, NOT A CONTROL — unchanged from DDR-218. `.git` is untouched,
|
|
11408
|
+
// a terminal `git` behaves exactly as before, the server routes keep their own
|
|
11409
|
+
// gates, and Disconnect restores every surface live.
|
|
11410
|
+
const cellManaged = !!cfg.cloud;
|
|
11411
|
+
const cloudManaged = !cfg.cloud && !!cloudLinkedHub?.credentialed;
|
|
11412
|
+
const savingIsManaged = cellManaged || cloudManaged;
|
|
11413
|
+
// The raw truth stays available for surfaces that legitimately need it (the
|
|
11414
|
+
// panel's own file list, `dirtyByPath`); only the COUNT — the thing that reads
|
|
11415
|
+
// as a to-do — is withheld when this project's saving is somebody else's job.
|
|
11416
|
+
const unsavedCount = savingIsManaged ? 0 : gitStatus?.files?.length || 0;
|
|
11387
11417
|
|
|
11388
11418
|
// Phase 28 (E3) — keep remote ahead/behind fresh so the "Get latest" nudge
|
|
11389
11419
|
// surfaces on its own: probe once a repo is known, again whenever the Changes
|
|
@@ -14446,7 +14476,7 @@ function App() {
|
|
|
14446
14476
|
// mislead, it does not remove a capability. The real gates are
|
|
14447
14477
|
// server-side (`projectReadOnly`, the manifest's role matrix), and
|
|
14448
14478
|
// they are unchanged by this flag.
|
|
14449
|
-
historyOnly={
|
|
14479
|
+
historyOnly={cellManaged}
|
|
14450
14480
|
// DDR-218 (fix 8) — the DESKTOP half of the same withdrawal: a repo
|
|
14451
14481
|
// linked+credentialed to Maude Cloud is cloud-managed (the cell
|
|
14452
14482
|
// commits every edit as it lands), so the local Changes surface is
|
|
@@ -14454,7 +14484,7 @@ function App() {
|
|
|
14454
14484
|
// Live: CloudBar lifts every link change (resolve/attach/detach)
|
|
14455
14485
|
// into `cloudLinkedHub`. Same presentation-not-a-control rule as
|
|
14456
14486
|
// `historyOnly` above; Disconnect restores the panel in place.
|
|
14457
|
-
cloudManaged={
|
|
14487
|
+
cloudManaged={cloudManaged}
|
|
14458
14488
|
resizing={resizingFor('changes')}
|
|
14459
14489
|
onClose={() => setChangesOpen(false)}
|
|
14460
14490
|
onCommit={gitCommit}
|
|
@@ -14474,6 +14504,7 @@ function App() {
|
|
|
14474
14504
|
<SyncPanel
|
|
14475
14505
|
status={syncStatus}
|
|
14476
14506
|
project={cfg?.cloud?.projectName || project}
|
|
14507
|
+
cloud={cfg?.cloud ?? null}
|
|
14477
14508
|
groupPaths={(groups || []).map((g) => g.path).filter(Boolean)}
|
|
14478
14509
|
resizing={resizingFor('sync')}
|
|
14479
14510
|
onClose={() => setSyncPanelOpen(false)}
|
|
@@ -15520,7 +15551,11 @@ function App() {
|
|
|
15520
15551
|
// holds (opening Sync closes whatever else the right slot shows).
|
|
15521
15552
|
onOpenSync={syncStatus ? () => toggleRightPanel('sync') : undefined}
|
|
15522
15553
|
changesCount={unsavedCount}
|
|
15523
|
-
unpushed
|
|
15554
|
+
// `unpushed` is a LOCAL-git offer ("N to publish"), and the panel has
|
|
15555
|
+
// withdrawn Publish under either managed posture — so the chip must
|
|
15556
|
+
// not keep advertising it either.
|
|
15557
|
+
unpushed={savingIsManaged ? 0 : gitStatus?.unpushed || 0}
|
|
15558
|
+
savingIsManaged={savingIsManaged}
|
|
15524
15559
|
changesOpen={changesOpen}
|
|
15525
15560
|
onOpenChanges={gitStatus?.repo ? () => setChangesOpen(true) : undefined}
|
|
15526
15561
|
version={cfg?.version}
|
|
@@ -89,10 +89,22 @@ function readAssets(raw) {
|
|
|
89
89
|
return { ...raw, failures };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** The file plane (feature-sync-file-plane), or null when unreadable/absent —
|
|
93
|
+
* absent is the norm: the payload carries `files` only once a flag-on pull
|
|
94
|
+
* has run this boot. */
|
|
95
|
+
function readFiles(raw) {
|
|
96
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
97
|
+
if (![raw.synced, raw.pulled, raw.conflicts].every(isCount)) return null;
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
export default function SyncPanel({
|
|
93
102
|
status, // the live `sync:status` payload (never null while mounted)
|
|
94
103
|
project, // display name for the header sentence (hub-supplied → safeName'd)
|
|
95
104
|
groupPaths = [], // declared canvas-group paths, for row grouping
|
|
105
|
+
// Present exactly when the hub runs this studio (`/_config`'s `cloud` block);
|
|
106
|
+
// null on the desktop. Decides whether Resync is offered at all — see below.
|
|
107
|
+
cloud = null,
|
|
96
108
|
resizing,
|
|
97
109
|
onClose,
|
|
98
110
|
}) {
|
|
@@ -147,6 +159,7 @@ export default function SyncPanel({
|
|
|
147
159
|
const truncated = isCount(status?.itemsTruncated) ? status.itemsTruncated : 0;
|
|
148
160
|
const assets = readAssets(status?.assets);
|
|
149
161
|
const assetFailures = assets?.failures || [];
|
|
162
|
+
const files = readFiles(status?.files);
|
|
150
163
|
|
|
151
164
|
const { attention, byGroup } = useMemo(() => {
|
|
152
165
|
const attention = items.filter((i) => i.state === 'auth-rejected');
|
|
@@ -190,17 +203,29 @@ export default function SyncPanel({
|
|
|
190
203
|
<span className="gp-spacer" />
|
|
191
204
|
{/* Resync re-runs the WHOLE sync — every canvas and every asset — so
|
|
192
205
|
it lives in the header, not inside the assets section. It is
|
|
193
|
-
`syncControl.restart()`, the same cycle Connect performs.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
`syncControl.restart()`, the same cycle Connect performs.
|
|
207
|
+
|
|
208
|
+
NOT IN THE CLOUD. On a cell the sync runtime belongs to the process
|
|
209
|
+
serving the project to EVERYONE, so restarting it is an operator
|
|
210
|
+
action, not a member's — the hub refuses the route there on
|
|
211
|
+
purpose (v0.60.2). Rendering the button anyway meant a cloud member
|
|
212
|
+
could press a control that cannot work by design and be handed an
|
|
213
|
+
error for it; the honest surface is its absence. Discovery is
|
|
214
|
+
continuous now, so nobody needs this button to pick up a new
|
|
215
|
+
canvas — it is a repair tool, and repairing a cell is the hub's
|
|
216
|
+
job. See `.ai/logs/rca/issue-cloud-assets-open-findings.md` §5. */}
|
|
217
|
+
{!cloud && (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className="sp-resync"
|
|
221
|
+
data-testid="sync-resync"
|
|
222
|
+
onClick={resync}
|
|
223
|
+
disabled={resyncing || cooling}
|
|
224
|
+
title="Re-check every canvas and asset against the workspace"
|
|
225
|
+
>
|
|
226
|
+
{resyncing ? 'Resyncing…' : 'Resync'}
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
204
229
|
<button type="button" className="gp-x" aria-label="Close" onClick={onClose}>
|
|
205
230
|
×
|
|
206
231
|
</button>
|
|
@@ -314,6 +339,23 @@ export default function SyncPanel({
|
|
|
314
339
|
)}
|
|
315
340
|
</section>
|
|
316
341
|
)}
|
|
342
|
+
|
|
343
|
+
{files && (
|
|
344
|
+
<section aria-label="Project files" data-testid="sync-files">
|
|
345
|
+
<div className="gp-sect-label">
|
|
346
|
+
project files <span className="gp-group-count">{files.synced}</span>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="sp-assets-line">
|
|
349
|
+
{`${files.synced} synced` + (files.pulled > 0 ? ` · ${files.pulled} pulled` : '')}
|
|
350
|
+
</div>
|
|
351
|
+
{files.conflicts > 0 && (
|
|
352
|
+
<div className="sp-assets-retry" data-testid="sync-files-conflicts">
|
|
353
|
+
{files.conflicts} conflict{files.conflicts === 1 ? '' : 's'} — the older copies are
|
|
354
|
+
kept in _trash/.
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</section>
|
|
358
|
+
)}
|
|
317
359
|
</div>
|
|
318
360
|
</aside>
|
|
319
361
|
);
|
package/apps/studio/context.ts
CHANGED
|
@@ -47,6 +47,15 @@ export interface LinkedHub {
|
|
|
47
47
|
* would split peers of the same project apart. See sync/doc-name.ts.
|
|
48
48
|
*/
|
|
49
49
|
workspaceId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* feature-sync-file-plane — opt-in for the manifest-driven file plane
|
|
52
|
+
* (Plane B): the downward project-file pull AND the widened upward sweep
|
|
53
|
+
* (stylesheets, docs, code modules — classifier membership, see
|
|
54
|
+
* sync/file-membership.ts). Default OFF this release; `MAUDE_SYNC_FILES=1`
|
|
55
|
+
* forces it on for a session. The flag gates ONLY the new plane — the
|
|
56
|
+
* canvas CRDT lanes and the DDR-217 asset lanes run regardless.
|
|
57
|
+
*/
|
|
58
|
+
syncFiles?: boolean;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
export interface DevServerConfig {
|