diff --git a/lefthook.yml b/lefthook.yml index aea4e7448ff..b8e3cec0ea1 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,23 +1,28 @@ 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 @@ -25,7 +30,7 @@ pre-commit: # 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. @@ -33,19 +38,19 @@ pre-commit: # 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. diff --git a/package.json b/package.json index 2fefca4d1ae..c0af9aa5be9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/studio/tests/e2e/edit-accuracy/case.mjs b/packages/studio/tests/e2e/edit-accuracy/case.mjs index 0b7bbd297c7..0f91e623500 100644 --- a/packages/studio/tests/e2e/edit-accuracy/case.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/case.mjs @@ -678,13 +678,14 @@ 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 ( @@ -692,14 +693,18 @@ export async function sample(ctx, gesture, point, pointerScreen) { !(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"; diff --git a/packages/studio/tests/e2e/edit-accuracy/sequences.mjs b/packages/studio/tests/e2e/edit-accuracy/sequences.mjs index 9fa6288a176..4f20abb06db 100644 --- a/packages/studio/tests/e2e/edit-accuracy/sequences.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/sequences.mjs @@ -10,6 +10,7 @@ import { chord, controlDrag, measure, + measureShown, nextFrame, openStudio, pointerGesture, @@ -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; diff --git a/packages/studio/tests/e2e/edit-accuracy/settle.test.mjs b/packages/studio/tests/e2e/edit-accuracy/settle.test.mjs index c13b7f4ca45..ee2fa64794a 100644 --- a/packages/studio/tests/e2e/edit-accuracy/settle.test.mjs +++ b/packages/studio/tests/e2e/edit-accuracy/settle.test.mjs @@ -6,6 +6,7 @@ import { STILL_FRAMES, backgroundRequest, heldStill, + measureShown, networkBusy, seekBound, swapPending, @@ -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); + }); +}); diff --git a/scripts/lefthook-order.test.mjs b/scripts/lefthook-order.test.mjs new file mode 100644 index 00000000000..434df57b4c8 --- /dev/null +++ b/scripts/lefthook-order.test.mjs @@ -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")); +}); diff --git a/skills-manifest.json b/skills-manifest.json index d542ca6b83b..48b719cae4d 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -22,8 +22,8 @@ "files": 116 }, "remotion-to-hyperframes": { - "hash": "363d84ce81d7dd07", - "files": 77 + "hash": "37ed4536305b3c64", + "files": 78 }, "slideshow": { "hash": "c8a591e2aa7a6d86", diff --git a/skills/remotion-to-hyperframes/SKILL.md b/skills/remotion-to-hyperframes/SKILL.md index 10ffe5fb996..283b03619d8 100644 --- a/skills/remotion-to-hyperframes/SKILL.md +++ b/skills/remotion-to-hyperframes/SKILL.md @@ -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 /scripts/lint_source.py ` 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` ` +
…
+``` ## Sequencing diff --git a/skills/remotion-to-hyperframes/scripts/tests/boxSizing.test.mjs b/skills/remotion-to-hyperframes/scripts/tests/boxSizing.test.mjs new file mode 100644 index 00000000000..0785fd493eb --- /dev/null +++ b/skills/remotion-to-hyperframes/scripts/tests/boxSizing.test.mjs @@ -0,0 +1,21 @@ +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"; + +const SKILL = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const read = (path) => readFileSync(join(SKILL, path), "utf8"); +const RULE = /\*\s*\{\s*box-sizing:\s*border-box;?\s*\}/g; + +test("the skill tells the port to carry Remotion's box-sizing rule once", () => { + assert.match(read("SKILL.md"), /\* \{ box-sizing: border-box; \}/); + assert.match(read("references/api-map.md"), /width:200px;height:100px;padding:20px;border:10px/); +}); + +for (const tier of ["tier-1-title-card", "tier-2-multi-scene", "tier-3-data-driven"]) { + test(`${tier}: the ported root sets the rule exactly once`, () => { + const root = read(`assets/test-corpus/${tier}/hf-src/index.html`); + assert.equal(root.match(RULE)?.length, 1); + }); +}