diff --git a/.github/workflows/website-checks.yml b/.github/workflows/website-checks.yml index 3746cb49ea..b30976b468 100644 --- a/.github/workflows/website-checks.yml +++ b/.github/workflows/website-checks.yml @@ -363,6 +363,12 @@ jobs: - name: Queen WARS experiment ledger run: npm run check:wars && npm run test:wars-spec + # The WIDGETS tab: specs/widgets/gallery.t27 names what a reader can lift + # out and share; the generator reads each widget's own page for its words + # and card, so a widget whose page is gone fails here. + - name: Queen WIDGETS gallery + run: npm run check:widgets && npm run test:widgets-spec + # The IGLA board on WARS: specs/queen/igla_board.t27 is generated in # gHashTag/igla-coder-gpu from committed artifacts. The gate recompiles it, # re-derives every McNemar p from its discordants, and proves the public diff --git a/apps/website/package.json b/apps/website/package.json index 4f55d17b93..29c0fc1a03 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -6,7 +6,7 @@ "scripts": { "check:queen-evidence": "node scripts/queen-evidence-from-spec.mjs --check", "predev": "node scripts/queen-evidence-from-spec.mjs --check", - "prebuild:ci": "npm run core -- index && node scripts/skills-core.mjs index && node scripts/agents-from-specs.mjs && node scripts/docs-from-specs.mjs && node scripts/viewport-from-spec.mjs && node scripts/onboarding-from-spec.mjs && node scripts/play-from-spec.mjs && node scripts/queen-wars-from-spec.mjs && node scripts/igla-board-from-spec.mjs && node scripts/queen-evidence-from-spec.mjs --check", + "prebuild:ci": "npm run core -- index && node scripts/skills-core.mjs index && node scripts/agents-from-specs.mjs && node scripts/docs-from-specs.mjs && node scripts/viewport-from-spec.mjs && node scripts/onboarding-from-spec.mjs && node scripts/play-from-spec.mjs && node scripts/queen-wars-from-spec.mjs && node scripts/widgets-from-spec.mjs && node scripts/igla-board-from-spec.mjs && node scripts/queen-evidence-from-spec.mjs --check", "atlas": "node --experimental-strip-types scripts/universe-atlas.mjs", "discover": "node scripts/discover-t27-worlds.mjs", "check:discovery": "node --experimental-strip-types qa/t27-world-discovery.mjs", @@ -21,7 +21,7 @@ "check:traffic-arrival": "node --experimental-strip-types qa/traffic-arrival-contract.mjs", "core": "node scripts/spec-core.mjs", "check:shared-core": "node --experimental-strip-types qa/shared-spec-core.mjs", - "prebuild": "npm run core -- index && node scripts/skills-core.mjs index && node scripts/agents-from-specs.mjs && node scripts/docs-from-specs.mjs && node scripts/viewport-from-spec.mjs && node scripts/onboarding-from-spec.mjs && node scripts/play-from-spec.mjs && node scripts/queen-wars-from-spec.mjs && node scripts/igla-board-from-spec.mjs", + "prebuild": "npm run core -- index && node scripts/skills-core.mjs index && node scripts/agents-from-specs.mjs && node scripts/docs-from-specs.mjs && node scripts/viewport-from-spec.mjs && node scripts/onboarding-from-spec.mjs && node scripts/play-from-spec.mjs && node scripts/queen-wars-from-spec.mjs && node scripts/widgets-from-spec.mjs && node scripts/igla-board-from-spec.mjs", "dev": "vite", "build": "vite build", "build:ci": "vite build", @@ -117,6 +117,8 @@ "test:play-spec": "node --test scripts/play-from-spec.test.mjs", "check:wars": "node scripts/queen-wars-from-spec.mjs --check && node --experimental-strip-types qa/queen-wars-contract.mjs", "test:wars-spec": "node --test scripts/queen-wars-from-spec.test.mjs", + "check:widgets": "node scripts/widgets-from-spec.mjs --check", + "test:widgets-spec": "node --test scripts/widgets-from-spec.test.mjs", "check:igla-board": "node scripts/igla-board-from-spec.mjs --check && node qa/igla-board-contract.mjs", "check:roadmap-game": "node --experimental-strip-types qa/roadmap-game-contract.mjs", "check:og-preview": "node --experimental-strip-types qa/og-preview-contract.mjs", diff --git a/apps/website/public/widgets/gallery.t27 b/apps/website/public/widgets/gallery.t27 new file mode 100644 index 0000000000..768f32eee5 --- /dev/null +++ b/apps/website/public/widgets/gallery.t27 @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/gallery.t27 -- the widget gallery: what of t27 a person can lift out and share +; Source of truth for apps/website/scripts/widgets-from-spec.mjs (gHashTag/trinity), which +; compiles this file with the vendored compiler (public/t27/t27_compiler.wasm), checks the +; constant schema, evaluates every test block below, reads each widget's own share page for its +; title, description and card image, and only then writes src/lib/queenWidgets.generated.ts (the +; WIDGETS tab on the Queen board, key v) and a byte-identical copy of this file at +; public/widgets/gallery.t27. +; ASCII only (L3), English only (LANG-EN). + +; WHAT A WIDGET IS: something that already runs on t27.ai and has a page of its own -- a +; terminal recording of a real command, or the player that compiles a spec inside the reader's +; browser -- picked because an FPGA engineer, a compiler person or a player of the agent game +; would want to show it to someone else. The gallery adds no new claim: the title, the line under +; it and the card are read from the widget's page, so a widget cannot say here what its page does +; not say. A widget whose page is missing fails the build. + +; WHAT THE GALLERY GIVES A READER, per widget: the page; a live preview, loaded only on a click; +; the embed code (an iframe for a site, a linked GIF or card in Markdown for a README); and share +; links for X and Telegram. Every one of those is the reader's own click. The page posts nothing, +; sets no cookie and sends nothing anywhere. + +; WHAT IS DELIBERATELY ABSENT: a widget built for the gallery alone. IDEAS lists what engineers +; ask for that t27.ai does not draw yet; they are shown as ideas, with the data each would read, +; and never as a working widget. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_gallery; + +pub const KIND : str = "widgets"; +pub const ID : str = "widgets/gallery"; +pub const NAME : str = "t27 widgets"; +pub const SCHEMA_VERSION : u8 = 1; +; The derived files the generator rewrites (relative to apps/website). +pub const GENERATED : [2]str = ["src/lib/queenWidgets.generated.ts", "public/widgets/gallery.t27"]; + +; --- Where the widgets live ------------------------------------------------------------------ +; Every address a reader copies is absolute: an embed pasted into somebody's README must not +; depend on where the gallery was opened. The preview inside the gallery is relative, so the +; tab works the same on t27.ai and under app.t27.ai/game/. +pub const ORIGIN : str = "https://t27.ai/"; +pub const SITE_HANDLE : str = "@t27_dev"; +pub const HASHTAGS : [3]str = ["t27", "FPGA", "opensource"]; +; Embed size. The player is 16:9, as its card is; a recording page needs more height than that +; for its controls. +pub const EMBED_WIDTH : u16 = 640; +pub const PLAYER_EMBED_HEIGHT : u16 = 360; +pub const CAST_EMBED_HEIGHT : u16 = 460; +; The tab names a widget's page in the Queen board as ORIGIN, then this, then the view. +pub const TAB_PAGE : str = "#/queen?tab="; +pub const POSTS_NOTHING : bool = true; + +; --- Categories ------------------------------------------------------------------------------ +pub const CATEGORY_COUNT : u8 = 3; +pub const CATEGORY_IDS : [3]str = ["fpga", "compiler", "game"]; +pub const CATEGORY_NAMES : [3]str = ["FPGA bench", "Compiler", "Agent game"]; +pub const CATEGORY_LINES : [3]str = ["Bitstreams, timing, JTAG and pins, recorded on the bench as the commands ran.", "One spec, seven backends, compiled in the reader's browser by the same WebAssembly compiler.", "The Queen's loop and the arena where agent configurations are compared on real issues."]; + +; --- Kinds ----------------------------------------------------------------------------------- +; cast: a terminal recording, page at term// (session.gif, card.png, meta.json beside it). +; player: the X player, page at play//[/]; its twitter:player is the embed. +; tab: a view of the Queen board; WIDGET_PAGES holds the view's name. +pub const KINDS : [3]str = ["cast", "player", "tab"]; + +; --- The widgets ----------------------------------------------------------------------------- +; Parallel arrays, WIDGET_COUNT long. WIDGET_TITLES is empty wherever the page carries an +; og:title, which is every kind but tab: the page is the one place the words live. +pub const WIDGET_COUNT : u8 = 18; +pub const WIDGET_IDS : [18]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts"]; +pub const WIDGET_KINDS : [18]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "player", "player", "player", "cast", "cast", "tab", "cast", "cast", "cast"]; +pub const WIDGET_CATEGORIES : [18]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game"]; +pub const WIDGET_PAGES : [18]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/"]; +pub const WIDGET_TITLES : [18]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", ""]; +pub const WIDGET_LINES : [18]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", ""]; +; Why a person would pass this one on: the reason the widget is in the gallery at all. +pub const WIDGET_HOOKS : [18]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters."]; + +; --- Ideas: asked for, not built --------------------------------------------------------------- +; From what engineers use in the browser today (EDA Playground, Makerchip, 8bitworkshop, +; DigitalJS, Surfer, WaveDrom) and what none of them draws for a .t27 spec. Each names the data +; it would read, all of which exists in this repository already. None is shown as working. +pub const IDEA_COUNT : u8 = 7; +pub const IDEA_IDS : [7]str = ["placement-map", "resource-bars", "slack-waterfall", "bit-diff", "test-waves", "gf16-calc", "race-the-bee"]; +pub const IDEA_TITLES : [7]str = ["Placement map", "LUT, FF and BRAM bars", "Timing slack waterfall", "Bitstream diff proof", "Test waveforms", "GF16 and ternary calculator", "Race the bee"]; +pub const IDEA_LINES : [7]str = ["Where nextpnr put each cell of a spec's design on the XC7A200T die, as a picture that can be posted.", "The resources one spec costs, as bars against the part's totals, beside the same design from another language.", "Every path's slack from the SDF, sorted, with the failing ones at the top.", "Two bitstreams compared frame by frame: byte-identical, or the first frame that differs.", "A spec's test block drawn as a timing diagram, the way WaveDrom draws one.", "Type a number and see it as GF16 and as balanced ternary, bit by bit, with the format spec it obeys.", "A real open issue, one human and one bee: whoever lands an accepted patch first takes the lane."]; +pub const IDEA_SOURCES : [7]str = ["nextpnr-xilinx placement output of the bench builds", "synthesis reports of the bench builds", "nextpnr SDF, as tri fpga-txhold already reads it", "tri x7-audit and the x7-board byte-identical comparison", "spec test blocks through the in-browser evaluator", "FORMAT-SPEC-001.json and gf16.t27", "specs/queen/wars.t27 and the bees' run ledger"]; + +; --- Tests ----------------------------------------------------------------------------------- +test the_player_embed_is_sixteen_by_nine { + assert EMBED_WIDTH * 9 == PLAYER_EMBED_HEIGHT * 16; + assert CAST_EMBED_HEIGHT > PLAYER_EMBED_HEIGHT; +} + +test every_category_has_a_name_and_a_line { + assert CATEGORY_COUNT == 3; + assert CATEGORY_IDS[0] == "fpga"; + assert CATEGORY_IDS[1] == "compiler"; + assert CATEGORY_IDS[2] == "game"; +} + +test the_fpga_shelf_comes_first_and_opens_on_the_board { + assert WIDGET_CATEGORIES[0] == "fpga"; + assert WIDGET_IDS[0] == "x7-board"; + assert WIDGET_KINDS[0] == "cast"; +} + +test the_arena_is_a_tab_and_says_its_own_words { + assert WIDGET_IDS[14] == "wars"; + assert WIDGET_KINDS[14] == "tab"; + assert WIDGET_PAGES[14] == "wars"; +} + +test the_gallery_posts_nothing { + assert POSTS_NOTHING == true; +} + +test ideas_are_counted_apart_from_widgets { + assert IDEA_COUNT == 7; + assert IDEA_IDS[6] == "race-the-bee"; + assert WIDGET_COUNT == 18; +} diff --git a/apps/website/qa/agents-spec-contract.mjs b/apps/website/qa/agents-spec-contract.mjs index d7c42d3c0e..297012af8d 100644 --- a/apps/website/qa/agents-spec-contract.mjs +++ b/apps/website/qa/agents-spec-contract.mjs @@ -590,7 +590,12 @@ const jetton = JSON.parse(readFileSync('public/tri/jetton.json', 'utf8')) const MTRI_PER_TRI = Number(readFileSync('src/lib/triToken.ts', 'utf8').match(/export const MTRI_PER_TRI = (\d+)/)?.[1]) assert.equal(10 ** Number(jetton.decimals), MTRI_PER_TRI, 'public/tri/jetton.json decimals and MTRI_PER_TRI must agree') assert.match(jetton.description, /^TESTNET ONLY\./, 'the jetton metadata leads with TESTNET ONLY') -assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlxkg', 'the rail keys are 1-9, 0, t, p, r, b, w, m, l, x, k, g in that order') +const widgetsModule = MODULES.find((m) => m.tab === 'widgets') +assert.ok(widgetsModule && HUD_VIEWS.includes('widgets'), 'WIDGETS (what a reader lifts out and shares) is a module and a view') +assert.equal(widgetsModule.key, 'v', 'WIDGETS opens on v') +assert.ok(widgetsModule.en.hint.includes('(key v)') && widgetsModule.ru.hint.includes('(клавиша v)'), 'WIDGETS names its letter key in both hints') +for (const lang of ['en', 'ru']) assert.ok(widgetsModule[lang].body.includes('specs/widgets/gallery.t27'), `widgets: ${lang} copy must name its spec`) +assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlxkgv', 'the rail keys are 1-9, 0, t, p, r, b, w, m, l, x, k, g, v in that order') // The rail is no longer the whole vocabulary. HUD_VIEWS stays the fourteen // addresses -- every ?tab=, every key, every module card -- while the rail draws diff --git a/apps/website/qa/queen-contrast-contract.mjs b/apps/website/qa/queen-contrast-contract.mjs index a0740d1ee0..9c7ad35f0c 100644 --- a/apps/website/qa/queen-contrast-contract.mjs +++ b/apps/website/qa/queen-contrast-contract.mjs @@ -205,6 +205,7 @@ const REACHED = { 'src/components/QueenTri.css', 'src/components/QueenUniverseAtlas.css', 'src/components/QueenWars.css', + 'src/components/QueenWidgets.css', 'src/pages/Queen.css', 'src/pages/QueenUniverse.css', 'src/pages/passport.css', @@ -863,6 +864,7 @@ const TOKEN_SCOPES = new Map([ ['.queen-catalog-layer:has(.queen-catalog-toolbar.is-search-open)', 'board'], ['.queen-hive-display', 'board'], ['.queen-wars', 'board'], + ['.queen-widgets', 'board'], ['.queen27-context', 'board'], ['.queen27-cycle-brand', 'board'], ['.queen27-factory', 'board'], diff --git a/apps/website/qa/queen-viewport-contract.mjs b/apps/website/qa/queen-viewport-contract.mjs index 60027e16aa..36d23003bf 100644 --- a/apps/website/qa/queen-viewport-contract.mjs +++ b/apps/website/qa/queen-viewport-contract.mjs @@ -44,7 +44,8 @@ const SIZES = [[1920, 1080], [1440, 900], [1272, 806], [1280, 700], [1280, 600], // on a phone the board drew six 160px columns whose card boxes were 67px tall // round 204px cards, so no column showed a single whole card, and every check // above still passed (#1221 fixed it; nothing here would have caught it). -const VIEWS = ['wars', 'kanban']; +// WIDGETS joined 2026-10-04: a card grid that must fold to one column at 390px. +const VIEWS = ['wars', 'kanban', 'widgets']; // The rail used to draw one button per view, so this counted HUD_VIEWS and // compared. That stopped being the shape of the thing: SPECS has long stood for // six layers behind one button, and KANBAN now stands for itself, MAP and @@ -255,6 +256,7 @@ const DECLARED = [ '.queen27-city-head dl', '.queen27-city-build-queue dl', '.queen27-hardware-foundry dl', '.queen27-factory-command dl', '.queen27-activity-stream ol', '.queen27-flow-grid', '.queen-wars', '.queen-wars-table-scroll', '.queen-wars-ledger ol', '.queen-wars-flow', + '.queen-widgets', // the sub-navigation row: one line at every width, scrolling sideways when // the rungs are wider than the module -- overflow-x:auto with the bar hidden, // which is the declaration. This gate never met one before, because the row @@ -308,7 +310,7 @@ const PROBE = (phone) => `(() => { // arrive as a single child. Matching only direct children counted zero views // on any tab that had grown a row above it, and reported a rendered board as // a board that had not rendered at all. - const viewSel = '.queen27-comb, .queen27-kanban, .queen27-mission-map, .queen27-factory, .queen27-tech, .queen-wars'; + const viewSel = '.queen27-comb, .queen27-kanban, .queen27-mission-map, .queen27-factory, .queen27-tech, .queen-wars, .queen-widgets'; const views = body ? [...body.children].flatMap(child => child.matches(viewSel) ? [child] : [...child.querySelectorAll(':scope > ' + viewSel)]) diff --git a/apps/website/qa/tri-screens-contract.mjs b/apps/website/qa/tri-screens-contract.mjs index d593dd8490..7442dee13f 100644 --- a/apps/website/qa/tri-screens-contract.mjs +++ b/apps/website/qa/tri-screens-contract.mjs @@ -189,8 +189,9 @@ assert.equal(HUD_KEYS[at], 'r', 'TRI opens on r') // and r are TOOLS, PROJECT and TRI. Then w: BROWSER, m: ROADMAP, and l: // LEADERBOARD, whose lane the bees ran on. The eighteenth is x (the crossed // blades): WARS, then k: TOKEN, and the twentieth is g: PROVIDERS, the GPUs -// and boards that sell compute. -assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlxkg') +// and boards that sell compute. The twenty-first is v: WIDGETS, what a reader +// lifts out and shares. +assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlxkgv') // The typed Latin letter or digit decides, as the rail's badge says; the // physical key (KeyboardEvent.code) only when the character is not one, so r // opens TRI on a Russian layout too. diff --git a/apps/website/scripts/widgets-from-spec.mjs b/apps/website/scripts/widgets-from-spec.mjs new file mode 100644 index 0000000000..cffdfaed37 --- /dev/null +++ b/apps/website/scripts/widgets-from-spec.mjs @@ -0,0 +1,250 @@ +#!/usr/bin/env node +// widgets-from-spec.mjs -- the WIDGETS tab of the Queen board from `specs/widgets/gallery.t27`. +// +// Reads the spec through the real compiler (`t27_compiler.wasm`), checks the constant schema, +// evaluates every `test` block against the declared constants, then reads each widget's own page +// for the words and the card -- og:title, og:description, og:image, and the player's +// twitter:player -- so the gallery never says what the page does not. A recording whose meta.json +// shows a non-zero exit code is refused: a widget is something that worked. Writes the files +// nobody edits by hand: +// +// src/lib/queenWidgets.generated.ts the gallery the tab draws, embed code included +// public/widgets/gallery.t27 this spec, byte for byte +// +// Run: node scripts/widgets-from-spec.mjs (write) +// node scripts/widgets-from-spec.mjs --check (fail if the committed files are stale) +// node scripts/widgets-from-spec.mjs --json (print the gallery as JSON) +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { CYRILLIC, SITE, checkSchema, compilerErrors, constsOf, loadCompiler, sha256, verdictOf } from './agents-from-specs.mjs' +import { runSpecTests } from './viewport-from-spec.mjs' + +const WASM = 'public/t27/t27_compiler.wasm' +export const WIDGETS_SPEC = 'specs/widgets/gallery.t27' +export const TS_OUT = 'src/lib/queenWidgets.generated.ts' +export const PUBLIC_SPEC_OUT = 'public/widgets/gallery.t27' +export const EXPECTED_MODULE = 'widgets_gallery' +const HUD = 'src/components/queenHud.ts' + +const REQUIRED = { + KIND: 'str', ID: 'str', NAME: 'str', SCHEMA_VERSION: 'u8', GENERATED: 'arr', + ORIGIN: 'str', SITE_HANDLE: 'str', HASHTAGS: 'arr', EMBED_WIDTH: 'u16', PLAYER_EMBED_HEIGHT: 'u16', CAST_EMBED_HEIGHT: 'u16', + TAB_PAGE: 'str', POSTS_NOTHING: 'bool', + CATEGORY_COUNT: 'u8', CATEGORY_IDS: 'arr', CATEGORY_NAMES: 'arr', CATEGORY_LINES: 'arr', KINDS: 'arr', + WIDGET_COUNT: 'u8', WIDGET_IDS: 'arr', WIDGET_KINDS: 'arr', WIDGET_CATEGORIES: 'arr', WIDGET_PAGES: 'arr', + WIDGET_TITLES: 'arr', WIDGET_LINES: 'arr', WIDGET_HOOKS: 'arr', + IDEA_COUNT: 'u8', IDEA_IDS: 'arr', IDEA_TITLES: 'arr', IDEA_LINES: 'arr', IDEA_SOURCES: 'arr', +} + +const PARALLEL = { + CATEGORY_COUNT: ['CATEGORY_IDS', 'CATEGORY_NAMES', 'CATEGORY_LINES'], + WIDGET_COUNT: ['WIDGET_IDS', 'WIDGET_KINDS', 'WIDGET_CATEGORIES', 'WIDGET_PAGES', 'WIDGET_TITLES', 'WIDGET_LINES', 'WIDGET_HOOKS'], + IDEA_COUNT: ['IDEA_IDS', 'IDEA_TITLES', 'IDEA_LINES', 'IDEA_SOURCES'], +} + +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) +const uniq = (values) => new Set(values).size === values.length +const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'", apos: "'" } +const unescape = (s) => s.replace(/&(amp|lt|gt|quot|#39|apos);/g, (_, e) => ENTITIES[e]) +const escapeAttr = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/ (url.startsWith(origin) ? url.slice(origin.length).split(/[?#]/)[0] : null) + +const hudViews = (root) => { + const src = readFileSync(join(root, HUD), 'utf8') + const list = src.match(/export const HUD_VIEWS[^=]*=\s*\[([\s\S]*?)\]\s*as const/)?.[1] ?? '' + return [...list.matchAll(/"([a-z]+)"/g)].map((m) => m[1]) +} + +export function semanticProblems(f, file = WIDGETS_SPEC) { + const p = [] + if (f.KIND !== 'widgets') p.push(`${file}: KIND must be "widgets"`) + if (f.ID !== 'widgets/gallery') p.push(`${file}: ID must be "widgets/gallery"`) + if (f.SCHEMA_VERSION !== 1) p.push(`${file}: SCHEMA_VERSION must be 1`) + if (!same(f.GENERATED, [TS_OUT, PUBLIC_SPEC_OUT])) p.push(`${file}: GENERATED must be ${JSON.stringify([TS_OUT, PUBLIC_SPEC_OUT])}`) + if (!same(f.KINDS, ['cast', 'player', 'tab'])) p.push(`${file}: KINDS must be ["cast","player","tab"]`) + if (f.ORIGIN !== 'https://t27.ai/') p.push(`${file}: ORIGIN must be https://t27.ai/`) + if (f.POSTS_NOTHING !== true) p.push(`${file}: POSTS_NOTHING must be true; the gallery never posts for anyone`) + for (const [countName, arrays] of Object.entries(PARALLEL)) { + for (const name of arrays) if (f[name].length !== f[countName]) p.push(`${file}: ${name}.length ${f[name].length} != ${countName} ${f[countName]}`) + } + for (const ids of ['CATEGORY_IDS', 'WIDGET_IDS', 'IDEA_IDS']) if (!uniq(f[ids])) p.push(`${file}: ${ids} must be unique`) + for (const name of ['CATEGORY_NAMES', 'CATEGORY_LINES', 'WIDGET_HOOKS', 'IDEA_TITLES', 'IDEA_LINES', 'IDEA_SOURCES']) { + f[name].forEach((s, i) => { if (!s.trim()) p.push(`${file}: ${name}[${i}] is empty`) }) + } + for (let i = 0; i < f.WIDGET_COUNT; i++) { + const id = f.WIDGET_IDS[i] + const kind = f.WIDGET_KINDS[i] + if (!f.KINDS.includes(kind)) p.push(`${file}: widget ${id} has unknown kind ${kind}`) + if (!f.CATEGORY_IDS.includes(f.WIDGET_CATEGORIES[i])) p.push(`${file}: widget ${id} has unknown category ${f.WIDGET_CATEGORIES[i]}`) + const page = f.WIDGET_PAGES[i] + if (kind === 'cast' && page !== `term/${id}/`) p.push(`${file}: cast ${id} must live at term/${id}/, not ${page}`) + if (kind === 'player' && !/^play\/[a-z0-9-]+\/([a-z0-9-]+\/)?$/.test(page)) p.push(`${file}: player ${id} page ${page} is not play//[/]`) + // A page carries its own words; only a tab, which has no page of its own, says them here. + if (kind === 'tab') { + if (!f.WIDGET_TITLES[i].trim() || !f.WIDGET_LINES[i].trim()) p.push(`${file}: tab ${id} needs WIDGET_TITLES and WIDGET_LINES`) + if (!/^[a-z]+$/.test(page)) p.push(`${file}: tab ${id} page must be a view name, is ${page}`) + } else if (f.WIDGET_TITLES[i] || f.WIDGET_LINES[i]) { + p.push(`${file}: ${kind} ${id} takes its title and line from ${page}; leave WIDGET_TITLES and WIDGET_LINES empty`) + } + } + // Grouped by shelf: a category's widgets are one run, in CATEGORY_IDS order. + const runs = f.WIDGET_CATEGORIES.filter((c, i) => i === 0 || f.WIDGET_CATEGORIES[i - 1] !== c) + if (!same(runs, f.CATEGORY_IDS)) p.push(`${file}: WIDGET_CATEGORIES must run in CATEGORY_IDS order, one run each (is ${runs.join(', ')})`) + return p +} + +/** Reads every widget's page; returns { widgets, problems }. */ +// The preview inside the gallery names the file, not the directory: a static host resolves +// term// to its index.html, the dev server answers a directory with the app itself. +const framed = (path) => (path.endsWith('/') ? `${path}index.html` : path) + +export function widgetsOf(f, root = SITE, file = WIDGETS_SPEC) { + const problems = [] + const views = hudViews(root) + const widgets = [] + for (let i = 0; i < f.WIDGET_COUNT; i++) { + const id = f.WIDGET_IDS[i] + const kind = f.WIDGET_KINDS[i] + const page = f.WIDGET_PAGES[i] + const base = { id, kind, category: f.WIDGET_CATEGORIES[i], hook: f.WIDGET_HOOKS[i] } + if (kind === 'tab') { + if (!views.includes(page)) { problems.push(`${file}: tab ${id} names view ${page}, which ${HUD} does not have`); continue } + const url = `${f.ORIGIN}${f.TAB_PAGE}${page}` + widgets.push({ + ...base, title: f.WIDGET_TITLES[i], line: f.WIDGET_LINES[i], url, view: page, + image: null, gif: null, preview: null, commands: [], recorded: null, + iframe: ``, + markdown: `[${f.WIDGET_TITLES[i]}](${url})`, + }) + continue + } + const htmlPath = join(root, 'public', page, 'index.html') + if (!existsSync(htmlPath)) { problems.push(`${file}: widget ${id}: public/${page}index.html is missing`); continue } + const meta = metaOf(readFileSync(htmlPath, 'utf8')) + const title = meta['og:title'] ?? '' + const line = meta['og:description'] ?? '' + const image = meta['og:image'] ?? '' + if (!title || !line || !image) { problems.push(`${file}: widget ${id}: public/${page}index.html lacks og:title, og:description or og:image`); continue } + const imageFile = localOf(f.ORIGIN, image) + if (!imageFile || !existsSync(join(root, 'public', imageFile))) problems.push(`${file}: widget ${id}: og:image ${image} is not a file under public/`) + const url = `${f.ORIGIN}${page}` + if (kind === 'cast') { + const metaJson = join(root, 'public', page, 'meta.json') + if (!existsSync(metaJson)) { problems.push(`${file}: cast ${id}: public/${page}meta.json is missing`); continue } + const cast = JSON.parse(readFileSync(metaJson, 'utf8')) + const bad = (cast.exit_codes ?? []).filter((c) => String(c) !== '0') + if (bad.length) problems.push(`${file}: cast ${id} recorded exit code(s) ${bad.join(', ')}; a widget is something that worked`) + const gif = cast.gif && existsSync(join(root, 'public', page, 'session.gif')) ? cast.gif : null + widgets.push({ + ...base, title, line, url, view: null, image, gif, preview: framed(page), + commands: cast.commands ?? [], recorded: cast.recorded ?? null, + iframe: ``, + markdown: `[![${title}](${gif ?? image})](${url})`, + }) + } else { + const player = meta['twitter:player'] ?? '' + const embedFile = localOf(f.ORIGIN, player) + if (!embedFile || !existsSync(join(root, 'public', embedFile))) { problems.push(`${file}: player ${id}: twitter:player ${player || '(none)'} is not a page under public/`); continue } + widgets.push({ + ...base, title, line, url, view: null, image, gif: null, preview: framed(player.slice(f.ORIGIN.length)), + commands: [], recorded: null, + iframe: ``, + markdown: `[![${title}](${image})](${url})`, + }) + } + } + return { widgets, problems } +} + +const rows = (count, make) => Array.from({ length: count }, (_, i) => make(i)) + +export function galleryOf(f, widgets, specSha) { + return { + source: { spec: WIDGETS_SPEC, publicSpec: PUBLIC_SPEC_OUT, sha256: specSha, schemaVersion: f.SCHEMA_VERSION }, + name: f.NAME, + origin: f.ORIGIN, + handle: f.SITE_HANDLE, + hashtags: f.HASHTAGS, + postsNothing: f.POSTS_NOTHING, + embed: { width: f.EMBED_WIDTH, playerHeight: f.PLAYER_EMBED_HEIGHT, castHeight: f.CAST_EMBED_HEIGHT }, + categories: rows(f.CATEGORY_COUNT, (i) => ({ id: f.CATEGORY_IDS[i], name: f.CATEGORY_NAMES[i], line: f.CATEGORY_LINES[i] })), + widgets, + ideas: rows(f.IDEA_COUNT, (i) => ({ id: f.IDEA_IDS[i], title: f.IDEA_TITLES[i], line: f.IDEA_LINES[i], source: f.IDEA_SOURCES[i] })), + } +} + +export const renderTs = (gallery) => `// GENERATED by scripts/widgets-from-spec.mjs from ${WIDGETS_SPEC}\n// spec sha256 ${gallery.source.sha256}\n// Do not edit: change the .t27 source and regenerate.\n\nexport const QUEEN_WIDGETS = ${JSON.stringify(gallery, null, 2)} as const\n\nexport type QueenWidgetsGallery = typeof QUEEN_WIDGETS\n` + +export async function buildWidgets({ specText, analyze, root = SITE }) { + const problems = [] + const analysis = analyze(specText) + const verdict = verdictOf(analysis) + if (!verdict.typecheckOk || verdict.discarded > 0 || !verdict.hirOk) problems.push(`${WIDGETS_SPEC}: compiler verdict not clean (${JSON.stringify(verdict)})`) + problems.push(...compilerErrors(analysis).map((m) => `${WIDGETS_SPEC}: ${m}`)) + if (/[^\x00-\x7f]/.test(specText)) problems.push(`${WIDGETS_SPEC}: non-ASCII byte in the spec (L3)`) + if (CYRILLIC.test(specText)) problems.push(`${WIDGETS_SPEC}: Cyrillic in the spec (LANG-EN)`) + if (analysis.ast?.name !== EXPECTED_MODULE) problems.push(`${WIDGETS_SPEC}: module must be ${EXPECTED_MODULE}, is ${analysis.ast?.name ?? 'missing'}`) + let consts = {} + try { consts = constsOf(analysis) } catch (e) { problems.push(`${WIDGETS_SPEC}: ${e.message}`) } + problems.push(...checkSchema(consts, REQUIRED, {}, WIDGETS_SPEC)) + const fields = Object.fromEntries(Object.entries(consts).map(([k, v]) => [k, v.value])) + let tests = { tests: 0, asserts: 0, failures: [] } + let widgets = [] + if (problems.length === 0) { + problems.push(...semanticProblems(fields)) + tests = runSpecTests(analysis, fields) + if (tests.tests === 0) problems.push(`${WIDGETS_SPEC}: no test block; the gallery must test its own invariants`) + problems.push(...tests.failures.map((m) => `${WIDGETS_SPEC}: test ${m}`)) + } + if (problems.length === 0) { + const read = widgetsOf(fields, root) + widgets = read.widgets + problems.push(...read.problems) + } + const specSha = sha256(Buffer.from(specText, 'utf8')) + const gallery = problems.length ? null : galleryOf(fields, widgets, specSha) + return { problems, verdict, fields, specSha, tests, gallery, ts: gallery ? renderTs(gallery) : null, publicSpec: gallery ? specText : null } +} + +async function main() { + const check = process.argv.includes('--check') + const json = process.argv.includes('--json') + const specPath = join(SITE, WIDGETS_SPEC) + if (!existsSync(specPath)) { console.error(`widgets-from-spec: ${WIDGETS_SPEC} is missing`); process.exit(1) } + const analyze = await loadCompiler(readFileSync(join(SITE, WASM))) + const out = await buildWidgets({ specText: readFileSync(specPath, 'utf8'), analyze }) + if (out.problems.length) { + console.error(`widgets-from-spec: ${out.problems.length} problem(s)`) + for (const p of out.problems) console.error(' ' + p) + process.exit(1) + } + if (json) { process.stdout.write(JSON.stringify(out.gallery, null, 2) + '\n'); return } + const targets = [[TS_OUT, out.ts], [PUBLIC_SPEC_OUT, out.publicSpec]] + if (check) { + const stale = targets.filter(([rel, text]) => !existsSync(join(SITE, rel)) || readFileSync(join(SITE, rel), 'utf8') !== text) + if (stale.length) { + console.error(`widgets-from-spec --check: stale ${stale.map(([r]) => r).join(', ')}; run node scripts/widgets-from-spec.mjs`) + process.exit(1) + } + } else { + for (const [rel, text] of targets) { + mkdirSync(dirname(join(SITE, rel)), { recursive: true }) + writeFileSync(join(SITE, rel), text) + } + } + console.log(`widgets-from-spec: ${check ? 'up to date' : 'wrote'} ${targets.map(([r]) => r).join(', ')}; spec ${out.specSha.slice(0, 16)}; ${out.gallery.widgets.length} widget(s) in ${out.gallery.categories.length} categories, ${out.gallery.ideas.length} idea(s) not built; spec tests ${out.tests.tests}, asserts ${out.tests.asserts}, all hold`) +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch((e) => { console.error(e); process.exit(1) }) +} diff --git a/apps/website/scripts/widgets-from-spec.test.mjs b/apps/website/scripts/widgets-from-spec.test.mjs new file mode 100644 index 0000000000..8d633e5109 --- /dev/null +++ b/apps/website/scripts/widgets-from-spec.test.mjs @@ -0,0 +1,54 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { SITE, loadCompiler } from './agents-from-specs.mjs' +import { PUBLIC_SPEC_OUT, TS_OUT, WIDGETS_SPEC, buildWidgets, metaOf } from './widgets-from-spec.mjs' + +const analyze = await loadCompiler(readFileSync(join(SITE, 'public/t27/t27_compiler.wasm'))) +const source = readFileSync(join(SITE, WIDGETS_SPEC), 'utf8') +const replaceConst = (text, name, value) => { + const pattern = new RegExp(`^(pub const ${name} : [a-z0-9\\[\\]-]+ = )[^;]+;`, 'm') + assert.match(text, pattern, `${name} exists in the spec`) + return text.replace(pattern, `$1${value};`) +} + +test('the committed spec builds clean and the derived files are current', async () => { + const out = await buildWidgets({ specText: source, analyze }) + assert.deepEqual(out.problems, []) + assert.equal(readFileSync(join(SITE, TS_OUT), 'utf8'), out.ts) + assert.equal(readFileSync(join(SITE, PUBLIC_SPEC_OUT), 'utf8'), source) + assert.equal(out.gallery.widgets.length, out.fields.WIDGET_COUNT) + assert.equal(out.gallery.postsNothing, true) +}) + +test('every widget title comes from its page, never from the gallery', async () => { + const out = await buildWidgets({ specText: source, analyze }) + for (const w of out.gallery.widgets) { + assert.ok(w.title.length > 0, `${w.id} has a title`) + assert.ok(w.url.startsWith('https://t27.ai/'), `${w.id} copies an absolute address`) + if (w.kind === 'tab') continue + const meta = metaOf(readFileSync(join(SITE, 'public', w.url.slice('https://t27.ai/'.length), 'index.html'), 'utf8')) + assert.equal(w.title, meta['og:title'], `${w.id} title is the page's og:title`) + } +}) + +test('a widget whose page does not exist fails the build', async () => { + const broken = source.replace('"term/x7-board/"', '"term/no-such-recording/"') + const out = await buildWidgets({ specText: broken, analyze }) + assert.ok(out.problems.some((p) => p.includes('no-such-recording')), out.problems.join('\n')) + assert.equal(out.ts, null) +}) + +test('a failing spec test blocks the write', async () => { + const broken = replaceConst(source, 'POSTS_NOTHING', 'false') + const out = await buildWidgets({ specText: broken, analyze }) + assert.ok(out.problems.length > 0) + assert.equal(out.ts, null) +}) + +test('a tab widget must name a real board view', async () => { + const broken = source.replace(/"wars", "term\/tri-game-tick\/"/, '"nowhere", "term/tri-game-tick/"') + const out = await buildWidgets({ specText: broken, analyze }) + assert.ok(out.problems.length > 0) +}) diff --git a/apps/website/specs/widgets/gallery.t27 b/apps/website/specs/widgets/gallery.t27 new file mode 100644 index 0000000000..768f32eee5 --- /dev/null +++ b/apps/website/specs/widgets/gallery.t27 @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/gallery.t27 -- the widget gallery: what of t27 a person can lift out and share +; Source of truth for apps/website/scripts/widgets-from-spec.mjs (gHashTag/trinity), which +; compiles this file with the vendored compiler (public/t27/t27_compiler.wasm), checks the +; constant schema, evaluates every test block below, reads each widget's own share page for its +; title, description and card image, and only then writes src/lib/queenWidgets.generated.ts (the +; WIDGETS tab on the Queen board, key v) and a byte-identical copy of this file at +; public/widgets/gallery.t27. +; ASCII only (L3), English only (LANG-EN). + +; WHAT A WIDGET IS: something that already runs on t27.ai and has a page of its own -- a +; terminal recording of a real command, or the player that compiles a spec inside the reader's +; browser -- picked because an FPGA engineer, a compiler person or a player of the agent game +; would want to show it to someone else. The gallery adds no new claim: the title, the line under +; it and the card are read from the widget's page, so a widget cannot say here what its page does +; not say. A widget whose page is missing fails the build. + +; WHAT THE GALLERY GIVES A READER, per widget: the page; a live preview, loaded only on a click; +; the embed code (an iframe for a site, a linked GIF or card in Markdown for a README); and share +; links for X and Telegram. Every one of those is the reader's own click. The page posts nothing, +; sets no cookie and sends nothing anywhere. + +; WHAT IS DELIBERATELY ABSENT: a widget built for the gallery alone. IDEAS lists what engineers +; ask for that t27.ai does not draw yet; they are shown as ideas, with the data each would read, +; and never as a working widget. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_gallery; + +pub const KIND : str = "widgets"; +pub const ID : str = "widgets/gallery"; +pub const NAME : str = "t27 widgets"; +pub const SCHEMA_VERSION : u8 = 1; +; The derived files the generator rewrites (relative to apps/website). +pub const GENERATED : [2]str = ["src/lib/queenWidgets.generated.ts", "public/widgets/gallery.t27"]; + +; --- Where the widgets live ------------------------------------------------------------------ +; Every address a reader copies is absolute: an embed pasted into somebody's README must not +; depend on where the gallery was opened. The preview inside the gallery is relative, so the +; tab works the same on t27.ai and under app.t27.ai/game/. +pub const ORIGIN : str = "https://t27.ai/"; +pub const SITE_HANDLE : str = "@t27_dev"; +pub const HASHTAGS : [3]str = ["t27", "FPGA", "opensource"]; +; Embed size. The player is 16:9, as its card is; a recording page needs more height than that +; for its controls. +pub const EMBED_WIDTH : u16 = 640; +pub const PLAYER_EMBED_HEIGHT : u16 = 360; +pub const CAST_EMBED_HEIGHT : u16 = 460; +; The tab names a widget's page in the Queen board as ORIGIN, then this, then the view. +pub const TAB_PAGE : str = "#/queen?tab="; +pub const POSTS_NOTHING : bool = true; + +; --- Categories ------------------------------------------------------------------------------ +pub const CATEGORY_COUNT : u8 = 3; +pub const CATEGORY_IDS : [3]str = ["fpga", "compiler", "game"]; +pub const CATEGORY_NAMES : [3]str = ["FPGA bench", "Compiler", "Agent game"]; +pub const CATEGORY_LINES : [3]str = ["Bitstreams, timing, JTAG and pins, recorded on the bench as the commands ran.", "One spec, seven backends, compiled in the reader's browser by the same WebAssembly compiler.", "The Queen's loop and the arena where agent configurations are compared on real issues."]; + +; --- Kinds ----------------------------------------------------------------------------------- +; cast: a terminal recording, page at term// (session.gif, card.png, meta.json beside it). +; player: the X player, page at play//[/]; its twitter:player is the embed. +; tab: a view of the Queen board; WIDGET_PAGES holds the view's name. +pub const KINDS : [3]str = ["cast", "player", "tab"]; + +; --- The widgets ----------------------------------------------------------------------------- +; Parallel arrays, WIDGET_COUNT long. WIDGET_TITLES is empty wherever the page carries an +; og:title, which is every kind but tab: the page is the one place the words live. +pub const WIDGET_COUNT : u8 = 18; +pub const WIDGET_IDS : [18]str = ["x7-board", "tri-x7-fasm", "tri-x7-mutate", "perf2-loop", "tri-fpga-rgmii", "tri-fpga-jtag", "tri-fpga-pins", "devkit-flow", "play-verilog", "play-hello-world", "play-rust", "play-js", "t27c-hello-world", "t27-tri-misread", "wars", "tri-game-tick", "tri-game-vault", "perf2-knockouts"]; +pub const WIDGET_KINDS : [18]str = ["cast", "cast", "cast", "cast", "cast", "cast", "cast", "cast", "player", "player", "player", "player", "cast", "cast", "tab", "cast", "cast", "cast"]; +pub const WIDGET_CATEGORIES : [18]str = ["fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "fpga", "compiler", "compiler", "compiler", "compiler", "compiler", "game", "game", "game", "game"]; +pub const WIDGET_PAGES : [18]str = ["term/x7-board/", "term/tri-x7-fasm/", "term/tri-x7-mutate/", "term/perf2-loop/", "term/tri-fpga-rgmii/", "term/tri-fpga-jtag/", "term/tri-fpga-pins/", "term/devkit-flow/", "play/hello-world/verilog/", "play/hello-world/", "play/hello-world/rust/", "play/hello-world/js/", "term/t27c-hello-world/", "term/t27-tri-misread/", "wars", "term/tri-game-tick/", "term/tri-game-vault/", "term/perf2-knockouts/"]; +pub const WIDGET_TITLES : [18]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "WARS: agent configurations compared on real GitHub issues", "", "", ""]; +pub const WIDGET_LINES : [18]str = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", "", "", ""]; +; Why a person would pass this one on: the reason the widget is in the gallery at all. +pub const WIDGET_HOOKS : [18]str = ["The bitstream is byte-identical to openXC7's and runs on a real board: the claim most FPGA people doubt, shown.", "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", "Place and route to a loaded bitstream, with the clock on screen.", "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", "The cable asks which chip is there and the chip answers with its IDCODE.", "Before an edit to a Verilog file: every spec and generator it would touch.", "The whole FPGA flow, one layer per line, from spec to board.", "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", "The same spec as Rust, printed by a terminal in the page.", "The only backend that runs in the browser: generate the module, then run it.", "t27c on hello_world.t27, on a real machine, every byte as printed.", "Code that compiles and is wrong: the tool that finds it.", "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", "One tick of the Queen's loop: an anomaly scan that reports only what changed.", "The loop's skills checked against snapshots before an agent may use them.", "Three performance bits, and one knockout bitstream per bit to prove each one matters."]; + +; --- Ideas: asked for, not built --------------------------------------------------------------- +; From what engineers use in the browser today (EDA Playground, Makerchip, 8bitworkshop, +; DigitalJS, Surfer, WaveDrom) and what none of them draws for a .t27 spec. Each names the data +; it would read, all of which exists in this repository already. None is shown as working. +pub const IDEA_COUNT : u8 = 7; +pub const IDEA_IDS : [7]str = ["placement-map", "resource-bars", "slack-waterfall", "bit-diff", "test-waves", "gf16-calc", "race-the-bee"]; +pub const IDEA_TITLES : [7]str = ["Placement map", "LUT, FF and BRAM bars", "Timing slack waterfall", "Bitstream diff proof", "Test waveforms", "GF16 and ternary calculator", "Race the bee"]; +pub const IDEA_LINES : [7]str = ["Where nextpnr put each cell of a spec's design on the XC7A200T die, as a picture that can be posted.", "The resources one spec costs, as bars against the part's totals, beside the same design from another language.", "Every path's slack from the SDF, sorted, with the failing ones at the top.", "Two bitstreams compared frame by frame: byte-identical, or the first frame that differs.", "A spec's test block drawn as a timing diagram, the way WaveDrom draws one.", "Type a number and see it as GF16 and as balanced ternary, bit by bit, with the format spec it obeys.", "A real open issue, one human and one bee: whoever lands an accepted patch first takes the lane."]; +pub const IDEA_SOURCES : [7]str = ["nextpnr-xilinx placement output of the bench builds", "synthesis reports of the bench builds", "nextpnr SDF, as tri fpga-txhold already reads it", "tri x7-audit and the x7-board byte-identical comparison", "spec test blocks through the in-browser evaluator", "FORMAT-SPEC-001.json and gf16.t27", "specs/queen/wars.t27 and the bees' run ledger"]; + +; --- Tests ----------------------------------------------------------------------------------- +test the_player_embed_is_sixteen_by_nine { + assert EMBED_WIDTH * 9 == PLAYER_EMBED_HEIGHT * 16; + assert CAST_EMBED_HEIGHT > PLAYER_EMBED_HEIGHT; +} + +test every_category_has_a_name_and_a_line { + assert CATEGORY_COUNT == 3; + assert CATEGORY_IDS[0] == "fpga"; + assert CATEGORY_IDS[1] == "compiler"; + assert CATEGORY_IDS[2] == "game"; +} + +test the_fpga_shelf_comes_first_and_opens_on_the_board { + assert WIDGET_CATEGORIES[0] == "fpga"; + assert WIDGET_IDS[0] == "x7-board"; + assert WIDGET_KINDS[0] == "cast"; +} + +test the_arena_is_a_tab_and_says_its_own_words { + assert WIDGET_IDS[14] == "wars"; + assert WIDGET_KINDS[14] == "tab"; + assert WIDGET_PAGES[14] == "wars"; +} + +test the_gallery_posts_nothing { + assert POSTS_NOTHING == true; +} + +test ideas_are_counted_apart_from_widgets { + assert IDEA_COUNT == 7; + assert IDEA_IDS[6] == "race-the-bee"; + assert WIDGET_COUNT == 18; +} diff --git a/apps/website/src/components/QueenWidgets.css b/apps/website/src/components/QueenWidgets.css new file mode 100644 index 0000000000..497ad7bb6e --- /dev/null +++ b/apps/website/src/components/QueenWidgets.css @@ -0,0 +1,309 @@ +.queen-widgets { + --widgets-line: color-mix(in srgb, var(--hud-line, rgba(0, 255, 136, 0.22)) 48%, transparent); + --widgets-green: var(--hud-green, #00ff88); + --widgets-gold: var(--hud-gold, #ffd700); + --widgets-text-muted: #adc9c0; + --widgets-text-subtle: #8fa9a0; + position: relative; + /* The site-wide section rule is a full-viewport flex hero. WIDGETS is the + viewport's one scroll owner, as WARS is, so it opts out explicitly. */ + display: block; + height: 100%; + min-width: 0; + min-height: 0; + overflow: auto; + padding: clamp(14px, 2vw, 28px); + color: #e8fff7; + background: #000; + scrollbar-color: var(--widgets-green) rgba(0, 0, 0, 0.45); +} + +.queen-widgets-head, +.queen-widgets-filter, +.queen-widgets-shelf, +.queen-widgets-ideas, +.queen-widgets-honesty { + width: min(1380px, 100%); + margin-inline: auto; +} + +.queen-widgets-head { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; + gap: 20px; + padding: clamp(12px, 2vw, 28px) 0 18px; + border-bottom: 1px solid var(--widgets-line); +} + +.queen-widgets-kicker, +.queen-widgets-shelf-title, +.queen-widgets-tags { + margin: 0; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--widgets-green); +} + +.queen-widgets h3 { + margin: 6px 0 8px; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: clamp(1.6rem, 4vw, 2.6rem); + font-weight: 800; + line-height: 1.25; + color: #fff; +} + +.queen-widgets-lead, +.queen-widgets-shelf-line { + max-width: 72ch; + margin: 0; + color: var(--widgets-text-muted); + font-size: 0.92rem; + line-height: 1.5; +} + +.queen-widgets-source { + padding: 6px 12px; + border: 1px solid var(--widgets-line); + border-radius: 999px; + color: var(--widgets-gold); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.72rem; + text-decoration: none; + white-space: nowrap; +} + +.queen-widgets-filter { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 14px 0 4px; +} + +.queen-widgets-filter button, +.queen-widgets-actions :is(a, button), +.queen-widgets-embed button { + min-height: 32px; + padding: 4px 12px; + border: 1px solid var(--widgets-line); + border-radius: 999px; + background: transparent; + color: var(--widgets-text-muted); + font: inherit; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.74rem; + text-decoration: none; + white-space: nowrap; + cursor: pointer; +} + +.queen-widgets-filter button[aria-pressed="true"], +.queen-widgets-actions :is(a, button):hover, +.queen-widgets-actions button[aria-pressed="true"], +.queen-widgets-actions button[aria-expanded="true"] { + border-color: var(--widgets-green); + color: var(--widgets-green); +} + +.queen-widgets-shelf { + padding: 22px 0 6px; +} + +.queen-widgets-shelf-line { + margin: 4px 0 14px; +} + +.queen-widgets-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); + gap: 14px; +} + +.queen-widgets-card { + display: flex; + flex-direction: column; + min-width: 0; + border: 1px solid var(--widgets-line); + border-radius: 10px; + background: #050807; + overflow: hidden; +} + +.queen-widgets-stage { + position: relative; + aspect-ratio: 1200 / 630; + background: #000; + border-bottom: 1px solid var(--widgets-line); +} + +.queen-widgets-stage.is-wide { + aspect-ratio: 16 / 9; +} + +.queen-widgets-stage:has(iframe) { + aspect-ratio: 4 / 3; +} + +.queen-widgets-stage :is(img, iframe) { + display: block; + width: 100%; + height: 100%; + border: 0; + object-fit: cover; +} + +.queen-widgets-glyph { + display: grid; + place-items: center; + height: 100%; + font-size: 3.4rem; + color: var(--widgets-gold); +} + +.queen-widgets-body { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + min-width: 0; + padding: 12px 14px 14px; +} + +.queen-widgets-tags { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + color: var(--widgets-text-subtle); + font-size: 0.62rem; +} + +.queen-widgets-tags span:first-child { + color: var(--widgets-green); +} + +.queen-widgets-card h4 { + margin: 0; + color: #fff; + font-size: 1rem; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.queen-widgets-line, +.queen-widgets-hook { + margin: 0; + color: var(--widgets-text-muted); + font-size: 0.84rem; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.queen-widgets-hook b { + color: var(--widgets-gold); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.66rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.queen-widgets-actions { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: auto; + padding-top: 6px; +} + +.queen-widgets-embed { + display: grid; + gap: 10px; +} + +.queen-widgets-embed label { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px 6px; + align-items: start; +} + +.queen-widgets-embed label > span { + grid-column: 1 / -1; + color: var(--widgets-text-subtle); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.64rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.queen-widgets-embed textarea { + min-width: 0; + resize: vertical; + padding: 6px 8px; + border: 1px solid var(--widgets-line); + border-radius: 6px; + background: #000; + color: #e8fff7; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.7rem; + line-height: 1.4; +} + +.queen-widgets-ideas { + padding: 26px 0 8px; + border-top: 1px solid var(--widgets-line); + margin-top: 22px; +} + +.queen-widgets-ideas ol { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} + +.queen-widgets-ideas li { + display: grid; + gap: 6px; + padding: 12px 14px; + border: 1px dashed var(--widgets-line); + border-radius: 10px; +} + +.queen-widgets-ideas b { + color: #fff; + font-size: 0.92rem; +} + +.queen-widgets-ideas span { + color: var(--widgets-text-muted); + font-size: 0.84rem; + line-height: 1.45; +} + +.queen-widgets-ideas small { + color: var(--widgets-text-subtle); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.68rem; +} + +.queen-widgets-honesty { + margin-block: 18px 6px; + color: var(--widgets-text-subtle); + font-size: 0.78rem; +} + +@media (max-width: 520px) { + .queen-widgets-head { + grid-template-columns: minmax(0, 1fr); + } + + .queen-widgets-source { + justify-self: start; + } +} diff --git a/apps/website/src/components/QueenWidgets.tsx b/apps/website/src/components/QueenWidgets.tsx new file mode 100644 index 0000000000..5289254505 --- /dev/null +++ b/apps/website/src/components/QueenWidgets.tsx @@ -0,0 +1,198 @@ +import { useCallback, useState } from 'react' +import { QUEEN_WIDGETS } from '../lib/queenWidgets.generated' +import './QueenWidgets.css' + +// WIDGETS: what of t27 a person can lift out and share. Every widget, its words and its card +// come from specs/widgets/gallery.t27 through scripts/widgets-from-spec.mjs, which reads each +// widget's own page; this view only draws them and hands out the embed code. It posts nothing: +// every share link opens the reader's own composer. + +type Widget = (typeof QUEEN_WIDGETS.widgets)[number] +type Lang = 'en' | 'ru' + +const COPY = { + en: { + kicker: 'SHAREABLE WIDGETS', + title: 'WIDGETS', + lead: 'Real recordings and the in-browser compiler, each with a page of its own. Preview it, embed it in a site or a README, or share it.', + source: 'SOURCE .T27', + all: 'All', + categories: { fpga: 'FPGA bench', compiler: 'Compiler', game: 'Agent game' } as Record, + kinds: { cast: 'recording', player: 'live player', tab: 'board tab' } as Record, + why: 'why share', + preview: 'Preview', + hide: 'Hide', + openTab: 'Open the tab', + open: 'Open page', + embed: 'Embed', + iframe: 'HTML (iframe)', + markdown: 'Markdown (README)', + copy: 'Copy', + copied: 'Copied', + copyLink: 'Copy link', + recorded: 'recorded', + ideas: 'NEXT WIDGETS, NOT BUILT', + ideasLead: 'What FPGA engineers draw in the browser elsewhere and t27.ai does not draw yet. Each would read data this repository already has.', + reads: 'would read', + honesty: 'Nothing here posts for you. The share links open your own X or Telegram composer, and the embed code is yours to paste.', + count: (n: number) => `${n} widgets`, + }, + ru: { + kicker: 'ВИДЖЕТЫ ДЛЯ РЕПОСТА', + title: 'ВИДЖЕТЫ', + lead: 'Настоящие записи команд и компилятор прямо в браузере, у каждого своя страница. Посмотрите, вставьте на сайт или в README, поделитесь.', + source: 'ИСХОДНИК .T27', + all: 'Все', + categories: { fpga: 'FPGA-стенд', compiler: 'Компилятор', game: 'Игра агентов' } as Record, + kinds: { cast: 'запись', player: 'живой плеер', tab: 'вкладка доски' } as Record, + why: 'зачем делиться', + preview: 'Смотреть', + hide: 'Скрыть', + openTab: 'Открыть вкладку', + open: 'Страница', + embed: 'Встроить', + iframe: 'HTML (iframe)', + markdown: 'Markdown (README)', + copy: 'Копировать', + copied: 'Скопировано', + copyLink: 'Ссылка', + recorded: 'записано', + ideas: 'СЛЕДУЮЩИЕ ВИДЖЕТЫ, ЕЩЁ НЕ СДЕЛАНЫ', + ideasLead: 'То, что FPGA-инженеры рисуют в браузере в других местах, а t27.ai пока нет. Каждому хватит данных, которые уже есть в репозитории.', + reads: 'данные', + honesty: 'Здесь ничего не публикуется за вас. Ссылки открывают ваш собственный редактор поста в X или Telegram, код для встраивания вставляете вы.', + count: (n: number) => `${n} виджетов`, + }, +} as const + +const ORIGIN = QUEEN_WIDGETS.origin +// The card and the preview load from this site, wherever it is served (t27.ai, or app.t27.ai/game/ +// with a relative base); only the addresses a reader copies are absolute. +const local = (url: string) => (url.startsWith(ORIGIN) ? url.slice(ORIGIN.length) : url) + +function useCopy() { + const [done, setDone] = useState(null) + const copy = useCallback(async (key: string, text: string) => { + try { + await navigator.clipboard.writeText(text) + setDone(key) + window.setTimeout(() => setDone((k) => (k === key ? null : k)), 1400) + } catch { + setDone(null) + } + }, []) + return { done, copy } +} + +function shareLinks(w: Widget) { + const u = encodeURIComponent(w.url) + const text = encodeURIComponent(`${w.title} ${QUEEN_WIDGETS.handle}`) + const tags = QUEEN_WIDGETS.hashtags.join(',') + const tg = encodeURIComponent(`${w.title}\n${w.hook}\n\n${QUEEN_WIDGETS.hashtags.map((t) => `#${t}`).join(' ')}`) + return [ + { name: 'X', href: `https://twitter.com/intent/tweet?url=${u}&text=${text}&hashtags=${tags}` }, + { name: 'Telegram', href: `https://t.me/share/url?url=${u}&text=${tg}` }, + ] +} + +function WidgetCard({ w, c }: { w: Widget; c: (typeof COPY)[Lang] }) { + const [live, setLive] = useState(false) + const [embed, setEmbed] = useState(false) + const { done, copy } = useCopy() + const isPlayer = w.kind === 'player' + return ( +
+
+ {live && w.preview ? ( + ", + "markdown": "[![A bitstream from t27 specs, run on the AX7203](https://t27.ai/term/x7-board/session.gif)](https://t27.ai/term/x7-board/)" + }, + { + "id": "tri-x7-fasm", + "kind": "cast", + "category": "fpga", + "hook": "A Xilinx 7-series bitstream assembled with no Vivado anywhere in the path.", + "title": "tri x7-fasm · a bitstream assembled without Vivado", + "line": "FASM to frames: 22 of 22 byte-identical to fasm2frames, ours 6.31 s in this run (fasm2frames took 150.7 s at corpus build). Frames to .bit: 16 of 16 byte-identical to xc7frames2bit; a wrong-part input is refused.", + "url": "https://t27.ai/term/tri-x7-fasm/", + "view": null, + "image": "https://t27.ai/term/tri-x7-fasm/card.png", + "gif": "https://t27.ai/term/tri-x7-fasm/session.gif", + "preview": "term/tri-x7-fasm/index.html", + "commands": [ + "tri x7-fasm", + "tri x7-writer" + ], + "recorded": "2026-10-03 19:49 UTC", + "iframe": "", + "markdown": "[![tri x7-fasm · a bitstream assembled without Vivado](https://t27.ai/term/tri-x7-fasm/session.gif)](https://t27.ai/term/tri-x7-fasm/)" + }, + { + "id": "tri-x7-mutate", + "kind": "cast", + "category": "fpga", + "hook": "Plant a bug in the bitstream path and watch the tests catch it: mutation testing for hardware.", + "title": "tri x7-mutate · plant a bug, the tests must catch it", + "line": "Five planted bugs in packets.t27 (CRC polynomial bit, CRC address width, LFRM no-ops, COR0 bit 0, type-2 word count). Each mutant is rebuilt and re-swept; every one is CAUGHT by its tests and by the sweeps. mutate: all 5 caught.", + "url": "https://t27.ai/term/tri-x7-mutate/", + "view": null, + "image": "https://t27.ai/term/tri-x7-mutate/card.png", + "gif": "https://t27.ai/term/tri-x7-mutate/session.gif", + "preview": "term/tri-x7-mutate/index.html", + "commands": [ + "tri x7-mutate packets.t27" + ], + "recorded": "2026-10-04 04:48 UTC", + "iframe": "", + "markdown": "[![tri x7-mutate · plant a bug, the tests must catch it](https://t27.ai/term/tri-x7-mutate/session.gif)](https://t27.ai/term/tri-x7-mutate/)" + }, + { + "id": "perf2-loop", + "kind": "cast", + "category": "fpga", + "hook": "Place and route to a loaded bitstream, with the clock on screen.", + "title": "Place and route to a loaded bitstream, timed", + "line": "router1, bitwalk, xc7frames2bit, SRAM load: 19.06 s here, on a laptop at load average ~110.", + "url": "https://t27.ai/term/perf2-loop/", + "view": null, + "image": "https://t27.ai/term/perf2-loop/card.png", + "gif": "https://t27.ai/term/perf2-loop/session.gif", + "preview": "term/perf2-loop/index.html", + "commands": [ + "python3 perf2.py loop --out rec --flash" + ], + "recorded": "2026-10-03 19:54 UTC", + "iframe": "", + "markdown": "[![Place and route to a loaded bitstream, timed](https://t27.ai/term/perf2-loop/session.gif)](https://t27.ai/term/perf2-loop/)" + }, + { + "id": "tri-fpga-rgmii", + "kind": "cast", + "category": "fpga", + "hook": "RGMII hold margins read from nextpnr's own SDF, not from a datasheet.", + "title": "tri fpga-txhold · RGMII margins from nextpnr's own SDF", + "line": "Two recorded model runs (2026-09-27 and 09-28), printed: smallest TX hold 3445 ps on eth_txd[1], smallest RX mid-nibble margin 10900 ps on rxctl_n, both PASS. A model over the routed design's SDF, not a board measurement.", + "url": "https://t27.ai/term/tri-fpga-rgmii/", + "view": null, + "image": "https://t27.ai/term/tri-fpga-rgmii/card.png", + "gif": "https://t27.ai/term/tri-fpga-rgmii/session.gif", + "preview": "term/tri-fpga-rgmii/index.html", + "commands": [ + "tri fpga-txhold", + "tri fpga-rxcap" + ], + "recorded": "2026-10-03 19:49 UTC", + "iframe": "", + "markdown": "[![tri fpga-txhold · RGMII margins from nextpnr's own SDF](https://t27.ai/term/tri-fpga-rgmii/session.gif)](https://t27.ai/term/tri-fpga-rgmii/)" + }, + { + "id": "tri-fpga-jtag", + "kind": "cast", + "category": "fpga", + "hook": "The cable asks which chip is there and the chip answers with its IDCODE.", + "title": "tri fpga-jtag · which chip answered the cable", + "line": "IDCODEs decoded offline: 0x03636093 is an XC7A200T, 0x13631093 an XC7A100T, and an even code is no IDCODE at all (BYPASS or a broken shift). Self-test: 17 of 17 ok.", + "url": "https://t27.ai/term/tri-fpga-jtag/", + "view": null, + "image": "https://t27.ai/term/tri-fpga-jtag/card.png", + "gif": "https://t27.ai/term/tri-fpga-jtag/session.gif", + "preview": "term/tri-fpga-jtag/index.html", + "commands": [ + "tri fpga-jtag --decode 0x03636093", + "tri fpga-jtag --decode 0x13631093", + "tri fpga-jtag --decode 0x12345678", + "tri fpga-jtag --self-test" + ], + "recorded": "2026-10-03 19:49 UTC", + "iframe": "", + "markdown": "[![tri fpga-jtag · which chip answered the cable](https://t27.ai/term/tri-fpga-jtag/session.gif)](https://t27.ai/term/tri-fpga-jtag/)" + }, + { + "id": "tri-fpga-pins", + "kind": "cast", + "category": "fpga", + "hook": "Before an edit to a Verilog file: every spec and generator it would touch.", + "title": "tri fpga-pins · the blast radius of an edit, before the edit", + "line": "Before editing trinet_node_core.v: 5 specs pin it by sha256 and 5 generators read those specs. radius: 5 specs, 1 level, the exact set fpga-repin walks after the edit.", + "url": "https://t27.ai/term/tri-fpga-pins/", + "view": null, + "image": "https://t27.ai/term/tri-fpga-pins/card.png", + "gif": "https://t27.ai/term/tri-fpga-pins/session.gif", + "preview": "term/tri-fpga-pins/index.html", + "commands": [ + "tri fpga-pins fpga/portable/trinet_node_core.v" + ], + "recorded": "2026-10-04 04:52 UTC", + "iframe": "", + "markdown": "[![tri fpga-pins · the blast radius of an edit, before the edit](https://t27.ai/term/tri-fpga-pins/session.gif)](https://t27.ai/term/tri-fpga-pins/)" + }, + { + "id": "devkit-flow", + "kind": "cast", + "category": "fpga", + "hook": "The whole FPGA flow, one layer per line, from spec to board.", + "title": "tri devkit · the FPGA flow, layer by layer", + "line": "One real XC7A200T build: 116.9 s with openXC7, 83.5 s with t27 L3+L4, byte-identical. Place & route is 60%.", + "url": "https://t27.ai/term/devkit-flow/", + "view": null, + "image": "https://t27.ai/term/devkit-flow/card.png", + "gif": "https://t27.ai/term/devkit-flow/session.gif", + "preview": "term/devkit-flow/index.html", + "commands": [ + "tri devkit flow --build --out /tmp/devkit-flow/rec1", + "tri devkit impact --out /tmp/devkit-flow/rec1 --builds 20" + ], + "recorded": "2026-10-03 04:31 UTC", + "iframe": "", + "markdown": "[![tri devkit · the FPGA flow, layer by layer](https://t27.ai/term/devkit-flow/session.gif)](https://t27.ai/term/devkit-flow/)" + }, + { + "id": "play-verilog", + "kind": "player", + "category": "fpga", + "hook": "A spec turned into Verilog inside a post, with the RTL read the way a hardware engineer reads it.", + "title": "hello_world.t27 to Verilog, in a terminal inside the post", + "line": "A terminal in your browser running the t27 compiler as WebAssembly: t27c gen verilog prints the Verilog it emits for hello_world.t27, and t27c test runs the spec's tests.", + "url": "https://t27.ai/play/hello-world/verilog/", + "view": null, + "image": "https://t27.ai/play/hello-world/verilog/card.png?v=ac89920ce957", + "gif": null, + "preview": "play/embed.html?spec=specs%2Fdemos%2Fhello_world.t27&cast=t27c-hello-world&tab=term&backend=verilog&v=ded547ceeb2f", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![hello_world.t27 to Verilog, in a terminal inside the post](https://t27.ai/play/hello-world/verilog/card.png?v=ac89920ce957)](https://t27.ai/play/hello-world/verilog/)" + }, + { + "id": "play-hello-world", + "kind": "player", + "category": "compiler", + "hook": "The t27 compiler running as WebAssembly in a post: compile, test, seven backends.", + "title": "hello_world.t27, compiled inside the post", + "line": "The t27 compiler, as WebAssembly, running on one spec in your browser: lexer to seven backends, the spec's own tests, the emitted code, its Verilog read for synthesis, and a terminal.", + "url": "https://t27.ai/play/hello-world/", + "view": null, + "image": "https://t27.ai/play/hello-world/card.png?v=a52d019c491d", + "gif": null, + "preview": "play/embed.html?spec=specs%2Fdemos%2Fhello_world.t27&cast=t27c-hello-world&v=ded547ceeb2f", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![hello_world.t27, compiled inside the post](https://t27.ai/play/hello-world/card.png?v=a52d019c491d)](https://t27.ai/play/hello-world/)" + }, + { + "id": "play-rust", + "kind": "player", + "category": "compiler", + "hook": "The same spec as Rust, printed by a terminal in the page.", + "title": "hello_world.t27 to Rust, in a terminal inside the post", + "line": "A terminal in your browser running the t27 compiler as WebAssembly: t27c gen rust prints the Rust it emits for hello_world.t27, and t27c test runs the spec's tests.", + "url": "https://t27.ai/play/hello-world/rust/", + "view": null, + "image": "https://t27.ai/play/hello-world/rust/card.png?v=a5b20a3b7864", + "gif": null, + "preview": "play/embed.html?spec=specs%2Fdemos%2Fhello_world.t27&cast=t27c-hello-world&tab=term&backend=rust&v=ded547ceeb2f", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![hello_world.t27 to Rust, in a terminal inside the post](https://t27.ai/play/hello-world/rust/card.png?v=a5b20a3b7864)](https://t27.ai/play/hello-world/rust/)" + }, + { + "id": "play-js", + "kind": "player", + "category": "compiler", + "hook": "The only backend that runs in the browser: generate the module, then run it.", + "title": "hello_world.t27 to JavaScript, in a terminal inside the post", + "line": "A terminal in your browser running the t27 compiler as WebAssembly: t27c gen js prints the JavaScript it emits for hello_world.t27, and t27c test runs the spec's tests.", + "url": "https://t27.ai/play/hello-world/js/", + "view": null, + "image": "https://t27.ai/play/hello-world/js/card.png?v=2c648964eea2", + "gif": null, + "preview": "play/embed.html?spec=specs%2Fdemos%2Fhello_world.t27&cast=t27c-hello-world&tab=term&backend=js&v=ded547ceeb2f", + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[![hello_world.t27 to JavaScript, in a terminal inside the post](https://t27.ai/play/hello-world/js/card.png?v=2c648964eea2)](https://t27.ai/play/hello-world/js/)" + }, + { + "id": "t27c-hello-world", + "kind": "cast", + "category": "compiler", + "hook": "t27c on hello_world.t27, on a real machine, every byte as printed.", + "title": "t27c on hello_world.t27", + "line": "t27c 0.4.0 on specs/demos/hello_world.t27: spec-status IMPLEMENTED, the two tests and two invariants the Zig backend emits, and the Verilog module with its parameters.", + "url": "https://t27.ai/term/t27c-hello-world/", + "view": null, + "image": "https://t27.ai/term/t27c-hello-world/card.png", + "gif": "https://t27.ai/term/t27c-hello-world/session.gif", + "preview": "term/t27c-hello-world/index.html", + "commands": [ + "t27c --version", + "t27c spec-status specs/demos/hello_world.t27", + "t27c gen specs/demos/hello_world.t27 | sed -n 48,71p", + "t27c gen-verilog specs/demos/hello_world.t27 | sed -n 10,26p" + ], + "recorded": "2026-10-04 06:14 UTC", + "iframe": "", + "markdown": "[![t27c on hello_world.t27](https://t27.ai/term/t27c-hello-world/session.gif)](https://t27.ai/term/t27c-hello-world/)" + }, + { + "id": "t27-tri-misread", + "kind": "cast", + "category": "compiler", + "hook": "Code that compiles and is wrong: the tool that finds it.", + "title": "tri misread: code that compiles and is wrong", + "line": "1,144 of 1,156 specs generate; 41 spec-shape pairs are read wrongly with every gate green.", + "url": "https://t27.ai/term/t27-tri-misread/", + "view": null, + "image": "https://t27.ai/term/t27-tri-misread/card.png", + "gif": "https://t27.ai/term/t27-tri-misread/session.gif", + "preview": "term/t27-tri-misread/index.html", + "commands": [ + "tri misread" + ], + "recorded": "2026-10-03 16:18 UTC", + "iframe": "", + "markdown": "[![tri misread: code that compiles and is wrong](https://t27.ai/term/t27-tri-misread/session.gif)](https://t27.ai/term/t27-tri-misread/)" + }, + { + "id": "wars", + "kind": "tab", + "category": "game", + "hook": "Pick a lane and see whether the TRI decision layer beats the baseline on the same issue.", + "title": "WARS: agent configurations compared on real GitHub issues", + "line": "One issue, one pinned commit, the same tools and gates; only the decision layer changes, and a missing run stays unknown.", + "url": "https://t27.ai/#/queen?tab=wars", + "view": "wars", + "image": null, + "gif": null, + "preview": null, + "commands": [], + "recorded": null, + "iframe": "", + "markdown": "[WARS: agent configurations compared on real GitHub issues](https://t27.ai/#/queen?tab=wars)" + }, + { + "id": "tri-game-tick", + "kind": "cast", + "category": "game", + "hook": "One tick of the Queen's loop: an anomaly scan that reports only what changed.", + "title": "tri game-tick: one anomaly scan, only what changed", + "line": "25 live checks on t27.ai and its publisher; prints only NEW red or CLEARED rows. This run: 0 red of 25.", + "url": "https://t27.ai/term/tri-game-tick/", + "view": null, + "image": "https://t27.ai/term/tri-game-tick/card.png", + "gif": "https://t27.ai/term/tri-game-tick/session.gif", + "preview": "term/tri-game-tick/index.html", + "commands": [ + "tri game-tick" + ], + "recorded": "2026-10-03 13:02 UTC", + "iframe": "", + "markdown": "[![tri game-tick: one anomaly scan, only what changed](https://t27.ai/term/tri-game-tick/session.gif)](https://t27.ai/term/tri-game-tick/)" + }, + { + "id": "tri-game-vault", + "kind": "cast", + "category": "game", + "hook": "The loop's skills checked against snapshots before an agent may use them.", + "title": "tri game-vault: the loop's skills checked against snapshots", + "line": "129 files intact against the newest of 54 content-addressed snapshots.", + "url": "https://t27.ai/term/tri-game-vault/", + "view": null, + "image": "https://t27.ai/term/tri-game-vault/card.png", + "gif": "https://t27.ai/term/tri-game-vault/session.gif", + "preview": "term/tri-game-vault/index.html", + "commands": [ + "tri game-vault --check" + ], + "recorded": "2026-10-03 12:59 UTC", + "iframe": "", + "markdown": "[![tri game-vault: the loop's skills checked against snapshots](https://t27.ai/term/tri-game-vault/session.gif)](https://t27.ai/term/tri-game-vault/)" + }, + { + "id": "perf2-knockouts", + "kind": "cast", + "category": "game", + "hook": "Three performance bits, and one knockout bitstream per bit to prove each one matters.", + "title": "Three PERF2 bits, and one knockout bitstream per bit", + "line": "The A/B bitstreams differ in three bits. Each knockout clears one: 3 bytes from the working .bit.", + "url": "https://t27.ai/term/perf2-knockouts/", + "view": null, + "image": "https://t27.ai/term/perf2-knockouts/card.png", + "gif": "https://t27.ai/term/perf2-knockouts/session.gif", + "preview": "term/perf2-knockouts/index.html", + "commands": [ + "python3 perf2.py diff bufr-old.frames bufr-new.frames", + "python3 perf2.py knockout bufr-new.frames --out rec" + ], + "recorded": "2026-10-03 19:53 UTC", + "iframe": "", + "markdown": "[![Three PERF2 bits, and one knockout bitstream per bit](https://t27.ai/term/perf2-knockouts/session.gif)](https://t27.ai/term/perf2-knockouts/)" + } + ], + "ideas": [ + { + "id": "placement-map", + "title": "Placement map", + "line": "Where nextpnr put each cell of a spec's design on the XC7A200T die, as a picture that can be posted.", + "source": "nextpnr-xilinx placement output of the bench builds" + }, + { + "id": "resource-bars", + "title": "LUT, FF and BRAM bars", + "line": "The resources one spec costs, as bars against the part's totals, beside the same design from another language.", + "source": "synthesis reports of the bench builds" + }, + { + "id": "slack-waterfall", + "title": "Timing slack waterfall", + "line": "Every path's slack from the SDF, sorted, with the failing ones at the top.", + "source": "nextpnr SDF, as tri fpga-txhold already reads it" + }, + { + "id": "bit-diff", + "title": "Bitstream diff proof", + "line": "Two bitstreams compared frame by frame: byte-identical, or the first frame that differs.", + "source": "tri x7-audit and the x7-board byte-identical comparison" + }, + { + "id": "test-waves", + "title": "Test waveforms", + "line": "A spec's test block drawn as a timing diagram, the way WaveDrom draws one.", + "source": "spec test blocks through the in-browser evaluator" + }, + { + "id": "gf16-calc", + "title": "GF16 and ternary calculator", + "line": "Type a number and see it as GF16 and as balanced ternary, bit by bit, with the format spec it obeys.", + "source": "FORMAT-SPEC-001.json and gf16.t27" + }, + { + "id": "race-the-bee", + "title": "Race the bee", + "line": "A real open issue, one human and one bee: whoever lands an accepted patch first takes the lane.", + "source": "specs/queen/wars.t27 and the bees' run ledger" + } + ] +} as const + +export type QueenWidgetsGallery = typeof QUEEN_WIDGETS diff --git a/apps/website/src/pages/Queen.css b/apps/website/src/pages/Queen.css index b6f77a028f..5c9dd27775 100644 --- a/apps/website/src/pages/Queen.css +++ b/apps/website/src/pages/Queen.css @@ -4309,12 +4309,14 @@ main:where(.queen27-page.is-shell) section { @media (max-height: 919px) { /* Nineteen commands (TOKEN, 2026-10-02) at 6 px gaps put the last one 4 px under COLLAPSE at 1272x806 (measured by the viewport contract); 4 px gaps - give back 36 px. */ + give back 36 px. WIDGETS (2026-10-05) made it twenty and put the last + one 22 px under COLLAPSE again; 3 px gaps and 0.26 rem rows give back + about 60 px more. */ .queen27-page.is-shell .queen27-hud-command:not(.is-compact) { - gap: 4px; + gap: 3px; } .queen27-page.is-shell .queen27-hud-command:not(.is-compact) > .queen27-hud-cmd { - padding: 0.35rem 0.65rem; + padding: 0.26rem 0.65rem; } .queen27-page.is-shell .queen27-hud-command:not(.is-compact) > .queen27-hud-cmd small { display: none; @@ -4341,15 +4343,17 @@ main:where(.queen27-page.is-shell) section { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(var(--queen-tile-rows, 6), max-content) minmax(0, 1fr) max-content; - gap: 4px; + gap: 3px; padding: 6px; } + /* ten tile rows (twenty commands) overran COLLAPSE by 10 px at 1280x600; + 0.24 rem tiles and 3 px gaps give back about 45 px */ .queen27-page.is-shell .queen27-hud-command > .queen27-hud-cmd { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; - gap: 0.15rem; + gap: 0.1rem; justify-items: center; - padding: 0.35rem 0.3rem; + padding: 0.24rem 0.3rem; text-align: center; } .queen27-page.is-shell .queen27-hud-command > .queen27-hud-cmd > i { diff --git a/apps/website/src/pages/Queen.tsx b/apps/website/src/pages/Queen.tsx index f0ee14cb90..e40f72055d 100644 --- a/apps/website/src/pages/Queen.tsx +++ b/apps/website/src/pages/Queen.tsx @@ -98,6 +98,7 @@ import { QueenTri } from "../components/QueenTri"; import QueenRoadmap from "../components/QueenRoadmap"; import QueenLeaderboard from "../components/QueenLeaderboard"; import { QueenWars } from "../components/QueenWars"; +import { QueenWidgets } from "../components/QueenWidgets"; import QueenToken from "../components/QueenToken"; import Passport from "./Passport"; import { QueenBrowser } from "../components/QueenBrowser"; @@ -523,6 +524,8 @@ const COPY = { // The seventh layer, on g (the GPU): who sells the compute the ladder spends. providersView: "PROVIDERS", providersHint: "Who sells compute: the Gonka models, and a GPU or FPGA rented out for TRI (key g)", + widgetsView: "WIDGETS", + widgetsHint: "Shareable widgets: recordings, the X player, embed code (key v)", providersDirective: "PROVIDERS ARE SPECS", providersDirectiveBody: "The seventh layer: Specs → Skills → Crons → Agents → Tools → Functions → Providers — who sells the compute the rest of the ladder spends. A provider exists when a .t27 spec under specs/providers states it: a model the Gonka chain lists, every number copied from a public chain endpoint on the date the card names (hosts serving it, GPU memory, context, validation threshold, price), or a class of hardware a person could rent out to TRI-NET for TRI — an Artix-7 FPGA measured on three boards, and a 24 GB gaming GPU that is a design and claims no measurement. The page re-reads the Gonka chain from your browser when you ask and prints every difference; it never calls a paid model, never asks for a key and never shows a host's address. Under both families sits specs/providers/tri_gnk_pair.t27: TRI is on testnet, Gonka has no DEX pool, and its bridge goes to Ethereum, not to TON or Solana.", @@ -1001,6 +1004,8 @@ const COPY = { "Слой, где спека встречается с работающим сервисом — шестой на лестнице этого сайта, после инструментов; specs/functions/README.md в t27 называет его пятым, как и specs/tools/README.md — инструменты, и два README расходятся: каждая функция Inngest бота 999-multibots-telegraf заявлена спекой .t27 в specs/functions — триггер, событие и старые события или крон, шаги в порядке исходника, повторы, действие при сбое, побочные эффекты, страж, безопасная проба и её результат — и засвидетельствована копией манифеста функций, прочитанного из репозитория на названном коммите. Где спека и манифест расходятся, карточка говорит об этом. Живые счётчики запусков читаются с бота раз в минуту и не придумываются: недоступный источник статуса показан как недоступный, а счётчик, которого он не прислал, — как «неизвестно», а не ноль.", providersView: "ПРОВАЙДЕРЫ", providersHint: "Кто продаёт вычисления: модели Gonka и GPU или FPGA в аренду за TRI (клавиша g)", + widgetsView: "ВИДЖЕТЫ", + widgetsHint: "Виджеты для репоста: записи, X-плеер, код встраивания (клавиша v)", providersDirective: "ПРОВАЙДЕРЫ — ЭТО СПЕКИ", providersDirectiveBody: "Седьмой слой: спеки → скиллы → кроны → агенты → инструменты → функции → провайдеры — кто продаёт вычисления, которые тратит вся остальная лестница. Провайдер существует, когда его заявляет спека .t27 в specs/providers: модель из списка цепи Gonka, где каждое число скопировано с публичного эндпоинта цепи в дату, которую называет карточка (сколько хостов её обслуживают, память GPU, контекст, порог валидации, цена), или класс железа, который человек мог бы сдать в аренду TRI-NET за TRI — FPGA Artix-7, измеренная на трёх платах, и игровая GPU на 24 ГБ, которая пока проект и не заявляет измерений. Страница по вашему запросу перечитывает цепь Gonka из вашего браузера и печатает каждое расхождение; она никогда не вызывает платную модель, не просит ключ и не показывает адрес хоста. Под обоими семействами лежит specs/providers/tri_gnk_pair.t27: TRI в testnet, у Gonka нет пула на DEX, а её мост ведёт в Ethereum, а не в TON или Solana.", @@ -3693,6 +3698,9 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) // On the letter g (the GPU): the seventh layer of the ladder, who sells // compute. Folded into SPECS like the other layers, so it has no rail button. { view: "providers" as const, glyph: "⌬", label: c.providersView, hint: c.providersHint }, + // On the letter v: what a reader can lift out of t27 and share, from + // specs/widgets/gallery.t27. Its own rail door, not folded. + { view: "widgets" as const, glyph: "⧉", label: c.widgetsView, hint: c.widgetsHint }, ].map((item) => ({ ...item, hotkey: hudKeyOf(item.view) })); // TRI is drawn as one button per screen, owner's word 2026-09-21: every // screen of the app its own tab. The first keeps TRI's key; the rest are @@ -4334,6 +4342,8 @@ export default function Queen({sharedCatalog}:{sharedCatalog?:UniverseAtlas}={}) /> ) : boardView === "wars" ? ( + ) : boardView === "widgets" ? ( + ) : boardView === "token" ? ( ) : boardView === "roadmap" ? (