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
The aspect-agnostic resolution aliases (`--resolution 1080p` / `hd` / `4k` / `uhd`) previously all normalized to a landscape preset, which rejected portrait 1080x1920 compositions with 'Output resolution incompatible'. Users had to specify the orientation-bearing alias (`1080p-portrait`) or render at native.
This threads two new fields (`outputResolutionAspectAgnostic` + `outputResolutionRaw`) through the render pipeline. At the CLI layer we detect whether the user's flag was an aspect-agnostic alias; at the compile stage we re-map the preset to the composition's orientation via the existing `suggestMatchingPreset` sibling-lookup (formerly private). Explicit orientation-bearing aliases and canonical presets stay strict.
Field signal: ts=1784176662 (darwin/arm64, CLI 0.7.59, `--resolution 1080p` on a 1080x1920 portrait comp).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
— Via
* fix(lint): stop CSS comments in <style> from manufacturing phantom root tags
extractOpenTags scans raw source text with a flat regex that has no
concept of <style>/<script> block boundaries, so a CSS comment like
`/* <g> wrapper */` inside a <style> block reads as a real open tag.
findRootTag consumes that flat tag list and only skips tags literally
named script/style/meta/link/title, so the phantom <g> tag (not in
that skip list) wins the "first non-ignored body tag" search and gets
returned as the composition root instead of the real one that follows.
This manufactured root_missing_composition_id and root_missing_dimensions
(the phantom tag has neither) plus head_leaked_text (the leaked-text
scan slices up to the phantom tag's position, landing inside the
<style> block before its real closing tag, so the raw CSS text reads
as leaked markup) on an otherwise valid sub-composition — reported
with an exact bisected repro: a <template>-wrapped SVG sub-composition
whose <style> block comments reference an inner <g> element.
Fix: compute <style>/<script> content spans up front (reusing the
existing extractBlocks + STYLE_BLOCK_PATTERN/SCRIPT_BLOCK_PATTERN) and
skip any TAG_PATTERN match that falls inside one, before it ever
reaches findRootTag or any other extractOpenTags consumer. Same shape
as the prior fix for a leading <svg> defs block being mistaken for the
root (8ee4b7df) — this closes a sibling gap in the same function.
Test: new regression case with a <style> block containing a `/* <g> */`
comment ahead of an <svg data-composition-id> root, asserting none of
the three findings fire. Full lint package suite (318 tests) passes.
* feat(lint): flag duplicate data-composition-id values
Declaring data-composition-id on more than one element (commonly the <meta>
tag from the quickstart template AND the root <div> added to satisfy
root_missing_composition_id) is a silent collision: `compositions --json`
returns two entries for the same id (one duration:0) and inspect/snapshot
crash with "Cannot read properties of undefined (reading totalDuration)".
Lint passed clean through all of it.
New rule `duplicate_composition_id`: group elements by data-composition-id
value and error on any value shared by 2+ elements, naming the id and calling
out the meta-vs-root collision in the fixHint. 3 tests: dup fires, single id
passes, two distinct ids don't collide. (Implemented via Codex; verified
independently: 111 lint tests pass, oxfmt/oxlint clean.)
* fix(audits): avoid caption false positives
* fix(lint): ignore proxy-label tween overlaps
* fix(cli): preserve the five-percent text audit floor
* fix(lint): preserve proxy identity across lexical scopes
* fix(cli): audit only directly painted text
* fix(lint): compare live composition ids canonically
* fix(lint): preserve expanded proxy identities
* fix(cli): measure directly painted text geometry
* fix(lint): preserve first duplicate attribute value
* fix(lint): keep shared proxy identity across helpers
* fix(parsers): preserve expanded proxy identity
* fix(lint): decode composition IDs consistently
Review follow-ups (both reviewers, all findings):
- resize captures scope to the resize group: convert-to-keyframes
resolvedFromValues and the whole-offset backfill pass the group filter,
so an opacity-touching intro tween can't ride into a converted scale
tween (the rotation fix's contract, now uniform across intercepts)
- commitStaticSet resolves every group's target set BEFORE committing and
coalesces groups landing on the same legacy mixed set into one commit —
the second commit can no longer chase a stale group-derived id
- installAuthoredOpacityCapture also stamps an element the moment it GAINS
data-color-grading at runtime (attributeFilter), not just at insertion
- both writer twins now share the same emitted-set dedupe shape
- applySoftReload's positional tail becomes a SoftReloadOptions object
- readAllAnimatedProperties builds the group-filtered key set immutably
instead of deleting from the set mid-iteration
- applyAuthoredInlineOpacity documents the priority-lossy round-trip
- the marquee hit-test reads activeCompositionPathRef like its neighbors
New tests: resize intercept (scale route + group filter + non-uniform
longhands), after-write-HTML / stamp / empty-stamp opacity restore, the
no-op-commit-with-missed-instant-patch soft-reload contract, and the
runtime-gained-grading stamp.
A parsed timeline set carries immediateRender in extras; the recast
statement builder ALSO pushed the flag unconditionally, so every
split/re-add of a set wrote 'immediateRender: true, immediateRender:
true' into the file, doubling on each pass. Both writer twins (recast +
acorn) now emit each vars key exactly once, properties winning over
extras, and reconcileEditableProps skips newProps keys it already
preserved.
A base `gsap.set(...)` written AFTER the tween calls is wiped on the next
soft reload: when a `from()` tween on the same target lazily initializes
during a backwards render (the studio rebind's progress(0.0001) kick), GSAP
reverts its internal isFromStart set, which removes the whole inline
`transform` — taking the base set's x/y with it. The from() tween then
re-parses the computed transform as identity and bakes x/y = 0 into the
GSAP cache, so every element previously moved in the studio snaps back to
its authored position whenever any other element is edited.
Emitting the global set BEFORE the timeline construction makes it part of
the pre-tween state the from() records, so every revert restores the moved
pose instead of stripping it.
- addAnimationToScript: global sets insert above the timeline declaration;
the new-id lookup now diffs content-based ids instead of assuming the
appended statement is last in source order.
- updateAnimationInScript: a legacy trailing global set is relocated above
the declaration whenever it's touched, healing files written before this
change on the next nudge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session
Root-causes the setTiming element_not_found resolver-shadow divergence class:
timeline edits carry hf-ids read from the live preview DOM, but the preview
minted ids AFTER rewriting attributes (and never persisted them for sub-comps),
while the SDK session mints from the raw file — content-keyed minting then
yields different ids for the same element. Template-based comps were worse:
the SDK excluded the whole <template> subtree, so the session had zero
elements and every edit diverged.
- parsers: ensureHfIds now descends into <template> subtrees (linkedom's
querySelectorAll does not), minting and pinning inner ids
- sdk: buildRoots/buildElement treat <template> as a transparent container,
and resolution (resolveScoped, animation-id map) searches template subtrees
via querySelectorAllDeep — template comps now model, resolve, and edit
- studio-server: the sub-comp preview route persists hf-ids to the raw file
BEFORE the rewrite pipeline (mirrors the main route), pinning one id space
across served DOM, disk, and SDK session
- studio: resolver-shadow skips structurally-empty sessions (no event, no
attempt) and tags fail-open emissions with sourceReadFailed so read errors
are distinguishable from unwired readers in telemetry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(parsers,sdk,studio-server,studio): scope template descent, guard persist route
Addresses the 10 verified findings from the PR #1981 review:
- Restrict template transparency to COMPOSITION templates
(<template data-composition-id>) everywhere — ensureHfIds, SDK
buildChildren, querySelectorAllDeep. A plain <template> (runtime
clone-source) keeps its old fully-excluded behavior: stamping its
interior would duplicate one persisted id across every runtime clone,
and modeling it would show phantom timeline clips.
- Guard the sub-comp persist: only .html files (the wildcard route can
serve any project path — stamping an SVG corrupted it on disk),
try/catch the read (file-removed race becomes 404, not 500), salt the
etag (v2) so pre-fix cached clients don't 304 past the id pin, and
thread the stamped content into buildSubCompositionHtml so served ids
match the mint even when the disk write is skipped.
- Rewrite querySelectorAllDeep as a document-order DOM walk — appending
template matches after top-level matches made duplicate-id tiebreaks
disagree with the preview's unwrapped DOM (wrong-element edits).
- Recurse sourceMutation.querySelectorAllWithTemplates so server-side
ops resolve ids at any template depth, matching SDK resolution.
- Replace the empty-session silent skip with ONE tagged session_empty
event per session — silence would blind the tripwire to exactly the
modeling-gap class that exposed the template bug. Attempts stay
uncounted (an unmodelable comp can't cut over).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio-server): close TOCTOU in sub-comp hf-id persist (CodeQL js/file-system-race)
Replace the route-level stat/read/persist sequence with stampFileHfIds:
validation (fstat), read, mint, and write-back all go through ONE open
file descriptor (O_NOFOLLOW where supported), so the path cannot be
swapped between validation and write. Falls back to read-only stamping
when the file isn't writable — content-keyed minting means the SDK
derives the same ids from the same bytes even without the disk write.
Addresses miguel-heygen's blocking review on PR #1981.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio-server): linear-time template-attr match (CodeQL js/polynomial-redos)
promoteTemplateCompositionId's single-pattern regex backtracked
polynomially on crafted input. Two-step match: grab each <template>
open tag linearly, then find data-composition-id within that short
tag text. Same semantics (first template carrying the attr wins).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>