Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,56 @@
pre-commit:
parallel: true
commands:
lint:
jobs:
- name: lint
glob: "*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}"
run: node_modules/.bin/oxlint --no-error-on-unmatched-pattern {staged_files}
format:
glob: "*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,json,md,yaml,yml}"
# Auto-format and re-stage so the committed snapshot is always formatted.
# Replaces --check which only reports — that left unformatted files in
# commits when the hook ran after the amend snapshot was taken.
run: node_modules/.bin/oxfmt --no-error-on-unmatched-pattern {staged_files} && git add -f {staged_files}
skills-manifest:
glob: "skills/**"
# Regenerate the freshness fingerprint when a skill changes, then re-stage
# it so skills-manifest.json (repo root) never drifts from skills/ (CI
# enforces the same via the "Skills: manifest in sync" job). Churn-free:
# the generator rewrites only when a content hash actually changed.
run: bun packages/cli/scripts/gen-skills-manifest.ts && git add skills-manifest.json
typecheck:
# format rewrites staged files and skills-manifest hashes them, so they run in order:
# in parallel the hash could be taken from a file the formatter was still reflowing.
- group:
piped: true
jobs:
- name: format
glob: "*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,json,md,yaml,yml}"
# Auto-format and re-stage so the committed snapshot is always formatted.
# Replaces --check which only reports — that left unformatted files in
# commits when the hook ran after the amend snapshot was taken.
run: node_modules/.bin/oxfmt --no-error-on-unmatched-pattern {staged_files} && git add -f {staged_files}
- name: skills-manifest
glob: "skills/**"
# Regenerate the freshness fingerprint when a skill changes, then re-stage
# it so skills-manifest.json (repo root) never drifts from skills/ (CI
# enforces the same via the "Skills: manifest in sync" job). Churn-free:
# the generator rewrites only when a content hash actually changed.
run: bun packages/cli/scripts/gen-skills-manifest.ts && git add skills-manifest.json
- name: typecheck
glob: "*.{ts,tsx}"
run: cd packages/core && node_modules/.bin/tsc --noEmit && cd ../studio && node_modules/.bin/tsc --noEmit && cd ../.. && node_modules/.bin/tsc --noEmit -p scripts/tsconfig.json
# Mirrors the CI gate (same `--base origin/main` so the local hook can't
# pass on a branch CI would fail). Audits the working tree, which means
# unstaged WIP in `packages/**` is part of the diff — stash before
# committing if that surprises you. `--gate new-only` (the default) only
# fails on issues introduced by the branch, not inherited findings.
fallow:
- name: fallow
glob: "packages/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
# Unset git worktree env vars: fallow creates a temp worktree internally and
# GIT_DIR/GIT_INDEX_FILE (set by git in worktree hook context) break that.
# env -u is safe in non-worktree contexts (no-op when var is unset).
# fallow reuses one base-snapshot cache per checkout; `audit-cache prune` drops caches unused for a day,
# including those of removed checkouts. The commit gets the audit's own status.
run: env -u GIT_DIR -u GIT_INDEX_FILE -u GIT_WORK_TREE sh -c 'node_modules/.bin/fallow audit --base origin/main --fail-on-issues; status=$?; node_modules/.bin/fallow audit-cache prune --max-age-days 1 > /dev/null; exit $status'
cli-process-ownership:
- name: cli-process-ownership
glob: "packages/cli/src/**/*.ts"
run: bun run check:cli-process-ownership
largefiles:
- name: largefiles
# Rejects staged binaries over the limit that are not LFS pointers; see
# the script header. CI runs the same script with --range.
run: ./scripts/check-large-files.sh
tracked-artifacts:
- name: tracked-artifacts
# Keep ignored dependency trees and platform metadata out of commits.
# `git ls-files` observes the staged index, so staged removals pass and
# an accidental force-add fails before the commit is created.
run: bun run check:tracked-artifacts
filesize:
- name: filesize
# Scoped to packages/studio — the 600 LOC limit is a studio architecture
# standard enforced as part of the App.tsx decomposition work. Player and
# other packages enforce size discipline via code review and convention.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"player:perf": "bun run --filter @hyperframes/player perf",
"format:check": "oxfmt --check .",
"knip": "knip",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-cli-lazy-deps.test.mjs scripts/file-url-paths.test.mjs scripts/check-studio-copy-dashes.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/clawhub-unlist-retired.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/check-edit-accuracy-filter.test.mjs scripts/edit-accuracy-retry.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/rewrite-relative-import-extensions.test.mjs scripts/lint-skills.test.mjs scripts/check-core-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/flake-gate.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-theme-fallbacks.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs registry/components/filmed-screen/filmed-screen.test.mjs registry/components/echo-smear/echo-smear.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-cli-lazy-deps.test.mjs scripts/file-url-paths.test.mjs scripts/check-studio-copy-dashes.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/clawhub-unlist-retired.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/check-edit-accuracy-filter.test.mjs scripts/edit-accuracy-retry.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/rewrite-relative-import-extensions.test.mjs scripts/lint-skills.test.mjs scripts/check-core-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/lefthook-order.test.mjs scripts/ci/flake-gate.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-theme-fallbacks.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs registry/components/filmed-screen/filmed-screen.test.mjs registry/components/echo-smear/echo-smear.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
"generate:previews": "tsx scripts/generate-template-previews.ts",
Expand Down
15 changes: 10 additions & 5 deletions packages/studio/tests/e2e/edit-accuracy/case.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -678,28 +678,33 @@ async function waitingQuad(page, map) {
return quad;
}

export async function sample(ctx, gesture, point, pointerScreen) {
/** The box as the preview shows it: the waiting marker's quad while a press waits for its replay, else the element. */
export async function measureShown(ctx, gesture, read = measure, quadOf = contentQuad) {
for (;;) {
const waiting = await ctx.page.$("[data-dom-edit-press-waiting]");
try {
const m = await measure(ctx);
const m = await read(ctx);
if (gesture === "crop") m.outline = await cropOutline(ctx, m.map);
const quad = waiting && (await contentQuad(waiting)).map(m.map.toComp);
const quad = waiting && (await quadOf(waiting)).map(m.map.toComp);
// Replay removes the marker before moving the element. Do not combine a pre-replay
// element read with a post-replay marker read into a position nobody displayed.
if (
waiting &&
!(await waiting.evaluate((e) => e.hasAttribute("data-dom-edit-press-waiting")))
)
continue;
const shown = quad ? { ...m, quad, visible: quad } : m;
return { m: shown, actual: m.visible, p: point(shown), c: m.map.toComp(pointerScreen) };
return { m: quad ? { ...m, quad, visible: quad } : m, actual: m.visible };
} finally {
await waiting?.dispose();
}
}
}

export async function sample(ctx, gesture, point, pointerScreen) {
const { m, actual } = await measureShown(ctx, gesture);
return { m, actual, p: point(m), c: m.map.toComp(pointerScreen) };
}

const TRACE_CATEGORIES = ["toplevel", "devtools.timeline", "blink.user_timing"];
const TRACE_MARK = "edit-bench-end";

Expand Down
10 changes: 8 additions & 2 deletions packages/studio/tests/e2e/edit-accuracy/sequences.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
chord,
controlDrag,
measure,
measureShown,
nextFrame,
openStudio,
pointerGesture,
Expand Down Expand Up @@ -272,8 +273,13 @@ async function driveStep(ctx, step, state) {
}
const name = step.element ?? "A";
const c = ctx[name];
// In flight: pressed where the last drag let go, without reading the preview first.
let pre = step.inFlight ? state.last.lastMeasure : await measure(c);
// In flight: pressed where the last drag let go, without reading the preview first. The waiting
// marker belongs to the selected element, so another element is read bare until it is selected.
let pre = step.inFlight
? state.last.lastMeasure
: name === state.selected
? (await measureShown(c)).m
: await measure(c);
if (name !== state.selected) {
await selectTarget(c, pre);
state.selected = name;
Expand Down
36 changes: 36 additions & 0 deletions packages/studio/tests/e2e/edit-accuracy/settle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
STILL_FRAMES,
backgroundRequest,
heldStill,
measureShown,
networkBusy,
seekBound,
swapPending,
Expand Down Expand Up @@ -178,3 +179,38 @@ describe("the size auto-record keys hold", () => {
expect(heldSize(new Map(), 1)).toBeUndefined();
});
});

describe("the box a press is measured against", () => {
const quad = (x) => [
[x, 0],
[x + 1, 0],
[x + 1, 1],
[x, 1],
];
const marker = (answers) => ({
evaluate: async () => answers.shift() ?? false,
dispose: async () => {},
});
const read = async () => ({ map: { toComp: (p) => p }, visible: quad(0) });
const page = (markers) => ({ $: async () => markers.shift() ?? null });

it("reads the waiting marker while a press waits for its replay, not the element under it", async () => {
const shown = await measureShown({ page: page([marker([true])]) }, "move", read, async () =>
quad(260),
);
expect(shown.m.visible[0][0]).toBe(260);
expect(shown.actual[0][0]).toBe(0);
});

it("reads the element when nothing waits", async () => {
const shown = await measureShown({ page: page([]) }, "move", read, async () => quad(260));
expect(shown.m.visible[0][0]).toBe(0);
});

it("reads again when the replay took the marker away between the two reads", async () => {
const shown = await measureShown({ page: page([marker([false])]) }, "move", read, async () =>
quad(260),
);
expect(shown.m.visible[0][0]).toBe(0);
});
});
20 changes: 20 additions & 0 deletions scripts/lefthook-order.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
import { parse } from "yaml";

const config = parse(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "lefthook.yml"), "utf8"),
);
const jobs = config["pre-commit"].jobs ?? [];

test("the skills manifest is hashed after the formatter has rewritten the skills", () => {
const group = jobs.find((job) => job.group?.jobs?.some((j) => j.name === "skills-manifest"));
assert.ok(group, "skills-manifest must sit in a group, not run beside the formatter");
assert.equal(group.group.piped, true, "the group must run its jobs in order");
const names = group.group.jobs.map((j) => j.name);
assert.ok(names.includes("format"), "the group must hold the formatter");
assert.ok(names.indexOf("format") < names.indexOf("skills-manifest"));
});
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"files": 116
},
"remotion-to-hyperframes": {
"hash": "363d84ce81d7dd07",
"files": 77
"hash": "37ed4536305b3c64",
"files": 78
},
"slideshow": {
"hash": "c8a591e2aa7a6d86",
Expand Down
2 changes: 1 addition & 1 deletion skills/remotion-to-hyperframes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Code that depends on wall-clock time, live physics or the network can only come
## Native: translate, then verify

1. For Remotion, run `python3 <SKILL_DIR>/scripts/lint_source.py <source dir>` first. Blockers (`useState`, `useReducer`, `useEffect` with dependencies, async `calculateMetadata`, React UI kits such as MUI, Chakra, Mantine, antd, shadcn, Radix, NextUI) don't translate: host them or refactor them out, never approximate (`references/escape-hatch.md`). Warnings (`@remotion/lambda` config, `delayRender`, `useMemo`, custom hooks) are dropped and noted.
2. Translate to the `/hyperframes` contract: one sub-composition per scene, times in seconds (`frame / fps`), asset paths root-relative. Each Remotion API's equivalent is in `references/api-map.md`.
2. Translate to the `/hyperframes` contract: one sub-composition per scene, times in seconds (`frame / fps`), asset paths root-relative. Each Remotion API's equivalent is in `references/api-map.md`. Put Remotion's render-page rule `* { box-sizing: border-box; }` once in the root `index.html` `<style>`: without it every bordered or padded element grows by its border and padding.
3. Keep it editable: the source's props become `data-composition-variables` bound with `data-var-text`, `data-var-src` or `var(--id)`; each `Sequence` becomes a timed clip or a sub-composition; a reused component becomes one sub-composition mounted several times with `data-variable-values`.
4. Compare frame by frame: `npx hyperframes snapshot --at <t,…> --against <original.mp4>` saves the original's frame at each time next to yours. Sample every scene and both sides of every cut; look for one late or recoloured element, not only the overall picture. For a scored check, `scripts/render_diff.sh <original.mp4> <hf.mp4> <out>` gives SSIM per frame and `scripts/frame_strip.sh` shows which frames diverged (thresholds per complexity tier in `references/eval.md`). Render the Remotion side with `Config.setVideoImageFormat("png")` and `Config.setColorSpace("bt709")`, or the diff measures the encoders (about 0.05 SSIM).
5. Write what didn't translate cleanly (dropped volume ramps, approximated presentations, substituted fonts) to `TRANSLATION_NOTES.md`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<title>tier-1-title-card</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<title>tier-2-multi-scene</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<title>tier-3-data-driven</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
Expand Down
Loading
Loading