diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index ca9aa3f621..e64667ce73 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -429,6 +429,16 @@ card's `PRESETS`; its lab check compares that list to `PRESET_NAMES` byte for by Five parallel libx264 encoders on the lab failed with "Error while opening encoder", the pipe closed, and md5 hashed a truncated stream: 3 of 5 read DIFF. Run md5 separately, then encode one at a time. A DIFF that coincides with an encoder error is the harness, not the code. +- **Run `preflight(IDEA)` before you render an idea** (gHashTag/t27#10820). It returns `PF_EMPTY` + (no scene), `PF_GLYPH` (a character glyph57 would draw as `?`, such as phi, a euro sign, `{`, `~` + or a malformed byte; `first_missing` gives its byte offset), `PF_LONG` (the reel would run past + `MAX_S` = 60 s) or `PF_OK`. `main` writes **0 bytes** for a refused idea, so an empty output file + means the idea was refused, not that the render broke. Rewrite the idea instead: `PHI^2`, not the + Greek letter. **Check the guard on the binary, with a control:** the same refused idea built with + the guard line `sed`-ed out must write frames (144 for `A{B}`), or the 0 bytes prove nothing. + **A mutant can survive and still be equivalent:** reels grow in 72-frame steps, so `MAX_S` from + 60 to 62 refuses exactly the same ideas. Say that in the report, and do not add a test that only + restates the constant. **Keep it FPGA-shaped.** Use Q8 fixed point. The per-step and per-pixel paths use only add, multiply, compare, table lookup (`kt` kernel ROM, `tt` tone ROM) and power-of-two division. diff --git a/.trinity/seals/t27f_idea_factory.json b/.trinity/seals/t27f_idea_factory.json index f6d9cd564a..ef63bbe1aa 100644 --- a/.trinity/seals/t27f_idea_factory.json +++ b/.trinity/seals/t27f_idea_factory.json @@ -15,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:bb1ef6081add1e9fa249288e6ebd223bc66be7f13d91210eed6dac48ed4a01aa", - "gen_hash_rust": "sha256:2420620e8b22b844cf0912ec4a21d7358a0045cec3508d8a14c6d8206f169ccb", - "gen_hash_verilog": "sha256:8f8cd1625c7f3d516bc5e597c4860b81f2a4860ce6d6f3cb4b06c2578568a49d", - "gen_hash_zig": "sha256:b4136a42488a6715a58f8a2e2e482871209c94a0009a78cab7a1f357db42c0d2", + "gen_hash_c": "sha256:b4cb42ec55deec10b3180769d04e36217f39dc786c7756387880cf26bc7f3f15", + "gen_hash_rust": "sha256:7632796cc4ee12e7d310e8115091e72248074a2e25ff02e8773ea5776ec6b2d1", + "gen_hash_verilog": "sha256:a1e8068e1df97e43a750658c626f76e47f52b7d62f249bc598af057a78a9e35f", + "gen_hash_zig": "sha256:6b2f7675bb3198c6045686c29eac989768cb5d3e0b00b14795be854be56a8c9f", "module": "idea_factory", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T07:20:44Z", + "sealed_at": "2026-10-19T07:36:57Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:a68fbc93ea94b32e264ae91b82b149eb7530c3af4e2787c8b457a2eeec0c3482", + "spec_hash": "sha256:44432d34553ebeb531b415544066d45f5928975c5a98fb95aa7bd69267430a5a", "spec_path": "specs/t27f/idea_factory.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 14, - "total": 14, + "passed": 17, + "total": 17, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/idea_factory.t27 b/specs/t27f/idea_factory.t27 index 9ce8b39d95..e7c0429f92 100644 --- a/specs/t27f/idea_factory.t27 +++ b/specs/t27f/idea_factory.t27 @@ -26,6 +26,9 @@ // A RUSSIAN IDEA is a u8 array of UTF-8 bytes (L3 keeps sources ASCII, and t27c keeps "\xNN" as // four characters): RU_IDEA below, passed as RU_IDEA[0..80]. // +// PRE-FLIGHT: main draws nothing for an idea preflight refuses -- empty, a character the font +// lacks, or a reel longer than MAX_S. +// // NOT CLAIMED: capitals only (lower case folds); no voice or music (lesson_reel mode 5 has those); // the Verilog is emitted, not synthesised, and no board has drawn it. // phi^2 + 1/phi^2 = 3 | TRINITY @@ -33,6 +36,8 @@ module idea_factory; use t27f::glyph57::glyph57_on; +use t27f::glyph57::glyph57_of; +use t27f::glyph57::QMARK; use t27f::glyph57::cp_len; use t27f::glyph57::cp_at; use t27f::glyph57::cp_count; @@ -304,6 +309,48 @@ pub fn pick_preset(t: []const u8, p: usize) -> usize { return p; } +// ---- pre-flight: an idea the factory cannot draw is refused before a frame (gHashTag/t27#10820) ---- + +// glyph57 draws a character it does not have as '?', so without this check an idea with a phi or +// a euro sign ships with the wrong letter on screen, and nothing notices. +pub const PF_OK : i32 = 0; +pub const PF_EMPTY : i32 = 1; // no scene: nothing but spaces and cuts +pub const PF_GLYPH : i32 = 2; // a character the font draws as '?' +pub const PF_LONG : i32 = 3; // the reel would run past MAX_S +pub const MAX_S : i32 = 60; // the factory's own cap on a reel, seconds + +// The characters of t that the font draws as '?': what 0 = how many, 1 = the byte offset of the +// first (-1 when there is none). A real '?', a '|' cut and a malformed byte are decided by code +// point: '?' and '|' do not count, BAD does. +pub fn missing_walk(t: []const u8, what: i32) -> i32 { + const n = @as(i32, @intCast(t.len)); + var i : i32 = 0; + var m : i32 = 0; + while (i < n) { + const c = cp_at(t, i); + if (c != 63 && c != 124 && glyph57_of(c) == QMARK) { + if (what == 1) { return i; } + m = m + 1; + } + i = i + cp_len(t, i); + } + if (what == 1) { return 0 - 1; } + return m; +} +pub fn missing_glyphs(t: []const u8) -> i32 { + return missing_walk(t, 0); +} +pub fn first_missing(t: []const u8) -> i32 { + return missing_walk(t, 1); +} +// Why the factory would refuse idea t, checked in this order; PF_OK when it would draw it. +pub fn preflight(t: []const u8) -> i32 { + if (scene_count(t) == 0) { return PF_EMPTY; } + if (missing_glyphs(t) > 0) { return PF_GLYPH; } + if (reel_frames(t) > MAX_S * FPS) { return PF_LONG; } + return PF_OK; +} + // ---- assign ---- // Where card particle m of mm sits on the border of the rectangle (x0, y0)-(x1, y1): what 0 = x, @@ -361,6 +408,7 @@ pub fn main() -> void { var accb : [1080]i32 = undefined; var row : [3240]u8 = undefined; + if (preflight(IDEA) != PF_OK) { return; } // refused: not one frame, so no reel ships wrong const p = pick_preset(IDEA, PRESET); const rr = reach(p); var d2 : i32 = 0; @@ -721,3 +769,52 @@ test "a batch of five ideas covers all five presets under PRESET_AUTO" { assert(preset_of("THE FPGA RUNS|THE SPEC") == P_PAPER); assert(preset_of("SEALED|EVERY HASH") == P_RAIN); } + +// ---- pre-flight (gHashTag/t27#10820) ---- +// UTF-8 bytes, since L3 keeps the source ASCII: phi (CF 86) " = 1"; "A " and the euro sign +// (E2 82 AC); "A", a stray continuation byte, "B". +pub const PF_PHI : [6]u8 = [207, 134, 32, 61, 32, 49]; +pub const PF_EURO : [5]u8 = [65, 32, 226, 130, 172]; +pub const PF_STRAY : [3]u8 = [65, 150, 66]; + +test "pre-flight counts the characters the font would draw as a question mark" { + assert(missing_glyphs("PARTICLES BUILD EVERY FRAME") == 0); + assert(missing_glyphs("WHY?") == 0); // a real '?' is drawn as itself + assert(missing_glyphs("A|B") == 0); // a cut is not drawn + assert(missing_glyphs("lower case folds") == 0); + assert(missing_glyphs(RU_IDEA[0..80]) == 0); + assert(missing_glyphs("A{B}") == 2); + assert(missing_glyphs("X~Y") == 1); + assert(missing_glyphs(PF_PHI[0..6]) == 1); + assert(missing_glyphs(PF_EURO[0..5]) == 1); // one code point, three bytes + assert(missing_glyphs(PF_STRAY[0..3]) == 1); // a malformed byte reads as BAD + assert(first_missing("WHY?") == 0 - 1); + assert(first_missing("A{B}") == 1); + assert(first_missing(PF_PHI[0..6]) == 0); + assert(first_missing(PF_EURO[0..5]) == 2); + assert(first_missing(PF_STRAY[0..3]) == 1); +} + +test "pre-flight refuses an empty idea, a missing glyph and a reel past MAX_S, in that order" { + assert(preflight("") == PF_EMPTY); + assert(preflight(" | | ") == PF_EMPTY); + assert(preflight(PF_PHI[0..6]) == PF_GLYPH); + assert(preflight(PF_EURO[0..5]) == PF_GLYPH); + assert(preflight(PF_STRAY[0..3]) == PF_GLYPH); + // 24 scenes and the address: 25 * 72 = 1800 frames, exactly MAX_S * FPS. + assert(reel_frames("A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X") == 1800); + assert(preflight("A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X") == PF_OK); + assert(preflight("A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y") == PF_LONG); + assert(preflight("{|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y") == PF_GLYPH); +} + +test "every idea this file renders passes pre-flight" { + assert(preflight(IDEA) == PF_OK); + assert(preflight(CTA) == PF_OK); + assert(preflight(RU_IDEA[0..80]) == PF_OK); + assert(preflight("SPEC IS THE SOURCE|EVERY VIEW DERIVED") == PF_OK); + assert(preflight("ONE LAW|MANY SURFACES") == PF_OK); + assert(preflight("THREE STATES|BEAT TWO") == PF_OK); + assert(preflight("THE FPGA RUNS|THE SPEC") == PF_OK); + assert(preflight("SEALED|EVERY HASH") == PF_OK); +}