Files
zeekay 7675352bc5 The deck draws the surface it sells, and every page of the appendix is resolved
Eleven new slides walk the platform. P0 is the twelve-call tour the hero already
tells — real operations, in the order somebody calls them, each with the verb,
the path and the document's own sentence. P1 through P10 are one product from
each of the ten layers slide 7 promises: what it is, its base path, the
operations under it, its page and its repository.

A page declares ONE thing, data-family on its section, and deck/sync.mjs
resolves the rest out of lib/data/catalog.json and lib/data/families.json — the
two snapshots scripts/sync-catalog.mjs takes of api.hanzo.ai/v1/openapi.json and
the commerce catalog, behind its own ratchets. The deck fetches nothing itself:
each of those files already has an owner that probes for it, and a second probe
here is how the deck and the site come to state different numbers. A family the
document stops carrying stops the sync rather than shipping a stale slide. The
same script now stamps every data-start from the slide's position, so inserting
one renumbers the rest.

A6 draws the whole surface: one squarified rectangle per family, its area in the
ratio of the operations under it, 183 of them and 2,441 operations. Every product
page redraws that map with its own family lit — one instrument seen eleven times
rather than eleven loose cards, and a five-operation family is findable in it.

The graphics pass on the twelve. The price now rides the score row as a second
bar on a log axis, so "the best score at a fraction of the price" is a shape and
not an adjective — long white on top, short white underneath. The ten layers
carry the products the catalog sells in each, drawn: a span is inline, so the
fill painted nothing at all until it was told to be a block. The decades between
what a call costs to serve and what the category charges for it are an axis, with
the multiple as the distance across it. The dormant agent sits INSIDE the
megabyte, in its corner, because that is what the comparison is. The cold-start
bars move to the cold-start sheet, which until now was a headline and one row.
A4 draws the distance left to close off its own table.

A label is what precedes a colon. `.*?` matched the empty string ahead of an
optional one, so every A1-A3 detail row fell back to the claim and the sheet
printed "A dormant agent" three times.

deck/shoot.mjs is the verification the contract already described, made
reproducible. It seeks each slide to its own still frame before measuring — the
paused timeline holds every part it has not reached 22px down, and measuring
through that reports a phantom 22 on a dozen unrelated boxes — and it measures a
second property beside overflow: a flex or grid box shrunk below its own content.
That is the failure a screenshot hides and an overflow check misses, because
nothing leaves the stage; the boxes just shrink, and slide 2's captions printed
through the footnote under them. Twenty-nine slides, no overflow and no squeeze
at 1280, 1600 and 2560, no console errors, both Zen faces loaded; hyperframes
check passes with 173/173 contrast.

deck/assets/logos/ was a second copy of the artwork MARK already holds inline,
and nothing read it. The laptop shot is retaken without the site's own navigation
bar cut across the top of it.
2026-09-07 18:22:26 -07:00

110 lines
5.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Page the deck the way a reader does, and photograph it. Two things a
// screenshot cannot tell you are measured here instead: whether any child
// overflows the 1920×1080 stage at a width nobody designed at, and whether the
// faces that actually painted came from @hanzo/font.
//
// node deck/shoot.mjs # measures; writes deck/snapshots/
//
// Run it from the repository root — the deck has no dependencies of its own and
// @playwright/test resolves there. The directory it serves is this one, so what
// is measured is exactly what a flat server hands a browser.
import { chromium } from '@playwright/test'
import http from 'node:http'
import fs from 'node:fs'
import path from 'node:path'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
const out = process.argv[2] ?? join(here, 'snapshots')
const TYPE = {
'.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json',
'.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2',
}
const srv = http.createServer((q, r) => {
const rel = decodeURIComponent(q.url.split('?')[0])
const f = join(here, rel === '/' ? 'index.html' : rel)
if (!f.startsWith(here) || !fs.existsSync(f)) { r.writeHead(404).end(); return }
r.writeHead(200, { 'content-type': TYPE[path.extname(f)] ?? 'application/octet-stream' })
fs.createReadStream(f).pipe(r)
}).listen(0)
await new Promise((r) => srv.once('listening', r))
const url = `http://127.0.0.1:${srv.address().port}/`
fs.mkdirSync(out, { recursive: true })
let failed = 0
const browser = await chromium.launch()
for (const w of [2560, 1600, 1280]) {
const pg = await browser.newPage({ viewport: { width: w, height: Math.round((w * 9) / 16) }, deviceScaleFactor: 1 })
const errors = []
pg.on('console', (m) => m.type() === 'error' && errors.push(m.text()))
pg.on('pageerror', (e) => errors.push(String(e)))
await pg.goto(url, { waitUntil: 'networkidle' })
await pg.waitForFunction(() => document.fonts.status === 'loaded')
// The faces reporting loaded is not the reflow they cause. Measuring in the
// frame after that flag flips reads pre-reflow metrics, which is why the same
// page measured clean at one width and squeezed at the next.
await pg.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))))
await pg.waitForTimeout(250)
const n = await pg.evaluate(() => document.querySelectorAll('.slide').length)
if (w === 2560) {
const faces = await pg.evaluate(() => [...document.fonts].map((f) => `${f.family} ${f.status}`))
console.log(`faces ${faces.join(' · ')}`)
// The pager starts on the first slide, so photograph BEFORE advancing —
// pressing first would name slide two `s01` and never show slide one.
for (let i = 0; i < n; i++) {
await pg.waitForTimeout(90)
await pg.screenshot({ path: join(out, `${String(i + 1).padStart(2, '0')}.png`) })
await pg.keyboard.press('ArrowRight')
}
}
// Two measurements a screenshot will not give you, each slide taken at its
// OWN still frame. Measuring the lot in one pass reads the paused timeline's
// opening state — every part of every slide it has not reached yet is still
// held 22px down by its own entrance — and that offset lands in the numbers as
// a phantom 22 on a dozen unrelated boxes. Seeking first is also what a reader
// and a renderer both do, so this measures the frame they will see.
//
// OVERFLOW: every child against the stage's own rect. The stage is scaled, so
// this is in painted pixels and a fraction is rounding — a whole pixel is a
// slide that is off the edge.
//
// SQUEEZE: a slide is a flex column, so when its contents are too tall the
// BOXES shrink and stay inside the stage while the TEXT prints straight
// through the caption below it. Nothing overflows and the page looks wrong,
// which is exactly the shape this was blind to on slide 2 — a band whose
// labels ran into the footnote. Flex and grid boxes only: those are the ones
// that can be shrunk, and a heading set at line-height 1.03 reports a taller
// scroll box than its own line without anything having gone wrong.
const bad = await pg.evaluate(() => {
const tl = window.__timelines.deck
const slides = [...document.querySelectorAll('.slide')]
const hold = Number(slides[1].dataset.start) - Number(slides[0].dataset.start)
const over = [], tight = []
slides.forEach((s, i) => {
tl.seek(i * hold + hold / 2, false)
tl.pause()
const r = s.getBoundingClientRect()
for (const e of s.querySelectorAll('*')) {
const b = e.getBoundingClientRect()
if (!b.width && !b.height) continue
if (b.right > r.right + 1 || b.bottom > r.bottom + 1 || b.left < r.left - 1 || b.top < r.top - 1)
over.push(`${s.id}·${e.className || e.tagName} r${(b.right - r.right).toFixed(0)} b${(b.bottom - r.bottom).toFixed(0)}`)
if (/flex|grid/.test(getComputedStyle(e).display) && e.scrollHeight > e.clientHeight + 2)
tight.push(`${s.id}·${e.className || e.tagName} +${e.scrollHeight - e.clientHeight}`)
}
})
return { over, tight }
})
failed += bad.over.length + bad.tight.length + errors.length
console.log(`${w}px slides=${n} overflow=${bad.over.length ? bad.over.slice(0, 8).join(' ; ') : 'none'}` +
` squeezed=${bad.tight.length ? bad.tight.slice(0, 8).join(' ; ') : 'none'}` +
` errors=${errors.length ? errors.join(' | ') : 'none'}`)
await pg.close()
}
await browser.close()
srv.close()
process.exit(failed ? 1 : 0)