@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
@@ -0,0 +1,149 @@
1
+ // Is this repo's .gitignore telling git to drop content Maude calls VERSIONED?
2
+ //
3
+ // THE FAILURE THIS DETECTS. DDR-115 splits the design root in two: per-machine
4
+ // runtime state is ignored, and durable project content (`*.tsx`,
5
+ // `*.meta.json`, `*.annotations.svg`, `system/**`, `config.json`) is committed.
6
+ // `gitignore-block.mjs` encodes that — the versioned paths are "deliberately
7
+ // absent" from the block it writes. But that block lives between
8
+ // `# maude:begin` / `# maude:end` markers and `applyBlock` only ever replaces
9
+ // what is BETWEEN them, so a rule sitting OUTSIDE the markers is invisible to
10
+ // it, forever.
11
+ //
12
+ // Observed in a real project (Brno Alligators, 2026-08-13): a hand-authored
13
+ // pre-markers block carried `.design/*.annotations.svg`, copied from a v1.0
14
+ // planning document that predates the DDR-115 correction. Consequence: not one
15
+ // annotation sidecar had ever been committed. The draw layer was live in both
16
+ // UIs and absent from every clone, every backup and every PR — and nothing
17
+ // anywhere said so, because each half was behaving exactly as written. Maude
18
+ // believed the file was versioned; git had been told otherwise years earlier.
19
+ //
20
+ // WHY DETECTION AND NOT SILENT SURGERY. The obvious "fix" is to make the block
21
+ // writer delete offending lines. It must not: those lines are OUTSIDE our
22
+ // markers, which is precisely the region the writer promises never to touch
23
+ // ("anything the user authored outside the markers is untouched"). A tool that
24
+ // quietly edits a person's hand-written gitignore on every `maude design link`
25
+ // is a worse bug than the one it closes. So this module only ever REPORTS, and
26
+ // removal is an explicit `maude doctor --fix` — the surface whose whole contract
27
+ // is that it is never silent.
28
+
29
+ /**
30
+ * Design-root-relative patterns that name VERSIONED content.
31
+ *
32
+ * Kept as literal shapes rather than a glob engine on purpose: this decides
33
+ * whether to propose deleting a line from somebody's `.gitignore`, and a clever
34
+ * matcher that is right 95 % of the time is not good enough for that. Every
35
+ * entry here is a rule that CANNOT be legitimate under DDR-115 — matching one is
36
+ * proof of drift, not a heuristic. Mirrors the "deliberately absent" list in
37
+ * `gitignore-block.mjs` and the taxonomy in `apps/studio/git/service.ts`.
38
+ */
39
+ const VERSIONED_PATTERNS = [
40
+ { pattern: '*.annotations.svg', what: 'canvas draw layers (annotations)' },
41
+ { pattern: '*.meta.json', what: 'canvas sidecars (layout, artboards, tags)' },
42
+ { pattern: '*.tsx', what: 'canvas bodies' },
43
+ { pattern: 'config.json', what: 'the project design config' },
44
+ { pattern: 'system', what: 'the design system' },
45
+ { pattern: 'system/', what: 'the design system' },
46
+ { pattern: 'system/**', what: 'the design system' },
47
+ ];
48
+
49
+ /** One `.gitignore` line that ignores versioned content. */
50
+ /**
51
+ * @typedef {object} GitignoreDrift
52
+ * @property {number} line 1-indexed line number in `.gitignore`
53
+ * @property {string} text the offending line, verbatim
54
+ * @property {string} pattern the design-root-relative shape it matched
55
+ * @property {string} what plain-language name of what it drops
56
+ */
57
+
58
+ /**
59
+ * Find rules that ignore VERSIONED design content.
60
+ *
61
+ * SCOPED TO THE DESIGN ROOT. Only lines that explicitly name the design root
62
+ * (`.design/*.annotations.svg`) are considered. A bare `*.tsx` at the repo root
63
+ * is a decision about a whole codebase — very possibly deliberate, and never
64
+ * ours to second-guess from here.
65
+ *
66
+ * NEGATIONS ARE NOT DRIFT. A `!` line RE-INCLUDES a path; it is the fix, not the
67
+ * fault. Comments and blank lines are skipped.
68
+ *
69
+ * Lines INSIDE the `# maude:begin` / `# maude:end` markers are skipped too: that
70
+ * region is generated, so anything wrong there is a bug in the generator and is
71
+ * corrected by re-running it, not by editing a person's file.
72
+ *
73
+ * @param {string} contents raw `.gitignore` text ('' when absent)
74
+ * @param {string} designRel design root relative to the repo root
75
+ * @returns {GitignoreDrift[]}
76
+ */
77
+ export function findGitignoreDrift(contents, designRel = '.design') {
78
+ const root = String(designRel ?? '.design')
79
+ .replace(/^\.\//, '')
80
+ .replace(/^\/+|\/+$/g, '');
81
+ if (!root) return [];
82
+ /** @type {GitignoreDrift[]} */
83
+ const out = [];
84
+ let inBlock = false;
85
+ const lines = String(contents ?? '').split('\n');
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const raw = lines[i];
88
+ const text = raw.trim();
89
+ if (text === '# maude:begin') {
90
+ inBlock = true;
91
+ continue;
92
+ }
93
+ if (text === '# maude:end') {
94
+ inBlock = false;
95
+ continue;
96
+ }
97
+ if (inBlock || !text || text.startsWith('#') || text.startsWith('!')) continue;
98
+ // Anchor and trailing-slash forms are the same rule to git.
99
+ const body = text.replace(/^\/+/, '').replace(/\/+$/, '');
100
+ if (body === root) continue; // ignoring the WHOLE design root is a choice, not drift
101
+ if (!body.startsWith(`${root}/`)) continue;
102
+ // Normalise BOTH sides the same way, or the comparison silently never
103
+ // matches: `system/**` was stripped to `system` on the pattern side only, so
104
+ // a real `.design/system/**` rule — a shape this module lists as versioned —
105
+ // was reported clean. A detector that quietly covers less than it claims is
106
+ // worse than one that covers less openly.
107
+ const norm = (s) => s.replace(/\/+$/, '').replace(/\/\*\*$/, '');
108
+ const rel = norm(body.slice(root.length + 1));
109
+ const hit = VERSIONED_PATTERNS.find((p) => rel === norm(p.pattern));
110
+ if (hit) out.push({ line: i + 1, text, pattern: hit.pattern, what: hit.what });
111
+ }
112
+ return out;
113
+ }
114
+
115
+ /**
116
+ * Remove the drifting lines, and only those.
117
+ *
118
+ * Deletes whole lines by NUMBER, taken from a `findGitignoreDrift` run over the
119
+ * same text — never by re-matching, so what is removed is exactly what was
120
+ * reported to the person who approved it. Everything else, including their
121
+ * comments and their blank lines, survives byte-for-byte.
122
+ *
123
+ * @param {string} contents
124
+ * @param {GitignoreDrift[]} drift
125
+ * @returns {string}
126
+ */
127
+ export function removeGitignoreDrift(contents, drift) {
128
+ if (!drift?.length) return contents;
129
+ const drop = new Set(drift.map((d) => d.line));
130
+ return String(contents ?? '')
131
+ .split('\n')
132
+ .filter((_, i) => !drop.has(i + 1))
133
+ .join('\n');
134
+ }
135
+
136
+ /**
137
+ * One line per finding, for the report.
138
+ *
139
+ * Names the CONSEQUENCE, not the rule: "your annotations are never committed"
140
+ * is what the person needs to hear. The rule is already on screen.
141
+ *
142
+ * @param {GitignoreDrift[]} drift
143
+ * @returns {string[]}
144
+ */
145
+ export function describeGitignoreDrift(drift) {
146
+ return (drift ?? []).map(
147
+ (d) => `.gitignore:${d.line} ${d.text} — drops ${d.what} from git (DDR-115 says versioned)`
148
+ );
149
+ }
@@ -0,0 +1,156 @@
1
+ // Gitignore drift — the check that catches git dropping VERSIONED design work.
2
+ //
3
+ // The case that produced this file, verbatim from a real project: a
4
+ // hand-authored pre-markers block carrying `.design/*.annotations.svg`, copied
5
+ // from a v1.0 planning document that predates the DDR-115 correction. Not one
6
+ // annotation sidecar had ever been committed. Both UIs rendered the draw layer;
7
+ // no clone, backup or PR contained it; nothing said so.
8
+ //
9
+ // What is pinned here is mostly the NEGATIVE space. A detector that proposes
10
+ // deleting lines from somebody's hand-written `.gitignore` is only as good as
11
+ // its refusals, so most of these tests assert what it must NOT flag.
12
+
13
+ import assert from 'node:assert/strict';
14
+ import { test } from 'node:test';
15
+
16
+ import {
17
+ describeGitignoreDrift,
18
+ findGitignoreDrift,
19
+ removeGitignoreDrift,
20
+ } from './gitignore-drift.mjs';
21
+
22
+ test('finds the real-world case: annotations ignored outside the markers', () => {
23
+ const gitignore = [
24
+ 'node_modules/',
25
+ '',
26
+ '# design plugin dev-server runtime state',
27
+ '.design/_active.json',
28
+ '.design/_history/',
29
+ '.design/*.annotations.svg',
30
+ '.design/_chat/',
31
+ ].join('\n');
32
+ const drift = findGitignoreDrift(gitignore, '.design');
33
+ assert.equal(drift.length, 1);
34
+ assert.equal(drift[0].line, 6);
35
+ assert.equal(drift[0].text, '.design/*.annotations.svg');
36
+ assert.match(drift[0].what, /annotations/);
37
+ });
38
+
39
+ test('the runtime-state block is NOT drift — those paths are ignored by design', () => {
40
+ // Every line the block writer itself emits must come back clean, or the check
41
+ // would tell people to un-ignore their own `_history/` on every run.
42
+ const gitignore = [
43
+ '.design/_server.json',
44
+ '.design/_active.json',
45
+ '.design/_active.*.json',
46
+ '.design/_sync.json',
47
+ '.design/_history/',
48
+ '.design/_trash/',
49
+ '.design/_comments/',
50
+ '.design/_canvas-state/',
51
+ '.design/_untrusted/',
52
+ '.design/assets/',
53
+ ].join('\n');
54
+ assert.deepEqual(findGitignoreDrift(gitignore, '.design'), []);
55
+ });
56
+
57
+ test('lines INSIDE the maude block are skipped — a generator bug is fixed by the generator', () => {
58
+ const gitignore = [
59
+ '# maude:begin',
60
+ '.design/*.annotations.svg',
61
+ '# maude:end',
62
+ '.design/_history/',
63
+ ].join('\n');
64
+ assert.deepEqual(findGitignoreDrift(gitignore, '.design'), []);
65
+ });
66
+
67
+ test('a negation is the fix, not the fault', () => {
68
+ const gitignore = ['.design/', '!.design/*.annotations.svg'].join('\n');
69
+ assert.deepEqual(findGitignoreDrift(gitignore, '.design'), []);
70
+ });
71
+
72
+ test('comments and blank lines are not rules', () => {
73
+ const gitignore = ['', '# .design/*.annotations.svg', ' ', '#.design/*.tsx'].join('\n');
74
+ assert.deepEqual(findGitignoreDrift(gitignore, '.design'), []);
75
+ });
76
+
77
+ test('ignoring the WHOLE design root is a choice we do not second-guess', () => {
78
+ // Somebody who ignores `.design/` outright has decided their design work is
79
+ // not in this repo. That is coherent; flagging it would be nagging.
80
+ for (const line of ['.design', '.design/', '/.design/']) {
81
+ assert.deepEqual(findGitignoreDrift(line, '.design'), [], line);
82
+ }
83
+ });
84
+
85
+ test('a repo-wide rule is out of scope — it is a decision about a whole codebase', () => {
86
+ // `*.tsx` at the repo root is very possibly deliberate and is not ours to
87
+ // reinterpret from inside a design-tool diagnostic.
88
+ const gitignore = ['*.tsx', 'config.json', '*.meta.json'].join('\n');
89
+ assert.deepEqual(findGitignoreDrift(gitignore, '.design'), []);
90
+ });
91
+
92
+ test('a non-default design root is honoured', () => {
93
+ const gitignore = ['design/*.annotations.svg', '.design/*.annotations.svg'].join('\n');
94
+ const drift = findGitignoreDrift(gitignore, 'design');
95
+ assert.equal(drift.length, 1);
96
+ assert.equal(drift[0].line, 1);
97
+ });
98
+
99
+ test('anchors and trailing slashes are the same rule to git', () => {
100
+ const drift = findGitignoreDrift('/.design/system/', '.design');
101
+ assert.equal(drift.length, 1);
102
+ assert.match(drift[0].what, /design system/);
103
+ });
104
+
105
+ test('every versioned shape is covered, none of the runtime ones', () => {
106
+ const versioned = [
107
+ '.design/*.annotations.svg',
108
+ '.design/*.meta.json',
109
+ '.design/*.tsx',
110
+ '.design/config.json',
111
+ '.design/system/',
112
+ // The glob form. Both sides of the comparison must be normalised the same
113
+ // way — normalising only the pattern made this shape report clean while the
114
+ // module listed it as covered.
115
+ '.design/system/**',
116
+ ];
117
+ assert.equal(findGitignoreDrift(versioned.join('\n'), '.design').length, versioned.length);
118
+ });
119
+
120
+ test('removal deletes exactly the reported lines, byte-for-byte otherwise', () => {
121
+ const gitignore = [
122
+ 'node_modules/',
123
+ '',
124
+ '# my own comment',
125
+ '.design/*.annotations.svg',
126
+ '.design/_history/',
127
+ ].join('\n');
128
+ const drift = findGitignoreDrift(gitignore, '.design');
129
+ const next = removeGitignoreDrift(gitignore, drift);
130
+ assert.equal(
131
+ next,
132
+ ['node_modules/', '', '# my own comment', '.design/_history/'].join('\n'),
133
+ 'the user comment and the blank line survive'
134
+ );
135
+ // And the result is clean on a re-run — the fix converges.
136
+ assert.deepEqual(findGitignoreDrift(next, '.design'), []);
137
+ });
138
+
139
+ test('removal with no drift is a byte-identical no-op', () => {
140
+ const gitignore = 'node_modules/\n.design/_history/\n';
141
+ assert.equal(removeGitignoreDrift(gitignore, []), gitignore);
142
+ assert.equal(removeGitignoreDrift(gitignore, undefined), gitignore);
143
+ });
144
+
145
+ test('an absent or empty gitignore is not drift', () => {
146
+ assert.deepEqual(findGitignoreDrift('', '.design'), []);
147
+ assert.deepEqual(findGitignoreDrift(undefined, '.design'), []);
148
+ });
149
+
150
+ test('the description names the consequence, not just the rule', () => {
151
+ const drift = findGitignoreDrift('.design/*.annotations.svg', '.design');
152
+ const [line] = describeGitignoreDrift(drift);
153
+ assert.match(line, /\.gitignore:1/);
154
+ assert.match(line, /drops/);
155
+ assert.match(line, /DDR-115/);
156
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.60.2",
3
+ "version": "0.60.4",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -54,13 +54,13 @@
54
54
  "test:e2e:desktop:parity:cloud": "pnpm --filter @maude/desktop-e2e e2e:parity:cloud"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@1agh/maude-darwin-arm64": "0.60.2",
58
- "@1agh/maude-darwin-x64": "0.60.2",
59
- "@1agh/maude-linux-arm64": "0.60.2",
60
- "@1agh/maude-linux-arm64-musl": "0.60.2",
61
- "@1agh/maude-linux-x64": "0.60.2",
62
- "@1agh/maude-linux-x64-musl": "0.60.2",
63
- "@1agh/maude-win32-x64": "0.60.2"
57
+ "@1agh/maude-darwin-arm64": "0.60.4",
58
+ "@1agh/maude-darwin-x64": "0.60.4",
59
+ "@1agh/maude-linux-arm64": "0.60.4",
60
+ "@1agh/maude-linux-arm64-musl": "0.60.4",
61
+ "@1agh/maude-linux-x64": "0.60.4",
62
+ "@1agh/maude-linux-x64-musl": "0.60.4",
63
+ "@1agh/maude-win32-x64": "0.60.4"
64
64
  },
65
65
  "files": [
66
66
  "cli",