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
4 changes: 4 additions & 0 deletions .claude/skills/t27f/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ Traps from this tick:
- Lab scripts run under `sh`, which is dash: `$((10#$x))` fails with "expecting EOF". Strip leading zeros with `sed 's/^0*//'` and default with `${i:-0}`.
- The Mac hook reads the whole Bash command. A heredoc PR body that names the report command gets refused. Write the body with the Write tool and pass `--body-file`.

**Holding the address when the voice runs long (t27#10629).** If an overlay must stay on screen for a minimum time and must also stay off a crossfade, then the crossfade has to move; the overlay cannot. Work out in frames the latest cut that still leaves the last scene its minimum: cut 1 is capped at `BEAT_CUT1_LAST_MS` = 13249. Prove it is the latest with a one-ms-later control. Only the voices the reel speeds up are affected, so the `cmp` diff is limited to those voices and to crossfade 1's offset. The cost is real: the crossfade starts up to 184 ms under the task's last word, and lesson 1's measured voice (1300/4200/6800) is one of the affected voices. Write a test that bounds that overlap.

- Before trusting a "fast voice" grid result, work out which fixture voices are fast. `voice_room_ms(3)` is 12250 ms, so the measured lesson 1 voice (raw 12300) is already sped up. A change aimed at "fast voices" therefore changes the reel everyone actually sees.

## Show a spec (the core: exporter for what it reads, by hand for the rest)

1. Load the spec: `mcp__t27__t27_spec` with `path` and `source: true`, or the file from a
Expand Down
16 changes: 8 additions & 8 deletions .trinity/seals/t27f_lesson_reel.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:30f4f017bc4dd4c32c1ec195d83f827f10e9d260f30056ebd6f444cf034ca3ea",
"gen_hash_rust": "sha256:cd6858c82060106802b3cced271e84811d8ee02b8c4f2ed18ba75cf8068861eb",
"gen_hash_verilog": "sha256:d9d8bbee11770df310cf863654c914b318ea03940780b5a9e116cec3f4e33991",
"gen_hash_zig": "sha256:f5762a5368a29d6b2978d7ea365deed7c9c7a0d4994814a3d6586c039fdcbf25",
"gen_hash_c": "sha256:97c582c40a1e76a50e35c68414ce2653739784f9b0e3b58006deede027640fb3",
"gen_hash_rust": "sha256:f3b086a0827512fdebe957c9bc73b30592914373a1dd9137e57efb851ebf720f",
"gen_hash_verilog": "sha256:cc184e56898034ed556e2e7c9d111c028f56d60bb6adb21952a6b105d9baa0c9",
"gen_hash_zig": "sha256:aa16bbf44971ec5a6eacbd80389bca963b739a76f888cf401df14cb51abea29d",
"module": "lesson_reel",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-18T19:36:34Z",
"sealed_at": "2026-10-18T20:05:15Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:b1ca187bbf04f6595be7dbf25b6a656286b07a86c50def9a0605c9ec56f017ec",
"spec_hash": "sha256:a380a3fec6de690924f36c45f40ba7aac1e002f4482fdb23d71e472357b16f19",
"spec_path": "specs/t27f/lesson_reel.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 25,
"total": 25,
"passed": 27,
"total": 27,
"vacuous": 0
},
"toolchain": {
Expand Down
1 change: 1 addition & 0 deletions docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
{"path": "specs/claw/t27f_gate.t27", "t27b": "pass"},
{"path": "specs/claw/t27f_gate_pins.t27", "t27b": "pass"},
{"path": "specs/claw/text.t27", "t27b": "pass"},
{"path": "specs/claw/token_shape.t27", "t27b": "pass"},
{"path": "specs/compiler/c_array_repeat_wrapper.t27", "t27b": "pass"},
{"path": "specs/compiler/c_fixed_array_return.t27", "t27b": "pass"},
{"path": "specs/compiler/codegen_conformance.t27", "t27b": "pass"},
Expand Down
144 changes: 124 additions & 20 deletions specs/t27f/lesson_reel.t27
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@
// the same places, and only their windows change with the beat. The cards are still on their
// own frames over a crossfade: token_floor_mux prints their enable= and this file does not reach
// it. Both cuts are centred on a sentence change, so a crossfade can begin under the last words
// of a phrase: mode 5 then drops a caption with too little room left (the cost tests), and on a
// voice the reel had to speed up it holds the address for less than REEL_CTA_MIN_MS. The scenes
// themselves are R3.
// of a phrase: mode 5 then drops a caption with too little room left (the cost tests). Cut 1 is
// capped at BEAT_CUT1_LAST_MS so mode 5 holds the address for REEL_CTA_MIN_MS (t27#10629): on a
// voice the reel had to speed up, lesson 1's measured voice among them, crossfade 1 then starts
// up to 184 ms before the task's last word ends, in modes 4 and 5 alike. The scenes themselves
// are R3.
// phi^2 + 1/phi^2 = 3 | TRINITY

module lesson_reel;
Expand Down Expand Up @@ -709,11 +711,21 @@ pub fn beat_frames() -> i32 {
// the open floor, a crossfade, and the main beat alone for BEAT_MIN_SOLO_MS.
pub const BEAT_SPOKEN_MIN_MS : i32 = 2750;

// The latest cut 1 that still leaves the close beat's own frames REEL_CTA_MIN_MS to the reel's end
// (t27#10629): its crossfade starts on frame 389, so the close beat owns frames 405..449, 45 frames,
// 1500 ms. Mode 5 shows the address on those frames only, so a later cut holds it for less.
pub const BEAT_CUT1_LAST_MS : i32 = 13249;

// Cut k of lesson t's reel in language lang: 0 open to main, 1 main to close. Cut 0 is the goal's
// start, held to the open floor, but never so late that the main beat loses its scene: when the
// voice reaches the address before BEAT_SPOKEN_MIN_MS the open beat gives way, not the main one.
// Cut 1 is where the address takes over, but never after BEAT_CUT1_LAST_MS: on a voice the reel
// had to speed up the close beat comes in early, so the address keeps its hold in mode 5.
pub fn beat_cut_ms(t: usize, lang: usize, m0: i32, m1: i32, m2: i32, k: usize) -> i32 {
const c1 = cta_from(t, lang, m0, m1, m2);
var c1 : i32 = cta_from(t, lang, m0, m1, m2);
if (c1 > BEAT_CUT1_LAST_MS) {
c1 = BEAT_CUT1_LAST_MS;
}
if (k == 0) {
var s : i32 = phrase_start(m0, m1, m2, 1);
if (s < BEAT_OPEN_FLOOR_MS) {
Expand Down Expand Up @@ -1217,8 +1229,10 @@ test "on lesson 1's measured voice the cuts fall where the sentence changes" {
const c1 = beat_cut_ms(0, 0, M_EN0, M_EN1, M_EN2, 1);
assert(c0 == phrase_start(M_EN0, M_EN1, M_EN2, 1));
assert(c0 > BEAT_OPEN_FLOOR_MS);
assert(c1 == cta_from(0, 0, M_EN0, M_EN1, M_EN2));
// the open beat ends after the title is spoken, and the close starts after the task is
// the reel speeds this voice up to the address's floor, so cut 1 is capped (t27#10629)
assert(cta_from(0, 0, M_EN0, M_EN1, M_EN2) > BEAT_CUT1_LAST_MS);
assert(c1 == BEAT_CUT1_LAST_MS);
// the open beat ends after the title is spoken, and the close is at least half in after the task is
assert(c0 > phrase_start(M_EN0, M_EN1, M_EN2, 0) + played(M_EN0, M_EN1, M_EN2, 0));
assert(c1 >= phrase_start(M_EN0, M_EN1, M_EN2, 2) + played(M_EN0, M_EN1, M_EN2, 2));
}
Expand Down Expand Up @@ -1708,9 +1722,40 @@ test "the cost on lesson 1's measured voice: a caption across a crossfade with n
assert(dropped > 0);
}

test "the cost: on a voice the reel speeds up, mode 5 holds the address for less than REEL_CTA_MIN_MS" {
var short : usize = 0;
var least : i32 = beat_frames();
// ---- the address hold's tests (t27#10629) ----

// The voice's last spoken ms: the end of the last phrase, before its caption's tail.
fn spoken_end(m0: i32, m1: i32, m2: i32) -> i32 {
const n = phrases_for(m0, m1, m2);
return phrase_start(m0, m1, m2, n - 1) + played(m0, m1, m2, n - 1);
}

// How many frames mode 5 shows the address: from its first kept frame to the reel's last.
fn cta_held(t: usize, lang: usize, m0: i32, m1: i32, m2: i32) -> i32 {
const f0 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 0));
const f1 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 1));
const lo = beat_frame_from(cta_from(t, lang, m0, m1, m2));
return beat_frames() - beat_first_lo(lo, beat_frames() - 1, f0, f1);
}

test "BEAT_CUT1_LAST_MS is the latest cut 1 whose close beat owns REEL_CTA_MIN_MS" {
const f1 = beat_xfade_frame(BEAT_CUT1_LAST_MS);
assert(f1 == 389);
const own = beat_frames() - beat_scene_lo(BEAT_CLOSE, 0, f1);
assert(own == 45);
assert(own * 1000 >= REEL_CTA_MIN_MS * BASE_FPS);
// one ms later the crossfade starts a frame later and the close beat owns 44 frames
const g1 = beat_xfade_frame(BEAT_CUT1_LAST_MS + 1);
assert(g1 == 390);
assert((beat_frames() - beat_scene_lo(BEAT_CLOSE, 0, g1)) * 1000 < REEL_CTA_MIN_MS * BASE_FPS);
// the reel's address starts no later than the close beat could
assert(BEAT_CUT1_LAST_MS < REEL_MS - REEL_CTA_MIN_MS);
}

test "mode 5 holds the address for REEL_CTA_MIN_MS, every lesson, both languages, every voice the clock accepts" {
var voices : usize = 0;
var moved : usize = 0;
var short_before : usize = 0;
var t : usize = 0;
while (t < TOPICS) {
var lang : usize = 0;
Expand All @@ -1725,15 +1770,26 @@ test "the cost: on a voice the reel speeds up, mode 5 holds the address for less
const m1 = BEAT_GRID_M1[j];
const m2 = BEAT_GRID_M2[k];
if (voice_fits(m0, m1, m2)) {
const f0 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 0));
const f1 = beat_xfade_frame(beat_cut_ms(t, lang, m0, m1, m2, 1));
const lo = beat_frame_from(cta_from(t, lang, m0, m1, m2));
const held = beat_frames() - beat_first_lo(lo, beat_frames() - 1, f0, f1);
if (held < least) {
least = held;
voices = voices + 1;
const cta = cta_from(t, lang, m0, m1, m2);
const c1 = beat_cut_ms(t, lang, m0, m1, m2, 1);
assert(cta_held(t, lang, m0, m1, m2) * 1000 >= REEL_CTA_MIN_MS * BASE_FPS);
if (c1 < cta) {
moved = moved + 1;
assert(c1 == BEAT_CUT1_LAST_MS);
} else {
assert(c1 == cta);
}
// control: the cut centred on the address, as before t27#10629
const lo = beat_frame_from(cta);
var old_lo : i32 = beat_scene_lo(BEAT_CLOSE, 0, beat_xfade_frame(cta));
if (lo > old_lo) {
old_lo = lo;
}
if (held * 1000 < REEL_CTA_MIN_MS * BASE_FPS) {
short = short + 1;
if ((beat_frames() - old_lo) * 1000 < REEL_CTA_MIN_MS * BASE_FPS) {
short_before = short_before + 1;
// every voice the old cut held short is one the cap moves
assert(c1 < cta);
}
}
k = k + 1;
Expand All @@ -1746,7 +1802,55 @@ test "the cost: on a voice the reel speeds up, mode 5 holds the address for less
}
t = t + 1;
}
assert(short > 0);
// never less than the close beat's own frames: its solo floor less the crossfade's last frame
assert(least >= MIN_SCENE_FRAMES - 1);
assert(voices > 0);
assert(short_before > 0);
assert(moved >= short_before);
// most voices end early enough that the cut stays on the address
assert(moved < voices);
}

test "the cost: on a voice the cap moves, crossfade 1 starts under the task's last words, by at most 184 ms" {
const bound = REEL_MS - REEL_CTA_MIN_MS - CAPTION_TAIL_MS - beat_frame_ms(beat_xfade_frame(BEAT_CUT1_LAST_MS));
assert(bound == 184);
var under : usize = 0;
var t : usize = 0;
while (t < TOPICS) {
var lang : usize = 0;
while (lang < 2) {
var i : usize = 0;
while (i < 4) {
var j : usize = 0;
while (j < 4) {
var k : usize = 0;
while (k < 5) {
const m0 = BEAT_GRID_M0[i];
const m1 = BEAT_GRID_M1[j];
const m2 = BEAT_GRID_M2[k];
if (voice_fits(m0, m1, m2)) {
const c1 = beat_cut_ms(t, lang, m0, m1, m2, 1);
const x1 = beat_frame_ms(beat_xfade_frame(c1));
const e = spoken_end(m0, m1, m2);
if (x1 < e) {
under = under + 1;
// only a moved cut starts under the voice, and never by more than the bound
assert(c1 < cta_from(t, lang, m0, m1, m2));
assert(e - x1 <= bound);
}
}
k = k + 1;
}
j = j + 1;
}
i = i + 1;
}
lang = lang + 1;
}
t = t + 1;
}
assert(under > 0);
// lesson 1's measured voice is one of them: the reel speeds it up to the address's floor
const c1 = beat_cut_ms(0, 0, M_EN0, M_EN1, M_EN2, 1);
assert(c1 == BEAT_CUT1_LAST_MS);
assert(cta_from(0, 0, M_EN0, M_EN1, M_EN2) > BEAT_CUT1_LAST_MS);
assert(spoken_end(M_EN0, M_EN1, M_EN2) > beat_frame_ms(beat_xfade_frame(c1)));
}
Loading