## What
Refactor distributed planning around one shared local execution-plan builder:
- `buildLocalExecutionPlan()` now owns compile/probe/extract/audio/freeze.
- Legacy `plan()` remains a deprecated v1 transport wrapper.
- Plan v2 calls the shared builder directly and publishes through the existing manifest/CAS contract.
- Add neutral `createPlanV2FromExecutionPlan()`, `publishPlanV2FromExecutionPlan()`, `getPlanV2ExecutionPlanHash()`, and `PLAN_PROTOCOL_V1` names.
- Retain deprecated v1-named exports and wire aliases.
- Recommend explicit Plan v2 opt-in for new producer, Lambda, and Cloud Run integrations.
## Why
Plan v2 previously looked like it invoked a v1 planner even though v1 and v2 share the same frozen local execution representation. This removes that migration-era coupling while preserving the public minor-version compatibility contract.
## How
The shared builder returns neutral internal execution-plan fields. The v1 wrapper maps those fields back to the existing `PlanResult`; the v2 publisher consumes them directly.
Compatibility is intentional and covered by exact shape tests:
- omitted `planProtocol` still serializes/selects `"v1"`;
- v1 layouts, descriptor-less decoding, event unions, workflow branches, and exports remain;
- the v1 descriptor JSON is byte-identical and `CURRENT_PLAN_PROTOCOL` is an identity-preserving alias;
- v2 manifest bytes, key order, hash framing, and `sourcePlanV1Hash` wire key remain unchanged;
- no enumerable neutral hash field was added to manifests or returned result objects;
- v1/v2 result objects, cloud event payloads, and SDK handle key sets remain unchanged.
## Test plan
- Focused Plan v1/v2/protocol/export/size compatibility: 141 passed
- `@hyperframes/core`: 1,419 passed
- `@hyperframes/producer` unit lane: 990 passed
- `@hyperframes/aws-lambda`: 140 passed
- `@hyperframes/gcp-cloud-run`: 101 passed
- Producer, Lambda, and Cloud Run typechecks
- Repository-wide lint, format check, workspace/package-subpath checks
- Full workspace build
- `git diff --check`
- [x] Unit tests added/updated
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
## 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.
## Summary
- Adds `validateEventS3Uris()`, called immediately after `unwrapEvent()` in the Lambda handler before any S3 I/O.
- If `HYPERFRAMES_RENDER_BUCKET` env var is set, every S3 URI in the event (`ProjectS3Uri`, `PlanOutputS3Prefix`, `PlanS3Uri`, `ChunkOutputS3Prefix`, `ChunkS3Uris`, `AudioS3Uri`, `OutputS3Uri`) must resolve to that bucket. Mismatches throw `S3_URI_NOT_ALLOWED`.
- Env var unset → validation skips (backwards-compatible; existing deployments without the var continue to work).
- CDK stack (`HyperframesRenderStack`) auto-wires `HYPERFRAMES_RENDER_BUCKET: this.bucket.bucketName` so new deployments are protected without manual config.
- `S3_URI_NOT_ALLOWED` added to all three `NON_RETRYABLE_*` lists in the Step Functions state machine so the state machine does not retry on this error.
## Security
**F-004 MED** — The Lambda handler accepted S3 URIs from the event payload without verifying they targeted the function's own render bucket. An attacker who could inject a crafted Step Functions execution input could route `GetObject` / `PutObject` calls to arbitrary buckets in the same AWS account, potentially exfiltrating plan data or overwriting objects in unrelated buckets.
## Test plan
- [x] `handler` rejects a `plan` event whose `ProjectS3Uri` targets a different bucket — `S3_URI_NOT_ALLOWED` thrown, zero S3 ops recorded
- [x] `handler` rejects an `assemble` event with one cross-bucket chunk URI
- [x] Validation is skipped when `HYPERFRAMES_RENDER_BUCKET` is unset (no regression for existing callers)
- [x] All 12 handler unit tests pass
The producer's probe stage launches Chromium when Plan has to resolve
browser-only data: root duration unknown, unresolved sub-compositions, or
data-hf-auto-start media. Only handleRenderChunk was setting
PRODUCER_HEADLESS_SHELL_PATH, so a cold Plan invocation launched
puppeteer-core with no executablePath and failed before writing plan.tar.gz.
Mirror the renderChunk env-var guard inside handlePlan so the bundled
Sparticuz binary gets resolved on the first Plan invocation and reused on
warm starts. The skipChromeResolution dep stays honored for SAM-local RIE
smokes.
A warm Lambda environment can mask this only if it previously served a
renderChunk from another execution and left the env var sticky. Within a
single Step Functions execution, Plan still runs before RenderChunks.
A new dispatch test exercises the guard path by pre-seeding
PRODUCER_HEADLESS_SHELL_PATH and asserting Plan does not overwrite it.
* feat(lambda): add Lambda handler, ZIP bundling, and BeginFrame probe
Phase 6 of the distributed rendering plan: AWS Lambda turnkey adoption
(see DISTRIBUTED-RENDERING-PLAN.md §11 Phase 6 + §15).
This PR adds the new packages/aws-lambda/ workspace package that wraps
the OSS plan/renderChunk/assemble primitives in an AWS Lambda handler,
plus a build pipeline that bundles the handler + Chromium runtime +
ffmpeg into a deployable ZIP.
Architecture: ZIP deploy (not Docker image), Chrome via @sparticuz/chromium
with chrome-headless-shell fallback, dispatch on event.Action ∈ {plan,
renderChunk, assemble}.
The load-bearing concern — does @sparticuz/chromium's chrome-headless-shell
build honour CDP HeadlessExperimental.beginFrame? — is pinned by the new
scripts/probe-beginframe.ts regression guard. Probe boots the runtime
inside public.ecr.aws/lambda/nodejs:22, navigates to a static page, and
asserts beginFrame returns a PNG buffer. Verified locally + inside the
Docker container; both pass with hasDamage=true.
Sizes (sparticuz source): unzipped 157 MiB, zipped 99 MiB. Well under
the 240 MiB / 150 MiB in-house gates and the Lambda 250 MiB hard ceiling.
This is part of a stack of 8 PRs (3 in Phase 6a, 5 in Phase 6b); this is
PR 6.1.
* fix(lambda): address PR 878 review feedback
- Verify event.PlanHash against the untarred plan.json at the handler
boundary before invoking the producer primitive. Throws typed
PLAN_HASH_MISMATCH on divergence so Step Functions routes it as
non-retryable; previously the field was schema bloat the handler
ignored, leaving enforcement entirely inside the producer.
- Standardize on MiB throughout build-zip.ts, verify-zip-size.ts, and
the README. Lambda's hard ceiling is 250 MiB (AWS docs label "250 MB"
but use binary mebibytes); previously mixed units made the 248 MiB
budget look like a ~5 MB margin instead of the 2 MiB it actually is.
- stageChromeHeadlessShell now picks Chrome versions via numeric semver
comparison instead of lexicographic sort+reverse — the latter would
silently pick "99.x" over "131.x" once Chrome cached three-digit
majors that aren't width-aligned.
- Drop _setSparticuzChromiumForTests from the public index barrel.
Test-only DI seam imported directly from ./chromium.js in tests.
- Replace require("node:fs") inside walkSize() with the top-level fs
imports — file is ESM and the same module is already imported.
* docs(lambda): drop internal plan-doc refs from package README
* ci(windows): fix bun filter UNION bug excluding producer from Windows tests
`bun run --filter "!a" --filter "!b" test` composes as a UNION (any
package matching either negation runs), not an intersection. Effect:
@hyperframes/producer was still being tested on Windows even though
it's explicitly excluded — its regression harness (Docker + LFS golden
mp4 baselines) is Linux-only and was driving the 32min timeout.
Enumerate the packages we DO want to test instead.