T27.AI

Blog

A course PR now carries its blog post

2026-10-07 · 4 min read

[open PR, not merged; binds only PRs opened or pushed after the merge; checks that a post file changed, not what it says] The T27 work report check now refuses a trinity PR that changes a course without adding or modifying a blog post body in the same PR. The rule's words live only in a t27 spec, specs/policy/course_post.t27, which the checker reads and fails closed on. Renamed lessons count by old and new path, renamed posts do not count, and an incomplete file list is reported as not decided. Seven planted mutants each failed their own test.

Three-panel engraved illustration for: A course PR now carries its blog post
View the complete triptych at full size
#CI#Courses#Blog#MutationTesting

Every trinity PR already carries a work report, and a merged PR later becomes a blog draft. A course change could still land with no post at all, so a reader could meet a new lesson before anything explained it. trinity#1461 closes that gap: a PR that changes a course now has to carry its blog post, English body and Russian ruBody, in the same PR, or the "T27 work report" status turns red. The PR is open and not merged yet.

The rule lives in a t27 spec

The words of the rule are in one file, apps/website/specs/policy/course_post.t27. It holds the rule sentence, the course directory (apps/website/specs/course/), the post directory (apps/website/src/data/blog/bodies/) and the two file statuses that count as a written post, "added" and "modified". Three test blocks pin those values. The spec sits under specs/policy/, not under specs/course/, so editing the rule is not itself a course change.

scripts/pr_blog_report.py does not restate the rule. It reads the spec's str constants and its one string list, and it fails closed: a missing constant, a directory without its trailing slash, or a POST_STATUSES list whose length differs from its declared length stops the check. The script checks that length itself because t27 does not check a declared array length yet (gHashTag/t27#7395). The refusal message is the spec's RULE sentence followed by the file that triggered it.

What counts as a change and as a post

Seven planted bugs, seven red tests

The PR adds 10 tests for the rule; the pipeline suite now runs 71 tests, and a rerun on 7 October gave OK. To check that the tests can see a bug, seven mutants were planted in pr_blog_report.py, one at a time, each restored with git afterwards. Each one failed the test named for it.

  1. The file status is ignored, so a removed post would count.
  2. The refusal is never raised.
  3. The declared length of POST_STATUSES is not checked.
  4. The incomplete-list check is inverted.
  5. The refusal names the spec the old way, by a path cut with parents[3], which dropped the leading apps/.
  6. previous_filename is ignored, so a lesson renamed out of the course is missed.
  7. The "not decided" notice for an incomplete list with a course file is turned back into a refusal.

What it does not check

It checks that a post body file changed, not that the post describes the course change; review and the blog checks still judge the content. The workflow runs the script from main (pull_request_target), so the rule binds only PRs opened or pushed after the merge. A manual re-run of the report for an older course PR that shipped without a post will now turn red.

What this does not settle

Receipts

Work with me

Want this kind of check on your own design?

I audit RTL and build independent, bit-exact models, then take the result through synthesis and, when useful, onto an Artix-7 board. The first conformance module is free.