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
@@ -718,11 +718,11 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
|
||||
| AWS account | `327082975128` `[verified 2026-08-26 — inventory]` |
|
||||
| Region | **`ca-central-1`** throughout — hosting, Lambda, DynamoDB `[verified 2026-08-26]` |
|
||||
| S3 bucket | **`adr-smlcompany-site`** — versioning **Enabled**, so rollback works `[verified 2026-08-26]` |
|
||||
| CloudFront | **`E1OK7G98KNKUTA`**, alias `adr.smlcompany.ca`, origin `adr-smlcompany-site.s3.ca-central-1.amazonaws.com`, Deployed `[verified 2026-08-26]` |
|
||||
| CloudFront | **`E1OK7G98KNKUTA`**, alias `adr.smlcompany.ca`, origin `adr-smlcompany-site.s3.ca-central-1.amazonaws.com` with OAC **`E13GAFUL6UQP6R`**, Deployed `[verified 2026-08-26; config re-read 2026-09-01]`. Default behaviour: `Managed-CachingOptimized`, `Managed-SecurityHeadersPolicy`, methods HEAD/GET, `redirect-to-https`, default root object `index.html`. ⚠️ **AND THREE THINGS THE SITE NEEDS ARE ABSENT: NO FUNCTION ASSOCIATIONS, NO CUSTOM ERROR RESPONSES, NO CACHE BEHAVIOURS** `[verified 2026-09-01 — get-distribution-config]`. The first is why **22 of the 23 pages do not serve**: `astro.config.mjs` sets `trailingSlash: 'always'` with `build.format: 'directory'`, so CloudFront asks S3 for the key `about/`, which does not exist. Measured on the live distribution the same day: `/` **200**, `/about/` and `/definitely-not-a-page/` both **403 with an 111-byte `application/xml` body** — S3's `AccessDenied`, served raw. `docs/09-cutover-runbook.md` Parts 1–3 configure all three; `infra/cloudfront/` holds the function and the config script |
|
||||
| ACM certificate | `arn:aws:acm:us-east-1:327082975128:certificate/2b6d5bdf-6790-430c-9b82-c00ab66e6d87` — ISSUED `[verified 2026-08-26]` |
|
||||
| Intake API | `adr-intake-api`, HTTP API `4tl0m5igkj`, endpoint `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` `[verified 2026-08-26]` |
|
||||
| Intake Lambda | `adr-intake-handler`, `nodejs24.x` `[verified 2026-08-26]` |
|
||||
| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]`. **TTL IS `ENABLED`, `AttributeName: ttl`** ``[verified 2026-08-31 — Pouya ran `describe-time-to-live` and read `TimeToLiveStatus: ENABLED`]``. The handler side matches: `backend/intake/handler.mjs` writes `ttl` as a Number in **epoch seconds** at **24 months** (`RETENTION_MONTHS = 24`, added to `getUTCMonth()`), which is `docs/05` §Retention and the `ttl` row of its item table `[verified 2026-08-31 — read from the handler, not recalled]`. ⚠️ **IT WAS `DISABLED` AT FIRST VERIFICATION EARLIER THE SAME DAY, AND THAT IS RECORDED RATHER THAN OVERWRITTEN.** Pouya ran `describe-time-to-live` on **2026-08-31** and it returned `DISABLED`; he enabled it on **2026-08-31** and re-read `ENABLED` the same day. `/legal/privacy/` has stated since build step 10 that a record is *"deleted automatically by the database rather than by someone remembering to do it"* after 24 months, so **that promise was unbacked from the day it was written until the day it was enabled** — the handler wrote the attribute and nothing on the table consumed it. This is the Q22 shape on a public privacy commitment rather than on a deploy control: a documented mechanism that did not exist. ⚠️ **`ENABLED` PROVES THE SETTING, NOT THE BEHAVIOUR, AND THE BEHAVIOUR IS STILL UNPROVEN — §9 Q60 STAYS OPEN.** No record has been written with a near-future `ttl` and watched to disappear. `docs/06`'s cutover checklist carries that test as a blocking item, it is not ticked by reading this row or the handler code, and §12 R19 keeps it surfacing until a deletion has actually been observed |
|
||||
| Intake API | `adr-intake-api`, HTTP API `4tl0m5igkj`, endpoint `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` `[verified 2026-08-26]`. **One route, `POST /submissions`** → integration `0ftgjgv` (`AWS_PROXY`, payload format **2.0**, which is the format `handler.mjs` reads). Stage `$default`, auto-deploy on, **no throttling**, no access log. CORS allows `POST` from the site origin `[verified 2026-09-01 — get-routes, get-api, get-stages, get-integrations]`. `DisableExecuteApiEndpoint` is **false** and must stay false: the CloudFront origin **is** that hostname. The form's route (`POST /api/intake`) does not exist yet — `docs/09` Part 6 |
|
||||
| Intake Lambda | `adr-intake-handler`, `nodejs24.x`, **arm64**, handler `index.handler`, timeout **10 s**, memory **128 MB**, role `adr-intake-lambda-role`, **no environment variables**, no DLQ, code **1,527 bytes**, last modified 2026-05-26 `[verified 2026-09-01 — get-function-configuration]`. **That is still the HAND-BUILT function, not `backend/intake/handler.mjs`** — nothing has been deployed (D11). Its resource policy has **one** statement, `apigateway.amazonaws.com` conditioned on `SourceArn` `…/4tl0m5igkj/*/*/submissions` — **the old route's path only**, so a new route needs its own permission or API Gateway is refused and answers 500 with nothing in the Lambda log. The execution role is **sufficient as it stands**: `dynamodb:PutItem` on the table (write-only — it cannot read it), `ses:SendEmail`/`SendRawEmail`, plus `AWSLambdaBasicExecutionRole`. Deployment commands: `docs/09-cutover-runbook.md` Part 5 |
|
||||
| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]`. ⚠️ **KEY SCHEMA: PARTITION KEY `submissionId` (S), NO SORT KEY** ``[verified 2026-09-01 — `aws dynamodb describe-table`]``. **This contradicted `docs/05`, which specified `pk`/`sk`, and `backend/intake/handler.mjs` was written to the spec** — a `PutItem` missing the key attribute fails the whole write with `ValidationException`, the handler catches it and returns the failure page, so **every submission would have been lost while looking like a browser problem**. A DynamoDB key schema cannot be altered after creation; the handler was changed to the table on 2026-09-01 and `docs/05` §Storage carries the correction and the declined alternative. PITR **`ENABLED`**, 35-day window `[verified 2026-09-01 — describe-continuous-backups]`. Encryption at rest uses the **AWS-owned key — there is no customer-managed KMS key** `[verified 2026-09-01 — describe-table returns no SSEDescription]`, which `/legal/privacy/` does not claim, so nothing published depends on it. **4 items predate this repo**, written by the hand-built handler, which writes **no `ttl`** — so they never expire; `docs/06` carries that as Pouya's call. **TTL IS `ENABLED`, `AttributeName: ttl`** ``[verified 2026-08-31 — Pouya ran `describe-time-to-live` and read `TimeToLiveStatus: ENABLED`]``. The handler side matches: `backend/intake/handler.mjs` writes `ttl` as a Number in **epoch seconds** at **24 months** (`RETENTION_MONTHS = 24`, added to `getUTCMonth()`), which is `docs/05` §Retention and the `ttl` row of its item table `[verified 2026-08-31 — read from the handler, not recalled]`. ⚠️ **IT WAS `DISABLED` AT FIRST VERIFICATION EARLIER THE SAME DAY, AND THAT IS RECORDED RATHER THAN OVERWRITTEN.** Pouya ran `describe-time-to-live` on **2026-08-31** and it returned `DISABLED`; he enabled it on **2026-08-31** and re-read `ENABLED` the same day. `/legal/privacy/` has stated since build step 10 that a record is *"deleted automatically by the database rather than by someone remembering to do it"* after 24 months, so **that promise was unbacked from the day it was written until the day it was enabled** — the handler wrote the attribute and nothing on the table consumed it. This is the Q22 shape on a public privacy commitment rather than on a deploy control: a documented mechanism that did not exist. ⚠️ **`ENABLED` PROVES THE SETTING, NOT THE BEHAVIOUR, AND THE BEHAVIOUR IS STILL UNPROVEN — §9 Q60 STAYS OPEN.** No record has been written with a near-future `ttl` and watched to disappear. `docs/06`'s cutover checklist carries that test as a blocking item, it is not ticked by reading this row or the handler code, and §12 R19 keeps it surfacing until a deletion has actually been observed |
|
||||
| SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` |
|
||||
| SES account | **Production access GRANTED** — out of the sandbox in `ca-central-1`, confirmed by AWS in writing and effective immediately `[verified 2026-08-26 — Q19 closed]`. Mail now reaches unverified recipients, so the inquirer confirmation in D18 works |
|
||||
| Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` |
|
||||
@@ -735,7 +735,7 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
|
||||
| SES identity — `us-east-1` | **BEING DELETED — Pouya's instruction 2026-08-29, and the reason is a monitoring principle rather than a cleanup.** It was `Verified: false`, `DkimStatus: PENDING`, tokens `3zsn…` / `jejgp…` / `xpiwy…` — the doubled-domain set — a stray identity in a region this project does not use `[verified 2026-08-26]`. On **2026-08-29** it produced an AWS Health event, `AWS_SES_DKIM_PENDING_TO_FAILED`, which is the **expected** outcome of DKIM CNAMEs that never resolved. **That is its third false-alarm signal:** (1) a pending identity on the console dashboard, (2) a near-miss on requesting production access in the wrong region — the trap the row below records — and (3) this health event. Pouya's reason, kept because it generalises past SES: *"A monitoring surface that reports a failure you do not have trains you to discount the ones you do."* **No impact on mail.** Deletion removes the identity, not the records — the three live DKIM CNAMEs belong to the `ca-central-1` identity and are untouched by it |
|
||||
| ⚠️ **SES sandbox is per-region** | Recorded as a standing caution, not an open task — production access **was** granted in `ca-central-1` (Q19). The trap: the AWS console defaults to `us-east-1`, and granting access there leaves `ca-central-1` in the sandbox with the intake email still failing. Applies again to any future SES request `[verified 2026-08-26]` |
|
||||
| SES custom MAIL FROM | **Not configured** — SES reports "No MAIL FROM records found". This is why SES's SPF is unaligned and DMARC rests on DKIM. Optional future improvement, not required `[verified 2026-08-26]` |
|
||||
| SES monitoring — SNS topic | **`ses-alerts`**, `ca-central-1`. Email subscription to `info@smlcompany.ca` is **PENDING CONFIRMATION, not confirmed** `[verified 2026-08-26]`. **Until the subscription is confirmed the alarms below fire into nothing** — confirm the link in the subscription email |
|
||||
| SES monitoring — SNS topic | **`ses-alerts`**, `ca-central-1`. Email subscription to `info@smlcompany.ca` is **CONFIRMED** ``[verified 2026-09-01 — `aws sns list-subscriptions-by-topic` returns a real subscription ARN, `…:ses-alerts:44369cbe-1e56-494a-b0be-c20c0e09993e`, not the literal `PendingConfirmation`]``. ⚠️ **THIS ROW SAID `PENDING CONFIRMATION` UNTIL 2026-09-01 AND IS RECORDED RATHER THAN OVERWRITTEN.** It was written `[verified 2026-08-26]` and was true then; the link was clicked at some point after and the record did not move. So for some part of six days the alarms were working while four documents said they fired into nothing — **the same staleness as Q22 and the SES DKIM inversion, in the safe direction**, which is exactly why it survived: nothing broke, so nothing prompted a re-read. §12 **R9 closes on this**, and the lesson it leaves is that a row whose staleness is harmless is the row that stays stale longest |
|
||||
| SES monitoring — alarms | **`SES-BounceRate-High`** (bounce rate ≥ `0.03`) and **`SES-ComplaintRate-High`** (complaint rate ≥ `0.001`), both `ca-central-1`, `treat-missing-data: notBreaching` `[verified 2026-08-26]` |
|
||||
| SES bounce/complaint handling | **Email feedback forwarding** — SES's default, on unless disabled. **Deliberately not an SNS feedback topic:** at under 100 messages a month there is nothing to consume a programmatic feed. Revisit when code needs to act on a bounce `[verified 2026-08-26]` |
|
||||
| **DMARC alignment** | SES satisfies DMARC via **DKIM**, not SPF — without a custom MAIL FROM domain its envelope sender is at `amazonses.com` and is unaligned. The SPF record's real job is authenticating **Google Workspace** mail `[verified 2026-08-26]` |
|
||||
@@ -773,7 +773,8 @@ Nothing below can be invented. Each needs an answer from Pouya.
|
||||
|
||||
| # | Question | Blocks |
|
||||
|---|---|---|
|
||||
| **Q61** | 🛑 **THE MINIMUM-FONT-SIZE STICKY HEADER OBSCURES KEYBOARD FOCUS — SC 2.4.11 (AA). IT WAS RULED ACCEPTED ON 2026-09-01 AND THE RULING NEEDS RETAKING, BECAUSE BOTH FACTS IT RESTED ON ARE WRONG.** The ruling was *"you proved no CSS mechanism can see minimum-font-size; the only fix is JS and zero-JS is a founding decision"*, and the cost was recorded as a short skip-link landing traded against a WCAG 1.4.10 failure — *"one of the two is a WCAG failure while the other degrades the convenience of a skip link that still works"*. **(1) The premise is false.** Only the `rem`/`em` family is blind. 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` — measured, tabulated in `docs/02` §Reflow. A pure-CSS detector exists. **(2) The cost is a conformance failure of the same Level AA**, not a convenience loss: a Shift+Tab walk over 6 pages × 5 widths ≥ 1056 px finds **290 focus stops of 1,455 landing entirely behind the opaque sticky header** — 254 in the footer, **36 inside `#main`** — against **0** at the default text size, hit-tested with `elementFromPoint` rather than inferred from geometry. **(3) And the 2026-09-01 header fix created it:** a build of `fce89d4~1` measures **0** under the same setting. So the trade was 830 px of horizontal overflow (SC 1.4.10) for 290 obscured focus stops (SC 2.4.11), both AA, and the asymmetry that carried the decision does not exist. **WHAT IS NEEDED IS A RULING, AND THE RECOMMENDED CANDIDATE IS MEASURED AND WAITING.** `adversarial-reviewer` round 2 proposed `scroll-padding-top` — the property that already exists for this job — instead of un-sticking the header, and it is **strictly better than the `ch` gate on the one ground that deferred that gate.** Two declarations inside the existing `@media (min-width: 66rem)` block: `scroll-padding-top: calc(var(--header-h) + var(--space-4))` first as a fallback, then `scroll-padding-top: max(calc(var(--header-h) + var(--space-4)), calc(10lh - 83px))`. **Verified independently, values read from `getComputedStyle` rather than reasoned:** at the default size it computes **97 px — byte-identical to what ships** — and under the setting **287 px** against a header of at most 270.56 px, taking hidden focus stops to **0**. ⚠️ **`1lh` on `<html>` is IMMUNE TO THE WEBFONT FALLBACK**, which is what the `ch` candidate could not claim: `<html>` keeps the UA family (`--font-sans` is set on `body`, measured `Times`), so `1lh` is **18 px / 37 px identical with every `.woff2` request blocked**, and the offset still clears the header under Chrome's Georgia (277 px) and Verdana (307 px) presets. **Why it is NOT implemented:** `10lh - 83px` is still two fitted constants; the first declaration is load-bearing, because an engine without `lh` drops the whole `max()` as invalid and falls back to 16 px, which is *worse* than today; it needs the full 1,408-cell sweep plus the fallback-metrics and seven-item cases; the focus probe behind it is Chrome-only; and D19 caps the review at two rounds, so a header change made now would ship unreviewed. **This is Pouya's call, not a measurement gap.** Raised by `adversarial-reviewer` rounds 1 and 2, every figure reproduced independently | `docs/06`'s cutover checklist — the item was ticked as accepted this session and has been **restored to open**. It blocks no page and no other decision: the 22 pages build and the footer fix is unaffected. What it blocks is the claim that the site meets AA |
|
||||
| **Q62** | 🛑 **`/legal/privacy/` TELLS THE PUBLIC SOMETHING FALSE ABOUT WHO CAN READ THE INTAKE TABLE, AND IT IS A PRIVACY POLICY.** The page says: *"The table is reachable by the function that writes to it and by one administrative account, which is mine — nobody else has access to the table. There is no team, no assistant and no external administrator."* **The account has an IAM group `admins` carrying `AdministratorAccess` with TWO members**, and `iam simulate-principal-policy` returns **allowed** for `dynamodb:GetItem`, `dynamodb:Query` and `dynamodb:Scan` on the table for both of them — identical access, by the same route, the other user's own attachments being only `IAMUserChangePassword` `[verified 2026-09-01 — the five users, the group, and a five-row simulation, all commands in `docs/reference/intake-table-access-verification.md`]`. So **both halves of the sentence are wrong**: a second account has access, and it belongs to a second administrator of a shared account (§10). The three deploy users are `implicitDeny`; two CDK bootstrap roles carry `AdministratorAccess` but are assumable only by the same two administrators; the writing role holds **`PutItem` only and cannot read the table**, which is a stronger fact than the page currently claims and is the part of the sentence that is true. **THE QUESTION, AND IT IS ONE OF TWO THINGS.** (1) **Remove the access** — take that user out of `admins`, or deny DynamoDB on this table — after which the sentence becomes true as written. ⚠️ Note the likely collision: **Q23 records the Gitea instance as jointly administered and blocked on "its second administrator"**, so that access is probably not only for this account and removing it may cost something elsewhere. (2) **State the true number** — how many people hold administrative access, and that the function which writes cannot read. ⚠️ **DO NOT RESOLVE IT BY SOFTENING.** *"Access is limited to authorised administrators"* is the shape §4 exists to bar: defensible, uninformative, and it would replace a false specific with a true vacancy on the one page where a reader describing a live dispute is entitled to the specific. **Why this was invisible until now:** it is the only claim on the site whose subject lives entirely outside the repository, so R14 applies — *"unverifiable by construction"* — and there was no committed artefact to compare it against. There is now. Raised by `claims-auditor`, D20 cutover pass, finding 8 | **`/legal/privacy/` going public, and therefore the cutover.** `src/pages/legal/privacy.astro` carries the `TODO(pouya)` beside the sentence; `docs/06`'s checklist carries it as one of three items blocking the whole list. It blocks no other page and no other decision |
|
||||
| ~~**Q61**~~ | ✅ **CLOSED 2026-09-01 — RULED *fix now*, IMPLEMENTED AND MEASURED.** The minimum-font-size sticky header obscured keyboard focus: **290 entirely-hidden focus stops of 1,455** under `minimumFontSize=32`, a WCAG 2.2 **SC 2.4.11 (AA)** failure, created by the 2026-09-01 header fix. The fix is two declarations on `html` in the existing `@media (min-width: 66rem)` block — the plain `calc(var(--header-h) + var(--space-4))` first as a fallback, then `max(calc(var(--header-h) + var(--space-4)), calc(10lh - 83px))`. **`1lh`, not `rem`: the font-metric units read the *used* font size**, which is the mechanism the withdrawn ruling's premise denied existed. **Result, on the identical grid with the pre-fix tree rebuilt in a worktree as the control: 290 → 0, control still 290.** Default-settings rendering unchanged: **0 differences over 352 page-widths × 17 fields**, positive control detecting exactly 1 injected difference. `scroll-padding-top` 97 px at the default, 287 px under the setting against a 270.56 px header; `1lh` on `<html>` is 18 / 37 px **with every `.woff2` blocked**, identical, because `<html>` keeps the UA family. A wider grid than the ruling asked for — **777 cells over 37 settings — went from 63 failing to 12, with no cell worse.** The 12 are `minimumFontSize=16` and `=20`, they are **pre-existing and reduced**, and they were deliberately not fixed under Pouya's *"stop and report, do not widen"*: the setting floors sub-root type without moving the root, so `1lh` reads a quantity that did not change. `docs/06` carries it as its own item | *(closed)* |
|
||||
| **Q60** | ⚠️ **HAS A TEST RECORD BEEN OBSERVED TO DISAPPEAR FROM THE INTAKE TABLE?** **Half one closed 2026-08-31: TTL is `ENABLED` with `AttributeName: ttl`, verified by command — §7 holds that status and this row does not restate it.** The question is now the second half alone, and it was never the smaller half. `/legal/privacy/` does not merely publish a retention *period* — it asserts a **mechanism**: *"the record is deleted automatically by the database rather than by someone remembering to do it"*. **`ENABLED` proves the setting; only a record written with a near-future `ttl` and watched to vanish proves the behaviour.** Two things this may NOT be answered from: the handler code, which writes the attribute and nothing more (that side is verified and is not what is being asked); and the table setting, which is what was just confirmed. ⚠️ **AND THE FIRST HALF IS THE REASON TO TRUST THE SECOND LESS, NOT MORE:** `describe-time-to-live` returned **`DISABLED`** when Pouya first ran it on 2026-08-31, so the sentence above was published against a mechanism that was not running, and nothing in the repo, the build or AWS reported it. A setting that was off for as long as nobody looked is not evidence that the behaviour now works. `TODO(pouya)` sits on the retention section of `src/pages/legal/privacy.astro`; `docs/06`'s cutover checklist carries the test as blocking; §12 R19 keeps it surfacing. **Why this is a numbered question and not only a checklist line:** `CLAUDE.md` requires a `TODO(pouya)` plus a §9 row when a page needs a fact the repository does not have, and this page needs one — a cutover checklist fires once, at cutover, and §9 is what a person editing this page reads. Raised by `adversarial-reviewer` round 2, 2026-08-31 | **`/legal/privacy/` going public.** Nothing else — no other page states the mechanism, verified by sweeping `dist/` for the retention vocabulary and reading each hit in context |
|
||||
| ~~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 and `docs/07` carries it in full. ⚠️ **THIS ROW NAMED A CONSTANT THAT NO LONGER EXISTS** — `FEES.mediation.overtimeStartsAfterSessionHours` was deleted the same day as dead data: nothing read it, so reversing it would have changed nothing and failed nothing, which is Q22's shape at constant scope. **Where the ruling actually lives:** the trigger is rendered on `/fees/` from `halfDay.hours` / `fullDay.hours`, and `FEES.mediation.reservation` carries the half that publishes as prose. Found by `adversarial-reviewer` round 2 — §9 is what a later implementer reads to find where a ruling is recorded, so pointing it at a deleted identifier is the same defect one layer up. ⚠️ **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; Q15–Q17'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 |
|
||||
@@ -921,7 +922,7 @@ never being raised again.
|
||||
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
|
||||
| R5 | **Fee review at 12 months.** Published rates are sticky; the right moment to move them is deliberate, not reactive. ⚠️ **ONE ITEM IS ALREADY WAITING AND IT IS ARITHMETIC RATHER THAN JUDGEMENT — added 2026-08-31:** the half-day-plus-overtime route is cheaper than the full-day rate at **every** session length, by $2,000 at three hours narrowing to $500 from six on, because the half-to-full step is $2,000 and three hours of overtime is $1,500. Written out in `docs/07` §Recorded dissent with the table, which is the section built for this review to test against. The **trigger** for the overtime hour is a separate open question — §9 Q59 | 2026-08-26 | D14 is priced for where the practice is going, not where it is. And the anomaly above was assigned to this reminder twice in one change set and written into neither place the reminder lives, which is the failure §12 exists to prevent |
|
||||
| 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 |
|
||||
| ~~R9~~ | ✅ **CLOSED 2026-09-01. The `ses-alerts` email subscription is CONFIRMED** — `aws sns list-subscriptions-by-topic` returns a real subscription ARN rather than the literal `PendingConfirmation` `[verified 2026-09-01]`, so the two bounce/complaint alarms reach `info@smlcompany.ca`. ⚠️ **It had been confirmed for some unknown part of six days while §7, `docs/05`, `docs/06` and this row all said the alarms fired into nothing.** That is the Q22 staleness in the **safe** direction, and the direction is why it lasted: nothing was broken, so nothing prompted anyone to re-read it. The generalisable half, and it is worth more than the row: **a record whose staleness is harmless is the record that stays stale longest**, because every other kind announces itself by breaking something. Re-read the harmless ones on a schedule or they are never re-read at all. *Previous text follows.* **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. ✅ **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. ✅ **SWEPT AGAIN 2026-09-01, all 19 pins against `npm view`, and TWO MAJORS ARE DEFERRED BY RULING rather than left unstated:** `@astrojs/mdx` **^7.0.8 → 8.0.0** and `typescript` **^6.0.3 → 7.0.2**. Pouya's reasoning — *"npm audit is clean and majors mid-walkthrough add churn without user value"* — with `npm audit` at **0 vulnerabilities** `[verified 2026-09-01]`, which makes it a churn decision and not a security one, **and one that flips the moment that stops being true.** Both now sit on a new **Cutover prep** group at the head of `docs/06`'s cutover checklist, dated, because deferring a thing and forgetting it look identical three weeks later. Four more are a minor or patch behind and already satisfied by their carets, so they need no edit — `astro` 7.2.9 → 7.2.10, `@astrojs/sitemap` 3.7.3 → 3.7.4, `globals` 17.11.0 → 17.12.0, `typescript-eslint` 8.68.0 → 8.69.0; the other 13 are current. **This row is the deferral's reminder, not its replacement** — R11 fires at the next phase boundary regardless | 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 |
|
||||
@@ -940,6 +941,512 @@ never being raised again.
|
||||
|
||||
# Change Log
|
||||
|
||||
## 2026-09-01 (am) — The production run: Q61 fixed and closed, the intake backend and distribution given an executable runbook — and reading the running system found four launch blockers the specs could not see, one of them a false statement on the privacy policy
|
||||
|
||||
**Pouya's direction:** *"get to production as soon as possible; nothing new gets
|
||||
opened unless it blocks launch."* Five items: fix Q61 with a full sweep; produce
|
||||
the exact command sequence for the intake backend; fold Q60's TTL test into it;
|
||||
run the D20 cutover claims audit; plan the first deploy and **do not deploy**.
|
||||
Everything below either discharges one of those or is a launch blocker found
|
||||
while doing so. Commit `ca1c252` (the reviewed footer step) opened the session.
|
||||
|
||||
### 1. Q61 — ruled *fix now*, fixed, closed
|
||||
|
||||
Two declarations on `html` inside the existing `@media (min-width: 66rem)` block:
|
||||
|
||||
```css
|
||||
scroll-padding-top: calc(var(--header-h) + var(--space-4)); /* fallback: no `lh` */
|
||||
scroll-padding-top: max(calc(var(--header-h) + var(--space-4)), calc(10lh - 83px));
|
||||
```
|
||||
|
||||
**The result, and the control is the part that makes it a result.** The Shift+Tab
|
||||
walk over 6 pages x 5 widths >= 1056 px goes from **290 entirely-hidden focus
|
||||
stops of 1,455 to 0** under `minimumFontSize=32`. The pre-fix tree was rebuilt in
|
||||
a git worktree and put through the identical probe: it **still reports 290**
|
||||
(footer 254, `#main` 36), geometry candidates 333 against the fixed build's 43.
|
||||
So the probe is proven able to see the failure it now reports gone — which
|
||||
`CLAUDE.md`'s rule about uniform passes demands, because both modes reading 0 with
|
||||
identical step counts is exactly the shape that ends a check instead of starting
|
||||
one. A second instrument check confirmed `minimumFontSize` was actually being
|
||||
applied (`bodyFont` 32 px) rather than silently ignored.
|
||||
|
||||
**Default settings byte-identical, as ruled: 0 differences over 352 page-widths x
|
||||
17 fields = 5,984 comparisons**, including the full-page geometry fingerprint,
|
||||
with a positive control detecting exactly 1 injected difference.
|
||||
|
||||
⚠️ **The first attempt at that comparison reported 4,224 differences**, because it
|
||||
keyed rows on `page`/`width` where the harness names them `url`/`w` — every row
|
||||
collapsed onto one map entry and was compared against an arbitrary single row. It
|
||||
was caught because the result was *uniformly bad*, which is the survivable half of
|
||||
that bug; the rewritten version **asserts the key fields exist and are unique**
|
||||
before comparing, and that assertion is the fix rather than the corrected field
|
||||
names.
|
||||
|
||||
**The sweep was wider than the ruling asked for, and it found a residual.** 777
|
||||
cells over 37 settings — roots 9–32, all five `defaultFontSize` presets, six
|
||||
`minimumFontSize` values, fonts blocked, and fonts blocked under the setting —
|
||||
measuring the **actual skip-link landing** rather than a proxy:
|
||||
|
||||
| setting | before | after |
|
||||
|---|---|---|
|
||||
| root-style 9..32 (504 cells) | 0 | 0 |
|
||||
| `defaultFontSize` 9/12/16/20/24 (105) | 0 | 0 |
|
||||
| `minimumFontSize=9`, `=12` | 0 | 0 |
|
||||
| `minimumFontSize=16` | 6 (52 px) | **6 (52 px)** |
|
||||
| `minimumFontSize=20` | 12 (64 px) | **6 (14 px)** |
|
||||
| `minimumFontSize=24` | 15 (75 px) | **0** |
|
||||
| `minimumFontSize=32` | 15 (174 px) | **0** |
|
||||
| fonts blocked | 0 | 0 |
|
||||
| fonts blocked + `minFont=32` | 15 (174 px) | **0** |
|
||||
| **total** | **63 of 777** | **12 of 777** |
|
||||
|
||||
**Nothing is worse in any cell.** The 12 remaining are pre-existing, and **they
|
||||
were not fixed** — Pouya's instruction was *"if the sweep surfaces anything beyond
|
||||
this one declaration's blast radius, stop and report — do not widen."* The reason
|
||||
the `lh` ramp cannot see them is the interesting part: `minimumFontSize=16` leaves
|
||||
the root at 16, so `1lh` on `<html>` is 18 px exactly as at the default, while the
|
||||
header grows to 149.38 px because the **sub-16 px** type (11 px tagline, 14 px
|
||||
eyebrow and nav) is floored up. The ramp reads a quantity the setting did not
|
||||
move. `docs/06` carries it as its own item.
|
||||
|
||||
⚠️ **And one column of that sweep was measuring nothing.** A summary column
|
||||
reported `min(scroll-padding-top − headerH)` of **−71.89** for root-style while
|
||||
the same rows reported **0 px covered** — two metrics of one quantity
|
||||
disagreeing. The cause: `insetBlockStart` is **−900 px** in those rows. The header
|
||||
is `position: sticky` and *not pinned*, because that is how the saturating
|
||||
`clamp()` gate un-sticks it, so comparing the offset to the header height is
|
||||
meaningless there. The `covered` metric — real geometry after a real hash
|
||||
navigation — is the valid one. Recorded because the diagnostic was written before
|
||||
the conclusion, which is the only reason the contradiction resolved instead of
|
||||
being averaged away.
|
||||
|
||||
### 2. Four launch blockers, none of which a spec could have told us
|
||||
|
||||
Every one came from reading the running system. Two would have lost every
|
||||
submission; one breaks 22 of 23 pages; one is a false public statement.
|
||||
|
||||
**(a) The handler wrote the wrong DynamoDB key — every submission lost, silently.**
|
||||
`handler.mjs` built its item as `pk: INTAKE#<uuid>` / `sk: <timestamp>`, from
|
||||
`docs/05`'s item table. The live table's key schema is **`submissionId` (S), no
|
||||
sort key** `[verified — describe-table]`. A `PutItem` missing the key attribute
|
||||
fails with `ValidationException`; the handler catches it and returns
|
||||
`/contact/could-not-send/`. **So the form would have looked broken to every
|
||||
inquirer while the record went nowhere**, from the moment `/api/*` was wired. A
|
||||
key schema cannot be altered after creation, so the handler moved to the table.
|
||||
The alternative — a new table matching the spec — was declined: it would re-open
|
||||
the §7-verified TTL and the verified PITR on a fresh resource at cutover, to buy
|
||||
a sort key nothing queries.
|
||||
|
||||
**(b) The Lambda's invoke permission is scoped to the old route's path.** One
|
||||
statement, `apigateway.amazonaws.com`, `SourceArn`
|
||||
`…/4tl0m5igkj/*/*/submissions`. Add `POST /api/intake` without adding a
|
||||
permission and API Gateway is refused: **500 to the caller, and nothing in the
|
||||
Lambda log at all**, because the function is never entered. The hardest possible
|
||||
shape to diagnose, since CloudWatch is where anyone looks first.
|
||||
|
||||
**(c) 22 of the 23 pages do not serve, and there is no 404 page.** `astro.config`
|
||||
sets `trailingSlash: 'always'` with `build.format: 'directory'`, and its own
|
||||
comment says the URLs are *"matched by the CloudFront trailing-slash function"* —
|
||||
**which does not exist.** The distribution has no function associations, no custom
|
||||
error responses and no cache behaviours. Measured on the live distribution rather
|
||||
than inferred: `/` **200**, `/about/` and `/definitely-not-a-page/` both **403,
|
||||
`application/xml`, 111 bytes** — S3's `AccessDenied` served raw to the reader.
|
||||
Separately, `docs/04` has asked for a 404 page since before build step 1 and
|
||||
**there was no `src/pages/404.astro` at all** — 22 pages, and the checklist item
|
||||
"404 returns a 404 status" was unsatisfiable in both halves.
|
||||
|
||||
**(d) `/legal/privacy/` tells the public something false about who can read the
|
||||
intake table.** Opened as **Q62**, found by `claims-auditor`. The page says
|
||||
*"nobody else has access to the table… no external administrator"*; the account's
|
||||
`admins` group carries `AdministratorAccess` and has **two members**, and
|
||||
`simulate-principal-policy` returns **allowed** for
|
||||
`GetItem`/`Query`/`Scan` for both. Evidence committed as
|
||||
`docs/reference/intake-table-access-verification.md` with every command, because
|
||||
this was **the only claim on the site whose subject lived entirely outside the
|
||||
repository** — R14's "unverifiable by construction", on a privacy policy. Left
|
||||
in place with a `TODO(pouya)` rather than rewritten: it is one of two decisions
|
||||
(remove the access, or state the true number) and both are his, and the
|
||||
softened middle — *"authorised administrators"* — is the shape §4 exists to bar.
|
||||
|
||||
### 3. What shipped for items 2, 3 and 5
|
||||
|
||||
- **`docs/09-cutover-runbook.md`** — the copy-paste sequence, ten parts, each
|
||||
command followed by its verification and the output to expect, with a rollback
|
||||
per part and the order dependency spelled out (**distribution first, site sync
|
||||
last**; reversing them puts 22 of 23 pages behind a 403 for the length of a
|
||||
CloudFront deployment). Part 10 is Q60's TTL test.
|
||||
- **`infra/cloudfront/router.js`** + **`router.test.mjs`** — the viewer-request
|
||||
function, 15 local cases passing, proven able to fail. One of those cases exists
|
||||
purely as documentation: `/api/intake` **must not** be associated with it,
|
||||
because a 301 turns a POST into a GET and the submission body is gone.
|
||||
- **`infra/cloudfront/configure.mjs`** — the three distribution changes as one
|
||||
reviewable, **idempotent, dry-run-by-default** transaction that sends the
|
||||
`IfMatch` ETag it read. It resolves both managed policy ids **by name from the
|
||||
account**, never as literals: `CLAUDE.md`'s registry rule applies to an AWS
|
||||
identifier as much as to an npm pin, and a wrong cache-policy id would ship a
|
||||
cached POST endpoint. Dry-run against the live distribution reports exactly the
|
||||
4 expected changes and writes nothing.
|
||||
- **`scripts/intake-env.mjs`** — emits the six Lambda variables, reading
|
||||
`RESPONSE_TIME` and `NO_RETAINER_NOTICE` **from `src/data/site.ts`** so that two
|
||||
published commitments are never retyped into a shell command. It asserts the
|
||||
notice still contains its fourth clause and its en dash, and the response time
|
||||
still says two business days. **Both guards probed**: the fourth-clause guard
|
||||
exits **1** with the right message when the clause is removed, and the file was
|
||||
restored and the restoration verified with `git diff --exit-code`.
|
||||
- **Item 5's dry run, executed:** three `--dryrun` passes — **51 / 56 / 52
|
||||
lines**, the third containing the two expected `delete:` lines for the old
|
||||
site's logos. Largest file in `dist` is **171,876 bytes**, comfortably under the
|
||||
8 MB multipart threshold the deploy user's missing `AbortMultipartUpload`
|
||||
depends on. **Nothing was deployed.**
|
||||
- **`docs/06`** records the Gitea-runner deferral as Pouya ruled it, with the
|
||||
reason (co-admin dependency, non-blocking) and — stated rather than waved past
|
||||
— what it costs: a long-lived key in a person's environment and no merge gate.
|
||||
|
||||
⚠️ **The dry-run counts are a superset of the real run and the runbook says so.**
|
||||
34 of pass 2's 56 lines are files under `_astro/` that pass 1 uploads first; each
|
||||
`--dryrun` compares against the bucket as it is now, so it cannot see the
|
||||
preceding pass's uploads. In the real sequential run `sync` skips them, and that
|
||||
skipping is what preserves pass 1's `immutable` header. Without the note, the
|
||||
smaller real counts read as a failed upload.
|
||||
|
||||
### 4. Two of three instruments could not see the new page
|
||||
|
||||
`check:claims` takes any `.html`, so the 404 page's copy was inside the claim
|
||||
register from its first build. **`og:proof` and `lighthouse` both collected
|
||||
`index.html` only**, and `404.astro` is emitted as `dist/404.html`, outside
|
||||
`build.format: 'directory'`.
|
||||
|
||||
**`og:proof` reported it backwards** — as the card being orphaned, *"generated,
|
||||
but no built page references it"* — rather than as the page being unchecked. That
|
||||
is the more dangerous shape: it points at the wrong artefact. `lighthouse` would
|
||||
have asserted the performance budget over 22 of 23 pages while its own header
|
||||
claimed it enumerated the site, and said nothing at all. Both fixed; the two use
|
||||
different path forms for the same file (`/404/` is an `OG_CARDS` key, `/404.html`
|
||||
is a URL to serve) and each says why beside the code.
|
||||
|
||||
**One instrument of three saw the page, which is the argument for having three.**
|
||||
|
||||
### 5. The D20 cutover claims audit — FAIL, eight findings
|
||||
|
||||
The one full claims pass this project gets, over all 23 pages in visitor order.
|
||||
**It 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 class a per-step run on one page
|
||||
is least able to see and that `check:claims` cannot grep for at all. Five
|
||||
corrected here, three open.
|
||||
|
||||
| # | the over-reach | corrected to |
|
||||
|---|---|---|
|
||||
| 3 | `/practice/energy/`: *"rewriting how large loads **and new generation** get connected"* — the extract's only quotation on generation says the normal System Impact Assessment applies to all generation *"equally"* | *"rewriting how large loads get connected"* |
|
||||
| 4 | `/practice/insurance/`: the case-conference adjudicator *"is then **disqualified** from hearing the matter"* — LAT Rule 14.3 says *"except with the consent of the parties"* | *"does not then sit on the hearing panel unless the parties consent"* |
|
||||
| 5 | `/practice/insurance/`: *"The Tribunal's **own materials point** parties toward mediation"* — a plural class and a direction, from one permissive sentence pairing mediation with negotiation | *"The Tribunal's accident-benefits page names mediation as something to consider before applying"* |
|
||||
| 2 | `/practice/technology/`: *"**no federal or Ontario statute** requires data to be stored in Canada"* — a universal over the **four** instruments the extract checked; and the section note then disclaimed it as the Commissioner's words, which is true of the PIPEDA half only | named instruments, and a note that says which half is quotation and which is a reading |
|
||||
| 6 | `/med-arb/`: the ADRIC Med-Arb Rules *"presented to the membership at ADRIC's 2019 annual conference"* — the source says **a discussion draft** was | *"presented to the membership as a discussion draft"* |
|
||||
|
||||
**Still open, and each has an item on `docs/06`'s checklist:** finding 1 (the
|
||||
`/med-arb/` gloss — the page's own voice says med-arb *"is the two of them run
|
||||
under one agreement"* one section below publishing ADRIC's *"not merely the
|
||||
merging of separate mediation and arbitration processes"*: **the gloss asserts
|
||||
what its cited authority denies, on the same page**, and it needs a ruling on
|
||||
which of the two goes); finding 7 (**Q60**); finding 8 (**Q62**).
|
||||
|
||||
Note on finding 1: the 2026-08-30 fix took the competence proposition out of the
|
||||
**heading** and left it in the **body**. Resolving it by adding a competence claim
|
||||
is barred — ADRIC answers its own competence sentence with the Chartered Med-Arb
|
||||
designation and the Med-Arb Foundational Course, neither held, and C.Med-Arb is
|
||||
struck from this site entirely.
|
||||
|
||||
⚠️ **And the auditor found a stale instruction inside its own brief — the fifth.**
|
||||
`.claude/agents/claims-auditor.md` still said *"**Q.Arb.** Commenced August 2026.
|
||||
Flag anything reading as held, imminent, or nearly complete. The Arbitration page
|
||||
must state plainly what is available now versus what follows designation."* §4
|
||||
recorded Q.Arb as **HELD** on 2026-08-29 and struck every stage form. **Applied
|
||||
literally it would have flagged correct copy and demanded the struck form**, and
|
||||
an imperative about what a page "must state plainly" is the kind an agent obeys.
|
||||
It read §4 instead, which is luck rather than process — the same conclusion
|
||||
`CLAUDE.md` already draws about that file. Rewritten to hold **the question and
|
||||
not the answer**, generalising the rule the memberships paragraph had already
|
||||
learned: any state that can change is read from §4 at audit time. Five instances
|
||||
of one shape in one file is no longer a coincidence.
|
||||
|
||||
### 6. Also corrected, and neither is a claim
|
||||
|
||||
**The DLQ is struck from `docs/05` and `docs/06`.** Both asked for a dead-letter
|
||||
queue on `adr-intake-handler` with an alarm on depth >= 1. **`DeadLetterConfig`
|
||||
applies only to asynchronous invocations**, and API Gateway invokes
|
||||
synchronously — so the queue would have sat at depth 0 for ever and the alarm
|
||||
would have been a permanently green light. That is this project's most expensive
|
||||
recurring shape, after Q22 and the Lighthouse row. Replaced by two alarms that can
|
||||
actually fire: **Lambda `Errors`** (a synchronous failure, which a DLQ cannot see)
|
||||
and **API Gateway `5xx`** (the case where the function is never entered — which is
|
||||
blocker (b) above).
|
||||
|
||||
**`docs/05`'s per-IP rate limit is struck.** It asked for "5 requests / 5 minutes
|
||||
per source IP **at API Gateway**". API Gateway throttling is **aggregate** — per
|
||||
route and per stage, across all callers. Per-IP needs AWS WAF, which is paid, so
|
||||
it is a decision on `docs/06`'s list and explicitly **not** a launch blocker. An
|
||||
aggregate route throttle ships instead and must never be described as per-IP.
|
||||
|
||||
**`docs/07` line 366** called Q59 *"open"* while line 144 of the same file records
|
||||
it ruled and closed. Non-blocking; corrected because §Recorded dissent is what
|
||||
R5's 12-month review reads.
|
||||
|
||||
### 7. Verified, and the stamps that moved
|
||||
|
||||
Read-only against AWS, `[verified 2026-09-01]`, exit status read on every call and
|
||||
stderr suppressed nowhere:
|
||||
|
||||
- **R9 CLOSES.** The `ses-alerts` email subscription is **confirmed** — a real
|
||||
subscription ARN, not `PendingConfirmation`. §7, `docs/05`, `docs/06` and R9
|
||||
itself had all said the alarms fired into nothing. It had been confirmed for
|
||||
some part of six days. **That is the Q22 staleness in the safe direction, and
|
||||
the direction is why it lasted**: nothing broke, so nothing prompted a re-read.
|
||||
The general form is worth more than the row — **a record whose staleness is
|
||||
harmless is the record that stays stale longest**, because every other kind
|
||||
announces itself by breaking something.
|
||||
- **PITR is `ENABLED`**, 35-day window. **No customer-managed KMS key** — the
|
||||
table uses the AWS-owned key, which `/legal/privacy/` does not claim, so nothing
|
||||
published depends on it.
|
||||
- **4 items predate this repo** and the hand-built handler writes **no `ttl`**, so
|
||||
they never expire while the policy says 24 months. Pouya's call, because whether
|
||||
they are the guide's smoke test or real inquiries is not determinable from here.
|
||||
- SES domain verified, DKIM `SUCCESS` and signing enabled, no custom MAIL FROM.
|
||||
S3 versioning `Enabled`, all four public-access blocks `true`, direct S3 object
|
||||
GET **403**. Managed policy ids for `CachingDisabled` and
|
||||
`AllViewerExceptHostHeader` confirmed by name against the account rather than
|
||||
recalled — `CachingOptimized` matching the live distribution was the check that
|
||||
the other two could be trusted.
|
||||
|
||||
**Gates, all run on the shipping bytes with exit status read:** `check` 0,
|
||||
`build` 0, `check:claims` 0, `og:proof` 0 (**23 pages**, 22 cards, 21 headlines
|
||||
matched), `check:intake` 0, `lint` 0, `router.test.mjs` 0 (15/15).
|
||||
**`npm run lighthouse` exit 0 — 23 pages, worst of 23 is 99 / 100 / 100 / 100**,
|
||||
CLS 0.000 everywhere. ⚠️ **One reported number sits at its budget: LCP on `/` is
|
||||
2.03 s against `docs/04`'s < 2.0 s.** The script reports LCP without asserting it,
|
||||
deliberately, because simulated throttling on a loopback server is not a Slow 4G
|
||||
field measurement — so it is not a failing gate and must not be called a passing
|
||||
one either.
|
||||
|
||||
### Adversarial review — two rounds, 24 findings, and round 2's first one was a defect in round 1's own fix
|
||||
|
||||
`adversarial-reviewer` only. **`claims-auditor` ran once, at cutover, per D20** —
|
||||
its result is §5 above.
|
||||
|
||||
**Round 1: 13 findings and 2 considers. Two blocking.**
|
||||
|
||||
1. 🛑 **A PROTOCOL-RELATIVE OPEN REDIRECT IN `router.js`, ON EVERY VIEWER
|
||||
REQUEST.** The 301 `Location` was built from `request.uri` unnormalised, so
|
||||
`//evil.example.com/x` produced `Location: //evil.example.com/x/` — a
|
||||
network-path reference (RFC 3986 §4.2) where the browser keeps the scheme and
|
||||
**replaces the authority**, on a link that reads as `adr.smlcompany.ca…` in a
|
||||
mail client. The reviewer settled the one open question by measurement:
|
||||
**CloudFront resolves dot-segments and does NOT collapse duplicate slashes**
|
||||
(`GET //index.html` → 403 with S3 receiving the key `/index.html`, while
|
||||
`/./index.html` and `/a/../index.html` both → 200). A backslash does the same
|
||||
through the URL Standard, which is why a `startsWith('//')` guard is not
|
||||
enough. **The 15-case test had no `//` case.**
|
||||
2. 🛑 **THE FALSE PRIVACY SENTENCE SHIPPED IN `dist/` WITH NOTHING STOPPING IT.**
|
||||
My resolution had been a `TODO(pouya)` plus three prose checklist items, on the
|
||||
reasoning that the existing machinery gates it. **It does not: Astro strips a
|
||||
JSX comment, so `grep -rn "TODO(pouya)" dist/` returns nothing**, and `check`,
|
||||
`build` and `check:claims` all exited 0 over the sentence. `npm run deploy`
|
||||
would have published it. The gate was human memory, which is the thing
|
||||
`CLAUDE.md` names: *"a build that fails on an unanswered question is a correct
|
||||
build."*
|
||||
|
||||
Also: the `?list-type=2` verification I wrote **could not fail** (the default root
|
||||
object means no viewer path is ever a bucket-root request, so the check was
|
||||
evidence of nothing — Q22's shape, in a runbook written to avoid it); the 404
|
||||
mapping is distribution-wide so it also covers `/api/*`, making three diagnostics
|
||||
wrong about what a 404 means; `sourceIp` records the CloudFront edge rather than
|
||||
the inquirer while `/legal/privacy/` says *"your IP address"*; three struck
|
||||
controls survived as live instructions in `handler.mjs` and in the `docs/06`
|
||||
bullet this change set had just edited; `docs/06` still described an intake probe
|
||||
neither script runs; `/practice/technology/`'s corrected lead was **still** a
|
||||
universal, over MFIPPA, which the extract never checked; `lat-case-conference.md`'s
|
||||
own commentary still asserted the absolute the page had just been corrected away
|
||||
from; `/404/`'s route list broke the 44 × 44 floor on one axis **while the comment
|
||||
beside it claimed compliance**; `404.astro`'s header said no credential claim
|
||||
appears on the page, which the JSON-LD contradicts; `docs/01` still said 22 pages
|
||||
and carried a reassurance this change set disproved; and **`_archive/` was not
|
||||
gitignored** while Part 8.1 writes 3.3 MB of the old fabricated-credentials site
|
||||
into the working tree eleven lines before a `git tag`.
|
||||
|
||||
**One finding declined.** That `/med-arb/`'s *"developed by a task force"* is
|
||||
unsourced. It is sourced: `docs/reference/adric-extract/rules-codes__adric-med-arb-rules.txt:166`
|
||||
reads *"a **Task Force** was formed with a dedicated working group of med-arb
|
||||
professionals. The Task Force completed an initial draft of the Rules."* The
|
||||
round-1 sweep was `git grep -n "task force"` — **case-sensitive**, against a
|
||||
source that capitalises it, so it returned one hit (the page) instead of two.
|
||||
`CLAUDE.md`'s rule that a grep is not a finding until you read what it matched,
|
||||
from the other side: **a grep that does not match is not a finding either.** Round
|
||||
2 read the file and agreed. The useful half was real, though — the *curated*
|
||||
`adric-rules.md` did not carry the sentence, so a reader consulting the curated
|
||||
extract alone would have drawn the same conclusion. It now quotes it.
|
||||
|
||||
**Round 2: 11 findings. One blocking, and it was mine.**
|
||||
|
||||
🛑 **`viewerIp()` — ROUND 1'S FIX WAS WORSE THAN WHAT IT REPAIRED.** It replaced
|
||||
`requestContext.http.sourceIp`, which a client cannot forge, with the **leftmost**
|
||||
entry of `x-forwarded-for`, which is whatever the client sent: **CloudFront
|
||||
appends** the viewer address to a client-supplied XFF rather than replacing it, so
|
||||
a submission with `X-Forwarded-For: 8.8.8.8` stored `8.8.8.8`. A field held for
|
||||
abuse investigation became one that **can be made to name an uninvolved third
|
||||
party**, on a page that promises it holds *"your IP address"*.
|
||||
|
||||
And the 14-line comment I wrote beside it contained its own refutation —
|
||||
*"CloudFront appends, so the leftmost is the viewer"* — appending puts the viewer
|
||||
**last**. **The comment was not a description of the code; it was an argument
|
||||
against it.** Reverted to the unforgeable value, with the limitation stated: it
|
||||
records the CDN and therefore cannot serve abuse investigation. The usable value
|
||||
is `CloudFront-Viewer-Address`, which needs a **custom** origin request policy
|
||||
(the managed `AllViewerAndCloudFrontHeaders` forwards `Host` and would 403 every
|
||||
request at API Gateway, which is why `AllViewerExceptHostHeader` was chosen) —
|
||||
deliberately not taken inside a capped review loop. **`docs/09` Part 7.2 now
|
||||
measures what the field actually contains, with a decision table for each of the
|
||||
three outcomes**, because the proxy chain has now been reasoned about wrongly
|
||||
twice and once is the limit.
|
||||
|
||||
**Round 2's other findings, and four more were defects in round 1's fixes:**
|
||||
|
||||
- **The open-redirect fix normalised as a *rewrite*, which closed the redirect and
|
||||
opened duplicate content.** `//about/`, `///about/` and `/\about/` each served
|
||||
the About page with a **200** — an unbounded family of duplicate URLs, against
|
||||
the same `docs/04` concern the 301 branch exists for. And the file branch
|
||||
returned `request` untouched, so `//robots.txt` reached S3 unnormalised and
|
||||
404'd. Now: normalise, and **redirect if anything changed**, on both branches.
|
||||
- **`safe()` stripped characters browsers legitimately send and missed the one
|
||||
that matters.** `| ^ ` { }` are not in WHATWG's query percent-encode set, so
|
||||
they go on the wire raw — and `|` is routine in ad-platform tracking values, so
|
||||
the 301 that exists *"so a campaign link survives"* silently corrupted exactly
|
||||
those, into a plausible-looking value rather than an error. Meanwhile `#` — the
|
||||
only character that changes the **structure** of the emitted header, dropping
|
||||
`&b=y` into a fragment — was not stripped. Set narrowed to C0 controls, DEL,
|
||||
space, `"`, `#`, `<`, `>`.
|
||||
- **The runtime test exercised none of the new code.** `docs/09` Part 2's
|
||||
`test-function` loop passed `"querystring":{}` on all six cases and tried
|
||||
neither redirect path — so in the only environment that runs the real
|
||||
`cloudfront-js-2.0` runtime, *"the authoritative check"* checked none of it.
|
||||
Four normalisation URIs and a query-string call added.
|
||||
- **The `docs/09` fix for the worthless `?list-type=2` check did not reach
|
||||
`docs/06`**, which went on prescribing it as *"the check that matters"* — and
|
||||
gave as its proof the very fact `docs/09` uses to show it cannot fail. **Two
|
||||
documents, one fix, and the discarded check survived in the one an operator
|
||||
ticks.**
|
||||
- **`.gitea/workflows/deploy.yml`'s new diagnostic printed a command that cannot
|
||||
run** — `aws apigatewayv2 get-routes --query 'Items[].RouteKey'` exits **252**
|
||||
on `ParamValidation` without `--api-id`. The sibling in `deploy-local.sh` has
|
||||
it. The two are described in their own headers as one artefact in two places and
|
||||
they diverged on the single command the diagnostic exists to supply — in the
|
||||
copy that runs in CI, where the reader has a log rather than a script.
|
||||
- Three stale `Expect` values in `docs/09`: `router: 15 of 15` (now 30; the count
|
||||
is no longer quoted, since it has moved three times in a day and an `Expect`
|
||||
line that never matches teaches the operator to stop reading them),
|
||||
and `CodeSize` *"around 23,000"* against a measured **10,819** — that figure was
|
||||
5.1's *uncompressed* number applied to the zip, and an operator seeing `10819`
|
||||
would reasonably conclude the wrong artefact went up.
|
||||
- **`docs/05`'s 3-second timestamp check was still an unstruck imperative**, in
|
||||
the same list whose rate-limit sibling had just been struck with nine lines of
|
||||
explanation, and recorded as unimplementable in three other places. Struck.
|
||||
- A permanent lint warning from a redundant `eslint-disable` whose stated reason
|
||||
was itself an argument for not needing it. Deleted.
|
||||
- **Comment bulk, and this one is D19's predicted cost realised rather than a
|
||||
style note:** `router.js` was **61%** comment and `handler.mjs` **49%**, and
|
||||
*the two longest comments in the change set were the two carrying a wrong
|
||||
constraint* — the 15-line normalisation block asserting a doubled slash resolves
|
||||
to the 404 page, and the 14-line `sourceIp` block with the XFF direction
|
||||
backwards. **An implementer who trusted either would have shipped both
|
||||
defects.** Trimmed to the live constraint; `router.js` is now 42%.
|
||||
- Two `consider`s taken: `configure.mjs` reported a *replaced* 404 mapping as an
|
||||
addition, and `tokens.css` told the next reader to re-measure `--header-h`
|
||||
without saying that `global.css`'s fitted `83px` moves with it.
|
||||
|
||||
**Round 2 also removed the `2>/dev/null || true` from both deploy paths' second
|
||||
`curl`** — the construct `CLAUDE.md` bans by name, in a verification script, which
|
||||
the reviewer noted rather than raised because the first `curl`'s status is read.
|
||||
Two characters, in files already open.
|
||||
|
||||
**STOPPING AT TWO ROUNDS — D19, and the reasoning is stated so a later reader does
|
||||
not read the cap as laxness.** Round 2 produced 11 findings **of which five were
|
||||
defects in round 1's own fixes**, including the blocking one. That ratio is the
|
||||
measurement the cap is built on: past two rounds, each further round manufactures
|
||||
defects at roughly the rate it finds them. What round 2 found is fixed; the round-3
|
||||
findings that would exist are not being sought.
|
||||
|
||||
⚠️ **AND THE MOST INSTRUCTIVE DEFECT OF THE SESSION WAS IN A FIX FOR A BLOCKING
|
||||
FINDING, CAUGHT BY MY OWN VERIFICATION RATHER THAN BY A REVIEWER.** The
|
||||
`check:claims` pattern added to close round 1's finding 2 used **literal spaces**.
|
||||
It ran, printed `ok sole-administrator-q62`, and exited 0 — with the breach
|
||||
sitting in `dist/legal/privacy/index.html` two directories away. `compressHTML`
|
||||
collapses whitespace **between** tags and leaves it alone **inside** a text node,
|
||||
so the published bytes read `nobody else has access to\n the table`. **A
|
||||
tripwire added to close a paper-control finding, which was itself a paper
|
||||
control.** Found by grepping the built page instead of believing the green line;
|
||||
the regex is `\s+` now and fires with 2 matches, exit 1.
|
||||
|
||||
### Gates on the final bytes
|
||||
|
||||
Every one run with its exit status read, no pipes over a verdict:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `npm run check` | **0** — 0 errors, 0 warnings, 0 hints |
|
||||
| `npm run build` | **0** — 23 pages |
|
||||
| `npm run check:claims` | **1**, and that is correct: the single `sole-administrator-q62` pattern, 2 matches in `dist/legal/privacy/index.html`. **Both deploy paths run it, so a deploy is blocked by a mechanism until Q62 is ruled** |
|
||||
| `npm run og:proof` | **0** — 23 pages, 22 cards, 21 headlines matched |
|
||||
| `npm run check:intake` | **0** |
|
||||
| `npm run lint` | **0**, and now with **no warnings** |
|
||||
| `npm run lighthouse` | **0** — 23 pages, worst of 23 **99 / 100 / 100 / 100**, CLS 0.000 everywhere, `/` LCP 2.03 s reported and not asserted |
|
||||
| `router.test.mjs` | **0** — **30 of 30**, and **8 of the 30 fail against the round-1 router**, which is what makes the pass a result |
|
||||
| `intake-env.mjs` | **0** — six variables, en dash and fourth clause intact |
|
||||
| `configure.mjs` dry run | **0** — 3 changes against the live distribution, nothing sent |
|
||||
| Q61, final bytes | **0 hidden focus stops of 1,455** under `minimumFontSize=32`; `scroll-padding-top` 97 px / 287 px / 287 px with fonts blocked |
|
||||
| minifier | the `@media (min-width: 66rem)` block in `dist` carries **exactly two** `scroll-padding-top` declarations **with the plain fallback first** — asserted, because an engine without `lh` drops the `max()` and the order is the whole safety |
|
||||
| `grep -rE 'animation:[^;}]*(scroll\(\)|view\(\))' dist` | **exit 1, no output** |
|
||||
| JS in `dist` | **0 files** |
|
||||
|
||||
⚠️ **Three of those checks were run once from the wrong working directory and
|
||||
reported `exit=2` and `0 files` — "no such directory", not "clean".** A compound
|
||||
command's `cd` had persisted. Re-run from the repository root, which is where the
|
||||
table above comes from. *A command that did not run is not evidence of absence*,
|
||||
committed and caught in the same session it is written down in.
|
||||
|
||||
### The sweeps, with their commands and output
|
||||
|
||||
Written after reading the output, not before.
|
||||
|
||||
**The five copy corrections reached `dist/` and the five originals are gone.**
|
||||
`grep -rl "<string>" dist --include='*.html' | wc -l` on each, ten runs:
|
||||
|
||||
```
|
||||
corrected "does not then sit on the hearing panel unless the parties consent" 1
|
||||
corrected "accident-benefits page names mediation as something to consider" 1
|
||||
corrected "rewriting how large loads get connected" 1
|
||||
corrected "presented to the membership as a" 1
|
||||
corrected "neither PIPEDA nor Ontario" 1
|
||||
original "who is then disqualified from hearing the matter" 0
|
||||
original "own materials point parties toward mediation" 0
|
||||
original "large loads and new" 0
|
||||
original "no federal or Ontario statute requires data" 0
|
||||
original ", developed by a task force and presented to the membership at" 0
|
||||
```
|
||||
|
||||
**The old item shape is gone from the source and the spec.**
|
||||
`grep -rn "pk: { S:\|'INTAKE#\|\`INTAKE#" backend/ src/` -> **exit 1, no output**.
|
||||
`grep -c '| \`pk\` |' docs/05-backend-spec.md` -> **0**.
|
||||
|
||||
**Q62 is cross-referenced in five files** — `AGENTS.md`, `docs/05`, `docs/06`,
|
||||
`docs/reference/intake-table-access-verification.md`,
|
||||
`src/pages/legal/privacy.astro`.
|
||||
|
||||
⚠️ **And `git grep -l 'Q62'` returned four of the five.** It cannot see untracked
|
||||
files, and the reference extract was untracked at that moment — `CLAUDE.md` names
|
||||
exactly this hole, and it appeared on the first sweep of this change set. `grep
|
||||
-rln` found all five. **A clean `git grep` across new work means nothing until the
|
||||
files are staged.**
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-01 (al) — The last recorded overflow is closed by one declaration, and half its cause had been misdiagnosed; the skip-link residual is accepted rather than fixed; R20 becomes a hard gate on publishing article #2; two majors deferred with a date
|
||||
|
||||
**The header tree of entry (ak) is committed as `fce89d4`** — eight files, 737
|
||||
|
||||
Reference in New Issue
Block a user