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
+411 -6
View File
@@ -703,9 +703,10 @@ the audience it targets. Revisit at month 1218. `[verified 2026-08-25 — dec
| Thing | Value |
|---|---|
| Framework | **Astro 7.2.9**, `output: 'static'` `[verified 2026-08-27 — npm view astro version, D1 as amended]`. `@astrojs/mdx` 7.0.8, `@astrojs/sitemap` 3.7.3, `sharp` 0.35.4 — all three at `latest`. Bumped from 7.2.7 at the step 1 → step 2 phase boundary under R11: 7.2.8 published 2026-08-26 and 7.2.9 on 2026-08-27, i.e. **two patches appeared inside 48 hours**, which is the argument for checking at boundaries rather than on failure. `engines` unchanged at `node >=22.12.0`, `npm >=9.6.5` `[verified 2026-08-27 — npm view astro@7.2.9 engines]` |
| **TypeScript — HELD at 6.x, and the hold is a peer constraint rather than a preference** | Pinned `^6.0.3`; `npm view typescript version` returns **7.0.2** `[verified 2026-08-31 — R11, step 67 phase boundary; 13 of 14 pins current, this the only hold]`. **The removal trigger was re-checked rather than recalled:** `npm view @astrojs/check@0.9.10 peerDependencies` still returns `{ typescript: '^5.0.0 || ^6.0.0' }`, so the hold stands. ⚠️ **AND THERE ARE TWO GATES, NOT ONE — corrected 2026-08-31, and this row named only the first for two days.** `npm view typescript-eslint peerDependencies` returns `{ typescript: '>=4.8.4 <6.1.0' }`, which is **tighter**: widening `@astrojs/check` alone would not lift the hold, so the trigger as recorded was unreachable. **A second consequence, live:** the pin is a caret, so if a 6.1.x ever ships, a plain `npm install` moves the tree into a peer-range breach with nothing in the repo saying so. Nothing is breached today — `npm ls typescript` resolves **6.0.3**, which is the highest stable 6.x on the registry. Found by running R11's own check rather than reading the row. **One major behind, which is the exact shape D1 was amended over — so the reason is recorded rather than the hold being silent.** `@astrojs/check@0.9.10` declares `peerDependencies: { typescript: '^5.0.0 \|\| ^6.0.0' }` `[verified 2026-08-29 — npm view @astrojs/check@0.9.10 peerDependencies]`, and `npm run check` is `astro check`, which is the type gate the deploy path runs before it builds. **Removal trigger: `@astrojs/check` AND `typescript-eslint` both widen to include 7** — either alone is not enough. Re-check at every phase boundary under R11 — this is a hold on a dependency's schedule, not on a judgement, so it lifts without a decision |
| **TypeScript — HELD at 6.x, and the hold is a peer constraint rather than a preference** | Pinned `^6.0.3`; `npm view typescript version` returns **7.0.2** `[verified 2026-08-31 — R11, re-run at the step 1011 boundary after the run added five dependencies: **18 of 19 pins current**, this the only hold, `npm audit` 0 vulnerabilities. The earlier run that day, at the step 6 → 7 boundary, read 13 of 14]`. **The removal trigger was re-checked rather than recalled:** `npm view @astrojs/check@0.9.10 peerDependencies` still returns `{ typescript: '^5.0.0 || ^6.0.0' }`, so the hold stands. ⚠️ **AND THERE ARE TWO GATES, NOT ONE — corrected 2026-08-31, and this row named only the first for two days.** `npm view typescript-eslint peerDependencies` returns `{ typescript: '>=4.8.4 <6.1.0' }`, which is **tighter**: widening `@astrojs/check` alone would not lift the hold, so the trigger as recorded was unreachable. **A second consequence, live:** the pin is a caret, so if a 6.1.x ever ships, a plain `npm install` moves the tree into a peer-range breach with nothing in the repo saying so. Nothing is breached today — `npm ls typescript` resolves **6.0.3**, which is the highest stable 6.x on the registry. Found by running R11's own check rather than reading the row. **One major behind, which is the exact shape D1 was amended over — so the reason is recorded rather than the hold being silent.** `@astrojs/check@0.9.10` declares `peerDependencies: { typescript: '^5.0.0 \|\| ^6.0.0' }` `[verified 2026-08-29 — npm view @astrojs/check@0.9.10 peerDependencies]`, and `npm run check` is `astro check`, which is the type gate the deploy path runs before it builds. **Removal trigger: `@astrojs/check` AND `typescript-eslint` both widen to include 7** — either alone is not enough. Re-check at every phase boundary under R11 — this is a hold on a dependency's schedule, not on a judgement, so it lifts without a decision |
| Lint toolchain | ESLint **10.9.1**, `@eslint/js` 10.0.1, `globals` 17.11.0, `eslint-plugin-astro` **3.1.0**, `eslint-plugin-jsx-a11y` 6.10.2, `typescript-eslint` 8.68.0, `typescript` **6.0.3****every pin at the registry's `latest` except `typescript`** `[verified 2026-08-30 — npm view, all 14 pins, R11 at the step 5 → 6 boundary; thirteen current, one held]`. `@eslint/js` and `globals` are declared explicitly; before 2026-08-26 `eslint.config.js` imported them and they resolved only by npm hoisting accident. **Accessibility linting is on**`flat/jsx-a11y-recommended`, 36 rules `[verified 2026-08-26 — 7 rules fired on a deliberately inaccessible .astro file]`. `eslint-plugin-jsx-a11y@6.10.2` declares a stale `eslint ^3..^9` peer range; a one-line `overrides` entry in `package.json` resolves it. ⚠️ **`eslint-plugin-astro@3.1.0` declares `node ^22.22.3 \|\| ^24.16.0 \|\| >=26.3.0`**, which excludes Pouya's Node 25.6.0 — `npm install` prints EBADENGINE there. Dev-time only, and `nvm use` (Node 22 LTS, per `.nvmrc`) clears it. **`typescript` — the hold, and it was too wide by a whole major until 2026-08-27.** `latest` is **7.0.2** and it is unusable here: `typescript-eslint@8.68.0` peers `typescript >=4.8.4 <6.1.0` and `@astrojs/check@0.9.10` peers `^5.0.0 \|\| ^6.0.0`, so taking 7 breaks **both** the linter and `astro check` `[verified 2026-08-27 — npm view <pkg> peerDependencies]`. **But this row previously read "held at 5.x", and 6.0.3 is a stable release both peers accept** — so the pin sat a full major behind for no reason the record could name, in a row whose whole job is to name the reason. Now at **6.0.3**, the newest version compatible with both peers; `npx tsc --version` reports 6.0.3 and the full gate is green `[verified 2026-08-27]`. The 6.x listing is mostly `-dev` and `-beta` tags; **6.0.2 and 6.0.3 are the only stable 6.x releases**, which is why `npm view typescript version` (7.0.2) is not the number to pin against here. Re-check at the next phase boundary: the hold ends when `typescript-eslint` widens its peer range past `<6.1.0` |
| Lighthouse CI | **Not installed, and that is now stated wherever the check is claimed.** `@lhci/cli` was the sole source of all 10 `npm audit` findings (7 high, via `lighthouse → puppeteer-core → extract-zip`), 0.15.1 is `latest` so there was no clean upgrade, and it cannot run at all — no pages, no `lighthouserc`. Removed 2026-08-26; **`npm audit` now reports 0 vulnerabilities** `[verified 2026-08-26 — npm audit]`. Removing it left a **documented control that no longer exists**`docs/04`, `CLAUDE.md` and `/build` Phase 5 all required a run that had silently become impossible, which is Q22's defect in a different costume. Fixed 2026-08-26: the unavailability is stated in `docs/04-seo-spec.md` (budget table, the callout under Performance, and the post-launch checklist), `CLAUDE.md` (performance budget and the definition of done), `.claude/commands/build.md` Phase 5, `docs/06-deployment.md` (PR checks and the cutover checklist), and `.claude/agents/adversarial-reviewer.md` §4 — which is also told **not** to raise the absence as a finding. **Re-add at build step 7** with a pin verified that day and a check for a patched release; do not assume 0.15.1 is still the ceiling (R11) |
| OG card generator | **`satori` 0.33.4 + `sharp`** (already a dependency), plus **`@fontsource/geist` 5.3.0** and **`@fontsource/instrument-serif` 5.3.0** as devDependencies — all four at `latest`, `npm audit` **0 vulnerabilities** with them in the tree `[verified 2026-08-31 — npm view, npm audit]`. Added at build step 7b; discharges R15. Chosen over `astro-og-canvas` 0.13.0 (also 0 vulnerabilities): `sharp` was already here to rasterise satori's SVG, so it is one library rather than a CanvasKit wasm blob, and it renders the site's real fonts and tokens instead of approximating them. ⚠️ **THE FONT PACKAGES ARE A WORKAROUND AND THE REASON IS MEASURED, NOT ASSUMED.** satori parses TTF/OTF/WOFF and **not WOFF2**, and decompressing this site's own `public/fonts/geist-latin-wght-normal.woff2` to TTF then **throws inside satori's `opentype.js` fork**`parseFvarAxis: Cannot read properties of undefined` — because Fontsource's subsetting drops the `name` records the variable font's `fvar` table points at. `@fontsource/*` ships static 400 cuts as `.woff`, which satori reads directly: same typefaces, same upstream 5.3.0 that `docs/reference/fonts-provenance.md` records for the committed files, same weight. **Build-time only — no visitor fetches these**, and they are not covered by that file's provenance table because they are not served. ⚠️ **AND `src/lib/og-card.ts` RESOLVES ITS ASSETS FROM `process.cwd()`, NOT `import.meta.url`.** With `import.meta.url` the build fails on `ENOENT .../dist/.prerender/chunks/../styles/tokens.css`: Astro bundles that module into `dist/.prerender/chunks/`, so `import.meta.url` is the CHUNK's location. **It works under `astro dev` and dies at build** — the third defect of that shape in this run, alongside the `animation-timeline` minifier fold and Prettier's `{/* */}``{/_ _/}` rewrite. **The card palette is parsed out of `src/styles/tokens.css`** rather than duplicated, and a missing token throws rather than falling back to a wrong colour |
| Lighthouse | **INSTALLED AND RUNNING AGAIN — `npm run lighthouse`, build step 7a, 2026-08-31. `lighthouse@13.4.1` + `chrome-launcher@1.2.1`, both `latest`, `npm audit` 0 vulnerabilities with them in the tree** `[verified 2026-08-31 — npm view, npm audit]`. ⚠️ **THIS ROW'S OWN ADVISORY ATTRIBUTION WAS WRONG, AND THE WRONG ATTRIBUTION IS WHY THE BUDGET WAS UNMEASURABLE FOR FIVE DAYS.** It read: *"`@lhci/cli` was the sole source of all 10 `npm audit` findings (7 high, via `lighthouse → puppeteer-core → extract-zip`)"*. The first clause is true; **the parenthesis is not**, and it is the parenthesis an implementer reads. Measured from two probe lockfiles rather than recalled: `@lhci/cli@0.15.1` carries 10 findings (7 high) and **pins `lighthouse` 12.6.1**; the two high carriers are **`tmp@0.1.0`, a DIRECT dependency of `@lhci/cli` itself**, and **`extract-zip@2.0.1` via `@puppeteer/browsers`**. In `lighthouse@13.4.1`'s 109-package tree **both are ABSENT** and `npm audit` returns **0**. **So Lighthouse was never the carrier.** `@lhci/cli` is still `latest` at 0.15.1 and still carries all ten `[verified 2026-08-31]` — the tool that was unusable stayed unusable, and the one the budget actually needs was clean the whole time. `npm view` alone would not have found this; it took the lockfiles. **DELIBERATE DEVIATION FROM R11's LITERAL TRIGGER, recorded as one.** R11 says *"at step 7, put `@lhci/cli` back"*. It is not back. **What that costs, stated rather than summarised away:** `lhci autorun`'s declarative assertion config, the LHCI server, and CI upload/history. `scripts/lighthouse.mjs` replaces the first with an inline threshold and does not replace the other two — there is no run history, so a slow regression across builds is invisible; only an absolute breach fires. **AND IT IS A LOCAL GATE, NOT A CI CHECK.** Standalone Lighthouse drives an installed browser; the `act_runner` is not registered at all (Q23) and would need Chrome on the host. So: `npm run lighthouse` at a keyboard, plus a blocking item on `docs/06`'s cutover checklist. It is **not** wired into `npm run build` or either deploy path, and `docs/06` now says why in those terms — a check described as running where it cannot is Q22's defect in a different costume, which is the same trap this row just came out of from the other side. **Pages are enumerated from `dist/`, never listed** — a hand-written page list stops covering the site the first time a page is added. ⚠️ **THE ACCESSIBILITY CATEGORY IS MEASURED WITH `prefers-reduced-motion` FORCED, and the qualification must travel with the number.** Measured twice per condition on `/process/`: motion on → **96**, `color-contrast` failing on **24 nodes**; motion off → **100**, 0 nodes. The 24 were the scroll-driven reveal caught mid-flight — axe reported foregrounds such as `#d0cbc4` on `#f8f4ed` and **neither value is in this palette**; they are the real colours blended toward the background by an in-progress `opacity` keyframe. A category that reports 24 known-false nodes on ten of fourteen pages cannot surface the twenty-fifth real one. The reduced-motion rendering is not synthetic: it is the branch `global.css` ships for a real user setting, and the one in which every element sits at its final colour. **Performance is measured on a default profile**, where the animation runs. ⚠️ **AND THE RUNNER'S FIRST VERSION SET `cache-control: no-store` ON ITS OWN SERVER, WHICH FAILED THE `bf-cache` AUDIT ON EVERY PAGE***"pages whose main resource has cache-control:no-store cannot enter back/forward cache"*. Lighthouse resets storage between runs by default, so the header bought nothing and cost an audit, in a report whose whole job is to find defects. Removed; verified by toggling the one header, twice each way. **Both of these are the same rule: a measurement is a claim about your instrument.** **Baseline, 14 pages, 2026-08-31:** performance 99100, accessibility 100, best-practices 100, SEO 100, CLS 0.000 on every page. One budget note — **`/` LCP 2.03 s against `docs/04`'s < 2.0 s**, and the LCP element is `p.hero-lede`, **text**, not the portrait `[verified 2026-08-31 — lcp-breakdown-insight]`. So `fetchpriority="high"` on the hero image would misdirect priority away from the Geist face that actually gates it; the remaining lever is `BaseLayout`'s three-face preload, which is a documented measured trade. Left alone deliberately, not overlooked |
| Node | **22 LTS line**, `.nvmrc` holds `22` so it tracks the latest 22.x rather than freezing at a floor. `package.json` declares **`engines.node` = `^22.13.0 || >=24`** (verbatim — it excludes Node 23, an EOL odd line, which a flat `>=22.13.0` would have admitted) and **`engines.npm >=9.6.5`** — the intersection of every dependency's own floor: Astro 7 and `@astrojs/mdx` 7 need `>=22.12.0`, ESLint 10 needs `^20.19.0 \|\| ^22.13.0 \|\| >=24` `[verified 2026-08-26 — npm view astro@7.2.7 engines, npm view eslint@10.9.1 engines]`. **Pouya's machine reports Node v25.6.0** `[verified 2026-08-26 — node -v]`, an odd-numbered non-LTS line; it satisfies the floor, but `nvm use` keeps local and CI on the same runtime |
| Styling | Plain CSS with custom properties. No Tailwind, no CSS-in-JS `[verified 2026-08-25]` |
| Known HTML-conformance error | **Astro's `<Picture>` + `densities` emits a duplicate-density `srcset`, and it is upstream of this repo.** The pattern is `url, url 1x, url 2x, url 3x` — the first candidate carries no descriptor and therefore defaults to `1x`, so two candidates claim the same density, which puts the document in error per the HTML spec. **Behaviour is benign** and selection is correct at DPR 1, 2 and 3 `[verified 2026-08-27 — measured in headless Chrome]`. It affects **4 of the 12 `srcset` attributes on `/`** — the `<source>` elements of every `<Picture>` using `densities`, i.e. the three `InfinityMark` instances. The hero portrait uses `widths` + `sizes` and emits clean `w` descriptors. **Consequence for the record: do not claim "HTML validator clean" for any page carrying a mark**, and do not re-open this as a defect in this repo — it is `<Picture>`'s output, not ours. It would go if `InfinityMark` moved to `widths` + `sizes`, which would mean every call site supplying a `sizes` string for a fixed-size decorative image; not worth it for a conformance nit. Recorded on `adversarial-reviewer`'s request, 2026-08-27; first raised as step-1 review finding #8 |
@@ -771,7 +772,7 @@ Nothing below can be invented. Each needs an answer from Pouya.
| # | Question | Blocks |
|---|---|---|
| **Q59** | **WHERE DOES THE OVERTIME HOUR START?** `docs/07`'s card carries *"Overtime, per hour — $500"* and has never said what it is overtime **to**. Q58's ruling settled the two allowances (up to 3 h / 6 h of session, up to 2 h / 3 h of preparation) and did not reach this, and Q15Q17's answer records the rate with no trigger either. **The two candidates are the session cap (3 h / 6 h) and the billed envelope (5 h / 9 h), and this repository must not pick one** — it is a fee term, and `CLAUDE.md`'s rule for a fact we do not have is a question, not an inference. A first pass at `docs/07`'s Q58 note asserted the session cap as applied fact; `adversarial-reviewer` struck it in the same change set that wrote it. ⚠️ **AND THE ANSWER EXPOSES SOMETHING IN D14's FIGURES THAT IS WORTH SEEING BEFORE `/fees/` PUBLISHES THEM.** Under the session-cap trigger the half-day route costs `2000 + 500 × max(0, n 3)` and the full-day route is **flat $4,000 until hour 6** — so **booking a half day and paying overtime is cheaper at EVERY length: by $2,000 at 3 h, $1,500 at 4 h, $1,000 at 5 h, and $500 from 6 h on. The full-day rate is never the cheaper choice.** The cause is the relationship between two of D14's own numbers rather than the trigger — the half-to-full step is $2,000 and three hours of overtime is $1,500 — and the envelope trigger trades bands rather than fixing it (worse at 6 h, closing to zero from 9 h). ⚠️ *This row first stated the spread as a flat $500 and generalised the full-day route as `500n + 1000` for all `n ≥ 3`, which is only valid from hour 6. The real spread is up to FOUR TIMES larger and is largest at 35 h, the band a half-day booking actually overruns into — so the wrong figure understated exactly the thing this row exists to escalate. Corrected 2026-08-31; found by `adversarial-reviewer` on round 2 of the change set that wrote it.* The table is in `docs/07` §The confirmed rate card and, for the rate decision, §Recorded dissent. **What is needed:** one line on where the hour starts, and a decision on whether the half/full step is a choice or an oversight (R5's 12-month review is the other place that belongs). Raised by `adversarial-reviewer`, 2026-08-31 | **`/for-parties/` NOW, and the overtime line on `/fees/` at step 9.** That page states an unambiguous session cap, so an overtime RATE printed on it defines the trigger **by adjacency**there is no other quantity for the charge to attach to, and the answer travels alone into the `FAQPage` node. The rate is therefore off that page until this is answered; it names the charge without its rate or its trigger. `/mediation/` says only that overtime is on the rate card |
| ~~Q59~~ | ✅ **RULED AND CLOSED 2026-08-31 — Pouya. OVERTIME RUNS FROM THE SESSION CAP**: the fourth hour of a half day, the seventh of a full day. Not the billed envelope. `/fees/` shipped at build step 9 on this ruling, `FEES.mediation.overtimeStartsAfterSessionHours` records it, and `docs/07` carries it in full. ⚠️ **AND THE RULING CAME WITH A SECOND HALF THAT ANSWERS THE ARITHMETIC ANOMALY THIS ROW EXISTED TO ESCALATE, WHICH THE TRIGGER ALONE COULD NOT.** His words: *"a full day reserves the day; half-day overtime is subject to availability."* **The full-day fee buys the DAY, not six hours of it.** Read as a price comparison the table below says the full-day rate is never the cheaper choice; read knowing what each fee reserves, the $2,000-narrowing-to-$500 spread is the price of certainty rather than a defect. The sentence is `FEES.mediation.reservation` and it publishes **adjacent to the overtime row**, not as a footnote — the same structural rule as `PROCESS_FRAMING` beside the five timings under Q43, because a reader who takes the number and skips the framing has read a different offer. **THE ANOMALY IS NOT CLOSED AND STAYS ON §12 R5.** The gap is in D14's own figures — the half-to-full step is $2,000 against $1,500 for three hours of overtime — and the reservation point explains what it buys without removing it; the spread is largest at three to five hours, which is the band a half-day booking actually overruns into. `docs/07` §Recorded dissent carries the table for the 12-month review. **The original question, kept because the shape of it is the lesson.** *Where does the overtime hour start?* `docs/07`'s card carried *"Overtime, per hour — $500"* and had never said what it was overtime **to**. Q58's ruling settled the two allowances and did not reach this; Q15Q17's answer records the rate with no trigger. The two candidates were the session cap (3 h / 6 h) and the billed envelope (5 h / 9 h), and this repository was barred from picking one — a fee term is a fact we do not have, and `CLAUDE.md`'s rule for that is a question, not an inference. **It cost two strikes to hold that line:** a first pass at `docs/07`'s Q58 note asserted the session cap as applied fact and `adversarial-reviewer` struck it in the change set that wrote it; a round-1 fix then published the $500 rate on `/for-parties/` beside an unambiguous *"up to 3 hours"*, which **defines the trigger by adjacency**nothing else on the page is a quantity it can attach to and round 2 struck that too. Both strikes were right, and the ruling supplied the value they were waiting for | ~~`/fees/`, `/for-parties/`~~ — both now unblocked and shipped |
| ~~Q58~~ | **RULED 2026-08-31 — `hours` IS THE SESSION, AND THE AMBIGUITY WAS IN `docs/07` RATHER THAN IN ANY COPY. Pouya owned it in terms:** *"the ambiguity is mine… My `docs/07` wording said "up to 3.5 h, including 2 h preparation", which is genuinely unclear: 3.5 was meant as the TOTAL time committed, of which 2 is preparation — leaving 1.5 hours in the room. Your arithmetic caught it: if prep sat inside, 3.5 and 7 wouldn't be exactly 2×, because preparation doesn't scale with session length. The intended reading is the market's, and my wording obscured it."* **THE CORRECTED CARD, in his words:** *"Half day — up to 3 hours of session. Fee includes up to 2 hours of preparation. $2,000. Full day — up to 6 hours of session. Fee includes up to 3 hours of preparation. $4,000."* His reason for 3 and 6: *"the market convention — Patey and Zuber both publish "all or part of 3 hours" and "all or part of 6 hours", and those were the comparables the rate was set against. Selling 1.5 hours of room time as a half day would be an outlier nobody would recognise."* ⚠️ **ONE PROVENANCE NOTE, and it is R14's rule rather than a doubt about the ruling:** `docs/07`'s committed extract records Patey and Zuber at **3 h** and **6 h** but **does not carry the phrase "all or part of"** — so `docs/07` cites the hours, not the phrase, and the phrase is not attributed to them anywhere in the repository. The hours corroborate the ruling on their own, and ADR Chambers' roster rate in the same table is the clearest corroboration of the *shape*: *"one half hour of preparation time per party **and** up to three hours of mediation"* — preparation counted separately from a three-hour session. **APPLIED:** `docs/07`'s two card rows and its §All parameters confirmed (which prescribed the flat *"including 2 hours"*, the form `/for-parties/` then shipped); `FEES.mediation.*.hours` 3.5 → 3 and 7 → 6 with the semantics in the constant's doc comment; `/for-parties/` now states the session length interpolated from the constant and the preparation allowance **as a cap**. **The preparation allowance is CAPPED and must be published as capped***"including **up to** 2 hours"*, never the flat form and never "preparation included". **`/fees/` is UNBLOCKED for build step 9.** **The question as raised is preserved below.** **DOES `hours` IN THE MEDIATION RATE CARD MEAN THE LENGTH OF THE DAY, OR THE BILLED ENVELOPE INCLUDING PREPARATION?** `docs/07-fees.md` reads *"Half day — **up to 3.5 h, including 2 h preparation**"* and *"Full day — up to 7 h, including 3 h preparation"*. Taken at face value, 3.5 is the whole billed envelope and the **time in the room is 1.5 h** for a half day and **4 h** for a full day. **Against that reading:** 3.5 and 7 are exactly 2×, which they would not be if preparation sat inside them (1.5 vs 4 is not 2×). So either the card's wording is wrong in the one document that is the authority on money, or `FEES.mediation.*.hours` in `src/data/site.ts` does not mean what a page would naturally publish it as. **This was one sentence from shipping.** A draft of `/for-parties/` answered *"What happens on the day?"* with *"A half day is about 3.5 hours"* — the envelope presented as the day, to the reader least able to check it. The sentence was removed rather than resolved by inference; the page now says only that you book a half day or a full day. **What is needed:** one line from Pouya saying which the 3.5 and 7 are. Then `docs/07`'s two rows or `site.ts`'s field gains the correction, and the semantics go in the constant's doc comment (a warning is there now). **`/fees/` at build step 9 publishes this table and cannot be built without the answer.** Raised by `adversarial-reviewer`, 2026-08-30 | **Nothing.** No page stated a duration while the question was open — the one draft sentence that did was removed rather than reconciled, which is why the ruling had nothing to correct in public copy |
| ~~Q57~~ | **CLOSED 2026-08-31 — NO SEVENTH UNDERTAKING, AND THE PAGE IS COMPLETE AS IT STANDS.** Pouya: *"`/process/` stating when conflicts are run and what the check needs is complete. A reader assumes the outcome, and the obvious undertaking ("if a conflict is found I decline") adds nothing a reader doesn't already infer. Your restraint was right — §4's gate held. Record it closed rather than open, so it stops appearing in the live list."* **So this is a closure, not a deferral:** the answer is that the page says nothing further, which was one of the two outcomes the question named. §4 gains no seventh conduct undertaking and `CONDUCT_UNDERTAKINGS` still holds six. **APPLIED:** the `TODO(pouya)` is removed from `src/pages/process.astro` §Conflicts and replaced with the ruling, so a later reader finds the decision where the question was rather than an open marker; the file header's *"see the TODO below"* is corrected to cite this closure. `src/` now carries **zero** live `TODO(pouya)` markers. **The question as raised is preserved below.** **WHAT HAPPENS WHEN A CONFLICTS CHECK TURNS SOMETHING UP?** `/process/` §Conflicts ships saying **when** the check runs (the intake call, before anything is agreed) and **what it needs** (full legal names of the parties, the parent or affiliate actually behind the dispute, counsel on each side). It stops there, and the stop is deliberate: **any sentence naming the outcome is a SEVENTH conduct undertaking**, and §4's gate for that class is one line — *"an undertaking may be published only where Pouya has made it in terms. Not 'would obviously agree to', not 'follows from the process' — said."* *"If a conflict appears I decline the appointment"* is exactly what that gate refuses to let this repository infer, however obvious it looks. **What is needed:** one sentence from Pouya, in his words, or a decision that the page says nothing further. `TODO(pouya)` sits at `src/pages/process.astro` §Conflicts. Raised at build step 6, 2026-08-30 | **Nothing.** The section shipped accurate and unchanged; what closed is whether anything more was owed |
| ~~Q56~~ | **RULED 2026-08-30 — MEDIATION IS *NOT* SCOPED COMMERCIAL.** Pouya: *"Correct the four 'Commercial Mediation' surfaces to 'Mediation'; leave §4's mediation row unscoped."* **And the asymmetry with arbitration is DESIGNED, not an oversight — the reason is now recorded beside both §4 rows so nobody tidies them into a matching pair.** **Arbitration is scoped commercial because of a LEGAL GATE:** Q39 — family arbitration in Ontario requires prescribed training, and Pouya has excluded it by choice. **Mediation has no such gate**; he mediates commercial, construction, insurance/SABS, shareholder and cross-cultural matters, and the practice pages say so. So the site-wide "commercial" framing was **under-describing a wider offering**, which is why it is corrected rather than ratified as a positioning choice. `/practice/insurance/`'s SABS framing needs no reconciliation: it was never outside the offering. **The question as raised is preserved below.** ⚠️ **IS THE MEDIATION OFFERING SCOPED COMMERCIAL, OR NOT? THE SITE SAID BOTH.** §4 Offerings rows arbitration three times, each **scoped commercial**; the mediation row is `**Mediation** — sole mediator`, **unscoped**. Shipped output scopes it anyway: `/mediation/`'s `<title>` is *"Commercial Mediation"*, its `Service` node is `name: "Commercial mediation"`, and `/` and `/about/` both say *"I mediate commercial disputes"*. Meanwhile **`/practice/insurance/` offers mediation in matters that are not commercial on any ordinary reading** — *"Disputes between an insured person and an insurer under the Statutory Accident Benefits Schedule"*, with *"What I offer is private mediation"*. An individual claimant against their own insurer is not a commercial dispute. **Nothing here is a false claim** — scoping a page to commercial mediation asserts *less* than the unscoped §4 row grants, and narrower than the row is always safe. **The problem is that the two halves cannot both be the whole picture**: either the practice takes non-commercial mediations (and the site-wide "commercial" framing under-describes it, including on the page an appointing body reads), or it does not (and `/practice/insurance/` is offering something outside the offering). **What is needed:** either a §4 Offerings row scoping mediation, with `/practice/insurance/`'s SABS framing reconciled to it — or a decision that mediation is deliberately unscoped, in which case the site-wide "commercial" wording is a positioning choice and should be recorded as one rather than read as a limit. **Pre-existing, not created 2026-08-30** — but this change set newly wrote the claim onto `/med-arb/` and it has been removed again pending this answer. Raised by `adversarial-reviewer`, 2026-08-30 | **Nothing — ruled the same day it was raised.** No page was wrong; the register was silent where the site was specific, and the ruling makes the site match the register rather than the other way round |
@@ -919,12 +920,12 @@ never being raised again.
| R6 | **Booking tool.** Parked by Pouya on 2026-08-26; `/contact/` ships with the intake form and a reserved slot for an embed | 2026-08-26 | He asked to be reminded. D10 committed to booking because it removes the back-and-forth that loses appointments — the form alone is a partial answer |
| R9 | **The SES alarms notify nobody until the `ses-alerts` email subscription is confirmed.** `SES-BounceRate-High` and `SES-ComplaintRate-High` are configured and live; the SNS email subscription to `info@smlcompany.ca` is **pending confirmation**, and an unconfirmed subscription drops every message | 2026-08-26 | A monitoring control that exists but does not deliver is worse than none, because it reads as covered. At this volume five bounces can cross the ~5% suspension threshold. Tracked in §7 and on the cutover checklist, but a one-click task nobody owns is exactly what §12 is for |
| R10 | ⚠️ **A THIRD SURFACE, 2026-08-30: `/process/` §Confidentiality renders `MEMBERSHIP_ORGS[0]` ("I am a member of the ADR Institute of Canada").** It is rendered from the constant rather than typed, so the sweep this reminder prescribes reaches it — that was `adversarial-reviewer`'s finding and the fix, in that order. **DISCHARGED AS WRITTEN 2026-08-28 — AND RE-ARMED WITH AN EVENT TRIGGER INSTEAD OF A DATE. STILL LIVE.** Pouya re-confirmed all four memberships as current (Q44), which discharges the prohibition this row carried, and `/about/` now publishes the Memberships group. **The row does not close, because he declined renewal-date tracking**, and that was his instruction for what to do about it: *"Without renewal months it cannot fire on a date, so make it fire on an event: re-confirm memberships before any cutover or major republish, and re-stamp §4 when confirmed."* **THE TRIGGER: re-confirm before any cutover, and before any major republish. Then re-stamp §4 the same day.** **His reason, kept verbatim because it is the general principle and not a membership detail:** *"§4 already carries OCNI as lapsed and unpublishable, and that was found roughly a year late. A stamp with no trigger behind it goes stale silently, which is exactly how OCNI got onto a list of things to feature."* **Two things the discharge did NOT license.** (1) **No currency warranty on the page** — list the memberships, promise nothing about their future state; the struck sentence stays struck and nothing replaces it. (2) ~~`memberOf` stays out of the JSON-LD~~**SUPERSEDED. Q53, ruled 2026-08-28: EMIT IT.** `/about/`'s Person node now carries the four memberships as `Organization` nodes. Pouya took `adversarial-reviewer`'s argument: they are already crawlable in `/about/`'s HTML, so withholding the triple reduced no exposure and only made the graph less complete than the page. **The consequence for THIS reminder is that it now covers two surfaces** — re-confirming before a cutover means `src/data/schema.ts` as well as the visible list, and they must not be allowed to diverge. **Renewal periods, stated once and not widened again:** the OBA sections and the CTF renew yearly; §4 records **nothing** about ADRIC's or ADRIO's period, and the widened form ("all four renew yearly") reached four files before it was swept. *Previous text described the prohibition and the withheld group; it held for one session and did its job.* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this. The group is on a public page now, which raises the cost of a lapse rather than lowering it — *(This rationale ended by pointing at **Q48** as a possible widening of the row. Q48 closed 2026-08-28 as not site-relevant — ADRIO retention governs whether Pouya keeps a designation, not what the site may say about holding one — so the clause is struck. §12 is read aloud every session; a live reminder pointing at a struck row produces a false surface every time.)*, not just a list |
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. **Carries one specific re-add trigger: at step 7, put `@lhci/cli` back.** Check for a patched release — do not assume `0.15.1` is still the ceiling just because it was on 2026-08-26 — pin it against `npm view` that day, write a `lighthouserc` with the budgets from `docs/04-seo-spec.md`, and delete the UNAVAILABLE notices this repo now carries in six places (§7 lists them). If the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. **THE STEP-7 RE-ADD TRIGGER IS DISCHARGED, 2026-08-31 — and NOT as written.** It said *"at step 7, put `@lhci/cli` back"*. `@lhci/cli` is still 0.15.1, still `latest`, and still carries 10 findings (7 high) `[verified 2026-08-31]`, so the literal instruction would have re-added a tool with seven high-severity advisories. What shipped is **`lighthouse@13.4.1` + `chrome-launcher@1.2.1`, 0 vulnerabilities**, as `npm run lighthouse`. **The reason is that §7's own advisory attribution was wrong** — it blamed `lighthouse → puppeteer-core → extract-zip`; the carriers were `@lhci/cli`'s own `tmp` and `@puppeteer/browsers`' `extract-zip`, and neither exists in Lighthouse's tree. **The last clause of this trigger is the one that earned its place:** *"if the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated."* They are still unfixed; the decision was taken; §7 records what it costs (no `lhci` assertion config, no server, no run history) and that the gate is local rather than CI, because standalone Lighthouse needs an installed browser and the runner has none. **All six UNAVAILABLE notices are deleted**`docs/04` (budget table, Performance callout, post-launch checklist), `CLAUDE.md` (performance budget, definition of done), `/build` Phase 5, `docs/06` (PR checks, cutover checklist), `.claude/agents/adversarial-reviewer.md` §4. The **general** half of R11 — re-check every pin at every phase boundary — is untouched and still fires | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
| R12 | **`compressHTML: true` is a deliberate deviation from the Astro 7 default (`'jsx'`).** Measured 2026-08-26: in an `.astro` template an inline pair split across two lines renders as `<em>a</em><strong>b</strong>` under the default — the space is silently deleted. MDX prose is unaffected | 2026-08-26 | It is a deviation, and undocumented deviations become folklore. Revisit **with a measurement**, not a preference — and re-measure after any Astro major, since the behaviour could change again |
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it***"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38 | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
| R14 | **Anything a spec makes a claim about must be reachable from the repository.** If the artefact lives only in Drive, in a console, or on someone's disk, no reviewer can check the claim and the claim is unverifiable by construction — see the rule now in `CLAUDE.md` | 2026-08-26 | **This has now cost twice.** Q24 was `AWS-Hosting-Guide.md`, the only record of how the infrastructure was hand-built, living outside the repo. Q32 was the infinity mark: Claude Code traced it from the old site's *loading placeholder*, wrote a scrupulous provenance comment recording exactly that doubt — and **two adversarial review passes still could not catch it**, because the artwork they would have had to compare against was not in the repo. The doubt was correctly stated and structurally uncheckable. That is the failure mode: not a missing warning, a missing artefact |
| R15 | **Every page shares ONE Open Graph image — the portrait — and that is an interim for seventeen of the nineteen. RULED, DEFERRED, AND TRACKED HERE ON POUYA'S INSTRUCTION:** *"Record the trigger so it cannot quietly become permanent."* Q40, 2026-08-27. The portrait is the **decided** card for `/` and `/about/`a face is the strongest social preview for a personal brand. It is the wrong card for the other seventeen, where a typed card carrying the page title does the work. **Removal trigger: build step 7**, where Insights needs per-article cards anyway — one generator (`satori` or `astro-og-canvas`, pinned against `npm view` that day per R11), one dependency, one review. Then delete the interim note from `docs/04` and the cutover line from `docs/06`. **Blocks cutover; does not block steps 36** | 2026-08-27 | Nobody on this project will ever see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a reader who is not us, and `docs/04` requires a **unique** title and description per page — nineteen unique pages that all preview identically is the failure, and it is invisible from inside the repo. It costs nothing and works, which is exactly the R13 shape |
| R16 | **The one-page PDF bio does not exist, and it is deferred to build step 9 — not dropped. RULED AND TRACKED HERE ON POUYA'S INSTRUCTION:** *"Record the trigger so it doesn't become another quiet interim."* Q45, 2026-08-28. His reasoning: it is a **derived artefact**, so building it before `/about/` and `/fees/` are final means building it twice — and an appointment proposal needs the fee card as much as the bio. **Removal trigger: build step 9, alongside `/fees/`.** Two decisions travel with it and are **not** settled by the deferral: (a) generated at build (a dependency and an `npm audit` surface, against R11) or authored once as a designed artefact; (b) whether it carries anything the site does not — a matter list, a fee summary, referees — **each of which is a §4 question of its own**, and the matter list would collide with §4 Forbidden directly. Then delete `docs/01` §`/about/` item 7's pending note | 2026-08-28 | **It is the one artefact class this project's review apparatus cannot reach.** A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again. `/about/` currently ships with no link to it, which is correct — a dead link on the page an appointing body reads is worse than the absence — and that means **nothing on the site will ever prompt anyone about this.** Only this row will |
| ~~R15~~ | **DISCHARGED 2026-08-31 — build step 7b. Seventeen pages and every future article now carry their own generated card; `/` and `/about/` keep the portrait, which Q40 called "not an interim" and which is unchanged.** `src/pages/og/[...slug].jpg.ts` renders them at build from `satori@0.33.4` + `sharp`; `src/data/og-cards.ts` is the registry; `SEO.astro` resolves which of the two kinds a page gets from its own pathname, so the decision lives in one data file rather than in nineteen call sites. `satori` over `astro-og-canvas@0.13.0` — both 0 vulnerabilities `[verified 2026-08-31]` — because `sharp` was already a dependency, so it is one library rather than a CanvasKit wasm blob, and it renders the site's real fonts and tokens instead of approximating them. **The interim note is deleted from `docs/04` and the cutover line from `docs/06`**, which is what this row's trigger asked for. ⚠️ **AND IT SURFACED A HOLE THAT IS WORTH MORE THAN THE ROW: 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 is the one surface on this site where new prose would face no mechanical check at all. So card copy is constrained structurally rather than editorially: **every headline is its page's own `<h1>`, character for character**, the kicker renders `CREDENTIALS.designations`, and **`npm run og:proof` enforces both against the built HTML**it fails if the registry and the page disagree in either direction, and it also confirms every `og:image` resolves to a file that exists. That check found a straight-versus-typographic apostrophe on its first run. **A page with no registry 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, and a silent fallback recreates it exactly. **What is NOT discharged and is deliberately left open:** the check compares strings, so it cannot see a card that renders badly — a headline that overflows, a mark that clips. `npm run og:proof -- --sheet` writes a contact sheet of every card for that, and **a human still has to look**; `docs/06`'s cutover item says so. Two cards were read at step 7b (`/practice/energy/`, the longest headline, and `/practice/insurance/`) and rendered correctly | 2026-08-27 | Nobody on this project will ever see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a reader who is not us, and `docs/04` requires a **unique** title and description per page — nineteen unique pages that all preview identically is the failure, and it is invisible from inside the repo. It cost nothing and worked, which is exactly the R13 shape |
| ~~R16~~ | **DISCHARGED 2026-08-31 — build step 9, which is the trigger this row set. The one-page PDF exists, is committed at `public/pouya-lajevardi-bio.pdf`, and `/about/` links it.** Q45's two open sub-decisions are both taken, and the second one is what makes the first safe. **(a) Generated at build, or authored once? NEITHER — and the third option answers this row's actual objection.** The bio is a PAGE, `src/pages/bio.astro`, so every line of it is reviewed by the same apparatus as every other page: `astro check`, `npm run check:claims` on the built HTML, the per-step code review, and the cutover claims pass. `npm run bio:pdf` then RENDERS the PDF from the built page through Chrome, with **no new dependency**`chrome-launcher` is already a devDependency because Lighthouse needs it. That is aimed squarely at what this row said the problem was: *"It is the one artefact class this project's review apparatus cannot reach… a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again."* A rendering of a reviewed page is back inside the apparatus. **It is deliberately NOT part of `astro build`** — CI has no Chrome, and a build step that cannot run in CI is Q22's shape again — so the PDF is committed, which R14 wants anyway. **(b) Does it carry anything the site does not? NO, and that is the answer that avoids the §4 question this row flagged.** Every line renders from `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No matter list — which this row correctly said *"would collide with §4 Forbidden directly"* — no referees, and no figure that is not on `/fees/`. The fee summary IS there, because this row's own reasoning is that an appointment proposal needs the rate card as much as the bio. ⚠️ **`npm run bio:pdf` ASSERTS ONE PAGE and writes nothing if the count is wrong** — counted from the PDF's own page objects, cross-checked against the page tree's `/Count`. A one-page bio that silently becomes two is invisible from the source, because it depends on the print stylesheet, the paper size and how much §4 has grown. **And reading the rendered PDF caught a real breach the source review had not:** the opening sentence read *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **mediation**, which Q56 leaves unscoped deliberately. `/bio/` is `noindex` and out of the sitemap: it is a condensed duplicate of `/about/` and `/fees/`. **What is NOT discharged:** nothing in the build regenerates the PDF. It must be re-rendered and re-committed whenever `/bio/`, §4, the rate card or the print styles change, and `docs/06`'s cutover checklist carries that | 2026-08-28 | It is the one artefact class this project's review apparatus cannot reach. A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again |
| R17 | **THE DEPLOY KEY'S FIRST ROTATION IS DUE 2026-11-26, AND THAT DATE IS THE WHOLE ROW.** Access key for `adr-sml-deploy` created **2026-08-26**; D3 commits to **quarterly** rotation. Pouya's instruction when Q22 closed: *"Put the DATE in the R-series reminder, not the intention."* **Rotation procedure is in `docs/06-deployment.md` §Key rotation** — create the second key, update the Gitea secrets, verify a deploy, **then** delete the first; never delete before verifying, or the next push fails with no way to authenticate the fix. **Then set the next date here (2027-02-26) rather than deleting the row.** Also re-run **all nine** `simulate-principal-policy` checks after any policy edit (they are listed and reproducible in `docs/reference/deploy-credential-verification.md` §5) — the narrowness is the control (§10), and a widened policy is invisible from inside the repo. **The key is currently UNUSED — `AccessKeyLastUsed` returns `ServiceName`/`Region` = `N/A` and no `LastUsedDate`** — because deploys still run locally via `npm run deploy`; that does not pause the clock, and an unused long-lived key is exactly the one nobody remembers to rotate | 2026-08-28 | This is the reminder D3 has been waiting for a subject since 2026-08-26. §10 records the reason it matters more here than on a normal project: the secret lives on **jointly-administered** infrastructure, and the account also holds another business's client-database backups. A static key on shared infrastructure is only as safe as its age and its scope, and **nothing in the repo, the build, or AWS will prompt anyone on either.** §7 carries the inventory; this row carries the date |
| R18 | **THE SIX `docs/reference/` EXTRACTS BEHIND `/practice/*` ARE DATED 2026-08-29, AND SIX SHIPPED SENTENCES TURN ON FACTS THAT MOVE.** Build step 5 put statute, regulation, tribunal and bill status onto public pages — sourced, but **sourced as at one day**. The volatile ones, in order of how fast they move: **(a) federal Bill C-36** — `/practice/technology/` says it *"was introduced in June 2026 and was at second reading when this page was written"*; if it receives royal assent the page is wrong about the most load-bearing fact on it. **(b) the Ontario regulation under `Electricity Act` s. 28.1** — `/practice/energy/` says it *"had not been made as of August 2026"*. **(c) the ERO 026-0853 consultation**, comment period to **12 September 2026**. **(d) the Tribunals Ontario annual report** — `/practice/insurance/` publishes FY2024-25 figures and the extract records that a 2025-26 edition was never ruled out. **(e) the SABS**, amended with effect 1 July 2026. **(f) the ADRIC National Mediation Rules**, under review by ADRIC's own committee. **(g) ADRIC's Code of Ethics** — added 2026-08-30, build step 6. `/process/` §Confidentiality quotes it verbatim from `docs/reference/adr-institution-names.md` (retrieved 2026-08-29) **with a live link to ADRIC's page**, which is what makes it checkable and also what makes a stale quotation visible. It is the slowest-moving item here — a professional code, not a bill — so it does not change the cadence; it is listed because the trigger below says "all six" and there are now seven. **THE TRIGGER: re-check all seven extracts before any cutover, and before any republish that turns on one of them — the same event trigger R10 uses.** Then re-stamp the extract. **A page that was true when it was written and is false when it is read is still a false page**, and this is the first change set on the project to put that class of fact into public copy at volume | 2026-08-29 | Six sentences, six files, one retrieval date. Nothing here fires on its own; a fact with a shelf life and no owner is exactly what §12 exists for |
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
@@ -934,6 +935,410 @@ never being raised again.
# Change Log
## 2026-08-31 (ah) — Build steps 7a10: the site is complete and reviewable. Lighthouse returns and its first run found three defects in itself; the OG generator discharges R15 and exposes a hole in `check:claims`; and an article auditor found a false assertion on a page that shipped two days ago
**Type:** build (four steps) + two rulings applied + two standing reminders discharged.
**Scope:** Pouya authorised steps 7a through 10 as one run, with standing authorisation to decide and record rather than stop and ask, and to batch every question to the end. Nothing was deployed. `claims-auditor` did not run — D20.
---
### What shipped
**22 built pages, up from 14.** New: `/insights/`, `/contact/`, `/contact/received/`, `/contact/could-not-send/`, `/fees/`, `/bio/`, `/legal/privacy/`, `/legal/terms/`. Plus `/insights/<slug>/` as a route with five drafted articles behind it, and 21 generated Open Graph cards.
**Step 7a — Lighthouse is installed and running again.** `lighthouse@13.4.1` + `chrome-launcher@1.2.1`, `npm run lighthouse`, `npm audit` 0 vulnerabilities.
**Step 7b — the OG card generator.** `satori@0.33.4` + `sharp`, `src/pages/og/[...slug].jpg.ts`, `src/data/og-cards.ts`, `src/lib/og-card.ts`. R15 discharged. Plus the Insights plumbing: `ArticleCard`, `Prose`, the index, the article route, `articleGraph`, and `/`'s section 7.
**Step 7c — five drafted articles**, `draft: true` / `reviewedByPouya: false`.
**Step 8 — `/contact/`, the intake form, and `backend/intake/`.** Undeployed.
**Step 9 — `/fees/` on Q59's ruling, and the one-page PDF bio.** R16 discharged.
**Step 10 — `/legal/privacy/` and `/legal/terms/`.**
**Plus `/about/`'s inverse credentials band**, approved at step 6 and outstanding since.
---
### The rulings applied
**Q59 — RULED AND CLOSED. Overtime runs from the session cap** (the fourth hour of a half day, the seventh of a full day), not the billed envelope.
**And the ruling's second half is what made `/fees/` publishable rather than merely unblocked.** *"A full day reserves the day. Half-day overtime is subject to availability."* The full-day fee buys the **day**. Without that sentence a reader adds `2000 + 500 × 3 = 3500` against `4000` and concludes the full-day rate is a $500 penalty for booking properly — which is how the R5 anomaly reads when the two options are treated as the same product. So the sentence publishes **adjacent to the overtime row**, from `FEES.mediation.reservation`, on the same structural rule as `PROCESS_FRAMING` beside the five timings under Q43. **The anomaly is not closed** — the gap is in D14's figures and stays on R5.
---
### What the run found, in order of how much it cost
**1. `AGENTS.md` §7's Lighthouse advisory attribution was wrong, and the wrong attribution is why the budget was unmeasurable for five days.** §7 recorded the ten findings as arriving *"via `lighthouse → puppeteer-core → extract-zip`"*. Measured from two probe lockfiles: `@lhci/cli@0.15.1` carries them and pins **lighthouse 12.6.1**; the two high carriers are **`tmp@0.1.0`, its own direct dependency**, and `extract-zip@2.0.1` via `@puppeteer/browsers`. In `lighthouse@13.4.1` both are **absent** and `npm audit` returns **0**.
**Lighthouse was never the carrier.** `npm view` alone would not have found this — it took the lockfiles. Deliberate deviation from R11's literal *"put `@lhci/cli` back"*, recorded with what it costs (no `lhci` assertion config, no server, **no run history**, so only an absolute breach fires) and with the fact that it is a **local gate and cannot be CI**, because standalone Lighthouse drives an installed browser and the runner has none.
**2. Lighthouse's first run found three defects — all three in the instrument.**
- **Accessibility read 96 on ten of fourteen pages.** 24 `color-contrast` failures, foregrounds like `#d0cbc4` on `#f8f4ed`**neither value is in this palette**. They were the scroll-driven reveal caught mid-animation. Forcing `prefers-reduced-motion`: 96 → **100**, 24 nodes → **0**, twice per condition. A category reporting 24 known-false nodes cannot surface the twenty-fifth real one.
- **`bf-cache` failed on every page** — caused by the runner's own `cache-control: no-store`, which Lighthouse did not need (it resets storage itself). Verified by toggling the one header.
- **SEO read 69 on the `noindex` pages**, and the only failing audit was `is-crawlable`, firing on the `noindex` `docs/04` requires. The budget as written is unsatisfiable for those pages. **Fixed by asserting them audit by audit** — every SEO audit must pass except that one — rather than by lowering a threshold or excepting the pages. **Probed:** removing a `<title>` from a `noindex` page exits 1 naming `document-title`; restored, exit 0. ⚠️ *One reading is unreproduced and is recorded rather than explained: the run that surfaced this listed **five** such pages and scored `/insights/` 100, while the final run lists **six** and scores it 69. `dist/insights/index.html` does carry `<meta name="robots" content="noindex,follow">`, and `/insights/` has emitted it since the page was written, so the earlier 100 should not have happened. It changes nothing — the page is correct and the gate now handles it — but I could not reconstruct the cause and am not going to guess one.*
**3. Text baked into a JPEG is unreachable by `check:claims`, which under D20 is the only per-step claims control there is.** An OG card would have been the one surface on this site with no mechanical check over its copy at all. So card copy is constrained structurally: **every headline is its page's own `<h1>`, character for character**, the kicker renders `CREDENTIALS.designations`, and **`npm run og:proof` enforces both against the built HTML**, in both directions, plus confirms every `og:image` resolves to a file that exists. A missing registry 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*.
**4. An article auditor found a false assertion on a page that shipped at step 5.** `/practice/energy/` said *"The regulation that would set them had not been made as of August 2026."* The extract's own adversarial check answers that exact question: **"NOT ESTABLISHED either way, and DO NOT ASSERT ITS ABSENCE"** — and supplies safe wording, which the page now uses. **A defect in an unpublished draft surfaced a shipped one**, and neither of step 5's review passes caught it, because both read the sentence against the extract's *quotations* rather than against its adversarial check.
**5. Reading the rendered PDF caught a mediation-scoping breach the source review had not.** `/bio/` opened *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **all three**, mediation included. Q56 leaves mediation unscoped deliberately. It is the `/practice/` shape exactly — the two words never appear in the same element, so no proximity grep reaches it.
**6. `.btn` is scoped to `Button.astro`, so a hand-written `<button class="btn btn-primary">` renders unstyled.** `/contact/`'s submit button was written that way. The parent-scope trap `CLAUDE.md` records, arrived at from the other direction.
**7. FOUR of my own verification commands were wrong, in four different ways, and all four are the same rule.**
- `grep -c 'acard' dist/index.html` returned **1**, which I read as one incidental mention. **`grep -c` counts LINES**, and minified CSS is one line — the `1` was the entire rule set. `ArticleCard`'s CSS does ship to `/` for a section that renders nothing: **10 rules, 1,496 bytes, 4.4% of the document**. Kept, because `npm run lighthouse -- /` returns **perf 99, LCP 2.03 s, CLS 0.000 — identical before and after**. A comment claiming the CSS was absent was corrected to the measurement.
- A check for `<option value="">` on `/contact/`'s selects reported **0 of 4**. The *check* was wrong: Astro inserts `data-astro-cid` before the closing bracket. All four have it. **A grep that does not match is not an absence until you read the artefact** — the mirror of the rule already in `CLAUDE.md`.
- The computed-contrast sweep written to VERIFY the ghost-button fix then reported **two new failures at 1.39:1 and 1.52:1** on `/for-parties/` and `/process/`. Both false: it measured each button's text against its **parent's** background, and a `.btn-gold` on a cream section carries its own ink background, so gold-l text was being compared to cream it never touches. The sweep's own failure list — which resolves the background from the element itself — reported **0**. Read before reporting; a sweep written to confirm a fix is still an instrument.
- And the worst of the four, because of what it was about. A check on the rendered med-arb article printed `undertakings render: False`, which reads as **a published conduct commitment failing to appear on a page that quotes it**. It was the test: one of its three search strings was `'stays in that caucus'`, which belongs to undertaking **(d)** — a `/mediation/` surface — and not to the three this article carries. Re-run with the right keys, all three of (a), (b) and (c) are **verbatim at 200, 162 and 161 characters**, and (d), (e), (f) are correctly absent. A false alarm on §4's most softening-prone class would have been the most expensive wrong report available in this run.
**The through-line is worth stating once, because it is four for four in one run:** each produced a number or a boolean, none was about what it appeared to be about, and in every case the disproof was reading the artefact rather than re-reading the command. Note the direction, too — **two of the four pointed at defects that did not exist.** `CLAUDE.md` already warns that a uniformly-good result ends a check instead of starting one; these are the mirror, and they cost differently: a false pass ships a defect, a false alarm ships a correction to something that was right.
---
### Decisions taken under standing authorisation
- **The intake form posts to a same-origin `/api/intake`**, not to the execute-api hostname: tighter CSP (`form-action 'self'`), no cross-origin POST, the endpoint id stays out of the HTML and the repo, and **submitting from a laptop does nothing**. Cost, stated: the form does not work until a CloudFront `/api/*` behaviour exists. Cutover item.
- **POST-redirect-GET**, so the form works with zero JavaScript and cannot double-submit. Two `noindex` landing pages, excluded from the sitemap.
- **The 3-second timing check is NOT implemented** — it is unimplementable on a CDN-cached static page and would be a control that does nothing, which is Q22's shape.
- **CORS is not what protects the form; the `Origin` check is.** A form POST is a top-level navigation and is exempt from preflight.
- **Two intake field tables, deliberately** — the server must not validate against a list the client shipped it — **cross-checked by `npm run check:intake`**, probed with three deliberate mismatches.
- **`ANALYTICS.installed = false`**, and `/legal/privacy/` renders its analytics paragraph from it. D15 decided Plausible; deciding is not installing, and a policy naming a processor that processes nothing is a false disclosure.
- **`/legal/privacy/` claims "encrypted at rest" and does NOT claim the customer-managed KMS key or PITR** — §7 verifies neither, and DynamoDB encrypts at rest unconditionally.
- **`/legal/terms/` has no governing-law clause, no limitation of liability and no warranty disclaimer.** §4 bars this repository from concluding a proposition of law. Batched for Pouya.
- **`/insights/` is `noindex` while empty**, derived from the collection so it self-clears. It stays in the sitemap because `astro.config.mjs`'s filter cannot see collection data — recorded rather than fixed with a frontmatter parser in build config.
- **R16 (a):** the bio is a **page**, and the PDF is a rendering of it, so the artefact circulated with an appointment proposal is inside the review apparatus. **(b): it carries nothing the site does not.**
- **The PDF asserts one page** and writes nothing if the count is wrong.
### The five drafted articles, and what auditing them cost
`docs/03`'s launch slate, drafted 1,4981,795 words each, `draft: true` /
`reviewedByPouya: false`. **They generate no page, appear in no index, no
sitemap and no card set** — one predicate, `!data.draft`, is the filter every
`getCollection('insights')` call on the site passes, so a draft is absent as a
consequence of not existing rather than because five places each remembered.
**An independent compliance audit of each returned 76 findings — 12 blocking, 34
should-fix, 30 consider — and 57 assertions the committed extracts do not
establish.** Every blocking and should-fix finding was applied; the consider
findings were applied except where doing so made the prose worse. Two spot-checks
against the extracts confirmed the audits were reading them correctly rather than
inventing objections.
**What the findings were mostly about is worth naming, because it is R14's own
argument arriving from a new direction.** They were not voice or claim-register
breaches. They were **facts about the world that the extract does not support**
a section number flattened, a statutory branch dropped, "about six weeks" for a
statutory timetable that computes to five or seven, an IESO process described
from four pages as though the four were exhaustive. The extracts in
`docs/reference/` each carry an *"Adversarial check on this extract"* section
recording what was searched and what was **NOT ESTABLISHED**, and that section is
what caught almost all of it. **Sourced quotations were never the risk; the
inferences drawn beside them were.**
⚠️ **AND THE ARTICLES ARE INVISIBLE TO `check:claims`, WHICH READS `dist/`.** A
draft ships no HTML, so under D20 the five articles sit outside the only
per-step claims control there is. They were checked once, deliberately: all five
were temporarily set `draft: false` / `reviewedByPouya: true`, the site built to
**27 pages**, `npm run check:claims` run over the article prose — **clean, 11
patterns, 27 pages** — and then restored, with digests compared and **both files
asserted to exist before comparing**. `docs/06` now carries the instruction to
repeat that before any of them publishes.
**Three of the five undertaking quotations were hand-typed and are now rendered
from `CONDUCT_UNDERTAKINGS`.** §4's third class says why in terms: the strings
live in one constant so the diff that would soften one is visible on one line.
A fourth hand-copy of a published commitment, in a file nobody re-reads against
§4, is exactly the silent failure that class was flagged for. Verified by
building with the articles published and matching all three strings byte for
byte against the constant (200, 162 and 161 characters).
⚠️ **IF AN ARTICLE PUBLISHES, §4's ROWS (a), (b), (c), (d) AND (f) EACH GAIN A
SURFACE** and their "where it ships" column has to say so. Not done, because
nothing is published — flagged by two of the fix agents independently, and
batched for Pouya.
### A process finding of my own, recorded because it cost real time
**I ran five article-fix agents concurrently with an adversarial reviewer that I
had told to probe the check scripts.** Three of the five reported their file
being reverted mid-edit, one reporting frontmatter reading `draft: false` /
`reviewedByPouya: true` — which is a state that existed only inside my own
temporary-publish verification, and which the reviewer would reproduce to probe
the D9 gate. A fourth diagnosed a second, independent cause: **a batch of twelve
concurrent `Edit` calls to one file lost four of them.**
Both causes are mine. **Do not run a probing reviewer against the same working
tree as agents editing it**, and do not fan out concurrent edits to a single
file. Ground truth was verified rather than assumed: all five files
`draft: true` / `reviewedByPouya: false`, zero article pages in `dist/`, every
fix marker present, `git stash` empty, and the reviewer's own probe — a
`DELIBERATELY WRONG CARD TEXT` headline it had injected into the OG endpoint —
restored to `entry.data.title`.
### Review — `adversarial-reviewer` alone, per D20. Round 1: 16 findings, all acted on
**Two blocking, and both were on pages built in this run.**
**1. `/fees/` shipped an invisible navigation link — measured at 1.00:1.**
`<Button variant="ghost">` sets `color: var(--text)` (ink) and a border of
`--border` (ink at 10% alpha). On a `.section-inverse` ground both are the
background colour: `{"t":"How an engagement runs →","color":"rgb(26, 22,
20)","bg":"rgb(26, 22, 20)","ratio":1}`. Worse than the gold-on-cream 2.10:1 this
project treats as unshippable, and a screenshot showed one button where the
markup has two.
⚠️ **AND THE PAGE SCORED LIGHTHOUSE ACCESSIBILITY 100.** axe's `color-contrast`
rule **skips a foreground that exactly equals its background** as "unable to
determine". So the a11y category cannot be the only contrast control here, and a
computed-contrast sweep is not redundant with it — which is the generalisable
half of this finding.
Fixed with **inheriting custom properties on `Button.astro`, not a descendant
rule in `global.css`**, and that choice is load-bearing: `.section-inverse
.btn-ghost` would compile at (0,2,0), identical to `.btn-ghost[data-astro-cid]`,
so which won would depend on injection order — a trap this record shows being hit
once already on `.btn-gold`. Custom properties inherit, which is the one
mechanism that legitimately crosses the boundary. A site-wide computed-contrast
sweep over all 22 pages now returns **0 failures**.
**2. `/legal/privacy/` stated a falsehood about who receives the data.** It said
*"Amazon Web Services is therefore a processor for this information. **No other
third party receives it.**"* §7 records mail hosting as **Google Workspace**, and
D18 sends the notification to `info@smlcompany.ca` — so **Google receives and
stores every submission**, including the opposing parties and their counsel,
which is the most sensitive thing the form collects. The page's own next section
already admitted it: *"The notification sits in my mailbox."* That mailbox is
Google's. A reader making a PIPEDA access request was told there was one
processor when there are two. The page now names both, and names the inquirer's
own provider as receiving the confirmation.
**The thirteen should-fix findings, and the two most instructive.**
⚠️ **`npm run og:proof` did not check article cards at all — the one surface
`check:claims` cannot reach.** For an article path it matched the card FILENAME
and `continue`d, skipping the headline comparison entirely, so its own header and
`docs/04` both described a check it was not performing. Proved with a probe:
`DELIBERATELY WRONG CARD TEXT` in the endpoint, one article published, and the
script printed *"no card asserts anything its page does not"*, exit 0.
`checkedHeadlines` stayed pinned at the registry size however many articles
published — a coverage number that reads like completeness and falls further
behind as the site grows.
⚠️ **AND THE FIRST REPAIR WAS WORSE: IT COMPARED THE ARTICLE'S `<h1>` AGAINST
ITSELF.** A tautology. The same probe passed again. Caught by re-running the
probe rather than by reading the patch — which is the entire argument for round 2
and for probing a check instead of inspecting it.
**The working fix is not a cleverer comparison; it leaves nothing to compare.**
The derivation moved into `articleCard()` in `src/data/og-cards.ts`, called by
both the endpoint and the proof script, so the endpoint holds no headline literal
that could disagree with anything. The script then checks the one thing still
able to drift — whether the article's own frontmatter `title` is what the route
renders as its `<h1>` — plus a coverage assertion that every non-portrait page
was compared. Re-probed by making the route render `{data.title} — probe`: exit
1, both sides printed, and the coverage assertion fired as an independent second
signal.
The other eleven, each acted on: `SiteFooter` links `/insights/` from **all 22
pages** while `insights/index.astro` claimed nothing linked into an empty section
(the link stays — `docs/01` specifies the footer as the full sitemap — so the
sentence changed); `/contact/received/` stated a confirmation *"has been sent"*
on two paths that reach it having sent nothing (the honeypot, and
`Promise.allSettled` on both emails rejecting); the committed PDF is
**indexable**, re-creating the duplicate `/bio/` was made `noindex` to prevent —
`X-Robots-Tag` is now a cutover item, because a `Disallow` will not de-index;
the radio group's touch target measured **25.6 px** against `docs/02`'s 44 px
floor; the consent checkbox's accessible name was a **250-character paragraph**
with a link nested inside a label for another control; the handler **hand-typed**
`NO_RETAINER_NOTICE` and **dropped its fourth clause** (*"and does not itself
create a conflict check"*, which `docs/01` requires) three lines below a comment
explaining why `RESPONSE_TIME` is injected rather than typed; the confirmation
email printed **raw field names** to the inquirer (`practiceArea: Construction`);
`overtimeStartsAfterSessionHours` was **dead data with a 21-line comment
claiming a live constraint** — nothing read it, so reversing Q59 would have
changed nothing and failed nothing, which is Q22's shape at constant scope;
`priceRange` **mixed units and understated the floor** (see below); two comments
asserted `priceRange` was absent in the file that set it; and the deploy guard
blocked on `INTAKE_ENDPOINT`, which **nothing in `src/` reads** since step 8
moved the form to a same-origin path, while the thing that decides whether the
form works — the CloudFront `/api/*` behaviour — was guarded nowhere.
⚠️ **`priceRange` IS OUT, AND IT WENT IN AND OUT ON THE SAME DAY.** `docs/04`
gates it on `/fees/` existing; the gate was met, so it shipped as `$500$9,500`.
Its own defence rejected a `min`/`max` over `FEES` because *"a range whose ends
mean different units is a range that misinforms"* — **and the ends it chose had
different units too**: the floor was the hourly rate, the ceiling a flat fee. The
floor also misinformed in the direction that matters: the least anyone pays for
the headline service is **$2,000**, so the one machine-readable number on the
site advertised a quarter of the real entry price. **Omitted rather than
repaired** — `docs/04` gates the field, it does not require it, and `/fees/`
publishes the conditions that make any single range meaningless.
**Declined: none.** All 16 acted on. One partially: the D19 comment-ratio
"consider". The named outliers were trimmed — the parenthetical in `index.astro`
narrating a previous version of itself, `insights.ts`, the OG endpoint,
`ANALYTICS.installed`'s 17 lines for one boolean, and `/about/`'s contrast
reasoning written twice — **and the ratio still rose, 0.40 → 0.44**, because
sixteen fixes each carry their reason. Reported rather than churned; it is the
one review item this entry does not claim to have satisfied.
**One false claim of my own, struck:** `scripts/bio-pdf.mjs` said the PDF renders
*"deterministically"*. Two runs gave 89,496 bytes both times and **different
digests** — Chrome stamps a `/CreationDate`. The consequence is real: every
re-render is a binary diff, so a reviewer cannot tell a content change from a
no-op. The comment now says so, and `docs/06` says re-commit it when something
actually changed.
### ⚠️ `npm run format` BROKE THE BUILD, AND `*.mdx` IS NOW IN `.prettierignore`
Found at the end of the run, by running the check rather than assuming it.
**Prettier rewrote an MDX JSX comment from `{/* … */}` to `{/_ … _/}`** — it read
the asterisks as markdown emphasis — and the build then failed with
`Could not parse expression with oxc: Unterminated regular expression`, because
MDX parses `{/_ …` as a regex literal. **It is fine in the source and dead at
build**, which is the same shape as the `animation-timeline` minifier defect and
the `import.meta.url` path bug in the card generator: three defects in one run
that only exist in the built output.
`.prettierignore` already carried `*.md` and not `*.mdx`. It now carries both,
and the entry records the second reason as well, which would matter even if
Prettier fixed the first: **these five files are hand-audited prose.** They were
read line by line against §4 and against the sourced extracts, and 76 findings
were applied to them. Machine-reflowing audited copy means the committed bytes
are no longer the bytes that were audited. The two articles Prettier had already
touched were restored from the audited copies, digests compared, **both files
asserted to exist before comparing**.
*(Prettier's other change was benign and is worth recording so the ignore does
not read as broader than it is: on the second article it converted a YAML
`'Ontario''s …'` to `"Ontario's …"`, which is the same string more clearly
escaped. Restored anyway, so the committed articles are byte-identical to the
ones `check:claims` passed over.)*
**One `eslint-disable` was added in the same pass, with its reason in the code:**
`no-control-regex` on the handler's sanitiser. That rule exists to catch a control
character that reached a pattern by accident; here the control range **is** what
is being matched, and it is the part of the function that stops a submitted value
forging an email header.
### `README.md`'s script table was missing a control, and had been for two days
It listed seven commands and **not `npm run check:claims`** — the one that runs on
every deploy and is, under D20, the only per-step claims control there is. The
four added in this run were missing too. **A table of the project's controls that
omits a control is the shape those controls exist to catch**, and it is the first
table a newcomer reads. Now eleven rows, with the two that cannot run in CI
marked as such and the reason given, and with a parenthetical recording the
omission rather than quietly closing it.
### Verification — run, with exit statuses read directly rather than through a pipe
```
npm run check exit 0 0 errors, 0 warnings, 0 hints (56 files)
npm run lint exit 0 eslint + prettier, after `*.mdx` was ignored
npm run build exit 0 22 pages
npm run check:claims exit 0 11 patterns, 22 pages · and 27 pages with the articles published
npm run og:proof exit 0 21 og:image targets resolve; 20 card headlines == their page <h1>
npm run check:intake exit 0 12 form fields vs 12 handler fields
npm run lighthouse exit 0 22 pages — table below
npm run bio:pdf exit 0 89,496 bytes, 1 page, Letter
minifier tripwire exit 1 clean (no `animation:` beside a scroll timeline)
```
**All 20 generated cards were LOOKED AT, not just string-compared.**
`npm run og:proof -- --sheet` writes a contact sheet to `dist/og-proof.jpg`; the
3 × 7 grid was read. Every card is legible, on the site's own type and palette,
carries the designation line and the mark, and none overflows or clips — the
longest headline (`/practice/energy/`, 63 characters) wraps to two lines and
fits. **This is the step the R15 discharge says the check cannot do**: `og:proof`
compares strings and cannot see a card that renders badly. The 21st `og:image`
target is the portrait, which `/` and `/about/` share by decision.
**And the five drafted articles were link-checked separately, because the sweep
over `dist/` cannot see them.** A draft ships no HTML, so its links are outside
every check on this project until the day it publishes — which is the worst
moment to discover one is broken. **29 internal links across the five, 0
unresolved against the 22 built pages, and 0 external links in any of them** (so
no link rot to inherit, and no URL that would need its own source under R14).
**Rendered with SCRIPT EXECUTION DISABLED at the CDP level** — not merely "no
`<script>` tags present", which is the weaker claim:
```
/ nodes= 315 text= 4761ch h1=1 links=50 forms=0 controls= 0
/contact/ nodes= 295 text= 2972ch h1=1 links=40 forms=1 controls=16
/fees/ nodes= 296 text= 3736ch h1=1 links=39 forms=0 controls= 0
/insights/ nodes= 177 text= 1611ch h1=1 links=39 forms=0 controls= 0
/legal/privacy/ nodes= 209 text= 5655ch h1=1 links=39 forms=0 controls= 0
/bio/ nodes= 218 text= 3128ch h1=1 links=37 forms=0 controls= 0
```
`/contact/` is the page that matters there: **the intake form and all sixteen of
its controls render and are usable with scripting off**, which is the whole point
of the POST-redirect-GET design rather than a happy consequence of it.
Measured on `dist/` as well: **0 `.js` files, 0 `client:*` directives**, one
`<h1>` on every page, **22 unique titles all 5060 characters and 22 unique
descriptions all 140160**, canonical + ≥10 OG + ≥5 Twitter tags on every page,
every JSON-LD block parsing with a `@graph`, no heading-level jumps, **25 distinct
internal links and 0 dangling**, and a sitemap of **17 URLs** — 22 pages less the
five `noindex` ones, which is the arithmetic working.
**Lighthouse, 22 pages, mobile — re-run on the FINAL bytes after every round-1
fix, because the earlier numbers described superseded output:**
| | worst | note |
|---|---|---|
| Performance | **99** | `/` only; every other page 100 |
| Accessibility | **100** | every page. Measured with `prefers-reduced-motion` forced — see §7 |
| Best practices | **100** | every page |
| SEO | **100** | every *indexable* page. The five `noindex` pages are asserted audit by audit |
| CLS | **0.000** | every page |
| LCP | 1.502.03 s | one page at or over budget: `/` |
**`/` LCP is 2.03 s against `docs/04`'s < 2.0 s, and it is left alone
deliberately.** The LCP element is **`p.hero-lede` — text, not the portrait**
`[verified — lcp-breakdown-insight]`, so `fetchpriority="high"` on the hero image
would misdirect priority away from the Geist face that actually gates it. The
remaining lever is `BaseLayout`'s three-face preload, which is a documented trade
set by measurement. Reported, not chased.
**`/about/`'s new inverse band was verified by measuring the rendered elements**,
not by reading the CSS: background `rgb(26,22,20)`, 14 text elements, **worst
ratio 11.09:1**. The one rule that had to change was `.cred-title`, which was
`--text-secondary`**1.43:1 on ink**, worse than the gold-on-cream 2.10:1 this
project treats as the defect that must never ship.
### What is NOT done, and is not hidden
- **Nothing is deployed.** D11, and step 11 has not started.
- **The intake form does not work.** The handler is undeployed and the CloudFront
`/api/*` behaviour does not exist. Three cutover items.
- **DynamoDB TTL is unverified and `/legal/privacy/` promises automatic
deletion.** The handler writes `ttl`; TTL must also be *enabled on the table*
and §7 does not record that it is. **A cutover blocker on a published promise.**
- **API Gateway rate limiting is not configured**, and it is carrying load the
unimplementable timing check was specified to carry.
- **`claims-auditor` has not run.** D20 puts its single pass at cutover, over the
whole finished site, and `docs/06` carries it as blocking.
- **The five articles are unread by Pouya**, which is D9 and is the point.
- **R6 booking stays parked**; `/contact/` mentions it nowhere.
- **`struck-universal-q39`** still has no §4 row and no checklist line — carried,
unresolved, and his call which side it is fixed on.
- **`/legal/terms/` has no governing-law clause, no limitation of liability and no
warranty disclaimer.** §4 bars this repository from concluding a proposition of
law; batched for him.
---
## 2026-08-31 (ag) — Q58 and Q57 ruled and closed; §4's mediation row enumerates all six areas; `docs/03`'s compliance checklist stops paraphrasing §4 and cites it; and a derived fee term was struck from the one document that is the authority on money
**Four rulings from Pouya, plus what implementing them turned up.** Both of the