@1agh/maude 0.58.0 → 0.58.1
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/artboard-hug-commit.ts +30 -0
- package/apps/studio/client/app.jsx +22 -11
- package/apps/studio/client/panels/timeline-media-cache.js +38 -2
- package/apps/studio/clip-ops.ts +84 -35
- package/apps/studio/dist/client.bundle.js +936 -936
- package/apps/studio/test/artboard-hug-commit.test.ts +36 -0
- package/apps/studio/test/clip-ops.test.ts +84 -2
- package/apps/studio/test/timeline-media-cache.test.ts +41 -0
- package/package.json +8 -8
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// artboard-hug-commit.ts — issue #79. The Artboard Inspector's Height
|
|
2
|
+
// NumberField silently dropped a typed value whenever the artboard was in
|
|
3
|
+
// Hug mode (fixed=false, the default per DDR-027 Design Decision 1) — Width
|
|
4
|
+
// always worked because it has no such mode. Typing a height should instead
|
|
5
|
+
// promote the artboard to Fixed at that height, same as the Hug/Fixed
|
|
6
|
+
// toggle's own freeze-current-height write.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
import { resolveHeightCommit } from '../artboard-hug-commit.ts';
|
|
11
|
+
|
|
12
|
+
describe('artboard-hug-commit / resolveHeightCommit', () => {
|
|
13
|
+
test('fixed=true → plain resize (Width-field parity)', () => {
|
|
14
|
+
expect(resolveHeightCommit(true, 480)).toEqual({ kind: 'resize', height: 480 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('fixed=false (Hug, the default) → promotes to Fixed at the typed height', () => {
|
|
18
|
+
// Before the fix this typed value was dropped entirely (no action) —
|
|
19
|
+
// reproducing "when I want to set height... manually from inspector
|
|
20
|
+
// panel, nothing happened. Width works."
|
|
21
|
+
expect(resolveHeightCommit(false, 480)).toEqual({ kind: 'promote-to-fixed', height: 480 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rounds a fractional typed value', () => {
|
|
25
|
+
expect(resolveHeightCommit(true, 480.6)).toEqual({ kind: 'resize', height: 481 });
|
|
26
|
+
expect(resolveHeightCommit(false, 480.6)).toEqual({ kind: 'promote-to-fixed', height: 481 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('non-finite or non-positive input → noop, in either mode', () => {
|
|
30
|
+
expect(resolveHeightCommit(true, Number.NaN)).toEqual({ kind: 'noop' });
|
|
31
|
+
expect(resolveHeightCommit(true, 0)).toEqual({ kind: 'noop' });
|
|
32
|
+
expect(resolveHeightCommit(true, -10)).toEqual({ kind: 'noop' });
|
|
33
|
+
expect(resolveHeightCommit(false, Number.NaN)).toEqual({ kind: 'noop' });
|
|
34
|
+
expect(resolveHeightCommit(false, 0)).toEqual({ kind: 'noop' });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
|
|
32
32
|
const CANVAS = '/abs/Canvas.tsx';
|
|
33
33
|
|
|
34
|
+
// Adversarial fixture (security review 2026-07-30) — a prompt embedding a
|
|
35
|
+
// fake </AIPlaceholder> close tag + template-literal/env-var syntax, proving
|
|
36
|
+
// the JSON.stringify escaping discipline (DDR-150 P1) holds under attack.
|
|
37
|
+
const EVIL = '*/ `backtick` ${process.env.HOME} </AIPlaceholder> "q" \\';
|
|
38
|
+
|
|
34
39
|
const SERIES = [
|
|
35
40
|
"import { TransitionSeries } from '@remotion/transitions';",
|
|
36
41
|
'const S1 = 90;',
|
|
@@ -568,8 +573,6 @@ describe('greenfield first drop (Task 20)', () => {
|
|
|
568
573
|
});
|
|
569
574
|
|
|
570
575
|
describe('AIPlaceholder insert + resolve (Tasks 21–22)', () => {
|
|
571
|
-
const EVIL = '*/ `backtick` ${process.env.HOME} </AIPlaceholder> "q" \\';
|
|
572
|
-
|
|
573
576
|
test('prompt is JSON.stringified into the JSX (injection fixtures) and enumerates back', () => {
|
|
574
577
|
const r = applyInsertClipAt(CANVAS, SERIES, 'a', {
|
|
575
578
|
lane: 'storyline',
|
|
@@ -833,4 +836,83 @@ describe('layers model — set-text + move between layers', () => {
|
|
|
833
836
|
expect(cc.clips.filter((c) => c.tag === 'Sequence').length).toBe(0); // overlay row gone
|
|
834
837
|
expect(back.source).toContain('const TOTAL = 270;'); // exact content end again
|
|
835
838
|
});
|
|
839
|
+
|
|
840
|
+
// Bug: issue #81 — an AI placeholder slate (no media src yet) was refused by
|
|
841
|
+
// both move verbs, so it could never leave the storyline it was dropped
|
|
842
|
+
// into. A placeholder has no <src> to graft, but it has a prompt + kind
|
|
843
|
+
// that can ride over unchanged.
|
|
844
|
+
test('AI placeholder beat → overlay layer keeps the prompt (no media src required)', () => {
|
|
845
|
+
const withPh = applyInsertClipAt(CANVAS, HARDCUT, 'a', {
|
|
846
|
+
lane: 'storyline',
|
|
847
|
+
index: 3,
|
|
848
|
+
durationInFrames: 60,
|
|
849
|
+
placeholder: { prompt: 'drone shot over Brno', kind: 'veo' },
|
|
850
|
+
});
|
|
851
|
+
const r = applyMoveClipToOverlay(
|
|
852
|
+
CANVAS,
|
|
853
|
+
withPh.source,
|
|
854
|
+
'a',
|
|
855
|
+
withPh.stableId as string,
|
|
856
|
+
undefined
|
|
857
|
+
);
|
|
858
|
+
const cc = enumerateClips(CANVAS, r.source, 'a');
|
|
859
|
+
const overlays = cc.clips.filter((c) => c.tag === 'Sequence');
|
|
860
|
+
expect(overlays.length).toBe(1);
|
|
861
|
+
expect(overlays[0]?.placeholder?.prompt).toBe('drone shot over Brno');
|
|
862
|
+
expect(overlays[0]?.mediaSrc).toBe(null);
|
|
863
|
+
const beats = cc.clips.filter((c) => c.kind === 'sequence' && c.tag !== 'Sequence');
|
|
864
|
+
expect(beats.length).toBe(3); // the placeholder beat left the storyline
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test('AI placeholder overlay → storyline round-trips the prompt back into a beat', () => {
|
|
868
|
+
const withPh = applyInsertClipAt(CANVAS, HARDCUT, 'a', {
|
|
869
|
+
lane: 'overlay',
|
|
870
|
+
from: 0,
|
|
871
|
+
durationInFrames: 45,
|
|
872
|
+
placeholder: { prompt: 'sunset time-lapse', kind: 'image' },
|
|
873
|
+
});
|
|
874
|
+
const back = applyMoveClipToStoryline(
|
|
875
|
+
CANVAS,
|
|
876
|
+
withPh.source,
|
|
877
|
+
'a',
|
|
878
|
+
withPh.stableId as string,
|
|
879
|
+
undefined
|
|
880
|
+
);
|
|
881
|
+
const cc = enumerateClips(CANVAS, back.source, 'a');
|
|
882
|
+
const beats = cc.clips.filter((c) => c.kind === 'sequence' && c.tag !== 'Sequence');
|
|
883
|
+
expect(beats.some((c) => c.placeholder?.prompt === 'sunset time-lapse')).toBe(true);
|
|
884
|
+
expect(cc.clips.filter((c) => c.tag === 'Sequence').length).toBe(0); // overlay row gone
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
// Adversarial (security review 2026-08-07): the move path must go through
|
|
888
|
+
// the same JSON.stringify escaping as a fresh insert (DDR-150 P1) — prove
|
|
889
|
+
// it with the same EVIL fixture the insert/resolve suite uses above, not
|
|
890
|
+
// just infer it from code-path sharing.
|
|
891
|
+
test('AI placeholder move preserves EVIL-prompt escaping (F-A3 companion)', () => {
|
|
892
|
+
const withPh = applyInsertClipAt(CANVAS, HARDCUT, 'a', {
|
|
893
|
+
lane: 'storyline',
|
|
894
|
+
index: 3,
|
|
895
|
+
durationInFrames: 60,
|
|
896
|
+
placeholder: { prompt: EVIL, kind: 'veo' },
|
|
897
|
+
});
|
|
898
|
+
const moved = applyMoveClipToOverlay(
|
|
899
|
+
CANVAS,
|
|
900
|
+
withPh.source,
|
|
901
|
+
'a',
|
|
902
|
+
withPh.stableId as string,
|
|
903
|
+
undefined
|
|
904
|
+
);
|
|
905
|
+
expect(moved.source).toContain(`<span>{${JSON.stringify(EVIL)}}</span>`);
|
|
906
|
+
const back = applyMoveClipToStoryline(
|
|
907
|
+
CANVAS,
|
|
908
|
+
moved.source,
|
|
909
|
+
'a',
|
|
910
|
+
moved.stableId as string,
|
|
911
|
+
undefined
|
|
912
|
+
);
|
|
913
|
+
expect(back.source).toContain(`<span>{${JSON.stringify(EVIL)}}</span>`);
|
|
914
|
+
const cc = enumerateClips(CANVAS, back.source, 'a');
|
|
915
|
+
const beats = cc.clips.filter((c) => c.kind === 'sequence' && c.tag !== 'Sequence');
|
|
916
|
+
expect(beats.some((c) => c.placeholder?.prompt === EVIL)).toBe(true);
|
|
917
|
+
});
|
|
836
918
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// timeline-media-cache.test.ts — bug fix: dropping a clip on the timeline
|
|
2
|
+
// used to always trim it to a flat 3s instead of its real duration
|
|
3
|
+
// (durationInFrames was `Math.round(fps * 3)` regardless of the probed
|
|
4
|
+
// media length). durationFramesForDrop is the pure decision the drop
|
|
5
|
+
// handler in app.jsx now delegates to.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import { durationFramesForDrop } from '../client/panels/timeline-media-cache.js';
|
|
10
|
+
|
|
11
|
+
describe('durationFramesForDrop', () => {
|
|
12
|
+
test('uses the real probed duration, not the flat 3s default', () => {
|
|
13
|
+
// A 7.4s clip at 30fps must land at its own length (222 frames), not the
|
|
14
|
+
// fixed fps*3 = 90 frames the drop handler used to hardcode.
|
|
15
|
+
expect(durationFramesForDrop(30, 7.4)).toBe(Math.round(30 * 7.4));
|
|
16
|
+
expect(durationFramesForDrop(30, 7.4)).not.toBe(Math.round(30 * 3));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('rounds to the nearest frame', () => {
|
|
20
|
+
expect(durationFramesForDrop(24, 2.501)).toBe(60); // 24 * 2.501 = 60.024
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('falls back to 3s when no duration was probed (null/failed probe)', () => {
|
|
24
|
+
expect(durationFramesForDrop(30, null)).toBe(90);
|
|
25
|
+
expect(durationFramesForDrop(30, undefined)).toBe(90);
|
|
26
|
+
expect(durationFramesForDrop(30, NaN)).toBe(90);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('falls back to 3s for a non-positive probed duration', () => {
|
|
30
|
+
expect(durationFramesForDrop(30, 0)).toBe(90);
|
|
31
|
+
expect(durationFramesForDrop(30, -1)).toBe(90);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('a caller can override the fallback (e.g. synthetic content)', () => {
|
|
35
|
+
expect(durationFramesForDrop(30, null, 5)).toBe(150);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('never returns less than 1 frame', () => {
|
|
39
|
+
expect(durationFramesForDrop(30, 0.001)).toBeGreaterThanOrEqual(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.1",
|
|
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.58.
|
|
58
|
-
"@1agh/maude-darwin-x64": "0.58.
|
|
59
|
-
"@1agh/maude-linux-arm64": "0.58.
|
|
60
|
-
"@1agh/maude-linux-arm64-musl": "0.58.
|
|
61
|
-
"@1agh/maude-linux-x64": "0.58.
|
|
62
|
-
"@1agh/maude-linux-x64-musl": "0.58.
|
|
63
|
-
"@1agh/maude-win32-x64": "0.58.
|
|
57
|
+
"@1agh/maude-darwin-arm64": "0.58.1",
|
|
58
|
+
"@1agh/maude-darwin-x64": "0.58.1",
|
|
59
|
+
"@1agh/maude-linux-arm64": "0.58.1",
|
|
60
|
+
"@1agh/maude-linux-arm64-musl": "0.58.1",
|
|
61
|
+
"@1agh/maude-linux-x64": "0.58.1",
|
|
62
|
+
"@1agh/maude-linux-x64-musl": "0.58.1",
|
|
63
|
+
"@1agh/maude-win32-x64": "0.58.1"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"cli",
|