@1agh/maude 0.22.0 → 0.23.0

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 (60) hide show
  1. package/cli/commands/design-link.test.mjs +53 -1
  2. package/cli/commands/hub.test.mjs +10 -9
  3. package/cli/lib/design-link.mjs +154 -7
  4. package/cli/lib/hubs-config.mjs +42 -4
  5. package/package.json +9 -9
  6. package/plugins/design/dev-server/bin/check-runtime-bundles.sh +125 -0
  7. package/plugins/design/dev-server/bin/runtime-health.sh +47 -6
  8. package/plugins/design/dev-server/build.ts +87 -7
  9. package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
  10. package/plugins/design/dev-server/canvas-lib.tsx +22 -6
  11. package/plugins/design/dev-server/canvas-shell.tsx +25 -226
  12. package/plugins/design/dev-server/client/app.jsx +37 -15
  13. package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
  14. package/plugins/design/dev-server/collab/registry.ts +51 -0
  15. package/plugins/design/dev-server/config.schema.json +20 -0
  16. package/plugins/design/dev-server/context.ts +7 -0
  17. package/plugins/design/dev-server/dist/client.bundle.js +21 -12
  18. package/plugins/design/dev-server/dist/comment-mount.js +1801 -0
  19. package/plugins/design/dev-server/dist/runtime/.min-sizes.json +16 -0
  20. package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +2 -6
  21. package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +2 -6
  22. package/plugins/design/dev-server/dist/runtime/motion.js +4787 -8
  23. package/plugins/design/dev-server/dist/runtime/motion_react.js +9654 -7
  24. package/plugins/design/dev-server/dist/runtime/pixi-js.js +5865 -5469
  25. package/plugins/design/dev-server/dist/runtime/react-dom.js +4 -22
  26. package/plugins/design/dev-server/dist/runtime/react-dom_client.js +5 -23
  27. package/plugins/design/dev-server/dist/runtime/react.js +4 -22
  28. package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +4 -22
  29. package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +4 -22
  30. package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +2 -6
  31. package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +2 -6
  32. package/plugins/design/dev-server/dist/runtime/yjs.js +2 -6
  33. package/plugins/design/dev-server/dom-selection.ts +156 -0
  34. package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
  35. package/plugins/design/dev-server/input-router.tsx +99 -61
  36. package/plugins/design/dev-server/server.ts +18 -0
  37. package/plugins/design/dev-server/sync/agent.ts +323 -0
  38. package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
  39. package/plugins/design/dev-server/sync/codec.ts +169 -0
  40. package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
  41. package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
  42. package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
  43. package/plugins/design/dev-server/sync/index.ts +474 -0
  44. package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
  45. package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
  46. package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
  47. package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
  48. package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
  49. package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
  50. package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
  51. package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
  52. package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
  53. package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
  54. package/plugins/design/dev-server/test/sync-runtime.test.ts +285 -0
  55. package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
  56. package/plugins/design/dev-server/use-collab.tsx +157 -13
  57. package/plugins/design/dev-server/use-selection-set.tsx +12 -0
  58. package/plugins/design/dev-server/use-tool-mode.tsx +12 -0
  59. package/plugins/design/templates/_shell.html +15 -5
  60. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -0
@@ -0,0 +1,96 @@
1
+ // Echo-guard unit tests — Phase 9 Task 4.
2
+
3
+ import { describe, expect, test } from 'bun:test';
4
+
5
+ import { ECHO_TTL_MS, createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
6
+
7
+ describe('hashBytes', () => {
8
+ test('stable hex SHA-256 of bytes', () => {
9
+ expect(hashBytes('hello')).toBe(
10
+ '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
11
+ );
12
+ });
13
+
14
+ test('matches across string + Uint8Array inputs', () => {
15
+ const buf = new TextEncoder().encode('hello');
16
+ expect(hashBytes(buf)).toBe(hashBytes('hello'));
17
+ });
18
+ });
19
+
20
+ describe('createEchoGuard', () => {
21
+ test('record + consume same hash returns true (echo dropped)', () => {
22
+ const g = createEchoGuard();
23
+ g.record('a.html', 'h1');
24
+ expect(g.consume('a.html', 'h1')).toBe(true);
25
+ // Entry was popped — second consume of the same hash is a fresh match.
26
+ expect(g.consume('a.html', 'h1')).toBe(false);
27
+ });
28
+
29
+ test('consume with non-matching hash returns false (real edit)', () => {
30
+ const g = createEchoGuard();
31
+ g.record('a.html', 'h1');
32
+ expect(g.consume('a.html', 'different-hash')).toBe(false);
33
+ // Original entry stays available for matching.
34
+ expect(g.consume('a.html', 'h1')).toBe(true);
35
+ });
36
+
37
+ test('consume on path with no pending entries returns false', () => {
38
+ const g = createEchoGuard();
39
+ expect(g.consume('a.html', 'whatever')).toBe(false);
40
+ });
41
+
42
+ test('expires entries past TTL', () => {
43
+ const g = createEchoGuard(100);
44
+ g.record('a.html', 'h1', 0);
45
+ // Same instant — still valid.
46
+ expect(g.consume('a.html', 'h1', 50)).toBe(true);
47
+
48
+ g.record('a.html', 'h2', 0);
49
+ // Past TTL — expired, not consumed.
50
+ expect(g.consume('a.html', 'h2', 200)).toBe(false);
51
+ });
52
+
53
+ test('per-path isolation', () => {
54
+ const g = createEchoGuard();
55
+ g.record('a.html', 'h1');
56
+ g.record('b.html', 'h2');
57
+ expect(g.consume('a.html', 'h2')).toBe(false);
58
+ expect(g.consume('b.html', 'h1')).toBe(false);
59
+ expect(g.consume('a.html', 'h1')).toBe(true);
60
+ expect(g.consume('b.html', 'h2')).toBe(true);
61
+ });
62
+
63
+ test('stacked writes — N record() calls allow N consume() matches', () => {
64
+ const g = createEchoGuard();
65
+ g.record('a.html', 'h1');
66
+ g.record('a.html', 'h1');
67
+ g.record('a.html', 'h1');
68
+ expect(g.consume('a.html', 'h1')).toBe(true);
69
+ expect(g.consume('a.html', 'h1')).toBe(true);
70
+ expect(g.consume('a.html', 'h1')).toBe(true);
71
+ expect(g.consume('a.html', 'h1')).toBe(false);
72
+ });
73
+
74
+ test('sweep removes expired entries idempotently', () => {
75
+ const g = createEchoGuard(100);
76
+ g.record('a.html', 'h1', 0);
77
+ g.record('b.html', 'h2', 0);
78
+ expect(g.size()).toBe(2);
79
+ g.sweep(50);
80
+ expect(g.size()).toBe(2);
81
+ g.sweep(200);
82
+ expect(g.size()).toBe(0);
83
+ // Double-sweep is fine.
84
+ g.sweep(300);
85
+ expect(g.size()).toBe(0);
86
+ });
87
+
88
+ test('default TTL is ECHO_TTL_MS', () => {
89
+ const g = createEchoGuard();
90
+ g.record('a.html', 'h1', 0);
91
+ expect(g.consume('a.html', 'h1', ECHO_TTL_MS - 1)).toBe(true);
92
+
93
+ g.record('a.html', 'h2', 0);
94
+ expect(g.consume('a.html', 'h2', ECHO_TTL_MS + 1)).toBe(false);
95
+ });
96
+ });
@@ -0,0 +1,182 @@
1
+ // fs-mirror unit tests — Phase 9 Task 4.
2
+
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
8
+
9
+ import { hashBytes } from '../sync/echo-guard.ts';
10
+ import { createFsReader } from '../sync/fs-mirror.ts';
11
+
12
+ let dir: string;
13
+
14
+ beforeEach(() => {
15
+ dir = mkdtempSync(join(tmpdir(), 'fs-mirror-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(dir, { recursive: true, force: true });
20
+ });
21
+
22
+ const ACCEPT_ALL = () => true;
23
+
24
+ describe('createFsReader', () => {
25
+ test('reads file bytes + hash after the quiet window', async () => {
26
+ writeFileSync(join(dir, 'a.html'), 'hello');
27
+ const events: { path: string; bytes: Uint8Array; hash: string }[] = [];
28
+ const r = createFsReader({
29
+ rootDir: dir,
30
+ quietMs: 10,
31
+ accept: ACCEPT_ALL,
32
+ onRead: (e) => {
33
+ events.push(e);
34
+ },
35
+ });
36
+ r.notify('a.html');
37
+ await new Promise((res) => setTimeout(res, 50));
38
+
39
+ expect(events.length).toBe(1);
40
+ expect(events[0].path).toBe('a.html');
41
+ expect(new TextDecoder().decode(events[0].bytes)).toBe('hello');
42
+ expect(events[0].hash).toBe(hashBytes('hello'));
43
+ r.stop();
44
+ });
45
+
46
+ test('debounces rapid notify() calls on the same path', async () => {
47
+ writeFileSync(join(dir, 'b.html'), 'v0');
48
+ const events: { path: string; hash: string }[] = [];
49
+ const r = createFsReader({
50
+ rootDir: dir,
51
+ quietMs: 30,
52
+ accept: ACCEPT_ALL,
53
+ onRead: (e) => {
54
+ events.push({ path: e.path, hash: e.hash });
55
+ },
56
+ });
57
+ // 5 notify calls within 5ms should collapse to one read.
58
+ for (let i = 0; i < 5; i++) r.notify('b.html');
59
+ expect(r.pending()).toBe(1);
60
+ await new Promise((res) => setTimeout(res, 80));
61
+ expect(events.length).toBe(1);
62
+ r.stop();
63
+ });
64
+
65
+ test('reads the latest disk content after the quiet window (not stale)', async () => {
66
+ const p = join(dir, 'c.html');
67
+ writeFileSync(p, 'old');
68
+ const events: string[] = [];
69
+ const r = createFsReader({
70
+ rootDir: dir,
71
+ quietMs: 20,
72
+ accept: ACCEPT_ALL,
73
+ onRead: (e) => {
74
+ events.push(new TextDecoder().decode(e.bytes));
75
+ },
76
+ });
77
+ r.notify('c.html');
78
+ // Update the file BEFORE the quiet window elapses — the read should see
79
+ // the new content.
80
+ writeFileSync(p, 'new');
81
+ await new Promise((res) => setTimeout(res, 50));
82
+ expect(events).toEqual(['new']);
83
+ r.stop();
84
+ });
85
+
86
+ test('per-path isolation — different paths debounce independently', async () => {
87
+ writeFileSync(join(dir, 'd.html'), 'a');
88
+ writeFileSync(join(dir, 'e.html'), 'b');
89
+ const paths: string[] = [];
90
+ const r = createFsReader({
91
+ rootDir: dir,
92
+ quietMs: 10,
93
+ accept: ACCEPT_ALL,
94
+ onRead: (e) => {
95
+ paths.push(e.path);
96
+ },
97
+ });
98
+ r.notify('d.html');
99
+ r.notify('e.html');
100
+ expect(r.pending()).toBe(2);
101
+ await new Promise((res) => setTimeout(res, 50));
102
+ expect(paths.sort()).toEqual(['d.html', 'e.html']);
103
+ r.stop();
104
+ });
105
+
106
+ test('accept() filter rejects paths before any timer fires', async () => {
107
+ writeFileSync(join(dir, 'skipme.css'), 'x');
108
+ let fired = false;
109
+ const r = createFsReader({
110
+ rootDir: dir,
111
+ quietMs: 10,
112
+ accept: (p) => p.endsWith('.html'),
113
+ onRead: () => {
114
+ fired = true;
115
+ },
116
+ });
117
+ r.notify('skipme.css');
118
+ expect(r.pending()).toBe(0);
119
+ await new Promise((res) => setTimeout(res, 50));
120
+ expect(fired).toBe(false);
121
+ r.stop();
122
+ });
123
+
124
+ test('onDeleted fires when the file is gone by the time the timer pops', async () => {
125
+ writeFileSync(join(dir, 'goner.html'), 'bye');
126
+ const deleted: string[] = [];
127
+ const reads: string[] = [];
128
+ const r = createFsReader({
129
+ rootDir: dir,
130
+ quietMs: 20,
131
+ accept: ACCEPT_ALL,
132
+ onRead: (e) => {
133
+ reads.push(e.path);
134
+ },
135
+ onDeleted: (p) => {
136
+ deleted.push(p);
137
+ },
138
+ });
139
+ r.notify('goner.html');
140
+ rmSync(join(dir, 'goner.html'));
141
+ await new Promise((res) => setTimeout(res, 60));
142
+ expect(deleted).toEqual(['goner.html']);
143
+ expect(reads).toEqual([]);
144
+ r.stop();
145
+ });
146
+
147
+ test('stop() cancels pending timers', async () => {
148
+ writeFileSync(join(dir, 'h.html'), 'x');
149
+ let fired = false;
150
+ const r = createFsReader({
151
+ rootDir: dir,
152
+ quietMs: 20,
153
+ accept: ACCEPT_ALL,
154
+ onRead: () => {
155
+ fired = true;
156
+ },
157
+ });
158
+ r.notify('h.html');
159
+ r.stop();
160
+ await new Promise((res) => setTimeout(res, 50));
161
+ expect(fired).toBe(false);
162
+ });
163
+
164
+ test('flush() fires all pending timers immediately', async () => {
165
+ writeFileSync(join(dir, 'i.html'), 'x');
166
+ writeFileSync(join(dir, 'j.html'), 'y');
167
+ const reads: string[] = [];
168
+ const r = createFsReader({
169
+ rootDir: dir,
170
+ quietMs: 10000, // long enough that the test would time out without flush
171
+ accept: ACCEPT_ALL,
172
+ onRead: (e) => {
173
+ reads.push(e.path);
174
+ },
175
+ });
176
+ r.notify('i.html');
177
+ r.notify('j.html');
178
+ await r.flush();
179
+ expect(reads.sort()).toEqual(['i.html', 'j.html']);
180
+ r.stop();
181
+ });
182
+ });