Skip to content

Commit 0f74b49

Browse files
Merge branch 'main' into fix/runtime-hide-clips-before-first-pass
2 parents 903d6af + 1f0cfa7 commit 0f74b49

41 files changed

Lines changed: 2619 additions & 107 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/comments.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Comments
2+
3+
permissions:
4+
contents: read
5+
6+
# No secrets and a read-only token, so fork PRs run it too. Rules: CONTRIBUTING.md, "Comments".
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, reopened]
10+
11+
concurrency:
12+
group: comments-${{ github.event.pull_request.number }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
comments:
17+
name: Comments
18+
# A release PR carries commits that each passed this gate on their own PR.
19+
if: ${{ !startsWith(github.head_ref, 'release/') }}
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 10
22+
steps:
23+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
24+
with:
25+
# The PR's own head, graded against its merge-base: not the merge preview, which would
26+
# charge the branch for every comment main changed since it forked.
27+
ref: ${{ github.event.pull_request.head.sha }}
28+
fetch-depth: 0
29+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
30+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
31+
with:
32+
node-version: 22
33+
# The commented-out-code rule asks the TypeScript parser, a root dependency.
34+
- run: bash scripts/ci/install-workspace-dependencies.sh --ignore-scripts
35+
- name: Test the checker
36+
run: node --test scripts/check-comment-citations.test.mjs
37+
- name: Check the comments this diff touched
38+
env:
39+
COMMENT_CHECK_BASE: ${{ github.event.pull_request.base.sha }}
40+
run: node scripts/check-comment-citations.mjs

‎.github/workflows/windows-render.yml‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ jobs:
5555
id: filter
5656
with:
5757
token: ""
58+
# A file counts only if it matches every pattern. Player and Studio `src/` run in a browser
59+
# and no code reads a package README, so a diff confined to them skips Windows.
60+
predicate-quantifier: every
5861
filters: |
5962
code:
60-
- "packages/**"
61-
- "scripts/**"
62-
- "package.json"
63-
- "bun.lock"
64-
- ".github/workflows/windows-render.yml"
63+
- "{packages/**,scripts/**,package.json,bun.lock,.github/workflows/windows-render.yml}"
64+
- "!packages/player/src/**"
65+
- "!packages/studio/src/**"
66+
- "!packages/*/README.md"
6567
6668
preflight:
6769
name: Preflight (lint + format)

‎CONTRIBUTING.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ If you must add a cast, add a comment:
6969
const event = data as unknown as RuntimeEvent;
7070
```
7171

72+
#### Comments
73+
74+
A comment says what the code cannot: the reason, the invariant, the non-obvious constraint. Names, types and tests carry the rest, and reasoning or history goes in the PR description, where it stays attached to the change.
75+
76+
The `Comments` check (`scripts/check-comment-citations.mjs`) grades the comments a PR adds or edits:
77+
78+
- **Citations must resolve.** A backticked path, a `path:line`, a backticked camelCase symbol, or "pinned by" / "covered by" / "see" plus a test file must point at something in the repo. A comment that names its source reads as evidence, so a stale one sends the next reader to a dead end.
79+
- **No history.** "used to", "previously", "was removed", "before this change", "PR #123" describe the past, which git already records. State what is true now.
80+
- **No commented-out code.** Delete it; git keeps it.
81+
- **No block over 40 lines.** Cut it to the why and the invariant. A block that must stay whole (a licence, a diagram, a protocol table) starts with `comment-length: <reason>`.
82+
83+
Only comment blocks holding a line your PR added can fail. Findings elsewhere in a file you touched are printed as warnings, and fixing one while you are there is welcome. To grade files by hand, pass their paths: `node scripts/check-comment-citations.mjs path/to/file.ts`.
84+
7285
## Adding Registry Items (Blocks & Components)
7386

7487
The registry at `registry/` contains reusable items installable via `hyperframes add <name>`. Each item lives in its own directory under `registry/blocks/` or `registry/components/`.

‎docs/concepts/compositions.mdx‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,35 @@ script, CLI overrides, and which value wins. If you are building tooling on
242242
`@hyperframes/core`, `extractCompositionMetadata()` reads the same
243243
`data-composition-variables` array — that is how Studio builds its editing UI.
244244

245+
## Load a composition's own files from its script
246+
247+
When a composition is mounted, a `src` or `href` attribute that points at a
248+
file next to it is rewritten to where that file lives, so
249+
`<img src="assets/logo.png">` keeps working. A path your script builds is a
250+
plain string and is never rewritten, so resolve it with
251+
`window.__hyperframes.assetUrl()`. It takes a path written relative to the
252+
composition's own file and returns a URL the page can load, whether the file is
253+
mounted, previewed or opened on its own.
254+
255+
A path meant from the project root, such as `assets/logo.png` in a composition
256+
at `compositions/intro.html` whose image sits in the project's own `assets/`
257+
folder, already resolves from the page. Leave it as a plain string:
258+
`assetUrl()` would look for it beside the composition and miss.
259+
260+
```html compositions/globe/globe.html
261+
<div data-composition-id="globe" data-width="1920" data-height="1080">
262+
<script>
263+
const texture = new THREE.TextureLoader().load(
264+
__hyperframes.assetUrl("assets/earth.jpg"),
265+
);
266+
</script>
267+
</div>
268+
```
269+
270+
A separate script file (`<script src="...">`) has no composition of its own to
271+
resolve against. Resolve the URLs in the composition's inline script and hand
272+
them over, for example on a global the file reads.
273+
245274
## See every composition in a project
246275

247276
```bash

‎docs/packages/cli.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ npx hyperframes catalog --human-friendly # picker; installs on select
172172

173173
Filter with `--type` (`block` or `component`) and `--tag` (`social`,
174174
`transition`, `text`, …). Default output is a table of name, type, description,
175-
and tags, shaped for agents to parse; `--json` is the structured form.
175+
and tags, shaped for agents to parse; `--json` is the structured form. Each
176+
`--json` row has the item's name, type, title, description and tags, plus its
177+
dimensions, duration and `preview` (`video` and `poster` URLs) when it has them.
176178
`--human-friendly` opens a picker that runs `add` on whatever you select.
177179

178180
## Bring in source material

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"player:perf": "bun run --filter @hyperframes/player perf",
5353
"format:check": "oxfmt --check .",
5454
"knip": "knip",
55-
"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-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-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/install-workspace-dependencies.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/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-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/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-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 && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
55+
"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/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-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/install-workspace-dependencies.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/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-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/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-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 && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
5656
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
5757
"test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
5858
"generate:previews": "tsx scripts/generate-template-previews.ts",

‎packages/cli/src/commands/catalog.test.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

3-
import { countUnindexed, pickByName, searchMissCommand } from "./catalog.js";
3+
import type { RegistryItem } from "@hyperframes/core";
4+
import { catalogRow, countUnindexed, pickByName, searchMissCommand } from "./catalog.js";
45

56
/** The whole registry, which is what "in this registry" has to be measured against. */
67
const registryNames = new Set(["fade-through", "whip-pan", "count-up"]);
@@ -667,3 +668,30 @@ describe("the on-device download offer", () => {
667668
expect(state.consentRecorded).toEqual([]);
668669
});
669670
});
671+
672+
describe("catalogRow", () => {
673+
const block = {
674+
name: "app-showcase",
675+
type: "hyperframes:block",
676+
title: "App Showcase",
677+
description: "Three phones",
678+
tags: ["showcase"],
679+
dimensions: { width: 1920, height: 1080 },
680+
duration: 5,
681+
files: [],
682+
} as unknown as RegistryItem;
683+
684+
it("carries the preview so an app can show the item before add downloads it", () => {
685+
const preview = { video: "https://example.test/a.mp4", poster: "https://example.test/a.png" };
686+
expect(catalogRow({ ...block, preview } as RegistryItem)).toEqual({
687+
name: "app-showcase",
688+
type: "block",
689+
title: "App Showcase",
690+
description: "Three phones",
691+
tags: ["showcase"],
692+
dimensions: { width: 1920, height: 1080 },
693+
duration: 5,
694+
preview,
695+
});
696+
});
697+
});

‎packages/cli/src/commands/catalog.ts‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const examples: Example[] = [
1010
];
1111

1212
import * as clack from "@clack/prompts";
13-
import { type ItemType } from "@hyperframes/core";
13+
import { type ItemType, type RegistryItem } from "@hyperframes/core";
1414
import { c } from "../ui/colors.js";
1515
import { loadAllItems } from "../registry/resolver.js";
1616
import { fetchRegistryManifest } from "../registry/remote.js";
@@ -332,15 +332,7 @@ export default defineCommand({
332332
}
333333

334334
if (json) {
335-
const output = matching.map((item) => ({
336-
name: item.name,
337-
type: item.type.replace("hyperframes:", ""),
338-
title: item.title,
339-
description: item.description,
340-
tags: item.tags ?? [],
341-
...("dimensions" in item && item.dimensions ? { dimensions: item.dimensions } : {}),
342-
...("duration" in item && item.duration ? { duration: item.duration } : {}),
343-
}));
335+
const output = matching.map(catalogRow);
344336
if (!query) {
345337
// A plain listing has no tier and no drop count, and this array shape
346338
// is already released. Leave it alone.
@@ -484,6 +476,20 @@ export default defineCommand({
484476
},
485477
});
486478

479+
/** One `--json` row: what an agent or app needs to pick an item and show it before `add` downloads anything. */
480+
export function catalogRow(item: RegistryItem) {
481+
return {
482+
name: item.name,
483+
type: item.type.replace("hyperframes:", ""),
484+
title: item.title,
485+
description: item.description,
486+
tags: item.tags ?? [],
487+
...("dimensions" in item && item.dimensions ? { dimensions: item.dimensions } : {}),
488+
...("duration" in item && item.duration ? { duration: item.duration } : {}),
489+
...(item.preview ? { preview: item.preview } : {}),
490+
};
491+
}
492+
487493
/**
488494
* Resolve ranked names against the items this registry actually has.
489495
*

‎packages/cli/src/server/studioServer.test.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ vi.mock("../browser/manager.js", () => ({
5757
ensureBrowser: async () => ({ executablePath: undefined, source: "system" }),
5858
}));
5959

60+
// Lets one test hold the project history in its opening; every other test opens the real one.
61+
const historyState = vi.hoisted(() => ({
62+
open: null as null | ((...args: unknown[]) => Promise<unknown>),
63+
}));
64+
vi.mock("@hyperframes/studio-server", async (importOriginal) => {
65+
const original = await importOriginal<typeof import("@hyperframes/studio-server")>();
66+
return {
67+
...original,
68+
openProjectHistory: (...args: Parameters<typeof original.openProjectHistory>) =>
69+
historyState.open ? historyState.open(...args) : original.openProjectHistory(...args),
70+
};
71+
});
72+
6073
// Only `fs.watch` is replaced, so the SSE describe below can fire a file-change
6174
// on demand; every other server test keeps reading and writing real files.
6275
const mockWatcher = new EventEmitter() as EventEmitter & { close: () => void };
@@ -113,6 +126,47 @@ describe("Studio thumbnail GPU capture plumbing", () => {
113126
});
114127
});
115128

129+
describe("createStudioServer project history (D-491)", () => {
130+
it("serves the project's history, and a change the watcher sees becomes an entry", async () => {
131+
const projectDir = tmpProject();
132+
writeFileSync(join(projectDir, "index.html"), "<html>before</html>");
133+
server = createStudioServer({ projectDir, historyRoot: tmpProject() });
134+
const historyUrl = `/api/projects/${encodeURIComponent(basename(projectDir))}/history`;
135+
const list = async () =>
136+
(await (await server!.app.request(historyUrl)).json()) as {
137+
entries: Array<{ who: { kind: string } }>;
138+
back: { label: string } | null;
139+
};
140+
expect(await list()).toMatchObject({ entries: [], back: null });
141+
142+
writeFileSync(join(projectDir, "index.html"), "<html>agent</html>");
143+
mockWatcher.emit("change", "change", "index.html");
144+
145+
// Writes with no window open group until 2 s of quiet.
146+
await vi.waitFor(async () => expect((await list()).entries).toHaveLength(1), {
147+
timeout: 5_000,
148+
interval: 200,
149+
});
150+
expect((await list()).entries[0]!.who.kind).toBe("outside");
151+
await server.shutdown();
152+
});
153+
154+
it("shutdown returns within preview's exit watchdog while the history is still opening", async () => {
155+
historyState.open = () => new Promise(() => {});
156+
try {
157+
const projectDir = tmpProject();
158+
server = createStudioServer({ projectDir, historyRoot: tmpProject() });
159+
void server.app.request(`/api/projects/${encodeURIComponent(basename(projectDir))}/history`);
160+
await new Promise((resolve) => setTimeout(resolve, 50));
161+
const started = Date.now();
162+
await server.shutdown();
163+
expect(Date.now() - started).toBeLessThan(2_900);
164+
} finally {
165+
historyState.open = null;
166+
}
167+
});
168+
});
169+
116170
describe("createStudioServer autoProxy plumbing", () => {
117171
it("hyperframes.json media.autoProxy=false flows through to the adapter", () => {
118172
const projectDir = tmpProject();

0 commit comments

Comments
 (0)