$ tri devkit flow --build --out /tmp/devkit-flow/rec1
+$ tri devkit impact --out /tmp/devkit-flow/rec1 --builds 20
+
Staged: the prompt and the typing. Real: every byte the commands printed, at the time they printed it. Any silence longer than 2 s is shown for 2 s, and the title bar says so while it happens. Edited: home directory shown as ~.
$ tri x7-board compare --ref
+$ tri x7-board load x7-board-gif3-load --owner-yes 'check it on the board!! this matters!'
+$ tri x7-board receipts x7-board-gif3 --quick
+
Staged: the prompt and the typing. Real: every byte the commands printed, at the time they printed it. Any silence longer than 2 s is shown for 2 s, and the title bar says so while it happens. Edited: home directory shown as ~.
+
+ )
+}
diff --git a/apps/website/src/components/FaqBlock.css b/apps/website/src/components/FaqBlock.css
index 6e86570c76..21166c1212 100644
--- a/apps/website/src/components/FaqBlock.css
+++ b/apps/website/src/components/FaqBlock.css
@@ -4,7 +4,7 @@
its definition -- which also gives a screen reader the pairing for free. */
.faq-block {
border-top: 1px solid rgba(255, 255, 255, 0.1);
- background: #010403;
+ background: #000;
color: var(--text, #FFFFFF);
}
diff --git a/apps/website/src/components/GameHero.css b/apps/website/src/components/GameHero.css
index b3360007d4..762cbda7ea 100644
--- a/apps/website/src/components/GameHero.css
+++ b/apps/website/src/components/GameHero.css
@@ -1,7 +1,7 @@
/* The mark, the claim, and three ways in. One column, centred, sized from the
viewport — there is nothing here that needs a breakpoint to decide. */
.game-hero {
- background: #020605;
+ background: #000;
color: var(--text, #FFFFFF);
}
@@ -85,8 +85,8 @@
.game-hero-primary {
border: 1px solid #00ff88;
- background: #00ff88;
- color: #001b0e;
+ background: #000;
+ color: #00ff88;
font-weight: 600;
}
diff --git a/apps/website/src/components/LanguageSwitcher.tsx b/apps/website/src/components/LanguageSwitcher.tsx
index 4b43f5ca8b..6f796b5cba 100644
--- a/apps/website/src/components/LanguageSwitcher.tsx
+++ b/apps/website/src/components/LanguageSwitcher.tsx
@@ -4,29 +4,21 @@ import { useI18n } from '../i18n/context'
const flags: Record = {
en: '🇺🇸',
- ru: '🇷🇺',
- de: '🇩🇪',
- zh: '🇨🇳',
- es: '🇪🇸'
+ ru: '🇷🇺'
}
const labels: Record = {
en: 'EN',
- ru: 'RU',
- de: 'DE',
- zh: '中文',
- es: 'ES'
+ ru: 'RU'
}
const langNames: Record = {
en: 'English',
- ru: 'Russian',
- de: 'German',
- zh: 'Chinese',
- es: 'Spanish'
+ ru: 'Russian'
}
-const LANGS = ['en', 'ru', 'de', 'zh', 'es']
+// The site is written in English and Russian only.
+const LANGS = ['en', 'ru']
export default memo(function LanguageSwitcher() {
const { lang, setLang } = useI18n()
diff --git a/apps/website/src/components/ModuleHeroBlock.css b/apps/website/src/components/ModuleHeroBlock.css
index 39e118994a..375161dab2 100644
--- a/apps/website/src/components/ModuleHeroBlock.css
+++ b/apps/website/src/components/ModuleHeroBlock.css
@@ -4,7 +4,7 @@
gold on this site means a spec. */
.module-hero {
border-bottom: 1px solid rgba(0, 255, 136, 0.2);
- background: #030605;
+ background: #000;
color: var(--text, #FFFFFF);
}
@@ -90,8 +90,8 @@
}
.module-hero-primary {
- background: #00ff88;
- color: #001b0e;
+ background: #000;
+ color: #00ff88;
border: 1px solid #00ff88;
}
diff --git a/apps/website/src/components/ModulesBlock.css b/apps/website/src/components/ModulesBlock.css
index 786daa89e0..92b6f1d5e5 100644
--- a/apps/website/src/components/ModulesBlock.css
+++ b/apps/website/src/components/ModulesBlock.css
@@ -4,7 +4,7 @@
a desktop, without a media query naming any of them. */
.modules-block {
border-top: 1px solid rgba(255, 255, 255, 0.1);
- background: #020605;
+ background: #000;
color: var(--text, #FFFFFF);
}
@@ -171,7 +171,7 @@
}
.modules-block-all:hover {
- background: rgba(0, 255, 136, 0.08);
+ border-color: #7dffc0;
}
/* Under a finger every card is a target of its own, and the 44px floor is the
diff --git a/apps/website/src/components/ModulesBlock.tsx b/apps/website/src/components/ModulesBlock.tsx
index 99b4cd07b1..695cfbcce6 100644
--- a/apps/website/src/components/ModulesBlock.tsx
+++ b/apps/website/src/components/ModulesBlock.tsx
@@ -3,18 +3,16 @@ import { MODULES } from '../lib/queenModules'
import { BOARD_VIEWS, PROJECT_VIEWS, RAIL_VIEWS, SPEC_LAYERS, isBoardView, isProjectView, isSpecLayer } from './queenHud'
import './ModulesBlock.css'
-// A compact index of the modules: one card each, linking to its tab. Not on the
-// homepage any more — every module has a block of its own there — but kept
-// because the data it reads now lives in lib/queenModules and this is the one
-// place that shows all of them at a glance.
+// A compact index of the modules: one card each, linking to its tab. It closes
+// the homepage, under the module blocks, because it is the one place that shows
+// all of them at a glance; the data it reads lives in lib/queenModules.
//
-// It is drawn in the shell's own shape, which is no longer a flat list of
-// fourteen. Seven are buttons on the rail. Five — SKILLS, CRONS, AGENTS, TOOLS,
+// It is drawn in the shell's own shape, which is no longer a flat list. Seven are buttons on the rail. Five — SKILLS, CRONS, AGENTS, TOOLS,
// FUNCTIONS — are rungs of the ladder inside SPECS, because each is a catalogue
-// generated from .t27 specs and each names the one below it. Two — MISSION MAP
-// and FACTORY — sit under KANBAN, because the three are three readings of the
-// one board. PASSPORT sits inside PROJECT (2026-09-21), the record the project
-// keeps beside itself. A reader who scrolled past fourteen equal cards and then
+// generated from .t27 specs and each names the one below it. MISSION MAP,
+// FACTORY and TECH TREE sit under KANBAN, as further readings of the one board
+// (BOARD_VIEWS; the tech tree joined it after this block last named the group). PASSPORT sits inside PROJECT (2026-09-21), the record the project
+// keeps beside itself. A reader who scrolled past a row of equal cards and then
// opened the rail was reading a map of a shell that no longer existed.
//
// Which group a module is in is asked of the shell's own lists rather than
@@ -53,14 +51,14 @@ const COPY = {
en: {
eyebrow: `QUEEN / ${N} MODULES`,
title: `One screen, ${N} ways to read it`,
- lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; ${BOARD_N} are the board's other two readings, under KANBAN; and ${PROJECT_N} is the record kept beside the project, inside PROJECT.`,
+ lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; ${BOARD_N} are further readings of the board, under KANBAN; and ${PROJECT_N} is the record kept beside the project, inside PROJECT.`,
railHead: `On the rail · ${RAIL_N}`,
ladderHead: `Inside SPECS, on the ladder · ${LADDER_N}`,
ladderNote:
'Specs, skills, crons, agents, tools and functions are six layers of one thing: each is generated from .t27 and each names the one below it. They open on the same keys as before, one step inside SPECS.',
boardHead: `Inside KANBAN, on the board · ${BOARD_N}`,
boardNote:
- 'The kanban, the mission map and the factory read one board: the same issues as columns, as a map, and as what the swarm is building from them. Same keys, one step inside KANBAN.',
+ 'The kanban, the mission map, the factory and the tech tree read one board: the same issues as columns, as a map, as what the swarm is building from them, and as the path the .t27 language took to get here. Same keys, one step inside KANBAN.',
projectHead: `Inside PROJECT, beside it · ${PROJECT_N}`,
projectNote:
'The passport is part of how the project describes itself, not an instrument of its own: the record every result has to carry. Same key, one step inside PROJECT.',
@@ -70,14 +68,14 @@ const COPY = {
ru: {
eyebrow: `КОРОЛЕВА / ${N} МОДУЛЕЙ`,
title: `Один экран и ${N} способов его прочитать`,
- lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, ${BOARD_N} — два других прочтения доски, внутри КАНБАНА, и ${PROJECT_N} — запись, которую проект держит рядом с собой, внутри ПРОЕКТА.`,
+ lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, ${BOARD_N} — ещё прочтения доски, внутри КАНБАНА, и ${PROJECT_N} — запись, которую проект держит рядом с собой, внутри ПРОЕКТА.`,
railHead: `На панели · ${RAIL_N}`,
ladderHead: `Внутри СПЕК, на лестнице · ${LADDER_N}`,
ladderNote:
'Спеки, скиллы, кроны, агенты, инструменты и функции — шесть слоёв одного: каждый порождается из .t27 и каждый называет следующий. Клавиши прежние, просто на шаг внутрь СПЕК.',
boardHead: `Внутри КАНБАНА, на доске · ${BOARD_N}`,
boardNote:
- 'Канбан, карта миссий и фабрика читают одну доску: те же задачи — колонками, картой и тем, что рой из них строит. Клавиши прежние, просто на шаг внутрь КАНБАНА.',
+ 'Канбан, карта миссий, фабрика и тех-дерево читают одну доску: те же задачи — колонками, картой, тем, что рой из них строит, и путём, которым язык .t27 сюда пришёл. Клавиши прежние, просто на шаг внутрь КАНБАНА.',
projectHead: `Внутри ПРОЕКТА, рядом с ним · ${PROJECT_N}`,
projectNote:
'Паспорт — часть того, как проект описывает себя, а не отдельный инструмент: запись, которую должен нести каждый результат. Клавиша прежняя, просто на шаг внутрь ПРОЕКТА.',
@@ -92,7 +90,7 @@ export default function ModulesBlock() {
const t = COPY[key]
// A list of modules, not MODULES itself: `as const` makes that a tuple of
- // fourteen, and a filtered group is never that tuple.
+ // every module, and a filtered group is never that tuple.
const cards = (modules: readonly (typeof MODULES)[number][]) => (
{modules.map((module) => {
diff --git a/apps/website/src/components/Navigation.tsx b/apps/website/src/components/Navigation.tsx
index f7473627d6..e3d18babc1 100644
--- a/apps/website/src/components/Navigation.tsx
+++ b/apps/website/src/components/Navigation.tsx
@@ -16,7 +16,7 @@ const DOCS_URL = '#/docs'
// The locale files have no keys for the commercial pages yet, so the labels
// live next to the links. Missing locales fall back to English.
const PAGES_LABEL: Record = {
- ru: 'Страницы', de: 'Seiten', es: 'Páginas', zh: '页面',
+ ru: 'Страницы',
}
type PageLink = { href: string; en: string; ru: string; note: string; noteRu: string; external?: boolean; color?: string }
diff --git a/apps/website/src/components/PlayBlock.css b/apps/website/src/components/PlayBlock.css
index 7e5ccd044d..3530c7396a 100644
--- a/apps/website/src/components/PlayBlock.css
+++ b/apps/website/src/components/PlayBlock.css
@@ -4,7 +4,7 @@
styles/card.css, shared with every other block on the page. */
.play-block {
border-top: 1px solid rgba(255, 255, 255, 0.1);
- background: #020605;
+ background: #000;
color: var(--text, #FFFFFF);
}
diff --git a/apps/website/src/components/QueenHeroBlock.css b/apps/website/src/components/QueenHeroBlock.css
index 541c52857f..b3bbafc353 100644
--- a/apps/website/src/components/QueenHeroBlock.css
+++ b/apps/website/src/components/QueenHeroBlock.css
@@ -2,7 +2,7 @@
idea, and a divider there made two slabs out of one pair. */
.queen-hero-block {
border-top: 1px solid rgba(0, 255, 136, .24);
- background: #020605;
+ background: #000;
color: var(--text, #FFFFFF);
}
@@ -48,7 +48,7 @@
font: 600 .82rem/1.2 "JetBrains Mono", monospace;
text-decoration: none;
}
-.queen-hero-block-primary { background: #00ff88; color: #001b0e; border: 1px solid #00ff88; }
+.queen-hero-block-primary { background: #000; color: #00ff88; border: 1px solid #00ff88; }
.queen-hero-block-secondary { color: #ffd45a; border: 1px solid #ffd45a88; background: transparent; }
.queen-hero-block-primary:hover, .queen-hero-block-secondary:hover { filter: brightness(1.12); }
diff --git a/apps/website/src/components/SpecHeroBlock.css b/apps/website/src/components/SpecHeroBlock.css
index 70d8308276..81131f62e8 100644
--- a/apps/website/src/components/SpecHeroBlock.css
+++ b/apps/website/src/components/SpecHeroBlock.css
@@ -3,7 +3,7 @@
this one answers at a lower pitch instead of repeating the hero's shape. */
.spec-hero-block {
border-bottom: 1px solid rgba(255, 212, 90, .24);
- background: #050403;
+ background: #000;
color: var(--text, #FFFFFF);
}
@@ -55,7 +55,7 @@
font: 600 .82rem/1.2 "JetBrains Mono", monospace;
text-decoration: none;
}
-.spec-hero-block-primary { background: #ffd45a; color: #1b1400; border: 1px solid #ffd45a; }
+.spec-hero-block-primary { background: #000; color: #ffd45a; border: 1px solid #ffd45a; }
.spec-hero-block-secondary { color: #ffd45a; border: 1px solid #ffd45a88; background: transparent; }
.spec-hero-block-primary:hover, .spec-hero-block-secondary:hover { filter: brightness(1.12); }
diff --git a/apps/website/src/components/TerminalCast.tsx b/apps/website/src/components/TerminalCast.tsx
new file mode 100644
index 0000000000..56e9d8fa44
--- /dev/null
+++ b/apps/website/src/components/TerminalCast.tsx
@@ -0,0 +1,48 @@
+import { useEffect, useRef } from 'react'
+import { useI18n } from '../i18n/context'
+
+// A recorded terminal session in a post. The player itself is public/term/player.js,
+// the one the t27.ai/term// share pages use, loaded at run time so the VT code
+// exists once on the web. Recordings are made and published with `tri cast`.
+
+type Player = { destroy: () => void }
+type PlayerModule = { mount: (el: HTMLElement, o: { src: string; title?: string; share?: string }) => Player }
+
+export default function TerminalCast({ src, title, caption, share }: { src: string; title?: string; caption: string; share?: string }) {
+ const box = useRef(null)
+ const { lang } = useI18n()
+
+ useEffect(() => {
+ let player: Player | null = null
+ let live = true
+ const url = new URL('term/player.js', document.baseURI).href
+ import(/* @vite-ignore */ url)
+ .then((m: PlayerModule) => {
+ if (live && box.current) player = m.mount(box.current, { src: new URL(src, document.baseURI).href, title, share })
+ })
+ .catch(() => {
+ if (box.current) box.current.textContent = 'The terminal player could not load.'
+ })
+ return () => {
+ live = false
+ player?.destroy()
+ }
+ }, [src, title, share])
+
+ return (
+
+
+
+ {caption}
+ {share && (
+ <>
+ {' '}
+
+ {lang === 'ru' ? 'Открыть запись на отдельной странице.' : 'Open the recording on its own page.'}
+
+ >
+ )}
+
+
+ )
+}
diff --git a/apps/website/src/data/agents.ts b/apps/website/src/data/agents.ts
new file mode 100644
index 0000000000..d1707ef2dd
--- /dev/null
+++ b/apps/website/src/data/agents.ts
@@ -0,0 +1,53 @@
+// The agents the bot sells. Live source: GET /api/agents/packages on the bot
+// (gHashTag/999-multibots-telegraf, generated from AGENT_PACKAGES); the list
+// below is only the fallback the showcase renders while that call is pending
+// or failing. The price is not here — every agent costs AGENT_PRICE
+// (data/club.ts); agents differ by profile.
+export type AgentProfile = {
+ id: string
+ icon: string
+ name: { en: string; ru: string }
+ promise: { en: string; ru: string }
+}
+
+export const AGENTS_ENDPOINT =
+ 'https://999-multibots-telegraf-production-2008.up.railway.app/api/agents/packages'
+
+export const AGENT_PROFILES: readonly AgentProfile[] = [
+ {
+ id: 'manager',
+ icon: '◎',
+ name: { en: 'Manager Agent', ru: 'Менеджер агент' },
+ promise: {
+ en: 'Your own Telegram bot that answers clients around the clock in your voice and brings the leads to your CRM.',
+ ru: 'Собственный Telegram-бот, который круглосуточно отвечает клиентам в вашем тоне и приносит заявки в CRM.',
+ },
+ },
+ {
+ id: 'blogger',
+ icon: '✦',
+ name: { en: 'Blogger Agent', ru: 'Блогер агент' },
+ promise: {
+ en: 'Reels with your face and voice, a content plan and publishing — the blogger agent working for your channel.',
+ ru: 'Рилсы с вашим лицом и голосом, контент-план и публикации — агент-блогер работает на ваш канал.',
+ },
+ },
+ {
+ id: 'twin',
+ icon: '◈',
+ name: { en: 'Developer Agent', ru: 'Разработчик агент' },
+ promise: {
+ en: 'An agent the team develops for your task: your character, voice, face, skills and tools in a bot of its own.',
+ ru: 'Агент, которого команда разрабатывает под вашу задачу: ваш характер, голос, лицо, навыки и инструменты в отдельном боте.',
+ },
+ },
+ {
+ id: 'tridev',
+ icon: '▽',
+ name: { en: 'TRI DEV', ru: 'TRI DEV' },
+ promise: {
+ en: 'Give it a GitHub issue: it works the tri pipeline remotely and sends back a pull request and a t27.ai/term recording of every command it ran.',
+ ru: 'Дайте задачу на GitHub: агент решает её удалённо по конвейеру tri и присылает pull request и запись терминала t27.ai/term со всеми командами.',
+ },
+ },
+]
diff --git a/apps/website/src/data/blog/bodies/golden-ratio-weights-ran-on-the-board.ts b/apps/website/src/data/blog/bodies/golden-ratio-weights-ran-on-the-board.ts
index 4d9763894c..cf6c591126 100644
--- a/apps/website/src/data/blog/bodies/golden-ratio-weights-ran-on-the-board.ts
+++ b/apps/website/src/data/blog/bodies/golden-ratio-weights-ran-on-the-board.ts
@@ -1,4 +1,5 @@
import type { Block } from '../types'
+import { X7_BOARD } from '../../casts'
export const body: Block[] = [
{
@@ -59,6 +60,13 @@ export const body: Block[] = [
kind: 'p',
text: "The node's own Verilog, unchanged, also ran a request stream of the same kind in simulation, with synthetic weights for one matrix: 7,680 of 7,680 receipts and 128 of 128 rows.",
},
+ {
+ kind: 'terminal',
+ src: X7_BOARD.src,
+ share: X7_BOARD.share,
+ title: X7_BOARD.title,
+ caption: "A later recording of the same receipt harness on the same board, with int8 activations rather than this post's Z[phi] ones, so its counts are not this run." + ' ' + X7_BOARD.caption.en,
+ },
{
kind: 'h',
text: 'What this is not',
@@ -146,6 +154,13 @@ export const ruBody: Block[] = [
kind: 'p',
text: 'Собственный Verilog узла без изменений тоже прогнал в симуляции поток запросов того же вида, с синтетическими весами для одной матрицы: 7 680 из 7 680 квитанций и 128 из 128 строк.',
},
+ {
+ kind: 'terminal',
+ src: X7_BOARD.src,
+ share: X7_BOARD.share,
+ title: X7_BOARD.title,
+ caption: 'Более поздняя запись того же харнесса квитанций на той же плате, с активациями int8, а не Z[phi], как в этом посте, так что её счёт — не этот прогон.' + ' ' + X7_BOARD.caption.ru,
+ },
{
kind: 'h',
text: 'Чем это не является',
diff --git a/apps/website/src/data/blog/bodies/half-the-build-is-bitstream-generation.ts b/apps/website/src/data/blog/bodies/half-the-build-is-bitstream-generation.ts
index 397e1658e8..b5750e43ed 100644
--- a/apps/website/src/data/blog/bodies/half-the-build-is-bitstream-generation.ts
+++ b/apps/website/src/data/blog/bodies/half-the-build-is-bitstream-generation.ts
@@ -1,4 +1,5 @@
import type { Block } from '../types'
+import { DEVKIT_FLOW } from '../../casts'
export const body: Block[] = [
{
@@ -119,6 +120,17 @@ export const body: Block[] = [
"kind": "p",
"text": "The floor stops dominating once the design is big enough. At gf128_mul the bitstream stage is 122.3 s of 533.2 s — 23% — while place-and-route takes 253.7 s and synthesis 163.1 s. Both of those grow with the netlist; the bitstream stage grows much more slowly."
},
+ {
+ kind: 'p',
+ text: 'A later run timed the same stage on a larger design: 33.9 s of a 116.9 s build for trinet_node_v2_ax7203, 121,587 lines of FASM, on a laptop rather than a CI runner. It also ran bitwalk, the FASM-to-frames and frames-to-.bit steps rebuilt from t27 specs, on the same FASM: 0.42 s and 0.25 s, with the same bytes. A different design and a different machine, so it is not a row in the table above.',
+ },
+ {
+ kind: 'terminal',
+ src: DEVKIT_FLOW.src,
+ share: DEVKIT_FLOW.share,
+ title: DEVKIT_FLOW.title,
+ caption: 'A later recording, not one of the runs above.' + ' ' + DEVKIT_FLOW.caption.en,
+ },
{
"kind": "h",
"text": "A single build time is not a measurement"
@@ -305,6 +317,17 @@ export const ruBody: Block[] = [
"kind": "p",
"text": "Пол перестаёт доминировать, когда дизайн достаточно велик. У gf128_mul битстрим — 122.3 с из 533.2 с, то есть 23%, тогда как трассировка занимает 253.7 с, а синтез 163.1 с. Эти две растут вместе с нетлистом, битстрим-стадия — гораздо медленнее."
},
+ {
+ kind: 'p',
+ text: 'Более поздний прогон засёк тот же этап на дизайне побольше: 33.9 с из 116.9 с сборки trinet_node_v2_ax7203, 121 587 строк FASM, на ноутбуке, а не на CI-раннере. Там же на том же FASM запущен bitwalk — шаги FASM → фреймы и фреймы → .bit, пересобранные из t27-спеков: 0.42 с и 0.25 с, байты те же. Другой дизайн и другая машина, поэтому это не строка таблицы выше.',
+ },
+ {
+ kind: 'terminal',
+ src: DEVKIT_FLOW.src,
+ share: DEVKIT_FLOW.share,
+ title: DEVKIT_FLOW.title,
+ caption: 'Более поздняя запись, не один из прогонов выше.' + ' ' + DEVKIT_FLOW.caption.ru,
+ },
{
"kind": "h",
"text": "Одно время сборки — это не измерение"
diff --git a/apps/website/src/data/blog/bodies/the-back-half-of-openxc7-from-a-spec.ts b/apps/website/src/data/blog/bodies/the-back-half-of-openxc7-from-a-spec.ts
new file mode 100644
index 0000000000..47f80a1494
--- /dev/null
+++ b/apps/website/src/data/blog/bodies/the-back-half-of-openxc7-from-a-spec.ts
@@ -0,0 +1,359 @@
+import type { Block } from '../types'
+
+// Both figures are drawn from the numbers below, so a number lives in one place.
+// Seconds. fasm2frames: timed when each corpus file was built (x7 manifest-v2).
+// bitwalk: best of 3 on the same machine on 2026-10-03, load average about 120.
+const RUNS = [
+ { name: ['One real design', 'xc7a100t, nextpnr, 1,785 lines'], ru: ['Один настоящий дизайн', 'xc7a100t, nextpnr, 1 785 строк'], py: 1.58, ours: 0.2 },
+ { name: ['Synthetic test file', 'xc7a100t, 1,049 lines'], ru: ['Синтетический тест', 'xc7a100t, 1 049 строк'], py: 19.33, ours: 0.14 },
+ { name: ['Synthetic test file', 'kintex7 xc7k325t, 1,123 lines'], ru: ['Синтетический тест', 'kintex7 xc7k325t, 1 123 строки'], py: 71.83, ours: 0.34 },
+ { name: ['All 22 corpus files', 'one after another'], ru: ['Все 22 файла корпуса', 'подряд'], py: 150.7, ours: 3.33 },
+]
+
+const GOLD = '#d4af37'
+
+type Lang = 'en' | 'ru'
+
+const SPEED_TEXT = {
+ en: { py: 'fasm2frames (prjxray, Python)', ours: 'bitwalk --fasm (t27 specs)', axis: 'seconds, log scale', label: 'Time to turn FASM into frames, fasm2frames against bitwalk' },
+ ru: { py: 'fasm2frames (prjxray, Python)', ours: 'bitwalk --fasm (t27-спеки)', axis: 'секунды, лог. шкала', label: 'Время перевода FASM в кадры: fasm2frames против bitwalk' },
+}
+
+const CHAIN_TEXT = {
+ en: { a: 'openXC7 today', b: 'this branch (gHashTag/t27#5609)', same: 'byte-identical', label: 'The back half of the openXC7 flow: FASM to frames to bitstream, prjxray tools against bitwalk built from t27 specs', note: 'yosys + nextpnr produce the FASM; openFPGALoader loads the .bit' },
+ ru: { a: 'openXC7 сегодня', b: 'эта ветка (gHashTag/t27#5609)', same: 'байт в байт', label: 'Задняя половина flow openXC7: FASM в кадры и в битстрим, инструменты prjxray против bitwalk из t27-спеков', note: 'FASM дают yosys + nextpnr; .bit загружает openFPGALoader' },
+}
+
+function sec(s: number, lang: Lang = 'en'): string {
+ const t = s < 1 ? s.toFixed(2) : s.toFixed(1)
+ return lang === 'ru' ? `${t.replace('.', ',')} с` : `${t} s`
+}
+
+function speedSvg(lang: Lang = 'en'): string {
+ const x0 = 230
+ const w = 420
+ const lo = -1 // 0.1 s
+ const hi = Math.log10(200)
+ const px = (s: number) => x0 + ((Math.log10(s) - lo) / (hi - lo)) * w
+ const ticks = [0.1, 1, 10, 100]
+ const top = 34
+ const step = 62
+ const h = top + RUNS.length * step + 40
+ const t = SPEED_TEXT[lang]
+ let s = `'
+}
+
+function box(x: number, y: number, w: number, lines: string[], gold: boolean): string {
+ const h = 46
+ let s = ``
+ const y0 = lines.length === 1 ? y + 28 : y + 20
+ lines.forEach((l, i) => {
+ s += `${l}`
+ })
+ return s
+}
+
+function arrow(x1: number, x2: number, y: number): string {
+ return ``
+}
+
+function chainSvg(lang: Lang = 'en'): string {
+ const t = CHAIN_TEXT[lang]
+ const cols = [
+ { x: 10, w: 70 },
+ { x: 110, w: 210 },
+ { x: 350, w: 70 },
+ { x: 450, w: 190 },
+ { x: 660, w: 50 },
+ ]
+ const rowA = [['FASM'], ['fasm2frames', 'prjxray · Python'], [lang === 'ru' ? 'кадры' : '.frames'], ['xc7frames2bit', 'prjxray · C++'], ['.bit']]
+ const rowB = [['FASM'], ['bitwalk --fasm', 'frames.t27'], [lang === 'ru' ? 'кадры' : '.frames'], ['bitwalk --write', 'packets.t27 · far.t27'], ['.bit']]
+ const ya = 30
+ const yb = 150
+ let s = `'
+}
+
+const speedRows = (lang: Lang = 'en') =>
+ RUNS.map((r) => [(lang === 'ru' ? r.ru : r.name).join(', '), sec(r.py, lang), sec(r.ours, lang), `${Math.round(r.py / r.ours)}×`])
+
+export const body: Block[] = [
+ {
+ kind: 'p',
+ text: 'openXC7 builds a Xilinx 7-series bitstream in two halves. The first half is yosys and nextpnr: Verilog in, FASM out. FASM is a text list of features such as `CLBLM_R_X3Y0.SLICEM_X0.ALUT.INIT[63:0] = 64\'h...`. The second half turns that list into a bitstream. prjxray\'s `fasm2frames` (Python) looks up every feature\'s bits and writes configuration frames, and `xc7frames2bit` (C++) wraps the frames in configuration packets. We rewrote the second half so that every bit position, frame address and packet word comes from a t27 spec, and compared it byte for byte with the two tools it replaces.',
+ },
+ {
+ kind: 'figure',
+ svg: chainSvg(),
+ caption: 'The second half of the flow. Each gold box is a driver generated from t27 specs. The dashed lines are the corpus checks against the prjxray tool above it.',
+ },
+ { kind: 'h', text: 'What the specs hold' },
+ {
+ kind: 'ul',
+ items: [
+ '`packets.t27`: the configuration packet stream. Headers, registers, commands, the CRC and the write sequence `xc7frames2bit` uses.',
+ '`far.t27`: the frame address walk for xc7a35t, xc7a100t and xc7a200t, with the part picked by IDCODE.',
+ '`frames.t27`: the frame ECC, and where a database bit lands in a frame. That covers the tile\'s word offset, the alias shift of a `*_SING` tile, a tile that starts below its frame, and what happens to a bit that falls outside.',
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'The driver, `bitwalk`, is Rust. `t27c gen-rust` generates its rule functions from these specs. The FASM grammar, the feature lookup, file I/O and the text output are written by hand; the bit arithmetic is not. Each spec section cites the prjxray file and line it reproduces (prjxray c9f02d857, prjxray-db 517d66a).',
+ },
+ { kind: 'h', text: 'Measured' },
+ {
+ kind: 'table',
+ head: ['Check', 'Corpus', 'Result'],
+ rows: [
+ ['FASM to frames, against `fasm2frames`', '22 files. 13 real designs: 7 from nextpnr on xc7a100t, and 6 Vivado bitstreams on xc7a35t read back with `bit2fasm`. 7 synthetic files, 2 of them on kintex7 xc7k325t. 2 files that must be refused', '22/22 byte-identical. Both refuse files are refused for the same reason'],
+ ['FASM to frames, against fpga-assembler\'s reference frames', 'The 5 reference-parity cases in lromor/fpga-assembler#49', '5/5, including the kintex7 pseudo-PIP case and the 83-bit `RXCDR_CFG` value'],
+ ['Frames to .bit, against `xc7frames2bit`', '16 frame files on xc7a35t, xc7a100t and xc7a200t', '16/16 byte-identical'],
+ ['CRC, against Vivado', '6 Vivado bitstreams', 'All 12 CRC words reproduce'],
+ ['Frame ECC, against Vivado', '32,520 frames, 752 of them with data', '0 wrong'],
+ ['Mutation gate', '20 defects seeded into the three specs, one at a time', '20/20 caught. A defect counts as caught only if the spec tests and the corpus sweep both fail'],
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'The mutation gate earned its place again while this post was being written. Adding kintex7 support changed how the driver counts bits outside a tile, and one seeded defect survived: a tile window one word too long. The spec tests caught it, but the corpus sweep did not, because the new count absorbed exactly the bit the defect moved. The count is now split in two, so a tile that starts inside its frame can never have its own bits outside it, and the defect is caught again.',
+ },
+ { kind: 'h', text: 'Speed' },
+ {
+ kind: 'figure',
+ svg: speedSvg(),
+ caption: 'Seconds per file, log scale. fasm2frames was timed when each corpus file was built; bitwalk is the best of 3 runs on the same laptop. The machine was busy with other jobs in both cases, so read the ratios as rough.',
+ },
+ {
+ kind: 'table',
+ head: ['File', 'fasm2frames', 'bitwalk --fasm', 'Ratio'],
+ rows: speedRows(),
+ },
+ {
+ kind: 'p',
+ text: 'The synthetic files are no longer than a real design, about 1,100 lines against 1,785, yet `fasm2frames` takes 12 to 45 times longer on them. We have not profiled why, so we make no claim about the cause. We also re-timed `fasm2frames` on three files at the same load as our runs: 3.4 s, 32.5 s and 104.4 s, which makes the ratios larger, not smaller. The figure keeps the lower ones.',
+ },
+ { kind: 'h', text: 'What else does this job' },
+ {
+ kind: 'table',
+ head: ['Tool', 'Language', 'Step', 'Where the bit rules live'],
+ rows: [
+ ['`fasm2frames` (prjxray)', 'Python', 'FASM to frames', 'Python code over prjxray-db'],
+ ['`xc7frames2bit` (prjxray)', 'C++', 'frames to .bit', 'C++ code'],
+ ['fpga-assembler (lromor)', 'C++', 'FASM to .bit', 'C++ code over prjxray-db. Its README reports about 10 times the speed of `fasm2frames`; we did not time it here'],
+ ['`bitwalk` (gHashTag/t27#5609)', 'Rust generated from t27, plus a hand-written driver', 'both', 't27 specs over prjxray-db'],
+ ],
+ },
+ { kind: 'h', text: 'Found on the way' },
+ {
+ kind: 'p',
+ text: 'A differential is only useful when the two sides disagree. Three times they did, and each time the tool we compared against had the defect.',
+ },
+ {
+ kind: 'ul',
+ items: [
+ '`fasm2frames` does not refuse a feature of a site that a `*_SING` tile lacks. On a bottom SING tile the bits wrap into words 99-100 of the frame, which belong to another tile. On a top SING tile they are dropped. The exit code is 0 in both cases, and one line of FASM reproduces it. Reported as f4pga/prjxray#2574. The same happens on kintex7: one synthetic file has 337 wrapped and 356 dropped bits. `bitwalk` writes the same bytes by default, which is why the 22/22 above holds; `--strict` refuses the file and names the line.',
+ '`xc7frames2bit` accepts frames for the wrong part: xc7a100t frames with an xc7a200t part exit 0 and write 192 extra frames. `bitwalk` refuses them. Reported in f4pga/prjxray#2573, with fixes in openXC7/prjxray#27 and #28.',
+ 'In case 05 of lromor/fpga-assembler#49, the 83-bit `RXCDR_CFG` value, fpga-assembler\'s own frames differ from the reference: 18 bits missing, 5 extra. `bitwalk` matches the reference. Our reading of the source: the parser packs a long binary literal into 64-bit words from its most significant end, so for 83 bits the first word holds only the low 19 bits. Emulating that packing gives exactly the 18 missing and 5 extra bits.',
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'One defect was ours. The kintex7 tile grid has four tiles that start 2 words below their frame, and our driver read the offset as unsigned and stopped. The spec now says how such a tile\'s bits are placed (`seg_shift`), with a test.',
+ },
+ { kind: 'h', text: 'On the board' },
+ {
+ kind: 'p',
+ text: 'Files that match are a claim about files. To check the chain on hardware we built one real design for the ALINX AX7203 (XC7A200T): trinet node 0, 121,587 FASM lines, 20,230 frames. On that FASM `bitwalk` was again byte-identical to openXC7 for both the frames and the .bit, and took about 0.8 s where `fasm2frames` took about 68 s, on a host that was busy the whole time. `openFPGALoader` then loaded bitwalk\'s .bit into SRAM over JTAG, with no flash write, and the FPGA reported DONE.',
+ },
+ {
+ kind: 'table',
+ head: ['Run on the AX7203', 'Result'],
+ rows: [
+ ['SRAM load of bitwalk\'s .bit', '`done 1` in 16.7 s'],
+ ['All 42 ternary matrices of the model, each answer tagged under the node key', '`receipts verified (tag) : 403200/403200`, 33,792/33,792 rows bit-exact'],
+ ['One layer with int8 activations', '`receipts verified (tag) : 51840/51840`, 320/320 rows bit-exact'],
+ ],
+ },
+ {
+ kind: 'terminal',
+ src: 'term/x7-board/session.cast',
+ share: 'https://t27.ai/term/x7-board/',
+ title: 'tri x7-board · t27 back half on the AX7203',
+ caption: 'A recorded session: `tri x7-board compare`, `load` and `receipts`, run again for the recording. Every byte printed is real and arrives when it did; the prompt and the typing are staged, and silences over 2 s are shortened, with a note in the title bar while that happens. That session re-timed `fasm2frames` at 33.3 s against 0.43 s, on a machine with a load of 23 on 8 cpus.',
+ },
+ {
+ kind: 'p',
+ text: 'Because the two .bit files are identical, this shows nothing the openXC7 file would not have shown. The claim is equivalence on one design on one board, not a better bitstream. The front half, yosys and nextpnr-xilinx, is openXC7\'s and unchanged.',
+ },
+ { kind: 'h', text: 'What this does not show' },
+ {
+ kind: 'ul',
+ items: [
+ 'One design has run on a board. Every other check above compares files.',
+ 'Apart from the board design, no xc7a200t FASM is in the corpus, and there is no Vivado reference for xc7a100t or xc7a200t. There, byte-identical means identical to the prjxray tools, not to Vivado.',
+ 'The rule for a tile that starts below its frame is covered by spec tests only. No database bit reaches those kintex7 tiles, so the corpus cannot exercise it.',
+ 'The FASM grammar and feature lookup are hand-written Rust, checked by the corpus rather than derived from a spec.',
+ ],
+ },
+]
+
+export const ruBody: Block[] = [
+ {
+ kind: 'p',
+ text: 'openXC7 собирает битстрим Xilinx 7-series в две половины. Первая — yosys и nextpnr: на входе Verilog, на выходе FASM. FASM — это текстовый список фич вроде `CLBLM_R_X3Y0.SLICEM_X0.ALUT.INIT[63:0] = 64\'h...`. Вторая половина превращает этот список в битстрим. `fasm2frames` из prjxray (Python) находит биты каждой фичи и пишет конфигурационные кадры, а `xc7frames2bit` (C++) упаковывает кадры в конфигурационные пакеты. Мы переписали вторую половину так, что каждая позиция бита, адрес кадра и слово пакета берутся из t27-спека, и сравнили её байт в байт с двумя инструментами, которые она заменяет.',
+ },
+ {
+ kind: 'figure',
+ svg: chainSvg('ru'),
+ caption: 'Вторая половина flow. Каждый золотой блок — драйвер, сгенерированный из t27-спеков. Пунктир — проверки на корпусе против инструмента prjxray над ним.',
+ },
+ { kind: 'h', text: 'Что лежит в спеках' },
+ {
+ kind: 'ul',
+ items: [
+ '`packets.t27`: поток конфигурационных пакетов. Заголовки, регистры, команды, CRC и последовательность записи, которую использует `xc7frames2bit`.',
+ '`far.t27`: обход адресов кадров для xc7a35t, xc7a100t и xc7a200t; микросхема выбирается по IDCODE.',
+ '`frames.t27`: ECC кадра и то, куда бит из базы попадает в кадре. Это смещение тайла в словах, сдвиг псевдонима у тайла `*_SING`, тайл, который начинается ниже своего кадра, и что происходит с битом, который выпадает за его пределы.',
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'Драйвер, `bitwalk`, написан на Rust. `t27c gen-rust` генерирует его функции-правила из этих спеков. Грамматика FASM, поиск фич, файловый ввод-вывод и текстовый вывод написаны руками; битовая арифметика — нет. Каждый раздел спека ссылается на файл и строку prjxray, которые он воспроизводит (prjxray c9f02d857, prjxray-db 517d66a).',
+ },
+ { kind: 'h', text: 'Измерено' },
+ {
+ kind: 'table',
+ head: ['Проверка', 'Корпус', 'Результат'],
+ rows: [
+ ['FASM в кадры, против `fasm2frames`', '22 файла. 13 настоящих дизайнов: 7 из nextpnr на xc7a100t и 6 битстримов Vivado на xc7a35t, прочитанных обратно через `bit2fasm`. 7 синтетических файлов, 2 из них на kintex7 xc7k325t. 2 файла, которые должны быть отвергнуты', '22/22 байт в байт. Оба файла на отказ отвергнуты по той же причине'],
+ ['FASM в кадры, против эталонных кадров fpga-assembler', '5 случаев на совпадение с эталоном из lromor/fpga-assembler#49', '5/5, включая случай с псевдо-PIP на kintex7 и 83-битное значение `RXCDR_CFG`'],
+ ['Кадры в .bit, против `xc7frames2bit`', '16 файлов кадров на xc7a35t, xc7a100t и xc7a200t', '16/16 байт в байт'],
+ ['CRC, против Vivado', '6 битстримов Vivado', 'Все 12 слов CRC воспроизводятся'],
+ ['ECC кадров, против Vivado', '32 520 кадров, 752 из них с данными', '0 ошибок'],
+ ['Мутационный гейт', '20 дефектов, внесённых в три спека по одному', '20/20 пойманы. Дефект считается пойманным, только если падают и тесты спека, и прогон по корпусу'],
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'Мутационный гейт ещё раз окупился, пока писался этот пост. Поддержка kintex7 изменила то, как драйвер считает биты за пределами тайла, и один внесённый дефект выжил: окно тайла на одно слово длиннее. Тесты спека его поймали, а прогон по корпусу — нет, потому что новый счётчик поглотил ровно тот бит, который сдвигал дефект. Теперь счётчик разделён на два, так что у тайла, который начинается внутри своего кадра, собственные биты никогда не окажутся снаружи, и дефект снова ловится.',
+ },
+ { kind: 'h', text: 'Скорость' },
+ {
+ kind: 'figure',
+ svg: speedSvg('ru'),
+ caption: 'Секунды на файл, логарифмическая шкала. fasm2frames засекали, когда собирался каждый файл корпуса; bitwalk — лучший из 3 прогонов на том же ноутбуке. В обоих случаях машина была занята другими задачами, так что отношения стоит читать как грубые.',
+ },
+ {
+ kind: 'table',
+ head: ['Файл', 'fasm2frames', 'bitwalk --fasm', 'Отношение'],
+ rows: speedRows('ru'),
+ },
+ {
+ kind: 'p',
+ text: 'Синтетические файлы не длиннее настоящего дизайна, около 1 100 строк против 1 785, но `fasm2frames` тратит на них в 12–45 раз больше времени. Мы не профилировали, почему, и о причине ничего не утверждаем. Ещё мы перезасекли `fasm2frames` на трёх файлах при той же нагрузке, что и наши прогоны: 3,4 с, 32,5 с и 104,4 с — это делает отношения больше, а не меньше. На графике оставлены меньшие.',
+ },
+ { kind: 'h', text: 'Что ещё делает эту работу' },
+ {
+ kind: 'table',
+ head: ['Инструмент', 'Язык', 'Шаг', 'Где живут правила битов'],
+ rows: [
+ ['`fasm2frames` (prjxray)', 'Python', 'FASM в кадры', 'Код на Python поверх prjxray-db'],
+ ['`xc7frames2bit` (prjxray)', 'C++', 'кадры в .bit', 'Код на C++'],
+ ['fpga-assembler (lromor)', 'C++', 'FASM в .bit', 'Код на C++ поверх prjxray-db. Его README сообщает примерно 10-кратную скорость против `fasm2frames`; здесь мы его не засекали'],
+ ['`bitwalk` (gHashTag/t27#5609)', 'Rust, сгенерированный из t27, плюс написанный руками драйвер', 'оба', 't27-спеки поверх prjxray-db'],
+ ],
+ },
+ { kind: 'h', text: 'Найдено по дороге' },
+ {
+ kind: 'p',
+ text: 'Дифференциальное сравнение полезно только тогда, когда стороны расходятся. Три раза они разошлись, и каждый раз дефект был у инструмента, с которым мы сравнивали.',
+ },
+ {
+ kind: 'ul',
+ items: [
+ '`fasm2frames` не отвергает фичу сайта, которого нет у тайла `*_SING`. На нижнем SING-тайле биты заворачиваются в слова 99–100 кадра, которые принадлежат другому тайлу. На верхнем SING-тайле они отбрасываются. Код выхода в обоих случаях 0, и воспроизводится это одной строкой FASM. Сообщено в f4pga/prjxray#2574. На kintex7 то же самое: в одном синтетическом файле 337 завёрнутых и 356 отброшенных битов. `bitwalk` по умолчанию пишет те же байты, поэтому 22/22 выше и держится; `--strict` отвергает файл и называет строку.',
+ '`xc7frames2bit` принимает кадры для чужой микросхемы: кадры xc7a100t с микросхемой xc7a200t дают код выхода 0 и 192 лишних кадра. `bitwalk` их отвергает. Сообщено в f4pga/prjxray#2573, исправления в openXC7/prjxray#27 и #28.',
+ 'В случае 05 из lromor/fpga-assembler#49, 83-битном значении `RXCDR_CFG`, собственные кадры fpga-assembler отличаются от эталона: 18 битов не хватает, 5 лишних. `bitwalk` совпадает с эталоном. Наше прочтение исходников: парсер упаковывает длинный двоичный литерал в 64-битные слова со старшего конца, так что для 83 битов первое слово содержит только младшие 19 битов. Эмуляция такой упаковки даёт ровно 18 недостающих и 5 лишних битов.',
+ ],
+ },
+ {
+ kind: 'p',
+ text: 'Один дефект был наш. В сетке тайлов kintex7 есть четыре тайла, которые начинаются на 2 слова ниже своего кадра, а наш драйвер читал смещение как беззнаковое и останавливался. Теперь спек говорит, как размещаются биты такого тайла (`seg_shift`), и к этому есть тест.',
+ },
+ { kind: 'h', text: 'На плате' },
+ {
+ kind: 'p',
+ text: 'Совпадение файлов — это утверждение о файлах. Чтобы проверить цепочку на железе, мы собрали один настоящий дизайн для ALINX AX7203 (XC7A200T): trinet node 0, 121 587 строк FASM, 20 230 кадров. На этом FASM `bitwalk` снова совпал с openXC7 байт в байт и по кадрам, и по .bit, и занял около 0,8 с там, где `fasm2frames` занял около 68 с, на хосте, который всё это время был занят. Затем `openFPGALoader` загрузил .bit от bitwalk в SRAM по JTAG, без записи во флеш, и FPGA сообщила DONE.',
+ },
+ {
+ kind: 'table',
+ head: ['Прогон на AX7203', 'Результат'],
+ rows: [
+ ['Загрузка .bit от bitwalk в SRAM', '`done 1` за 16,7 с'],
+ ['Все 42 тернарные матрицы модели, каждый ответ помечен ключом узла', '`receipts verified (tag) : 403200/403200`, 33 792/33 792 строк бит в бит'],
+ ['Один слой с int8-активациями', '`receipts verified (tag) : 51840/51840`, 320/320 строк бит в бит'],
+ ],
+ },
+ {
+ kind: 'terminal',
+ src: 'term/x7-board/session.cast',
+ share: 'https://t27.ai/term/x7-board/',
+ title: 'tri x7-board · t27 back half on the AX7203',
+ caption: 'Записанная сессия: `tri x7-board compare`, `load` и `receipts`, запущенные ещё раз для записи. Каждый напечатанный байт настоящий и появляется тогда, когда появился; приглашение и набор команд постановочные, паузы длиннее 2 с сокращены, и пока это происходит, в заголовке окна стоит пометка. В этой сессии `fasm2frames` перезасечён: 33,3 с против 0,43 с, при нагрузке 23 на 8 ядрах. Сама запись на английском.',
+ },
+ {
+ kind: 'p',
+ text: 'Поскольку два файла .bit идентичны, это не показывает ничего, чего не показал бы файл openXC7. Утверждение — эквивалентность на одном дизайне на одной плате, а не лучший битстрим. Передняя половина, yosys и nextpnr-xilinx, принадлежит openXC7 и не менялась.',
+ },
+ { kind: 'h', text: 'Чего это не показывает' },
+ {
+ kind: 'ul',
+ items: [
+ 'На плате прогнан один дизайн. Все остальные проверки выше сравнивают файлы.',
+ 'Кроме дизайна для платы, в корпусе нет FASM для xc7a200t, и нет эталона Vivado для xc7a100t и xc7a200t. Там «байт в байт» значит «как у инструментов prjxray», а не «как у Vivado».',
+ 'Правило для тайла, который начинается ниже своего кадра, покрыто только тестами спека. Ни один бит из базы не попадает в эти тайлы kintex7, так что корпус его не проверяет.',
+ 'Грамматика FASM и поиск фич — написанный руками Rust, проверенный корпусом, а не выведенный из спека.',
+ ],
+ },
+]
diff --git a/apps/website/src/data/blog/bodies/the-fpga-flow-layer-by-layer.ts b/apps/website/src/data/blog/bodies/the-fpga-flow-layer-by-layer.ts
new file mode 100644
index 0000000000..e9389552a6
--- /dev/null
+++ b/apps/website/src/data/blog/bodies/the-fpga-flow-layer-by-layer.ts
@@ -0,0 +1,167 @@
+import type { Block } from '../types'
+import { FLOW, LAYERS, flowBefore, flowAfter, savedPerBuild, ceiling, hoursPerYear } from '../../devkit'
+
+// Every number comes from src/data/devkit.ts, the record of one `tri devkit flow --build` run.
+const pct = (s: number) => `${((100 * s) / flowBefore).toFixed(1)} %`
+const open = LAYERS.filter(l => !l.t27)
+
+export const body: Block[] = [
+ {
+ kind: 'p',
+ text: `The previous post rebuilt the back half of openXC7 from t27 specs. FASM to frames and frames to bitstream now come from the specs, byte for byte. That raises the next question: what would it be worth to do the same for every layer of the flow? To answer it we timed every layer of one real build and put the numbers next to each other.`,
+ },
+ { kind: 'h', text: 'One build, every layer' },
+ {
+ kind: 'p',
+ text: `The design is ${FLOW.design} for the ${FLOW.part} on an AX7203 board, ${FLOW.fasmLines.toLocaleString('en-US')} lines of FASM. \`tri devkit flow --build\` runs the openXC7 flow with every step timed. It then runs the t27 replacements on the same FASM, three times each, and compares their output with openXC7's byte for byte. The times are wall seconds on one laptop at a load of ${FLOW.load}.`,
+ },
+ {
+ kind: 'table',
+ head: ['Layer', 'openXC7 tool', 'Now', 'Share', 't27', 'Same bytes'],
+ rows: [
+ ...LAYERS.map(l => [
+ `${l.id} ${l.name}`,
+ `\`${l.tool}\``,
+ `${l.s.toFixed(2)} s`,
+ pct(l.s),
+ l.t27 ? `\`${l.t27.tool}\` ${l.t27.s.toFixed(2)} s` : 'not yet',
+ l.t27 ? (l.t27.identical ? 'yes' : 'no') : '–',
+ ]),
+ ['Whole flow', '', `${flowBefore.toFixed(2)} s`, '', `${flowAfter.toFixed(2)} s (${(flowBefore / flowAfter).toFixed(2)}×)`, ''],
+ ],
+ },
+ {
+ kind: 'terminal',
+ src: FLOW.cast,
+ share: FLOW.share,
+ title: 'tri devkit · the FPGA flow, layer by layer',
+ caption: 'The recorded run: `tri devkit flow --build` and `tri devkit impact`. Every byte printed is real and arrives when it did; the prompt and the typing are staged, and silences over 2 s are shortened, with a note in the title bar while that happens.',
+ },
+ {
+ kind: 'p',
+ text: `The rewrite so far saves ${savedPerBuild.toFixed(1)} s on this build, all of it in L3. L4 is a tie: \`bitwalk --write\` took 0.25 s against 0.22 s for \`xc7frames2bit\`. The bitstream is the same file either way, so this is a faster route to the same result, not a better result.`,
+ },
+ { kind: 'h', text: 'Where the rest of the time goes' },
+ {
+ kind: 'p',
+ text: `After L3, place and route is ${pct(LAYERS[1].s)} of the build and synthesis is ${pct(LAYERS[0].s)}. Amdahl's law gives the most a rewrite of either could ever give: make the layer take 0 s and see what is left.`,
+ },
+ {
+ kind: 'table',
+ head: ['If this took 0 s', 'Flow', 'Against openXC7 today'],
+ rows: open.map(l => {
+ const c = ceiling(l)
+ return [`${l.id} ${l.name}`, `${flowAfter.toFixed(1)} s → ${c.flow.toFixed(1)} s`, `${c.x.toFixed(2)}×`]
+ }),
+ },
+ {
+ kind: 'p',
+ text: 'So the next lever is place and route, by a wide margin. A spec-driven synthesis step, even an instant one, caps the whole flow at 1.65×. These are ceilings, not plans. nextpnr-xilinx is a large, mature C++ program, and the realistic path is work inside it (we already send patches upstream) plus spec-checked timing and routing rules, not a rewrite from zero.',
+ },
+ { kind: 'h', text: 'What it is worth in hours' },
+ {
+ kind: 'p',
+ text: `\`tri devkit impact\` turns the saving into hours. At an assumed 20 builds a day for one person over 230 working days, ${savedPerBuild.toFixed(1)} s a build is ${hoursPerYear(savedPerBuild, 20, 1).toFixed(1)} hours a year of waiting. The builds and the people are assumptions, so the page at t27.ai/#/devkit lets you put in your own.`,
+ },
+ { kind: 'h', text: 'Can I run it?' },
+ {
+ kind: 'p',
+ text: 'Not yet. The run above used a local build of `tri devkit`, and `bitwalk` is built from an open pull request, gHashTag/t27#5609. Neither is in a `tri` release, so there is no install command to give here. Both are to ship in the `tri` release archives with a one-line install; that work is tracked in github.com/gHashTag/trinity/issues/1272, and this post will get the command when a release has it.',
+ },
+ { kind: 'h', text: 'What this does not show' },
+ {
+ kind: 'ul',
+ items: [
+ 'One design on one laptop. The 1.40× is for this build; a design that spends less time in fasm2frames gains less.',
+ 'The openXC7 times come from the build, and the t27 times were taken right after it in the same session. Load moved between them.',
+ 'L1 and L2 are not rewritten. Their rows are ceilings.',
+ 'Loading onto the board (16.7 s SRAM load in the earlier board run) is outside the flow time above.',
+ ],
+ },
+]
+
+// The same post in Russian, from the same numbers. Decimal comma, as in the other Russian posts.
+const ru = (n: number, d = 2) => n.toFixed(d).replace('.', ',')
+const pctRu = (s: number) => `${ru((100 * s) / flowBefore, 1)} %`
+const loadRu = FLOW.load.replace(/^([\d.]+) on (\d+) cpus$/, (_, a: string, n: string) => `${a.replace('.', ',')} на ${n} ядрах`)
+const NAME_RU: Record = {
+ L1: 'Синтез',
+ L2: 'Размещение и разводка',
+ L3: 'FASM → кадры',
+ L4: 'Кадры → .bit',
+}
+
+export const ruBody: Block[] = [
+ {
+ kind: 'p',
+ text: `В прошлом посте мы пересобрали заднюю половину openXC7 из t27-спеков. FASM в кадры и кадры в битстрим теперь получаются из спеков, байт в байт. Отсюда следующий вопрос: сколько стоило бы сделать то же самое для каждого слоя flow? Чтобы ответить, мы засекли каждый слой одной настоящей сборки и поставили числа рядом.`,
+ },
+ { kind: 'h', text: 'Одна сборка, все слои' },
+ {
+ kind: 'p',
+ text: `Дизайн — ${FLOW.design} для ${FLOW.part} на плате AX7203, ${FLOW.fasmLines.toLocaleString('ru-RU')} строк FASM. Команда \`tri devkit flow --build\` запускает flow openXC7 с замером каждого шага. Затем она трижды запускает t27-замены на том же FASM и сравнивает их вывод с выводом openXC7 байт в байт. Время — секунды по часам на одном ноутбуке при нагрузке ${loadRu}.`,
+ },
+ {
+ kind: 'table',
+ head: ['Слой', 'Инструмент openXC7', 'Сейчас', 'Доля', 't27', 'Те же байты'],
+ rows: [
+ ...LAYERS.map(l => [
+ `${l.id} ${NAME_RU[l.id]}`,
+ `\`${l.tool}\``,
+ `${ru(l.s)} с`,
+ pctRu(l.s),
+ l.t27 ? `\`${l.t27.tool}\` ${ru(l.t27.s)} с` : 'пока нет',
+ l.t27 ? (l.t27.identical ? 'да' : 'нет') : '–',
+ ]),
+ ['Весь flow', '', `${ru(flowBefore)} с`, '', `${ru(flowAfter)} с (${ru(flowBefore / flowAfter)}×)`, ''],
+ ],
+ },
+ {
+ kind: 'terminal',
+ src: FLOW.cast,
+ share: FLOW.share,
+ title: 'tri devkit · the FPGA flow, layer by layer',
+ caption: 'Записанный прогон: `tri devkit flow --build` и `tri devkit impact`. Каждый напечатанный байт настоящий и появляется тогда, когда появился; приглашение и набор команд постановочные, паузы длиннее 2 с сокращены, и пока это происходит, в заголовке окна стоит пометка. Сама запись на английском.',
+ },
+ {
+ kind: 'p',
+ text: `Переписанное на сегодня экономит ${ru(savedPerBuild, 1)} с на этой сборке, и всё это в L3. L4 — ничья: \`bitwalk --write\` занял 0,25 с против 0,22 с у \`xc7frames2bit\`. Битстрим в обоих случаях один и тот же файл, так что это более быстрый путь к тому же результату, а не лучший результат.`,
+ },
+ { kind: 'h', text: 'Куда уходит остальное время' },
+ {
+ kind: 'p',
+ text: `После L3 размещение и разводка — ${pctRu(LAYERS[1].s)} сборки, синтез — ${pctRu(LAYERS[0].s)}. Закон Амдала даёт максимум, который мог бы дать переписанный любой из них: пусть слой занимает 0 с, и посмотрим, что останется.`,
+ },
+ {
+ kind: 'table',
+ head: ['Если бы это заняло 0 с', 'Flow', 'Против openXC7 сегодня'],
+ rows: open.map(l => {
+ const c = ceiling(l)
+ return [`${l.id} ${NAME_RU[l.id]}`, `${ru(flowAfter, 1)} с → ${ru(c.flow, 1)} с`, `${ru(c.x)}×`]
+ }),
+ },
+ {
+ kind: 'p',
+ text: 'Значит, следующий рычаг — размещение и разводка, с большим отрывом. Синтез из спеков, даже мгновенный, ограничивает весь flow 1,65×. Это потолки, а не планы. nextpnr-xilinx — большая зрелая программа на C++, и реалистичный путь — работа внутри неё (мы уже отправляем туда патчи) плюс проверяемые спеками правила тайминга и разводки, а не переписывание с нуля.',
+ },
+ { kind: 'h', text: 'Сколько это в часах' },
+ {
+ kind: 'p',
+ text: `\`tri devkit impact\` переводит экономию в часы. При допущении 20 сборок в день у одного человека за 230 рабочих дней ${ru(savedPerBuild, 1)} с на сборку — это ${ru(hoursPerYear(savedPerBuild, 20, 1), 1)} часа ожидания в год. Число сборок и людей — допущения, поэтому страница t27.ai/#/devkit даёт подставить свои.`,
+ },
+ { kind: 'h', text: 'Можно ли это запустить у себя?' },
+ {
+ kind: 'p',
+ text: 'Пока нет. Прогон выше сделан локальной сборкой `tri devkit`, а `bitwalk` собран из открытого pull request, gHashTag/t27#5609. Ни того, ни другого нет в релизе `tri`, поэтому команды установки здесь нет. Оба должны войти в архивы релиза `tri` с установкой одной строкой; эта работа ведётся в github.com/gHashTag/trinity/issues/1272, и команда появится в посте, когда она будет в релизе.',
+ },
+ { kind: 'h', text: 'Чего это не показывает' },
+ {
+ kind: 'ul',
+ items: [
+ 'Один дизайн на одном ноутбуке. 1,40× — для этой сборки; дизайн, который проводит меньше времени в fasm2frames, выиграет меньше.',
+ 'Время openXC7 взято из сборки, время t27 — сразу после неё в той же сессии. Нагрузка между ними менялась.',
+ 'L1 и L2 не переписаны. Их строки — потолки.',
+ 'Загрузка в плату (16,7 с загрузки в SRAM в прошлом прогоне на плате) не входит во время flow выше.',
+ ],
+ },
+]
diff --git a/apps/website/src/data/blog/bodies/trained-weights-ran-receipts-were-not-checked.ts b/apps/website/src/data/blog/bodies/trained-weights-ran-receipts-were-not-checked.ts
index d76e25f0c2..e512ca1290 100644
--- a/apps/website/src/data/blog/bodies/trained-weights-ran-receipts-were-not-checked.ts
+++ b/apps/website/src/data/blog/bodies/trained-weights-ran-receipts-were-not-checked.ts
@@ -1,4 +1,5 @@
import type { Block } from '../types'
+import { X7_BOARD } from '../../casts'
export const body: Block[] = [
{
@@ -127,6 +128,13 @@ export const body: Block[] = [
kind: 'p',
text: "One side prediction missed. The slip at 30 came after 150,017 jobs, not within the first 20,000 as at 64, so the stalls that fill the buffer are rarer than a single fixed gap would explain. What stalls the bridge's USB transfers is not measured, and the hub is not excluded. The rule the harness follows now is to keep 19 x W under 512 bytes, which is why the default is 24 (456 bytes). It costs no measurable throughput: 4,716 answers/s, against about 4,660 to 4,750 at 64 before the slips.",
},
+ {
+ kind: 'terminal',
+ src: X7_BOARD.src,
+ share: X7_BOARD.share,
+ title: X7_BOARD.title,
+ caption: "A later recording of the same five-check receipt harness on the same board, made for the t27 back-half post. It ran a quick set on the trinet node, so its counts are not this post's run." + ' ' + X7_BOARD.caption.en,
+ },
{
kind: 'h',
text: 'Two items booked as new hardware need none',
@@ -294,6 +302,13 @@ export const ruBody: Block[] = [
kind: 'p',
text: 'Одно побочное предсказание не сбылось. Срыв при 30 пришёл после 150 017 задач, а не в первые 20 000, как при 64, так что задержки, заполняющие буфер, реже, чем объяснил бы один фиксированный простой. Что задерживает USB-передачи моста, не измерено, и хаб не исключён. Правило харнесса теперь — держать 19 x W меньше 512 байт, поэтому по умолчанию окно 24 (456 байт). Скорости это заметно не стоит: 4 716 ответов в секунду против примерно 4 660–4 750 при 64 до сбоев.',
},
+ {
+ kind: 'terminal',
+ src: X7_BOARD.src,
+ share: X7_BOARD.share,
+ title: X7_BOARD.title,
+ caption: 'Более поздняя запись того же харнесса квитанций с пятью проверками на той же плате, сделанная для поста о задней половине t27. Там прогнан короткий набор на узле trinet, так что её счёт — не прогон этого поста.' + ' ' + X7_BOARD.caption.ru,
+ },
{
kind: 'h',
text: 'Две задачи, записанные в новое железо, его не требуют',
diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts
index cd906d4702..b27967edc9 100644
--- a/apps/website/src/data/blog/index.ts
+++ b/apps/website/src/data/blog/index.ts
@@ -2,6 +2,69 @@ import type { PostMeta } from './types'
/** Индекс блога: список и метаданные без тяжёлых тел публикаций. */
export const postsIndex: PostMeta[] = [
+ {
+ slug: "the-fpga-flow-layer-by-layer",
+ title: "The FPGA flow, layer by layer: what a rewrite from specs is worth",
+ summary: "[one design, one laptop at a load of 12.6 on 8 cpus; L1 and L2 figures are Amdahl ceilings, not results; hours a year rest on stated assumptions] We timed every layer of one real XC7A200T build through openXC7. Synthesis took 12.7 s, place and route 70.1 s, FASM to frames 33.9 s and frames to bitstream 0.2 s, 116.9 s in all. With the two layers already rebuilt from t27 specs, the flow takes 83.5 s, 1.40 times faster, and the bitstream is byte-identical. Place and route is now 60% of the build: if it took no time the flow would be 8.75 times faster, while an instant synthesis step caps out at 1.65 times. The numbers are in a recorded terminal session. The tools are not in a tri release yet.",
+ date: "2026-10-03",
+ readingMinutes: 5,
+ tags: ["FPGA", "Open toolchain", "t27"],
+ receipts: [
+ { label: "The recorded run: tri devkit flow and impact", href: "https://t27.ai/term/devkit-flow/" },
+ { label: "The layer calculator", href: "https://t27.ai/#/devkit" },
+ { label: "trinity#1272: tri devkit and bitwalk in a tri release (open)", href: "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/gHashTag/trinity/issues/1272" },
+ { label: "The back half of openXC7, from a spec (previous post)", href: "https://t27.ai/#/blog/the-back-half-of-openxc7-from-a-spec" },
+ { label: "t27#5609: bitwalk and the three specs (open)", href: "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/gHashTag/t27/pull/5609" },
+ ],
+ openQuestions: [
+ "One design on one laptop; a design that spends less of its build in fasm2frames gains less.",
+ "Place and route is the largest layer, and nothing here shows a spec-driven route to making it faster.",
+ ],
+ published: true,
+ ru: {
+ title: "FPGA-flow по слоям: сколько стоит переписать его из спеков",
+ summary: "[один дизайн, один ноутбук при нагрузке 12,6 на 8 ядрах; цифры L1 и L2 — потолки по Амдалу, а не результаты; часы в год опираются на названные допущения] Мы засекли каждый слой одной настоящей сборки XC7A200T через openXC7. Синтез занял 12,7 с, размещение и разводка 70,1 с, FASM в кадры 33,9 с, кадры в битстрим 0,2 с, всего 116,9 с. С двумя слоями, уже пересобранными из t27-спеков, flow занимает 83,5 с, в 1,40 раза быстрее, и битстрим совпадает байт в байт. Размещение и разводка теперь 60% сборки: если бы они не занимали времени, flow был бы в 8,75 раза быстрее, а мгновенный синтез упирается в 1,65 раза. Числа — в записанной терминальной сессии. В релизе tri этих инструментов пока нет.",
+ openQuestions: [
+ "Один дизайн на одном ноутбуке; дизайн, который тратит меньшую долю сборки в fasm2frames, выиграет меньше.",
+ "Размещение и разводка — самый большой слой, и ничто здесь не показывает пути из спеков, который сделал бы его быстрее.",
+ ],
+ },
+ },
+ {
+ slug: "the-back-half-of-openxc7-from-a-spec",
+ title: "The back half of openXC7, from a spec: FASM to frames to bitstream",
+ summary: "[one design on one board, equivalence not a better bitstream; timings on one busy laptop, ratios are rough; the fpga-assembler speed figure is its README's, not ours] openXC7 turns FASM into a Xilinx 7-series bitstream with two prjxray tools, fasm2frames in Python and xc7frames2bit in C++. We rebuilt that half so every bit position, frame address and packet word comes from three t27 specs. Against the two tools it matches byte for byte on 22 of 22 FASM files and 16 of 16 frame files, and it is about 6 to 14 times faster on small real designs, about 85 times on a 121,587-line one, and up to about 200 times on synthetic test files. The comparison found a silent bit-wrap in fasm2frames (f4pga/prjxray#2574), a missing part check in xc7frames2bit (#2573), and a bug of our own on kintex7. On an AX7203 board (XC7A200T), a .bit written this way for a 121,587-line design was byte-identical to openXC7's, loaded into SRAM, and returned 403,200 of 403,200 tagged receipts.",
+ date: "2026-10-03",
+ readingMinutes: 8,
+ tags: ["FPGA", "Open toolchain", "Verification", "t27"],
+ receipts: [
+ { label: "t27#5609: bitwalk and the three specs (open)", href: "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/gHashTag/t27/pull/5609" },
+ { label: "prjxray#2574: fasm2frames wraps or drops bits of a site a SING tile lacks", href: "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/f4pga/prjxray/issues/2574" },
+ { label: "prjxray#2573: xc7frames2bit accepts frames for the wrong part", href: "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/f4pga/prjxray/issues/2573" },
+ { label: "openXC7/prjxray#27: xc7frames2bit refuses frame addresses the part does not have (open)", href: "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/openXC7/prjxray/pull/27" },
+ { label: "openXC7/prjxray#28: xc7frames2bit exits 1 when the bitstream cannot be written (open)", href: "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/openXC7/prjxray/pull/28" },
+ { label: "trinity-fpga: the AX7203 run, logs and receipts", href: "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/gHashTag/trinity-fpga/blob/421ae920fb477c98505e4143df3cab7954c268a7/conformance/X7_BITWALK_ON_THE_BOARD.md" },
+ { label: "fpga-assembler#49: the reference-parity cases", href: "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/lromor/fpga-assembler/issues/49" },
+ { label: "fpga-assembler, a C++ FASM-to-bitstream tool", href: "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/lromor/fpga-assembler" },
+ ],
+ openQuestions: [
+ "One design, one board: the AX7203 run covers trinet node 0 only, and its .bit is byte-identical to openXC7's, so it cannot show a case where the two differ.",
+ "Apart from the board design, there is no xc7a200t FASM in the corpus, and there is no Vivado reference for xc7a100t or xc7a200t.",
+ "gHashTag/t27#5609 has no review yet. f4pga/prjxray#2573 and #2574 and openXC7/prjxray#27 and #28 have no maintainer reply yet.",
+ "fpga-assembler was not timed here, so the table carries its README's figure, not a measurement.",
+ ],
+ published: true,
+ ru: {
+ title: "Задняя половина openXC7 из спека: FASM в кадры и в битстрим",
+ summary: "[один дизайн на одной плате, эквивалентность, а не лучший битстрим; время на одном занятом ноутбуке, отношения грубые; скорость fpga-assembler — из его README, не наша] openXC7 превращает FASM в битстрим Xilinx 7-series двумя инструментами prjxray: fasm2frames на Python и xc7frames2bit на C++. Мы пересобрали эту половину так, что каждая позиция бита, адрес кадра и слово пакета берутся из трёх t27-спеков. Против двух инструментов она совпадает байт в байт на 22 из 22 файлов FASM и 16 из 16 файлов кадров и быстрее примерно в 6–14 раз на небольших настоящих дизайнах, примерно в 85 раз на дизайне в 121 587 строк и до примерно 200 раз на синтетических тестах. Сравнение нашло тихий заворот битов в fasm2frames (f4pga/prjxray#2574), отсутствующую проверку микросхемы в xc7frames2bit (#2573) и наш собственный баг на kintex7. На плате AX7203 (XC7A200T) .bit, записанный так для дизайна в 121 587 строк, совпал с openXC7 байт в байт, загрузился в SRAM и вернул 403 200 из 403 200 помеченных квитанций.",
+ openQuestions: [
+ "Один дизайн, одна плата: прогон на AX7203 покрывает только trinet node 0, и его .bit совпадает с openXC7 байт в байт, так что он не может показать случай, где они расходятся.",
+ "Кроме дизайна для платы, в корпусе нет FASM для xc7a200t, и нет эталона Vivado для xc7a100t и xc7a200t.",
+ "У gHashTag/t27#5609 ещё нет ревью. На f4pga/prjxray#2573 и #2574 и openXC7/prjxray#27 и #28 мейнтейнеры ещё не ответили.",
+ "fpga-assembler здесь не засекали, поэтому в таблице цифра из его README, а не измерение.",
+ ],
+ },
+ },
{
slug: "one-regional-clock-twelve-pull-requests",
title: "One regional clock, twelve pull requests, and a spec that found a compiler bug",
diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts
index c719d8d043..760bc107e8 100644
--- a/apps/website/src/data/blog/posts.ts
+++ b/apps/website/src/data/blog/posts.ts
@@ -1,3 +1,5 @@
+import { body as body_the_fpga_flow_layer_by_layer, ruBody as ruBody_the_fpga_flow_layer_by_layer } from './bodies/the-fpga-flow-layer-by-layer'
+import { body as body_the_back_half_of_openxc7_from_a_spec, ruBody as ruBody_the_back_half_of_openxc7_from_a_spec } from './bodies/the-back-half-of-openxc7-from-a-spec'
import { body as body_how_to_join_the_swarm, ruBody as ruBody_how_to_join_the_swarm } from './bodies/how-to-join-the-swarm'
import { postsIndex } from './index'
import { body as body_queen_browser_actions_you_can_follow, ruBody as ruBody_queen_browser_actions_you_can_follow } from './bodies/queen-browser-actions-you-can-follow'
@@ -82,6 +84,8 @@ import { body as body_real_value_in_integer_container, ruBody as ruBody_real_val
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 = {
+ 'the-fpga-flow-layer-by-layer': { body: body_the_fpga_flow_layer_by_layer, ruBody: ruBody_the_fpga_flow_layer_by_layer },
+ 'the-back-half-of-openxc7-from-a-spec': { body: body_the_back_half_of_openxc7_from_a_spec, ruBody: ruBody_the_back_half_of_openxc7_from_a_spec },
'tri-mined-not-sold': { body: body_tri_mined_not_sold, ruBody: ruBody_tri_mined_not_sold },
'how-to-join-the-swarm': { body: body_how_to_join_the_swarm, ruBody: ruBody_how_to_join_the_swarm },
'queen-browser-actions-you-can-follow': { body: body_queen_browser_actions_you_can_follow, ruBody: ruBody_queen_browser_actions_you_can_follow },
diff --git a/apps/website/src/data/blog/types.ts b/apps/website/src/data/blog/types.ts
index 452fdbc8df..02feaf2b4c 100644
--- a/apps/website/src/data/blog/types.ts
+++ b/apps/website/src/data/blog/types.ts
@@ -7,6 +7,9 @@ export type Block =
| { kind: 'code'; text: string }
| { kind: 'table'; head: string[]; rows: string[][] }
| { kind: 'figure'; svg: string; caption: string }
+ // A recorded terminal session (asciicast v2 under public/), replayed in the page by
+ // TerminalCast; `share` is its page at /term// (made by `tri cast publish`).
+ | { kind: 'terminal'; src: string; title: string; caption: string; share?: string }
export interface PostRuMeta {
title: string
diff --git a/apps/website/src/data/casts.ts b/apps/website/src/data/casts.ts
new file mode 100644
index 0000000000..975b60e0f9
--- /dev/null
+++ b/apps/website/src/data/casts.ts
@@ -0,0 +1,29 @@
+// Recorded terminal sessions published under public/term// with `tri cast publish`.
+// Pages and posts that replay one take its text from here, so a caption is written once.
+
+export interface Cast {
+ src: string
+ share: string
+ title: string
+ caption: { en: string; ru: string }
+}
+
+export const X7_BOARD: Cast = {
+ src: 'term/x7-board/session.cast',
+ share: 'https://t27.ai/term/x7-board/',
+ title: 'tri x7-board · t27 back half on the AX7203',
+ caption: {
+ en: 'One recorded session on the AX7203, 2026-10-02: a bitstream built from t27 specs is compared byte for byte with openXC7\'s, loaded into SRAM, and checked with 51,840 of 51,840 tagged receipts from the board. Every byte printed is real; the prompt and the typing are staged, and silences over 2 s are shortened.',
+ ru: 'Одна записанная сессия на AX7203, 2026-10-02: битстрим, собранный из t27-спеков, сравнивается байт в байт с битстримом openXC7, загружается в SRAM и проверяется 51 840 из 51 840 тегированных квитанций с платы. Каждый напечатанный байт настоящий; приглашение и набор команд постановочные, паузы длиннее 2 с сокращены. Сама запись на английском.',
+ },
+}
+
+export const DEVKIT_FLOW: Cast = {
+ src: 'term/devkit-flow/session.cast',
+ share: 'https://t27.ai/term/devkit-flow/',
+ title: 'tri devkit · the FPGA flow, layer by layer',
+ caption: {
+ en: 'The recorded run: `tri devkit flow --build` and `tri devkit impact` on one XC7A200T design, 2026-10-03. Every byte printed is real and arrives when it did; the prompt and the typing are staged, and silences over 2 s are shortened, with a note in the title bar while that happens.',
+ ru: 'Записанный прогон: `tri devkit flow --build` и `tri devkit impact` на одном дизайне XC7A200T, 2026-10-03. Каждый напечатанный байт настоящий и появляется тогда, когда появился; приглашение и набор команд постановочные, паузы длиннее 2 с сокращены, и пока это происходит, в заголовке окна стоит пометка. Сама запись на английском.',
+ },
+}
diff --git a/apps/website/src/data/club.ts b/apps/website/src/data/club.ts
new file mode 100644
index 0000000000..ee7e441fcf
--- /dev/null
+++ b/apps/website/src/data/club.ts
@@ -0,0 +1,33 @@
+// One price for every agent, in one place: #/foundry, #/devkit, the agents
+// showcase and the homepage read it from here.
+//
+// Owner, 2026-10-03: every agent is sold at the same price — one Telegram
+// Stars subscription at its maximum, 10000 Stars per 30 days — and agents
+// differ by profile, not by price. USD is derived with the bot's own rate
+// (STAR_PRICE_USD in gHashTag/999-multibots-telegraf), never typed twice.
+export const STAR_PRICE_USD = 0.016
+
+export const AGENT_PRICE = {
+ stars: 10000,
+ periodDays: 30,
+}
+
+export const agentPriceUsd = Math.round(AGENT_PRICE.stars * STAR_PRICE_USD)
+
+const BOT = 'https://t.me/t27ai_bot'
+export const orderUrl = (id: string) => `${BOT}?start=agentpkg_${id}`
+
+// The Golden Foundry club is the TRI DEV profile — `tri dev`
+// (docs/docs/cli/dev.md): one GitHub issue, one service, one autonomous
+// Claude Code agent.
+export const CLUB = {
+ stars: AGENT_PRICE.stars,
+ usdPerMonth: agentPriceUsd,
+ href: '#/foundry',
+ telegram: orderUrl('tridev'),
+}
+
+const stars = AGENT_PRICE.stars.toLocaleString('en-US').replace(/,/g, ' ')
+
+export const clubPrice = (ru: boolean) => `${stars} ⭐ / ${ru ? 'мес' : 'mo'}`
+export const clubPriceUsd = (ru: boolean) => `≈ $${agentPriceUsd} / ${ru ? 'мес' : 'mo'}`
diff --git a/apps/website/src/data/devkit.ts b/apps/website/src/data/devkit.ts
new file mode 100644
index 0000000000..d328b95df9
--- /dev/null
+++ b/apps/website/src/data/devkit.ts
@@ -0,0 +1,65 @@
+// TRI DEV KIT: the numbers behind #/devkit and the devkit blog post, kept in one place.
+//
+// Source: `tri devkit flow --build` recorded at /term/devkit-flow/ (flow.json of that run,
+// 2026-10-03 11:31 +07, load 12.6 on 8 cpus). Wall seconds on one laptop: they move with
+// load, so ratios are the quotable part, not the absolute times.
+
+export const FLOW = {
+ recorded: '2026-10-03',
+ design: 'trinet_node_v2_ax7203',
+ part: 'xc7a200tfbg484-2',
+ fasmLines: 121587,
+ load: '12.6 on 8 cpus',
+ cast: 'term/devkit-flow/session.cast',
+ share: 'https://t27.ai/term/devkit-flow/',
+}
+
+export interface Layer {
+ id: string
+ name: string
+ tool: string
+ s: number
+ t27?: { tool: string; s: number; identical: boolean }
+}
+
+export const LAYERS: Layer[] = [
+ { id: 'L1', name: 'Synthesis', tool: 'yosys', s: 12.69 },
+ { id: 'L2', name: 'Place & route', tool: 'nextpnr-xilinx', s: 70.09 },
+ { id: 'L3', name: 'FASM → frames', tool: 'fasm2frames.py', s: 33.86, t27: { tool: 'bitwalk --fasm', s: 0.42, identical: true } },
+ { id: 'L4', name: 'Frames → .bit', tool: 'xc7frames2bit', s: 0.22, t27: { tool: 'bitwalk --write', s: 0.25, identical: true } },
+]
+
+export const flowBefore = LAYERS.reduce((a, l) => a + l.s, 0)
+export const flowAfter = LAYERS.reduce((a, l) => a + (l.t27 ? l.t27.s : l.s), 0)
+export const savedPerBuild = flowBefore - flowAfter
+
+/** The whole flow if one more not-yet-rewritten layer took 0 s: the most its rewrite could give. */
+export function ceiling(l: Layer): { flow: number; x: number } {
+ const flow = flowAfter - l.s
+ return { flow, x: flowBefore / flow }
+}
+
+export const WORKING_DAYS = 230
+
+export function hoursPerYear(savedS: number, buildsPerDay: number, people: number): number {
+ return (savedS * buildsPerDay * WORKING_DAYS * people) / 3600
+}
+
+// Crowd Supply campaigns of open FPGA hardware, read from their pages on 2026-10-03.
+export const CROWD = [
+ { name: 'Glasgow revC', by: '1BitSquared', href: 'https://www.crowdsupply.com/1bitsquared/glasgow', raised: 458567, backers: 2321, goal: 25000, price: '$179' },
+ { name: 'ULX3S', by: 'Radiona', href: 'https://www.crowdsupply.com/radiona/ulx3s', raised: 239437, backers: 1162, goal: 15000, price: '$155–275' },
+ { name: 'iCEBreaker', by: '1BitSquared', href: 'https://www.crowdsupply.com/1bitsquared/icebreaker-fpga', raised: 155931, backers: 1188, goal: 15000, price: '$80' },
+]
+
+// GitHub stars and forks, read through the API on 2026-10-03.
+export const FIELD = [
+ { name: 'Vivado', href: 'https://www.amd.com/en/products/software/adaptive-socs-and-fpgas/vivado.html', stars: null, what: 'The vendor flow. Closed source; the reference every open flow is compared against.' },
+ { name: 'openXC7', href: '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/openXC7/nextpnr-xilinx', stars: 71, what: 'yosys + nextpnr-xilinx + prjxray for Xilinx 7-series. The flow this kit builds on; L1 and L2 are its tools.' },
+ { name: 'F4PGA / prjxray', href: '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/f4pga/prjxray', stars: 921, what: 'The 7-series bit database and fasm2frames. Last push 2025-06-05.' },
+ { name: 'fpga-assembler', href: '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/lromor/fpga-assembler', stars: 29, what: 'A C++ rewrite of FASM-to-bitstream, active. The same L3/L4 job as bitwalk, from code rather than specs.' },
+ { name: 'LiteX', href: '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/enjoy-digital/litex', stars: 4140, what: 'SoC builder with an openXC7 backend. A user of the flow, not a replacement for a layer.' },
+ { name: 'openFPGALoader', href: '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/trabucayre/openFPGALoader', stars: 1746, what: 'The common open loader (L5). Not rewritten here.' },
+ { name: 'yosys', href: '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/YosysHQ/yosys', stars: 4785, what: 'Open synthesis (L1). 1,170 forks; a mature project with a large community.' },
+ { name: 'nextpnr', href: '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/YosysHQ/nextpnr', stars: 1763, what: 'Open place and route (L2). nextpnr-xilinx is its 7-series fork.' },
+]
diff --git a/apps/website/src/i18n/context.tsx b/apps/website/src/i18n/context.tsx
index cc507c7ed3..d31bb603f2 100644
--- a/apps/website/src/i18n/context.tsx
+++ b/apps/website/src/i18n/context.tsx
@@ -3,18 +3,14 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import en from '../../messages/en.json';
-const LANGS = ['en', 'ru', 'de', 'zh', 'es'] as const;
+const LANGS = ['en', 'ru'] as const;
type Lang = typeof LANGS[number];
// English stays static: it is the default and the fallback base for deepMerge,
-// so it has to be there on the first render. The other four were static imports
-// too, which put all five catalogues — 356 kB of JSON — in the entry chunk and
-// made every visitor download four languages they had not asked for.
+// so it has to be there on the first render. Russian is loaded on demand, so a
+// reader of English does not download the Russian catalogue.
const loaders: Record, () => Promise<{ default: any }>> = {
ru: () => import('../../messages/ru.json'),
- de: () => import('../../messages/de.json'),
- zh: () => import('../../messages/zh.json'),
- es: () => import('../../messages/es.json'),
};
interface I18nContextType {
@@ -65,21 +61,6 @@ const SEO = {
title: 'TRINITY | TNF · GFTernary — референсная пара форматов для тернарного датапути',
description: 'TNF и GFTernary: референсная пара числовых форматов для тернарного датапути. 52 теоремы, и каждое аппаратное число измерено на открытом потоке на XC7A200T.',
},
- de: {
- locale: 'de_DE',
- title: 'TRINITY | TNF · GFTernary — Referenzformate für den ternären Datenpfad',
- description: 'TNF und GFTernary: ein Referenzpaar von Zahlenformaten für den ternären Datenpfad. 52 Theoreme, und jede Hardwarezahl auf einem offenen Flow auf XC7A200T gemessen.',
- },
- es: {
- locale: 'es_ES',
- title: 'TRINITY | TNF · GFTernary — formatos de referencia para la ruta de datos ternaria',
- description: 'TNF y GFTernary: un par de formatos numéricos de referencia para la ruta de datos ternaria. 52 teoremas, y cada número de hardware medido en un flujo abierto sobre XC7A200T.',
- },
- zh: {
- locale: 'zh_CN',
- title: 'TRINITY | TNF · GFTernary — 三进制数据通路的参考数字格式',
- description: 'TNF 与 GFTernary:面向三进制数据通路的一对参考数字格式。52 条定理,所有硬件数据均在 XC7A200T 上以开源流程实测。',
- },
} as const
export function I18nProvider({ children }: { children: React.ReactNode }) {
diff --git a/apps/website/src/index.css b/apps/website/src/index.css
index 94324181cb..ab22314d42 100644
--- a/apps/website/src/index.css
+++ b/apps/website/src/index.css
@@ -117,6 +117,35 @@ section {
min-height: 60vh;
}
+/* A section centres its children and lets them shrink to fit, so one wide
+ child (a terminal recording is ~104 columns) widened the whole column past a
+ phone screen. The inner column takes the section's width and lets wide
+ children scroll inside themselves instead. */
+.section-inner {
+ width: 100%;
+ min-width: 0;
+}
+
+.section-inner.narrow {
+ max-width: 860px;
+}
+
+.section-inner > * {
+ max-width: 100%;
+ min-width: 0;
+}
+
+@media (max-width: 640px) {
+ .foundry-cta {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .foundry-cta .btn {
+ text-align: center;
+ }
+}
+
/* A list is read down a common left edge; centring destroys that edge. The rule
above centres everything in a section, so any list that did not happen to sit
inside a left-aligned wrapper came out ragged — the marker pinned at the flex
diff --git a/apps/website/src/lib/queenModules.ts b/apps/website/src/lib/queenModules.ts
index 4e11571946..c4bf099012 100644
--- a/apps/website/src/lib/queenModules.ts
+++ b/apps/website/src/lib/queenModules.ts
@@ -64,13 +64,13 @@ export const MODULES = [
glyph: '▦',
en: {
name: 'KANBAN',
- hint: 'Operational columns',
+ hint: 'The board in six columns',
body: 'The board as six columns — backlog, blocked, running, in review, done, dropped — with the issue cards themselves, each carrying its number and how many acceptance criteria it states.',
play: 'Where you read the contract. The acceptance criteria a card states are what a review will hold you to; a card that states none is a card nobody can be judged against.',
},
ru: {
name: 'КАНБАН',
- hint: 'Операционные колонки',
+ hint: 'Доска в шесть колонок',
body: 'Доска шестью колонками — бэклог, заблокировано, в работе, на ревью, готово, отложено — с самими карточками задач: номер и сколько критериев приёмки в ней заявлено.',
play: 'Место, где читают договор. Критерии приёмки на карточке — это то, по чему вас будет судить ревью; карточка без них — карточка, по которой судить нечем.',
},
@@ -166,13 +166,13 @@ export const MODULES = [
glyph: 'Ω',
en: {
name: 'AGENTS',
- hint: 'The 27-letter alphabet, each agent stated by a .t27 spec',
+ hint: 'Who holds the skills',
body: 'The Agent Explorer, embedded whole. Level by level the system is built: Specs state what exists, Skills state what a run does, Crons state what starts a run, and Agents — the fourth layer — state who holds the skills, under which law (SOUL.md, AGENTS.md), with which entry and exit invariant. Twenty-seven letters, A to Z and the reserved Ti, each with its spec first, then its skills, its derived crons, and its experience joined from the episode log by evidence: an agent no episode names says so.',
play: 'Where the players on the other side are named. Twenty-seven letters, each with the law it works under; an agent no episode names has no recorded experience yet, and says so itself.',
},
ru: {
name: 'АГЕНТЫ',
- hint: 'Алфавит из 27 букв, каждый агент заявлен спекой .t27',
+ hint: 'Кто держит скиллы',
body: 'Обозреватель агентов целиком. Уровень за уровнем мы создаём систему: спеки говорят, что существует, скиллы — что делает запуск, кроны — что его запускает, а агенты — четвёртый слой — кто держит скиллы, под каким законом (SOUL.md, AGENTS.md), с каким входным и выходным инвариантом. Двадцать семь букв, от A до Z и зарезервированная Ti, у каждой сначала спека, затем её скиллы, выведенные кроны и опыт, присоединённый из журнала эпизодов по свидетельствам: агент, которого не называет ни один эпизод, говорит об этом сам.',
play: 'Место, где названы игроки с той стороны. Двадцать семь букв, у каждой свой закон; агент, которого не называет ни один эпизод, ещё не имеет записанного опыта — и говорит об этом сам.',
},
diff --git a/apps/website/src/main.tsx b/apps/website/src/main.tsx
index a374dc4eb7..0b95bd8e5c 100644
--- a/apps/website/src/main.tsx
+++ b/apps/website/src/main.tsx
@@ -54,7 +54,7 @@ handExplorerLinksToQueen()
const view = params.get('view')
if (!leaving && /^#\/queen(?:\?|$)/.test(hash) && params.get('embed') !== '1' && !params.has('repo') && view !== 'atlas' && view !== 'core') {
const asked = new URLSearchParams(window.location.search).get('lang')
- let lang = asked && ['en', 'ru', 'de', 'zh', 'es'].includes(asked) ? asked : null
+ let lang = asked && ['en', 'ru'].includes(asked) ? asked : null
if (!lang) {
try {
lang = window.localStorage.getItem('trinity-lang')
@@ -88,6 +88,7 @@ const Trinity = lazy(() => import('./pages/Trinity.tsx'))
const AboutAuthor = lazy(() => import('./pages/AboutAuthor.tsx'))
const Resources = lazy(() => import('./pages/Resources.tsx'))
const Foundry = lazy(() => import('./pages/Foundry.tsx'))
+const DevKit = lazy(() => import('./pages/DevKit.tsx'))
const Queen = lazy(() => import('./pages/QueenUniverse.tsx'))
// The PASSPORT: the record proposed to the OCP neuromorphic working group, and
// the three measured cases behind it. One component, two faces.
@@ -152,6 +153,7 @@ createRoot(document.getElementById('root')!).render(
{/* Клуб. /club — короткий синоним для ссылок в рилсах и профиле. */}
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/website/src/pages/Blog.tsx b/apps/website/src/pages/Blog.tsx
index 04d1594ec1..938b353853 100644
--- a/apps/website/src/pages/Blog.tsx
+++ b/apps/website/src/pages/Blog.tsx
@@ -3,6 +3,7 @@ import { Link, useParams } from 'react-router-dom'
import Navigation from '../components/Navigation'
import Footer from '../components/Footer'
import Cover from '../components/BlogCover'
+import TerminalCast from '../components/TerminalCast'
import { publishedPosts, postBySlug } from '../data/blog/index'
import type { Block, Post, PostBody, PostMeta } from '../data/blog/types'
import { useI18n } from '../i18n/context'
@@ -374,6 +375,8 @@ function renderBlock(b: Block, i: number) {
)
+ case 'terminal':
+ return
case 'table':
return (
diff --git a/apps/website/src/pages/DevKit.tsx b/apps/website/src/pages/DevKit.tsx
new file mode 100644
index 0000000000..4cc7bb6a6d
--- /dev/null
+++ b/apps/website/src/pages/DevKit.tsx
@@ -0,0 +1,319 @@
+import { useState } from 'react'
+import { clubPrice } from '../data/club'
+import { motion } from 'framer-motion'
+import { usePageMeta } from '../hooks/usePageMeta'
+import Navigation from '../components/Navigation'
+import Footer from '../components/Footer'
+import QuantumBackground from '../components/QuantumBackground'
+import TerminalCast from '../components/TerminalCast'
+import { FLOW, LAYERS, flowBefore, flowAfter, savedPerBuild, ceiling, hoursPerYear, WORKING_DAYS, CROWD, FIELD } from '../data/devkit'
+
+/**
+ * TRI DEV KIT: the open Xilinx 7-series flow, rewritten layer by layer from t27 specs.
+ *
+ * A proposal page. Every timing comes from one recorded build (src/data/devkit.ts);
+ * prices and revenue are drafts and scenarios and say so on the page.
+ */
+
+const CONTACT = { email: 'admin@t27.ai', telegram: 'https://t.me/t27ai_bot?start=devkit' }
+
+/** Prices and kit contents are not confirmed. While true, the page says so above them. */
+const DRAFT_TERMS = true
+
+const ROADMAP = [
+ { id: 'L0', name: 'Chip database', state: 'partly', body: 'Three t27 specs already hold frame addressing, bit positions and packet words for the 7-series. The rest of prjxray-db (tile grid, routing graph) is still read from its JSON.' },
+ { id: 'L1', name: 'Synthesis', state: 'not started', body: 'yosys is mature and large. A spec-driven technology mapper for 7-series primitives is possible, but the whole layer is 11% of a build, so even an instant one gives at most 1.65× on the flow.' },
+ { id: 'L2', name: 'Place & route', state: 'not started', body: '60% of a build, and the biggest lever: at 0 s the flow would be 8.75× faster. Also the hardest. The realistic path is upstream work on nextpnr-xilinx (we already send patches there) plus spec-checked timing and routing rules, not a rewrite from zero.' },
+ { id: 'L3', name: 'FASM → frames', state: 'done', body: 'bitwalk --fasm, built from the specs. Byte-identical to fasm2frames on 22 of 22 corpus files; 33.9 s → 0.42 s on this build.' },
+ { id: 'L4', name: 'Frames → .bit', state: 'done', body: 'bitwalk --write. Byte-identical to xc7frames2bit on 16 of 16 frame files. Not faster: 0.25 s against 0.22 s, a tie.' },
+ { id: 'L5', name: 'Loader', state: 'not started', body: 'openocd / openFPGALoader. The SRAM load of this design on the AX7203 took 16.7 s, outside the 116.9 s above.' },
+ { id: 'L6', name: 'Board receipts', state: 'done for one design', body: 'tri x7-board receipts: the loaded design answers tagged requests. 403,200 of 403,200 on trinet node 0. One design, one board.' },
+]
+
+const OPTIONS = [
+ { name: 'Open core', price: 'Free', body: 'The tri CLI, the t27 specs and bitwalk under Apache-2.0. Anyone can check the layer table above on their own machine.' },
+ { name: 'Kit', price: '$149–199 (draft)', body: 'An Artix-7 board with the flow preinstalled, a receipts harness and one design that proves itself on first power-up. Sold through Crowd Supply, priced against the campaigns below.', featured: true },
+ { name: 'Club', price: clubPrice(false), body: 'The Golden Foundry club: a TRI DEV developer agent on your own GitHub issue, plus remote runs on live boards and conformance receipts for your design.' },
+]
+
+function Section({ title, children, narrow }: { title: string; children: React.ReactNode; narrow?: boolean }) {
+ return (
+
+
+ {`If ${l.id} ${l.name.toLowerCase()} took 0 s: at most ${hoursPerYear(flowAfter - c.flow, builds, people).toFixed(1)} more h / year, flow ${flowBefore.toFixed(1)} s → ${c.flow.toFixed(1)} s (${c.x.toFixed(2)}×). A ceiling, not a result.`}
+
+ )
+ })}
+
+
+ )
+}
+
+export default function DevKit() {
+ usePageMeta('TRI DEV KIT', 'The open Xilinx 7-series flow, rewritten layer by layer from t27 specs: measured per-layer times, byte-identical replacements, Amdahl ceilings and a business model proposal.')
+ const usd = (n: number) => `$${n.toLocaleString('en-US')}`
+
+ return (
+ <>
+
+
+
+
+
+
+
+ ▼ Trinity S³AI · proposal
+
+
+ TRI DEV KIT
+
+ the FPGA flow, layer by layer
+
+
+ {`An open Xilinx 7-series flow where each layer is either rewritten from t27 specs and byte-compared with the tool it replaces, or marked as not yet. On one real XC7A200T build, the two layers done so far take the flow from ${flowBefore.toFixed(1)} s to ${flowAfter.toFixed(1)} s with an identical bitstream. Place and route is 60% of what is left.`}
+
+ {`Design ${FLOW.design}, part ${FLOW.part}, ${FLOW.fasmLines.toLocaleString('en-US')} FASM lines, recorded ${FLOW.recorded} with \`tri devkit flow --build\`. Wall seconds on one laptop at a load of ${FLOW.load}; they move with load, the ratios less so. The t27 times are the best of 3 runs right after the build.`}
+
+
+
+
+
+
+
+
+
+
+
+ Amdahl ceilings: how fast the whole flow would be if one more layer took no time at all. A rewrite cannot beat its ceiling, and most will land well below it.
+
+ The last column is a scenario, not a forecast: the same number of backers at a draft $179, before the platform fee, manufacturing and shipping. All three campaigns had a known board and community before launch; this kit has neither yet.
+
+
+
+
+ >
+ )
+}
diff --git a/apps/website/src/pages/Foundry.tsx b/apps/website/src/pages/Foundry.tsx
index 6a4341f100..f3d8b18068 100644
--- a/apps/website/src/pages/Foundry.tsx
+++ b/apps/website/src/pages/Foundry.tsx
@@ -5,6 +5,10 @@ import { useI18n } from '../i18n/context'
import Navigation from '../components/Navigation'
import Footer from '../components/Footer'
import QuantumBackground from '../components/QuantumBackground'
+import TerminalCast from '../components/TerminalCast'
+import { X7_BOARD } from '../data/casts'
+import { CLUB, clubPrice } from '../data/club'
+import AgentShowcase from '../components/AgentShowcase'
/**
* Golden Foundry — платный клуб разработчиков на кремнии.
@@ -17,36 +21,18 @@ import QuantumBackground from '../components/QuantumBackground'
*/
// Клубный бот — @t27ai_bot (выделен владельцем под клуб 2026-08-22)
-const CONTACT = { email: 'admin@t27.ai', telegram: 'https://t.me/t27ai_bot?start=foundry' }
+const CONTACT = { email: 'admin@t27.ai', telegram: CLUB.telegram }
/**
- * УСЛОВИЯ НЕ ПОДТВЕРЖДЕНЫ ВЛАДЕЛЬЦЕМ.
- * Суммы ниже — черновик по логике /course. Пока DRAFT_TERMS=true, читателю
- * показывается предупреждение. Продавать вход по выдуманной цене — та же
- * ошибка, что печатать в блоге неизмеренное число.
+ * Цена — владелец, 2026-10-03: все агенты по одной цене, подписка Telegram
+ * Stars на максимуме (10000 ⭐ / 30 дней, data/club.ts); клуб — это профиль
+ * агента-разработчика TRI DEV. Один уровень вместо трёх черновых.
+ * Описание агента — только то, что делает `tri dev` (docs/docs/cli/dev.md):
+ * одна задача GitHub — один сервис на Railway — один автономный агент
+ * Claude Code. Ничего сверх этого не обещать.
*/
-const DRAFT_TERMS = true
-
-const TIERS = [
- {
- ru: { name: 'Подмастерье', price: '$29 / мес', body: 'Закрытый канал, еженедельные разборы замеров, KAT-векторы и репозитории до публикации.' },
- en: { name: 'Apprentice', price: '$29 / mo', body: 'Private channel, weekly measurement teardowns, KAT vector sets and repositories before they go public.' },
- stars: 1499,
- },
- {
- ru: { name: 'Мастер', price: '$79 / мес', body: 'Всё выше, плюс удалённые прогоны на живых платах Artix-7 и разбор вашего RTL раз в месяц.' },
- en: { name: 'Journeyman', price: '$79 / mo', body: 'Everything above, plus remote runs on live Artix-7 boards and a monthly teardown of your own RTL.' },
- stars: 3999,
- featured: true,
- },
- {
- ru: { name: 'Литейщик', price: '$249 / мес', body: 'Всё выше, плюс сопровождение вашего дизайна до тейпаута и совместная публикация замеров.' },
- en: { name: 'Founder', price: '$249 / mo', body: 'Everything above, plus your design walked to tape-out and co-published measurements.' },
- stars: 12499,
- },
-]
-
const INSIDE = [
+ { ru: ['Агент TRI DEV', 'Автономный агент-разработчик на одной вашей задаче: от спеки до pull request, каждый шаг — комментарием в задаче.'], en: ['A TRI DEV agent', 'An autonomous developer agent on one of your issues: from spec to pull request, every step a comment on the issue.'] },
{ ru: ['Разбор чужих замеров', 'Каждую неделю берём один опубликованный бенчмарк и проверяем, выдерживает ли он собственную методику.'], en: ['Teardowns of published numbers', 'Each week we take one published benchmark and check whether it survives its own methodology.'] },
{ ru: ['Свой стенд', 'Удалённый доступ к живым платам: присылаете RTL — получаете измерение, а не симуляцию.'], en: ['A bench of your own', 'Remote access to live boards: send RTL, get a measurement rather than a simulation.'] },
{ ru: ['Право первым проверить', 'Методы и векторы попадают в клуб раньше блога — вместе с тем, что ещё не сошлось.'], en: ['First right to check', 'Methods and vectors reach the club before the blog — including the parts that do not yet agree.'] },
@@ -58,11 +44,13 @@ const NOT_THIS = {
'Это не курс. Курс отдельно, на /course, и в клуб он не входит.',
'Это не гарантия тейпаута: кремний зависит от шаттла, а не от подписки.',
'Это не чат «вопрос — ответ»: работа идёт по замерам, которые вы приносите.',
+ 'Агент не сливает сам: каждое изменение приходит pull request’ом, решение за вами.',
],
en: [
'This is not the course. The course lives at /course and is not bundled here.',
'This is not a tape-out guarantee: silicon depends on a shuttle, not a subscription.',
'This is not a Q&A chat: the work is driven by measurements you bring.',
+ 'The agent does not merge on its own: every change arrives as a pull request, and the decision is yours.',
],
}
@@ -75,30 +63,26 @@ const WHO = [
const RU = {
eyebrow: 'Закрытый клуб · набор волнами',
h1pre: 'Клуб разработчиков на кремнии',
- lede: 'Закрытый круг тех, кто проверяет свои числа на живом кремнии. Внутри — разбор замеров, доступ к стенду и право первым увидеть метод до публикации.',
+ lede: `Закрытый круг тех, кто проверяет свои числа на живом кремнии. ${clubPrice(true)}: свой агент-разработчик TRI DEV на вашей задаче, разбор замеров, доступ к стенду и право первым увидеть метод до публикации.`,
cta: 'Вступить через Telegram',
ctaAlt: 'Сначала блог',
seatNote: 'Беру столько людей, скольким успеваю читать RTL лично. Оплата — в боте, картой или звёздами Telegram.',
insideTitle: 'Что внутри',
- tiersTitle: 'Уровни',
notTitle: 'Чем это не является',
whoTitle: 'Кому подойдёт',
- draftNote: 'Условия уточняются: суммы — черновик, окончательные будут подтверждены до открытия набора.',
join: 'Вступить',
}
const EN = {
eyebrow: 'Private club · intake in waves',
h1pre: 'A club for people who build on silicon',
- lede: 'A closed circle of people who check their numbers on live silicon. Inside: measurement teardowns, bench access, and first sight of methods before they are published.',
+ lede: `A closed circle of people who check their numbers on live silicon. ${clubPrice(false)}: your own TRI DEV developer agent on your issue, measurement teardowns, bench access, and first sight of methods before they are published.`,
cta: 'Join via Telegram',
ctaAlt: 'Read the blog first',
seatNote: 'I take as many people as I can personally read RTL for. Payment in the bot — card or Telegram Stars.',
insideTitle: 'What is inside',
- tiersTitle: 'Tiers',
notTitle: 'What this is not',
whoTitle: 'Who it fits',
- draftNote: 'Terms are being finalised: the figures are a draft and will be confirmed before intake opens.',
join: 'Join',
}
@@ -132,7 +116,7 @@ export default function Foundry() {
{ru ? 'Как выглядит прогон на стенде' : 'What a bench run looks like'}
+
+
+
{/* КОМУ ПОДОЙДЁТ */}
diff --git a/apps/website/src/pages/HardwareVerification.tsx b/apps/website/src/pages/HardwareVerification.tsx
index ad5e9a0993..a95d0221f5 100644
--- a/apps/website/src/pages/HardwareVerification.tsx
+++ b/apps/website/src/pages/HardwareVerification.tsx
@@ -13,6 +13,8 @@ import ExampleReport from '../components/ExampleReport'
import ConformanceEvidence from '../components/ConformanceEvidence'
import SignalHealth from '../components/SignalHealth'
import { TnfFrontier, TnfReproduce } from '../components/sections/tnf'
+import TerminalCast from '../components/TerminalCast'
+import { X7_BOARD } from '../data/casts'
const THEOREM_GROUPS = [
{
@@ -680,6 +682,9 @@ export default function HardwareVerification() {
arXiv:2606.09686
+
+
+
{/* Related pages. The header dock only carries one link to this service,
diff --git a/apps/website/src/pages/Queen.css b/apps/website/src/pages/Queen.css
index 3f5933d046..07699fcef4 100644
--- a/apps/website/src/pages/Queen.css
+++ b/apps/website/src/pages/Queen.css
@@ -817,8 +817,7 @@
}
.queen27-card:hover {
- border-color: rgba(0, 255, 136, 0.5);
- background: rgba(0, 255, 136, 0.04);
+ border-color: rgba(255, 255, 255, 0.22);
}
.is-running .queen27-card {
@@ -901,9 +900,11 @@
box-shadow: none;
}
-/* The tint is an inset shadow and not a border, so turning it on moves nothing:
- a filter that reflows the board costs the reader the place they were looking
- at. 7% keeps the card a dark card — this is a hue, not a highlight.
+/* The card is black. It carried a 7% wash of its direction colour, and on a
+ board that is mostly code that read as a green board: the owner asked for the
+ cards black again, 2026-10-03. Direction keeps three signs that are not a
+ surface — the dot, the tag in words, and the inset rule down the right edge,
+ which is a shadow and not a border, so it moves nothing.
THE SECOND SELECTOR IS NOT A DUPLICATE. The shell layout states
`.queen27-page.is-shell .queen27-card { background: transparent }` far below,
@@ -913,21 +914,17 @@
and the right edge rendered and the colour did not. Measured in the browser,
not guessed. Naming the shell explicitly wins the cascade whatever the source
order, and qa/queen-direction-contract.mjs fails the build if this selector
- goes missing again.
-
- It also keeps the shell's argument intact. The mix is against `transparent`,
- not against the old card's white: this adds ONE layer, the hue itself at 7%,
- over whatever ground the layout chose. It is not a second veil. */
+ goes missing again, or if a direction colour comes back as a card surface. */
.queen27-card[data-dir],
.queen27-page.is-shell .queen27-card[data-dir] {
- background: color-mix(in srgb, var(--queen-dir) 7%, transparent);
+ background: #000;
box-shadow: inset -3px 0 0 var(--queen-dir);
}
.queen27-card[data-dir]:hover,
.queen27-page.is-shell .queen27-card[data-dir]:hover {
border-color: var(--queen-dir);
- background: color-mix(in srgb, var(--queen-dir) 16%, transparent);
+ background: #000;
}
/* The topline was a one-line row with the issue number at the left and, on the
@@ -1232,7 +1229,7 @@
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
- background: linear-gradient(90deg, rgba(0, 255, 136, 0.06), transparent 48%);
+ background: #000;
}
.queen27-factory-command > div {
@@ -2583,7 +2580,7 @@
.queen27-worker-slots .is-busy {
border-color: rgba(0, 255, 136, 0.52);
color: var(--q-green);
- background: rgba(0, 255, 136, 0.05);
+ background: #000;
}
.queen27-worker-slots .is-busy i {
@@ -7185,7 +7182,8 @@ body.queen-shell .queen-starfield-source {
}
.queen27-page.is-shell .queen27-card:hover {
- background: rgba(0, 255, 136, 0.05);
+ background: #000;
+ border-color: rgba(255, 255, 255, 0.22);
}
/* And it speaks in the same hand: the titles were the page's proportional face
diff --git a/apps/website/src/pages/Queen.tsx b/apps/website/src/pages/Queen.tsx
index 42b5f30c64..46bb1bcfd3 100644
--- a/apps/website/src/pages/Queen.tsx
+++ b/apps/website/src/pages/Queen.tsx
@@ -384,7 +384,7 @@ const COPY = {
cronsDirectiveBody:
"A scheduled job exists when a .t27 spec under specs/crons states it: host, schedule, what it runs, what happens on failure, whether it is on. The workflow, Inngest function or timer the sync script found is the witness. “Run now” goes to the host that owns the job; a timer inside a process has no outside handle and the card says so. No live control plane is deployed.",
agentsView: "AGENTS",
- agentsHint: "The 27-letter alphabet, each agent stated by a .t27 spec",
+ agentsHint: "Who holds the skills",
agentsDirective: "AGENTS ARE SPECS",
agentsDirectiveBody:
"Level by level the system is built: Specs → Skills → Crons → Agents. An agent exists when a .t27 spec under specs/agents states it — its letter, domain, archetype, the skills it holds, its entry and exit invariant — bound by SOUL.md and AGENTS.md at the repository root. Its crons are derived from the crons' RUNS, never listed by hand. Its experience is joined from the episode log only when an episode names its letter; an agent no episode names says so, and the episodes that name no one are counted as unattributed, not assigned.",
@@ -826,7 +826,7 @@ const COPY = {
cronsDirectiveBody:
"Задание по расписанию существует, когда его заявляет спека .t27 в specs/crons: хост, расписание, что запускает, что при сбое, включено ли. Workflow, функция Inngest или таймер, найденные скриптом синхронизации, — свидетель. «Запустить сейчас» ведёт к хосту, которому задание принадлежит; у таймера внутри процесса внешней ручки нет, и карточка так и говорит. Живой контур управления не развёрнут.",
agentsView: "АГЕНТЫ",
- agentsHint: "Алфавит из 27 букв, каждый агент заявлен спекой .t27",
+ agentsHint: "Кто держит скиллы",
agentsDirective: "АГЕНТЫ — ЭТО СПЕКИ",
agentsDirectiveBody:
"Уровень за уровнем мы создаём систему: спеки → скиллы → кроны → агенты. Агент существует, когда его заявляет спека .t27 в specs/agents — буква, домен, архетип, скиллы, которые он держит, входной и выходной инвариант, — под законом SOUL.md и AGENTS.md в корне репозитория. Его кроны выводятся из RUNS кронов, а не пишутся руками. Его опыт присоединяется из журнала эпизодов только когда эпизод называет его букву; агент, которого не называет ни один эпизод, говорит об этом сам, а эпизоды без имени считаются неатрибутированными, а не приписываются.",
diff --git a/apps/website/src/styles/card.css b/apps/website/src/styles/card.css
index 98464fbafd..120632a878 100644
--- a/apps/website/src/styles/card.css
+++ b/apps/website/src/styles/card.css
@@ -8,11 +8,13 @@
tenth, a 135deg white gradient two steps darker than the border, and a
radius. That is the card restored here, written once so the four blocks
that show cards cannot drift apart again.
- Green stays where it belongs: on the numbers, the links, the figures. */
+ Green stays where it belongs: on the numbers, the links, the figures.
+ 2026-10-03 the owner asked for the cards black, so the glass went too: the
+ ground is #000 and the card is drawn by its border alone. */
.site-card {
- border: 1px solid rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.018));
+ background: #000;
padding: clamp(1.25rem, 2.8vw, 1.9rem);
transition: border-color 160ms ease, background 160ms ease;
}
@@ -23,8 +25,8 @@
a.site-card:hover,
a.site-card:focus-visible,
.site-card:has(> a:hover) {
- border-color: rgba(255, 255, 255, 0.22);
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
+ border-color: rgba(255, 255, 255, 0.32);
+ background: #000;
}
a.site-card:focus-visible {