feat(sdk): ws-3 — reorderElements op (batch z-index update) (#1502)

* feat(sdk): ws-3 — reorderElements op (batch z-index update)

Adds the reorderElements EditOp: each entry sets inline zIndex on one element.
Last-write-wins per target so a duplicated target collapses to a single zIndex
patch. Positioning is unchanged — z-index only takes effect on non-static
elements, so the caller must ensure the target is positioned.

Also fixes single-dispatch undo to reverse the inverse patch list (parity with
batch()): an op emitting multiple patches whose undo order matters — a duplicated
reorderElements target, an aliased multi-target, or a nested parent+child
removeElement — must undo in reverse application order, or undo lands on an
intermediate value / drops a subtree.

validateOp resolves every entry target (E_TARGET_NOT_FOUND for unknown ids;
empty entries is a clean no-op). Tests cover set/inverse/validate/duplicate-target.

Rebuilt standalone on main (reorderElements only depends on handleSetStyle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(sdk): remove unused HTTP persist adapter

The HTTP PersistAdapter (createHttpAdapter) was dead weight after the studio
cutover went single-writer (ws-4): Studio's writeProjectFile is the sole writer
and useSdkSession opens with no persist queue, so the adapter's write/flush/
listVersions/loadFrom were never used — only read() was, to fetch the
composition source. Replace those two read() calls with a direct optional fetch
(GET /files/<path>?optional=1) and drop the adapter + its export-map entries.

Saved for later re-introduction (when a non-Studio SDK host needs server-backed
persist) at docs/hyperframes/plans/sdk-http-adapter/ (outside the repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(studio): resolver shadow for z-index reorder targets

The z-index reorder commit takes the server path (no SDK persist), but the
resolver-shadow tripwire is decoupled from cutover — so it should still record
whether the SDK resolves each reordered element (reorderElements' targets), the
same as timing/delete already do before their cutover gate. This gives wild
resolver-parity telemetry on z-index targets before z-index reorder is cut over.

Threads an onReorderShadow callback (sdkSession-bound, mirrors onTrySdkDelete)
from useDomEditSession → useDomEditCommits → useElementLifecycleOps, called with
the reordered elements' hf-ids in handleDomZIndexReorderCommit. Read-only,
divergence-only, never throws — same contract as recordResolverParity elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(studio): guard project-file read path against traversal (CodeQL CSRF)

readProjectFileOptional interpolated a user-influenced composition path into the
fetch URL, which CodeQL flagged as client-side request forgery. Reject NUL/`..`
up front (mirrors the existing guard in timelineEditingHelpers) and
encodeURIComponent the projectId too, so both values stay confined to single
segments of the same-origin URL. Unsafe path → undefined (graceful for the
optional read).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-06-17 23:51:47 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent de87f3932e
commit 28bfe09f21
11 changed files with 154 additions and 452 deletions
@@ -28,6 +28,8 @@ interface UseElementLifecycleOpsParams {
clearDomSelection: () => void;
/** Route delete through SDK when session resolves the hf-id; returns true if handled. */
onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
onReorderShadow?: (targets: string[]) => void;
/** Resync the SDK session after a server-fallback delete. */
forceReloadSdkSession?: () => void;
commitPositionPatchToHtml: (
@@ -49,6 +51,7 @@ export function useElementLifecycleOps({
reloadPreview,
clearDomSelection,
onTrySdkDelete,
onReorderShadow,
forceReloadSdkSession,
commitPositionPatchToHtml,
onElementDeleted,
@@ -168,6 +171,11 @@ export function useElementLifecycleOps({
}>,
) => {
if (entries.length === 0) return;
// Resolver shadow (telemetry-only, decoupled from cutover): record whether
// the SDK resolves each reordered element — the reorderElements op's targets.
onReorderShadow?.(
entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
);
const coalesceKey = `z-reorder:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
@@ -202,7 +210,7 @@ export function useElementLifecycleOps({
).catch(() => undefined);
}
},
[commitPositionPatchToHtml],
[commitPositionPatchToHtml, onReorderShadow],
);
return {