Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/website-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ jobs:
# and /ru/learn/<id>/, and every course a landing at its share path; the
# check refuses a page, card or learn/sitemap.xml that lags the specs.
- name: Course
run: npm run check:course && npm run test:course && npm run check:course-pages
run: npm run check:course && npm run test:course && npm run check:course-pages && npm run check:course-recipe

# The IGLA board on WARS: specs/queen/igla_board.t27 is generated in
# gHashTag/igla-coder-gpu from committed artifacts. The gate recompiles it,
Expand Down
51 changes: 51 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,57 @@ Read the blog skill before writing content. Preserve the user's img2img
triptych, mandatory hashtags, truthful limitations and relevant service offer.
Never omit the work report for a small, documentation-only or automation PR.

## New course to open a topic (Queen and every bee)

When a topic needs explaining, the answer is a course: **9 modules of 3
lessons each, 27 lessons** (owner, 2026-10-07). One topic is one course; two topics are two
courses, chained (the last lesson opens lesson 1 of the next). Live chain:
0 t27 basics -> 1 FPGA -> 2 AI numbers.

The recipe is a t27 module, not this prose:
[specs/course_recipe/course-27.t27](specs/course_recipe/course-27.t27) (steps,
rules, gates, cast commands; its tests hold the order). Planned courses:
[specs/course_recipe/roadmap.t27](specs/course_recipe/roadmap.t27). Read both
first; this section only points at them.

In short, so the Queen can assign it:

1. Copy the newest course spec in `apps/website/specs/course/` with its `-ru`
twin, rename ID / ROUTE / SHARE_PATH, register it in `courses.t27`.
2. Every lesson: one widget unique across all courses, one t27 spec that
compiles in the browser, black-and-white infographic or card.
3. Run the generators from `apps/website` (all-or-nothing):
`widgets-from-spec.mjs`, `widget-pages-from-spec.mjs`,
`course-from-spec.mjs`, `course-pages.mjs`; then the `check:course*`,
`check:widgets`, `check:onboarding` gates.
4. Casts are recorded only through `tri` commands; a lesson without a recording
says "pending", never fakes one.
5. The same PR carries its blog post (`body` English + `ruBody` Russian) and the
work report above.
6. Main moved under the PR: merge `origin/main` in, never rebase/force push.
A merged trinity PR is not live: ship through the
`release(board): ship trinity <sha>` PR in gHashTag/999-multibots-telegraf.

Traps found by the first course built from this section alone (trinity#1470,
full list in its "Recipe gaps"):

- Rename **ID, ROUTE, SHARE_PATH, RU_CONTRACT and GENERATED** in the copied spec.
- Code edits the recipe needs: route, starfield root and nav entry in `src/`;
blog post registered in `src/data/blog/index.ts` and `posts.ts`.
- `gallery.t27` keeps each category as one unbroken block: insert the new
widgets inside their category, then fix the index asserts in the gallery
tests; `course-from-spec.test.mjs` asserts the course count, bump it.
- Full gate list: `check:widgets`, `test:widgets-spec`, `check:widget-pages`
(run widget-pages before widgets), `check:course`, `test:course`,
`check:course-pages`, `check:course-recipe`, `check:onboarding`,
`check:play`, `tsc --noEmit`.
- A pending lesson still needs a gallery entry; ship it as a
`public/term/<id>/` placeholder with `pending`, never a fake widget.
- Russian widget words go in `i18n/widgets.ru.json` under the contract
`specs/widgets/i18n/widgets-ru.t27` (no Cyrillic in specs); the Russian text
must keep the same digits as the English.
- Work-report format: copy the `<!-- t27-work-report -->` block of trinity#1463.

**Version**: 2.0
**Date**: 2026-04-04
**Status**: Active
Expand Down
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"check:course": "node scripts/course-from-spec.mjs --check",
"test:course": "node --test scripts/course-from-spec.test.mjs",
"gen:course-pages": "node scripts/course-pages.mjs",
"check:course-recipe": "node scripts/check-course-recipe.mjs",
"check:course-pages": "node scripts/course-pages.mjs --check && node --test scripts/course-pages.test.mjs",
"check:widget-pages": "node scripts/widget-pages-from-spec.mjs --check",
"test:widget-pages": "node --test scripts/widget-pages-from-spec.test.mjs",
Expand Down
30 changes: 30 additions & 0 deletions apps/website/scripts/check-course-recipe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Compile every specs/course_recipe/*.t27 with the site's wasm compiler and run its test blocks.
// Exit 1 on a compile error, a failing assert, a file with zero tests, or an empty directory,
// so a recipe step cannot be dropped silently. Run from apps/website: npm run check:course-recipe
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadCompiler, verdictOf, constsOf } from './agents-from-specs.mjs'
import { runSpecTests } from './viewport-from-spec.mjs'

const site = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const dir = path.resolve(site, process.argv[2] ?? '../../specs/course_recipe')
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.t27')).sort()
if (!files.length) {
console.error(`check:course-recipe: no .t27 files in ${dir}`)
process.exit(1)
}
const analyze = await loadCompiler(fs.readFileSync(path.join(site, 'public/t27/t27_compiler.wasm')))
let bad = 0
for (const f of files) {
const a = analyze(fs.readFileSync(path.join(dir, f), 'utf8'))
const v = verdictOf(a)
const env = Object.fromEntries(Object.entries(constsOf(a)).map(([k, c]) => [k, c.value]))
const r = runSpecTests(a, env)
const ok = v.typecheckOk && !v.errors && !v.discarded && v.hirOk && r.tests > 0 && !r.failures.length
if (!ok) bad++
console.log(`${ok ? 'ok ' : 'FAIL'} ${f} module ${a.ast?.name} tests ${r.tests} asserts ${r.asserts} ${JSON.stringify(v)}`)
if (!r.tests) console.log(' no tests')
for (const x of r.failures) console.log(' fail:', typeof x === 'string' ? x : JSON.stringify(x))
}
process.exit(bad ? 1 : 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Block } from '../types'

export const body: Block[] = [
{ "kind": "h", "text": "What changed" },
{ "kind": "p", "text": "The course recipe in specs/course_recipe had tests, but nothing ran them, so a step could be deleted and nobody would notice. The Course job of the website checks now runs npm run check:course-recipe, which compiles every .t27 file in that directory with the site's own wasm compiler and runs its test blocks." },
{ "kind": "h", "text": "What makes it fail" },
{ "kind": "p", "text": "A compile error, a failing assert, a file with zero tests, or an empty directory. To prove it, a mutant changed an asserted constant in roadmap.t27: the check went red with exit code 1, and after the file was restored it went green again." },
{ "kind": "h", "text": "Limits" },
{ "kind": "p", "text": "The check runs the tests the modules already have. It does not judge whether those tests cover every step of the recipe." }
]

export const ruBody: Block[] = [
{ "kind": "h", "text": "Что изменилось" },
{ "kind": "p", "text": "У рецепта курса в specs/course_recipe были тесты, но их никто не запускал, так что шаг можно было удалить незаметно. Теперь задание Course в проверках сайта запускает npm run check:course-recipe: он собирает каждый файл .t27 в этой папке wasm-компилятором самого сайта и прогоняет его тесты." },
{ "kind": "h", "text": "Когда проверка падает" },
{ "kind": "p", "text": "Ошибка сборки, проваленный assert, файл без тестов или пустая папка. Для доказательства мутант поменял проверяемую константу в roadmap.t27: проверка покраснела с кодом 1, а после восстановления файла снова позеленела." },
{ "kind": "h", "text": "Ограничения" },
{ "kind": "p", "text": "Проверка гоняет те тесты, что уже есть в модулях. Покрывают ли они каждый шаг рецепта, она не судит." }
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Block } from '../types'

export const body: Block[] = [
{ "kind": "h", "text": "What changed" },
{ "kind": "p", "text": "The recipe for a t27 course used to live in one agent's private skill folder, where the Queen and her bees could not read it. It now lives in the repository as two t27 modules: specs/course_recipe/course-27.t27 (the steps, rules, gates and cast commands, with tests that hold their order) and specs/course_recipe/roadmap.t27 (the planned courses). AGENTS.md gains a short section that points at them, so any agent asked to open a topic knows the shape: 9 modules of 3 lessons, one topic per course, courses chained." },
{ "kind": "h", "text": "What is checked" },
{ "kind": "p", "text": "Both modules compile clean in the site's own wasm compiler: course-27.t27 has 7 tests with 26 asserts, roadmap.t27 has 4 tests with 30 asserts. Nothing yet runs them in CI; that is the next gap." },
{ "kind": "h", "text": "Limits" },
{ "kind": "p", "text": "A recipe is not a course. No new lessons ship with this PR, and the casts of the existing courses are still pending where the post of each course says so." }
]

export const ruBody: Block[] = [
{ "kind": "h", "text": "Что изменилось" },
{ "kind": "p", "text": "Рецепт курса t27 жил в личной папке навыков одного агента, куда Королева и её пчёлы не могли заглянуть. Теперь он лежит в репозитории двумя модулями t27: specs/course_recipe/course-27.t27 (шаги, правила, шлюзы и команды записи, а тесты держат их порядок) и specs/course_recipe/roadmap.t27 (план курсов). В AGENTS.md появился короткий раздел со ссылкой на них, так что любой агент, которому поручили раскрыть тему, знает форму: 9 модулей по 3 урока, одна тема — один курс, курсы идут цепочкой." },
{ "kind": "h", "text": "Что проверено" },
{ "kind": "p", "text": "Оба модуля чисто собираются компилятором сайта на wasm: в course-27.t27 7 тестов и 26 проверок, в roadmap.t27 4 теста и 30 проверок. В CI их пока ничто не запускает — это следующая дыра." },
{ "kind": "h", "text": "Ограничения" },
{ "kind": "p", "text": "Рецепт — ещё не курс. Новых уроков в этом PR нет, а записи существующих курсов по-прежнему ждут там, где об этом сказано в посте каждого курса." }
]
46 changes: 46 additions & 0 deletions apps/website/src/data/blog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ export const postsIndex: PostMeta[] = [
],
},
},
{
slug: "a-recipe-step-cannot-be-dropped-silently",
title: "A recipe step cannot be dropped silently",
summary: "[a CI gate, not new lessons] The website checks now compile and run the tests of the t27 course recipe, and fail on a compile error, a failing assert or a module with no tests.",
date: "2026-10-07",
readingMinutes: 1,
tags: ["t27", "Course", "CI"],
receipts: [
{ label: "The gate, apps/website/scripts/check-course-recipe.mjs", href: "https://github.com/gHashTag/trinity/blob/main/apps/website/scripts/check-course-recipe.mjs" },
{ label: "The recipe, specs/course_recipe/course-27.t27", href: "https://github.com/gHashTag/trinity/blob/main/specs/course_recipe/course-27.t27" },
],
openQuestions: [
"Whether the recipe's tests cover every step is not checked.",
],
published: true,
ru: {
title: "Шаг рецепта больше не пропадёт незаметно",
summary: "[проверка CI, а не новые уроки] Проверки сайта теперь собирают и запускают тесты рецепта курса t27 и падают на ошибке сборки, проваленном assert или модуле без тестов.",
openQuestions: [
"Покрывают ли тесты рецепта каждый шаг, не проверяется.",
],
},
},
{
slug: "clocks-resets-and-cdc",
title: "Clocks, resets and clock-domain crossings, in 27 lessons",
Expand Down Expand Up @@ -253,6 +276,29 @@ export const postsIndex: PostMeta[] = [
],
},
},
{
slug: "the-queen-knows-how-to-build-a-course",
title: "The Queen now knows how to build a course",
summary: "[a recipe, not a course; the recipe modules compile but no CI job runs their tests yet] The t27 course recipe moved from one agent's private folder into the repository as two t27 modules, and AGENTS.md points at them, so the Queen can assign a new 27-lesson course for any topic.",
date: "2026-10-07",
readingMinutes: 2,
tags: ["t27", "Course", "Agents"],
receipts: [
{ label: "The recipe, specs/course_recipe/course-27.t27", href: "https://github.com/gHashTag/trinity/blob/main/specs/course_recipe/course-27.t27" },
{ label: "The plan, specs/course_recipe/roadmap.t27", href: "https://github.com/gHashTag/trinity/blob/main/specs/course_recipe/roadmap.t27" },
],
openQuestions: [
"No CI job runs the recipe's tests yet.",
],
published: true,
ru: {
title: "Королева теперь знает, как собрать курс",
summary: "[рецепт, а не курс; модули рецепта собираются, но их тесты пока не запускает CI] Рецепт курса t27 переехал из личной папки одного агента в репозиторий двумя модулями t27, а AGENTS.md ссылается на них, так что Королева может поручить новый курс из 27 уроков на любую тему.",
openQuestions: [
"Тесты рецепта пока не запускает ни одно задание CI.",
],
},
},
{
slug: "a-course-pr-carries-its-post",
title: "A course PR now carries its blog post",
Expand Down
4 changes: 4 additions & 0 deletions apps/website/src/data/blog/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { body as body_course_three_verifying_hardware, ruBody as ruBody_course_t
import { body as body_course_zero_t27_basics, ruBody as ruBody_course_zero_t27_basics } from './bodies/course-zero-t27-basics'
import { body as body_tri_test_now_runs_the_tests, ruBody as ruBody_tri_test_now_runs_the_tests } from './bodies/tri-test-now-runs-the-tests'
import { body as body_two_courses_twenty_seven_lessons_each, ruBody as ruBody_two_courses_twenty_seven_lessons_each } from './bodies/two-courses-twenty-seven-lessons-each'
import { body as body_a_recipe_step_cannot_be_dropped_silently, ruBody as ruBody_a_recipe_step_cannot_be_dropped_silently } from './bodies/a-recipe-step-cannot-be-dropped-silently'
import { body as body_the_queen_knows_how_to_build_a_course, ruBody as ruBody_the_queen_knows_how_to_build_a_course } from './bodies/the-queen-knows-how-to-build-a-course'
import { body as body_a_course_pr_carries_its_post, ruBody as ruBody_a_course_pr_carries_its_post } from './bodies/a-course-pr-carries-its-post'
import { body as body_one_outlier_twenty_three_zeros, ruBody as ruBody_one_outlier_twenty_three_zeros } from './bodies/one-outlier-twenty-three-zeros'
import { body as body_a_terminal_for_seven_backends_in_an_x_post, ruBody as ruBody_a_terminal_for_seven_backends_in_an_x_post } from './bodies/a-terminal-for-seven-backends-in-an-x-post'
Expand Down Expand Up @@ -110,6 +112,8 @@ const bodies: Record<string, PostBody> = {
'course-zero-t27-basics': { body: body_course_zero_t27_basics, ruBody: ruBody_course_zero_t27_basics },
'tri-test-now-runs-the-tests': { body: body_tri_test_now_runs_the_tests, ruBody: ruBody_tri_test_now_runs_the_tests },
'two-courses-twenty-seven-lessons-each': { body: body_two_courses_twenty_seven_lessons_each, ruBody: ruBody_two_courses_twenty_seven_lessons_each },
'a-recipe-step-cannot-be-dropped-silently': { body: body_a_recipe_step_cannot_be_dropped_silently, ruBody: ruBody_a_recipe_step_cannot_be_dropped_silently },
'the-queen-knows-how-to-build-a-course': { body: body_the_queen_knows_how_to_build_a_course, ruBody: ruBody_the_queen_knows_how_to_build_a_course },
'a-course-pr-carries-its-post': { body: body_a_course_pr_carries_its_post, ruBody: ruBody_a_course_pr_carries_its_post },
'one-outlier-twenty-three-zeros': { body: body_one_outlier_twenty_three_zeros, ruBody: ruBody_one_outlier_twenty_three_zeros },
'a-terminal-for-seven-backends-in-an-x-post': { body: body_a_terminal_for_seven_backends_in_an_x_post, ruBody: ruBody_a_terminal_for_seven_backends_in_an_x_post },
Expand Down
Loading
Loading