Plan: Phase 5 spec + directives round 4 (M10-M17, lanes E/F/G)

plan/20-phase5.md is the phase-5 canonical spec: M10 beats/onsets,
M11 auto-director, M12 cinematic export (lane E); M13 anchor manager +
friend tags, M14 photo mode, M15 moment FX (lane F); M16 server-side
camera paths, M17 static memory-capsule export (lane G). foundation2
freezes the new contracts (beats.json, camPath JSON, paths table,
PATCH anchors, __F4D_API_BASE__ / manifest.capsule, transport/scene3d
hooks) and pre-wires all UI so parallel lanes never touch shared files.

DIRECTIVES Round 4 records the fresh-eyes upgrades (FFT caching,
WebAudio master clock, spatial audio; suite floor now 121), ratifies
the additive contract amendments, and sets run order:
foundation2 -> E, G, F (own worktrees, mandatory) -> integration2
(real-footage field test, v0.2.0 proposal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-17 17:08:44 +10:00
parent db42c42932
commit 545f7a347d
7 changed files with 301 additions and 0 deletions

View File

@ -3,6 +3,12 @@
Parking lot for post-v0.1.0 extensions (spec §5 future-extensions). Not committed work — a place Parking lot for post-v0.1.0 extensions (spec §5 future-extensions). Not committed work — a place
to capture direction so the prototype's scope stays honest. Roughly ordered by payoff-to-effort. to capture direction so the prototype's scope stays honest. Roughly ordered by payoff-to-effort.
> **Status 2026-07-17:** splatting ✅ shipped (see `modelbeast-crossover.md`); spatial audio ✅
> shipped (🎧, WebAudio master clock). Cinematic export → **M12**, multi-modal audio analysis →
> **M10/M11**, anchor manager → **M13**, path persistence → **M16** — all now specced in
> `plan/20-phase5.md`. Still parked here: realtime ingest, object/artist tracking,
> per-project workspaces, sync-graph visualization, per-moment splats (crossover doc §3).
## Neural rendering — Gaussian Splatting ## Neural rendering — Gaussian Splatting
Replace the sparse COLMAP point cloud with a **3D Gaussian Splatting** model for photorealistic Replace the sparse COLMAP point cloud with a **3D Gaussian Splatting** model for photorealistic
free-roam instead of a dot cloud. COLMAP poses + images already feed splatting trainers free-roam instead of a dot cloud. COLMAP poses + images already feed splatting trainers

198
plan/20-phase5.md Normal file
View File

@ -0,0 +1,198 @@
# Phase 5 — Friends, Director's Cut & Capsules (M10M17)
Canonical spec for the phase-5 milestones. **M0M9 live in [`../OPUS_BUILD_INSTRUCTIONS.md`](../OPUS_BUILD_INSTRUCTIONS.md)
and every contract frozen there still applies** (timebase, DB schema, pose convention,
classifier contract, synthetic fixture). Standing protocol (status files, directives loop,
evidence discipline, worktrees) is unchanged from [`README.md`](README.md); the newest round of
[`DIRECTIVES.md`](DIRECTIVES.md) always wins.
## Context — what shipped since v0.1.0 (read before planning anything)
- **Splat rendering:** `GET /api/splat` + viewer `DropInViewer` with point-cloud fallback
(`docs/modelbeast-crossover.md`).
- **Live capture:** `FESTIVAL4D_CAPTURE=1` mounts `/capture` (README).
- **WebAudio master clock:** `t_global` derives from `AudioContext.currentTime`;
`GET /api/audio/{id}` serves a lazily-extracted stereo AAC per video; all `<video>` elements
are muted picture-only, with graceful fallback to the old `performance.now()` clock
(`frontend/src/transport.js` header comment is the reference).
- **Spatial audio (🎧):** per-camera HRTF `PannerNode`s at live poses; listener follows the
viewer camera (`transport.updateSpatial`, fed by `scene3d._updateAudio`).
- **Sync solver:** pairwise GCC-PHAT now reuses one rFFT per signal (`audio_sync.pairwise_offsets`).
- **Suite floor is 121 passed.** It only goes up.
## Phase map
```
Phase 5a (serial, ONE agent) Phase 5b (parallel, one agent per lane) Phase 5c (serial, ONE agent)
┌───────────────────────┐ ┌ lane/e-director ── M10 M11 M12 ─┐ ┌───────────────────────────┐
│ foundation2 │ ├ lane/f-social ──── M13 M14 M15 ─┤ │ integration2 │
│ contracts + stubs + │──merge──┤ ├──merge──│ full-system pass + │
│ UI wiring points │ └ lane/g-capsule ─── M16 M17 ─────┘ │ real-footage field test │
│ branch: foundation2 │ (independent — merge in any order) │ branch: integration2 │
└───────────────────────┘ └─────────────────────────────────── └───────────────────────────┘
```
Lanes depend **only** on foundation2, never on each other. Everything is developed and verified
against the synthetic fixture first; real footage is integration2's job.
## Milestones
### M10 — Audio features: beats & onsets (lane E, backend)
`backend/festival4d/audio_features.py`, entrypoint `run_features()`, CLI `python -m festival4d features`.
Load the **reference** video's ingest WAV (16 kHz mono; the reference timeline *is* `t_global`).
Beat/tempo tracking + onset-strength detection (librosa is already a dependency). Write
`data/work/beats.json` (shape frozen below); `GET /api/beats` serves it, 404 when absent.
Quiet/short/beatless audio must produce a *valid, empty* result — never a crash.
**Accept:** synthetic master audio's known pulse grid recovered (beats within ±50 ms, tempo ±2 BPM);
silence → empty-but-valid JSON; tests in `backend/tests/test_audio_features.py`.
### M11 — Auto-director (lane E, backend + thin UI)
`backend/festival4d/director.py`, entrypoint `generate_path(top_n=8, lead_s=2.0)`, CLI
`python -m festival4d direct`, `POST /api/director` (stubbed by foundation2) returning a
**camPath-compatible JSON** (contract below). Deterministic v1 rules — no ML:
pick the top-N events by confidence (ties → earlier); for each, choose the registered camera
with a pose nearest the event time (via `db` + `geometry.colmap_to_threejs`); keyframes at
`t_event lead_s` and `t_event + duration`; when `beats.json` exists, snap keyframe times to
the nearest beat; FOV from that camera's intrinsics (same formula as `scene3d.snapTo`).
Frontend: fill the stub `frontend/src/director.js` — the hidden **🎬 Auto-path** button fetches
`/api/director` and loads it via `camPath.fromJSON` (do not edit camPath.js).
**Accept:** output round-trips through `camPath.fromJSON` (structure-locked test against the
frozen shape); covers the top-N events; keyframe times land on beats when beats exist; live
browser evidence of the path playing.
### M12 — Cinematic export (lane E, frontend)
Fill the stub `frontend/src/exportVideo.js`. The hidden **⏺ Export** button: seek to the first
keyframe, play the path, capture `scene3d` canvas via `captureStream(30)` + the soundtrack via
the `transport.captureAudioStream()` hook (foundation2 provides it — taps the WebAudio graph
post-gain, spatial mix included), mux with `MediaRecorder` (vp9/opus webm), stop at the last
keyframe, download `festival4d-cut.webm`. Restore all prior UI/transport state afterwards.
**Accept:** exported file duration within ±5% of the path span, contains an audio track, plays
in Chrome — browser evidence (file size, duration probe via ffprobe) in the status file.
### M13 — Anchor manager & friend tags (lane F, frontend)
Fill the stub `frontend/src/anchorPanel.js`: an always-available collapsible panel listing every
anchor (color dot, label, jump-to, delete, rename/recolor via the new `PATCH /api/anchors/{id}`).
Jump-to uses the foundation2 hook `scene3d.focusOn(x, y, z)`. Add the "friend tag" flow: a
** Tag** action that runs the existing M8 bbox-annotation flow *without* an event and names the
resulting anchor (friends become labeled, colored anchors that x-ray onto every video for free).
**Accept:** create → rename → recolor → jump → delete round-trip live in the browser; labels
visible in the 3D scene and video overlays; anchors survive reload.
### M14 — Photo mode (lane F, frontend)
Fill the stub `frontend/src/photoMode.js`. **📷 / `P`**: pause, hide helpers + gizmos + anchor
labels (foundation2 hook `scene3d.setHelpersVisible(false)`), render one frame at 3840-wide
(preserving aspect) to an offscreen target, download PNG, restore everything.
**Accept:** PNG ≥ 3840 px wide with no grid/axes/frusta/gizmos; works in free-roam and
follow-cam; with a splat present it renders the splat (verify on the synthetic point cloud +
note the splat path).
### M15 — Moment FX (lane F, frontend)
Fill the stub `frontend/src/fx.js`. When the playhead crosses a `pyro`/`confetti` event, fire a
short particle burst at the event's resolved anchor (fall back to the stage centroid); on
`bass_drop`, pulse the point-cloud/splat scale for ~a beat. Toggle via the hidden **✨** button;
uses only `scene3d.addObject/removeObject`. Must fire when crossing markers in play *and* scrub.
**Accept:** browser evidence of both effect types; frame rate stays ≥ 30 fps on the synthetic
scene with FX enabled.
### M16 — Server-side camera paths (lane G, backend + thin UI)
Implement the foundation2-stubbed routes over the new `paths` table:
`GET /api/paths` (list), `GET /api/paths/{id}`, `POST /api/paths {name, json}`,
`DELETE /api/paths/{id}``json` is the frozen camPath JSON as text, validated on POST.
Fill the stub `frontend/src/pathsStore.js`: save-current-path (name prompt), load dropdown,
delete — wired to the hidden path-save controls.
**Accept:** endpoint tests (roundtrip deep-equal, 404s, invalid-JSON 422) in
`backend/tests/test_paths.py`; live save → reload page → load reproduces keyframes exactly.
### M17 — Memory capsule: static shareable export (lane G, backend)
`backend/festival4d/capsule.py`, CLI `python -m festival4d capsule [--out DIR]` (default
`data/capsule/`). Build a **zero-backend** bundle:
copy `frontend/dist` (error clearly if missing — tell the user to `npm run build`); copy media;
bake every read-only API response to files at the *same relative paths* (`api/manifest`,
`api/events`, `api/anchors`, `api/beats`, `api/videos/{id}/poses`, `api/pointcloud`, `api/splat`,
`api/audio/{id}` — extracting audio if not yet cached); inject
`<script>window.__F4D_API_BASE__=""</script>` into the copied `index.html`; set
`"capsule": true` in the baked manifest (the frontend hides all write UI — foundation2 wires
the flag); ship a stdlib **`serve.py`** in the bundle root that serves with HTTP Range support
(video seeking needs 206s; plain `http.server` can't).
**Accept:** `python -m festival4d capsule` on the synthetic project, then `python serve.py` in
the bundle: app loads with **zero** requests to :8000, videos play *and seek*, 3D scene +
timeline + spatial audio work, write UI absent; `backend/tests/test_capsule.py` covers bundle
structure, baked-JSON validity, and serve.py Range (206) behavior.
## Contracts (frozen at foundation2 merge)
1. **beats.json** (and the `GET /api/beats` body):
`{"tempo_bpm": float|null, "beats_s": [float], "onsets": [{"t_global_s": float, "strength": float 0..1}], "generated_by": "festival4d.audio_features"}`
— all times are `t_global` seconds.
2. **camPath JSON** (already shipped in `camPath.js`; now frozen):
`{"version": 1, "keyframes": [{"t_global": float, "pos": [x,y,z], "quat": [x,y,z,w], "fov": float}]}`
**Three.js scene space**, quat order **[x,y,z,w]** (NOT COLMAP's [w,x,y,z]; convert via
`geometry.colmap_to_threejs` on the backend, never inline).
3. **paths table:** `paths(id INTEGER PK, name TEXT NOT NULL, created_at TEXT, path_json TEXT NOT NULL)`
+ the four routes in M16.
4. **`PATCH /api/anchors/{id}`** `{label?, color?}` → updated anchor dict (404 on missing).
5. **Runtime API base override:** `state.js` resolves `window.__F4D_API_BASE__` first, then
`VITE_API_BASE`, then the localhost default. **`manifest.capsule: true`** ⇒ read-only UI.
6. **`transport.captureAudioStream()`** → `MediaStream` tapping the live WebAudio mix (and a
matching `releaseAudioStream()`); `scene3d.focusOn(x,y,z)` and `scene3d.setHelpersVisible(bool)`.
7. **Frozen files during 5b:** everything frozen in phase 1 **plus**
`frontend/src/{transport,scene3d,state,main,camPath,annotate}.js` and `frontend/index.html`
(foundation2 pre-wires every button — hidden — and every module import; lanes fill their own
stub modules only). Believe a frozen file must change → `plan/CHANGE_REQUESTS.md`, work
around locally, integration2 adjudicates.
## foundation2 scope (Phase 5a, one agent, branch `foundation2`)
- DB: `paths` table + `db.py` helpers. API: `/api/beats`, `/api/director`, `/api/paths*`,
`PATCH /api/anchors/{id}` — implemented against synthetic data where cheap, stubbed with the
spec's graceful-degradation pattern where a lane owns the logic. CLI: `features`, `direct`,
`capsule` subcommands dispatching into stubs.
- Backend stubs: `audio_features.py`, `director.py`, `capsule.py` (raise `NotImplementedError`;
API/CLI catch and degrade, exactly like phase 1).
- Frontend: create empty stub modules `director.js`, `exportVideo.js`, `anchorPanel.js`,
`photoMode.js`, `fx.js`, `pathsStore.js`; import them from `main.js`; add ALL new buttons to
`index.html` (hidden until their module reports ready); add the transport/scene3d hooks and
the `__F4D_API_BASE__` + `manifest.capsule` wiring from the contracts above.
- Tests for every new route shape (mirroring `test_api.py`'s exact-set style — note it locks
the manifest key set; add `capsule` consciously).
- Update the ownership table below if anything moved. Suite ≥ 121 + your new tests. Merge fast.
## integration2 scope (Phase 5c, one agent, branch `integration2`)
- Merge order irrelevant; resolve CHANGE_REQUESTS; full synthetic pass of every milestone
(evidence per feature); raise the suite floor to the new count.
- **Real-footage field test** (the actual milestone): 24 real overlapping clips through
`ingest → sync → reconstruct → events → features → direct → serve`, then capsule the result.
Log every failure as a `plan/ISSUES.md` entry (ISSUE-1 is the format) — fix nothing mid-test.
- Tag `v0.2.0` when accepted by the coordinator.
## File ownership (Phase 5b)
| Owner | Files |
|---|---|
| Lane E | `backend/festival4d/audio_features.py`, `director.py`, `backend/tests/test_audio_features.py`, `test_director.py`, `frontend/src/director.js`, `exportVideo.js` |
| Lane F | `frontend/src/anchorPanel.js`, `photoMode.js`, `fx.js` |
| Lane G | `backend/festival4d/capsule.py`, `backend/tests/test_paths.py`, `test_capsule.py`, `frontend/src/pathsStore.js`, the paths-route bodies in the area foundation2 marks for lane G |
| Every agent | its own `plan/status/<lane>.md` |
| **Coordinator only** | `plan/DIRECTIVES.md` |
| **Frozen during 5b** | contract item 7 above |
## Phase-5 pitfalls
1. **Quaternion order.** camPath is `[x,y,z,w]` (Three.js); COLMAP storage is `[w,x,y,z]`.
Every past pose bug in this repo was a convention slip — go through `geometry.colmap_to_threejs`
/ `lib/pose.js`, never inline the math.
2. **The pane you verify in throttles rAF** (backgrounded tab). Drive the loop with the
`window.__f4d` pump (`localStorage.setItem("f4dDebug","1")` in production builds); don't
file "playback frozen" issues against your own hidden tab. MediaRecorder export (M12) needs
a *focused* tab to capture real frames — say so in your evidence if you couldn't.
3. **`test_api.py` locks response key-sets on purpose.** Adding a manifest key or route means
consciously updating the exact-set assertion in the same commit — a failing set-lock is a
feature, not an obstacle.
4. **Range serving.** Any static path that serves video must answer 206 (spec M3 pitfall #3);
`python -m http.server` does not — that's why M17 ships `serve.py`.
5. **Never block on a missing optional input.** No beats.json → director still works (no beat
snapping). No splat → photo mode shoots the point cloud. No audio track → transport already
falls back. Follow the degradation pattern everywhere; it's the house style.
6. **Decoded audio is big** (~23 MB/min stereo). M12/M17 must not decode all tracks just
because they can — reuse what the transport already has.

View File

@ -4,6 +4,47 @@ Written **only** by the coordinator (the human's planning session). Agents: read
--- ---
## Round 4 — 2026-07-17 — Fresh-eyes upgrades landed; PHASE 5 begins (M10M17)
**Coordinator work landed directly on `main` (self-reviewed + live-verified):**
- `95b7178` — sync solver caches one rFFT per signal across the pairwise GCC-PHAT stage
(1.8× on 6 cams × 10 min; equivalence locked by a new unequal-lengths test).
- `c24af80`**audio is now the master clock** (`GET /api/audio/{id}` AAC + WebAudio;
`t_global` from `AudioContext.currentTime`; all `<video>` muted picture-only; graceful
fallback) **+ spatial audio** (🎧 per-camera HRTF panners at live poses, listener = viewer
camera, gains 1/√N). Verified live: clock drives playback, mid-play source switching is
continuous, panners sit at the correct camera positions, toggle on/off mid-play is clean.
- `db42c42` — chore: `python-multipart` locked (declared-but-unlocked drift), launch configs.
- Suite is **121 passed**; frontend build clean. **The test floor is now 121.**
**Contract amendments (coordinator-ratified, additive only):** new route `GET /api/audio/{id}`;
`transport.js` is now the WebAudio clock owner (its header comment is the reference);
the `window.__f4d` debug hook is available in production builds via `localStorage f4dDebug=1`.
`test_api.py`'s manifest key-set lock is unchanged.
**Directives — Phase 5 (spec: `plan/20-phase5.md`; lane briefs: `plan/lane-E/F/G-*.md`):**
1. **Now: `foundation2` only** (one agent, branch `foundation2`, scope in 20-phase5.md).
Same rule as phase 1: contracts correctness beats speed — three lanes will hard-depend on
your route shapes, stubs, hooks, and UI wiring points. Maintain `plan/status/foundation2.md`.
2. **After foundation2 merges — lanes in priority order** (all three concurrently if capacity
allows): **E** (director — longest, earliest contract validation), **G** (capsule — the
shareable payoff), **F** (social UX — smallest, pure frontend).
3. **Worktree rule is MANDATORY this time:** every parallel agent creates its own `git worktree`
before its first edit (Round 1 standing lesson; the phase-2 shared-tree collision cost us a
stash cleanup). Single-agent phases (5a, 5c) may use the main working directory.
4. **Merge policy unchanged:** merge to `main` per-milestone where the lane brief allows it;
never wait on another lane. Then `integration2` (branch `integration2`), which owns the
real-footage field test and the `v0.2.0` tag proposal — failures go to `plan/ISSUES.md`,
not into hasty fixes.
5. Standing protocol (status files, evidence discipline, blocker rule, DIRECTIVES re-read after
every milestone) is unchanged from Round 0.
**For the human:** (a) the Round 3 focused-browser sync check is still open — now also put on
headphones, enable 🎧 3D audio, and fly around; (b) real overlapping clips for the field test
remain the gating item for integration2; (c) optional: a trained splat for a real project makes
photo mode (M14) and the capsule (M17) dramatically more impressive —
`scripts/splat_via_modelbeast.sh`.
## Round 3 — 2026-07-16 — Integration review: v0.1.0 ACCEPTED; polish + field test next ## Round 3 — 2026-07-16 — Integration review: v0.1.0 ACCEPTED; polish + field test next
**Coordinator review of the integration work (independently verified):** `main` == `origin/main` **Coordinator review of the integration work (independently verified):** `main` == `origin/main`

View File

@ -2,6 +2,10 @@
**Read [`../OPUS_BUILD_INSTRUCTIONS.md`](../OPUS_BUILD_INSTRUCTIONS.md) first — it is the canonical spec** (architecture, repo layout, data model, milestone details M0M9, pitfalls). This directory only organizes *who builds what, in what order, without colliding*. Lane briefs reference spec milestones by number; they do not restate them. **Read [`../OPUS_BUILD_INSTRUCTIONS.md`](../OPUS_BUILD_INSTRUCTIONS.md) first — it is the canonical spec** (architecture, repo layout, data model, milestone details M0M9, pitfalls). This directory only organizes *who builds what, in what order, without colliding*. Lane briefs reference spec milestones by number; they do not restate them.
> **Current phase: 5** (M10M17) — spec and organization in [`20-phase5.md`](20-phase5.md), lane
> briefs `lane-E/F/G-*.md`. Phases 14 below are complete (v0.1.0 + polish); their protocol
> sections (git, status, directives loop, evidence discipline) remain the standing rules.
## Phase map ## Phase map
``` ```

18
plan/lane-E-director.md Normal file
View File

@ -0,0 +1,18 @@
# Lane E — Director & export (branch `lane/e-director`)
**Scope: phase-5 milestones M10 + M11 + M12** (beats/onsets, auto-director, cinematic export) — see [`20-phase5.md`](20-phase5.md).
- Owned files: `backend/festival4d/audio_features.py`, `backend/festival4d/director.py`,
`backend/tests/test_audio_features.py`, `backend/tests/test_director.py`,
`frontend/src/director.js`, `frontend/src/exportVideo.js`. Nothing else.
- Fill the stub bodies foundation2 left; signatures, routes, and CLI dispatch already exist.
- Contracts you consume (frozen): `beats.json` shape, camPath JSON (quat `[x,y,z,w]`, Three.js
space — convert COLMAP poses via `geometry.colmap_to_threejs`, never inline),
`transport.captureAudioStream()`.
- Build order M10 → M11 → M12: the director consumes beats; export consumes the director's path.
Each milestone is independently mergeable — don't hold M10/M11 hostage to M12.
- Degradation: no beats → director skips beat-snapping; no poses → director returns a clear
error body, never a crash; export with no path → button stays disabled.
**Done when:** M10M12 acceptance passes (browser evidence for M11/M12 in your status file),
`pytest` ≥ 121 + your tests, merged to `main` with synthetic end-to-end intact.

17
plan/lane-F-social.md Normal file
View File

@ -0,0 +1,17 @@
# Lane F — Social & fun UX (branch `lane/f-social`)
**Scope: phase-5 milestones M13 + M14 + M15** (anchor manager + friend tags, photo mode, moment FX) — see [`20-phase5.md`](20-phase5.md).
- Owned files: `frontend/src/anchorPanel.js`, `frontend/src/photoMode.js`, `frontend/src/fx.js`.
Nothing else — buttons, imports, and hooks are pre-wired by foundation2.
- Contracts you consume (frozen): `PATCH /api/anchors/{id}` + existing anchor GET/POST/DELETE,
`scene3d.focusOn(x,y,z)`, `scene3d.setHelpersVisible(bool)`, `scene3d.addObject/removeObject`,
the M8 annotation flow (drive it, don't modify `annotate.js`).
- This is the "friends connect & remember" lane — labels, colors, and jump-to should feel like
tagging people, not editing database rows. Keep interactions one-click where possible.
- All verification is in-browser: use the `__f4d` debug pump (phase-5 pitfall #2) and put
screenshots/measurements in your status file. No backend tests to write; the suite must
simply stay green.
**Done when:** M13M15 acceptance passes with browser evidence, frontend `npm run build` clean,
merged to `main` with synthetic end-to-end intact.

17
plan/lane-G-capsule.md Normal file
View File

@ -0,0 +1,17 @@
# Lane G — Persistence & capsule (branch `lane/g-capsule`)
**Scope: phase-5 milestones M16 + M17** (server-side camera paths, static memory-capsule export) — see [`20-phase5.md`](20-phase5.md).
- Owned files: `backend/festival4d/capsule.py`, `backend/tests/test_paths.py`,
`backend/tests/test_capsule.py`, `frontend/src/pathsStore.js`, plus the paths-route bodies in
the region foundation2 marks for lane G. Nothing else.
- Contracts you consume (frozen): `paths` table + route shapes, camPath JSON (validate on POST),
`window.__F4D_API_BASE__` override, `manifest.capsule` read-only flag.
- M17 discipline: the capsule bakes API responses to files at the **same relative paths** the
live API serves — the frontend must not know the difference. Ship `serve.py` (stdlib,
Range-capable) in the bundle root; acceptance explicitly includes video *seeking* under it.
- The capsule is the shareable artifact of the whole project — treat "zero requests to :8000"
and "write UI absent" as hard acceptance, not nice-to-haves.
**Done when:** M16M17 acceptance passes (endpoint tests + a served-capsule browser pass with
evidence), `pytest` ≥ 121 + your tests, merged to `main` with synthetic end-to-end intact.