A sub-composition ROOT is addressed by its data-composition-id, but the SDK's
whole element<->tween attribution is data-hf-id based, so the prior fix's
[data-composition-id] selector was invisible to three readers (validateOp/can,
selectorMatchesId -> setTiming + removeElement cascade, buildAnimationIdMap ->
getElement.animationIds), diverging can from apply and orphaning tweens.
Root fix: make composition ids first-class resolvable addresses and emit the
canonical selector everywhere.
- resolveScoped (model.ts): for a bare id with no data-hf-id match, fall back to
[data-composition-id]. data-hf-id keeps precedence; scoped-path and canonical
behavior intact. Fixes validateOp gating, findById/getElement, and every op
handler for comp-root targets in one place.
- gsapTargetSelector (mutate.ts): resolve the target and emit
[data-hf-id="<resolved host hf-id>"] (canonical). Normal targets unchanged;
comp-root targets resolve via comp-id -> host -> host hf-id. Defensive
[data-composition-id] only when the resolved element has no hf-id.
- setTiming syncs the GSAP tween via the resolved element's data-hf-id so a
comp-root target matches its host tween; removeElement cascade already covers
the host hf-id via collectSubtreeHfIds.
- export escapeHfId; escape both the querySelector probe and the emitted
selector string.
Tests: comp-id resolveScoped fallback + precedence (session.subcomp), canonical
selector, validateOp accept, setTiming sync, removeElement cascade, and
getElement.animationIds for comp-root tweens (mutate.gsap). The prior test only
called applyOp, masking all of this.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sdk): setStyle removes hyphenated properties (was kebab/camel key mismatch)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(sdk): agree removeElement/getElement on duplicate bare ids
A bare hf-id duplicated across a sub-composition element and a top-level
element resolved to different instances: removeElement → resolveScoped →
querySelector (document-order-first, the inner sub-comp dup) while getElement
preferred the canonical match (scopedId === id, the top-level dup). So
removeElement(bareId) removed the inner instance and getElement(bareId) still
found the surviving top-level one — they disagreed.
resolveScoped now resolves an ambiguous BARE id to the canonical (top-level)
instance via isCanonicalScope (walks ancestors for isNewHostBoundary), falling
back to document order when no canonical match exists — matching getElement.
Fully-scoped paths (hf-host/hf-dup) and non-duplicated bare ids are unchanged.
Surfaced by SDK shadow parity (op:delete expected removed, actual present).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sdk): stage 6 — sub-composition scoped ids (F9)
Adds fully-qualified scoped ids for addressing elements inside inlined
sub-compositions, so callers can target "hf-HOST/hf-LEAF" unambiguously
even when bare hf-ids collide across sub-composition boundaries.
Changes:
- model.ts: resolveScoped() traverses id segments through nested subtrees;
isNewHostBoundary() detects host boundaries (dcf ≠ parent dcf handles
outerHTML innerRoot edge case)
- types.ts: HyperFramesElement gains scopedId field
- document.ts: buildElement carries scopePrefix, propagates childPrefix
at host boundaries; buildRoots starts with ""
- patches.ts: RFC 6902 escapeIdForPath / decodePathSegment for scoped ids
containing "/"; all path builders and pathToKey/keyToPath updated
- session.ts: getElement() matches by scopedId; find() returns scopedIds;
orphan cleanup decodes RFC 6902 before key comparison, preserves removal
markers, purges property sub-keys for both bare and scoped ids
- mutate.ts: all element handlers use resolveScoped instead of findById;
handleRemoveElement collects full subtree hf-ids before removal for
complete GSAP animation cascade (Q3 fix); validateOp uses resolveScoped
20 new contract tests in session.subcomp.test.ts covering resolveScoped,
scopedId propagation, dispatch to scoped targets, RFC 6902 patch encoding,
override-set key format, orphan purge, and serialize stability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(sdk): add find({ composition }) filter — Stage 6 WS-C completion
Closes the last headless-testable Stage 6 gap (F9 workstream C).
`find({ composition: "hf-host" })` returns all scopedIds whose prefix
matches the given host id — i.e. every element mounted inside that
sub-composition, at any depth. Combinable with other FindQuery fields
(tag, text, name, track). 3 new contract tests in session.subcomp.test.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sdk): addGsapTween resolves scoped id to bare leaf; validateOp checks target exists
- handleAddGsapTween: strip host prefix for scoped ids (hf-host/hf-leaf →
selector [data-hf-id="hf-leaf"]) — DOM element carries only the leaf part
- validateOp addGsapTween: call resolveScoped to surface E_TARGET_NOT_FOUND
before the GSAP script checks (previously can() returned ok for missing targets)
- patches.ts pathToKey: remove dead ?? null (decodePathSegment never returns undefined)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>