feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Build and deploy / build-and-deploy (push) Failing after 4s
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:
co-authored by
Claude Opus 5
parent
ca1c2524e1
commit
bd282aa47d
+253
-48
@@ -39,6 +39,30 @@ One thing blocks the workflow, and it is not a fact to look up:
|
||||
- **Actions are not enabled and no runner is registered** (Q23). The Gitea
|
||||
instance is jointly administered, so both need its second administrator.
|
||||
|
||||
> ✅ **AND THAT IS NOW A DECISION RATHER THAN A BLOCKER — Pouya's ruling,
|
||||
> 2026-09-01: THE FIRST PRODUCTION DEPLOY GOES OUT VIA `scripts/deploy-local.sh`,
|
||||
> AND THE GITEA RUNNER IS DEFERRED UNTIL AFTER LAUNCH.**
|
||||
>
|
||||
> His reason, and it is the reason the deferral is safe: enabling Actions and
|
||||
> registering `act_runner` depends on the instance's **second administrator**, so
|
||||
> the date it happens is not his to set — and **nothing about the deploy depends
|
||||
> on it.** The script and the workflow are one artefact in two places; they run
|
||||
> the same guard, the same three passes with the same cache headers, the same
|
||||
> invalidation and the same intake route check. The pipeline changes only **how a
|
||||
> deploy is triggered**. So a launch that waits for a runner is a launch waiting
|
||||
> on someone else's calendar for no gain.
|
||||
>
|
||||
> **What the deferral actually costs, stated rather than waved past:** every
|
||||
> deploy until then is a person at a keyboard with a long-lived access key in
|
||||
> their environment, and nothing gates a merge to `main` (there is no
|
||||
> `pull_request` trigger). Both were already true; the ruling does not add a
|
||||
> risk, it declines to remove one yet. **`AGENTS.md` R17's rotation date does not
|
||||
> move** — the key is live either way.
|
||||
>
|
||||
> It is **not** a cutover-checklist item and must not become one: a checklist
|
||||
> fires once, at cutover, and this is deliberately scheduled for after it. The
|
||||
> `docs/09-cutover-runbook.md` Part 8 sequence is the deploy path that ships.
|
||||
|
||||
> ✅ **`adr-sml-deploy` EXISTS — created 2026-08-26, Q22 closed 2026-08-28.**
|
||||
> **DO NOT CREATE IT.** This bullet said *"`adr-sml-deploy` does not exist —
|
||||
> `aws iam get-user` returns `NoSuchEntity`… Create it from Create the user
|
||||
@@ -364,6 +388,27 @@ Then invalidate `/*`.
|
||||
|
||||
## Cutover checklist — D11 is a single shot, so run all of it
|
||||
|
||||
> **The commands are in `docs/09-cutover-runbook.md`.** This list is what must be
|
||||
> true; that file is how, in order, with each command's verification and the
|
||||
> output to expect. It also records the order dependency this list cannot show:
|
||||
> **the distribution work goes first and the site sync goes last**, because
|
||||
> reversing them puts 22 of 23 pages behind a 403 for as long as a CloudFront
|
||||
> deployment takes.
|
||||
|
||||
> 🛑 **THREE THINGS BLOCK THIS ENTIRE LIST AS AT 2026-09-01, AND TWO OF THEM WERE
|
||||
> FOUND BY READING THE RUNNING SYSTEM RATHER THAN THE SPECS.**
|
||||
>
|
||||
> 1. **`/legal/privacy/` states something false about who can read the intake
|
||||
> table** — §9 **Q62**, evidence in
|
||||
> `docs/reference/intake-table-access-verification.md`. A privacy policy is
|
||||
> the wrong page to be wrong on, and this one is wrong about third parties'
|
||||
> dispute information.
|
||||
> 2. **Q60 is still open** — TTL is `ENABLED` and no record has been watched to
|
||||
> disappear, and the same page asserts the mechanism. `docs/09` Part 10 is the
|
||||
> test and its answer arrives days after it starts, so **start it early**.
|
||||
> 3. **`claims-auditor`'s D20 pass returned six copy findings on 2026-09-01;
|
||||
> five are corrected and one is a ruling** — the `/med-arb/` gloss, below.
|
||||
|
||||
**Cutover prep — deferred maintenance, done BEFORE the checklist below**
|
||||
|
||||
This group exists because deferring a thing and forgetting it look identical
|
||||
@@ -406,6 +451,33 @@ the decision is re-readable rather than re-litigated.
|
||||
defects worth catching late are the ones that only exist once the pages sit
|
||||
next to each other. Give it the whole built site and the reading order a
|
||||
visitor takes.
|
||||
|
||||
✅ **IT HAS RUN ONCE, 2026-09-01, OVER ALL 23 PAGES — AND IT RETURNED
|
||||
FAIL WITH EIGHT FINDINGS, WHICH IS THE ARGUMENT FOR D20 RATHER THAN
|
||||
AGAINST IT.** Five are corrected (`/practice/energy/`'s lede asserted a
|
||||
change to how *generation* connects, which the extract's own quotation
|
||||
denies; `/practice/insurance/` stated an absolute adjudicator
|
||||
disqualification where LAT Rule 14.3 says *"except with the consent of the
|
||||
parties"*, and glossed one permissive sentence as *"the Tribunal's own
|
||||
materials point parties toward mediation"*; `/practice/technology/` asserted
|
||||
*"no federal or Ontario statute"* — a universal over the **four**
|
||||
instruments its extract checked — and its own note then disclaimed that
|
||||
conclusion as the Commissioner's words; `/med-arb/` said the ADRIC Med-Arb
|
||||
Rules were presented to the membership where the source says a **discussion
|
||||
draft** was). Three remain and each has its own item below or above:
|
||||
the `/med-arb/` gloss (a ruling), Q60, and Q62.
|
||||
|
||||
**Two things about that result are worth carrying forward.** The pass found
|
||||
no defect in any claim about Pouya, his credentials or his designations —
|
||||
every one traced. What it found was **five over-reaches in glosses on
|
||||
sourced legal material**, which is the failure mode a per-step claims run on
|
||||
a single page is least able to see and the one `check:claims` cannot grep
|
||||
for at all. **And it re-derived a stale instruction inside its own brief:**
|
||||
`.claude/agents/claims-auditor.md` still tells it Q.Arb is *"commenced
|
||||
August 2026"*, which §4 struck on 2026-08-29 — applied literally it would
|
||||
have flagged correct copy and demanded the struck form. It read §4 instead.
|
||||
That is the fifth stale claim found in that file and it is not the agent's
|
||||
to fix
|
||||
- [ ] **Pouya has read every page against `AGENTS.md` §4.** The human pass. It is
|
||||
the other half of D20 and it is not delegable — his reading is what the
|
||||
per-step audit was traded for.
|
||||
@@ -467,7 +539,27 @@ the decision is re-readable rather than re-litigated.
|
||||
Chrome — it is not in CI and cannot be (see above). Read the exit status.
|
||||
Every category ≥ 95 on every page; the accessibility figure is measured
|
||||
with `prefers-reduced-motion` forced and that qualification is reported
|
||||
with it (`AGENTS.md` §7)
|
||||
with it (`AGENTS.md` §7).
|
||||
✅ **Run 2026-09-01 on the 23-page build: exit 0, worst of 23 is
|
||||
99 / 100 / 100 / 100.** Re-run at cutover — this is dated, not discharged.
|
||||
⚠️ **The script was enumerating 22 of 23 and its header claimed it
|
||||
enumerated the site.** It collected `index.html` only, and `404.astro` is
|
||||
emitted as `dist/404.html`, outside `build.format: 'directory'`. So the
|
||||
budget would have been asserted over every page except the new one, and
|
||||
silently. `og:proof` had the identical blind spot and reported it
|
||||
**backwards** — as the 404 card being orphaned ("generated, but no built
|
||||
page references it") rather than the page being unmeasured. Both fixed; the
|
||||
two use different path forms for the same file on purpose, and each says why
|
||||
beside the code. **`check:claims` was never affected** — it takes any
|
||||
`.html`, so the new page's copy was inside the claim register from its first
|
||||
build. One instrument of three saw the page, which is the argument for
|
||||
having three.
|
||||
⚠️ **One reported number sits at its budget: LCP on `/` is 2.03 s against
|
||||
`docs/04`'s < 2.0 s.** The script reports LCP and CLS without asserting
|
||||
them, deliberately — simulated throttling on a loopback server is not the
|
||||
Slow 4G field measurement `docs/04` budgets against — so this is not a
|
||||
failing gate and must not be reported as a passing one either. CLS is
|
||||
0.000 on all 23
|
||||
- [ ] Every page renders fully with JavaScript disabled
|
||||
- [ ] `curl` of each URL returns real content, not a shell
|
||||
- [ ] All internal links resolve; no orphan pages
|
||||
@@ -480,12 +572,44 @@ the decision is re-readable rather than re-litigated.
|
||||
is invisible from inside the repo, and because card text cannot be reached
|
||||
by `check:claims`. Run `npm run og:proof -- --sheet` and **look at the
|
||||
contact sheet** as well — the check compares strings, not rendering
|
||||
- [ ] 404 returns a 404 status
|
||||
- [ ] **404 returns a 404 status AND serves the 404 page. Both halves, and until
|
||||
2026-09-01 neither existed.** There was **no 404 page in the site at all**
|
||||
— no `src/pages/404.astro`, 22 pages, and `docs/04` had asked for one
|
||||
("Real, styled, with search-intent links out") since before build step 1 —
|
||||
**and** no custom error response on the distribution, so a missing URL
|
||||
returned S3's `AccessDenied` XML. Measured, not inferred: `/about/` and
|
||||
`/definitely-not-a-page/` both answered **403, `application/xml`, 111
|
||||
bytes** on the live distribution. The page now exists (`/404/`, `noindex`,
|
||||
out of the sitemap, with its own OG card because the registry has one rule
|
||||
and no exceptions) and `docs/09` Parts 1–3 configure the distribution. **Tick
|
||||
this after `docs/09` Part 8.4**, which checks the status *and* greps the body
|
||||
for the page's own `<h1>` — a 404 status serving an empty body passes the
|
||||
status half and fails the point
|
||||
- [ ] Security headers present (`securityheaders.com` A or better)
|
||||
- [ ] **SES identities verified for sending** — confirmed 2026-08-26, re-check at cutover: `aws sesv2 get-email-identity --email-identity smlcompany.ca` and confirm `VerifiedForSendingStatus: true`
|
||||
- [ ] **SES bounce/complaint alarms actually notify someone** — `AGENTS.md` §7 records the `ses-alerts` email subscription as **pending confirmation**, and an unconfirmed SNS subscription drops every message. Confirm it, then `aws sns list-subscriptions-by-topic` and check the ARN is not `PendingConfirmation`. *(SES production access itself is granted — Q19 closed.)*
|
||||
- [x] **SES identities verified for sending** — `VerifiedForSendingStatus: true`, `DkimAttributes.Status: SUCCESS`, signing enabled, and no custom MAIL FROM (so DMARC rests on DKIM alignment, which is what §7 records) `[re-verified 2026-09-01 — sesv2 get-email-identity]`
|
||||
- [x] ✅ **SES bounce/complaint alarms DO notify someone — R9 DISCHARGED, 2026-09-01.** `aws sns list-subscriptions-by-topic` on `ses-alerts` returns the email subscription to `info@smlcompany.ca` with a **real subscription ARN**, not `PendingConfirmation`. §7 recorded it as pending, and §12 R9 said *"this is the first thing to check if `/contact/` ships"* — it had been confirmed at some point before this reading and the record had not moved, which is the same staleness in the safe direction. *(SES production access itself is granted — Q19 closed.)*
|
||||
- [ ] **THE INTAKE FORM DOES NOT WORK YET, AND THREE THINGS HAVE TO HAPPEN BEFORE
|
||||
IT DOES — build step 8 shipped the page and not the pipe.**
|
||||
⚠️ **THE COMMANDS ARE `docs/09-cutover-runbook.md` PARTS 5 AND 6, AND
|
||||
WRITING THEM FOUND TWO MORE THINGS, EACH OF WHICH WOULD HAVE LOST EVERY
|
||||
SUBMISSION.** Both were found by reading the running system rather than the
|
||||
specs, which is the only way either could have been found.
|
||||
**(a) The handler wrote the wrong key.** It built its item with
|
||||
`pk: INTAKE#<uuid>` / `sk: <timestamp>` from `docs/05`'s table, and the
|
||||
live table's key schema is **`submissionId` (S), no sort key** — a
|
||||
`PutItem` missing the key attribute fails the whole write with
|
||||
`ValidationException`, which the handler catches and answers with the
|
||||
failure page. **The form would have looked broken to every inquirer while
|
||||
the record went nowhere**, from the moment `/api/*` was wired. A DynamoDB
|
||||
key schema cannot be altered after creation, so the handler moved to the
|
||||
table; `docs/05` §Storage records the alternative that was declined.
|
||||
**(b) The Lambda's invoke permission is `SourceArn`-scoped to the OLD
|
||||
route.** The one statement on the function conditions
|
||||
`apigateway.amazonaws.com` on `…/4tl0m5igkj/*/*/submissions`. Add a route at
|
||||
`/api/intake` without adding a permission and API Gateway is refused: the
|
||||
caller gets a **500**, and **the Lambda logs nothing at all** because it is
|
||||
never entered — the hardest possible thing to diagnose from CloudWatch,
|
||||
which is where anyone would look first.
|
||||
1. **A CloudFront behaviour routing `/api/*`** to the HTTP API origin
|
||||
`AGENTS.md` §7 records. The form posts to the same-origin path
|
||||
`/api/intake` — deliberately, so the CSP can be `form-action 'self'`,
|
||||
@@ -512,21 +636,36 @@ the decision is re-readable rather than re-litigated.
|
||||
not itself creating a conflict check**, which a hand-typed copy in the
|
||||
handler had dropped. `RESPONSE_TIME` must read identically on `/contact/`,
|
||||
in the confirmation email, and in the bio.
|
||||
3. **API Gateway throttling, 5 requests / 5 minutes per source IP**
|
||||
(`docs/05`). Not expressible in handler code, and it is carrying load
|
||||
the unimplementable 3-second timing check was specified to carry — see
|
||||
`docs/05` §Three deviations.
|
||||
3. **An AGGREGATE API Gateway route throttle** — `docs/09` Part 6.3. Not
|
||||
expressible in handler code, and it carries load the unimplementable
|
||||
3-second timing check was specified to carry (`docs/05` §Three
|
||||
deviations). ⚠️ **THIS ITEM SAID "5 requests / 5 minutes per source IP"
|
||||
AND THAT IS NOT BUILDABLE AT API GATEWAY** — throttling there is per
|
||||
route and per stage across all callers. It contradicted this same file's
|
||||
own correction further down the list, and it is a to-do list an operator
|
||||
works through, so it would have cost an hour in the API Gateway console
|
||||
looking for a control that is not there. Per-IP needs AWS WAF, which is
|
||||
a cost decision and has its own item.
|
||||
|
||||
✅ **BOTH DEPLOY PATHS NOW WARN IF ITEM 1 IS MISSING**, so this does not
|
||||
rest on someone reading this list. `scripts/deploy-local.sh` and
|
||||
`.gitea/workflows/deploy.yml` each end with an **Intake route check**: a
|
||||
`POST` to `/api/intake` with no `Origin`. **404 means not routed. 403 means
|
||||
routed and refused**, which is the handler's `Origin` check doing its job
|
||||
and is therefore a pass — and is why the probe is safe against production:
|
||||
it is rejected before any DynamoDB write or any email. It warns rather than
|
||||
failing, because by that point the site is already deployed and failing the
|
||||
job would not un-deploy it. This replaced an `INTAKE_ENDPOINT` guard that
|
||||
blocked deploys on a variable nothing read.
|
||||
`.gitea/workflows/deploy.yml` each end with an **Intake route check**.
|
||||
⚠️ **AND THIS PARAGRAPH DESCRIBED A PROBE THE SCRIPTS NO LONGER RUN.** It
|
||||
said the probe was *"a `POST` to `/api/intake` with **no `Origin`**"* where
|
||||
*"404 means not routed, 403 means routed and refused… therefore a pass"*.
|
||||
Both scripts send **`Origin: https://adr.smlcompany.ca`** and assert **303
|
||||
to `/contact/could-not-send/`**; 403 is a **warning**, not a pass, because
|
||||
it cannot be told apart from CloudFront rejecting a disallowed method. A
|
||||
stale description of a live control, in the spec that governs it — found by
|
||||
`adversarial-reviewer`, 2026-09-01.
|
||||
**What the probe actually is:** the handler validates, finds an empty
|
||||
submission, and redirects — which happens **before any DynamoDB write and
|
||||
before any email**, and is what makes it safe against production. It warns
|
||||
rather than fails, because by that point the site is deployed and failing
|
||||
the job would not un-deploy it. **404 is ambiguous between three causes**
|
||||
and `docs/09` Part 7.1 lists them with the one command that separates them.
|
||||
This replaced an `INTAKE_ENDPOINT` guard that blocked deploys on a variable
|
||||
nothing read.
|
||||
- [ ] **DYNAMODB TTL ENABLED ON THE TABLE, AND VERIFIED BY A TEST RECORD. THIS
|
||||
ONE BACKS A PUBLISHED PROMISE AND MUST NOT BE TICKED FROM THE HANDLER
|
||||
CODE.** `/legal/privacy/` states that a record is *"deleted automatically
|
||||
@@ -542,6 +681,21 @@ the decision is re-readable rather than re-litigated.
|
||||
confirm it actually disappears. Both halves, or the item stays open. §9
|
||||
Q60 and §12 R19 track it; `src/pages/legal/privacy.astro` carries the
|
||||
matching `TODO(pouya)`.
|
||||
✅ **THE SETTING IS RE-CONFIRMED AND THE PROCEDURE NOW EXISTS:**
|
||||
`TimeToLiveStatus: ENABLED` on attribute `ttl` `[re-verified 2026-09-01 —
|
||||
describe-time-to-live]`, and `docs/09-cutover-runbook.md` **Part 10** is the
|
||||
behaviour test — write a row with a near-future `ttl`, record its id to a
|
||||
file, and check on a **different day**.
|
||||
⚠️ **START IT EARLY. DynamoDB's TTL sweeper is not prompt and AWS does not
|
||||
promise that it is** — deletion typically lands within a couple of days of
|
||||
the timestamp passing, so the runbook sets the earliest useful check at
|
||||
**48 hours** and refuses to call a failure before **7 days**. A check run
|
||||
five minutes after the `ttl` that finds the row present has measured
|
||||
nothing. This is the one blocking item on this list whose answer cannot be
|
||||
hurried, so it should be started before anything else here, not last.
|
||||
⚠️ **And `get-item` on a missing key exits 0 with an empty body** — read the
|
||||
output, not the exit status; a script checking only the status reports
|
||||
present and absent identically.
|
||||
- [ ] **`npm run check:intake`** — the form's field table and the Lambda's agree
|
||||
on names, requiredness, caps and option sets. They are two independent
|
||||
tables on purpose (`docs/05`); this is what stops them drifting
|
||||
@@ -613,35 +767,86 @@ the decision is re-readable rather than re-litigated.
|
||||
control proving `break-word` would not have worked, and the candidates
|
||||
declined as broader than the cause
|
||||
|
||||
- [ ] 🛑 **THE MINIMUM-FONT-SIZE STICKY RESIDUAL — REOPENED THE SAME DAY IT WAS
|
||||
TICKED, AND IT IS A WCAG 2.2 SC 2.4.11 (AA) FAILURE RATHER THAN THE
|
||||
SKIP-LINK INCONVENIENCE IT WAS ACCEPTED AS. NEEDS A FRESH RULING.**
|
||||
It was ruled accepted on 2026-09-01 on the basis *"you proved no CSS
|
||||
mechanism can see minimum-font-size; the only fix is JS and zero-JS is a
|
||||
founding decision"*, and this checklist item was ticked accordingly. Both
|
||||
supporting facts are wrong, and `adversarial-reviewer` found it the same day.
|
||||
**(1) The premise.** The proof covered `rem`/`em` and queries written in them.
|
||||
The font-metric units **`ch`, `ex`, `cap`, `lh`, `rlh`** read the *used* font
|
||||
size and double under the setting — in property values, in `@media` **and** in
|
||||
`@container`. A pure-CSS detector exists, so "the only fix is JS" does not
|
||||
hold. **(2) The cost.** Not a short skip-link landing but **ordinary keyboard
|
||||
focus landing entirely behind an opaque sticky header**: a Shift+Tab walk over
|
||||
6 pages × 5 widths ≥ 1056 px gives **290 entirely-hidden focus stops of 1,455**
|
||||
under the setting — **254 in the footer and 36 inside `#main`** — against **0**
|
||||
at the default size. Hit-tested with `elementFromPoint`, not inferred from
|
||||
geometry: geometry alone said 43 at the default size and all 43 were the skip
|
||||
link, which is stacked *above* the header on purpose. **(3) And the header fix
|
||||
created it:** a build of `fce89d4~1` measures **0** under the same setting,
|
||||
because `flex-wrap: nowrap` kept the masthead one row and short. So the trade
|
||||
was **830 px of horizontal overflow (SC 1.4.10) for 290 obscured focus stops
|
||||
(SC 2.4.11)** — both Level AA, and the asymmetry that was the whole argument
|
||||
does not exist. **A candidate fix is measured but deliberately not
|
||||
implemented:** a third gate term in the shipping idiom, `calc((15px - 1ch) *
|
||||
100000)`, un-sticks the header under the setting and leaves
|
||||
`inset-block-start: 0px` at the default — but 15 px is fitted to Geist's `ch`
|
||||
and needs checking under fallback metrics and Chrome's other font presets.
|
||||
**This item is ticked only by a ruling from Pouya, not by a re-measurement.**
|
||||
The decision is `AGENTS.md` **Q61**; `docs/02` §Reflow carries the measurements
|
||||
- [x] ✅ **THE MINIMUM-FONT-SIZE SC 2.4.11 (AA) FAILURE IS FIXED, 2026-09-01 —
|
||||
Q61 RULED *fix now* AND CLOSED.** Two declarations on `html` inside the
|
||||
existing `@media (min-width: 66rem)` block: the plain
|
||||
`calc(var(--header-h) + var(--space-4))` first, then
|
||||
`max(calc(var(--header-h) + var(--space-4)), calc(10lh - 83px))`.
|
||||
**`1lh` and not `rem`, because the font-metric units read the USED font
|
||||
size** — that is the mechanism the earlier ruling's premise denied existed.
|
||||
**Measured, with the pre-fix build rebuilt in a worktree and put through the
|
||||
identical probe:**
|
||||
the Shift+Tab walk over 6 pages × 5 widths ≥ 1056 px goes from **290
|
||||
entirely-hidden focus stops of 1,455 to 0** — same grid, same 1,455 steps,
|
||||
same 344 header-internal stops skipped — while the pre-fix control still
|
||||
reports **290** (footer 254, `#main` 36), so the probe is proven able to see
|
||||
the failure it now reports gone.
|
||||
**The default-settings rendering is unchanged: 0 differences across 352
|
||||
page-widths × 17 fields = 5,984 comparisons**, full-page geometry
|
||||
fingerprint included, with a positive control detecting exactly 1 injected
|
||||
difference. `scroll-padding-top` computes to **97 px** at the default and
|
||||
**287 px** under the setting against a 270.56 px header; `1lh` on `<html>`
|
||||
is **18 px / 37 px with every `.woff2` blocked**, identical, because
|
||||
`<html>` keeps the UA family — the property that the `ch` candidate could
|
||||
not claim.
|
||||
**A wider grid than the ruling asked for, and it found a residual: 777
|
||||
cells over 37 settings went from 63 failing to 12, and nothing got worse in
|
||||
any cell.** The remainder is the next item, it is **pre-existing**, and it
|
||||
was deliberately not fixed — Pouya's instruction was to stop and report
|
||||
anything beyond the one declaration's blast radius
|
||||
- [ ] ⚠️ **MINIMUM FONT SIZE AT 16 AND 20 STILL LEAVES `#main` BEHIND THE HEADER —
|
||||
PRE-EXISTING, REDUCED BY Q61'S FIX, AND OUTSIDE ITS MECHANISM.** At
|
||||
`minimumFontSize=16`, **52 px** of `#main` sits behind the sticky header
|
||||
after "Skip to content" on `/about/` and `/contact/` at 1280, 1440 and
|
||||
1920 px — 6 cells; at `minimumFontSize=20`, **14 px** on the same 6. Before
|
||||
Q61's fix the same grid gave 6 cells at 52 px, **12 at 64 px**, 15 at 75 px
|
||||
and 15 at 174 px, so every remaining cell is unchanged or better and none
|
||||
is new.
|
||||
**Why the `lh` ramp cannot see it, and this is the interesting part:** the
|
||||
setting *floors* computed sizes, so at `minimumFontSize=16` the root is
|
||||
genuinely 16 and `1lh` on `<html>` is 18 px — identical to the default —
|
||||
while the header grows to **149.38 px** because the sub-16 px type (the
|
||||
11 px tagline, the 14 px eyebrow and nav) is floored up. The ramp reads a
|
||||
quantity the setting did not move. **A different measurement would be
|
||||
needed** — `1lh` taken from an element that actually carries the small type,
|
||||
or a floor on the header — and that is a header change with site-wide reach,
|
||||
which is why it is filed rather than taken inside a capped review loop.
|
||||
Not a conformance failure of the same shape as Q61: a partial obscuring is
|
||||
not SC 2.4.11, which is about a component **entirely** hidden. `docs/02`
|
||||
§Reflow carries the 777-cell grid
|
||||
- [ ] ⚠️ **THE `/med-arb/` GLOSS NEEDS A RULING — `claims-auditor`, D20 pass,
|
||||
finding 1.** §Why this practice says *"Med-arb is not a third service
|
||||
bolted onto mediation and arbitration; it is the two of them run under one
|
||||
agreement"*, and one section above the page publishes ADRIC's own words:
|
||||
med-arb is *"not merely the merging of separate mediation and arbitration
|
||||
processes, but a unique process designed to meet the needs of particular
|
||||
disputants"*. **The gloss asserts what its own cited authority denies in
|
||||
terms, on the same page.** Remove one of the two — either the *"not merely
|
||||
the merging"* clause from the quotation, or the *"it is the two of them run
|
||||
under one agreement"* sentence. ⚠️ **Do not resolve it by adding a
|
||||
competence claim:** ADRIC answers its own competence sentence with the
|
||||
Chartered Med-Arb designation and the Med-Arb Foundational Course, neither
|
||||
of which is held, and C.Med-Arb is struck from this site entirely (§4). The
|
||||
2026-08-30 fix took the competence proposition out of the **heading** and
|
||||
left it in the **body**, which is why this is a second pass at the same
|
||||
paragraph
|
||||
- [ ] ⚠️ **FOUR PRE-EXISTING DYNAMODB ITEMS HAVE NO `ttl` AND WILL NEVER EXPIRE.**
|
||||
The table held 4 items before any of this work `[verified 2026-09-01 —
|
||||
describe-table ItemCount]`, written by the hand-built handler, whose code
|
||||
is in `docs/reference/AWS-Hosting-Guide.md` Part 8.3 and **writes no `ttl`
|
||||
attribute at all**. So they are retained indefinitely while
|
||||
`/legal/privacy/` says a record is deleted automatically after 24 months.
|
||||
**Pouya's call and not scriptable**, because it turns on something no agent
|
||||
can determine: whether they are the guide's own smoke test (`Test User`) or
|
||||
real inquiries that arrived through the old site. Read them, then delete the
|
||||
test rows or write a `ttl` onto the real ones. `docs/09` Part 7.3
|
||||
- [ ] ⚠️ **PER-IP RATE LIMITING NEEDS AWS WAF AND IS A COST DECISION — NOT A
|
||||
LAUNCH BLOCKER.** `docs/05` asked for "5 requests / 5 minutes per source
|
||||
IP at API Gateway"; **API Gateway throttling is aggregate, not per-IP**, so
|
||||
that spec line is struck and an aggregate route throttle ships instead
|
||||
(`docs/09` Part 6.3). A rate-based WAF rule on the distribution is what
|
||||
would do per-IP. Decide it on price, not on the spec's old wording — and do
|
||||
not let anything describe the throttle that ships as per-IP
|
||||
|
||||
- [ ] ⚠️ **A FOOTER NAV LABEL OVERRUNS ITS COLUMN BY 24 px AT 640 px UNDER
|
||||
MINIMUM FONT SIZE, WITH 7.7 px OF CLEARANCE TO THE NEXT COLUMN.** No document
|
||||
@@ -704,10 +909,10 @@ the decision is re-readable rather than re-litigated.
|
||||
checks passed while both defects shipped. **Owner: `AGENTS.md` R20**
|
||||
|
||||
**Infrastructure**
|
||||
- [ ] S3 versioning enabled
|
||||
- [ ] Bucket not publicly readable; OAC in force
|
||||
- [x] **S3 versioning enabled** — `Status: Enabled` `[re-verified 2026-09-01 — get-bucket-versioning]`. This is what makes pass 3's `--delete` recoverable at cutover
|
||||
- [x] **Bucket not publicly readable; OAC in force** — all four public-access blocks `true`, and a direct `GET` on the S3 hostname returns **403** `[re-verified 2026-09-01 — get-public-access-block, curl]`. ⚠️ **Re-check this after `docs/09` Part 1**, which adds an `s3:ListBucket` statement for the CloudFront service principal so that missing keys return 404 rather than 403. Part 1 asserts the two facts that actually make the grant safe: **no viewer path resolves to the bucket root** (the default root object, then the router, always turn `/` into a key), and the default behaviour **forwards no query strings** to the origin with **no origin request policy** overriding that. ⚠️ **This line used to prescribe `curl "$SITE/?list-type=2"` as "the check that matters"** — and Part 1 was rewritten in the same change set to explain that that check **cannot fail** and is therefore evidence of nothing. Two documents, one fix, and the discarded check survived in the one an operator ticks. Found by `adversarial-reviewer` round 2
|
||||
- [ ] ACM certificate valid; Namecheap validation CNAME still present
|
||||
- [ ] CloudWatch alarms: Lambda errors, DLQ depth, 5xx rate
|
||||
- [ ] **CloudWatch alarms: Lambda `Errors`, API Gateway `5xx`.** ⚠️ **"DLQ depth" was on this line and is struck: a dead-letter queue on this function would never receive anything.** `DeadLetterConfig` applies to **asynchronous** invocations, and API Gateway invokes synchronously — so the alarm would have sat permanently green while telling everyone it was watching, which is this project's most expensive recurring shape (`AGENTS.md` Q22, the Lighthouse row, and now this). `docs/05` §Notification carries the replacement and why there are two alarms rather than one: Lambda `Errors` catches a synchronous failure, and API Gateway `5xx` catches the case where the function is never entered at all
|
||||
- [ ] Billing budget/alarm still active — `aws budgets describe-budgets --account-id "$(aws sts get-caller-identity --query Account --output text)"`. `docs/reference/AWS-Hosting-Guide.md` set up an **AWS Budget**, which `cloudwatch describe-alarms` will never return. Whether one was actually created is not recorded anywhere: confirm, do not assume
|
||||
|
||||
**Post-cutover, same day**
|
||||
|
||||
Reference in New Issue
Block a user