# Changelog All notable changes to showtime. Each entry says what changed and why, so this file also answers "where did X go?". Versions follow [semantic versioning](https://semver.org); the version lives in `skills/showtime/lib/st/__init__.py` and `python3 scripts/check_release.py` keeps the plugin manifests, the registry files and `setup/package.json` in sync with it. ## 0.4.7 (unreleased) - **Fixed: the notes page shows the number of a box drawn at the top of the picture.** Each box note's number sits above its box, so a box within 24 px of the picture's top edge had its number clipped off the picture. That number now goes under the box (`review.js` marks such a box `.low`, `review.css` places its number), for notes on this render and on earlier ones alike; a test drags a box from the top edge and checks that its number is inside the picture. ## 0.4.6 (2026-10-16) - **Fixed: the X-ray template at 9:16 keeps its sound strip and labels where a phone shows them.** The strip sat under the reels safe box (y 1472-1676 at 1080x1920), where the feed's caption and buttons cover it, and its label and the part labels were 24-34 px, under the phone floor (43 px at 1920). The strip now sits above the counters, inside the safe box, with its label on the ticks' row (the label fits as before: shrink to the floor, drop "the clip's sound", then an ellipsis); the counters are one compact row, 64 px (they were 84). The part labels are 44 px, in a column at the safe box's left edge, ordered so no two leaders in a column cross: parts at rest muted, the ones acting now red with a dot on the part, leaders grey. When one column can't hold them (the drums name 8 parts), a second one sits at the safe box's right edge; a long label wraps to two lines; the drawing sits between the columns, never under them, with heavier lines, and ends at x 916, left of the platform's buttons. The layout is chosen once per film, so the drawing never moves. The drums', guitar's and piano's captions (24-30 px inside the drawing) are a 44 px row over it, and the guitar's string names (26 px on lines 21 px apart) are labels in the column; the kit's new calls for a mechanism are in `showtime guide xray "Mechanism library"`. This change leaves 16:9, 4:5 and 1:1 as they were (16:9's own fixes are the #68 entry below). Every library mechanism passes `check` in a fresh 9:16 project. At 4:5 and 1:1 the template still has `check` findings it had before this change (text under the phone floor, labels on each other, and at 4:5 text outside the safe box): a test keeps each mechanism's count of each kind at or under its count before this change, until a later release fixes them. - **Fixed: the X-ray sound strip with `"xray".offset_frames`.** When the clip starts some frames into the film, the strip placed its ticks and its caret by film time over the recording's length: late ticks were drawn past the strip's end, off the frame, and the caret stopped at the end while the recording still played. Both now follow the recording's own clock (the film frame minus the offset), in the `xray` template and its living-diagram page; before the clip starts the caret waits at the strip's start. With no measured recording length the strip takes the recording to run from the clip's start to the film's end (the diagram page: to the endcard), not from frame 0. And `"xray".track_until` (the clip's seconds) is read on the recording's clock too: with an offset the see-through used to fade out that many seconds early. - **Fixed: the X-ray piano, drums and guitar follow the recording's clock too.** `notes.json` times and its tempo map are seconds of the recording, but a note that no event took was shown at the film frame of its time, and the staff or tab scrolled by the film's time; with `"xray".offset_frames` both ran that many frames early. The drawing kit now gives a mechanism `k.rt` (this frame's time on the recording) and `k.frameAt(t)` (the film frame of a recording time), from the same mapping the sound strip and `track_until` use; the three mechanisms use them. - **New: `check` finds canvas text outside the vertical safe box between its samples, on any canvas.** The 9:16 safe-zone rule saw film-API text at the sample times only, so labels a page drew on its own canvas (chart notes, a date counter) passed `check` and `qa --platform reels` while the feed's UI covered them. Now every step of the dense timeline compares each canvas text with the safe box (x 64-916, y 220-1440 at 1080x1920): one `safe_zone` warning per text with its span, when it sits outside two steps in a row without moving or for 1 s in all. A counter is one text while its number changes. qa's phone check counts it, as before. - **New: `check` measures the size of canvas text a page draws itself** (#73, the check half). Raw `fillText`/`strokeText` on any page canvas now feeds the `tiny_text` rule and the phone check on the dense timeline, as DOM text and `F.text` already did; before, only the film API's text was measured, so a tiny label drawn by the page passed. A counter in one place is one text; text inside `F.decor`, faint text (under half opacity) and text mostly off the frame are not measured. - **Fixed: the repository stores files exactly as they are installed.** `.gitattributes` is now `* -text`, so nothing is converted at checkout, and the Windows launchers (`.cmd`, `.bat`, `.ps1`) are committed with CRLF; installed files match the repository byte for byte on every OS. `scripts/check_release.py` now fails on any attribute that converts content, on a Windows launcher with LF-only lines and on a shell script with CR bytes. - **Fixed: `showtime setup --home X` installs ffmpeg into X when a default install exists.** The default install's ffmpeg (first on the PATH setup runs with) counted as "the system ffmpeg", so X got none of its own and had no ffmpeg outside that PATH. Another showtime install's ffmpeg is no longer taken as the system's. - **Fixed: a page address with a malformed `%` escape** (`?x=%`) no longer stops the page runtime from starting in preview; that query pair is skipped. - **Changed: hardening.** The page runtime keeps its URL query parameters in a map with no prototype; the MCP server's `voice_say` writes its text to a new temporary file with a random name, readable by your user only; the benchmark harness marks its short sha1 labels as not used for security. - **Changed: `check` reports a probe that did not run as a warning** (`cut_probe_failed` and `webgpu_probe_failed`, were info), so a check whose first-frame-after-cut walk or WebGPU probe could not open its page never reads as clean, and `check --strict` fails on it. - **Changed: the X-ray embed element takes a `file:` address only on a page opened from disk.** A post read offline still loads its film from the disk; a web page that names a `file:` film now keeps the poster instead of framing an address the browser would never load. - **Fixed: `xray import` refuses a JSON file with too many object keys before parsing it.** One object with millions of keys passed the value and container limits and took about 930 MB to read (a 42 MB file); a file with more than 1,600,000 colons (object keys, counting those inside strings) is now refused, like too many values or objects. - **Fixed: `showtime assets icon` with a misspelled name suggests the right one at once.** A name the icon set does not have is checked against the set's listing (fetched once, kept 30 days) before its file is fetched; the icon CDN took about 40 s to say a file is missing, so the command ended with a network error and no suggestion. - **New: `check` finds text drawn over other text between its samples and on any canvas.** The page runtime records every text a page canvas paints (`fillText`/`strokeText`, the film API's or a scene's own) in render and check runs, and `check` compares their painted glyphs, the DOM's and both against each other on its whole-timeline grid: a pair that overlaps on two steps in a row, both holding (still, or blinking at half opacity or more; a crossfade or a fade does not hold), is a `text_overlap` warning that says when it starts and ends. A faint canvas text counts (alpha 0.15 and up, one of the two 0.5 and up). Canvas text counts only while it is on screen: not after its canvas is cleared, reset or painted over (an opaque fill or filled box, a film card), not clipped away, not under an opaque element or image above the canvas. Gradient text (`background-clip: text`) reads at full strength and hides nothing under it. A labelled counter ("TICK 12", "TICK 13", …) is one text, moving or not, while no other text on its canvas has the same words; labels that share their words ("ORBIT 1", "ORBIT 2") are each their own text, so they are followed as they move. A counter that shares its words with another label on its canvas is keyed by its string, which changes every step, so an overlap with it is not reported. A bare number ("42") is keyed by where it sits: one that moves gives short findings, or none when it moves more than 16 px a step. Not overlaps: the same string twice (a shadow pass), decor, a caption on its own plate over a canvas, DOM text turned far enough that its box is mostly empty (kinetic rows), and text layered on purpose: `F.text(..., {layer: true})`, `F.layer(() => ...)`, or `ST.layer(() => ...)` around raw canvas calls. - **Fixed: the `25-nobel-physics-ice-telescope` example had text over text.** Its "2,450 m" depth mark sat under a caption line at 0:57; both depth marks now sit above their ruler ticks. The `tutorial` template's closing line, drawn over the dimmed app on purpose, is marked as layered, so a new tutorial project checks clean. - **Fixed: `check` ends with an error when the page stops answering.** A seek on the dense timeline that missed its deadline only skipped that step, so a page whose renderer had stopped answering (while the browser itself still did) cost a minute per step, half an hour for a 3 s film, and check could wait with no end. Now a seek past its deadline asks the page for a quick answer: no answer, or a second late seek in a row, ends check with an error naming the seek and its last step, with a hint that points at the page's own code, and the browser closes with it. The dense timeline's other page reads, and the samples' text reads, wait under the same deadline. The error names the first seek that ran late, where the page got stuck (the seeks after it wait behind it), counts the seeks that failed in another way just before, and lists the last six seeks with how long each took; with `--json` the output is a report holding one `page_stuck` error and those seeks (`adopt` counts it as an error; `render --size` notes that check stopped early and goes on). - **Fixed: `export html --interactive` packs a page's own fonts** (#71). The X-ray live page and the living diagram's figure packed the files of linked stylesheets only; a project's own `@font-face` in the page's `