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>
Renames the motion-surfacing tool from `hyperframes keyframes` to `hyperframes motion`,
renames the implementation from keyframes*.ts to motion*.ts (keeping the keyframe data
model name where still accurate), and renames the shipped skill from
hyperframes-keyframes to hyperframes-motion. Expands the skill from a command
reference into a full motion-design workflow: reading motion, 3D angle verification,
layered GSAP motion, one-shot reference reproduction, diagnostic checks, and
eval-derived craft guidance.