@a-company/atelier 0.29.0 → 0.37.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 (76) hide show
  1. package/dist/chunk-5QQESXI6.js +4432 -0
  2. package/dist/chunk-5QQESXI6.js.map +1 -0
  3. package/dist/cli.cjs +2391 -530
  4. package/dist/cli.cjs.map +1 -1
  5. package/dist/cli.js +301 -429
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +2233 -38
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +584 -2
  10. package/dist/index.d.ts +584 -2
  11. package/dist/index.js +111 -3
  12. package/dist/mcp.cjs +1215 -365
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +1209 -365
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +20 -9
  17. package/src/web/inline-app.ts +867 -0
  18. package/src/web/tsconfig.json +9 -0
  19. package/templates/welcome.atelier +67 -0
  20. package/university/content/notes/N-atel-001-first-render.md +114 -0
  21. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  22. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  23. package/university/content/notes/N-atel-101-easings.md +97 -0
  24. package/university/content/notes/N-atel-101-layers.md +106 -0
  25. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  26. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  27. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  28. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  29. package/university/content/notes/N-atel-201-patterns.md +108 -0
  30. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  31. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  32. package/university/content/notes/N-atel-301-effects.md +136 -0
  33. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  34. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  35. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  36. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  37. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  38. package/university/content/notes/N-atel-401-transitions.md +94 -0
  39. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  40. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  41. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  42. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  43. package/university/content/notes/N-atel-601-carousel.md +71 -0
  44. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  45. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  46. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  47. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  48. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  49. package/university/content/notes/N-atel-701-vector.md +85 -0
  50. package/university/content/notes/N-atel-701-video.md +88 -0
  51. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  52. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  53. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  54. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  55. package/university/content/paths/LP-atel-001.yaml +21 -0
  56. package/university/content/paths/LP-atel-101.yaml +22 -0
  57. package/university/content/paths/LP-atel-201.yaml +23 -0
  58. package/university/content/paths/LP-atel-301.yaml +22 -0
  59. package/university/content/paths/LP-atel-401.yaml +22 -0
  60. package/university/content/paths/LP-atel-501.yaml +22 -0
  61. package/university/content/paths/LP-atel-601.yaml +22 -0
  62. package/university/content/paths/LP-atel-701.yaml +22 -0
  63. package/university/content/paths/LP-atel-801.yaml +22 -0
  64. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  65. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  66. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  67. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  68. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  69. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  70. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  71. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  72. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  73. package/university/index.yaml +720 -0
  74. package/university/pack.yaml +21 -0
  75. package/dist/chunk-JV7RGETS.js +0 -2292
  76. package/dist/chunk-JV7RGETS.js.map +0 -1
@@ -0,0 +1,66 @@
1
+ id: Q-atel-301-visual-system
2
+ title: 'ATEL-301: Visual System'
3
+ description: Verify the visual surface — shapes, text, images/video, effects (fills/strokes/shadows/blends/clips/motion paths/tints), composition (groups/refs), and the overlay namespace.
4
+ author: atelier
5
+ created: '2026-05-18'
6
+ updated: '2026-05-18'
7
+ tags:
8
+ - course
9
+ - atel-301
10
+ difficulty: intermediate
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'For text to render consistently across the browser studio and the CLI PNG export, what is the recommended pattern?'
15
+ choices:
16
+ A: Set `fontFamily` to a generic name like `sans-serif` and let each renderer pick
17
+ B: 'Ship the font as an asset (`atelier_add_asset({ kind: "font", src: "fonts/Inter.woff2" })`) and reference it by PostScript name in `fontFamily`'
18
+ C: Use only system fonts that ship with macOS, Windows, and Linux
19
+ D: Convert all text to SVG paths before rendering
20
+ correct: B
21
+ explanation: Without font assets, the studio uses browser-available fonts and the CLI PNG export uses node-canvas with whatever's installed on the machine. Renders diverge. Registering the font as an asset and referencing its PostScript name in `fontFamily` makes the renderer load the same font everywhere. The CSS-style fallback chain still works for graceful degradation.
22
+ - id: q2
23
+ question: 'You want to crop an image to show only its center 50% in a layer that sits in a 1080×1080 layout. The source image is 2000×1500. Which approach fits the "cropping vs scaling" model best?'
24
+ choices:
25
+ A: Pre-process the image with a tool like ImageMagick and ship the cropped version
26
+ B: 'Set `sourceRect: {x: 500, y: 375, width: 1000, height: 750}` to pick the center 50% from the source; the layer''s bounds and the image''s drawn region handle the rest'
27
+ C: 'Set `Layer.scale.x: 2.0` and `Layer.scale.y: 2.0` to zoom in'
28
+ D: Resize the layer's `bounds` to be smaller than the image
29
+ correct: B
30
+ explanation: '`sourceRect` is the right tool — it picks a sub-rectangle from the source image in source pixels. The center 50% of a 2000×1500 image is `{x: 500, y: 375, width: 1000, height: 750}`. Layer.scale stretches the rendered layer (lossy zoom). Layer.bounds changes the destination size (the renderer fits per objectFit). Pre-processing forfeits the ability to animate the crop later (ken-burns pan/zoom via deltas on sourceRect).'
31
+ - id: q3
32
+ question: A `motionPath` is set on a layer that also has a delta animating `frame.x`. What happens?
33
+ choices:
34
+ A: The delta wins; the motion path is ignored
35
+ B: 'The motion path wins; the frame.x delta is ignored — animate `motionProgress` instead'
36
+ C: They additively combine, producing a sum trajectory
37
+ D: 'The schema rejects the document at validate time'
38
+ correct: B
39
+ explanation: When a motion path is present, the layer's frame.x and frame.y are overridden — the path is the trajectory. Deltas on frame.x conflict with the path and are ignored at render time. To control speed along the path, animate `motionProgress` (0..1). If you want a custom trajectory, you've already chosen the motion path — express speed/timing through motionProgress + easings, not through frame deltas.
40
+ - id: q4
41
+ question: 'Why do overlay layers (handle, page-number) carry `tags: ["overlay"]` instead of being plain text layers?'
42
+ choices:
43
+ A: For sidebar grouping in the studio's layer panel only
44
+ B: 'So the `applyRecipeToOverlay` translator can drop only overlay-tagged layers when re-running — user-authored layers and other pipelines'' tagged outputs are never touched'
45
+ C: 'For pricing tier enforcement — overlay layers count toward a quota'
46
+ D: 'Because TextVisual without the tag has reduced rendering quality'
47
+ correct: B
48
+ explanation: 'The `overlay` tag is part of the layer-tag isolation invariant. Each pipeline owns a tag namespace; re-running a pipeline drops only its own tagged layers. Without the tag, re-applying a recipe would either destroy the user''s authored text layers (bad) or fail to clear stale overlay layers (also bad). With the tag, recipes are idempotent and human edits survive re-runs.'
49
+ - id: q5
50
+ question: 'A `RefVisual` layer references `./lower-third.atelier`. You edit the referenced file in another editor. What does `atelier studio` do?'
51
+ choices:
52
+ A: 'Nothing — refs are resolved once at load time'
53
+ B: The studio file-watches every referenced file; on change, it re-renders the parent composition with the updated ref
54
+ C: It throws a stale-ref error and forces a reload
55
+ D: 'It requires the user to re-import the ref manually'
56
+ correct: B
57
+ explanation: '`RefVisual` is how Atelier expresses single-source-of-truth for reusable scenes (lock-ups, lower-thirds, carousel templates). The studio file-watches every referenced `.atelier` file and re-renders parent compositions automatically. Editing the source once updates every composition that uses it.'
58
+ - id: q6
59
+ question: 'A `page_number` overlay rule has `format: "{current:02d}/{total:02d}"`. You apply the recipe to a single-frame composition (no carousel context). What happens?'
60
+ choices:
61
+ A: The page-number layer is created with content `"{current:02d}/{total:02d}"` (template not rendered)
62
+ B: 'The applyRecipeToOverlay translator skips the page-number layer (logger warns) — page-numbers don''t make sense on standalone compositions; handle still gets added if configured'
63
+ C: It throws an error halting the whole apply-recipe pipeline
64
+ D: It picks defaults (`current=1`, `total=1`) and renders `"01/01"`
65
+ correct: B
66
+ explanation: 'Page-numbers require carousel context (`currentIndex`, `totalCount`). On single-frame applies, the translator skips the page-number layer with a logger warning — by design, since `01/01` on a standalone post is misleading. The handle overlay is unconditional and still gets added. When the carousel batch driver eventually applies the same recipe per page, it threads the current/total values into the apply context and the page-number renders correctly.'
@@ -0,0 +1,66 @@
1
+ id: Q-atel-401-state-machines
2
+ title: 'ATEL-401: State Machines & Motion'
3
+ description: 'Verify hierarchical states, transitions and the interaction layer, motion (springs, expressions, motion paths), and the preset-vs-template distinction.'
4
+ author: atelier
5
+ created: '2026-05-20'
6
+ updated: '2026-05-20'
7
+ tags:
8
+ - course
9
+ - atel-401
10
+ difficulty: intermediate
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'A state `hover` has `parent: default`. `default` animates `title:opacity` with a spring. `hover` adds its own `title:opacity` delta with a linear easing. What does `resolveFrame` use for the title''s opacity on `hover`?'
15
+ choices:
16
+ A: 'The child''s linear delta only — the parent''s spring delta for that layer+property is dropped entirely'
17
+ B: 'Both deltas, additively combined into one curve'
18
+ C: 'The parent''s spring, because parents take precedence over children'
19
+ D: 'A field-level merge: the child''s easing with the parent''s from/to values'
20
+ correct: A
21
+ explanation: 'Inheritance merges per layer+property group, not per field. Deltas are grouped by a `layer+property` key, and the most-derived state''s group wins outright. Because `hover` declares any `title:opacity` delta, the parent''s `title:opacity` group is dropped wholesale — there is no field-level patching. To keep the parent''s spring and add motion, the child must animate a different property or layer.'
22
+ - id: q2
23
+ question: 'You configured `atelier_configure_transition` from `default` to `hover`. Does the reverse transition (`hover` to `default`) now exist?'
24
+ choices:
25
+ A: 'Yes — configuring one direction auto-creates the symmetric reverse'
26
+ B: 'No — transitions are directional; you must configure `hover` to `default` separately'
27
+ C: 'Yes, but only if both states share a parent'
28
+ D: 'No — transitions cannot point back to a previously visited state'
29
+ correct: B
30
+ explanation: 'Transitions are directional. `default` to `hover` and `hover` to `default` are configured independently, and they usually differ in duration and easing — entrances ease-out, exits ease-in. There is no auto-symmetry.'
31
+ - id: q3
32
+ question: 'A layer has a `click` interaction whose action is `go-to-state` targeting `pressed`. No transition is configured from the current state to `pressed`. What happens on click?'
33
+ choices:
34
+ A: 'The click is ignored because the transition is missing'
35
+ B: 'It plays a default 300ms ease-in-out transition'
36
+ C: 'The runtime cuts directly to `pressed` frame 0 — the action fires, but there is no bridge to animate across'
37
+ D: 'The schema rejected the document at validate time, so the click never wired up'
38
+ correct: C
39
+ explanation: 'Interactions decide when; transitions decide how it looks getting there. A `go-to-state` action runs the configured transition for that state pair. With no transition configured, the action still fires but the runtime snaps to the target state''s frame 0. A click that appears to "do nothing" is almost always a missing transition for that pair, not a missing interaction.'
40
+ - id: q4
41
+ question: 'In a spring easing `{ type: spring, stiffness: 100, damping: 10 }`, what most directly determines whether the motion visibly overshoots and bounces?'
42
+ choices:
43
+ A: 'Damping relative to stiffness — low damping against high stiffness oscillates; high damping glides in with no overshoot'
44
+ B: 'The `mass` field alone — higher mass always adds bounce'
45
+ C: 'The delta''s frame range length'
46
+ D: 'Whether `position: start` or `position: end` is set'
47
+ correct: A
48
+ explanation: 'Bounce is governed by damping relative to stiffness. Low damping against high stiffness oscillates; raising damping at the same stiffness removes overshoot and the spring glides in. Mass adds inertia and lag, not bounce. `position` belongs to step easings, not springs.'
49
+ - id: q5
50
+ question: 'You want a layer to follow a defined curved trajectory and control its speed along that curve. Which approach matches Atelier''s model?'
51
+ choices:
52
+ A: 'Animate `frame.x` and `frame.y` with matching deltas; the engine infers the curve'
53
+ B: 'Use an expression on `frame.x` that returns the full 2D path'
54
+ C: 'Define the path as a preset and apply it to the layer'
55
+ D: 'Set a `motionPath` on the layer and animate the `motionPath.progress` property; `frame.x`/`frame.y` deltas would be overridden by the path'
56
+ correct: D
57
+ explanation: 'A motion path overrides the layer''s `frame.x`/`frame.y` — the path is the trajectory, so frame deltas conflict and are ignored at render time. You control position along the path by animating `motionPath.progress` (0 to 1); that delta''s easing shapes speed along the curve. `autoRotate` can additionally bank the layer into the tangent.'
58
+ - id: q6
59
+ question: 'What is the difference between a preset and a template in Atelier?'
60
+ choices:
61
+ A: 'A preset is a bundle of deltas applied to an existing layer; a template is a layer subtree instantiated (with variable bindings) into a new document'
62
+ B: 'A preset is for export; a template is for the studio editor'
63
+ C: 'A preset creates new layers; a template animates existing ones'
64
+ D: 'They are interchangeable names for the same reuse mechanism'
65
+ correct: A
66
+ explanation: 'Presets = delta bundles; templates = layer subtrees. `atelier_define_preset` then `atelier_apply_preset` stamp motion onto a layer that already exists (expanding `offset` windows relative to `startFrame`). `atelier_instantiate_template` resolves `{{variable}}` placeholders against bindings and creates a new document with the structure. Motion from one mechanism, structure from the other.'
@@ -0,0 +1,66 @@
1
+ id: Q-atel-501-video-pipeline
2
+ title: 'ATEL-501: Video Project Pipeline'
3
+ description: 'Verify the video pipeline — silence trimming with parametric cuts, Whisper transcription and the transcript edit family, caption generation, and the two re-run safety aspects (detected-vs-user-edited, layer-tag-isolation).'
4
+ author: atelier
5
+ created: '2026-05-20'
6
+ updated: '2026-05-20'
7
+ tags:
8
+ - course
9
+ - atel-501
10
+ difficulty: intermediate
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'In a CutEntry, which two fields are the immutable AI detections, and which two are the user-tunable overrides?'
15
+ choices:
16
+ A: 'paddingPre/paddingPost are detected; rawStart/rawEnd are user-tunable'
17
+ B: 'rawStart/rawEnd are detected (immutable); paddingPre/paddingPost are the user-tunable overrides'
18
+ C: 'All four are detected; the user edits the layers directly'
19
+ D: 'All four are user-tunable; nothing is detected'
20
+ correct: B
21
+ explanation: 'rawStart and rawEnd are the detected speech boundaries from silencedetect — you do not edit them by hand. paddingPre and paddingPost are your editorial overrides; the effective cut is [rawStart - paddingPre, rawEnd + paddingPost]. Keeping them in separate fields is what lets a re-detect regenerate the raw boundaries while preserving your padding.'
22
+ - id: q2
23
+ question: 'You run `atelier trim ./proj --loosen 120`. What happens?'
24
+ choices:
25
+ A: 'The 120 is treated as seconds and added to every paddingPre/paddingPost'
26
+ B: 'Every cut padding increases by 120 milliseconds (0.12s), composing with any existing per-cut overrides; padding still floors at 0'
27
+ C: 'Only cut #120 is loosened'
28
+ D: 'The raw boundaries shift outward by 120 frames'
29
+ correct: B
30
+ explanation: '`--tighten` and `--loosen` take milliseconds. 120ms is converted to 0.12s and added to the current padding of every cut, so it composes with overrides you already set rather than clobbering them. `--tighten` subtracts; padding is floored at 0 so you cannot drive a cut negative. Per-cut targeting is done with `--cut <index>`, not `--loosen`.'
31
+ - id: q3
32
+ question: 'What does `atelier transcript delete ./proj --word 7` actually do?'
33
+ choices:
34
+ A: 'Removes word 7 from transcript.json entirely'
35
+ B: 'Deletes the caption layer at index 7'
36
+ C: 'Sets hidden=true on word 7 — it stays in the transcript but is skipped when captions are built'
37
+ D: 'Lowers word 7 confidence below the caption threshold'
38
+ correct: C
39
+ explanation: 'delete is a soft, reversible cut. It sets hidden on the word; the word remains in transcript.json (so a future re-transcribe can re-recognize it) but groupIntoPhrases skips hidden words, so it never appears in a caption. To truly remove it you would have to edit it back in — by design, nothing is destroyed.'
40
+ - id: q4
41
+ question: 'A re-transcribe must decide whether to carry your correction forward to a fresh detection. Both conditions must hold for the override to be re-attached. Which pair?'
42
+ choices:
43
+ A: 'Same word index AND same segment'
44
+ B: 'Timing within tolerance (0.3s) AND the detected string is unchanged'
45
+ C: 'Same confidence value AND same language'
46
+ D: 'Identical text AND identical start time to the millisecond'
47
+ correct: B
48
+ explanation: 'mergeTranscriptWithExisting matches a fresh detection to an old entry on timing within 0.3s AND an unchanged detected string. Timing alone is not enough — if Whisper now hears a different word at that timestamp, carrying the old fix forward would graft it onto the wrong word. Requiring the detected string to match guarantees you are editing the same finding. (Cuts use the same idea with a 0.5s tolerance on both raw boundaries.)'
49
+ - id: q5
50
+ question: 'You added a word with `atelier transcript add` that Whisper never heard. On the next `atelier transcribe`, what happens to it?'
51
+ choices:
52
+ A: 'It is dropped — there is no matching fresh detection'
53
+ B: 'It is converted into a detected word with confidence 1.0'
54
+ C: 'It blocks the re-transcribe until you remove it'
55
+ D: 'It is preserved as a userAdded orphan and re-inserted into the right segment by its timing'
56
+ correct: D
57
+ explanation: 'A userAdded word has no fresh detection to match against by definition — Whisper did not produce it. The merge collects these orphans and re-inserts each into the segment that contains its timing. Your manual additions survive a re-transcribe just like your text corrections do.'
58
+ - id: q6
59
+ question: 'You run `atelier transcribe`, which rewrites caption layers. You also have hand-authored title layers and silence-trim layers in the same document. Why are your titles safe?'
60
+ choices:
61
+ A: 'transcribe only ever appends; it never removes layers'
62
+ B: 'Layer-tag isolation — the rewrite drops only caption-tagged layers; untagged (user-authored) and silence-trim-tagged layers fall into the preserved set and pass through unchanged'
63
+ C: 'Title layers are stored in a separate file from caption layers'
64
+ D: 'The schema locks all non-caption layers during a transcribe run'
65
+ correct: B
66
+ explanation: 'rewriteCaptionLayers partitions the document: everything that is NOT caption-tagged is preserved, and only the caption set is dropped and rebuilt. Your untagged titles and the silence-trim-tagged cut layers are untouched. Each pipeline owns exactly one tag namespace and cleans up only after itself — the invariant the test suite asserts directly.'
@@ -0,0 +1,66 @@
1
+ id: Q-atel-601-recipes
2
+ title: 'ATEL-601: Recipes, Overlays & Carousel'
3
+ description: 'Verify the Studio Recipe surface — what a recipe is and how partial recipes fall back, overlay_rules (handle + page_number, anchors, format templates), the CLI/MCP tooling and precedence, and the carousel batch driver.'
4
+ author: atelier
5
+ created: '2026-05-20'
6
+ updated: '2026-05-20'
7
+ tags:
8
+ - course
9
+ - atel-601
10
+ difficulty: intermediate
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'You author a recipe that sets only `caption_style.color`. What happens to the silence policy and the rest of the caption style when you apply it?'
15
+ choices:
16
+ A: 'The apply fails — a recipe must declare every Phase 1 section to be valid'
17
+ B: 'Omitted fields fall back to the code defaults (e.g. noise -30dB, font_size 84); the merge is shallow per section, so setting one caption field does not wipe the others'
18
+ C: 'Only the color is applied and all other styling is cleared to empty/zero'
19
+ D: 'The recipe silently does nothing because it is incomplete'
20
+ correct: B
21
+ explanation: 'Every section and every field is optional. Omitted fields fall back to the code defaults baked into renderRecipeWithDefaults, and the merge overlays your section over the defaults section — so setting one caption field leaves the rest of the defaults intact. Run `atelier recipe show <name> --with-defaults` to see the fully-resolved effective recipe.'
22
+ - id: q2
23
+ question: 'A recipe includes a `palette` section. You run `atelier recipe validate` on it. What is the result?'
24
+ choices:
25
+ A: 'FAIL — palette is not a recognized field'
26
+ B: 'PASS with no remarks — palette is fully implemented'
27
+ C: 'PASS, but with a forward-compat warning that palette is a reserved Phase 3 field that Phase 1 ignores'
28
+ D: 'It strips the palette section and rewrites the file'
29
+ correct: C
30
+ explanation: 'palette is one of the six reserved Phase 3 fields. They parse cleanly as opaque (unknown) values so recipes stay forward-compatible, but Phase 1 ignores their contents. validate emits a forward-compat warning (not an error) so you are not caught off-guard that the section is inert. Note overlay_rules was promoted to first-class Phase 1.5 and does NOT warn.'
31
+ - id: q3
32
+ question: 'A `page_number` rule has `format: "{current:02d}/{total:02d}"` and you apply the recipe to a single-frame composition via `atelier apply-recipe` (no carousel context). What renders?'
33
+ choices:
34
+ A: 'A page-number layer with the literal content `{current:02d}/{total:02d}` (template not substituted)'
35
+ B: 'The page-number layer is skipped with a warning; the handle is still applied if present'
36
+ C: 'It defaults to current=1, total=1 and renders `01/01`'
37
+ D: 'The whole apply-recipe pipeline throws and halts'
38
+ correct: B
39
+ explanation: 'applyRecipeToOverlay only emits the page-number layer when both currentIndex and totalCount are in its context. A single-frame apply has no carousel context, so the page-number is skipped with a one-shot warning — `01/01` on a standalone post would be misleading. The handle overlay is unconditional and still gets added. The carousel driver is what threads current/total in.'
40
+ - id: q4
41
+ question: 'In the four-corner anchor math, what does the `anchor` value `bottom-right` map to (with margin m on a canvas of width W and height H)?'
42
+ choices:
43
+ A: 'frame `{ x: m, y: m }`, anchorPoint `{ x: 0, y: 0 }`'
44
+ B: 'frame `{ x: W - m, y: H - m }`, anchorPoint `{ x: 1, y: 1 }`'
45
+ C: 'frame `{ x: W - m, y: m }`, anchorPoint `{ x: 1, y: 0 }`'
46
+ D: 'frame `{ x: W / 2, y: H / 2 }`, anchorPoint `{ x: 0.5, y: 0.5 }`'
47
+ correct: B
48
+ explanation: 'anchorToFrame pairs the frame position with a matching anchorPoint so the corner sticks. bottom-right places frame at `{ W - margin, H - margin }` and sets anchorPoint `{ 1, 1 }`, so the overlay anchors its own bottom-right corner at the canvas bottom-right-minus-margin point and grows inward regardless of text length. The anchor doubles as the rotation/scale pivot.'
49
+ - id: q5
50
+ question: 'How does `atelier_recipe_save` (MCP) differ from `atelier recipe new` (CLI), and why does it matter for an editing agent?'
51
+ choices:
52
+ A: 'They are identical; save is just the MCP alias for new'
53
+ B: 'new writes an always-valid scaffold template; save validates BEFORE writing and refuses to write an invalid recipe — so an agent persisting an edited recipe cannot land a malformed file on disk'
54
+ C: 'save writes without validating; new validates first'
55
+ D: 'new can only write YAML, save can only write JSON'
56
+ correct: B
57
+ explanation: 'recipe new scaffolds a starter template that is valid by construction and does not validate. atelier_recipe_save is how an agent persists an EDITED recipe, so it runs validateRecipe first and refuses to write if invalid, returning the issues instead. That validate-before-write guard is what keeps a malformed agent edit from ever reaching disk.'
58
+ - id: q6
59
+ question: 'The carousel driver loops over a sorted folder of N images. For image at loop position n (0-based), what index does it thread into the page-number overlay, and how is the output filename prefixed?'
60
+ choices:
61
+ A: 'index = n (0-based); filename prefixed with the raw loop counter, no padding'
62
+ B: 'index = n + 1 (1-based); filename prefixed with a zero-padded number of width max(2, digits-in-N)'
63
+ C: 'index is a random uuid; filename uses the original image name unchanged'
64
+ D: 'index = N - n (counts down); filename prefixed with the total count'
65
+ correct: B
66
+ explanation: 'The driver computes `const index = n + 1` (1-based) and passes `{ currentIndex: index, totalCount: total }` so the page-number renders i/N. The output name is zero-padded to `Math.max(2, String(total).length)` digits then carries the original stem, so files sort in deck order. Both the page-number content and the filename prefix derive from the same 1-based index, keeping what the viewer sees and how files sort in agreement.'
@@ -0,0 +1,66 @@
1
+ id: Q-atel-701-export
2
+ title: 'ATEL-701: Export & Delivery'
3
+ description: Verify the export surface — single-frame raster (export-image / still), vector (export-svg / export-lottie), video (render via FFmpeg, studio via WebCodecs), the two canvas backends, and choosing a format by destination.
4
+ author: atelier
5
+ created: '2026-05-20'
6
+ updated: '2026-05-20'
7
+ tags:
8
+ - course
9
+ - atel-701
10
+ difficulty: intermediate
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'You run `atelier export-image hero.atelier --out card.png --width 400` on a 1920×1080 document. What dimensions is `card.png`?'
15
+ choices:
16
+ A: '400×400 — the missing dimension defaults to match the width'
17
+ B: '400×225 — the height is derived to preserve the document''s aspect ratio'
18
+ C: '1920×1080 — --width is ignored without a matching --height'
19
+ D: '400×1080 — width is overridden, height stays at the document value'
20
+ correct: B
21
+ explanation: 'When only one of --width/--height is set, the other is derived to preserve the document aspect ratio. 1920×1080 is 16:9, so a width of 400 gives a height of 225. Override both and the renderer uses both verbatim (which can squash); override one and the aspect ratio carries the other.'
22
+ - id: q2
23
+ question: 'A teammate says "PNG export needs cairo, so I have to run brew install before I can export a thumbnail." Are they right?'
24
+ choices:
25
+ A: 'Yes — all Atelier rendering goes through node-canvas, which needs cairo/pango'
26
+ B: 'No — the single-frame raster path uses @napi-rs/canvas, which ships prebuilt binaries and works on a plain npm install'
27
+ C: 'Only on Linux; macOS bundles cairo with the OS'
28
+ D: 'Yes, but only the first time — the binary is cached afterward'
29
+ correct: B
30
+ explanation: 'The raster path (export-image, still --format png, carousel) uses @napi-rs/canvas — prebuilt platform binaries, no node-gyp, no system libraries. PNG export works the moment npm install finishes. The cairo/pango requirement belongs to the separate node-canvas package used by the video render path, not to single-frame raster.'
31
+ - id: q3
32
+ question: 'Why does `atelier export-lottie` have no `--frame` flag when `atelier export-svg` does?'
33
+ choices:
34
+ A: 'An oversight — both should accept --frame'
35
+ B: 'Lottie exports the whole animated document (the entire timeline as keyframes); SVG exports a single resolved frame'
36
+ C: 'Lottie always exports frame 0 and ignores everything else'
37
+ D: 'Lottie requires the document to have exactly one frame'
38
+ correct: B
39
+ explanation: 'Scope is the difference. export-svg serializes one resolved frame as an SVG snapshot, so it needs a frame. export-lottie walks the whole document''s deltas and writes them as Lottie keyframes — it is the animation, not a still — so a single frame would make no sense.'
40
+ - id: q4
41
+ question: 'You run `atelier render intro.atelier -f webm` to get a WebM file. What happens?'
42
+ choices:
43
+ A: 'It renders a WebM via FFmpeg'
44
+ B: 'The CLI render command only supports mp4 and gif; webm is rejected. WebM comes from the studio Export button (WebCodecs)'
45
+ C: 'It silently falls back to MP4'
46
+ D: 'It renders WebM but warns that quality may be reduced'
47
+ correct: B
48
+ explanation: 'The CLI render path supports mp4 and gif only — its format type is literally "mp4" | "gif". WebM is a studio-only output, produced in the browser via WebCodecs. The two video surfaces have different format reach: CLI via FFmpeg (mp4/gif), studio via WebCodecs (commonly mp4 and webm).'
49
+ - id: q5
50
+ question: 'A user can export a PNG from a fresh `npm install` but `atelier render clip.atelier` fails. Which two things does the video path need that the raster path does not?'
51
+ choices:
52
+ A: 'A GPU and a license key'
53
+ B: 'FFmpeg on $PATH and the node-canvas (`canvas`) package with its cairo/pango system libraries'
54
+ C: 'A newer Node version and more RAM'
55
+ D: 'An internet connection and a registered asset'
56
+ correct: B
57
+ explanation: 'Video from the CLI asks for two things stills never do: FFmpeg on $PATH (render shells out to ffmpeg -version first) and the optional native `canvas` package with cairo/pango/etc. The raster path uses prebuilt @napi-rs/canvas and no encoder. A clean PNG install does not imply a clean MP4 install.'
58
+ - id: q6
59
+ question: 'You are handing an animation to a front-end team to drop into a product screen, and they want a small, scalable file they can play in a runtime. Which export fits best?'
60
+ choices:
61
+ A: 'A folder of PNG frames'
62
+ B: 'Lottie JSON via `atelier export-lottie`'
63
+ C: 'An MP4 via `atelier render`'
64
+ D: 'An SVG via `atelier export-svg`'
65
+ correct: B
66
+ explanation: 'Lottie is the motion-design / runtime handoff format — vector-crisp, tiny, and playable in every major runtime with a few lines of code. SVG is a single still (no motion); MP4 is heavier and not vector; PNG frames are neither animation nor small. Watch export-lottie''s stderr warnings, though — an unsupported feature is reported there, not silently dropped.'
@@ -0,0 +1,66 @@
1
+ id: Q-atel-801-studio-loop
2
+ title: 'ATEL-801: Studio & the Live Agent Loop'
3
+ description: 'Verify the live agent loop — `atelier studio` as a local-first editor, the autosave save-race fix, image drop and the layer-type picker, the WebSocket bridge + MCP-over-WS transport, the source-tagged-mutation aspect, and the symbiotic human-agent editing pattern.'
4
+ author: atelier
5
+ created: '2026-05-20'
6
+ updated: '2026-05-20'
7
+ tags:
8
+ - course
9
+ - atel-801
10
+ difficulty: advanced
11
+ passThreshold: 0.7
12
+ questions:
13
+ - id: q1
14
+ question: 'When you switch files in the studio sidebar while an autosave is still pending in the 800ms debounce window, what stops the outgoing file''s bytes from being written to the newly-loaded file''s path?'
15
+ choices:
16
+ A: 'The save is dropped — switching files cancels the pending timer with no write'
17
+ B: 'The pending timer closure captures the target path AND serialized bytes, and `loadFile` flushes the pending save to its OWN path before switching'
18
+ C: 'The server rejects any write whose path differs from the currently-open file'
19
+ D: 'A CRDT merges the two files so neither is corrupted'
20
+ correct: B
21
+ explanation: 'This is the #cli-studio autosave-corruption bug. The original timer closed over the *doc* but read the GLOBAL current file at fire time — so editing A then opening B within 800ms wrote A''s bytes to B''s path. The fix is twofold: the timer captures both `pendingSavePath` and `pendingSaveContent` so a fired timer writes the bytes it was given to the file it was given; and `loadFile` calls `flushPendingSave()` at its very top to write any in-flight edit to its own path before switching. The extracted, testable version of this logic is the `SaveCoordinator` class; the inline browser app embeds the same manual debounce pattern.'
22
+ - id: q2
23
+ question: 'You drag two images from Finder onto the studio canvas of a freshly-scaffolded `welcome.atelier`. What happens to the welcome scaffold''s placeholder layer?'
24
+ choices:
25
+ A: 'Both images append on top of it; the placeholder stays visible underneath'
26
+ B: 'The FIRST dropped image replaces the layer whose id is `background` in-place; the second is unshifted onto the layers array (behind any overlays)'
27
+ C: 'Both images replace the placeholder, leaving only the second image'
28
+ D: 'The drop is rejected until you delete the placeholder manually'
29
+ correct: B
30
+ explanation: '`createImageLayerFromFile` is called with `replaceBackground: true` only for the FIRST image in a drop batch (`firstDrop` flips to false after the first). `buildImageLayer` swaps in-place only when `doc.layers[0]?.id === "background"` — a specific id contract, the welcome scaffold''s placeholder. Subsequent images are `unshift`-ed onto the layers array, which puts them at the bottom of the z-stack (the renderer paints index 0 first) so they sit behind any handle / page-number overlays.'
31
+ - id: q3
32
+ question: 'A property slider in the studio fires `onInput` continuously during a drag and `onChange` once on release. Why is the commit (undo snapshot) gated to `onChange` rather than `onInput`?'
33
+ choices:
34
+ A: '`onInput` is unreliable across browsers, so commits must wait for `onChange`'
35
+ B: 'So one slider drag produces exactly one undo entry — `onInput` does a cheap live-preview re-render with no snapshot, `onChange` commits and notifies the host'
36
+ C: 'Because `onInput` cannot read the slider''s value mid-drag'
37
+ D: 'To throttle autosave to once per second'
38
+ correct: B
39
+ explanation: 'The PropertyPanel splits the two events. `onPropertyPreview` (fired on `input`) does a cheap canvas re-render only — no undo snapshot, no host notification — so dragging gives live feedback. `onPropertyChange` (fired on `change`) snapshots undo history and notifies the host (which triggers autosave). The result: one drag = one undo, not one undo per pixel of slider travel. This was the slider-commit-flood bug, hidden while the client lived as an un-typechecked string.'
40
+ - id: q4
41
+ question: 'The studio binds the dev server to `127.0.0.1` and the WS upgrade handler re-runs an Origin check on `/bridge` and `/mcp`. Why does `/mcp` tolerate a MISSING Origin header while `/bridge` requires a strict same-origin match?'
42
+ choices:
43
+ A: '`/mcp` is unauthenticated by design and accepts any connection'
44
+ B: 'Non-browser MCP clients (e.g. Claude Desktop) send NO Origin header; rejecting them would make MCP-over-WS unreachable, so `/mcp` allows a missing Origin but still rejects a present-but-foreign one'
45
+ C: '`/bridge` carries secrets and `/mcp` does not'
46
+ D: 'The Origin check on `/mcp` is a bug that has not been fixed yet'
47
+ correct: B
48
+ explanation: 'WS upgrades bypass the connect middleware (Node fires `upgrade` before connect), so the bridge re-runs the Origin check itself via `isAllowedOrigin` / `isAllowedMcpOrigin`. `/bridge` is a real browser origin, so it demands strict same-origin. `/mcp` serves non-browser MCP clients that send no Origin — `isAllowedMcpOrigin` returns true for `undefined` but still rejects a present-but-foreign Origin (a tab on evil.com still cannot reach `/mcp`). The loopback bind to `127.0.0.1` remains the primary protection (^localhost-only).'
49
+ - id: q5
50
+ question: 'An MCP tool call mutates the shared `DocumentStore`, firing `onChange` with a source tag. Under the ~source-tagged-mutation aspect, which sources cause the bridge to broadcast an `llm:mutation` envelope to the browser?'
51
+ choices:
52
+ A: 'All three — human, llm, and system — so the browser always stays current'
53
+ B: 'Only `llm` — `human` (the browser is already current) and `system` (file-read hydration) stay silent to avoid echo loops and false attribution'
54
+ C: 'Only `human`, since human edits are the ones worth showing'
55
+ D: 'Only `system`, because that is the default tag'
56
+ correct: B
57
+ explanation: '`shouldBroadcastMutation(source)` returns true only for `source === "llm"`. Human edits arrive via `doc:patch` and are written with `source: "human"` so the filter skips them (otherwise the browser would echo its own edit back to itself). File-read hydration writes `source: "system"` — broadcasting it would fire a phantom "agent edited document" toast and a bogus undo entry on a plain file-open. Only genuine agent mutations broadcast, get persisted to disk, and surface a toast with a Cmd-Z undo affordance.'
58
+ - id: q6
59
+ question: 'In the symbiotic loop, an agent generates a batch of images by shelling out to an external generator (e.g. a Higgsfield CLI) and brings them into the open document. Which statement correctly describes Atelier''s role?'
60
+ choices:
61
+ A: 'Atelier runs the image generator in-process as one of its MCP tools'
62
+ B: 'Atelier owns no generation — the agent runs the external generator, then `atelier_import_images` ingests the results into the doc as ImageVisual layers + asset entries, which the human watches appear live and keeps or undoes'
63
+ C: 'The human must manually copy each generated file into the project before Atelier sees it'
64
+ D: 'Atelier regenerates the images itself to guarantee consistency'
65
+ correct: B
66
+ explanation: 'Atelier is an ingest-and-compose tool, not a generator. The agent runs the generation step in chat; `atelier_import_images` brings the results into the open document; the live bridge broadcasts the mutation so the human watches the layers appear and keeps or Cmd-Z-undoes each one; recipes and overlays compose the set; and `atelier carousel` exports it. Generation lives outside Atelier — the human and agent co-edit the same single-store document.'