Skip to content
Merged
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
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": "Проверка гоняет те тесты, что уже есть в модулях. Покрывают ли они каждый шаг рецепта, она не судит." }
]
23 changes: 23 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
2 changes: 2 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,7 @@ 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'
Expand Down Expand Up @@ -111,6 +112,7 @@ 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 },
Expand Down
Loading