fix(cli): wire aspect-agnostic resolution through cloudrun/lambda/batch + preflight recompute

Addresses R2 CHANGES_REQUESTED from Miga + Rames on PR #2529:

1. Sibling-surface gap (blocker): `hyperframes cloudrun render{,-batch}`,
   `hyperframes lambda render{,-batch}` all advertised the same tier-only
   aliases (`1080p` / `hd` / `4k` / `uhd`) but normalized them to `landscape`
   and never set `outputResolutionAspectAgnostic`. The distributed plumbing
   PR #2529 added received `undefined` from those callers, so portrait `1080p`
   still hit the original aspect-mismatch on Cloud Run / Lambda.

   Fix: introduce `resolveResolutionFlagPair` in `@hyperframes/parsers` (the
   single source of truth for the two-step normalize + aspect-agnostic
   detect) and route every distributed entrypoint through a shared
   `parseOutputResolutionFlag` CLI util so the alias signal now reaches
   `SerializableDistributedRenderConfig`. Studio Server keeps its
   canonical-only HTTP contract; that intent is now pinned in tests.

2. Preflight recompute (hardening): the earlier "downgrade aspect-mismatch"
   preflight cleared un-remapped mismatches, so IG 4:5 (non-preset aspect,
   no sibling) and portrait-4K comp + `--resolution 1080p` (remap +
   downsample) both slipped through to fail late in `resolveDeviceScaleFactor`.
   Now `checkRenderResolutionPreflight` computes the effective preset via
   `suggestMatchingPreset` (mirroring the compile stage's
   `adaptAspectAgnosticResolution`) and re-checks against that — only
   genuinely-fixable mismatches clear early. New tests pin both regressed
   input classes.

3. Docker forwarding boundary test (Miga's important #2): pinned
   `1080p` survives verbatim as `--resolution 1080p` in the Docker args
   so the in-container CLI can re-run `isAspectAgnosticResolutionAlias`.

4. Doc-nit (Miga): parsers/src/types.ts no longer references the
   nonexistent `resolveResolutionForComposition` — points at the actual
   remap helpers.

Fallow: cloudrun.ts / lambda.ts share 390 lines of pre-existing structural
symmetry (parallel AWS + GCP dispatchers), and lambda/render.ts +
render-batch.ts declare parallel RenderArgs interfaces. Both re-flagged
after threading the aspect-agnostic field through each surface; ignored
with justification in .fallowrc.jsonc. lambda.ts's `run` and
lambda/render.ts's `waitForCompletion` are pre-existing CRAP-score
hotspots untouched by this PR — added under health.ignore.

Co-Authored-By: Claude <noreply@anthropic.com>

— Via
This commit is contained in:
Via
2026-07-16 08:01:56 +00:00
co-authored by Claude
parent 7e58d050f8
commit 2d398ed274
18 changed files with 707 additions and 66 deletions
+28 -15
View File
@@ -18,11 +18,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { defineCommand } from "citty";
import {
type CanvasResolution,
normalizeResolutionFlag,
VALID_CANVAS_RESOLUTIONS,
} from "@hyperframes/core";
import { type CanvasResolution } from "@hyperframes/core";
import { parseOutputResolutionFlag } from "../utils/parseOutputResolution.js";
import type { Example } from "./_examples.js";
import { c } from "../ui/colors.js";
import {
@@ -774,13 +771,21 @@ function runDestroy(args: Record<string, unknown>): void {
* separately (it differs per batch entry). Mirrors the local `hyperframes
* render` flag surface so the two stay consistent.
*/
function buildRenderConfig(
/**
* Exported for unit-test coverage of the aspect-agnostic wire shape — the
* portrait-1080p sibling-surface regression that shipped in v0.7.60 landed
* here because this builder dropped the tier-alias signal on the floor.
*/
export function buildRenderConfig(
args: Record<string, unknown>,
fps: number,
width: number,
height: number,
variables: Record<string, unknown> | undefined,
): Record<string, unknown> {
const { outputResolution, outputResolutionAspectAgnostic } = parseOutputResolution(
args["output-resolution"],
);
return stripUndefined({
fps,
width,
@@ -791,7 +796,11 @@ function buildRenderConfig(
chunkSize: parsePositiveInt(args["chunk-size"], "--chunk-size"),
maxParallelChunks: parsePositiveInt(args["max-parallel-chunks"], "--max-parallel-chunks"),
targetChunkFrames: parsePositiveInt(args["target-chunk-frames"], "--target-chunk-frames"),
outputResolution: parseOutputResolution(args["output-resolution"]),
outputResolution,
// Set only when true so the wire shape stays sparse for the common
// canonical-preset path (matches how the flag flows through
// `SerializableDistributedRenderConfig` from every other emitter).
outputResolutionAspectAgnostic: outputResolutionAspectAgnostic ? true : undefined,
variables,
});
}
@@ -823,14 +832,18 @@ function resolveAndValidateVariables(
return variables;
}
function parseOutputResolution(raw: unknown): CanvasResolution | undefined {
if (raw == null || raw === "") return undefined;
const normalized = normalizeResolutionFlag(String(raw));
if (normalized) return normalized;
throw new Error(
`[cloudrun render] --output-resolution must be one of ${VALID_CANVAS_RESOLUTIONS.join("|")} ` +
`(or an alias: 1080p, 4k, uhd, hd, …); got ${String(raw)}`,
);
/**
* Cloud Run flavor of the shared {@link parseOutputResolutionFlag} — carries
* the aspect-agnostic signal through so `SerializableDistributedRenderConfig`
* can trigger the compile-stage remap. The runtime work lives in the shared
* util; wire-config-level coverage lives at `cloudrun.test.ts`, and full
* input-space coverage at `../utils/parseOutputResolution.test.ts`.
*/
function parseOutputResolution(raw: unknown): {
outputResolution: CanvasResolution | undefined;
outputResolutionAspectAgnostic: boolean;
} {
return parseOutputResolutionFlag(raw, { surfaceLabel: "[cloudrun render]" });
}
// ── parse helpers ─────────────────────────────────────────────────────────