Repository navigation
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Also fixes playground/vite.config.ts: landing.html was never wired into vite build's rollup inputs, so `pnpm --filter playground build` silently dropped it from dist/ (only index.html survived). Task 6's hero-GIF recording serves dist/ via `vite preview`, so this would have 404'd. Now all three entries (index, landing, gallery) are explicit build inputs. Co-Authored-By: Claude <noreply@anthropic.com>
record.ts's type-down previously drove pressSequentially straight through
eff.text, ignoring eff.mistakes entirely — an external-mode recording of a
"mistakes: true" sheet always came out with perfectly clean typing, even
though the gallery's type-down clip promises a stray keystroke that gets
backspaced and corrected live.
Extract the typo-placement decision (which characters get a preceding wrong
keystroke, and what that wrong keystroke is) into planTyping(), a pure,
DOM/Node-free function in @telekinesis/schema — a shared dependency of both
@telekinesis/core (browser) and @telekinesis/engine (Node), and already home
to comparable pure timesheet logic (layout.ts's estimateEffectDuration).
core/effects.ts's self-mode typeInto now calls the same function instead of
its own inline probability check, so a live-preview typo and a recorded typo
are provably driven by identical odds/placement — not just two hand-written
implementations that could quietly drift apart. Self-mode's observable
behavior is unchanged (verified via the existing e2e suite): with
mistakes:false, planTyping returns one plain step per character, same as
before.
record.ts's type-down case now walks planTyping's steps with real Playwright
input: for a typo'd step it presses the wrong character, pauses, presses a
real Backspace, pauses, then presses the correct character — mirroring
typeInto's write/pause/backspace/pause rhythm and its marking semantics
(both the wrong keystroke and the correct one get a sound mark; the
backspace itself stays silent, same as self mode).
Validated by recording examples/gallery/type-down.timesheet.json end-to-end
and inspecting extracted frames: a wrong character visibly appears
("motionm"), gets backspaced back to "motion", and typing continues to the
exact final text "motion is the message". Mark count came out to 22 for one
injected typo (21 base + 1), consistent with the mirrored semantics.
Co-Authored-By: Claude <noreply@anthropic.com>
…pair The gallery's drag-and-drop clip only ever showed the ghost cursor traveling — gal-drag-src's chip never moved, because the gallery page was static chrome with no drag handlers, so Playwright's real dragTo() (real mousedown/mousemove/mouseup during a recording) had nothing to respond to. Per the architectural decision for this fix, the engine deliberately does NOT drive the element itself in external mode: on a real app, the app's own handlers react to the real mouse input, exactly like a real user's drag would. The gallery page needed to actually be that "real app" for this one pair. DragDemo implements a plain pointer-driven drag — mousedown on the chip, then window-level mousemove/mouseup while dragging — deliberately not HTML5 `draggable`. Playwright's Locator.dragTo() drives a drag purely by dispatching real mousedown/mousemove/mouseup; native HTML5 drag needs dragstart/dragover/drop, which dragTo never fires, so a draggable-based implementation would never respond to a recording. The chip follows the pointer via a CSS transform (no layout shift, no re-parenting) and, on release, checks whether its center landed inside the dropzone; if so it settles centered over it and the dropzone gets a "filled" style (reusing the existing --good success accent), otherwise it snaps back to its origin. Validated by recording examples/gallery/drag-and-drop.timesheet.json end-to-end (real headless Playwright run against the built gallery page) and inspecting extracted frames at the moment record.ts's real dragTo() fires: the chip is confirmed stationary at the source through the ghost cursor's whole pre-visualization glide, then jumps to the destination in the same ~100ms window as the real mousedown/mousemove/mouseup, and stays settled there for the rest of the clip (checked across a dozen later frames, including through the subsequent highlight effect). Co-Authored-By: Claude <noreply@anthropic.com>
… mistakes coverage record.ts's type-down case called planTyping(text, mistakes) without startNonEmpty, so external-mode recordings could never plant a first-char typo on a pre-filled field even though self-mode's typeInto (which derives startNonEmpty from its own buffer) could — a parity break the surrounding comment claimed didn't exist. Fix: read the field's live value (inputValue, falling back to textContent for contenteditable) before planning, mirroring effects.ts exactly. Also extracts pressAndWait to de-duplicate the press/mark/wait idiom in the typing loop, and rewords planTyping's startNonEmpty doc comment now that neither call site hardcodes a value. Adds e2e/tests/typing-mistakes.spec.ts: deterministic mistakes:true coverage for both consumers, forcing exactly one typo via a pinned Math.random. Two undocumented traps discovered while building it, both worked around rather than assumed away: (1) Playwright's own internals (randomIntInRange) and, in this repo's sourcemapped test run, even the act of reading Error().stack itself (via a randomized-quicksort prepareStackTrace hook) consume Math.random before user code runs, so a fixed-position queue must be installed *after* that unrelated consumption — a throwaway priming call in self mode, record()'s onStep hook in external mode — never blindly wrapped around the whole call. Co-Authored-By: Claude <noreply@anthropic.com>
DragDemo's mouse-based drag had two bugs: window-level mousemove/mouseup listeners registered in handleMouseDown were never removed if the component unmounted mid-drag (a real leak — window outlives the component), and a release outside the viewport would never fire mouseup at all, leaving the drag permanently stuck. Replaces mouse events with Pointer Events: pointerdown captures the pointer on the chip itself, so pointermove/pointerup listen on the chip (not window) and keep firing wherever the pointer actually is — including outside the viewport. pointercancel and lostpointercapture both snap back, same as missing the dropzone. A useEffect unmount cleanup removes any still-attached listeners as a safety net; normal drag-end already does this itself. Re-verified empirically end-to-end, since Playwright's Locator.dragTo() drives input via low-level mouse dispatch, not literal pointer events: built the playground, booted vite preview, and recorded examples/gallery/drag-and-drop.timesheet.json with @telekinesis/engine's record() (both directly and via hand-instrumented reproductions of record.ts's exact call sequence). Confirmed via ffmpeg frame extraction and direct DOM/computed-style checks that Chromium synthesizes genuine (isTrusted) pointer events from CDP-dispatched mouse input, and the chip settles centered in the dropzone with the correct .filled green border after its transition. (An initial frame-extraction pass wrongly suggested the drag was broken — the timestamps sampled were simply earlier than the real settle point, roughly ~2.3s into the drag step once the ghost-cursor pre-animation and dragTo()'s own overhead are accounted for; onStep timing and a denser resample resolved it.) Also: reuses Point/rectCenter from @telekinesis/core instead of a hand-rolled twin (rectCenter newly exported — additive) in App.tsx, and renames .is-dragging/.is-filled to .dragging/.filled to match the file's existing unprefixed state-class convention (.card.featured, .cta.ghost). .is-dropped was left as-is — out of the literal rename scope given, though it's now the odd one out; worth a follow-up. Co-Authored-By: Claude <noreply@anthropic.com>
The gallery drag's settle math (endDrag in DragDemo) centers the chip on
the dropzone's rect *after* the drop, but until now the label swap
(`{dropped ? "" : "Drop here"}`) unmounted the "Drop here" text at the
exact same moment, collapsing the dropzone's shrink-to-fit box (measured:
~90.66x49 -> ~24x32) and moving its center ~33px left of where the chip
actually landed. Compounding cause: `flex: 1` on `.gallery-chip`/
`.gallery-dropzone` was inert — TelekineticFrame's own wrapper div (not
those boxes) is the flex row's actual direct child, so both boxes were
always sized by content, never by the row.
Fix: keep the "Drop here" label mounted (only visually hidden via CSS once
`.filled`) so the dropzone's own box never changes size on drop, and move
`flex: 1` onto `.gallery-drag-row > [data-telekinesis-frame]`, the wrapper
that's actually a flex item. Verified by measurement, not eyeball: a
headless drag now lands the chip's center within <0.01px of the
dropzone's center (was off by 33.33px before), captured via a temporary
git-stash before/after comparison. New e2e/tests/gallery-drag.spec.ts is
the permanent guard — drives the drag the way record.ts does
(`Locator.dragTo(dest, { force: true })`) and asserts the two centers
stay within 3px with `.filled` present.
Also: handlePointerDown now guards against reentrant drags and non-primary/
non-left-button pointers, and every drag listener re-checks `ev.pointerId`
against the pointer that started the drag (pointer capture only retargets
that pointer's own events, not a second uncaptured one). Adds
`touch-action: none` to the chip so a touch drag doesn't fight the
browser's own scroll/pan gesture. Finishes the `is-dropped` -> `dropped`
rename flagged as a follow-up in 56f7384.
Co-Authored-By: Claude <noreply@anthropic.com>
… spec record.ts's typing loop presses a real Backspace to correct a planted typo, but that press is deliberately unmarked (same as self mode), so the external-mode mistakes test's mark-count assertions can't see it — deleting the Backspace press leaves that test green even though the field ends up with an uncorrected stray character. Mutation-tested to confirm: temporarily removed the Backspace press, re-ran the suite, and only the new value assertion below failed (typed value became "kaeyboard" instead of "keyboard"; both mark-count/profile assertions still passed unchanged), then restored it. Fix: add `RecordOptions.afterTimeline?: (page: Page) => void | Promise<void>`, invoked with the live page once the timeline finishes (after `durationMs` is captured, before the context closes) so a caller can inspect real in-page state before teardown — additive/optional, no existing caller affected. typing-mistakes.spec.ts's external test now uses it to read `gal-input`'s real `inputValue()` and assert it equals the intended text, closing the phantom-coverage gap. Also: custom expect messages on both tests' `marks`/`audioMap.marks` `toHaveLength` assertions, pointing at Math.random forcing-queue contamination as the likely cause of a mismatch (see the file's own comments on where that contamination comes from in each mode). Co-Authored-By: Claude <noreply@anthropic.com>
Split @telekinesis/core's drag-and-drop visual into two independently callable phases — dragApproach (cursor travels to the source + press pulse) and dragGlide (cursor glides to the destination) — and expose both on window.__telekinesis (runDragApproach/runDragGlide) alongside the existing atomic runEffect, which still runs both back-to-back for every other caller (self-mode preview, Studio playback, a bare external runEffect call) with no behavior change. record.ts's "drag-and-drop" case now awaits only the short approach leg, then fires the destination glide without awaiting it while driving a real, stepped Playwright mouse drag paced to the same eff.duration, and finally awaits the glide's completion. The gallery's DragDemo chip tracks real Pointer Events (setPointerCapture), so it now visibly rides along with the ghost cursor across the whole trip instead of the previous choreography, which awaited the entire visual first and only then snapped the real drag into place afterwards — cursor glides, a beat of dead air, the dragged element teleports. Verified by re-recording examples/gallery/drag-and-drop.timesheet.json and inspecting the extracted GIF frames: the chip is visibly mid- transit across multiple consecutive frames, riding with the cursor, with no dead pause, and ends settled in the dropzone under the highlight spotlight. Full e2e suite (25 tests) stays green. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
drag-and-drop's synchronized carry fires the destination glide (runDragGlide) without awaiting it, then races a real stepped mouse drag before awaiting it later. Between the fire and that later await, an unhandled rejection on glideDone (e.g. execution context destroyed mid-drag) would crash the whole Node process — skipping every `finally` and taking out the rest of a multi-GIF record-gallery batch over one flaky drag. Attach a no-op `.catch` on a separate chain right after firing so the rejection is marked handled; the later `await glideDone` still rethrows it normally through its own chain. Also comment the dragApproach/dragGlide split at its effects.ts call site: it exists so external-mode recording can fire the glide concurrently with the real drag, while self-mode (and every other caller) still awaits both phases back-to-back, unchanged from before the split. Co-Authored-By: Claude <noreply@anthropic.com>
The committed hero.gif ships at fps 10, but record-gallery.ts's uniform fps 12 default reproducibly regenerates it at ~905-908KB — over the 900KB per-GIF budget — breaking the "GIFs regenerate only by the script" rule for that one job. Add a JOB_OVERRIDES map (hero -> fps 10) resolved by resolveFps, precedence env TK_GALLERY_FPS > override > default, applied where fps flows into toGif. Re-recorded public/gallery/hero.gif via `TK_GALLERY_ONLY=hero pnpm gallery:record` itself (827584 bytes, within budget). Sanity-checked the no-override path with TK_GALLERY_ONLY=zoom-in — the regenerated zoom-in.gif is discarded here (not committed) to avoid encode-variance noise. Co-Authored-By: Claude <noreply@anthropic.com>
record.ts's real, stepped Playwright drag hardcoded its own local easeOutQuad for every drag-and-drop, while the ghost cursor's visual glide (dragGlide -> GhostCursor.moveTo) resolves its curve through cursor.ts's curveForEasing, which substitutes fittsEase for the schema default "ease-in-out" and jsEasing[easing] for everything else. The two curves only coincidentally overlapped in shape; for "linear" they diverge 25pp at t=0.5, and even for the *default* easing (what the gallery's own drag-and-drop.timesheet.json uses) fittsEase vs. the old easeOutQuad diverge ~14pp at t=0.5 — the dragged element could visibly drift off the cursor "carrying" it mid-trip. easeOutQuad was also a character-for- character duplicate of jsEasing["ease-out"], which it wasn't even using. Fix: both sides now resolve through the exact same curveForEasing. Implementation choice: moved the pure curve math (jsEasing, fittsEase, curveForEasing — no DOM references) from @telekinesis/core into @telekinesis/schema, and re-export it unchanged from core's easing.ts/ cursor.ts. Considered having engine depend on @telekinesis/core directly instead (importing curveForEasing from there), but core's package "exports" map only exposes its full index.ts barrel, which re-exports the React components (react/TelekineticFrame, react/TelekinesisStage) alongside the DOM-heavy runtime; engine's tsconfig resolves workspace packages straight to source (no project references/prebuilt .d.ts isolation) and restricts "types" to ["node"] (vs. core's ["react"]), so pulling core's whole barrel into engine's program risked breaking engine's typecheck on JSX/React ambient types for code engine would never use. Routing through schema instead is the smaller, honest diff: schema already has zero DOM dependencies, engine already depends on it (zero new dependency edges), core's public API is unchanged (every existing import of jsEasing/ fittsEase from "@telekinesis/core" or from "./easing"/"./cursor" keeps resolving to the same values via re-export), and there's no dependency cycle (schema -> nothing; core -> schema, pre-existing; engine -> schema, pre-existing). cssEasing (the CSS transition-timing-function table) deliberately stays in core — it's browser/CSS-specific and engine's Node-side recorder never touches it, so moving it would have widened the diff without serving this fix. Deleted the duplicated easeOutQuad. All 117 unit tests pass unchanged (easing.test.ts and cursor-motion.test.ts exercise jsEasing/fittsEase via core's re-exports, proving the chain works at runtime, not just at the type level), and every package's typecheck is clean, including engine's new value import of curveForEasing from @telekinesis/schema. Co-Authored-By: Claude <noreply@anthropic.com>
…s, failure recap
Four fixes to e2e/scripts/record-gallery.ts, all from real pain running it:
1. The budget guard wrote toGif's output straight to the tracked
public/gallery/<name>.gif and only checked its size *after*, so a run
that busts the 900KB per-GIF budget left a dirty oversized file sitting
in the tracked path (reproduced twice locally). Fixed: every job now
encodes into tmpOutDir (public/gallery/.tmp/<name>.gif — same
filesystem as the final path, so the rename below is never a
cross-device EXDEV; nested under outDir but excluded from the
total-budget scan since readdir(outDir) doesn't recurse), stats *that*,
and only renames onto the tracked path on a pass. A failing job's tmp
output is deleted, never renamed. main() also wipes+recreates
tmpOutDir up front so a prior hard-crash's leftovers can't linger.
2. JOB_OVERRIDES generalized from `{ fps?: number }` to
`{ fps?: number; width?: number }`, with a new `resolveWidth` mirroring
`resolveFps` (env TK_GALLERY_WIDTH > override > DEFAULT_WIDTH;
`resolveWidth`'s doc comment points back to `resolveFps`'s instead of
restating the precedence rule). Added overrides for zoom-in/zoom-out:
`{ fps: 10, width: 440 }`. Both rescale the entire viewport every frame,
defeating GIF palette compression — they regenerate at 1017-1061KB at
the global fps12/width480 defaults, over the 900KB budget. The
previously-committed 816KB/766KB bytes for these two were themselves
just within this encoder's ~±25-30% run-to-run size variance at those
settings, not evidence that fps12/480 reliably fits — fps10+width440
measured 602KB, a real 33% margin instead of a gamble.
3. A mid-batch failure used to skip straight to main().catch, so a run
that died on job 4 of 12 never showed what the first 3 actually
measured. The per-job loop now has a catch that prints the rows
collected so far as a "jobs completed before the failure" table before
rethrowing (finally still kills the preview server either way).
4. The two thrown error strings that were in Spanish/mixed language are
now English, same information, e.g. "<name>.gif weighs XKB — trim the
timesheet or lower width/fps" and "...over the 10MB budget; trim
timesheets or lower width/fps".
No behavior change for a passing run other than the width knob; e2e
package typecheck and the full monorepo typecheck are both clean.
Co-Authored-By: Claude <noreply@anthropic.com>
hero.timesheet.json's scroll-down covered ~970px in 600ms — at hero's fps 10 (JOB_OVERRIDES), that's too few distinct GIF frames for the scroll to read as motion; it played as a jump-cut. Slowed to 900ms, matching scroll-down.timesheet.json's own scroll duration. Verified on the regenerated hero.gif (gifsicle frame listing + pixel diff across consecutive frames): the scroll window now spans 10 distinct stored frames (vs. the old 2-3), with real progressive content change frame to frame (5-35% of pixels changing per tick, nav bar receding and new content entering by the window's end) — motion, not a cut. Regenerated via the script (`TK_GALLERY_ONLY=<name>`, playground built, `pnpm --filter @telekinesis/e2e gallery:record`), not by hand: GIF old (committed) new budget hero.gif 828KB 854KB 900KB (46KB margin) zoom-in.gif 817KB 597KB 900KB (303KB margin) zoom-out.gif 766KB 604KB 900KB (296KB margin) drag-and-drop 203KB 211KB 900KB (689KB margin) - hero: re-paced scroll (this commit) plus whatever this run's encode variance was; still comfortably under budget. - zoom-in/zoom-out: the previous commit's new fps10/width440 overrides landing where they were measured to. - drag-and-drop: engine now drives the real pointer through the same curveForEasing curve the ghost cursor uses (prior commit) instead of the old easeOutQuad — a few KB of encode difference from a different motion shape, still tiny next to budget. drag-and-drop.gif frame-verified the same way: gifsicle's frame list + pixel diffs show the chip riding rightward across at least 4 distinct mid-transit frames (source -> ~1/3 -> ~2/3 -> arrived, each a clearly different on-screen position, cursor still attached throughout — the synchronized-carry choreography this task's Commit 1 curve fix targets), then a sharp frame-to-frame jump (~90% of pixels) as the trailing highlight spotlight confirms the drop and the chip sits settled inside the dropzone. public/gallery totals 4.02MB across all 12 GIFs (10MB budget). The other 8 GIFs are untouched — `git diff --stat` against each is empty. Only examples/gallery/hero.timesheet.json and these 4 binaries are committed; no source changes. Co-Authored-By: Claude <noreply@anthropic.com>
…yet mirror the flySpring branch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red on the README assertions by design; the next commit's README turns it green (plan 5 task 7→8 TDD). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… timesheet Every effect in the README now ships with the GIF that shows it and the literal timesheet that filmed it, plus a hero GIF up top recorded by Telekinesis about Telekinesis. Turns green the coverage spec committed in the prior commit (plan 5 task 7→8 TDD). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hero.gif was the one GIF of the twelve whose README embed nothing asserted — stripping the hero <img> would have slipped through. Also align the budget comment with the strict toBeLessThan the assertions actually use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verage asserts - Hero test now also asserts examples/gallery/hero.timesheet.json exists, parses, and has a non-empty timeline, mirroring the per-action block. - New orphan-heading test: every `#### `name`` section in the README must map back to an EFFECT_ACTIONS entry or "hero", catching a stale section left behind after an effect rename. - Anchor the per-action heading regex to a whole line (^...$ with "m"), so it can no longer match inside a deeper heading level (#####/######) or a backticked mention of the action sitting mid-sentence. - Guard every file check with existsSync (custom message) instead of letting statSync throw a raw ENOENT, and make every assertion in the per-action and hero tests expect.soft, so a brand-new action reports all of its gaps (missing sheet, GIF, README refs) in one run. - Fix the double-slash path bug: repoRoot (from a bare "../.." URL) keeps a trailing slash, which template-literal concatenation was silently doubling up against; switched to path.join throughout, matching examples.spec.ts's own convention. Co-Authored-By: Claude <noreply@anthropic.com>
- click: the sound doesn't time itself to "the exact frame the ripple starts" — traced through effects.ts and record.ts, the ripple plays out fully first, and the sound lands with the real Playwright click right after it. - type-down: `mistakes: true` doesn't "plant exactly one typo per run" — packages/schema/src/typing.ts rolls TYPO_RATE (0.06) independently per eligible character with no cap, so a run can land clean or catch a few. - Replace all 11 generic "<action> effect" alt attributes with short, concrete first-clause descriptions of what's on screen. Hero's alt is left as-is. Co-Authored-By: Claude <noreply@anthropic.com>
…rding Co-Authored-By: Claude <noreply@anthropic.com>
The root workspace has no playwright dependency, so the unscoped 'pnpm exec playwright install' hard-fails on a clean runner (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL) — first exposed by the first-ever live run of this workflow_dispatch-gated job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… contract, not byte determinism Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…easings Plan 1 backlog, Task 1 (docs/superpowers/plans/2026-07-06-01-animation-transition-library.md). Test-first: extended packages/core/test/easing.test.ts's EXPECTED_EASINGS/ MONOTONIC/OVERSHOOTING lists before the schema had the values, confirming both a runtime assertion failure and (per pnpm typecheck) the expected compile error on the two EasingPattern[]-typed arrays. - schema/src/easing.ts: append the 3 names to the EasingPattern enum (additive only, per its own doc comment) and their closed-form jsEasing curves (easings.net forms) — Record<EasingPattern, ...> makes the compiler enforce completeness. - core/src/easing.ts: matching cssEasing cubic-bezier fallbacks, same completeness guarantee via Record<EasingPattern, string>. Co-Authored-By: Claude <noreply@anthropic.com>
…Spring Debt tracked in docs/superpowers/README.md's Plan 5 execution notes and in curveForEasing's doc comment (packages/schema/src/easing.ts): a drag-and-drop authored with easing: "spring" ran packages/engine/src/ record.ts's real pointer through curveForEasing's fixed-duration cubic-bezier approximation, while the ghost cursor's glide (GhostCursor.moveTo, core/src/cursor.ts) branched to flySpring — a true, variable-duration physics integration — before ever reaching that same function. No shipped timesheet used the combination, but the two legs would have visibly drifted apart on both self-mode (the dragged element's CSS transition already used the fixed-duration approximation) and external/recorded drags. Resolution (option a from the plan): teach GhostCursor.moveTo an opt-in escape hatch, MoveOptions.approximateSpring, that skips the flySpring branch and resolves "spring" through curveForEasing instead. dragGlide (core/src/effects.ts) sets it unconditionally on its cursor.moveTo call — a no-op for every non-spring easing, and for spring specifically it trades the nicer true-spring feel for guaranteed lockstep across all three legs of a drag (self-mode element, ghost cursor, external real pointer). Every other moveTo caller (cursor-move, click, a drag's own ease-out pick-up approach) is unaffected. Updated curveForEasing's doc comment to describe the closed gap instead of the dormant desync. Extended the existing e2e drag-and-drop coverage (e2e/tests/effects.spec.ts) with a spring-eased variant as a cheap regression guard for the new wiring. Co-Authored-By: Claude <noreply@anthropic.com>
…back-ease undershoot Review follow-up on 227d519 (three findings): - e2e/tests/effects.spec.ts: the spring-drag test's settle-position assertion had no teeth — GhostCursor.moveTo ends with an exact place(target) snap on BOTH branches, so reverting approximateSpring could not fail it. Rebuilt it around the elapsed ms runEffect already returns, over a long (~600px) explicit destX/destY carry where the branches actually diverge: approximation path measured ~1480ms (stable across 4 runs: 1475-1487) vs flySpring path ~2200ms (glide 150ms vs travel-spring physics ~860ms). Bound at 1800ms, >=300ms margin each way; every phase of the passing path is a wall-clock duration or cap, so CI jank costs ~a frame per phase. Verified by mutation: flipping the flag to false in dragGlide fails the test (Expected < 1800, Received 2201); restoring it passes. The comment now states exactly what the test does and doesn't catch (real Playwright pointer out of scope — record.ts owns that side). - packages/core/test/easing.test.ts: assert ease-in-out-back's documented undershoot (min ~ -0.100 near t~0.24) via an UNDERSHOOTING case — spring/ease-out-back deliberately excluded, they never dip below 0. - packages/schema/src/easing.ts: tightened curveForEasing's paragraph 4 ~40% (17 -> 11 lines): cut the self-referential opener, compressed the before/after history to two sentences. Paragraphs 1-3 untouched. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan 5 del backlog superpowers — completo
Este PR ejecuta el backlog de
docs/superpowers/sobre una única rama de desarrollo; el Plan 5 (QA + galería autograbada + README) está completo y los Planes 1-4 aterrizarán en esta misma rama.Qué entrega
e2e/(@playwright/test): 40 specs — runtime real en Chromium, huella-cero en modo usuario, invariantes observables de los 11 efectos, pipeline completo record→mix→gif contra ffmpeg real, ejemplos como contrato de schema, typos deterministas en ambos modos, y el gate de cobertura triple efecto↔GIF↔README.pnpm gallery:record— Telekinesis grabándose a sí misma. 12 GIFs committeados (11 efectos + hero condensado de la landing), presupuesto duro ≤900KB/GIF y ≤10MB total que falla en rojo (validate-before-publish, overrides por clip medidos).examples/gallery/, garantizado por test) + sección Quality.e2ebloqueante en push/PR (browser + ffmpeg, sin skips silenciosos) y regrabación de galería bajo demanda en el jobmotion(artifactsgallery-gifs). Run verde de los 3 jobs: 29261031483.mistakes: truecon pulsaciones reales (typo → backspace → corrección), hookafterTimeline, curvas de easing compartidas Node/navegador.Muestra
Puerta de calidad
pnpm typecheck(10 workspaces) ·pnpm test(117 unit) ·pnpm build·pnpm e2e(40/40, 21.4s) · galería 3.9MB/10MB · CI verde end-to-end.Cada task pasó implementación + review de spec + review de calidad con subagentes independientes; las notas de ejecución y desviaciones están en
docs/superpowers/README.md.🤖 Generated with Claude Code