@1agh/maude 0.60.3 → 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 +43 -9
- package/apps/studio/client/panels/SyncPanel.jsx +27 -0
- package/apps/studio/context.ts +9 -0
- package/apps/studio/dist/client.bundle.js +707 -707
- package/apps/studio/http.ts +12 -1
- package/apps/studio/sync/asset-pull.ts +210 -0
- package/apps/studio/sync/asset-push.ts +111 -98
- package/apps/studio/sync/file-membership.ts +290 -0
- package/apps/studio/sync/file-pull.ts +330 -0
- package/apps/studio/sync/index.ts +198 -10
- 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 +52 -2
- 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-remote-docs.test.ts +118 -8
- package/apps/studio/test/sync-status.test.ts +18 -0
- package/apps/studio/test/sync-tombstone-apply.test.ts +111 -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}
|
|
@@ -15521,7 +15551,11 @@ function App() {
|
|
|
15521
15551
|
// holds (opening Sync closes whatever else the right slot shows).
|
|
15522
15552
|
onOpenSync={syncStatus ? () => toggleRightPanel('sync') : undefined}
|
|
15523
15553
|
changesCount={unsavedCount}
|
|
15524
|
-
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}
|
|
15525
15559
|
changesOpen={changesOpen}
|
|
15526
15560
|
onOpenChanges={gitStatus?.repo ? () => setChangesOpen(true) : undefined}
|
|
15527
15561
|
version={cfg?.version}
|
|
@@ -89,6 +89,15 @@ 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)
|
|
@@ -150,6 +159,7 @@ export default function SyncPanel({
|
|
|
150
159
|
const truncated = isCount(status?.itemsTruncated) ? status.itemsTruncated : 0;
|
|
151
160
|
const assets = readAssets(status?.assets);
|
|
152
161
|
const assetFailures = assets?.failures || [];
|
|
162
|
+
const files = readFiles(status?.files);
|
|
153
163
|
|
|
154
164
|
const { attention, byGroup } = useMemo(() => {
|
|
155
165
|
const attention = items.filter((i) => i.state === 'auth-rejected');
|
|
@@ -329,6 +339,23 @@ export default function SyncPanel({
|
|
|
329
339
|
)}
|
|
330
340
|
</section>
|
|
331
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
|
+
)}
|
|
332
359
|
</div>
|
|
333
360
|
</aside>
|
|
334
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 {
|