Skip to content

feat(website): widget share cards drawn from their spec, with a staleness check - #1471

Open
gHashTag wants to merge 9 commits into
mainfrom
feat/widget-cards-from-spec
Open

gHashTag wants to merge 9 commits into
mainfrom
feat/widget-cards-from-spec

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

What

npm run cards:widgets (apps/website) draws the share card public/widgets/<id>/card.png of every widget tool in specs/widgets/gallery.t27 that has none. --id <id> draws one widget's card and --all draws every one. The card is black and white, and its words come from the widget spec: TITLE, DESCRIPTION and CATEGORY, plus the gallery's TOOL_BRAND, CATEGORY_NAMES and ORIGIN. Nothing on it is drawn by hand.

npm run check:widget-cards fails when a gallery tool has no card, or when a drawn card no longer matches its spec. It runs in website-checks as the "Widget cards" step, right after "Widget tool pages".

How

  • Pure node, no new dependency, no browser. scripts/widget-cards-from-spec.mjs does everything itself:
    • It decodes the site's own WOFF2 fonts with node's brotli. That covers the glyf/loca transform, cmap and hmtx.
    • It applies gvar so Outfit renders at weight 700.
    • It fills outlines with a scanline nonzero rasterizer: 4 sub-rows per pixel, exact horizontal coverage.
    • It writes an 8-bit grey PNG with its own fixed-Huffman deflate and CRC. Node's zlib is not used to compress, because its output can vary between builds.
  • Deterministic. The same spec gives a byte-identical PNG: drawing gatle twice gives sha-256 3850b6cd... both times. A card is about 45 KB and takes 1 to 2 s.
  • Drawn cards are marked. A drawn card carries the tEXt chunk Software=widget-cards-from-spec, plus Source=specs/widgets/<id>.t27. --check redraws marked cards in memory and compares bytes. The 27 hand-drawn cards on main have no mark, so the check only requires them to exist and be 1200x630; none is replaced here.
  • Pages follow the cards. After writing a card, the script re-runs widget-pages-from-spec.mjs, because a widget page's og:image link carries ?v=<card sha>.
  • scripts/widget-cards-from-spec.test.mjs, 6 tests:
    • Deflate round-trips through inflate.
    • The PNG has the right size, chunks and ink.
    • Two draws are byte-equal.
    • The fonts decode.
    • Missing, stale and current cards, plus a hand-drawn card and one that is not a PNG.

Proof (local, Node 22, on 5bdcf8e's tree)

  1. rm public/widgets/gatle/card.png, then npm run check:widget-cards gives rc 1: 1 problem(s), with the missing card named.
  2. npm run cards:widgets draws the card (46,566 bytes) and rewrites the 27 widget pages. check:widget-cards is then rc 0, reporting 1 drawn from spec and current, 26 drawn by hand and present.
  3. npm run cards:widgets -- --id gatle draws 0 cards: the sha-256 is unchanged, 3850b6cd...
  4. Change gatle's TITLE from "today's t27 module" to "today's t27 spec". check:widget-cards gives rc 1: public/widgets/gatle/card.png is stale: .... Then cards:widgets -- --id gatle redraws it, after which both check:widget-cards and check:widget-pages are rc 0.
  5. git checkout -- restores the spec, the card, the page and the spec copy. After that, check:widget-pages and check:widget-cards are rc 0 on the clean tree.

Other checks:

  • npx tsc --noEmit is rc 0.
  • The blog-cover tests pass 12 of 12.
  • The lefthook pre-commit hooks (queen and explorer language contracts) and the commit-msg hook pass.

Also in this PR

  • Blog post a-widget-card-drawn-from-its-spec, with an English body and a Russian ruBody. It is registered at the top of index.ts and posts.ts.
  • One AGENTS.md line pointing at cards:widgets, under "New course to open a topic". That section is not on main yet; it arrives with docs(agents): the Queen's recipe for a new 27-lesson course #1465. This PR adds the heading plus the one line at the same place, so whichever of the two merges second has a one-hunk conflict: keep docs(agents): the Queen's recipe for a new 27-lesson course #1465's section and append this line.
  • This PR adds new JavaScript (a generator and its test), hence the owner-approved-foreign label.

Limits

  • It ran on one machine. Cross-machine byte equality is argued from the arithmetic (IEEE doubles, no zlib); the CI run is its first test.
  • There is no kerning and no hinting. The fonts are Latin subsets, so cards are English only.
  • It covers widget tools only. Lesson, player and recording cards keep their own generators.
{
  "version": 1,
  "head_sha": "266446729eac0e46de8e73a5b6b92c94cfdbfbd9",
  "summary": "Widget share cards are now generated from each widget's t27 spec in black and white by a pure-node renderer, and a CI check fails on a missing card or a drawn card that lags its spec.",
  "changes": [
    "apps/website/scripts/widget-cards-from-spec.mjs: WOFF2 + gvar font decoder, scanline rasterizer, own deflate PNG writer, CLI with --id, --all and --check",
    "apps/website/scripts/widget-cards-from-spec.test.mjs: 6 tests for deflate, PNG shape, byte-identical redraw, fonts and the check's verdicts",
    "apps/website/package.json: cards:widgets and check:widget-cards",
    ".github/workflows/website-checks.yml: Widget cards step after Widget tool pages",
    "apps/website/src/data/blog: post a-widget-card-drawn-from-its-spec (body and ruBody), registered in index.ts and posts.ts",
    "AGENTS.md: one line under New course to open a topic pointing at cards:widgets"
  ],
  "tests": [
    {
      "command": "rm public/widgets/gatle/card.png; npm run check:widget-cards; npm run cards:widgets; npm run check:widget-cards",
      "result": "rc 1 naming the missing card, then one card drawn (46566 bytes) and pages rewritten, then rc 0",
      "status": "passed",
      "evidence": "local runs on the tree of 5bdcf8ea"
    },
    {
      "command": "edit gatle TITLE; npm run check:widget-cards; npm run cards:widgets -- --id gatle; check:widget-cards and check:widget-pages; git checkout",
      "result": "rc 1 calling the card stale, redraw, both checks rc 0, tree restored and both checks rc 0 again",
      "status": "passed",
      "evidence": "local runs on the tree of 5bdcf8ea"
    },
    {
      "command": "npm run cards:widgets -- --id gatle twice; shasum -a 256",
      "result": "second run writes 0 cards; sha-256 3850b6cd... unchanged",
      "status": "passed",
      "evidence": "local run on the branch head fec4d4fa1"
    },
    {
      "command": "node --test scripts/widget-cards-from-spec.test.mjs and the blog-cover tests; npx tsc --noEmit",
      "result": "6 of 6 and 12 of 12 pass; typecheck rc 0",
      "status": "passed",
      "evidence": "local node --test summaries"
    },
    {
      "command": "lefthook pre-commit and commit-msg",
      "result": "queen and explorer language contracts pass, subject is English ASCII",
      "status": "passed",
      "evidence": "hook output on 5bdcf8ea"
    }
  ],
  "limitations": [
    "Byte equality across machines is argued from the arithmetic and not yet measured; the CI run is its first test",
    "None of the 27 hand-drawn cards on main is replaced, and a stale hand-drawn card still passes the check",
    "No kerning or hinting, and the fonts are Latin subsets, so the cards are English only",
    "Only widget tools are covered; lesson, player and recording cards keep their own generators",
    "The AGENTS.md section heading also arrives with #1465, so the second of the two to merge has a one-hunk conflict"
  ],
  "tags": [
    "t27",
    "widgets",
    "cards"
  ],
  "blog": {
    "title": "A widget's share card, drawn from its spec",
    "summary": "Widget share cards are no longer drawn by hand: a node script draws them in black and white from the widget's t27 spec, byte for byte the same each time.",
    "outline": [
      "All 27 widget tool cards on t27.ai were drawn by hand, and nothing checked that a card still said what its spec says.",
      "The card's title, line, brand and category come from the widget spec and the gallery spec, read through the site's own t27 compiler.",
      "A browser screenshot is not byte-stable, so the script decodes the fonts, rasterizes and writes the PNG itself with node alone.",
      "The proof deletes a card, then changes a title, and shows the check fail, the generator fix it and the check pass again each time.",
      "The post names the limits: one machine so far, no kerning, Latin fonts only, and hand-drawn cards held only to being present."
    ]
  }
}

🤖 Generated with Claude Code

gHashTag and others added 3 commits October 7, 2026 18:13
Move the course-27 recipe into specs/course_recipe as two t27 modules and
point AGENTS.md at them, with the blog post for this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ness check

npm run cards:widgets draws public/widgets/<id>/card.png in black and white
from the widget spec's TITLE and DESCRIPTION and the gallery's words, in node
alone: its own WOFF2 decoder (glyf transform, gvar for bold), scanline
rasterizer and fixed-Huffman deflate, so the same spec gives the same bytes.
Default: gallery tools with no card; --id <id>; --all. Writing a card re-runs
widget-pages-from-spec (the og:image carries the card's hash).

check:widget-cards fails on a gallery tool with no card and on a drawn card
(tEXt Software mark) that no longer matches its spec; hand-drawn cards are held
to being present. Wired into website-checks next to the widget gates.

Also: blog post a-widget-card-drawn-from-its-spec, and one AGENTS.md line in
the course recipe section pointing at cards:widgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag gHashTag added the owner-approved-foreign Owner approved hand-written non-t27 code in this PR label Oct 7, 2026
gHashTag and others added 4 commits October 7, 2026 19:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s of 1

The owner's rule was 9 modules of 3 lessons (27 lessons); the recipe had
flattened it to 27x1. Constants, tests, roadmap and the AGENTS.md section
now say 9x3; the roadmap's 9 parts per course are the 9 modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cipe

# Conflicts:
#	apps/website/src/data/blog/index.ts
#	apps/website/src/data/blog/posts.ts
…feat/widget-cards-from-spec

# Conflicts:
#	AGENTS.md
#	apps/website/src/data/blog/index.ts
#	apps/website/src/data/blog/posts.ts
@github-actions github-actions Bot added the status:in-progress 🔵 Agent working label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

X Brain Health Check

Score: 100.0/100
Status: 🟢 HEALTHY
Threshold: 80/100

X Brain is above merge threshold

…m-spec

# Conflicts:
#	.github/workflows/website-checks.yml
#	apps/website/src/data/blog/index.ts
#	apps/website/src/data/blog/posts.ts
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

🧪 Brain Stress Test Results

Tests: 261/261 passed, 0 leaked
Status: 🟢 PASS
Gate: every test in zig build test-brain-stress passes and none leaks

Two post-registry entries began their slug: line inside the previous
entry's ru.openQuestions array: the main-first + head-after union kept
both conflict fragments but dropped the array/object closers that sat
between them. tsc -b reports 24 syntax errors and the typecheck ratchet
refuses the file (0 -> 24). Each seam gets the four closing lines a
healthy entry boundary already carries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

owner-approved-foreign Owner approved hand-written non-t27 code in this PR status:in-progress 🔵 Agent working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant