@1agh/maude 0.60.2 → 0.60.3
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/client/app.jsx +1 -0
- package/apps/studio/client/panels/SyncPanel.jsx +26 -11
- package/apps/studio/dist/client.bundle.js +393 -393
- package/apps/studio/http.ts +36 -6
- package/apps/studio/server.ts +11 -0
- package/apps/studio/sync/asset-push.ts +30 -0
- package/apps/studio/sync/connection-state.ts +23 -0
- package/apps/studio/sync/discovery.ts +139 -0
- package/apps/studio/sync/index.ts +804 -163
- package/apps/studio/test/sync-asset-push.test.ts +93 -2
- package/apps/studio/test/sync-attach-incremental.test.ts +527 -0
- package/apps/studio/test/sync-panel-surface.test.ts +10 -0
- package/apps/studio/test/sync-resync-routes.test.ts +43 -0
- package/apps/studio/test/sync-supervisor.test.ts +4 -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
|
@@ -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.
|
|
3
|
+
"version": "0.60.3",
|
|
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.
|
|
58
|
-
"@1agh/maude-darwin-x64": "0.60.
|
|
59
|
-
"@1agh/maude-linux-arm64": "0.60.
|
|
60
|
-
"@1agh/maude-linux-arm64-musl": "0.60.
|
|
61
|
-
"@1agh/maude-linux-x64": "0.60.
|
|
62
|
-
"@1agh/maude-linux-x64-musl": "0.60.
|
|
63
|
-
"@1agh/maude-win32-x64": "0.60.
|
|
57
|
+
"@1agh/maude-darwin-arm64": "0.60.3",
|
|
58
|
+
"@1agh/maude-darwin-x64": "0.60.3",
|
|
59
|
+
"@1agh/maude-linux-arm64": "0.60.3",
|
|
60
|
+
"@1agh/maude-linux-arm64-musl": "0.60.3",
|
|
61
|
+
"@1agh/maude-linux-x64": "0.60.3",
|
|
62
|
+
"@1agh/maude-linux-x64-musl": "0.60.3",
|
|
63
|
+
"@1agh/maude-win32-x64": "0.60.3"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"cli",
|