* 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>