Skip to content

feat: render and preview a composition file, with no project to maintain - #1

Merged
SimonErich merged 63 commits into
mainfrom
feat/single-file-cli
Jul 16, 2026
Merged

feat: render and preview a composition file, with no project to maintain#1
SimonErich merged 63 commits into
mainfrom
feat/single-file-cli

Conversation

@SimonErich

Copy link
Copy Markdown
Owner

A Fluvie project is now a composition file, an assets/ folder, and a
pubspec. No app, no main.dart, no capture harness, no registry, no
platform folders.

fluvie init --name my_clip
fluvie preview ./lib/my_clip.dart              # live, hot reloads on save
fluvie render ./lib/my_clip.dart --out out.mp4

Why

Two capture harnesses existed and had silently diverged. The one fluvie init
scaffolded hardcoded NoMediaResolver and read 6 of the 14 dart-defines, so it
accepted --aspect/--quality/--poster and ignored them, and any composition
declaring an Image or a Clip threw in capture. The real harness lived in an
example's test tree where nothing could depend on it.

renderVideo in package:fluvie is now the one capture entry. It derives
geometry, media, audio, captions and reactive audio from the Video itself; the
host supplies only pumpWidget, pumpFrame, setViewSize and runAsync.
buildCaptureShell, ImageResolverScope, SnapshotCaptureScope and
MediaRepository all stay private.

Notable

  • The CLI owns the pubspec's flutter: assets: block and re-derives it per run.
    Flutter enumerates a declared asset directory non-recursively, so a bare
    assets/ silently misses assets/images/foo.png with no build error.
  • Compositions live under lib/. A relative import cannot escape a package, so
    the preview app (which lives outside the project) can only reach one through a
    package: URI.
  • The preview app is cached in ~/.cache/fluvie/preview/, outside the project,
    because a nested pubspec breaks resolution inside a pub workspace. It copies
    dependency_overrides across, since pub applies them only from a resolution's
    root.
  • WebM/VP9 clips work, including alpha. The probe derives the frame count
    Matroska does not store, and vp9-with-alpha decodes through libvpx-vp9. Clip
    fps now comes from the declared rate rather than frames-over-duration, which
    read 29.86 for a 30fps WebM.
  • Failed capture output keeps its head as well as its tail; it discarded the
    Dart compile error printed first.
  • fluvie_server's Playground no longer needs examples/gallery on the host.

Breaking

fluvie init no longer scaffolds an app, a harness or a registry, and is no
longer interactive. fluvie render <key> still works for a project that kept
its harness. See documentation/reference/migration.md.

Verification

melos run format, analyze --fatal-infos, lint, all Dart and Flutter
suites, the doc snippet and fence gates, and the website build are green. All 30
goldens pass with no PNG touched. Coverage passes the 97% gate on every package
(fluvie_cli 99.42%). init -> render -> preview verified end to end on
scratch projects outside any workspace, with pixels inspected.

…rkspace

Add packages/fluvie_presenter (barrel, test harness, obers_ui git dep) and
the apps/slides shell to the pub workspace, the coverage gate list, and the
goldens scope. Seed PROGRESS.md with the build plan and decision log.
…me clock

LivePlaybackController maps elapsed wall time to frames (play, pause, exact
seek, hold, playRange with a completing future, rate with no-jump rebasing)
over the same RenderController capture uses; LivePlayer owns the Ticker,
mounts preview mode, and republishes frames through RenderControllerScope.
Both export from the barrel.
introspectTimeline(video) walks the declared scene trees (the same walk the
media collectors use, now sharing declaredChildren) into registrations,
resolves them through buildVideoPlan, and returns scene bounds plus
per-element windows and animation spans as plain FrameSpan values, keyed by
anchor, widget key, and walked widget instance. CollectibleChildren and the
tree walkers join the barrel so consumers can extend the walkable surface.
LiveScenePlayer wraps fluvie's LivePlayer behind one presenter seam: it
introspects the scene's absolute bounds and plays exactly that segment,
holding the last frame. FluvieSlides carries the minimal config surface and
stages the first scene flat. The slides app presents a demo deck, and the
fluvie live-playback + introspection API gets its documentation page with
compiled snippets.
Stop marks its children as a build step: absent before the step, then
mounted on a FrameRebase so the authored entrance plays from the reveal
moment (or past it, for the settled state back-navigation lands on) while
ambient time keeps flowing. Outside a presentation it is a transparent
passthrough, and it implements CollectibleChildren so fluvie renders of the
same deck still see the stopped media.

fluvie side: LocalMotionScope detaches a subtree from the composition
registrar (CompositionRegistrarScope.none), so live content can mount and
resolve locally at any point of playback — documented in live-playback.md.
compileSlidePlans walks each scene for Stops (document order, explicit
order override, duplicate orders rejected), measures every step's entrance
settle from the introspected enter spans (scene-relative, nested stops
excluded from their parents), and pre-validates that stopped elements use
only locally-resolvable triggers. AnimationIntrospection now carries the
declared trigger so consumers can tell composition-level placement apart.
A Riverpod notifier holding (slide, step) plus how the position was reached:
forward advances play entrances, backward moves and jumps land on held
states. next/back walk the flat position order across slides; jumps clamp to
the deck. The compiled plans arrive through slidePlansProvider.
SlideView mounts each scene as its own single-scene composition stretched to
the slide horizon on a free-running clock: forward advances reveal stops at
the current frame (entrances play from that moment), backs and jumps land on
settled states, ambient loops never pause. Slide changes play the authored
boundary transition mapped to a presenter-level blend (crossFade, slide,
wipe, zoom; flag to cut). FluvieSlides compiles the deck, owns the provider
scope, and re-mounts per deck. Goldens pin a slide at step 0, a middle step,
and its final step; a byte-comparison test pins back-navigation parity.
PresenterShell letterboxes the slide into any viewport on a flat stage,
mounts its own obers_ui theme scope (PresenterTheme carries the tokens plus
the stage color), and draws the minimal HUD: a slide counter and a flat
progress line over the whole position order, both toggleable. FluvieSlides
provides missing ambient scopes so the presenter can be someone's whole
runApp. Goldens pin the letterboxing at two viewport aspects.
PresentationShortcuts binds the Keynote and reveal.js muscle-memory set
(arrows, Space and Shift+Space, PageUp/PageDown for remotes, Enter, Home and
End, digits-then-Enter jumps, F and F5, Esc with jump-buffer precedence, O,
S, B and period, W, H) plus tap-to-advance and horizontal swipes, all onto a
PresenterHandlers value so tests record calls without pumping the shell. It
grabs focus on mount so keys work immediately, on the web too.
…eens

FullscreenController picks its platform by conditional import: the browser
Fullscreen API on the web, window_manager on desktop behind a testable
bridge seam, SystemChrome immersive modes on mobile with tracked state, and
an honest no-op elsewhere. BlankScreenNotifier holds the B/W cover over the
stage; the position underneath holds while it is up.
PresenterShell binds the input surface to the controller and chrome state:
navigation clears the blank screen (then the overview) before it moves, Esc
peels overlays before leaving fullscreen, F toggles, digits jump, H hides
the HUD. startFullscreen requests fullscreen post-frame, the FluvieSlides
flags seed sidebar and notes visibility, and a custom PresenterTheme paints
the stage. Goldens pin the black and white covers.
SlidePreviewService queues renders behind a concurrency cap, deduplicates
in-flight requests, caches into an LRU with invalidation, notifies when a
preview lands, and can pre-generate the whole deck. The pixels come from
SlidePreviewFrame (a slide settled on a held clock — the deterministic
still) rendered on PreviewRenderHost, a hidden one-at-a-time stage painted
beneath the opaque presenter stage and captured through its repaint
boundary.
SlideSidebar shows one tile per slide — the cached preview once it lands, a
flat placeholder until then — with the current slide highlighted and kept
in view. Click jumps to the slide at its base step. T toggles it (N is
wired for the notes panel ahead), and the shell now lays the hidden preview
host, stage, sidebar, and overlays out in one paint order.
O opens a grid of every slide over a dimmed stage, reusing the sidebar's
preview cache so nothing renders twice; picking a slide navigates and
closes, Esc just closes. Goldens pin the sidebar (ready and placeholder
tiles, current slide marked) and the grid.
SpeakerNotes carries the prose and the glanceable highlight bullets. It
renders nothing and takes no space; in a scene it is the slide's default,
inside a Stop it belongs to that step. The structural walkers see it in
both places.
compileNotes resolves SpeakerNotes into SlideNotes aligned with the
compiled plans: scene notes are the default every step inherits; a step
note replaces the text (when it has one) and appends its highlights; a
nested stop keeps its note for its own later step; several notes in one
scope merge in document order. One source of truth for the notes panel and
the Phase 6 speaker window.
NotesPanel shows the position's prose on the left and the highlight bullets
down the right, follows navigation as steps reveal, and hides behind N or
the showNotes flag. FluvieSlides compiles the notes next to the plans and
provides both; the shell lays the panel under the stage row. A golden pins
the panel with text plus highlights and the empty state.
PresentationSyncChannel carries JSON-safe SyncMessages (position updates
and navigation requests) between the presenting and speaker windows: a
BroadcastChannel on the web, an in-memory pair in tests, and an honest null
where no transport exists. Unknown payloads fail loud with a
FormatException so version-skewed peers never desync silently.
SpeakerNextPreview renders what the next input produces — the next step of
this slide, or the next slide's base step — as a settled still on a held
clock, and says 'End of deck' instead of guessing at the end. SpeakerView
lays it out with the notes, the highlight rail, the slide indicators, and a
tick-counted elapsed clock that stays testable on the fake test clock.
S opens the speaker view: a popup onto the speaker route on the web, an
app-supplied launcher on desktop (the provider is the seam), the in-app
notes panel on mobile, and an open-this-URL instruction card whenever no
window can open (cleared like every overlay). FluvieSpeaker is the second
window's root: same compiled deck, inputs sent as navigation requests when
a channel exists, self-navigating without one. A golden pins the speaker
layout.
SpeakerSyncBinding glues the presentation scope to the channel: the
presenting window applies navigation requests through its controller (so
forward moves keep forward semantics), broadcasts every position change,
and announces itself for late-joining speakers; the speaker end follows
instantly and never echoes. FluvieSlides and FluvieSpeaker now pin their
whole presentation state inside their own provider scope, so host-app
scopes above cannot capture it and a deck swap resets everything together.
The app boots to a flat obers_ui picker of the bundled tutorial decks or
opens a local .fluvie file (file_picker + the fluvie spec loader, with
friendly parse errors). Presenting mounts FluvieSlides; on the web the
speaker route (#/speaker) resolves the same deck through a localStorage
handoff and mounts FluvieSpeaker, so the S popup presents in lockstep.
Plain slides, builds with Stop, speaker notes with highlights, a
media-heavy deck (bar chart, code, typewriter), and one full end-to-end
talk using most of the tool. Every deck compiles into presentable plans
under test, and a golden pins each one's representative slide so the
tutorial cannot rot. Chart reveals are spelled out absolutely — the deck
rule the docs teach.
Fifteen pages in the package's documentation/ folder: the map, four
getting-started pages (five lines running, builds, notes, inputs), five
guides (speaker window, sidebar and overview, fullscreen and blanking, web,
deploying), three advanced pages (the stepping clock model, custom
navigation, performance and previews), and the shortcuts + FAQ reference.
Every dart fence is a compiled #docregion from the tutorial decks or the
new presenter snippets file, and the melos docs:snippets / docs:lint gates
now cover the presenter docs alongside the root docs.
The melos-driven jobs already pick the new workspace members up; this adds
the presenter lcov to codecov and a release web build of apps/slides so the
just-visit-and-present path cannot silently break.
…ounts them

An instant slide change disposed the outgoing clock synchronously, but the
old player's ticker still fires once before the rebuild lands (transient
frame callbacks run before build), hitting a disposed RenderController.
Replaced clocks now hand off to a post-frame callback.
…deck

Compile throughput, preview generation, stepping cost, and the preview
cache bound, tagged render so the default suite skips it. This harness is
what caught the retired-clock race.
…b guide

Also spells out in the input-map dartdoc why the key handling is one
Focus.onKeyEvent switch instead of Shortcuts and Actions: the digit jump
buffer and the Shift+Space chord are stateful across key events.
The input-map mechanism, the absent playback-state sync message (the hold
model has no play or pause to share), and index-keyed previews with
deck-swap invalidation, plus the perf harness results.
A remounted LivePlayer hands the controller a fresh ticker whose elapsed
starts over at zero; handleTick treated that as time before the epoch and
seeked back toward frame 0, replaying whatever had already played. A
backward elapsed jump now rebases at the current frame.
When the boundary blend settled, the incoming stage moved from inside
SlideTransitionBlend to being the root child; the structure change
remounted the whole stage, its player restarted the ticker, and the slide
replayed its entrance from frame 0 (visible on every crossFade, slide,
zoom, or wipe). Each mounted slide now anchors its stage with a GlobalKey
so it reparents instead of remounting.
Five small ghost buttons top-right (sidebar, notes, overview, speaker
window, fullscreen) reusing the exact handler set the keyboard fires, with
the shortcut in the tooltip. The shell now brings its own Overlay and a
fallback density scope, so the buttons render standalone too; H hides the
strip with the rest of the HUD.
An accent border and a soft tint on hover, plus a click cursor — in the
sidebar and the overview grid alike (one tile serves both).
…ullscreen

PresenterHandlers grew its one optional intention: onClose (through
FluvieSlides). When wired, the strip shows a close button set apart on the
right. The strip also now tracks the presenter's fullscreen transitions:
four seconds after entering it fades out, hovering its corner brings it
back, and five seconds later it hides again — windowed presenting keeps it
always on.
…luvie tour

The video deck embeds real HTML video players (with controls and sound) as
plain slide children; fluvie's Clip belongs to the file renderer and stays
silent in live playback, so a talk gets an actual player instead. The mp4s
under assets/videos/ are gitignored — drop your own in. The tour deck
explains fluvie in fourteen slides, every one carrying speaker notes that
describe exactly what is on screen, so the speaker window can be verified
slide by slide.
The app hands FluvieSlides an onClose instead of overlaying its own X, and
the picker scrolls now that seven decks outgrow a small window.
The birthday clip in the deck was a raw source asset with no audio track
at all — no player could make it sound. The deck now embeds the rendered
video (fluvie mixed the music in at encode time), and the players carry
autoplay + playsInline: Chromium starts them sounding once the page has
been interacted with, Firefox wants the site allowed once, and the
controls remain the fallback either way.
The spec learns free placement: a reserved id (preserved verbatim, never
minted by fluvie — identity policy belongs to the editing tool) and a
transform in canvas fractions (x/y anchor point, optional w/h, rotation,
anchor). A new Placed element lays the fractions out through one public
mapping (Placement.rectFor), buildElement wraps transformed elements in it,
and scenes carry a layout marker (canvas or stack) with stack as the
default so version 1 files render unchanged. Schema, validation, and the
Dart printer move in the same commit.
A first-class top-level editor key: preserved verbatim through the
round-trip, deliberately open inside, and stripped from digest() so
annotating a document never invalidates render caches — only
render-affecting content moves the digest. The conformance corpus pins the
lockstep: every fixture must validate clean, re-serialize canonically,
keep a stable digest, build a Video, and print to valid Dart.
Skeleton, barrel, analysis and golden config mirroring the presenter's;
wired into the coverage gate and the goldens scope. publish_to: none while
obers_ui stays a git pin.
The document is the .fluvie JSON itself behind an immutable wrapper: typed
mutations deep-copy, revalidate through the spec parser, and return a new
document; loading mints stable element ids. Sealed EditorCommands are the
only way a deck changes; DocumentHistory rides obers_ui's OiUndoStack with
the editor's merge policy in the callback, so a drag's stream of
transforms undoes as one step that lands before the drag.
CanvasViewport is the editor's own camera (programmatic zoom commands,
wheel-to-point zoom, one shared screen-to-canvas mapping — OiPinchZoom is
gesture-only); SlideDeriver memoizes single-scene videos per slide content;
EditorCanvas holds the derived slide at its settled frame and matches the
presenter's render byte for byte. The slides app picker gains an Edit
section: the bundled demo spec or your own .fluvie, read-only for now.
Placed implements CollectibleChildren, so timeline introspection and the
media collectors reach placed elements instead of treating the wrapper as
a leaf.
Epic 2.1. SceneGeometry resolves every element's shape through the one
Placement mapping (rotation-aware, separating-axis marquee, z-ordered
point hits) — never the widget tree. SelectionController holds the ids
(not undoable, Figma-style); CanvasInteraction wires click / shift-click /
empty-drag marquee / Space-pan / hover over the shared camera, and
SelectionChrome paints the boxes and marquee in viewport space. The
fluvie barrel now exports decodePlacement so the editor shares fluvie's
single geometry codec.
A richer worked example: an intro card, one scene per day, and an outro,
over a music bed. Each day is annotated with its number and date and groups
that day's media — photos and inline video clips together — into a
scattered, staggered collage, all data-driven off one day list through a
single tile builder. Registered as 'fluvie render days_recap'. Drop real
photos and clips into the gitignored assets/days_recap/ and point the day
list at them to fill it with your own content.
A live preview mounted no media resolver, so `Clip` and `Background.video`
had nothing to paint and fell back to their placeholder. Only images
resolved, through the async `Image.asset` fallback.

PreviewMediaScope runs the render's own pre-pass (preResolveAll, then
preResolveCompositionClips) and mounts the same ImageResolverScope, so
preview paints through the capture painters rather than a second,
divergent decode path.

Desktop's resolver streams clip frames, and that window is filled only by
the capture loop, which a preview does not have. Preview therefore passes
streamClipFrames: false and decodes every planned frame up front, bounded
by maxClipEdge (720 by default) so a full-HD clip does not hold hundreds
of megabytes of decoded frames. The proxy moves resolution only: a clip
still lays out at its source size, so preview geometry matches the render.

Failure is never fatal. No ffmpeg on PATH, no WebCodecs decoder wired, or
an unreadable source leaves the scope unmounted and the composition still
shows, with each media element on its own fallback, reported through
FlutterError in debug so the cause is never silent.
A capture harness had to assemble the render itself: pre-resolve media,
extract clip frames, rasterize snapshots, parse captions, analyse reactive
audio, build the capture shell, stage the audio mix, then loop the frames.
The pieces to do that live under src/, so the harness fluvie init writes
could not reach them and hardcoded NoMediaResolver instead. A composition
that declared an Image or a Clip threw FluvieRenderException in capture,
and the only fix was importing package:fluvie/src/.

renderVideo owns that sequence. It takes a Video and derives everything
from it, so a caller supplies no registry, no media list, and no geometry.
The host contributes only what it alone can: pumpWidget, pumpFrame,
setViewSize, and runAsync to escape fake async for real IO.

With resolver null it builds the platform resolver through resolverScope,
but only when the composition actually declares media, so a media-less
render still needs no ffmpeg. buildCaptureShell, ImageResolverScope,
SnapshotCaptureScope and MediaRepository all stay private: the public
surface is one function.

The untrusted size bound moves with it and keeps reading
FLUVIE_BLOCK_FILE_SOURCES inside fluvie's own lib/, since a define is
substituted into the library that names it and generated harness text
cannot carry one.

parseAspect/parseQuality/parseExportFormat/parsePosterTime and
writeRenderProgress move onto the pipeline surface too, so every harness
reads a --dart-define the same way instead of each parsing its own.
…ns on

Covers what a caller depends on and cannot see: the declared size wins
unless an aspect overrides it (and an aspect re-derives from the longer
declared edge, never a lossy VideoSize->Aspect map), frameCountOverride
shortens the capture, a media-less composition renders with no resolver
and therefore no ffmpeg, and two renders of the same video are
byte-identical.

The cache report is exercised cold then warm against a temp cache root, so
it asserts real hits rather than that the callback fires.

assertRenderWithinBounds is tested directly, including the reason it
checks each axis before any multiply: a 2^31 x 2^31 canvas wraps its
byte product to a small value in 64-bit and would otherwise slip past.
Two harnesses existed and had silently diverged. The gallery's was the
real one (media, audio, clips, snapshots, aspect, quality, poster,
progress) but lived in an example's test tree where nothing could depend
on it. The one `fluvie init` scaffolded was a weaker fork: it hardcoded
NoMediaResolver, read 6 of the 14 dart-defines, and so accepted
--aspect/--quality/--poster and silently ignored them. No test guarded the
drift.

runCaptureHarness is now a thin adapter over fluvie's renderVideo: it
supplies the flutter_test mechanics and the gallery's offline fakes, and
nothing else. The CLI's generated harness calls the same function with the
platform's real resolver, so the two cannot drift again.

CompositionEntry is now {key, video}. Its width, height, fps, frameCount
and mediaSources were all derivable from the Video, and holding a
`Widget Function()` instead of a `Video Function()` is what forced
harness_media to look each entry up by lesson key to find the Video it
could not walk. That coupling is gone.

The demo composition becomes a real Video rather than a raw VideoScope
tree, so every entry renders through one path. Its assertions are
behavioral (pixels differ between frames and repeat), and they still pass.

Two behavior changes, both intended. A track-bearing composition now
stages its audio whether or not its key names a lesson, because the mix is
derived from the Video. And a clip's embedded audio joins the mix, which
the gallery harness never did.
…maintain

A Fluvie project was a whole Flutter app: a previewer main.dart, a capture
harness, a hand-maintained registry map, platform folders, and a pile of
dev dependencies. None of that is the video.

A project is now a composition file, an assets/ folder, and a pubspec.
`fluvie render ./lib/my_video.dart --out out.mp4` renders it and
`fluvie preview ./lib/my_video.dart` runs it live with hot reload. Both
generate what they need per invocation, so there is nothing to keep in
step. The entry point is a top-level `Video build()`; --entry names
another.

This reuses the mechanism the Playground already ships rather than
inventing one: stage a generated harness that statically imports the
user's file, and point captureTestArgs(harnessPath:) at it. Render needs
no hidden project at all, because `flutter test` requires only a pubspec
in its working directory. Only preview needs a real app, because
`flutter run` is hard-gated on the platform directory existing.

The CLI owns the pubspec's `flutter: assets:` block and re-derives it every
run. Flutter enumerates a declared asset directory with a NON-recursive
listSync, so a bare `assets/` bundles only its top level: the first
assets/images/ a user creates would be silently missing at runtime with no
build error. Enumerating every subdirectory is what "the CLI manages the
project" has to mean.

The preview app lives in ~/.cache/fluvie/preview/<hash>/, outside the
project, because a nested pubspec breaks resolution inside a pub workspace
and this repo is one. That costs two things, both handled: the user's
dependency_overrides are copied in (pub applies overrides only from a
resolution's root, so a contributor path-overriding fluvie would otherwise
preview against pub.dev's copy and see different pixels), and assets are
symlinked rather than reached by a `../` asset path, which Flutter accepts
and then silently writes outside the bundle.

A composition must live under lib/. A relative import cannot escape a
package, so the preview app can only reach it through a package: URI.
Rendering works either way; preview says so plainly rather than emitting an
import the compiler cannot resolve.

resolveProjectDir now probes for a pubspec depending on fluvie instead of
for a committed harness, which no longer exists. The probe is
section-aware: fluvie_cli's own `executables: fluvie:` entry would
otherwise match and resolve the CLI package as the project.

The frame cache is off for a file target unless --cache. The render digest
keys on the config and the composition key, never the composition, so an
edited file with the same size and frame count would replay stale frames.
fluvie_server already worked around this for code renders.

Failed capture output now keeps its head as well as its tail. It kept only
the last 4 KiB, which discarded the Dart compile error printed first, and
"my composition does not compile" is the dominant failure for a file
target.

The capture subprocess now gets the gate-resolved ffmpeg on PATH. It
extracts clip frames itself, so a downloaded ffmpeg encoded fine while
every Clip failed to decode.

fluvie_server's Playground now stages through the same shared harness, so
it no longer needs examples/gallery on the render host, and two verbatim
copies of the harness template delete.

set_version.sh missed apps/, so a bump left apps/slides pinned and made the
whole workspace unresolvable. It also built its package list from directory
names, so a leftover artifact directory sharing a pub.dev package's name
had that third-party constraint rewritten to our release version.

BREAKING: `fluvie init` no longer scaffolds an app, a harness, or a
registry, and is no longer interactive (--yes, --path, --render are gone).
`fluvie render <key>` still works for a project that kept its harness.
See documentation/reference/migration.md.
… no record

Every getting-started path taught a workflow that no longer exists:
`fluvie init --yes`, a scaffolded capture harness, a registry, and
`flutter run` for preview. start-a-project is rewritten around the real
one, and render/preview/init references follow.

Two claims were wrong rather than stale. The CLI README told users they
need FFmpeg on PATH, contradicting installation.md's auto-download, and it
never documented `fluvie list` at all.

The decision log gets row 29 and, while there, the nine named decisions the
code cites (D-CLI, D-CaptureShell, D-Audio, D-Mix, D-Reactive,
D-CaptionsRender, D-Snapshot, P12-SNAP, AUDMIX-WIRE) that existed only as
code comments and appeared in no markdown. D-CLI is the decision this
change supersedes and it had no written record anywhere.

The docs sidebar gains rendering-surface and managing-ffmpeg, which were
built and reachable by URL but linked from nowhere.

Versions go to 0.3.0 across the workspace.
…king

Coverage sat at 93.65%, under the 97% gate, but most of that was an
artifact: init_support_test still named symbols the dead-code cleanup
removed, so it failed to compile and took its whole suite down with it.
Repointing it at compositionSlug alone restored 97.45%.

The genuinely untested behavior was the preview app's wiring, and it is the
part with the least obvious failure mode. The dependency_overrides copy now
has tests for an override from the project's own pubspec, from
pubspec_overrides.yaml, and from a workspace-root ancestor, plus a relative
override path becoming absolute. Without that copy a contributor who
path-overrides fluvie previews against the published package while their
renders use the local one, so the two disagree on pixels with nothing to
point at.

Also covered: a stamp match reuses the app rather than re-running
flutter create (which is not idempotent over an existing tree), a
composition outside lib/ fails with a stated reason instead of an
unresolvable import, the Windows copy fallback for asset symlinks, and the
capture subprocess getting the gate-resolved ffmpeg on PATH.

One of the new tests passed for the wrong reason and is fixed: it asserted
`contains('path: /tmp/fluvie')`, which substring-matched the project's own
path dependency because the temp directory name starts with fluvie, so it
would have passed whether or not the override file was ever read.

Coverage is now 99.42%. The remaining uncovered lines are the
Platform.isWindows/isMacOS host branches and the interactive
Process.start of flutter run.
@SimonErich
SimonErich merged commit eeded7a into main Jul 16, 2026
16 of 18 checks passed
@codecov-commenter

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants