mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
edfe66a953
* docs: add the shared page components Adds the six React snippets the rebuilt documentation pages compose against, plus the styles they need. Nothing imports them yet, so this lands with no user-visible change and no navigation churn. - DocsVideo / ShowcaseWall — the film player and the Showcase grid - LiveReferenceProject — embeds the Reference Project via <hyperframes-player> - WorkflowChooser, AgentAction, and the two grid snippets The scrub indicator is a timecode bubble rather than a thumbnail. Mounting a second <video> with the same src to drive a preview frame made every page carrying a film download the whole file twice, which is not worth a thumbnail. * docs: add the Reference Project example One real 10-second project the documentation can point at instead of describing a hypothetical one: a live capture of example.com, synthesised narration, and caption timings measured from that narration. It passes its own gates — `hyperframes lint` clean, `hyperframes check` passed, 28/28 text checks WCAG AA. No page imports it yet, so this lands without touching navigation. Only the two WAV masters exceed the repository's 500 KB non-LFS limit, so only those go through LFS. The MP3 stings and the capture PNG stay plain, which keeps the example usable after a clone without `git lfs pull`. `bun run docs:bundle-reference` regenerates the single-file embed the Introduction page loads from the CDN. * docs: keep the Reference Project verification report The Examples page links this file twice — as "What changed after review" and as "The real verification report" — in the section that makes the project's brief, source, revision notes, and checks public end to end. It is a published artifact, not leftover scaffolding. * docs: state the Reference Project embed's isolation contract The composition is fetched from the CDN and handed to the player as a blob: URL, which inherits the docs origin, and <hyperframes-player> sandboxes its iframe with allow-scripts + allow-same-origin. So the embedded composition runs with script access to this origin. That is a consequence of how the player works — it drives seeking through the iframe's document, which a cross-origin frame does not expose — not something this component can fix. Serving the CDN URL directly would isolate the frame and break playback. The guard is therefore the source, so the comment says so out loud: src must stay a first-party path we publish, never user- or community-supplied HTML. * fix(docs): resolve reduced-motion on the first render, and the embed's dep gap Both defects from Rames Jusso's review on #2977. Neither is visible today because nothing imports these files yet, which is what makes them cheap now. **Reduced motion resolved one paint too late, in all three grids.** `useState(false)` plus a `matchMedia` read in an effect meant the first committed render always emitted `<video src autoPlay loop>`; a reduce-motion visitor had 6 + 8 + 4 tiles already fetching before the attributes came off. `autoPlay` also overrides `preload="metadata"`, so those were the files, not metadata probes — and dropping `src` with no following `load()` is not a reliable abort. A lazy initializer knows the answer on the first render. **LiveReferenceProject never sent the initial variables.** The sending effect read `playerRef.current`, assigned by the effect above it on the commit where `compositionSrc` lands — a commit with nothing in the sending effect's dep array. So it ran once against a null ref and never again. It looked correct only because the three defaults match what the composition already renders. Also from the same review: - The object URL could outlive its revoke: once the body resolves, `abort()` no longer stops the chain, so the blob could be minted after cleanup ran with `objectUrl` still undefined. Same `cancelled` guard the effect above uses. - `postMessage` targeted `"*"` while the isolation comment argues the frame is same-origin. Naming `window.location.origin` turns that prose guard into an enforced one. - Nothing reached a terminal state when the player script never arrived: `whenDefined()` does not reject, and a later mount reuses the tag without its error listener. A CSP rule or content blocker never fires `error` at all. A deadline covers every path instead of sitting on "Loading…" forever. - `loadFailed` was never cleared, so one transient failure stuck. - The README claimed a clone works without `git lfs pull`. It does for the visuals; both WAVs are pointers and they are the bed and the voiceover, so the captions would play over silence. Says so now. - The bundler stripped trailing whitespace document-wide while inlining the runtime, which reaches inside script template literals where those spaces are data. It also assumed a literal `<head>` and would silently ship an embed with no `<base>`. Strip removed, anchor asserted. Copilot's five "missing hook imports" comments are wrong — Mintlify pre-injects the hooks, and `TemplateCard.jsx`, cited as the counter-example, uses the `export function` form the same page says is unsupported. * fix(docs): stop preview loops when Reduce Motion is turned on mid-session Miguel's changes-requested on #2977. He is right about the mechanism: dropping `src` and `autoPlay` through React props neither pauses a playing element nor aborts its selected resource, so a visitor who turned Reduce Motion on with the page already open kept every tile running. Measured in a browser rather than argued from the spec, same clip, same sequence: playing paused=false t=2.90 readyState=4 networkState=1 React props only paused=false t=3.90 readyState=4 networkState=1 + pause/removeAttr/load paused=true t=0 readyState=0 networkState=0 The middle row is the bug: time still advancing, resource still held. Rames' follow-up asked for a remount-to-poster instead, because a video that ends with `src` removed holds its last frame and `poster` only paints before playback begins. `load()` covers that too — it drops readyState to HAVE_NOTHING, which is precisely the state that paints the poster. Confirmed side by side on screen: the React-props-only tile sits on an arbitrary mid-clip frame, the pause/load tile shows the poster again. So no remount is needed. The guard cannot be shared as code — Mintlify compiles each snippet in isolation and forbids one importing another — so it is copy-pasted into all three grids. A duplicated invariant is the kind that rots, and a rendering test would mean adding React to a repo that only carries it inside packages/studio, plus mocking Mintlify's hook-injection contract with a mock that can stay green while the page breaks. `scripts/check-docs-snippet-motion.mjs` asserts the source instead, wired into `bun run lint`, with unit tests covering both edges. That gate immediately found `docs/snippets/TemplateCard.jsx`: autoplays with no reduced-motion handling at all. It is imported by zero pages, and it uses the `export function` form Mintlify's constraints page says is unsupported, so it would not work if it were. Deleted rather than fixed. * refactor(scripts): split the motion guard into named predicates fallow flagged findMotionGuardViolations at CRAP 42 — a finding this branch introduced, so it gets fixed rather than suppressed, same as the catalog generator earlier in the stack. The two conditions are now their own predicates behind a small requirements table, which drops the branch count under the threshold and makes each rule readable on its own line. Same output, same tests. * fix(docs): move the stop effect above ShowcaseWall's early return Rames' changes-requested on `e1a03c63`. The effect I added in the previous commit landed below `if (open) return`, so `ShowcaseWall` called five hooks on the grid render and four once a tile was open. That is a conditional hook: clicking a tile — the component's primary interaction — threw "Rendered fewer hooks than expected". Worth naming why it landed in one of three. `workflow-chooser` and `advanced-path-grid` have no early return, so the same paste position was fine there. `ShowcaseWall` is the only one with a conditional return and it got the same copy. That is the duplication cost this script's own header warns about, showing up in the commit that added the script. **The bespoke gate could not have caught it, and now the generic one does.** `.oxlintrc.json` already loaded the `react` plugin and never excluded `docs/` — only `.prettierignore` does, which is why formatting is not a finding here but linting reaches these files. Naming the two hook rules in an override scoped to `docs/snippets/**` reports this bug directly, and also reports the `compositionSrc` dependency gap from round one that was found by reading. Verified both ways: reintroducing the conditional hook produces `react-hooks(rules-of-hooks)`, and `bunx oxlint .` is clean repo-wide, so nothing lit up in `packages/studio`. **Two holes in the script itself, both from the same review.** It matched whole files while the invariant is per component, so a second unguarded grid in `docs-video.jsx` would have ridden in on `ShowcaseWall`'s guard. It now splits by component. That immediately surfaced the distinction between a component that decides to autoplay and one that forwards its caller's `autoPlay` prop — `DocsVideo` only ever plays because a reader clicked, so it does not owe a preference check. And `readsPreferenceLazily` never tied its halves: any lazy initializer plus the media-query string anywhere in the file passed, which is the original bug satisfying the check written to prevent it. The query now has to sit inside the initializer's own expression. Both holes have tests. fallow is clean at 0 introduced. * fix(scripts): close the two silent gaps in the motion gate Both from Rames' approval pass on #2977, and both found by running these functions rather than reading them. Both fail the same quiet way: a component `autoplays` misses is filtered out before any requirement runs, so the gate reports zero problems instead of a violation. `autoplays` had become narrower than the version it replaced. Excluding the `autoPlay={autoPlay}` passthrough was right, but the replacement only matched `autoPlay={` or `autoPlay` alone on a line, so `<video autoPlay muted />` on one line slipped through. Restored the old breadth. Two things are stripped first rather than one — the passthrough, and the prop's own default in the signature, which is a declaration and not a use. Without the second strip, `DocsVideo` is asked to own a decision it only forwards. `splitComponents` anchored on `^export`, so anything not exported folded into the previous exported component and inherited its guard. Same hole as the whole-file match, narrowed from file scope to non-export scope. The anchor no longer requires `export`. Ten tests now, including his exact examples for both. * docs: remove the live-composition embed and its build apparatus The Introduction no longer carries the embed (removed in #2979), and nothing else used any of this: the 200-line snippet, 26 CSS rules, the bundler that built the single-file HTML for the CDN, its npm script, and the README section explaining how to regenerate it. The Reference Project itself stays — Examples, Developers, and Go further all link to it as the worked example; only the interactive embed of it is gone. This also retires the isolation contract I documented two rounds ago. That comment existed because the embed handed CDN HTML to a same-origin blob; with the embed gone there is no such surface to reason about, which is a better outcome than a comment explaining why it was acceptable. * docs: remove the AgentAction snippet Its only consumer is gone. The Quickstart now shows the agent instruction in a plain fence instead, because this component rendered a Copy button and never displayed the request — a reader copied text they could not read, which is the wrong shape for the one affordance a non-technical visitor depends on. Mintlify fences already carry a copy button and show their contents.
286 lines
15 KiB
Markdown
286 lines
15 KiB
Markdown
# Verification report — v1 → v2
|
||
|
||
Project: `examples/docs-reference-project` · **1920×1080 · 10.000s · 30 fps · 300 frames**
|
||
CLI: `hyperframes@0.7.90` (project pin bumped from `0.7.88` during this pass and
|
||
re-verified — see "Toolchain" below).
|
||
|
||
---
|
||
|
||
## 1. Gate results
|
||
|
||
All commands were run in the project directory. These are the actual results.
|
||
|
||
| Gate | Result |
|
||
| ----------------------------------------------- | ----------------------------------------------------------- |
|
||
| `npx hyperframes lint --verbose` | **0 errors, 0 warnings** (2 files scanned) |
|
||
| `npx hyperframes check` | **passed** — `ok: true` |
|
||
| › lint | 0 errors · 0 warnings · 1 info |
|
||
| › runtime | 0 errors · 0 warnings · 0 info |
|
||
| › layout | 0 errors · 0 warnings · 1 info |
|
||
| › motion | 0 findings (no `*.motion.json` sidecars in this project) |
|
||
| › contrast | 0 errors · 0 warnings · 0 info |
|
||
| `check --caption-zone "…y0=.83…severity=error"` | **passed** — 0 caption-band collisions across 8 seek points |
|
||
| `npx hyperframes snapshot --at …` (19 frames) | captured + inspected; 3 contact sheets |
|
||
| `npx hyperframes render` | **passed** — H.264 1080p + AAC stereo, exactly 10.000s |
|
||
|
||
Final media inspection: 1920×1080 H.264, 30 fps, AAC stereo at 48 kHz,
|
||
10.000 seconds. Mean volume is −22.9 dB and the peak is −3.8 dB.
|
||
|
||
### The two info-level findings, and why they stay
|
||
|
||
Neither gates the exit code. Both are deliberate.
|
||
|
||
1. `pointer_events_none` on `compositions/captions.html` → `#root`.
|
||
The caption overlay spans the whole canvas above the artwork, so its root must be
|
||
click-through or nothing underneath is selectable in Studio. The pills themselves
|
||
carry `pointer-events: auto`, so the editable content _is_ selectable — which is
|
||
exactly what the finding's own fix hint asks for. Keeping `pointer-events: none` is
|
||
correct; the alternative is an invisible full-canvas div that eats every click.
|
||
|
||
2. `container_overflow` on `#title` at `t=0.556`, inside `span.title-mask`.
|
||
This is the mask doing its job: the title starts at `yPercent: 106` and rises into
|
||
view, so for the first ~0.7s its box is below the mask it is clipped by. Marking the
|
||
mask `data-layout-allow-overflow` would silence it, but that attribute is inherited
|
||
and would also disable `text-clipping`, `content-cramped-container` and
|
||
`foreground-over-panel` on the hero title for the whole composition. A transient info
|
||
finding is the cheaper price. The finding is reported once, at one sample.
|
||
|
||
### What "inspected snapshots" means here
|
||
|
||
19 frames on the **30 fps frame grid** (not arbitrary decimals), chosen to cover every
|
||
beat plus both caption cuts and the caption clear:
|
||
|
||
`0 · 0.533 · 1.3 · 1.967 · 2.6 · 2.8 · 2.867 · 3.133 · 4.5 · 5.0 · 5.433 · 5.5 · 5.733 · 6.4 · 7.1 · 7.933 · 8.067 · 9.5 · 9.967`
|
||
|
||
The root declares `data-fps="30"`, so those are real frame times. Requesting an
|
||
off-grid time (e.g. `2.833`) quantises to the nearest frame and renders `2.800` twice —
|
||
which briefly looked like a caption bleed until it was measured. It was not one.
|
||
|
||
Checked and confirmed:
|
||
|
||
- The plate lands at 1:1 and the page's real 16px body copy is legible — the headline,
|
||
the full sentence, and the `Learn more` link all read.
|
||
- The accent marker draws beside the page's own paragraph without touching the link
|
||
below it (6px clearance, measured).
|
||
- Both caption cuts hand off cleanly. At `2.800` only group 0 is drawn (fading out); at
|
||
`2.867` only group 1 (fading in). Same at `5.433` / `5.500`. This also holds by
|
||
construction: group _n_'s hard kill sits at exactly group _n+1_'s start, so before that
|
||
instant only _n_ can be non-zero and at/after it _n_ is set to `opacity: 0;
|
||
visibility: hidden`.
|
||
- Captions clear by `8.033s`: sampling the whole caption band at `8.067s` gives a
|
||
darkest pixel of `233` — pure canvas, nothing drawn. The frame then holds a still,
|
||
caption-free end card through the final frame at `9.967s`.
|
||
- No black frame, no blank panel, no clipped text, no element in the caption band.
|
||
|
||
`snapshot`'s optional Gemini frame-description pass failed (`API key not valid`) — the
|
||
ambient `GEMINI_API_KEY` is rejected. That is an optional annotation, not a gate; the
|
||
empty `descriptions.md` was deleted rather than shipped as a wall of identical errors.
|
||
Frames were inspected directly.
|
||
|
||
---
|
||
|
||
## 2. v1 → v2
|
||
|
||
v1 is the verified original at `quickstart/example-intro`: same request, same source,
|
||
same 10.0s / 1920×1080 output. v2 keeps its concept — _show the site as it is, the real
|
||
page is the proof_ — and its split composition. What changed, and why.
|
||
|
||
### 2.1 The capture actually reads now — the one that mattered
|
||
|
||
v1's biggest defect was invisible in the source and obvious in the render. The capture
|
||
was 1440×810 displayed inside a 940×529 card: **0.65×**. The page's 16px body text
|
||
rendered at roughly 10px in a 1080p frame, so the "real captured page" — the entire
|
||
argument of the video — was an unreadable grey smudge with a tiny cluster in one corner.
|
||
|
||
v2 shows the capture at **exactly 1:1**. A fresh 1920×1080 1x capture is displayed
|
||
through a 1000×400 window with `object-fit: none; object-position: -300px -104px`, so the
|
||
page's own type renders at the size it renders at in a browser. Three hard rules fall out
|
||
of that, and they are written into `frame.md`:
|
||
|
||
- the plate **never scales** (a 1x capture has no headroom above 1:1),
|
||
- the plate **never rotates** (v1 tilted it `rotationY: -10° → -4° → -1.5°`, resampling
|
||
the page text for the entire shot),
|
||
- the entrance is **`x` translate + opacity only**.
|
||
|
||
`object-fit: none` also means the `<img>` box is exactly 1000×400 instead of a 1920×1080
|
||
element hanging out of an `overflow: hidden` parent, so it needs no
|
||
`data-layout-allow-overflow` and trips no layout finding.
|
||
|
||
### 2.2 Decorative noise removed
|
||
|
||
| Removed from v1 | Why |
|
||
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||
| 160px background grid **and** 4px dot "paper" | Two textures stacked under a third layer (the light wash). Visible busywork on a flat page. |
|
||
| The sheen sweep across the card (`7.55 → 8.65s`) | A stock shine gimmick. It decorated the evidence instead of reading it. |
|
||
| The ambient accent bloom (`5.0s`, then `scale 1.05`) | Fired 2.5s after the plate landed, cued to nothing. |
|
||
| `https://example.com` caption under the card | A label repeating the `example.com` line already on screen, in near-invisible grey. |
|
||
| The `Learn more →` pill button | Invented UI. The page's action is a plain text link — and it is already visible _in the plate_, as itself. |
|
||
| The 1.9s `rotationY` "settle" + glow scale at the end | Lazy breathing. v2 holds completely still instead. |
|
||
|
||
What replaced them is **one** device with a job: a single 5px accent marker that draws
|
||
down the left edge of the page's own content block, on the narration cue
|
||
_"…documentation examples."_ It is the film's argument — _those are the page's words, not
|
||
ours_ — and it is the only new graphic element in v2.
|
||
|
||
### 2.3 Narration, captions and pacing (new in v2)
|
||
|
||
v1 was silent except for a music bed, so nothing cued anything; its beats were spaced by
|
||
feel (`0.1 · 0.45 · 1.55 · 2.5 · 3.9 · 5.0 · 6.5 · 7.55`).
|
||
|
||
v2 has narration, and **every visual cue is a measured word timing**, not an estimate:
|
||
`assets/narration.wav` → `npx hyperframes transcribe --model small.en` →
|
||
`transcript.json` → the tween positions in `index.html` and the group boundaries in
|
||
`compositions/captions.html`. Three phrase captions for three sentences, fixed position,
|
||
one visible at a time, per-word emphasis by luminance only.
|
||
|
||
Pacing consequence: v1 front-loaded the lockup then idled with decoration. v2's reveals
|
||
land at `1.00 · 1.55 · 1.78 · 2.66 · 4.55 · 6.42` — the last one inside the final 30% —
|
||
with two **deliberately empty** holds (`5.15 → 6.42` mid-film breather, `8.03 → 10.0`
|
||
still end card). The holds are left visibly empty in the timeline source, with comments
|
||
saying so, because a held read beats bad motion.
|
||
|
||
### 2.4 Determinism fix: non-embedded font weights
|
||
|
||
v1 asked for `font-weight: 500` on IBM Plex Mono and `600` on Inter. The renderer embeds
|
||
`inter` at **400/700/900** and `ibm-plex-mono` at **400/700** only, so both requests were
|
||
synthesised or substituted on a clean render machine — preview and output could disagree.
|
||
v2 uses only bundled weights: Inter 900 (title) / 400 (body), IBM Plex Mono 400
|
||
(address), Inter 700 (captions). `frame.md` states the rule so an editor cannot
|
||
reintroduce it.
|
||
|
||
### 2.5 Parameterisation (new in v2)
|
||
|
||
v1 had no variables — it was a one-off. v2 declares five with useful defaults and wires
|
||
them declaratively (`data-var-text`, `data-var-src`, `var(--accent)`), with no
|
||
`getVariables()` call anywhere, so the composition can front another site by flags alone.
|
||
|
||
### 2.6 Palette: one derived value, stated as derived
|
||
|
||
v1's brief claimed the palette was read from the capture, then set the canvas to
|
||
`#eeeeee` — the page's own background. The plate therefore had the same fill as the
|
||
ground and only its border separated them, which is why it read as a faint rectangle.
|
||
|
||
v2 keeps ink `#1b1b1b` and accent `#334488` from the captured page (its CSS literally
|
||
says `a { color:#348 }`) and steps the **canvas** down to `#e9e9e7`
|
||
so the plate has something to lift off. `frame.md` labels that as the one derived value
|
||
rather than pretending it was read.
|
||
|
||
### 2.7 One thing v2 gives up
|
||
|
||
v1's title was two hand-split `<span>`s waterfalling in 0.17s apart — a nicer reveal than
|
||
v2's single masked rise. That split is incompatible with `data-var-text`, which replaces
|
||
an element's own text and cannot drive per-line spans. v2 trades the waterfall for a
|
||
title that is actually a parameter. Called out here because it is a real regression, not
|
||
an oversight.
|
||
|
||
---
|
||
|
||
## 3. Two framework bugs found, with reproductions
|
||
|
||
Both were hit while building this project and both are fixed _in_ this project. Neither
|
||
is a blocker for it.
|
||
|
||
### 3.1 `window.getComputedStyle()` throws inside a sub-composition
|
||
|
||
**Severity: high** — the failure is near-silent and ships broken video.
|
||
|
||
The captions doctrine (`media-use/audio/references/captions/authoring.md` → "Self-lint
|
||
after building timeline") prescribes this snippet verbatim:
|
||
|
||
```js
|
||
var computed = window.getComputedStyle(el);
|
||
```
|
||
|
||
Inside a sub-composition it raises `TypeError: Illegal invocation`. The script dies
|
||
mid-self-lint, so `window.__timelines["captions"] = tl` never runs, the runtime waits out
|
||
its registration timeout, and the render captures whatever DOM state the throw left
|
||
behind. Observed symptom: caption group 0 frozen at ~14% opacity for the whole video,
|
||
with `check` reporting it only indirectly as 30 `contrast_aa_failure` errors against a
|
||
background of `rgb(205,205,203)` — a colour that exists nowhere in the design.
|
||
|
||
Root cause, measured from inside a sub-composition script:
|
||
|
||
```
|
||
window === globalThis → false
|
||
window.getComputedStyle === globalThis.getComputedStyle → true
|
||
window.__hyperframes.fitTextFontSize → function
|
||
window.__timelines → object
|
||
```
|
||
|
||
The runtime evaluates a sub-composition's inline script with a **`window` wrapper
|
||
object**. Property reads and writes proxy through to the real window, which is why
|
||
`__timelines` and `__hyperframes` work. But retrieving the _same_ native function through
|
||
the wrapper and calling it as a method makes the wrapper the receiver, and native code
|
||
rejects it.
|
||
|
||
Fix in this project (`compositions/captions.html`), with the reason in a comment:
|
||
|
||
```js
|
||
var computed = getComputedStyle(el); // bare — not window.getComputedStyle
|
||
```
|
||
|
||
`globalThis.getComputedStyle(el)` also works. Suggested upstream actions: make the
|
||
wrapper bind native `Window` methods, and fix the snippet in the captions doctrine —
|
||
every agent that follows it inside a sub-composition inherits this bug.
|
||
|
||
### 3.2 `lint`'s `missing_local_asset` mis-parses `data-var-src`
|
||
|
||
**Severity: low** — loud, harmless, one-line workaround.
|
||
|
||
```html
|
||
<img src="assets/example-com.png" data-var-src="pageImage" />
|
||
```
|
||
|
||
fails lint with `missing_local_asset: <img> element references local file(s) not found in
|
||
the project: pageImage`. The rule's regex is
|
||
`/<(video|img|source)\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi`; the greedy `[^>]*`
|
||
takes the **last** `src=` in the tag, and `data-var-src` ends in `src` with a `-` before
|
||
it, so `\b` matches and the variable id is read as a filename.
|
||
|
||
Workaround used here: author `data-var-src` **before** `src`. Suggested upstream fix:
|
||
require a whitespace or `"` boundary before `src` (`(?<=[\s"'])src\s*=`), or explicitly
|
||
skip `data-var-src`.
|
||
|
||
---
|
||
|
||
## 4. One documented authoring constraint
|
||
|
||
Not a bug, but a real trap worth stating: **keep `data-composition-variables` pure
|
||
ASCII.** It lives on `<html>`, which is consumed before `<meta charset>` is in effect, so
|
||
a literal em dash in a `default` renders as `â€"`. Verified both ways by snapshot: the
|
||
literal character mojibakes, the JSON escape `\u2014` renders a correct em dash. Text in
|
||
the document body and inside a sub-composition `<template>` is unaffected (the runtime
|
||
`fetch`es sub-compositions and decodes them as UTF-8).
|
||
|
||
v2 ships ASCII-only variable defaults and states the rule in a comment in `index.html`.
|
||
|
||
---
|
||
|
||
## 5. Toolchain
|
||
|
||
The project's `package.json` pinned `hyperframes@0.7.88`. Per the CLI's own upgrade
|
||
protocol the pin was probed before any render-affecting command:
|
||
|
||
```
|
||
npx hyperframes@latest upgrade --project . --check
|
||
→ would bump project scripts 0.7.88 → 0.7.90
|
||
```
|
||
|
||
Applied, then verified: `npx hyperframes check` passes on **0.7.90**. A passing check
|
||
confirms the compositions still validate on the new version — not that output is
|
||
frame-identical to the old pin. The project now runs on `0.7.90`; `hyperframes info`
|
||
reports `updateAvailable: false`.
|
||
|
||
---
|
||
|
||
## 6. Blockers
|
||
|
||
**None.** `lint` and `check` pass with zero errors and zero warnings, snapshots are
|
||
captured and inspected, and the final MP4 passed the media gate.
|
||
|
||
Two non-blocking environment notes: the ambient `GEMINI_API_KEY` is rejected by the API,
|
||
so `snapshot`'s optional vision descriptions are unavailable; and `hyperframes feedback`
|
||
was not sent, because the CLI's protocol sends it only after verifying a successful
|
||
render. Both framework findings in §3 are written up here in reproducible form so they
|
||
can be filed with that render.
|