diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index f3862bda01..46bceeec66 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -68,7 +68,7 @@ the reels. Describing T27F as #1677 is the error this section exists to stop (ow | Renderer in the Trinity UI | gHashTag/trinity | **missing**; no T27F widget is live | | Application: topic map (#1677) | `specs/t27f/topic_engine.t27`, `topic_reel.t27` | spec; procedure below | | Application: reels and deck | `specs/t27f/reel.t27`, `deck.t27`, `reel_*.t27` | spec; two reels | -| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. No renderer draws a plan yet (R3), and `lesson_reel` still draws token_floor (R4). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". A printing `main` makes t27b reject the spec | +| Application: content factory (epic t27#10560) | `specs/t27f/reel_factory.t27` (t27#10561) | spec with tests: an item's code and title, read keyword by keyword, give 11 features. Those pick 1 of 12 scene archetypes (best pair first, cap 4, no repeat within 2 items, then the least-used generic scene) plus an accent, a motion, a layout and 8 output formats. No renderer draws a plan yet (R3), and `lesson_reel` still draws token_floor (R4). Lessons: read the plan's spread over the whole course before trusting it. A greedy walk in course order gave 7 lessons one scene. Code counts alone handed lesson 8 the tests scene over "A test block". Gate: `tri factory`. The plan prints from a separate module, `specs/t27f/reel_factory_plan.t27` (t27#10573): `t27c gen` it, then run the zig (t27c has no `run`). Names come from the factory's comma lists, so the law module keeps no `main`. Traps: the lab's `/root/t27` t27b can predate #10455 and refuse `std.debug.print` that master accepts, so build a fresh t27b in its own worktree with its own `CARGO_TARGET_DIR` before you call a print unsupported. `t27c seal --save` needs zig on PATH (`/opt/zig`). A base64 line fetched back from fs-lab merges with the Railway warning, so wrap it as `B64:...:END`. A new `tri` arm must start its own line (` name)`) or the documented-commands checker misses it | | Related | trinity#1647 dashboards, #1652 MVP research, #1653 three skills, #1655 evolution runtime | open | When asked "how is T27F going", report against the core rows first. Progress on the applications diff --git a/.trinity/seals/t27f_reel_factory_plan.json b/.trinity/seals/t27f_reel_factory_plan.json new file mode 100644 index 0000000000..33c0b1da02 --- /dev/null +++ b/.trinity/seals/t27f_reel_factory_plan.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/t27f/reel_factory.t27", + "spec_hash": "sha256:96099d764ba86fa3ef2c29d81c3838ce255bfd3b3abbd37b27cb86e4bca91012" + }, + { + "spec": "specs/t27f/reel_topics.t27", + "spec_hash": "sha256:7c1762cde96524791711594d23f880fbad1f497a499277710f1fd61d52d50157" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:861641e09545b66dd73e9e63988853f140d9b6bf4ff1c8f87461dfdffd859b5f", + "gen_hash_rust": "sha256:0df5cdc9d474435a712a6628be2b9e4004f21cbfaec003a9740a09637cbdcb58", + "gen_hash_verilog": "sha256:cc61c0aa8776412954be480c0e2284d32178b5db0a3579d3b4e2d3c847f132ef", + "gen_hash_zig": "sha256:1cc73810b1c24913cdfd6589f4d01f0c5c205b6d148049c395f4a6a700883b6c", + "module": "reel_factory_plan", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T17:45:41Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:2d2373991269e2f9d252c07a274484dd983c9eda6e543cc4338620d77a71604c", + "spec_path": "specs/t27f/reel_factory_plan.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 6e4b977d5f..03b2d68e07 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1448,6 +1448,7 @@ {"path": "specs/t27f/quest.t27", "t27b": "pass"}, {"path": "specs/t27f/reel.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_factory.t27", "t27b": "pass"}, + {"path": "specs/t27f/reel_factory_plan.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_gf16_nmse.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_marketing.t27", "t27b": "pass"}, {"path": "specs/t27f/reel_particles.t27", "t27b": "blocked", "blocker": "ExprCall(@sin)", "reason": "unimplemented"}, diff --git a/scripts/tri b/scripts/tri index 058dce193f..0f8b3635aa 100755 --- a/scripts/tri +++ b/scripts/tri @@ -41,7 +41,7 @@ tri -- the t27 front door. Compiler commands (forwarded to t27c): tri test [--json] [...] run the conformance suite (t27c suite --repo-root .) - tri papers recompute the numbers three format papers print (specs/numeric/paper_claims.t27) + tri papers | factory the numbers three format papers print (specs/numeric/paper_claims.t27) | the reel factory's uniqueness gate (specs/t27f/reel_factory.t27) tri forwarded to t27c unchanged (gen, seal, fpga-build, ...) Local commands (no compiler needed): @@ -531,7 +531,8 @@ if [[ ! -x "$T27C" ]]; then exit 2 fi -[[ "$cmd" == "papers" ]] && exec "$T27C" test-report "$REPO_ROOT/specs/numeric/paper_claims.t27" "${@:2}" +case "$cmd" in papers) exec "$T27C" test-report "$REPO_ROOT/specs/numeric/paper_claims.t27" "${@:2}";; + factory) exec "$T27C" test-report "$REPO_ROOT/specs/t27f/reel_factory.t27" "${@:2}";; esac # Most subcommands are implemented in the Rust `tri` binary (cli/tri), # which is a DIFFERENT binary from t27c. Everything not handled above was # forwarded to t27c, so every one of them existed and was unreachable through the @@ -543,9 +544,8 @@ fi # as the fix when the gate fails, which answered `unrecognized subcommand # 'now'`. A gate whose remediation does not run is a gate that fails twice. # -# Only commands t27c does NOT implement are rerouted, so no existing route -# moves: ci, gates, gen, health, serve, test and verdict are names both -# binaries carry, and they keep going exactly where they went before. +# Only commands t27c does NOT implement are rerouted, so no existing route moves: ci, gates, gen, +# health, serve, test and verdict are names both binaries carry, and they keep going where they went. if [[ "$cmd" =~ ^[a-z][a-z0-9-]*$ ]]; then TRI_BIN="${TRI_BIN:-}" if [[ -z "$TRI_BIN" ]]; then diff --git a/specs/t27f/reel_factory_plan.t27 b/specs/t27f/reel_factory_plan.t27 new file mode 100644 index 0000000000..b14afaac68 --- /dev/null +++ b/specs/t27f/reel_factory_plan.t27 @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/reel_factory_plan.t27 -- prints the content factory's plan for course 0, one +// tab-separated row per lesson. Issue gHashTag/t27#10573, epic gHashTag/t27#10560. +// +// WHY: reel_factory.t27 decides every lesson's scene, but the plan lived only inside its tests. +// Its first version printed from a main, and t27b refused that main on std.debug.print, so the +// main was dropped. t27b now lowers print with a literal format of {d} and {s} (#10455), so the +// printer lives here, apart from the law it reads. reel_factory.t27 stays a module of decisions +// and nothing else, and this module only reads it out. +// +// HOW (main): t27c gen specs/t27f/reel_factory_plan.t27 > plan.zig, then run plan.zig with zig. +// It prints a header, 27 rows of +// lesson scene accent motion layout slides title +// and one summary line: how many of the twelve scenes are used, and the most uses of any one. +// Every name is read out of the factory's own comma lists (ARCH_NAMES, ACCENT_NAMES, +// MOTION_NAMES, LAYOUT_NAMES), so a renamed scene cannot drift between the law and its printout. +// +// NOT CLAIMED: +// - A title byte outside printable ASCII prints as '?'. The English titles of course 0 are ASCII +// today, and the tests check that, but the printer does not encode UTF-8. +// - The printout is a reading of arche(), not a second decision. Each row calls arche(t), which +// plans the whole course again. That is 27 plans, cheap at 27 lessons, and a cost to revisit +// if a course grows to hundreds of items. + +module reel_factory_plan; + +use t27f::reel_topics::TOPICS; +use t27f::reel_topics::item; +use t27f::reel_topics::item_len; +use t27f::reel_topics::cp; +use t27f::reel_factory::ARCHS; +use t27f::reel_factory::ARCH_CAP; +use t27f::reel_factory::ARCH_NAMES; +use t27f::reel_factory::ACCENTS; +use t27f::reel_factory::ACCENT_NAMES; +use t27f::reel_factory::MOTIONS; +use t27f::reel_factory::MOTION_NAMES; +use t27f::reel_factory::LAYOUTS; +use t27f::reel_factory::LAYOUT_NAMES; +use t27f::reel_factory::A_TRIT; +use t27f::reel_factory::arche; +use t27f::reel_factory::accent; +use t27f::reel_factory::motion; +use t27f::reel_factory::layout; +use t27f::reel_factory::carousel_slides; + +pub const TITLE_MAX : usize = 48; +pub const COMMA : u8 = 44; +pub const UNPRINTABLE : u8 = 63; + +// ---- names ---- + +// Where field k of a comma-separated list begins; names.len when the list has k fields or fewer. +pub fn field_start(names: []const u8, k: usize) -> usize { + var p : usize = 0; + var seen : usize = 0; + while (p < names.len && seen < k) { + if (names[p] == COMMA) { + seen = seen + 1; + } + p = p + 1; + } + return p; +} + +// One past the last byte of field k. +pub fn field_end(names: []const u8, k: usize) -> usize { + var p = field_start(names, k); + while (p < names.len && names[p] != COMMA) { + p = p + 1; + } + return p; +} + +// How many fields a comma-separated list has. +pub fn fields(names: []const u8) -> usize { + var n : usize = 1; + var p : usize = 0; + while (p < names.len) { + if (names[p] == COMMA) { + n = n + 1; + } + p = p + 1; + } + return n; +} + +// ---- titles ---- + +// How many bytes of lesson t's English title the printout shows. +pub fn title_n(t: usize) -> usize { + const n = item_len(item(t, 0, 0)); + if (n > TITLE_MAX) { + return TITLE_MAX; + } + return n; +} + +// Byte k of lesson t's English title, or UNPRINTABLE for a code point outside printable ASCII. +pub fn title_byte(t: usize, k: usize) -> u8 { + const c = cp(item(t, 0, 0), k); + if (c < 32 || c > 126) { + return UNPRINTABLE; + } + return @as(u8, @intCast(c)); +} + +// ---- main ---- + +pub fn main() -> void { + var used : [12]usize = undefined; + var a : usize = 0; + while (a < ARCHS) { + used[a] = 0; + a = a + 1; + } + std.debug.print("lesson\tscene\taccent\tmotion\tlayout\tslides\ttitle\n", .{}); + var t : usize = 0; + while (t < TOPICS) { + const s = arche(t); + used[s] = used[s] + 1; + const c = accent(t); + const m = motion(t); + const y = layout(t); + var buf : [48]u8 = undefined; + const n = title_n(t); + var k : usize = 0; + while (k < n) { + buf[k] = title_byte(t, k); + k = k + 1; + } + std.debug.print("{d}\t{s}\t{s}\t{s}\t{s}\t{d}\t{s}\n", .{ t, ARCH_NAMES[field_start(ARCH_NAMES, s)..field_end(ARCH_NAMES, s)], ACCENT_NAMES[field_start(ACCENT_NAMES, c)..field_end(ACCENT_NAMES, c)], MOTION_NAMES[field_start(MOTION_NAMES, m)..field_end(MOTION_NAMES, m)], LAYOUT_NAMES[field_start(LAYOUT_NAMES, y)..field_end(LAYOUT_NAMES, y)], carousel_slides(t), buf[0..n] }); + t = t + 1; + } + var kinds : usize = 0; + var most : usize = 0; + a = 0; + while (a < ARCHS) { + if (used[a] > 0) { + kinds = kinds + 1; + } + if (used[a] > most) { + most = used[a]; + } + a = a + 1; + } + std.debug.print("{d} of {d} scenes used, the most used one {d} times (cap {d})\n", .{ kinds, ARCHS, most, ARCH_CAP }); +} + +// ---- tests ---- + +test "a comma list reads field by field, first, middle and last" { + assert(field_start(ARCH_NAMES, 0) == 0); + assert(field_end(ARCH_NAMES, 0) == 4); + assert(ARCH_NAMES[field_start(ARCH_NAMES, 3)] == 116); + assert(field_end(ARCH_NAMES, 3) - field_start(ARCH_NAMES, 3) == 4); + assert(field_end(ARCH_NAMES, ARCHS - 1) == ARCH_NAMES.len); + assert(field_end(ARCH_NAMES, ARCHS - 1) - field_start(ARCH_NAMES, ARCHS - 1) == 3); + assert(field_start(ARCH_NAMES, ARCHS) == ARCH_NAMES.len); + assert(ACCENT_NAMES[field_start(ACCENT_NAMES, ACCENTS - 1)] == 119); +} + +test "every list has exactly as many names as the factory has kinds" { + assert(fields(ARCH_NAMES) == ARCHS); + assert(fields(ACCENT_NAMES) == ACCENTS); + assert(fields(MOTION_NAMES) == MOTIONS); + assert(fields(LAYOUT_NAMES) == LAYOUTS); + var k : usize = 0; + while (k < ARCHS) { + assert(field_end(ARCH_NAMES, k) > field_start(ARCH_NAMES, k)); + k = k + 1; + } +} + +test "every lesson's title prints, ASCII and within the width" { + var t : usize = 0; + while (t < TOPICS) { + const n = title_n(t); + assert(n > 0); + assert(n <= TITLE_MAX); + var k : usize = 0; + while (k < n) { + assert(title_byte(t, k) != UNPRINTABLE); + k = k + 1; + } + t = t + 1; + } +} + +test "the printed scene of the trits lesson is the word trit" { + const s = arche(7); + assert(s == A_TRIT); + const b = field_start(ARCH_NAMES, s); + assert(field_end(ARCH_NAMES, s) - b == 4); + assert(ARCH_NAMES[b] == 116); + assert(ARCH_NAMES[b + 1] == 114); + assert(ARCH_NAMES[b + 2] == 105); + assert(ARCH_NAMES[b + 3] == 116); +}