@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.
Files changed (38) hide show
  1. package/apps/studio/api.ts +14 -0
  2. package/apps/studio/client/app.jsx +44 -9
  3. package/apps/studio/client/panels/SyncPanel.jsx +53 -11
  4. package/apps/studio/context.ts +9 -0
  5. package/apps/studio/dist/client.bundle.js +525 -525
  6. package/apps/studio/http.ts +48 -7
  7. package/apps/studio/server.ts +11 -0
  8. package/apps/studio/sync/asset-pull.ts +210 -0
  9. package/apps/studio/sync/asset-push.ts +118 -75
  10. package/apps/studio/sync/connection-state.ts +23 -0
  11. package/apps/studio/sync/discovery.ts +139 -0
  12. package/apps/studio/sync/file-membership.ts +290 -0
  13. package/apps/studio/sync/file-pull.ts +330 -0
  14. package/apps/studio/sync/index.ts +995 -166
  15. package/apps/studio/sync/remote-docs.ts +98 -4
  16. package/apps/studio/sync/status.ts +25 -0
  17. package/apps/studio/sync/tombstone-apply.ts +131 -0
  18. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +90 -0
  19. package/apps/studio/test/exporters/jobs.test.ts +10 -4
  20. package/apps/studio/test/git-cloud-posture.test.ts +11 -2
  21. package/apps/studio/test/sync-asset-pull.test.ts +161 -0
  22. package/apps/studio/test/sync-asset-push.test.ts +144 -3
  23. package/apps/studio/test/sync-attach-incremental.test.ts +527 -0
  24. package/apps/studio/test/sync-file-membership.test.ts +331 -0
  25. package/apps/studio/test/sync-file-pull.test.ts +333 -0
  26. package/apps/studio/test/sync-fresh-link-parity.test.ts +267 -0
  27. package/apps/studio/test/sync-panel-surface.test.ts +10 -0
  28. package/apps/studio/test/sync-remote-docs.test.ts +118 -8
  29. package/apps/studio/test/sync-resync-routes.test.ts +43 -0
  30. package/apps/studio/test/sync-status.test.ts +18 -0
  31. package/apps/studio/test/sync-supervisor.test.ts +4 -0
  32. package/apps/studio/test/sync-tombstone-apply.test.ts +111 -0
  33. package/apps/studio/test/sync-two-peer-discovery.test.ts +343 -0
  34. package/apps/studio/whats-new.json +18 -0
  35. package/cli/commands/doctor.mjs +141 -6
  36. package/cli/lib/gitignore-drift.mjs +149 -0
  37. package/cli/lib/gitignore-drift.test.mjs +156 -0
  38. package/package.json +8 -8
@@ -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
- {changesCount > 0
4815
- ? `${changesCount} unsaved`
4816
- : unpushed > 0
4817
- ? `${unpushed} to publish`
4818
- : 'all saved'}
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
- const unsavedCount = gitStatus?.files?.length || 0;
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={!!cfg.cloud}
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={!cfg.cloud && !!cloudLinkedHub?.credentialed}
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={gitStatus?.unpushed || 0}
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
- <button
195
- type="button"
196
- className="sp-resync"
197
- data-testid="sync-resync"
198
- onClick={resync}
199
- disabled={resyncing || cooling}
200
- title="Re-check every canvas and asset against the workspace"
201
- >
202
- {resyncing ? 'Resyncing…' : 'Resync'}
203
- </button>
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
  );
@@ -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 {