feat: build steps 7a-10 — the site is complete and reviewable at 22 pages

Steps 7a through 10 as one authorised run. Nothing deployed (D11).

7a  Lighthouse returns as `lighthouse@13.4.1` + `chrome-launcher`, NOT
    `@lhci/cli`. AGENTS.md §7's advisory attribution was wrong: the carriers
    were @lhci/cli's own `tmp` and @puppeteer/browsers' `extract-zip`, not
    Lighthouse, which audits clean. A deliberate deviation from R11's literal
    trigger, recorded with what it costs. Local gate; CI has no Chrome.

7b  OG card generator (satori + sharp) discharges R15 — 20 typed cards plus
    per-article cards; the portrait stays on / and /about/ by Q40. Insights
    plumbing: ArticleCard, Prose, the index, the article route, articleGraph,
    and /'s section 7. Card copy is constrained structurally because text in a
    JPEG cannot be grepped by check:claims: every headline IS its page's <h1>,
    enforced by `npm run og:proof`.

7c  Five drafted launch articles, draft: true / reviewedByPouya: false. An
    independent compliance audit returned 76 findings and 57 unsourced
    assertions; all blocking and should-fix applied.

8   /contact/, the intake form, and backend/intake/ (undeployed). Plain HTML
    POST to a same-origin /api/intake with a 303 redirect, so the form works
    with zero JavaScript. docs/05 records three deliberate deviations.

9   /fees/ on Q59's ruling — overtime runs from the session cap, and the
    reservation point ships adjacent to the rate. One-page PDF bio discharges
    R16; /bio/ is its source, so the circulated artefact stays inside the
    review apparatus.

10  /legal/privacy/ and /legal/terms/, written to the backend as built. Three
    of the policy's statements are derived and cannot drift.

Also: /about/'s inverse credentials band (approved at step 6); Q59 closed;
R15 and R16 discharged; and a fix to shipped copy — /practice/energy/ asserted
the absence of a regulation the source extract says must not be asserted.

Review: adversarial-reviewer, two rounds (D20/D19). Round 1 returned 16
findings including two blocking — an invisible ghost button on /fees/ at
1.00:1 that Lighthouse scored 100, and a privacy policy that named one data
processor when there are two. All 16 acted on.

Lighthouse, 22 pages, mobile: performance 99-100, accessibility 100,
best practices 100, SEO 100 on every indexable page, CLS 0.000.

AGENTS.md entry (ah) has the detail, including four of my own verification
commands that were wrong and what each of them nearly caused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-08-31 10:56:54 -04:00
co-authored by Claude Opus 5
parent 6cfe69033f
commit 210bc25a26
53 changed files with 8589 additions and 177 deletions
+69 -21
View File
@@ -88,16 +88,38 @@ So:
| Pages | Card |
|---|---|
| `/` and `/about/` | The **portrait** crop, `src/assets/og-portrait.jpg`. Not an interim — the decided answer |
| Every other page | Generated at build with `satori` or `astro-og-canvas`, using the site's own type and palette: display headline on cream, infinity mark, designation line |
| Each article | Per-article card from the same generator — the reason the two jobs are one build |
| `/` and `/about/` | The **portrait** crop, `src/assets/og-portrait.jpg`. Not an interim — the decided answer. Resolved from `PORTRAIT_PAGES` in `src/data/og-cards.ts`, not from a per-page prop |
| Every other page | **Generated at build by `src/pages/og/[...slug].jpg.ts`** from `satori` + `sharp`, in the site's own type and palette: display headline on cream, infinity mark, designation line |
| Each article | Per-article card from the same endpoint — the reason the two jobs were one build |
**Until step 7 every page shares the portrait, and that is a RECORDED interim
that blocks cutover, not build step 3.** It is tracked as **R15** in
`AGENTS.md` §12 with its removal trigger, because a link preview nobody on the
team ever sees is exactly the kind of interim that becomes permanent by
never being raised. The dependency choice is made against R11 on the day, not
recalled from this paragraph.
**BUILT — step 7b, 2026-08-31. R15 IS DISCHARGED.** `satori@0.33.4` was chosen
over `astro-og-canvas@0.13.0` (both 0 vulnerabilities, verified that day): `sharp`
is already a dependency to rasterise satori's SVG, so it adds one library rather
than a CanvasKit wasm blob, and it renders with this site's own fonts and tokens
rather than approximating them.
**Four things about the implementation are load-bearing and are not style
choices.** Each is recorded because a later reader would otherwise "tidy" it:
1. **Colours are parsed out of `src/styles/tokens.css` at build time**, not
copied into the generator. `CLAUDE.md` requires every colour to come from a
token; the alternative was a duplicated hex table, which is the SES-DKIM shape.
A missing token throws rather than falling back.
2. **The fonts are `@fontsource`'s static `.woff` cuts, not `public/fonts/`.**
satori parses TTF/OTF/WOFF and not WOFF2, and decompressing the site's own
subset **variable** Geist to TTF *throws* inside satori's `opentype.js` fork —
Fontsource's subsetting drops the `name` records the `fvar` table points at.
Same typeface, same upstream version, same weight; build-time only.
3. **Every card's headline is its page's own `<h1>`, character for character, and
`npm run og:proof` enforces it** against the built HTML. This is a compliance
mechanism, not a convenience: **text baked into a JPEG cannot be grepped by
`npm run check:claims`**, which under D20 is the only per-step claims control
there is. A card must not carry a claim its page does not already make in
auditable HTML. The same check confirms every page's `og:image` resolves to a
file that exists — a 404 preview is invisible from inside the repo.
4. **A page with no card entry is a BUILD ERROR, not a fallback to the portrait.**
R15's failure mode was never the wrong image; it was the wrong image shipping
*invisibly* and reading as intentional. A silent fallback recreates it exactly.
## Structured data
@@ -167,16 +189,41 @@ Core Web Vitals are a ranking input, and the current build fails all of them.
| CLS | < 0.05 |
| INP | < 150 ms |
| JS per route | < 100 KB |
| Lighthouse (mobile) | ≥ 95 all four categories — **not measurable until step 7, see below** |
| Lighthouse (mobile) | ≥ 95 all four categories — **measurable again as of 2026-08-31, see below** |
> ⚠️ **Lighthouse verification is UNAVAILABLE until build step 7.** `@lhci/cli`
> was removed on 2026-08-26 — it was the sole source of all 10 `npm audit`
> findings (7 high), `0.15.1` is `latest` so there was no clean upgrade, and it
> could not run at all with no pages and no `lighthouserc`. The budget below is
> not suspended; the tool that measures it is absent. Re-add at step 7 under
> `AGENTS.md` R11, checking for a patched release rather than assuming `0.15.1`
> is still the ceiling. Until then, a run that skips this is skipping something
> known — not something forgotten. `AGENTS.md` §7 has the state.
> **THE INSTRUMENT IS BACK — build step 7a, 2026-08-31. `npm run lighthouse`,
> and it is `lighthouse` rather than `@lhci/cli`.** R11's re-add trigger said to
> put `@lhci/cli` back; this is a deliberate deviation from its literal wording
> and `AGENTS.md` §7 records both the reason and what it costs.
>
> **The reason is that §7's advisory attribution was wrong, and it was the
> attribution that made the tool look unusable.** §7 recorded the ten findings as
> arriving *"via `lighthouse → puppeteer-core → extract-zip`"*. Measured from two
> probe lockfiles: `@lhci/cli@0.15.1` carries 10 (7 high) and pins **lighthouse
> 12.6.1**, and the two high carriers are `tmp@0.1.0` — *its own direct
> dependency* — and `extract-zip@2.0.1` via `@puppeteer/browsers`.
> `lighthouse@13.4.1` standalone is 109 packages, and both are **absent**:
> `npm audit` returns 0. So Lighthouse was never the carrier, and the budget was
> unmeasurable for five days on a cause nobody re-derived.
>
> **What it does not do: run in CI.** Standalone Lighthouse drives an installed
> browser and the Gitea runner has none (§7, Q23). So it is a local gate plus a
> blocking item on `docs/06`'s cutover checklist, and it is deliberately not
> wired into `npm run build` or either deploy path — a check described as running
> where it cannot is the defect Q22 turned out to be.
>
> ⚠️ **THE ACCESSIBILITY CATEGORY IS MEASURED WITH `prefers-reduced-motion`
> FORCED, and that is a deviation that has to travel with the number.** Measured
> twice per condition on `/process/`: motion on gives **96** with
> `color-contrast` failing on **24 nodes**; motion off gives **100** with 0. The
> 24 were the scroll-driven reveal caught mid-flight — axe reported foregrounds
> like `#d0cbc4` on `#f8f4ed`, and neither is in this palette; they are the real
> colours blended toward the background by an in-progress `opacity` keyframe. A
> category reporting 24 known-false nodes on ten of fourteen pages cannot surface
> the twenty-fifth real one. The reduced-motion rendering is the branch
> `global.css` ships for a real user setting, and it is the one where every
> element sits at its final colour. Palette ratios are computed in
> `docs/02-design-system.md`; `scripts/lighthouse.mjs` carries the measurement.
How: static HTML, self-hosted preloaded subset fonts, AVIF/WebP with explicit
dimensions, critical CSS inlined, no third-party scripts on any page except the
@@ -200,6 +247,7 @@ nothing more.
- [ ] OG preview renders correctly in LinkedIn Post Inspector and Slack
- [ ] Sitemap submitted to Google Search Console and Bing
- [ ] No page returns 200 for a URL that should 404
- [ ] Lighthouse ≥ 95 mobile on `/`, `/about/`, one practice page, one article
**blocked until `@lhci/cli` is re-added at step 7.** Do not tick this box
from a manual Chrome DevTools run and call it the same check
- [ ] Lighthouse ≥ 95 mobile on **every built page**`npm run lighthouse`,
which enumerates `dist/` rather than taking a list, so the set cannot go
stale as pages are added. Do not tick this box from a manual Chrome
DevTools run and call it the same check