## What
- enforce a finite, validated `meta/videos.json` contract shared by Plan v1 and Plan v2
- preserve authored finite ends and source-derived trim-aware ends; bound any still-open end at the validated composition end
- fail distributed planning when any declared video source did not extract instead of publishing a blank-capable plan
- make the v1 chunk reader reject malformed/null video timing before frame injection
- route deterministic video-source/metadata failures as non-retryable in AWS and GCP while retaining retries for transient extraction failures
## Why
An open-ended video whose remote source could not be resolved retained `Infinity` through planning. Plan v2 correctly rejected that value, while Plan v1 serialized it as `null`; the v1 frame lookup could then suppress injected frames and silently produce incorrect output.
The invariant belongs at the shared metadata boundary. Both protocols must receive identical finite timing, and unavailable sources must fail closed before plan publication.
## Test plan
- [x] producer distributed planning, metadata, v1 chunk boundary, Plan v2 conversion/materialization, and public exports
- [x] core runtime media semantics (authored slots, natural duration, looping, non-looping hold)
- [x] engine video extraction and frame lookup
- [x] AWS Lambda/CDK/SAM and GCP Cloud Run error normalization/retry classification
- [x] producer, core, engine, AWS, and GCP typechecks/builds
- [x] formatting, oxlint, tracked-artifact, fallow, and commit hooks
- [x] exact incident composition replayed through the AWS Lambda handler's Lambda-local path in a Lambda-like container; Plan v1 and Plan v2 both fail closed as `VIDEO_SOURCE_UNRENDERABLE` during planning, before plan publication
- [x] full PR CI, including all nine regression shards and Windows render/tests
No production flags or deployment/release workflows are changed.
## What
- publishes Plan v2 content-addressed artifacts directly from the Cloud Run planner's private staging directory to GCS
- commits the Plan v2 manifest only after every referenced artifact is durable
- removes the second local Plan v2 CAS directory and preserves the existing Cloud Workflows wire contract
- exports a reusable GCS publisher adapter
## Why
Cloud Run planner, chunk, and assembler requests are independent containers and do not share a filesystem. The distributed contract must contain only durable GCS locators.
This layer makes the Plan v2 publication path object-store-native. Like the AWS parent PR, it still relies on one planner-local frozen v1 tree inside the producer; eliminating that remaining staging tree requires direct Plan v2 emission in a later layer.
## Design invariants
- local paths never cross a request or worker boundary
- manifest and artifact locators derive from one validated GCS output prefix
- immutable objects use generation-zero conditional creation and exact digest/size verification
- retries may reuse exact objects but never overwrite conflicts
- the manifest is the final publication commit point
- chunk and assembler requests independently download and verify only their target artifacts
## Test plan
- [x] all 95 GCP Cloud Run package tests pass
- [x] package typecheck passes
- [x] package build passes
- [x] changed-file lint, format, fallow, and repository commit gates pass
- [x] end-to-end adapter test covers plan, target-scoped chunk, and assemble through Fake GCS with no shared local directory
canvas.drawElementImage is an unlaunched Dev/Canary-only Blink feature
(~151+). The CLI's pinned CHROME_VERSION fallback was still 131.0.6778.85 —
a puppeteer 24→25.2.1 bump that pinned it to Chrome Dev 151.0.7912.0 was
written on 2026-06-29 but never merged (orphaned local commit, no PR). Any
render on that pin, or on the shared puppeteer-cache binary, or on system
Chrome (Stable, no drawElementImage at all) got a canvas.getContext("2d")
missing the method and crashed mid-capture with "ctx.drawElementImage is
not a function" instead of falling back (HF#2060).
Three changes:
- Bump puppeteer/puppeteer-core to ^25.2.1 across every package that
depends on it, and CHROME_VERSION to 152.0.7928.2 (today's Dev channel;
confirmed via direct probe to implement drawElementImage, unlike 131).
- `ensureBrowser({ preferManagedChrome: true })`, always used by `render`:
resolve straight to our pinned/cached build, skipping both the shared
puppeteer-cache preference and system Chrome. Rendering shouldn't depend
on whatever arbitrary Chrome a machine happens to have — that's exactly
how this regressed (any Mac with Chrome.app installed bypassed the CLI's
pin entirely).
- A runtime capability probe in the engine, right before any other
drawElement work: if `drawElementImage` isn't a function on the injected
canvas, route to the existing screenshot-fallback gate instead of
crashing. This is the real backstop — it protects every resolution path
(env override, stale cache entry, a future Chrome regression), not just
the ones `preferManagedChrome` reaches.
Verified end-to-end: rendering against chrome-headless-shell 131 (confirmed
to lack drawElementImage) now falls back cleanly and produces a valid MP4
instead of crashing; rendering against a capable build still engages
drawElement normally. 922 engine tests + 1373 CLI tests pass.
Fixes#2060.