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
9 changes: 8 additions & 1 deletion .github/workflows/pr-blog-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ jobs:
--jq '.[] | {filename, status, previous_filename}' > /tmp/pr-files.jsonl
gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/commits?per_page=100" \
--jq '.[] | {sha, author: .author.login, committer: .committer.login, verified: .commit.verification.verified}' > /tmp/pr-commits.jsonl
- name: Load changed post bodies as text (apps/website/specs/policy/post_widget.t27)
run: |
set -euo pipefail
DIR=$(sed -n 's/^pub const POST_DIR : str = "\(.*\)";$/\1/p' apps/website/specs/policy/post_widget.t27)
SHA=$(jq -er '.head.sha' /tmp/pr.json); mkdir -p /tmp/pr-posts
jq -r --arg d "$DIR" 'select(.status != "removed" and (.filename | startswith($d) and endswith(".ts"))) | .filename' /tmp/pr-files.jsonl |
while read -r f; do gh api -H 'Accept: application/vnd.github.raw+json' "repos/$GITHUB_REPOSITORY/contents/$f?ref=$SHA" > "/tmp/pr-posts/${f##*/}"; done
- name: Validate mandatory report and generate blog draft
id: validate
continue-on-error: true
run: >-
python3 scripts/pr_blog_report.py validate --event /tmp/event.json --output /tmp/pr-blog
--files /tmp/pr-files.jsonl --commits /tmp/pr-commits.jsonl
--files /tmp/pr-files.jsonl --commits /tmp/pr-commits.jsonl --posts /tmp/pr-posts
- name: Bind the required status to the current PR head
if: always()
env:
Expand Down
143 changes: 143 additions & 0 deletions apps/website/specs/policy/post_widget.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: Apache-2.0
; specs/policy/post_widget.t27 -- every blog post carries a widget
; Source of truth for scripts/pr_blog_report.py (gHashTag/trinity, repository root), which
; reads the str constants below when it validates a PR's work report with --posts. A post
; body under POST_DIR that the PR adds, modifies or renames must hold at least one block
; whose kind is in WIDGET_KINDS, in its English body and, when it has one, in its Russian
; ruBody, or the "T27 work report" status turns red with RULE as the reason. The rule's
; words live here and nowhere else.
; ASCII only (L3), English only (LANG-EN).

; WHY: the owner's rule of 2026-10-09. A post shows the reader the real command running:
; a tri or t27c terminal cast (a recording under public/term/<id>/, replayed in the page by
; public/term/player.js) or a figure drawn from the post's own numbers. From 2026-10-07 to
; 2026-10-09 eleven posts in a row shipped as text only, because the rule was said once
; and written nowhere. A rule nobody wrote down is a rule nobody checks.

; WHAT COUNTS: a block is a widget when its kind is one of WIDGET_KINDS ("terminal" is a
; recorded session, "figure" an SVG with a caption; see src/data/blog/types.ts). The check
; reads the body file as text and never runs it. A body that exports body and ruBody needs a
; widget in each; a body with no ruBody needs one in body. A removed body is not checked.

; GRANDFATHERED: the posts dated before WIDGET_SINCE that carried no widget on the day this
; rule was written, by body file name without .ts. They may be edited without adding one.
; The list only shrinks: a post that gains a widget may leave it, and no post joins it.
; WIDGET_SINCE is the owner's cutoff: every post dated on or after it carries a widget from
; the change that adds this file on, so none of them is listed.

; LIMIT: the workflow never checks out the PR (pull_request_target), so the check cannot
; see whether a terminal block's recording exists under public/term/; the site build and
; its player do. A kind is matched by its text, kind: 'terminal' or "kind": "terminal".
; phi^2 + 1/phi^2 = 3 | TRINITY

module post_widget_policy;

pub const KIND : str = "policy";
pub const SCHEMA_VERSION : u8 = 1;
pub const RULE_ID : str = "widget-per-post";
pub const RULE : str = "A blog post carries at least one widget, a terminal cast or a figure, in its English body and in its Russian ruBody.";
pub const POST_DIR : str = "apps/website/src/data/blog/bodies/";
pub const WIDGET_KIND_COUNT : u8 = 2;
pub const WIDGET_KINDS : [2]str = ["terminal", "figure"];
pub const WIDGET_SINCE : str = "2026-09-27";
pub const GRANDFATHERED_COUNT : u8 = 72;
pub const GRANDFATHERED : [72]str = [
"a-broken-reference-looks-exactly-like-broken-code",
"a-clean-merge-is-not-a-semantic-no-op",
"a-correct-gate-with-a-manual-remedy",
"a-gate-that-rejected-its-own-users",
"a-health-snapshot-changed-its-denominator",
"a-linux-target-with-no-abi-is-musl",
"a-measurement-number-needs-a-passport",
"a-multiplicity-correction-changed-the-deployment-reading",
"a-partial-module-needs-its-own-verdict",
"a-red-gate-was-missing-its-input",
"a-repair-reaches-only-the-copy-it-lands-in",
"a-small-agent-needs-an-exact-judge",
"a-suite-that-runs-nothing-exits-zero",
"an-inert-filter-is-safest-until-it-works",
"an-invitation-in-the-language-it-describes",
"clara-proposal-submitted-not-reviewed",
"context-length-resonance-not-power-law",
"each-half-imported-the-other",
"eight-theorems-audited",
"eleven-verdicts-were-windows-not-checkpoints",
"energy-asymmetry-activations",
"equal-stored-width-removed-an-accuracy-lead",
"features-that-change-no-bits",
"fifteen-merged-nine-credited",
"five-reasons-the-build-was-red",
"formal-was-green-and-had-never-run-a-solver",
"four-hundred-and-twelve-tests-that-were-sentences",
"four-languages-one-tri-extension",
"fourteen-rows-agreed-one-did-not",
"frame-length-margin-law",
"green-ci-does-not-mean-usable",
"how-to-join-the-swarm",
"i-wrote-the-post-then-did-the-thing",
"merge-diff-ci-boundaries",
"ninety-tests-were-unreachable",
"nobodys-example",
"one-commit-nine-workflow-outcomes",
"one-more-reading-then-it-cleared",
"one-saturation-rule-five-artefacts",
"open-gigabit-ethernet-artix7",
"phi-identity-machine-checked",
"physical-width-changed-the-question",
"queen-browser-actions-you-can-follow",
"queen-foundation-snapshot-contract",
"queen-phone-orientation-gate",
"queen-review-lifecycle-queues",
"readout-that-cannot-be-misread",
"real-value-in-integer-container",
"scale-field-width-already-published",
"signal-health-self-run32",
"six-and-a-half-years-in-a-discarded-return-value",
"ternary-won-the-wire-not-the-gate",
"the-auditor-made-the-mistake-it-audits",
"the-control-that-could-not-fail",
"the-experiment-that-could-not-answer",
"the-fpga-row-was-corrected",
"the-full-adder-made-the-cost-claim-comparable",
"the-gate-was-right-and-nothing-stopped",
"the-generated-file-was-three-years-old",
"the-only-stable-speed-belonged-to-the-tool",
"the-ratchet-counted-a-total-as-an-error",
"the-required-check-was-an-echo",
"the-scanner-scored-what-it-could-not-see",
"the-search-space-erased-a-significance-claim",
"the-silence-was-a-saturated-readout",
"the-tail-that-had-never-run",
"thirty-epochs-exposed-a-failure-rate-blind-spot",
"tri-claw-an-agent-you-can-audit",
"tri-mined-not-sold",
"twenty-merged-in-three-days",
"twenty-three-reference-models",
"two-bitstreams-one-bit-apart"
];

test the_rule_is_named_like_the_course_rule {
assert RULE_ID == "widget-per-post";
assert KIND == "policy";
assert SCHEMA_VERSION == 1;
}

test a_widget_is_a_cast_or_a_figure {
assert WIDGET_KIND_COUNT == 2;
assert WIDGET_KINDS[0] == "terminal";
assert WIDGET_KINDS[1] == "figure";
}

test posts_live_where_the_course_rule_looks {
assert POST_DIR == "apps/website/src/data/blog/bodies/";
}

test the_cutoff_date_is_pinned {
assert WIDGET_SINCE == "2026-09-27";
}

test the_grandfathered_list_is_closed {
assert GRANDFATHERED_COUNT == 72;
assert GRANDFATHERED[0] == "a-broken-reference-looks-exactly-like-broken-code";
assert GRANDFATHERED[71] == "two-bitstreams-one-bit-apart";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export const body: Block[] = [
kind: 'p',
text: '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.',
},
{
kind: 'terminal',
src: 'term/tri-blog-list/session.cast',
share: 'https://t27.ai/term/tri-blog-list/',
title: 'tri blog list: every t27.ai post and its receipt state',
caption: 'tri blog list, recorded on 3 October: 73 published posts, newest first, each with its date, its language and its revision. Under this rule a course change and the post that explains it reach this list together. 2.4 s.',
},
{ kind: 'h', text: 'The rule lives in a t27 spec' },
{
kind: 'p',
Expand Down Expand Up @@ -57,6 +64,13 @@ export const ruBody: Block[] = [
kind: 'p',
text: 'У каждого PR в trinity уже есть отчёт о работе, а влитый PR потом становится черновиком поста в блоге. Но изменение курса всё ещё могло попасть в main совсем без поста, и читатель встречал новый урок раньше, чем что-нибудь его объясняло. trinity#1461 закрывает эту дыру: PR, который меняет курс, обязан нести свой пост — английское body и русское ruBody — в том же PR, иначе статус «T27 work report» краснеет. PR открыт и ещё не влит.',
},
{
kind: 'terminal',
src: 'term/tri-blog-list/session.cast',
share: 'https://t27.ai/term/tri-blog-list/',
title: 'tri blog list: every t27.ai post and its receipt state',
caption: 'tri blog list, записано 3 октября: 73 опубликованных поста, сначала новые, у каждого дата, язык и ревизия. По этому правилу изменение курса и пост, который его объясняет, попадают в этот список вместе. 2,4 с.',
},
{ kind: 'h', text: 'Правило живёт в спеке t27' },
{
kind: 'p',
Expand Down
28 changes: 28 additions & 0 deletions apps/website/src/data/blog/bodies/an-eye-on-the-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export const body: Block[] = [
kind: 'p',
text: 'The first frame read "ALINX" on the circuit board. Our hardware notes had called this board a QMTech Wukong for months, an XC7A200T chip in an FGG676 package. It is an ALINX AX7203: the same XC7A200T chip in an FBG484 package. Over JTAG the chip reports the same ID code, 0x3636093, in both packages, so no tool in the flow could tell them apart.',
},
{
kind: 'terminal',
src: 'term/tri-fpga-jtag/session.cast',
share: 'https://t27.ai/term/tri-fpga-jtag/',
title: 'tri fpga-jtag · which chip answered the cable',
caption: 'tri fpga-jtag names a die and a revision from an IDCODE, never a package: 0x03636093 decodes to an XC7A200T whether the chip sits in an FGG676 or an FBG484 package. Recorded on 3 October, five days before the camera\'s first frame; the self-test passes 17 of 17. 13.5 s.',
},
{
kind: 'p',
text: '[measured] A bitstream made earlier for the AX7203 settled it. Clocked from the AX7203\'s 200 MHz oscillator, it answered 512 of 512 additions bit-exact over the AX7203\'s serial port.',
Expand All @@ -28,6 +35,13 @@ export const body: Block[] = [
kind: 'p',
text: 'specs/fpga/eth_beacon.t27 builds one UDP broadcast frame, byte by byte: the preamble, the broadcast address, an IPv4 header with its checksum, a UDP header, a payload of "T27E" and a frame counter, and the Ethernet checksum (CRC-32). Its 9 tests pin the IPv4 checksum, the header bytes, the CRC of "123456789" and the last four bytes of frames 0 and 1. Those four bytes were first computed with zlib, before the spec was written.',
},
{
kind: 'terminal',
src: 'term/t27c-eth-crc/session.cast',
share: 'https://t27.ai/term/t27c-eth-crc/',
title: 't27c on eth_crc.t27 -- the FCS, native',
caption: 'Not the beacon spec itself but specs/fpga/eth_crc.t27, the Ethernet frame check sequence on its own: t27c test-report runs 8 of 8 tests natively, none vacuous, among them check_value_123456789 and receiver_residue_is_the_magic. Recorded on 8 October; 9.7 s.',
},
{
kind: 'p',
text: 'The Verilog around the spec is 40 lines of Xilinx primitives and nothing else: an input buffer for the 200 MHz clock, a PLL that makes 125 MHz plus a copy shifted by 90 degrees, and output cells that put each byte on the four RGMII wires as two 4-bit halves. The shifted copy is the transmit clock. It lands in the middle of each data bit, so the network chip needs no extra setup.',
Expand Down Expand Up @@ -76,6 +90,13 @@ export const ruBody: Block[] = [
kind: 'p',
text: 'На первом кадре на плате читалось «ALINX». Наши записи о железе несколько месяцев называли эту плату QMTech Wukong, то есть кристалл XC7A200T в корпусе FGG676. На деле это ALINX AX7203: тот же кристалл XC7A200T, но в корпусе FBG484. По JTAG кристалл сообщает один и тот же код, 0x3636093, в обоих корпусах, поэтому ни один инструмент в цепочке не мог их различить.',
},
{
kind: 'terminal',
src: 'term/tri-fpga-jtag/session.cast',
share: 'https://t27.ai/term/tri-fpga-jtag/',
title: 'tri fpga-jtag · which chip answered the cable',
caption: 'tri fpga-jtag называет по IDCODE кристалл и ревизию, но не корпус: 0x03636093 расшифровывается как XC7A200T, в корпусе FGG676 или FBG484 — всё равно. Записано 3 октября, за пять дней до первого кадра камеры; самопроверка проходит 17 из 17. 13,5 с.',
},
{
kind: 'p',
text: '[измерено] Вопрос закрыл битстрим, собранный раньше для AX7203. От генератора 200 МГц этой платы он ответил по её последовательному порту 512 из 512 сложений, совпавших до бита.',
Expand All @@ -89,6 +110,13 @@ export const ruBody: Block[] = [
kind: 'p',
text: 'specs/fpga/eth_beacon.t27 собирает один широковещательный UDP-кадр байт за байтом: преамбулу, широковещательный адрес, заголовок IPv4 с контрольной суммой, заголовок UDP, полезную нагрузку «T27E» со счётчиком кадров и контрольную сумму Ethernet (CRC-32). Его 9 тестов закрепляют контрольную сумму IPv4, байты заголовка, CRC строки «123456789» и последние четыре байта кадров 0 и 1. Эти четыре байта сначала посчитали через zlib, ещё до того как спецификация была написана.',
},
{
kind: 'terminal',
src: 'term/t27c-eth-crc/session.cast',
share: 'https://t27.ai/term/t27c-eth-crc/',
title: 't27c on eth_crc.t27 -- the FCS, native',
caption: 'Это не сама спецификация маячка, а specs/fpga/eth_crc.t27 — контрольная сумма кадра Ethernet отдельно: t27c test-report нативно прогоняет 8 из 8 тестов, ни один не пустой, среди них check_value_123456789 и receiver_residue_is_the_magic. Записано 8 октября; 9,7 с.',
},
{
kind: 'p',
text: 'Вокруг спецификации — 40 строк Verilog, только примитивы Xilinx: входной буфер для тактовой частоты 200 МГц, PLL, который делает 125 МГц и копию со сдвигом на 90 градусов, и выходные ячейки, которые выдают каждый байт на четыре провода RGMII двумя половинами по 4 бита. Сдвинутая копия служит тактом передачи. Её фронт попадает в середину каждого бита данных, поэтому сетевой микросхеме не нужна дополнительная настройка.',
Expand Down
Loading
Loading