feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Build and deploy / build-and-deploy (push) Failing after 4s

Five items of Pouya's production run, 2026-09-01.

Q61 — scroll-padding-top becomes a max() ramp on `10lh - 83px`, with the
plain calc() first as the fallback for engines without `lh`. Hidden focus
stops under minimumFontSize=32: 290 of 1,455 -> 0, control build still
290. Default settings byte-identical (0 differences over 352 page-widths x
17 fields). The 12 residual cells at minimumFontSize=16/20 are pre-existing
and unchanged-or-better; reported, not widened, per instruction.

Intake backend + CloudFront — docs/09-cutover-runbook.md is the
copy-paste sequence for admin execution: every command followed by its
verification and expected output, rollback per part, and Part 10 is Q60's
TTL test. infra/cloudfront/router.js is the trailing-slash function
(30-case suite; 8 fail against the pre-review version, incl. a
protocol-relative open redirect). infra/cloudfront/configure.mjs is
dry-run-by-default and idempotent. scripts/intake-env.mjs emits the six
Lambda env vars from src/data/site.ts.

Four launch blockers found by reading the running system:
  - handler.mjs wrote pk/sk; the live table's key is submissionId with no
    sort key, so every submission would have failed validation silently
  - the Lambda invoke permission is scoped to the old route path
  - 22 of 23 pages 403 without the router function
  - there was no 404 page; src/pages/404.astro adds it

Claims audit (D20 cutover pass) — five gloss over-reaches corrected on
/practice/energy/, /practice/insurance/ (x2), /practice/technology/ and
/med-arb/. Three findings left open for Pouya: Q62, the /med-arb/ gloss,
and Q60.

Q62 — one frozen-tripwire pattern added under the freeze's own breach
exception, with a probe and four negative fixtures. check:claims exits 1
until the false /legal/privacy/ sentence is corrected, so both deploy
paths are blocked by a mechanism rather than by memory.

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-09-02 06:52:20 -04:00
co-authored by Claude Opus 5
parent ca1c2524e1
commit bd282aa47d
30 changed files with 3256 additions and 143 deletions
+7
View File
@@ -109,6 +109,13 @@ export const OG_CARDS: Record<string, { eyebrow: string; headline: string }> = {
eyebrow: 'Bio',
headline: 'Pouya Lajevardi',
},
/* `/404/` builds to `dist/404.html` and is `noindex`, but a shared 404 link is
exactly the kind of URL that gets pasted into a chat window — so it gets a
card on the same one-rule-no-exceptions basis as `/bio/`. */
'/404/': {
eyebrow: 'Not found',
headline: 'That page is not here.',
},
'/legal/privacy/': {
eyebrow: 'Privacy',
headline: 'What the intake form collects, and for how long.',
+42 -7
View File
@@ -259,14 +259,33 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
text: 'The Enhancing Digital Security and Trust Act, 2024 conditions each of its artificial-intelligence obligations on regulations prescribing who they apply to and when. Two regulations have been made under it — one on cyber security, one on digital technology affecting people under 18 — and neither is the AI one.',
},
{
lead: 'And no federal or Ontario statute requires data to be stored in Canada.',
text: "This is the one worth stating plainly, because data-residency clauses are often drafted against the opposite assumption. The federal Privacy Commissioner's own guidance says PIPEDA does not prohibit an organisation in Canada from transferring personal information to another jurisdiction for processing; what the Act requires instead is accountability — the organisation stays responsible for information it has transferred to a third party. Ontario's health privacy statute imposes no storage-location rule either.",
/* THE LEAD WAS "And no federal or Ontario statute requires data to
be stored in Canada" — a universal over the FOUR instruments the
extract actually checked (PIPEDA, Ontario FIPPA, PHIPA and
O. Reg. 329/04), which is the shape §4 Forbidden's struck Q39
universal bars in both directions: this repository does not
conclude a proposition of law. Named instruments only. */
/* ⚠️ NAMED, AND THE FIRST CORRECTION ONLY NARROWED THE CLASS.
"Ontario's public-sector privacy statutes" is a class of two —
FIPPA and MFIPPA — and the extract records a residency finding for
FIPPA and none for MFIPPA, so the narrowed lead was still a
universal over an unchecked instrument. Same shape, smaller.
The lead names the three ACTS the extract searched. O. Reg. 329/04
is searched too and is deliberately not named: it is a regulation
under PHIPA, so naming the Act covers it without putting a
regulation number on a marketing page. */
lead: "And neither PIPEDA, nor Ontario's Freedom of Information and Protection of Privacy Act, nor its Personal Health Information Protection Act requires data to be stored in Canada.",
text: 'This is the one worth stating plainly, because data-residency clauses are often drafted against the opposite assumption. The federal Privacy Commissioner\'s own guidance says PIPEDA does not prohibit an organisation in Canada from transferring personal information to another jurisdiction for processing; what the Act requires instead is accountability — the organisation stays responsible for information it has transferred to a third party. Neither Ontario statute contains a storage-location rule either — FIPPA has no data-localisation provision, and PHIPA\'s "Disclosure outside Ontario" section is a disclosure permission rather than a rule about where records sit.',
},
{
text: 'Which matters in a dispute because the parties are often arguing about a clause neither of them can point to a source for. Establishing what the obligation actually is, rather than what both sides assumed it was, frequently narrows the disagreement to something a mediation can close in a day.',
},
],
note: "Described as the state of the instruments, not applied to anyone's file, and the residency point is the Privacy Commissioner's own words rather than a conclusion of mine. All of it is sourced in docs/reference/canada-privacy-technology.md and all of it can change — a bill at second reading in August 2026 is not a bill at second reading forever. What any of it means for a particular contract is a question for each party's own counsel.",
/* THE NOTE CLAIMED THE WHOLE RESIDENCY POINT WAS THE COMMISSIONER'S
WORDS. It is his words for PIPEDA and a reading of the Ontario
statutes for the rest — so the note disclaimed a conclusion the page
does in fact draw, which is worse than drawing it openly. */
note: "Described as the state of the instruments, not applied to anyone's file. On residency the PIPEDA half is the federal Privacy Commissioner's own words; the Ontario half is what FIPPA and PHIPA say, and all three are named rather than described as a class. All of it is sourced in docs/reference/canada-privacy-technology.md and all of it can change — a bill at second reading in August 2026 is not a bill at second reading forever. What any of it means for a particular contract is a question for each party's own counsel.",
ground: 'inverse',
},
{
@@ -302,8 +321,14 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
'connection regime in Ontario.',
h1: 'Grid disputes are engineering disputes with a regulator attached.',
lede:
'Ontario has spent the last year rewriting how large loads and new ' +
'generation get connected. That produces commercial disputes between ' +
/* "and new generation" was here and is struck: the extract establishes a
change for large loads (Electricity Act s. 28.1) and for what the Board
may weigh on a leave-to-construct application, and its one quotation on
generation runs the other way — the normal System Impact Assessment
"applies to the connection of all generation facilities, renewable or
non-renewable, equally". A class asserted from one instance. */
'Ontario has spent the last year rewriting how large loads ' +
'get connected. That produces commercial disputes between ' +
'proponents, distributors, transmitters and municipalities long before ' +
'anything reaches a regulator.',
disputeTypesLede:
@@ -463,11 +488,21 @@ export const PRACTICE_PAGES: Record<PracticeSlug, PracticePage> = {
},
{
lead: "Its case conference is the Tribunal's own, and I am not appointed to it.",
text: "The Tribunal's settlement step is a case conference led by one of its adjudicators, who is then disqualified from hearing the matter. It is sometimes called a pre-hearing, which is the Tribunal's own label for it. A privately retained neutral does not conduct it and cannot be appointed to it, and nothing on this page should be read as offering that.",
/* "who is then disqualified from hearing the matter" was here and
overstated the rule. LAT Rule 14.3 disqualifies the Member
"except with the consent of the parties" — an absolute where the
rule is qualified. */
text: "The Tribunal's settlement step is a case conference led by one of its adjudicators, who does not then sit on the hearing panel unless the parties consent. It is sometimes called a pre-hearing, which is the Tribunal's own label for it. A privately retained neutral does not conduct it and cannot be appointed to it, and nothing on this page should be read as offering that.",
},
{
lead: 'What I offer is private mediation.',
text: 'Retained by the parties, on their own terms, under an agreement to mediate they sign. The Tribunal\'s own materials point parties toward mediation: under the heading "Consider other ways to resolve your dispute", the accident-benefits page says that before you apply, you may want to consider negotiation or mediation services.',
/* "The Tribunal's own materials point parties toward mediation" was
here: a plural class, and a direction, resting on one permissive
sentence on one page that pairs mediation with negotiation and
ranks neither. The extract's own adversarial check named both
over-reads; this is the same gloss one notch weaker, and it
survived the correction to the quotation beside it. */
text: 'Retained by the parties, on their own terms, under an agreement to mediate they sign. The Tribunal\'s accident-benefits page names mediation as something to consider before applying: under the heading "Consider other ways to resolve your dispute", it says that before you apply, you may want to consider negotiation or mediation services.',
},
],
note: 'That quotation is about mediation before an application is filed, and it is quoted no wider than it goes. Sourced in docs/reference/lat-case-conference.md, which carries the full passage and a correction to an earlier reading of it.',
+158
View File
@@ -0,0 +1,158 @@
---
/**
* The 404 page. `docs/04-seo-spec.md`: "Real, styled, with search-intent links
* out. CloudFront must return it with a genuine 404 status."
*
* ⚠️ THE PAGE IS HALF OF THE FIX AND THE DISTRIBUTION IS THE OTHER HALF. Astro
* emits this as `dist/404.html`; nothing in the build can make CloudFront serve
* it. Until the custom error response exists, a missing URL returns S3's
* `AccessDenied` XML — measured 2026-09-01, not assumed: `/about/` and
* `/definitely-not-a-page/` both answered **403, `application/xml`, 111 bytes**
* on the live distribution. `docs/06` carries the two commands and the reason
* the mapping is on 404 rather than 403.
*
* `noindex`, because a 404 that invites indexing is a 404 that gets indexed.
* `robots` is `noindex,follow` so the links out are still crawled, which is the
* whole point of a page with links out.
*
* NO CLAIM ABOUT PRACTICE OR CREDENTIALS APPEARS IN THE VISIBLE COPY, and the
* omission is the design rather than an oversight. An error page has no reader
* who came for a credential, so a sentence it adds is a sentence `AGENTS.md` §4
* has to carry for no return. It names pages and nothing else.
*
* ⚠️ THE JSON-LD IS A DIFFERENT MATTER AND THIS COMMENT USED TO DENY IT. The
* `pageGraph()` above is the shared graph and it DOES emit the §4 Person node —
* `jobTitle`, the `description`, and `hasCredential` for Q.Med and Q.Arb. Every
* one of those is registered, so it is not a §4 breach; the false statement was
* this comment, which invited the next editor to treat the 404 page as outside
* the register's blast radius. It is not: this is machine-readable credential
* assertion served on every unmatched URL, to exactly the reader `robots.txt`
* names — "an assistant that counsel is using to shortlist a neutral". Found by
* `adversarial-reviewer`, 2026-09-01.
*/
import BaseLayout from '../layouts/BaseLayout.astro';
import Button from '../components/Button.astro';
import Eyebrow from '../components/Eyebrow.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../assets/og-portrait.jpg';
import { pageGraph } from '../data/schema';
import { CONTACT } from '../data/site';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
/* The routes worth offering, in the order a lost reader is most likely to want
them. Not the full sitemap — the footer on this page already carries that. */
const routes = [
{ href: '/mediation/', label: 'Mediation' },
{ href: '/arbitration/', label: 'Arbitration' },
{ href: '/med-arb/', label: 'Med-Arb' },
{ href: '/practice/', label: 'Practice areas' },
{ href: '/fees/', label: 'Fees' },
{ href: '/about/', label: 'About' },
];
---
<BaseLayout
title="Page Not Found · Dispute Resolution · Pouya Lajevardi"
description="That page is not here. Mediation, arbitration and med-arb each have a page, the practice areas are listed, and an inquiry can be sent from contact."
jsonLd={graph}
noindex
>
<section class="section hero">
<div class="wrap">
<Eyebrow dot>Not found</Eyebrow>
<h1 class="display hero-h">That page is not here.</h1>
<div class="prose">
<p class="statement">
The address may have changed, or it may never have existed.
</p>
<p>
If you were looking for something specific, email <a
href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a
> and say what it was.
</p>
</div>
<nav class="routes" aria-label="Main pages">
<ul role="list">
{
routes.map((route) => (
<li>
<a href={route.href}>{route.label}</a>
</li>
))
}
</ul>
</nav>
<div class="cta">
<Button href="/">Start at the beginning</Button>
<Button href="/contact/" variant="ghost">Send an inquiry &rarr;</Button>
</div>
</div>
</section>
</BaseLayout>
<style>
.hero {
padding-block: var(--space-9) var(--space-11);
}
.hero-h {
margin-block: var(--space-4) var(--space-5);
font-size: var(--text-5xl);
}
.statement {
font-size: var(--text-lg);
line-height: var(--leading-body);
color: var(--text);
}
.routes {
margin-block-start: var(--space-7);
padding-block-start: var(--space-5);
border-block-start: 1px solid var(--rule);
}
.routes ul {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-6);
margin: 0;
padding: 0;
list-style: none;
}
.routes a {
display: flex;
align-items: center;
justify-content: center;
/* BOTH AXES. `min-block-size` alone left "Fees" at 37 x 44 px — measured, and
under `docs/02`'s 44 x 44 floor on the inline axis while the comment beside
it claimed compliance. WCAG 2.5.8's 24 x 24 AA minimum was still met via the
`--space-6` gap; this is the project's own stricter floor.
`padding-inline` as well as the minimum, so a short label is a wide target
rather than a narrow one centred in a wide box. */
min-block-size: 44px;
min-inline-size: 44px;
padding-inline: var(--space-2);
font-family: var(--font-mono);
font-size: var(--text-sm);
letter-spacing: var(--tracking-wide);
color: var(--link);
text-decoration: none;
}
.routes a:hover {
text-decoration: underline;
}
.cta {
display: flex;
flex-wrap: wrap;
gap: var(--space-3) var(--space-4);
margin-block-start: var(--space-8);
}
</style>
+17
View File
@@ -245,6 +245,23 @@ const COLLECTED = INTAKE_FIELDS.map((field) => field.label);
not the section that answers the same question is the sweep failure
`CLAUDE.md` describes. Found by `adversarial-reviewer` round 2. */
}
{
/* TODO(pouya): the sentence below is FALSE as at 2026-09-01 and this
page must not go public until you rule — §9 Q62. The AWS account has
an `admins` IAM group carrying `AdministratorAccess` with TWO
members, you and one other person, and
`simulate-principal-policy` returns **allowed** for
`dynamodb:GetItem`/`Query`/`Scan` on this table for both.
Evidence and commands:
`docs/reference/intake-table-access-verification.md`.
THE QUESTION: do you remove that access — which may be the same
co-administrator Q23's Gitea instance depends on, so it is not free
— or does this paragraph state the true number? Nothing here may be
softened into "authorised administrators": on this page a reader is
entitled to the specific, and a true vacancy is worse than a false
specific only in that it cannot be caught.
Raised by `claims-auditor`, D20 cutover audit, finding 8. */
}
<p>
I can. The table is reachable by the function that writes to it and by
one administrative account, which is mine — nobody else has access to
+3 -2
View File
@@ -213,8 +213,9 @@ const graph = medArbGraph({
<p>
The ADR Institute of Canada publishes <strong
>ADRIC Med-Arb Rules</strong
>, developed by a task force and presented to the membership at
ADRIC's 2019 annual conference, and designed, in ADRIC's words, to <q
>, developed by a task force and presented to the membership as a
discussion draft at ADRIC's 2019 annual conference, and designed, in
ADRIC's words, to <q
>work in tandem with ADRIC's existing Mediation Rules and
Arbitration Rules</q
>. They were drafted for domestic commercial disputes, and ADRIC notes
+24 -9
View File
@@ -121,18 +121,33 @@ html {
/* The header is sticky from 66rem up, and `scroll-padding-top` has to clear it
or "Skip to content" drops the reader behind it — the one control that exists
specifically for keyboard users, landing them on content they cannot see.
`--header-h` is defined in tokens.css beside the value it has to match, and it
is a FLOOR — above the default text size the masthead reflows and is taller, and
`SiteHeader` gates stickiness so the offset is generous rather than short.
⚠️ TWO CASES DEFEAT THAT GATE and leave this offset short — one a reader
setting, one the webfont swap window. Under Chrome's minimum font size the
shortfall is an OPEN WCAG 2.2 SC 2.4.11 (AA) failure, not an accepted one:
`AGENTS.md` Q61. **CSS CAN detect that setting** — the font-metric units
(`ch`, `ex`, `cap`, `lh`) respond to it where `rem`/`em` do not — so a fix is
possible here and is awaiting a ruling. Measurements: `docs/02` §Reflow. */
`--header-h` is a FLOOR at the default text size, so the `max()` ramp is what
carries the cases where the masthead reflows taller (`AGENTS.md` Q61).
⚠️ THREE THINGS HERE ARE LOAD-BEARING AND EACH BREAKS SILENTLY.
1. `1lh` and not `1rem`/`1em`. Chrome's minimum-font-size setting enlarges
text while `rem` keeps resolving at 16px; the font-metric units read the
USED size and track it. `rem` here measures 97px against a 270.56px
header — no error, no warning, focus behind the header.
2. The plain declaration comes FIRST and is not redundant. An engine without
`lh` discards the whole `max()` as invalid, and the property then falls
back to `--space-4` = 16px, which is worse than no fix at all.
3. `1lh` on `<html>` is immune to the `font-display: swap` window ONLY
because `<html>` keeps the UA font family — `--font-sans` is set on
`body`. Moving the family up to `html` makes this offset depend on whether
a webfont has arrived. Do not.
One case is still short: fallback metrics with a seventh nav item, gated by a
build failure in `SiteHeader` (`AGENTS.md` R20). `docs/02` §Reflow has the
measurements. */
@media (min-width: 66rem) {
html {
scroll-padding-top: calc(var(--header-h) + var(--space-4));
scroll-padding-top: max(
calc(var(--header-h) + var(--space-4)),
calc(10lh - 83px)
);
}
}
+12 -5
View File
@@ -140,11 +140,18 @@
/* ⚠️ A FLOOR, NOT A CONSTANT: the sticky header's height AT THE DEFAULT TEXT
SIZE. Above the default the masthead reflows and is deliberately taller, which
is why the nav no longer runs off-screen. `global.css` drives
`scroll-padding-top` off it and `SiteHeader` gates stickiness so the offset is
generous rather than short — **but not in every case**: `docs/02` §Reflow lists
the two cases where it is still short, one of them an OPEN SC 2.4.11 (AA)
failure (`AGENTS.md` Q61). If SiteHeader's padding or nav sizing changes,
re-measure and change this with it — one fact living in two files.
`scroll-padding-top` off it as the LOWER BOUND of a `max()` ramp — the ramp,
not this token, is what covers the settings where the masthead reflows taller
(`AGENTS.md` Q61, fixed 2026-09-01). One case remains short and is gated by a
build failure rather than by this value: `AGENTS.md` R20. If SiteHeader's
padding or nav sizing changes, re-measure and change this with it — one fact
living in two files.
⚠️ **AND THERE IS A THIRD FILE, WHICH IS THE ONE A READER MISSES.**
`global.css`'s ramp is `max(calc(var(--header-h) + var(--space-4)),
calc(10lh - 83px))`, and that `83px` is fitted so the second term equals the
first at the default text size — 97 px both ways, measured. **Change this
token and the 83 px moves with it**, or the ramp stops being a no-op at
normal settings and starts changing the shipped rendering.
[measured 2026-08-26, re-measured 2026-09-01 — headless Chrome, six nav items
and a seventh injected. 81.00px at all EIGHT widths from 1056 to 1920px
(1056/1100/1216/1240/1280/1440/1600/1920): 32 padding + 48 reserved brand