From 68c028280cb4160aa5bd2e92358771013f955110 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Wed, 7 Oct 2026 18:18:10 +0700 Subject: [PATCH 1/2] ci(website): compile and test specs/course_recipe in the Course job check:course-recipe fails on a compile error, a failing assert, a module with zero tests, or an empty directory. Blog post included. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/website-checks.yml | 2 +- apps/website/package.json | 1 + apps/website/scripts/check-course-recipe.mjs | 30 +++++++++++++++++++ ...-recipe-step-cannot-be-dropped-silently.ts | 19 ++++++++++++ apps/website/src/data/blog/index.ts | 23 ++++++++++++++ apps/website/src/data/blog/posts.ts | 2 ++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 apps/website/scripts/check-course-recipe.mjs create mode 100644 apps/website/src/data/blog/bodies/a-recipe-step-cannot-be-dropped-silently.ts diff --git a/.github/workflows/website-checks.yml b/.github/workflows/website-checks.yml index 21e49429fd..7e254727fb 100644 --- a/.github/workflows/website-checks.yml +++ b/.github/workflows/website-checks.yml @@ -388,7 +388,7 @@ jobs: # its own preview card at /learn// and /ru/learn//; the check # refuses a page, card or learn/sitemap.xml that lags the spec. - 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, diff --git a/apps/website/package.json b/apps/website/package.json index a4ac5a451b..a96b478993 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -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", diff --git a/apps/website/scripts/check-course-recipe.mjs b/apps/website/scripts/check-course-recipe.mjs new file mode 100644 index 0000000000..46f071006a --- /dev/null +++ b/apps/website/scripts/check-course-recipe.mjs @@ -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) diff --git a/apps/website/src/data/blog/bodies/a-recipe-step-cannot-be-dropped-silently.ts b/apps/website/src/data/blog/bodies/a-recipe-step-cannot-be-dropped-silently.ts new file mode 100644 index 0000000000..d4505b30c3 --- /dev/null +++ b/apps/website/src/data/blog/bodies/a-recipe-step-cannot-be-dropped-silently.ts @@ -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": "Проверка гоняет те тесты, что уже есть в модулях. Покрывают ли они каждый шаг рецепта, она не судит." } +] diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index dc8f13f528..8a8467e5af 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -2,6 +2,29 @@ import type { PostMeta } from './types' /** Индекс блога: список и метаданные без тяжёлых тел публикаций. */ 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: "the-queen-knows-how-to-build-a-course", title: "The Queen now knows how to build a course", diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts index 994c130219..bdb7f8dc82 100644 --- a/apps/website/src/data/blog/posts.ts +++ b/apps/website/src/data/blog/posts.ts @@ -1,3 +1,4 @@ +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_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' @@ -94,6 +95,7 @@ import { body as body_features_that_change_no_bits, ruBody as ruBody_features_th import { body as body_one_commit_nine_workflow_outcomes, ruBody as ruBody_one_commit_nine_workflow_outcomes } from './bodies/one-commit-nine-workflow-outcomes' const bodies: Record = { + '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 }, '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 }, From 38b445ca4a7c38e8a08dc3968eb2c2cd545a371e Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Thu, 8 Oct 2026 11:42:34 +0700 Subject: [PATCH 2/2] fix(blog): close the object seams the union merge left open in index.ts Two post-registry entries began their slug: line inside the previous entry's ru.openQuestions array: the main-first + head-after union kept both conflict fragments but dropped the array/object closers that sat between them. tsc -b reports 24 syntax errors and the typecheck ratchet refuses the file (0 -> 24). Each seam gets the four closing lines a healthy entry boundary already carries. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/website/src/data/blog/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index 1cdd0ed756..303b971564 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -28,6 +28,10 @@ export const postsIndex: PostMeta[] = [ "Раннер в браузере по-прежнему пропускает блоки test и invariant (trinity#1477, исправление открыто как PR 1495); уроки показывают записанный вывод t27c.", "Шесть из семи спек шин BLOCKED в нативном раннере по comptime-разрешению; их записи показывают check, gen-verilog и debug-hir, но не заявленный прогон тестов.", "Константы RGMII и CRC, взятые не из IEEE 802.3, помечены как допущения в шапках спек, а не измерения на плате.", + ], + }, + }, + { 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.", @@ -269,6 +273,10 @@ export const postsIndex: PostMeta[] = [ "15 новых уроков работают только в программе; ни один из них не запускался на плате.", "Записи используют tri test и tri mutate plant из t27#7400, который на момент написания не был влит, а t27c test-report выходит с кодом 0, когда тест падает (t27#7370), поэтому каждый вердикт читается из его текста.", "В сравнении оставлено 11 из 16 источников, чьи страницы читались 7 октября 2026 года; это не полный обзор. Курс, который уже подкладывает по одной ошибке на урок и показывает единственный упавший тест, сделал бы узкое утверждение поста неверным.", + ], + }, + }, + { 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.",