diff --git a/.trinity/seals/automation_automation::course_access.json b/.trinity/seals/automation_automation::course_access.json index 4772a25972..5a547384c3 100644 --- a/.trinity/seals/automation_automation::course_access.json +++ b/.trinity/seals/automation_automation::course_access.json @@ -1,28 +1,28 @@ { - "built_by": "t27c-bootstrap@0.5.2+d2c8e7d41", + "built_by": "t27c-bootstrap@0.5.2+239b4b059", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:0724a34f57d23c9924b871a392774c7a19f957876fd60045f6e627e32c4db2f2", - "gen_hash_rust": "sha256:52e27f4fece6b2c73d5c1086b3b33a648af434a9451c732d522e742337de6838", - "gen_hash_verilog": "sha256:7afb008af0e4a2e5e4ced3e5acf0d2c41b99ea72c7f2090f159c18e4cf8fb82e", - "gen_hash_zig": "sha256:6dc8d990374d50fdae0e7ba44161f7c23fe8fbbc3a48a2bbb1b1d0362b3d663f", + "gen_hash_c": "sha256:a44fa8733f70d5560f2443f5151341d100273210f6311940e5a3918e04ce6535", + "gen_hash_rust": "sha256:9780e1a4766e8590ab9496771900236e1472d8d7b8a8a5938f82b93fb96ca7b2", + "gen_hash_verilog": "sha256:d448bc2dfdf9ad7860a499e394bbf3e7946d65072cb60181c719e9c202584716", + "gen_hash_zig": "sha256:c6a00ec44d6a4198d10d7d2492f4bd5b7c6964cda26eb72a4366734be029a1b7", "module": "automation::course_access", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T05:03:39Z", + "sealed_at": "2026-10-19T03:00:14Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:89b4c6d4a9a8f0eb7534d0b9ee98af631cabc474e964e0dcb4b0233afdef09e4", + "spec_hash": "sha256:edd87ade1cc387f4e153bf22ce4c51856178377e4b4434e679ba7539ba39b978", "spec_path": "specs/automation/course-access.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 16, - "total": 16, + "passed": 18, + "total": 18, "vacuous": 0 }, "toolchain": { - "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", "zig": "0.16.0" } } \ No newline at end of file diff --git a/docs/now/2026-10-19-course-access-v3-every-course-free.md b/docs/now/2026-10-19-course-access-v3-every-course-free.md new file mode 100644 index 0000000000..d0339c314c --- /dev/null +++ b/docs/now/2026-10-19-course-access-v3-every-course-free.md @@ -0,0 +1,11 @@ +# NOW -- course-access VERSION 3: every course free for everyone (2026-10-19) + +## specs/automation/course-access.t27 (Closes #10798) + +- Why: owner, 2026-10-19 -- "make the courses free for everyone! our task is to grow the language". A course behind the club is a reader the language does not get; the club stays the bot's own subscription for generations. +- `step_of(opened)` returns `STEP_FREE` for every rung (v2: free for three, then paid). `LADDER_RUNGS` counts the rungs that differ before `step_of` repeats its last: 4 in v2, 1 now. +- `STEP_BOT`, `STEP_PAID`, `may_open`, `stays_open`, `mint_ok`, `key_days` unchanged: a key already minted keeps opening what it opened. `lesson_event`, `tells_owner`, `may_report` do not depend on the paid rung and are unchanged; `SITE_CHECKOUT` and `CONTENT_HIDDEN` stay false. +- Tests: the old paid-ladder asserts now assert free; a new loop pins `step_of(n) == STEP_FREE` for n = 0..255 (767 runtime asserts); a new test keeps a held paid key opening a paid step. VERSION 3 with a dated header note and its test. +- t27c (bootstrap code unchanged since 239b4b059), zig 0.16.0: test-report 18 of 18 pass, 0 vacuous. Negative control: the v2 `step_of` body turns 3 tests red. +- `scripts/t27-logic.mjs` (999) lowers 9 fns, 0 not; in node `stepOf(1..10)` is all 0. Seal saved. +- Not here: re-vendoring into 999 render and trinity (`courseLadder.ts`). A course stored on a paid rung under v2 still closes by `stays_open` when the club ends; a host that must keep it open re-rungs it with `step_of`. diff --git a/specs/automation/course-access.t27 b/specs/automation/course-access.t27 index fa12026837..e61e4b3d3b 100644 --- a/specs/automation/course-access.t27 +++ b/specs/automation/course-access.t27 @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -// specs/automation/course-access.t27 -- the ladder to the t27 courses: the reader picks -// any three courses and they open free, the fourth and every one after it need the paid -// club; every reader who reaches the bot is filed in the owner's CRM, and the owner is -// told, live, each step they take. +// specs/automation/course-access.t27 -- the ladder to the t27 courses: since VERSION 3 +// every course opens free, for every reader, with one press; every reader who reaches +// the bot is filed in the owner's CRM, and the owner is told, live, each step they take. // Host: 999-multibots-telegraf // apps/vibee-editor/render/src/agent/course-access.ts (the key: mint, check; the // learner row; the CRM person, tag and touch; the owner's live message) and the routes in @@ -18,11 +17,12 @@ // THE LADDER. The reader chooses which course is which rung: the rungs are steps, not // courses. A step is FREE (one press, nothing asked), BOT (the reader opens the // bot with a course link; the bot hands back a key) or PAID (the bot checks the -// club and hands back a key that lives as long as the club does). Rungs past the -// fourth stay PAID. A course opened on a free or a bot rung stays open for that -// reader; one opened on a paid rung stays open only while he holds a paid key that -// has not run out (stays_open): the club is a subscription, not a purchase. The -// order is kept in his browser, the keys prove the two steps that need proof. +// club and hands back a key that lives as long as the club does). Which step a +// rung is, is step_of's; past LADDER_RUNGS it repeats the last. A course opened +// on a free or a bot rung stays open for that reader; one opened on a paid rung +// stays open only while he holds a paid key that has not run out (stays_open): +// the club is a subscription, not a purchase. The order is kept in his browser, +// the keys prove the two steps that need proof. // THE KEY. Signed by the render with a secret only the render holds; it carries the // step, an opaque learner id and an expiry, never a Telegram id or a name. A BOT // key does not expire; a PAID key expires with the club period it was minted @@ -56,6 +56,21 @@ // one after it PAID. STEP_BOT stays a step the render mints and may_open and // stays_open still honour, so a course already opened with a bot key stays open. // Paying on the site is still not built (SITE_CHECKOUT). +// VERSION 3, owner, 2026-10-19 -- "make the courses free for everyone! our task is to +// grow the language". A course that asks for the club is a reader the language +// does not get, so step_of returns FREE for every rung: no course needs the club +// or the bot. The club stays the bot's own subscription, for generations; it is no +// longer a door to a course. LADDER_RUNGS counts the rungs that differ before +// step_of repeats its last one: four in VERSION 2 (free, free, free, paid, then +// paid for good), one now (free, then free for good). STEP_BOT and STEP_PAID stay +// steps the render mints and may_open, stays_open, mint_ok and key_days still +// honour, so a key already minted keeps opening what it opened. What this leaves +// to the hosts: a course a reader stored on a paid rung under VERSION 2 is judged +// by stays_open with its stored step and closes when his club ends; a host that +// must keep it open re-rungs it with step_of. With no rung asking for the bot, a +// reader is filed in the CRM and reports lessons only if he opens the bot himself; +// the site still sends nothing before that (SENDS_BEFORE_BOT). SITE_CHECKOUT stays +// false: there is no checkout, and no course needs one. // Claim status: written before any live run; no key has been minted, no reader filed. // phi^2 + 1/phi^2 = 3 | TRINITY @@ -64,13 +79,13 @@ module automation::course_access { pub const KIND : str = "automation"; pub const ID : str = "course-access"; pub const REPO : str = "999-multibots-telegraf"; - pub const VERSION : u8 = 2; + pub const VERSION : u8 = 3; // ---- The steps --------------------------------------------------------------------- pub const STEP_FREE : u8 = 0; pub const STEP_BOT : u8 = 1; pub const STEP_PAID : u8 = 2; - pub const LADDER_RUNGS : u8 = 4; // free, free, free, paid; every rung after it is paid + pub const LADDER_RUNGS : u8 = 1; // one rung, free; every course after it is free too pub const COURSES_KNOWN : u8 = 7; // the catalog today; the ladder does not depend on it // ---- The bot link ------------------------------------------------------------------ @@ -102,11 +117,9 @@ module automation::course_access { pub const RUN_LIVE : bool = false; // Which step the next course is, given how many this reader has opened already. - // No rung is BOT since VERSION 2: the bot step was where readers stopped. + // No rung is BOT since VERSION 2 (the bot step was where readers stopped) and no rung + // is PAID since VERSION 3: every course is free, to grow the language. fn step_of(opened: u8) -> u8 { - if opened >= 3 { - return STEP_PAID; - } return STEP_FREE; } @@ -231,14 +244,33 @@ module automation::course_access { assert(may_open(STEP_BOT, true, false)); } - test "the fourth and every one after it need the club" { - assert(step_of(3) == STEP_PAID); - assert(step_of(4) == STEP_PAID); - assert(step_of(6) == STEP_PAID); - assert(step_of(200) == STEP_PAID); + test "the fourth and every one after it are free too, no club asked" { + assert(step_of(3) == STEP_FREE); + assert(step_of(4) == STEP_FREE); + assert(step_of(6) == STEP_FREE); + assert(step_of(200) == STEP_FREE); + assert(may_open(step_of(3), false, false)); + assert(may_open(step_of(200), false, false)); + assert(step_of(3) != STEP_PAID); + assert(LADDER_RUNGS == 1); + } + + test "every course is free, for every reader, however many he has opened" { + var n : u8 = 0; + while (n < 255) { + assert(step_of(n) == STEP_FREE); + assert(step_of(n) == step_of(LADDER_RUNGS - 1)); + assert(may_open(step_of(n), false, false)); + n += 1; + } + assert(step_of(255) == STEP_FREE); + assert(may_open(step_of(255), false, false)); + } + + test "a paid key a reader already holds still opens a paid step" { + assert(!may_open(STEP_PAID, false, false)); assert(!may_open(STEP_PAID, true, false)); assert(may_open(STEP_PAID, false, true)); - assert(LADDER_RUNGS == 4); } test "a club course closes when the club ends, the others stay" { @@ -327,9 +359,10 @@ module automation::course_access { } test "VERSION says what the header's last note says" { - assert(VERSION == 2); + assert(VERSION == 3); assert(step_of(VERSION) == STEP_FREE); - assert(step_of(VERSION + 1) == STEP_PAID); + assert(step_of(VERSION + 1) == STEP_FREE); + assert(step_of(VERSION + 1) != STEP_PAID); } } // phi^2 + 1/phi^2 = 3 | TRINITY