Build and deploy / build-and-deploy (push) Failing after 4s
Pouya's rulings of 2026-09-03, in five parts. 1. THE INTAKE FORM IS NOT BROKEN. (ar) was wrong. docs/09 §7.1 verbatim — POST /api/intake with an Origin header — returns 303 to /contact/could-not-send/ with access-control-allow-origin echoed; the same probe without Origin returns 403. A bare POST 403s BY DESIGN and §7.1 says so three lines below the probe it prescribes: "403 means the Origin header did not arrive". The earlier finding read a status code without reading the document that defines it. Second time in two days. CLAUDE.md's instrument list goes eight to nine. D20 findings 12 and 19 fall with it; §7.2 (that both emails arrive) is still owed. The correction is APPENDED as entry (as); (ar) stands unedited. 2. The privacy retention comment was stale, not a defect — superseded by his decision to publish and confirm after launch, reading from 2026-09-04. Reworded; the TODO(pouya) came off with the gate it enforced. The mechanism finding survives: it was a JSX comment, stripped by Astro, so no build or deploy path could see it. A publication gate that lives only in a stripped comment is not a gate. §9 Q60 corrected. 3. The gloss class is fixed — 15 of the 20 D20 findings, 14 distinct edits across 9 files, under the rule "the gloss may say no more than the extract says; no new claims, no new sources". Swept three unpublished insights drafts too, and corrected the wrong CAA attribution at its source in docs/reference/, which is where a fixed page re-seeds. /bio/ changed, so the committed PDF is regenerated (89,549 B, 1 page asserted). Three findings outstanding: 10 needs a ruling, 11 is ruled and owed via Q60, 13 needs him to have said it. R1 is not one of the twenty. 4. X-Robots-Tag cannot be done with S3 object metadata — --metadata writes user metadata, returned as x-amz-meta-x-robots-tag, which no crawler reads. Built as the CloudFront response-headers policy docs/06 has specified all along: configure.mjs section 4. It needs a --apply run, not a deploy. The policy is cloned from whatever is attached at run time and reconciled on every run, because a response-headers policy replaces rather than merges. 5. Headshot deferred as an open non-defect. The master and the srcset ladder are both fine; Astro passes no quality, so AVIF encodes at sharp's default 50 and is served first. Two review rounds, 29 findings, all resolved, none declined; stopped at two per D19. NINE of round 2's fourteen were defects in round 1's own repairs — including a fix that harmonised both /fees/ rows onto wording that was itself unregistered, publishing an unsourced fee term twice where it had been once. Gates, exit status read for each: check 0 (0 errors, 0 warnings, 0 hints), build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, minifier grep exit 1, router.test.mjs 30/30. Lighthouse NOT run. Nothing deployed and nothing applied to the distribution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
1436 lines
98 KiB
Markdown
1436 lines
98 KiB
Markdown
# 06 — Deployment and cutover
|
||
|
||
Authority: `AGENTS.md` §3 **D3 as amended 2026-08-26** (git + **Gitea Actions**
|
||
→ existing S3/CloudFront) and D11 (build everything, one clean cutover).
|
||
Existing infrastructure: **`AGENTS.md` §7 is authoritative.**
|
||
`docs/reference/AWS-Hosting-Guide.md` records how that infrastructure was
|
||
originally built — it is a historical record carrying a do-not-execute banner,
|
||
not a procedure, and §7 wins wherever the two disagree (Q24).
|
||
|
||
---
|
||
|
||
## Topology
|
||
|
||
```
|
||
Gitea push to main
|
||
└─ Gitea Actions (act_runner)
|
||
├─ npm ci && npm run build → ./dist
|
||
├─ static scoped IAM user key (from Gitea secrets — NOT OIDC)
|
||
├─ aws s3 sync ./dist s3://<bucket> (three passes, see Cache policy)
|
||
└─ cloudfront create-invalidation
|
||
Namecheap DNS → CloudFront → S3 (OAC)
|
||
API Gateway → Lambda → DynamoDB / SES (intake, unchanged path)
|
||
```
|
||
|
||
DNS is at **Namecheap, not Route 53** `[verified 2026-08-25]`. Nothing in the
|
||
pipeline touches DNS. Certificate renewal is ACM-automatic as long as the
|
||
validation CNAME stays in place at Namecheap — **do not delete it.**
|
||
|
||
## Today, deploys run locally
|
||
|
||
**`npm run deploy`** (`scripts/deploy-local.sh`) is the current path. It runs
|
||
the same guard, the same three sync passes in the same order with the same
|
||
cache headers, and the same invalidation as the workflow — at this scale the
|
||
pipeline changes only **how a deploy is triggered**, not what it does. Treat the
|
||
script and the workflow as one artefact in two places: change one, change both.
|
||
|
||
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
|
||
> below"* until 2026-08-28, and an operator following it would have created a
|
||
> **second** IAM user, or hand-provisioned one with a different scope from the
|
||
> `adr-sml-deploy-minimal` policy §7 now records. §7 carries the inventory and
|
||
> the eight `simulate-principal-policy` results.
|
||
>
|
||
> *Found by `adversarial-reviewer`: the Q22 flip to PROVISIONED was swept in
|
||
> `AGENTS.md` and nowhere else, and the Change Log entry's sweep block covered
|
||
> the copy corrections only. Same shape as the SES-DKIM inversion — the stale
|
||
> copy instructed an action against a High-risk credential.*
|
||
>
|
||
> *Create the user* below is retained as the **record of how it was
|
||
> provisioned**, not as an instruction. Read it that way.
|
||
|
||
The script **refuses to run as `user/pouya`** — the broadly-permissioned
|
||
personal user that has been authenticating to this account. See §10.
|
||
|
||
## CI runs on Gitea, not GitHub
|
||
|
||
`AGENTS.md` D3 as amended, 2026-08-26: self-hosted **Gitea**. The instance,
|
||
version, and repository are recorded in §7 — the version is comfortably above
|
||
the floor for the `vars` context, so the first-step guard is belt-and-braces
|
||
rather than load-bearing.
|
||
|
||
**The live pipeline is `.gitea/workflows/deploy.yml`.** Gitea Actions speaks
|
||
GitHub Actions syntax, so it is a near-direct port — the build steps, the
|
||
three-pass sync, and the cache headers are unchanged. The GitHub Actions original,
|
||
with its OIDC role assumption, stays in the repo as
|
||
`docs/reference/github-actions-oidc.yml.example` — deliberately outside
|
||
`.github/workflows/`, because Gitea falls back to that directory when
|
||
`.gitea/workflows` is absent.
|
||
|
||
### The one real difference: no OIDC
|
||
|
||
Gitea is not an AWS OIDC provider. There is no role to assume, so deploys
|
||
authenticate with a **scoped IAM user** whose access key lives only in the
|
||
repository's Gitea secrets.
|
||
|
||
This is a genuine step down in security from an OIDC setup — which was designed
|
||
here but never built — and it should be treated as one. The mitigations are the
|
||
policy scope and the rotation schedule.
|
||
|
||
**Create the user:**
|
||
|
||
1. IAM → Users → `adr-sml-deploy`. **Programmatic access only** — no console
|
||
password, no MFA device, no group membership.
|
||
2. Attach this inline policy and nothing else. Substitute the real bucket name,
|
||
account ID, and distribution ID from `scripts/aws-discover.sh`:
|
||
|
||
```json
|
||
{
|
||
"Version": "2012-10-17",
|
||
"Statement": [
|
||
{
|
||
"Sid": "ListSiteBucket",
|
||
"Effect": "Allow",
|
||
"Action": "s3:ListBucket",
|
||
"Resource": "arn:aws:s3:::BUCKET_NAME"
|
||
},
|
||
{
|
||
"Sid": "WriteSiteObjects",
|
||
"Effect": "Allow",
|
||
"Action": ["s3:PutObject", "s3:DeleteObject"],
|
||
"Resource": "arn:aws:s3:::BUCKET_NAME/*"
|
||
},
|
||
{
|
||
"Sid": "InvalidateOneDistribution",
|
||
"Effect": "Allow",
|
||
"Action": "cloudfront:CreateInvalidation",
|
||
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Four actions on one bucket and one distribution. No `Action: "*"`, no
|
||
`Resource: "*"` — the only wildcard is `BUCKET_NAME/*`, which scopes to the
|
||
objects of that one bucket. `s3:PutObjectAcl` was dropped on 2026-08-26:
|
||
`aws s3 sync` does not use it without `--acl`, and it is inert under Origin
|
||
Access Control with ACLs disabled. If a deploy step needs a permission this
|
||
policy lacks, the correct response is to question the step, not to widen the
|
||
policy.
|
||
|
||
3. Create an access key. **Copy it once** — AWS will not show the secret again.
|
||
|
||
**`s3:AbortMultipartUpload` is deliberately absent, and here is the actual
|
||
reason.** `aws s3 sync` switches to multipart above its 8 MB
|
||
`multipart_threshold`; an interrupted multipart upload then cannot clean up its
|
||
own parts, and orphaned parts accrue storage charges that do not appear in the
|
||
bucket listing. What makes that safe today is simply that **nothing here comes
|
||
close to 8 MB** — the largest file the pipeline
|
||
uploads is well under it. The biggest source asset is
|
||
`src/assets/pouya-lajevardi.jpg` at 357,627 bytes `[verified 2026-08-26 — stat]`,
|
||
Astro emits it smaller still after AVIF/WebP conversion, and the self-hosted font
|
||
files are smaller again. **Re-measure `./dist` after the first successful build**
|
||
— that, not the repository, is what gets synced. No lifecycle rule exists; do not describe one
|
||
as the mitigation, because it is not there.
|
||
|
||
**Revisit if any single asset approaches 8 MB** — a video, a large PDF, an
|
||
un-optimised photograph. At that point either add an S3 lifecycle rule aborting
|
||
incomplete multipart uploads after 7 days (preferred — it costs no IAM
|
||
permission), or grant `s3:AbortMultipartUpload` on `BUCKET_NAME/*`.
|
||
|
||
### Gitea configuration
|
||
|
||
**Repository → Settings → Actions → Secrets:**
|
||
|
||
| Name | Value |
|
||
|---|---|
|
||
| `AWS_ACCESS_KEY_ID` | from the IAM user |
|
||
| `AWS_SECRET_ACCESS_KEY` | from the IAM user |
|
||
|
||
**Repository → Settings → Actions → Variables** — not secrets. These are not
|
||
sensitive, and keeping them as variables means they appear in run logs where
|
||
they are useful for debugging.
|
||
|
||
| Variable | Value |
|
||
|---|---|
|
||
| `AWS_REGION` | `AGENTS.md` §7 — Region |
|
||
| `S3_BUCKET` | §7 — S3 bucket |
|
||
| `CLOUDFRONT_DISTRIBUTION_ID` | §7 — CloudFront |
|
||
|
||
⚠️ **`INTAKE_ENDPOINT` AND `BOOKING_URL` WERE ROWS HERE AND ARE GONE, 2026-08-31.**
|
||
Build step 8 moved the intake form to the same-origin path `/api/intake`, after
|
||
which nothing in the build read either one — and both deploy paths were still
|
||
refusing to run without `INTAKE_ENDPOINT`. **The removal was made in the two
|
||
scripts and not here**, so this table went on telling an operator to set a
|
||
variable no guard checked and no build consumed. Found by
|
||
`adversarial-reviewer` round 2, whose sharper point is about the evidence: the
|
||
removal was justified with `git grep PUBLIC_INTAKE_ENDPOINT -- src/` — a
|
||
**path-scoped** grep offered as proof of a repo-wide removal, and `git grep`
|
||
additionally cannot see untracked files, which at that moment was most of the new
|
||
work. That is `CLAUDE.md`'s *a sweep is a command, not a claim* and *sweep the
|
||
vocabulary, not only the subject*, both at once.
|
||
|
||
⚠️ **AND ONE PLACE STILL CARRIES IT: `.env.example` sets
|
||
`PUBLIC_INTAKE_ENDPOINT` to the full execute-api hostname, and
|
||
`PUBLIC_BOOKING_URL`.** That falsifies a live claim in `src/data/intake.ts` —
|
||
which said the endpoint id *"is not restated in the repo either"* — and the claim
|
||
has been corrected there rather than left standing. **The file itself was not
|
||
edited: this environment denies read access to it, and nothing in this repo may
|
||
edit a file it cannot read.** It is in the batched list for Pouya: delete both
|
||
lines, and `PUBLIC_ANALYTICS_*` with them, since `ANALYTICS` is a literal in
|
||
`src/data/site.ts`.
|
||
|
||
The three values above fill the IAM policy's `BUCKET_NAME`, `ACCOUNT_ID` and
|
||
`DISTRIBUTION_ID` placeholders. **They are deliberately not restated here** —
|
||
§7 is the single source of truth for operational facts, and the copy that goes
|
||
stale is always the one nobody re-reads. `scripts/aws-discover.sh` regenerates
|
||
them from AWS if §7 ever needs re-verifying.
|
||
|
||
> **Read this before creating the key.** The AWS account is **not** a
|
||
> single-project account: it is shared with several unrelated sites and with a
|
||
> bucket whose name indicates another business's production client-database
|
||
> backups. `AGENTS.md` §10 has the specifics and the account identifier; they
|
||
> are kept there rather than repeated here. A static deploy key for a marketing
|
||
> site lives in that same account, and the scoped policy is what keeps a
|
||
> compromised Gitea runner from reaching any of it. Do not widen it, and never
|
||
> put the `user/pouya` credentials in CI.
|
||
|
||
### A runner must exist
|
||
|
||
Gitea Actions needs `act_runner` registered to this repository or its
|
||
organisation, and Actions enabled both site-wide in `app.ini`
|
||
(`[actions] ENABLED = true`) and per-repository. Without a runner the workflow
|
||
queues silently and never runs — which looks exactly like a broken pipeline.
|
||
|
||
The workflow installs the AWS CLI if the runner image lacks it, and runs
|
||
`aws sts get-caller-identity` before touching anything. **That check is
|
||
narrower than it looks:** `sts:GetCallerIdentity` requires no IAM permission at
|
||
all, so it succeeds for any valid key regardless of policy. It catches a
|
||
missing, malformed, or revoked key; it does **not** catch an under-scoped
|
||
policy, which still fails halfway through a sync and leaves the bucket
|
||
partially updated. Read it as a key check, not a permissions check.
|
||
|
||
### The variable guard runs first
|
||
|
||
The workflow's first step — before checkout, before the build, before any AWS
|
||
call — fails the run if `AWS_REGION`, `S3_BUCKET`, or
|
||
`CLOUDFRONT_DISTRIBUTION_ID` is empty.
|
||
|
||
This exists because Gitea only added the `vars` context in 1.21. On an older
|
||
instance every `${{ vars.* }}` interpolates to an empty string with no warning,
|
||
the sync target becomes `s3://`, and the run dies halfway through with an error
|
||
that names nothing useful. The guard converts that into a clean failure that
|
||
says which variable is missing — **on every Gitea version**. A recorded version
|
||
number would have gone stale; the guard does not.
|
||
|
||
### Key rotation — an operational obligation
|
||
|
||
**Rotate `adr-sml-deploy` quarterly.** OIDC would have made this unnecessary;
|
||
with a static key it is a standing task:
|
||
|
||
1. Create a second access key on the same user.
|
||
2. Update the Gitea secrets.
|
||
3. Run the workflow and confirm it succeeds.
|
||
4. **Delete the old key.** Rotation that leaves the old key active is not
|
||
rotation.
|
||
|
||
Set a calendar reminder. A key that is never rotated is the failure mode this
|
||
whole section exists to bound.
|
||
|
||
## Finding the AWS identifiers
|
||
|
||
`scripts/aws-discover.sh` re-collects the inventory — bucket, distribution
|
||
ID, regions, API endpoint, certificate, SES identities, and whether S3 versioning
|
||
is on. Read-only; no call creates or mutates anything.
|
||
|
||
```bash
|
||
chmod +x scripts/aws-discover.sh
|
||
./scripts/aws-discover.sh > aws-inventory.txt
|
||
```
|
||
|
||
The output contains resource names and IDs but no secrets.
|
||
|
||
## Why OIDC would have been better — and why it is unavailable
|
||
|
||
> **Do not execute this section.** It describes the design that was rejected
|
||
> because Gitea cannot support it. The live procedure is *Create the user* above.
|
||
> Nothing here should be created in AWS. Following it would add an unused GitHub
|
||
> federation trust to the shared AWS account (`AGENTS.md` §10).
|
||
|
||
A static `AWS_ACCESS_KEY_ID` never expires, is invisible once set, and grants its
|
||
permissions to anyone who can reach the repository. OIDC issues a short-lived
|
||
token per run, scoped to one repository and one branch — strictly better, and the
|
||
reason the rotation schedule above is not optional here.
|
||
|
||
It needs an identity provider AWS will federate with. GitHub and GitLab both
|
||
publish one; **Gitea and Forgejo do not**, so there is nothing for AWS to trust
|
||
and no role to assume. That is the whole of the constraint (D3 as amended).
|
||
|
||
If the project ever moves to GitHub, the workflow to adopt is
|
||
`docs/reference/github-actions-oidc.yml.example`, and the setup is: register
|
||
`token.actions.githubusercontent.com` as an IAM OIDC provider with audience
|
||
`sts.amazonaws.com`; create a role trusting it, conditioned on the `sub` claim
|
||
matching the repository and `refs/heads/main`; attach the same four-action policy
|
||
given above; then delete `adr-sml-deploy` and its key.
|
||
|
||
## Cache policy
|
||
|
||
The mistake to avoid is caching HTML aggressively — a stale index page is a site
|
||
that does not update.
|
||
|
||
| Pattern | `Cache-Control` |
|
||
|---|---|
|
||
| `*.html` | `public, max-age=0, must-revalidate` |
|
||
| `/_astro/*` (hashed) | `public, max-age=31536000, immutable` |
|
||
| Fonts | `public, max-age=31536000, immutable` |
|
||
| Images | `public, max-age=604800` |
|
||
| `robots.txt`, `sitemap*.xml` | `public, max-age=0, must-revalidate` |
|
||
|
||
Sync in **three** passes, in this order: hashed assets and fonts with the long
|
||
TTL, then images, then everything else. Uploading HTML last means a user never
|
||
fetches a new page whose assets have not landed yet.
|
||
|
||
Two ordering dependencies are load-bearing and easy to break:
|
||
|
||
- Pass 3 re-walks the whole tree; the image headers from pass 2 survive only
|
||
because `aws s3 sync` skips objects it has just uploaded. Reordering the
|
||
passes silently overwrites them with the HTML header.
|
||
- Pass 3's `--exclude "_astro/*" --exclude "fonts/*"` also excludes those
|
||
prefixes from `--delete`, so hashed assets from previous deploys are kept
|
||
deliberately — pages still in a browser cache need them. Do not "fix" it.
|
||
|
||
`robots.txt` and `sitemap*.xml` fall through to pass 3 and get the HTML header.
|
||
That is the intended behaviour: both should be re-fetched, and the table above
|
||
records what the pipeline actually does rather than an unimplemented ideal.
|
||
|
||
Invalidate `/*` on deploy. At this traffic volume the cost is nil, and partial
|
||
invalidation paths are a reliable source of confusing bugs.
|
||
|
||
## CloudFront configuration
|
||
|
||
- Origin: S3 with **Origin Access Control**, bucket not public. Verify the
|
||
bucket policy grants access only to the CloudFront distribution's OAC
|
||
principal and to nothing else, and that public access is still blocked.
|
||
- Redirect HTTP → HTTPS. TLS 1.2 minimum.
|
||
- Default root object `index.html`.
|
||
- **Custom error response:** 404 → `/404.html` with **response code 404**, not
|
||
200. Returning 200 for a missing page tells crawlers every bad URL is real
|
||
content, and it is the single most common misconfiguration in this stack.
|
||
- Compression on. Response-headers policy from `05-backend-spec.md`.
|
||
- A CloudFront Function for trailing-slash normalisation, so `/about` and
|
||
`/about/` do not both resolve as separate indexable URLs.
|
||
|
||
## Branch model
|
||
|
||
`main` is production; a push to `main` is what triggers a deploy. Work on
|
||
short-lived branches, open a PR, merge.
|
||
|
||
**The CI pipeline has never run.** Not for want of a lockfile — `npm ci`,
|
||
`astro check` and `astro build` all work now — but because the deploy user does
|
||
not exist (Q22) and Actions are not enabled with a runner registered (Q23).
|
||
Treat "every push deploys" as the design; today the path is `npm run deploy`.
|
||
|
||
**Pull request checks — planned, not implemented:** `npm run build` ·
|
||
`astro check` · lint · Lighthouse CI against the budgets in `04-seo-spec.md` ·
|
||
link check. `.gitea/workflows/deploy.yml` has **no `pull_request` trigger**
|
||
(only `push` on `main` and `workflow_dispatch`), so nothing gates a merge today.
|
||
`npm run build`, `npm run check` and `npm run lint` all run clean locally.
|
||
|
||
**Lighthouse is still not one of the checks that can be wired — and the reason
|
||
changed on 2026-08-31.** `npm run lighthouse` exists again (step 7a, `AGENTS.md`
|
||
§7), so the blocker is no longer a missing script: it is that standalone
|
||
Lighthouse drives an **installed browser** and the runner has none. Wiring it
|
||
would need Chrome on the `act_runner` host (Q23). Wire the other four; do not
|
||
write a workflow step that shells out to a browser that is not there. The gate
|
||
lives locally and on the cutover checklist below.
|
||
|
||
Tag every production deploy `v<year>.<n>` so a rollback has something to name.
|
||
|
||
## Rollback
|
||
|
||
1. Re-run the workflow at the last good tag, or
|
||
2. `git revert` and push, or
|
||
3. Restore from S3 object versioning — **already Enabled** on the site bucket
|
||
(`AGENTS.md` §7). It is the
|
||
difference between a rollback and a rebuild; do not turn it off.
|
||
|
||
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.
|
||
|
||
> ✅ **CUTOVER EXECUTED 2026-09-02 — THE SITE IS LIVE AT `https://adr.smlcompany.ca`.**
|
||
> Deployed by Pouya from this machine via `scripts/deploy-local.sh` with the
|
||
> `adr-sml-deploy` credential. **Verified independently the same day rather than
|
||
> transcribed**, 26 routes with the iteration count asserted (a `for r in $VAR`
|
||
> loop ran ONCE first — the zsh trap `CLAUDE.md` records, caught by the count):
|
||
> all 22 pages, `robots.txt`, `sitemap-index.xml` and the bio PDF return **200**;
|
||
> an unknown path returns **404** and serves the styled Astro page, 14,321 B, not
|
||
> S3's XML. HTML carries `max-age=0, must-revalidate`, `_astro` assets
|
||
> `max-age=31536000, immutable`, and the PDF is **89,496 B**, matching `public/`
|
||
> exactly. **All 22 live pages are byte-identical to a local `dist/` rebuilt at
|
||
> `67847d9`** — SHA-256 compared page by page, 22 same / 0 differ / 0 errors. The
|
||
> five `noindex` surfaces and the 17-URL sitemap are correct.
|
||
>
|
||
> 🛑 **THIS LIST WAS NOT CLEAN WHEN THE SITE PUBLISHED, AND THAT IS THE RECORD,
|
||
> NOT A REPROACH. TWO BLOCKING ITEMS WERE GENUINELY OPEN AT CUTOVER; BOTH ARE
|
||
> NOW NARROWED RATHER THAN CLOSED.** D11 is a single shot and the checklist
|
||
> exists because of it; a launch that crosses its own gates should be legible as
|
||
> one afterwards rather than smoothed over.
|
||
>
|
||
> ⚠️ **THE COUNT SAID THREE FOR ONE DAY AND THREE WAS WRONG — corrected
|
||
> 2026-09-03.** The third, *"the intake form is live and broken"*, was **a false
|
||
> alarm from a malformed probe** and is refuted in item 2 below. It is corrected
|
||
> here rather than deleted because a blocker that was never real, asserted on the
|
||
> most-read part of this page, is the same failure as a real one that goes
|
||
> unrecorded — and because **this is the first time the count moved for a reason
|
||
> the earlier notes did not anticipate: not closed, not deleted, not moot, but
|
||
> WRONG.** That is a fourth way off this list, and it looks identical to the
|
||
> other three in a tally.
|
||
>
|
||
> **The state as at 2026-09-03:** **Q60** is owed rather than pending — Pouya
|
||
> ruled the page publishes and the deletion is confirmed after launch, reading
|
||
> from **2026-09-04**. **The D20 pass** returned 20 confirmed findings, of which
|
||
> **15 are fixed, 2 refuted and 3 need a ruling from him** rather than an edit.
|
||
>
|
||
> ✅ **THE READ-THROUGH IS COMPLETE — Pouya, 2026-09-02, and it returned ONE
|
||
> FINDING WHICH WAS NOT COPY.** `public/favicon.ico` shipped with no
|
||
> transparency; fixed and verified the same day (see **Favicon set complete**
|
||
> below). His read carried the approvals with it, in terms: the
|
||
> `/legal/privacy/` §Who can see it wording, the **SML Company Ltd** consent
|
||
> line, and `/med-arb/` **as shipped**. That discharges blocker 2 and every
|
||
> wording sign-off that had been routed into it.
|
||
>
|
||
> ⚠️ **THE COUNT WENT UP, AND THAT IS THE FIRST TIME IT HAS — READ THE REASON,
|
||
> NOT THE NUMBER.** It read ONE for part of 2026-09-02 because an approval had
|
||
> gone **missing**; it then read ONE because the pass that approval was routed
|
||
> into had been **done**; it now reads **THREE**, because the site published and
|
||
> the D20 pass then ran against the shipped bytes and returned **FAIL**. Every
|
||
> earlier move in this note was a tally shrinking for a reason a tally could not
|
||
> show. This one grew, and the lesson is the same in the other direction: **the
|
||
> list did not get worse, the measurement finally happened.** Two of the three
|
||
> were true before cutover and unmeasured; one was invisible by construction.
|
||
>
|
||
> ⚠️ *(The count has moved repeatedly in one day and the DIRECTION is the only
|
||
> part worth reading — the number of moves is deliberately not stated, because a
|
||
> tally of how often a tally changed is the same trap one level up. It said ONE for part of 2026-09-02 and that was a
|
||
> **defect** — the wording approval Pouya reserved had been recorded only inside
|
||
> records marked closed, the `TODO(pouya)` deleted, Q62 struck, this callout
|
||
> ticked, so nothing would have stopped unapproved copy publishing
|
||
> (`adversarial-reviewer`, D20 pass round 1). Q63 was then **answered** in three
|
||
> limbs by ruling, which is a gate closed by an answer rather than by deletion —
|
||
> and answering it **opened Q64**, one paragraph lower on the same page. Q64 then
|
||
> left the list a **third** way: **the paragraph it was about was deleted**, so
|
||
> the question is moot rather than answered. **Closed, deleted, and moot look
|
||
> identical in a count and nowhere else, which is why the count is never the
|
||
> record.** The second slot is no longer a question at all — it is the human
|
||
> pass, promoted here from the checklist below because that is where the last
|
||
> reserved approval went missing.)*
|
||
>
|
||
> 1. 🛑 **Q60 — the retention MECHANISM has still not been observed, AND
|
||
> `/legal/privacy/` PUBLISHED ANYWAY.** TTL is `ENABLED` and no record has been
|
||
> watched to disappear, and the page asserts the **mechanism**, not merely the
|
||
> period. `docs/09` Part 10 is the test; earliest useful reading **48 hours**
|
||
> after the record is written, failure not called before **7 days** — Pouya
|
||
> started the window 2026-09-02, so **check from 2026-09-04**.
|
||
> ⚠️ **THE PAGE CARRIED ITS OWN INSTRUCTION NOT TO DO THIS — RULED STALE BY
|
||
> POUYA 2026-09-03 AND REWORDED.** `src/pages/legal/privacy.astro` held a
|
||
> `TODO(pouya)` ending *"This page must not go public until a deletion has
|
||
> actually been seen."* **His decision supersedes it: publish, then confirm the
|
||
> deletion after launch.** The comment now states that decision and its date,
|
||
> and the `TODO(pouya)` marker is gone, which also clears the checklist item
|
||
> *"No `TODO(pouya)` remains in any shipped page"* below.
|
||
> **What stays true is the mechanism finding, and it is worth keeping:** that
|
||
> instruction was a **JSX comment**, so Astro strips it and it never reached
|
||
> `dist/` — which is why `check:claims`, the build and both deploy paths were
|
||
> green over it. **A publication gate that lives only in a stripped comment is
|
||
> not a gate**, whatever the gate happens to say. Q60 itself is unchanged and
|
||
> the confirmation is now *owed* rather than *pending*.
|
||
> 2. ✅ **REFUTED BY MEASUREMENT 2026-09-03 — THE INTAKE FORM IS NOT BROKEN, AND
|
||
> THIS ENTRY IS THE CORRECTION.** Pouya's probe, reproduced here in both
|
||
> directions: `docs/09` §7.1 verbatim — `POST /api/intake` with
|
||
> `Origin: https://adr.smlcompany.ca` and
|
||
> `Content-Type: application/x-www-form-urlencoded` — returns **HTTP/2 303**,
|
||
> `location: https://adr.smlcompany.ca/contact/could-not-send/`, with
|
||
> `access-control-allow-origin` echoed and an `apigw-requestid` present. **The
|
||
> handler answered as designed**: it validated, found an empty submission and
|
||
> redirected to the failure page before any write and any email. The **same
|
||
> probe with the `Origin` header removed returns 403**, which is the control.
|
||
> ⚠️ **A BARE POST TO `/api/intake` RETURNS 403 BY DESIGN, AND §7.1 SAYS SO
|
||
> THREE LINES BELOW THE PROBE** — *"403 means the `Origin` header did not
|
||
> arrive"*. The earlier finding read a status code without reading the document
|
||
> that defines what that code means on that route. **This false alarm has now
|
||
> fired twice in two days** — Pouya's own probe tripped it 2026-09-02 — and it
|
||
> is recorded in `CLAUDE.md`'s instrument list, which stands at nine.
|
||
> **The only valid route probe is `docs/09` §7.1 verbatim, `Origin` included.**
|
||
>
|
||
> ⚠️ **AND THE TWO "BACKEND NOT DEPLOYED" CLAIMS FINDINGS FALL WITH IT.**
|
||
> `/legal/privacy/` §Where it is stored (*"Two emails are sent when you submit
|
||
> the form"*) and `/contact/received/` (*"A confirmation goes to the email
|
||
> address you gave"*) were both premised on the route not existing. It exists.
|
||
> **What is NOT settled by this probe is that both emails actually arrive** —
|
||
> §7.1 stops before any write and any email by design, and that is `docs/09`
|
||
> §7.2, the real-submission test Pouya has in progress. The disclosures are
|
||
> unblocked; the end-to-end confirmation is still owed.
|
||
> 3. ⚠️ **THE D20 CLAIMS PASS RETURNED FAIL WITH 20 CONFIRMED FINDINGS; 15 ARE
|
||
> NOW FIXED, 2 REFUTED, 3 OUTSTANDING — updated 2026-09-03, and the three
|
||
> numbers partition the twenty.** Fixed under Pouya's rule *"the gloss may say
|
||
> no more than the extract says; no new claims, no new sources"*: findings
|
||
> 1–9, 14–18 and 20 — the whole gloss class, plus `/bio/`'s role verb.
|
||
> ⚠️ **15 FINDINGS, 14 DISTINCT EDITS: findings 4 and 15 quote the same
|
||
> sentence** on `/practice/energy/`, so one edit closed both. **REFUTED:**
|
||
> findings 12 and 19, the two backend disclosures, with item 2 above.
|
||
> **OUTSTANDING — findings 10, 11 and 13, and each is outstanding for a
|
||
> different reason:**
|
||
> **(10) NEEDS A RULING.** `/fees/`'s *"Every figure is on this page"* against
|
||
> §4's **Med-Arb** offering, which `docs/07-fees.md` prices nowhere. Either a
|
||
> med-arb fee term or a scoped promise; it cannot be closed by narrowing.
|
||
> **(11) IS RULED, AND THE CONFIRMATION IS OWED.** The retention *mechanism*
|
||
> sentence on `/legal/privacy/` is unchanged and still ships, deliberately —
|
||
> that is blocker 1 above and §9 Q60, reading from 2026-09-04. It is listed so
|
||
> the twenty account for themselves, not because it is unresolved.
|
||
> **(13) NEEDS HIM TO HAVE SAID IT.** `/legal/privacy/`'s *"if a conflicts
|
||
> check has already been run I will tell you what its outcome was"* is an
|
||
> **undertaking**, and §4's gate for that class is one line: Pouya must have
|
||
> made it **in terms**. It is not in `CONDUCT_UNDERTAKINGS`.
|
||
> ⚠️ **§12 R1 IS NOT ONE OF THE TWENTY.** An earlier form of this item named it
|
||
> as the third outstanding finding and dropped 11 to make room — a tally that
|
||
> did not partition its own set. R1 is a standing reminder on licensure that a
|
||
> completeness critic reached independently from the copy; it is live, and it
|
||
> is counted nowhere. The original entry follows.
|
||
>
|
||
> 🛑 **THE D20 CLAIMS PASS HAS NOW RUN AGAINST THE SHIPPED BYTES AND RETURNED
|
||
> FAIL — 20 CONFIRMED FINDINGS ON LIVE PAGES.** Run 2026-09-02 at `67847d9`,
|
||
> after cutover, over all 23 built pages: 13 auditors (8 page groups + 5
|
||
> cross-cutting lenses) → 41 raw findings → 31 distinct → each adversarially
|
||
> verified by an independent `claims-auditor` instructed to refute it. **20
|
||
> CONFIRMED, 11 REFUTED**, plus 13 further findings from two completeness
|
||
> critics. See the **`claims-auditor`** item below for the breakdown, and
|
||
> `AGENTS.md`'s Change Log entry of 2026-09-02 (ar) for the full list.
|
||
> **Nothing here is a claim about Pouya, his credentials or his designations —
|
||
> every one of those traced, for the third pass running.** The failures are
|
||
> over-reaches in glosses on sourced legal material, and disclosures on
|
||
> `/legal/privacy/` and `/contact/received/` that describe a backend which is
|
||
> not deployed.
|
||
> 4. ✅ **DONE 2026-09-02 — Pouya read every page against `AGENTS.md` §4.** The
|
||
> human pass, the other half of D20 and not delegable. It was also where the
|
||
> §Who can see it approval was routed, and his ruling that the read-through
|
||
> *is* the approval means that sign-off is now discharged rather than
|
||
> pending. **Sole finding: the favicon's opaque ground.** No copy finding on
|
||
> any of the 23 pages.
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — Q64, MOOT.** It asked whether anyone else holds the
|
||
> AWS root password or its MFA device, because the page published *"has no
|
||
> programmatic key, and I hold it"* one paragraph below *"the small number of
|
||
> people who administer it with me"*, where a reader takes it as **sole**
|
||
> custody. **Pouya's second ruling that day deleted the sentence** — the section
|
||
> is now four plain statements and says nothing about root — so the question no
|
||
> longer gates anything. ⚠️ **The underlying fact is unchanged and unestablished:
|
||
> §7 records root as *held by Pouya*, which is not *held only by Pouya*, and
|
||
> nothing measured can settle it. Nothing may be published about root custody
|
||
> without asking again.**
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — Q63, all three limbs, by ruling.** **(a)** The §Who
|
||
> can see it wording is **approved with two trims** — the editorial closing
|
||
> sentence struck, and the mailbox clause rewritten per (b). **(b)**
|
||
> `info@smlcompany.ca` is a **delegated mailbox read by Pouya and by
|
||
> administrative staff**, and the page now says so instead of *"anyone who can
|
||
> reach that mailbox"*. **(c)** The account **root credential is held by Pouya**;
|
||
> it has no programmatic key and MFA is on, and the page now states the first two
|
||
> of those. ⚠️ **AND THE ANSWER CHANGED THE HEADLINE SENTENCE:** Pouya's
|
||
> attestation is that *"two people is an exaggeration… a handful is accurate"*,
|
||
> because **the simulation counts identities and the page was reading them as
|
||
> humans**. No numeric human headcount ships; the page attributes read access to
|
||
> *"the account's administrators — me, and the small number of people who
|
||
> administer it with me"*. §12 **R21** is re-scoped to match.
|
||
> ⚠️ *(Superseded the same day in its details, not in its rulings: the ruling
|
||
> below cut the section to four plain statements, so the sentence quoted above is
|
||
> no longer the shipped one and root is not mentioned at all. Each limb of Q63
|
||
> still stands — no headcount, the mailbox named, root attested in §7.)*
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — THE SECTION IS GENERIC, by a second ruling the same
|
||
> day.** *"It over-explains technical mechanics that belong in the evidence file,
|
||
> not in front of an inquirer."* §Who can see it is now **four short statements**
|
||
> — who can read it, that the receiving system can only add a record, where the
|
||
> notification goes and who reads it, and that the confirmation sits with the
|
||
> reader's own provider. **Deleted from §Who can see it:** the measurement
|
||
> paragraph, the root-credential sentence, the single-sign-on and federated-
|
||
> login enumeration, the resource-policy clause, the *"company that runs a
|
||
> database"* aside, the deploy-credential sentence and the three-copies
|
||
> summary. ⚠️ **THE SHARED-ACCOUNT CLAUSE WAS CUT WITH THEM AND THEN RESTORED
|
||
> — to §Where it is stored, where it belongs.** It is a storage disclosure
|
||
> rather than mechanics, the ruling did not name it, and without it no page
|
||
> told a reader their intake sits in an account that also runs unrelated
|
||
> systems (`adversarial-reviewer`, round 1). **These lists must stay identical
|
||
> — there were four of them and they named four different sets.** **None of
|
||
> that verified
|
||
> material was lost** — all of it stays in `AGENTS.md` §7 and
|
||
> `docs/reference/intake-table-access-verification.md`, and the section comment in
|
||
> `src/pages/legal/privacy.astro` bars restoring it to the page. **The risk moved
|
||
> in the right direction:** every deleted sentence was a claim about a system
|
||
> outside this repository that nothing reports on, which is what §12 **R21**
|
||
> exists for — R21 is re-scoped from five live claims to two.
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — THE CONSENT STRING NAMES THE CORPORATION.** *"I
|
||
> consent to **SML Company Ltd** storing and using the information in this
|
||
> form…"*, per ruling, replacing the natural person. It is the one sentence a
|
||
> submitter actually agrees to and it is the PIPEDA basis, and the policy it
|
||
> links to describes a mailbox read by administrative staff — a corporation is
|
||
> the party that matches, and `/legal/privacy/` now names it in terms under §Why
|
||
> it is collected. **The NAME ONLY:**
|
||
> §4 verifies the federal incorporation, records it as *not published*, and
|
||
> cautions that it must never be read beside the licence-status row. `docs/05`
|
||
> §Consent text carries the string verbatim and moved with it.
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — Q62.** `/legal/privacy/` no longer states anything
|
||
> false about who can read the intake table. Pouya's ruling was **state the
|
||
> truth**, not remove the second administrator's access: the page attributes read
|
||
> access to the account's administrators and names their role. *(It said "two
|
||
> people can read it" until the Q63 ruling later the same day replaced the count,
|
||
> and the ruling after that cut the section to four plain statements — of the two
|
||
> stronger facts this entry originally credited it with, the writing function's
|
||
> add-only access still ships and the deploy credential's lack of access does
|
||
> not. See the two blocks above.)* The
|
||
> `sole-administrator-q62` tripwire in `check-claims.mjs` **stays permanently**
|
||
> by the same ruling, extended from two alternatives to **five**: the clause the
|
||
> first form could not see two sections up the same page, the summary that would
|
||
> have re-asserted the struck number four lines below the corrected paragraph,
|
||
> and the sentence that carried the false count. Proven both ways against the
|
||
> pre-correction page rebuilt from `bd282aa` — **exit 1 with 5 matches**, exit 0
|
||
> on the corrected page, and **re-proven both ways after the Q63 rewrite**, same
|
||
> 5 matches at the same lines.
|
||
>
|
||
> ✅ **CLOSED 2026-09-02 — the `/med-arb/` gloss.** Struck, with no replacement
|
||
> and no competence claim, per ruling; **ratified as shipped** on 2026-09-02 with
|
||
> no credential line restored. **Pouya's note, recorded because it is the reason
|
||
> and not a detail: med-arb is a service he provides, not a designation.** That is
|
||
> what makes the struck gloss unrecoverable rather than merely unsourced — there
|
||
> is no designation to cite for it. The ADRIC-sourced material carries the
|
||
> section.
|
||
|
||
**Cutover prep — deferred maintenance, done BEFORE the checklist below**
|
||
|
||
This group exists because deferring a thing and forgetting it look identical
|
||
three weeks later. Each item carries the date it was deferred and the reason, so
|
||
the decision is re-readable rather than re-litigated.
|
||
|
||
- [ ] ⚠️ **TWO DEPENDENCY MAJORS ARE ONE BEHIND, DEFERRED 2026-09-01 BY RULING.**
|
||
`@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."* `npm audit` reports **0 vulnerabilities**
|
||
`[verified 2026-09-01]`, so this is a churn decision and not a security one —
|
||
and it would become a security decision the moment that stops being true.
|
||
**Take both majors here, before cutover, and read the changelogs rather than
|
||
the version numbers**: an MDX major can change how `docs/03`'s copy renders,
|
||
and a TypeScript major can change what `astro check` accepts, which is a
|
||
gate on both deploy paths. **`AGENTS.md` R11 keeps firing at every phase
|
||
boundary as designed** — this item is the deferral, not a replacement for the
|
||
reminder, and R11 is what will raise the next pin that drifts.
|
||
**The sweep behind this item, `npm view <pkg> version` across all 19 pins
|
||
`[verified 2026-09-01]`:** those two are the only majors behind. Four are a
|
||
minor or patch behind and are already satisfied by their own 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
|
||
remaining 13 are current. Re-run the sweep here rather than trusting this
|
||
list: it is dated for that reason
|
||
|
||
**Content and compliance**
|
||
|
||
> ⚠️ **THE FIRST TWO ITEMS ARE THE PROJECT'S ONLY FULL CLAIMS PASS — D20, Pouya,
|
||
> 2026-08-30.** `claims-auditor` no longer runs per build step; `/build` Phase 3
|
||
> is `adversarial-reviewer` alone. Everything the register is meant to prevent
|
||
> therefore lands here. `AGENTS.md` D20 records both the reasoning and what
|
||
> deferring it costs — read it before treating either item as a formality, and do
|
||
> not tick one because `npm run check:claims` is green. That script is a greppable
|
||
> tripwire; it cannot read a page.
|
||
|
||
- [ ] **`claims-auditor` run over EVERY page in `dist/`, findings resolved.**
|
||
**This is the project's only full claims pass. Nothing publishes until it
|
||
is clean.** Not per-page in isolation — the pass exists here because the
|
||
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). ✅ **Two of the three remaining were ruled and closed
|
||
2026-09-02** — Q62 (the page now states the measured truth) and the
|
||
`/med-arb/` gloss (struck, no replacement).
|
||
|
||
⚠️ **AND THE PASS WAS RE-RUN OVER THOSE TWO FIXES ON 2026-09-02, WHICH IS
|
||
WHAT MAKES THE ITEM CLOSEABLE — IT RETURNED FAIL WITH EIGHT MORE.** This is
|
||
the same pass completing, not a per-step audit reinstated: a FAIL whose
|
||
fixes are never re-audited leaves *"findings resolved"* asserted rather than
|
||
checked. **Five of the eight were in the fixes themselves** — the
|
||
replacement copy reintroduced an absolute third-party negative of the
|
||
shape struck from this page on 2026-08-31, claimed an enumeration the
|
||
evidence file did not support, asserted *"the one other place a copy
|
||
exists"* when the inquirer's confirmation carries the whole submission,
|
||
left `LAST_UPDATED` at 31 August on the change set that rewrote the page's
|
||
central disclosure, and left *"your IP address"* standing when `docs/09`
|
||
Part 7.2 had said in terms to fold it into the Q62 edit. Two were on
|
||
`/med-arb/`: the struck gloss left *"the section above"* pointing at the
|
||
ADRIC rule set and *"the agreement"* with no antecedent, and the bare
|
||
designations line was left sitting directly under ADRIC's competence
|
||
quotation. One was declined with a reason (§Rules' heading — see the
|
||
Change Log). **All corrected or declined; the pass is clean on findings
|
||
and the item is open only on the human halves above.**
|
||
|
||
🛑 **AND IT HAS NOW RUN A THIRD TIME — 2026-09-02, AFTER CUTOVER, AGAINST
|
||
THE BYTES THAT ACTUALLY SHIP — AND RETURNED FAIL WITH 20 CONFIRMED
|
||
FINDINGS. THIS ITEM STAYS UNTICKED.** The two runs above predate the
|
||
Q62/Q63 privacy rewrite, the `/med-arb/` strike and the consent change, so
|
||
neither had seen the shipped copy. This one did: all 22 live pages were
|
||
confirmed **byte-identical** to a `dist/` rebuilt at `67847d9` before the
|
||
audit began, and the auditors read a parse5 extraction of the visitor text
|
||
and JSON-LD of all 23 pages, validated against known shipped strings first.
|
||
**13 auditors — 8 page groups and 5 cross-cutting lenses (adjacency,
|
||
gloss-vs-source, structured data, licensure, §4 Forbidden) — returned 41
|
||
raw findings, deduped to 31, each then handed to an independent
|
||
`claims-auditor` instructed to REFUTE it. 20 confirmed, 11 refuted.** Two
|
||
completeness critics added 13 more. Full list: `AGENTS.md` Change Log
|
||
2026-09-02 (ar).
|
||
|
||
**The shape of the 20, because it is the same shape as run 2 and that is
|
||
the finding about the process rather than the pages.** Not one is a claim
|
||
about Pouya, his credentials, his designations, his memberships or the
|
||
boutique — **the credential spine has now traced clean three passes
|
||
running**, and D16, D13 and the Forbidden table were swept with every hit
|
||
printed and read. What failed is two classes: **glosses that assert more
|
||
than their committed extract establishes** (`/practice/energy/` calling the
|
||
IESO process six stages where the source says *"up to six"*;
|
||
`/practice/construction/` stating the 30-day adjudication deadline without
|
||
its *"Subject to subsection (2)"* extension; `/practice/technology/`
|
||
asserting Ontario *"has one AI instrument"*), and **disclosures describing a
|
||
backend that is not deployed** (`/legal/privacy/` on retention and on the
|
||
two emails, `/contact/received/` on the confirmation). The second class is
|
||
not a wording problem: it is the privacy policy of a live site describing a
|
||
mechanism that cannot run, which is the defect class `AGENTS.md` Q22 named.
|
||
|
||
⚠️ **AND THE PASS RAN AFTER THE SITE PUBLISHED, WHICH IS THE ONE THING D20
|
||
RESTED ON AND NO LONGER HAS.** D20's reasoning is explicit that deferring
|
||
the claims pass is safe because *"nothing has shipped and there is no public
|
||
site, so every claims finding to date has been about a page no visitor can
|
||
reach."* That premise expired at cutover. It is recorded here rather than
|
||
argued: whether D20 needs amending is Pouya's call, and the honest version
|
||
of the trade is that the deferral bought nine build steps of speed and the
|
||
bill came due on a live page.
|
||
|
||
**Two things about the earlier 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
|
||
- [x] ✅ **THE §Who can see it WORDING APPROVAL IS DISCHARGED — Pouya's ruling,
|
||
2026-09-02: *"the read-through is the approval."*** Q62 settled what the
|
||
section must **say** and he reserved the **wording**; he then ruled twice on
|
||
it the same day — Q63(a) approving with two trims, and the second ruling
|
||
cutting the section to four plain statements — and directed in terms that
|
||
nothing be held open waiting on a separate sign-off. ⚠️ **THIS TICK IS NOT
|
||
"THE TEXT HAS BEEN READ".** It records that the reserved approval has
|
||
**moved**, to the read-through blocker in the callout above and the item
|
||
below. Two ticked boxes stood here for one round — one for the ruling, one
|
||
for the text — because a single tick over changed copy is how an approval
|
||
requirement went missing the first time (`adversarial-reviewer`, round 2).
|
||
They collapse into this one only because the ruling collapsed them, and
|
||
the gate did not disappear: **it is blocker 2 in the callout above**, which
|
||
is the most-read place on this page rather than the least.
|
||
- [x] ✅ **CLOSED 2026-09-02 — Q64 IS MOOT: THE PARAGRAPH WAS DELETED.** It asked
|
||
whether anyone else holds the AWS root password or its MFA device, because
|
||
`/legal/privacy/` published *"has no programmatic key, and I hold it"* one
|
||
paragraph below *"the small number of people who administer it with me"* —
|
||
where a reader takes it as **sole** custody, which nothing establishes.
|
||
Pouya's second ruling that day struck the sentence along with the rest of
|
||
the mechanics, so no page says anything about root and the question gates
|
||
nothing. The `TODO(pouya)` is gone from
|
||
`src/pages/legal/privacy.astro` with the paragraph that carried it.
|
||
⚠️ **THE FACT IS STILL UNESTABLISHED AND THAT DID NOT CHANGE.** §7 records
|
||
root as *held by Pouya*, which is not *held only by Pouya*; root is not an
|
||
IAM principal and cannot be simulated. **Nothing about root custody may be
|
||
published without asking him again** — the section comment in the page
|
||
source carries that bar, because "we deleted it" and "we checked it" are
|
||
the same green tick from three weeks away.
|
||
- [x] ✅ **Q63(b) ANSWERED 2026-09-02 — `info@smlcompany.ca` is a DELEGATED
|
||
MAILBOX: Pouya and administrative staff read it.** The page said *"anyone
|
||
who can reach that mailbox"*, which was true either way and answered the
|
||
mail half of the question on a lower standard than the table half. It now
|
||
states who reads it. The fact is in `AGENTS.md` §7 and **§12 R21's trigger
|
||
covers it** — like the AWS enumeration, nothing reports when a delegation
|
||
changes. ⚠️ **The answer reached FOUR sentences, not the one the question
|
||
named** — §Where it is stored twice, §How long it is kept once, §Who can
|
||
see it once — because *"my mailbox"* had been written as a personal one
|
||
throughout. That is the fifth partial sweep on this page's who-can-see-it
|
||
set; the section comment in `src/pages/legal/privacy.astro` enumerates
|
||
them **by opening phrase rather than by count** — deliberately, because the
|
||
count has been wrong twice, and this line carried "eight" for a round after
|
||
the comment itself had been corrected to nine (`adversarial-reviewer`,
|
||
round 2). Read the list, not a number
|
||
- [x] ✅ **DONE 2026-09-02 — Pouya has read every page against `AGENTS.md` §4.**
|
||
The human pass, the other half of D20 and not delegable — his reading is
|
||
what the per-step audit was traded for. **It returned one finding across 23
|
||
pages and that finding was not copy:** the favicon shipped with an opaque
|
||
cream ground. **The `/legal/privacy/` §Who can see it wording, the
|
||
SML Company Ltd consent line and `/med-arb/` as shipped are approved by
|
||
this read**, per his ruling that the read-through *is* the approval.
|
||
⚠️ **This does NOT discharge `claims-auditor`'s cutover pass**, which is a
|
||
separate item on this list: D20 traded the per-step machine audit for the
|
||
human pass **plus** one machine pass over the finished site, and one of
|
||
those two has now happened.
|
||
~~⚠️ **START WITH `/legal/privacy/` §Who can see it. IT IS BLOCKER 2 IN THE
|
||
CALLOUT ABOVE, AND THIS READ *IS* THE APPROVAL**~~ — **struck 2026-09-02:
|
||
the pass is DONE, and an unstruck imperative on a ticked item told an
|
||
operator to begin a read this page also records as finished, pointing at a
|
||
blocker that no longer exists** (`adversarial-reviewer`, round 2). What it
|
||
said remains true of what happened: Pouya ruled on
|
||
2026-09-02 that nothing waits on a separate wording sign-off, and he read
|
||
§Who can see it first. Every
|
||
sentence in it changed three times that day — Q62's ruling, Q63's, then the
|
||
ruling that cut it to **four plain statements** — and it is the only
|
||
section on the site whose subject lives entirely outside this repository.
|
||
**It is now four sentences and should take a minute**; that is the point of
|
||
the cut. The verified material behind them is in
|
||
`docs/reference/intake-table-access-verification.md` and `AGENTS.md` §7 if
|
||
he wants to check any of it, and **the page deliberately no longer cites
|
||
it**.
|
||
**Then read the two `/contact/` sentences against it, which is a judgement
|
||
rather than a defect** — `/contact/received/` says *"email me directly at
|
||
`info@smlcompany.ca` — that reaches me whether or not the receipt did"* and
|
||
`/contact/` says *"Send the form below, or email me directly"*. **Neither is
|
||
false and neither asserts exclusivity**; the mail does reach him. But
|
||
`/legal/privacy/` now discloses that administrative staff read that mailbox,
|
||
and a party who has just been told to send dispute detail *"directly"* to
|
||
the neutral may take more from the word than is true. A sweep of all 23
|
||
built pages found these two as the only other surfaces touching the point.
|
||
Raised as **consider**, not blocking, by `adversarial-reviewer` round 1.
|
||
✅ **THE THIRD SURFACE IS DECIDED — the CONSENT string now names the
|
||
corporation.** *"I consent to **SML Company Ltd** storing and using the
|
||
information in this form…"*, Pouya's ruling 2026-09-02, replacing the
|
||
natural person. It is the one sentence a submitter actually agrees to and
|
||
it is the PIPEDA basis, and the policy it links to describes a mailbox read
|
||
by administrative staff — a corporation is the party that matches, and
|
||
`/legal/privacy/` now names it in terms under §Why it is collected. **The sweep that had missed it was anchored on
|
||
mailbox vocabulary** (*"email me directly"*, *"reaches me"*), which is R8's
|
||
sharpest edge: the right command, the wrong anchor.
|
||
⚠️ **THE PAGE AROUND IT STILL SAYS "me" AND "I", AND THAT IS DELIBERATE, NOT
|
||
AN OVERSIGHT — read the two together and say if it reads wrong.** The
|
||
ruling changed the consent sentence and nothing else; `/legal/privacy/` is
|
||
written in the first person throughout (*"whatever you send me"*, *"in your
|
||
hands rather than mine"*), and `/contact/` is too. Nothing is false either
|
||
way — he is the accountable individual, the corporation holds the systems —
|
||
but the checkbox and the prose beside it now name different parties, and
|
||
**that is a judgement about voice which is his and not a reviewer's.**
|
||
- [x] ✅ **MEMBERSHIPS RE-CONFIRMED 2026-09-02 — Pouya: ADRIC, ADRIO, the three
|
||
OBA sections and the CTF are all current.** §4 and `src/data/site.ts` are
|
||
re-stamped `[verified 2026-09-02 — Pouya]`. ⚠️ **THERE ARE TWO ARRAYS AND
|
||
RE-STAMPING DOES NOT CHECK THAT THEY AGREE.** `CREDENTIALS.memberships`
|
||
feeds `/about/`'s visible list and `/bio/`; **`MEMBERSHIP_ORGS` feeds
|
||
`/process/` §Confidentiality and the `memberOf` triples**, and `site.ts`
|
||
records that the two differ on three of four lines. `_MembershipParity`
|
||
compares their lengths only, so a substitution passes `npm run check` in
|
||
silence. An earlier form of this line said `schema.ts` emitted from the
|
||
same constant (`adversarial-reviewer`, round 1).
|
||
**RE-ARM THIS FOR THE NEXT REPUBLISH — the row does not close.** `AGENTS.md`
|
||
§12 **R10**, which is an **event trigger and cutover is one of its two
|
||
events.** Q44 closed 2026-08-28 and the group is published on `/about/`
|
||
(ADRIC, ADRIO, the three OBA sections, the CTF, `[verified 2026-08-28 —
|
||
Pouya]`), so this item is no longer "publish them" — it is **"ask him
|
||
again, then re-stamp §4 with the cutover date."**
|
||
**Pouya declined renewal-date tracking**, which is exactly why this sits on
|
||
the checklist: there is no date on which anyone would otherwise re-check.
|
||
OCNI lapsed quietly and §4 records it as "not current, do not publish" —
|
||
found roughly a year late. A stamp is not a renewal receipt.
|
||
**Do NOT add a currency sentence to the page while you are here** — his
|
||
ruling is *"list the memberships; promise nothing about their future
|
||
state"*, and the struck sentence stays struck.
|
||
**`memberOf` IS EMITTED on `/about/`** — Q53 answered 2026-08-28 and the
|
||
withholding is dropped, so the graph asserts the same four memberships the
|
||
page shows. **This item therefore covers both**: re-confirming before
|
||
cutover means `src/data/schema.ts` as well as the visible list, and they
|
||
must not be allowed to diverge.
|
||
**§4 records yearly renewal for the OBA sections and the CTF only** — it
|
||
says nothing about ADRIC's or ADRIO's period, and an earlier version of
|
||
this line asserted "all renew yearly", which §4 does not support.
|
||
⚠️ **ASKED AND ANSWERED ON 2026-09-02 — that is why this is ticked, and
|
||
the distinction is the whole of R10.** It was ticked against a fresh
|
||
one-line confirmation from Pouya, not against the 2026-08-28 stamp: *a
|
||
stamp is not a renewal receipt.* The question to ask next time is
|
||
unchanged — *"are ADRIC, ADRIO, the three OBA sections and the CTF all
|
||
still current?"* — and the answer is followed by re-stamping **all four
|
||
stamp-bearing sites**: §4, `src/data/site.ts`, `src/data/schema.ts` and
|
||
**`src/pages/about.astro`**, which is the page that renders the group and
|
||
is therefore the file an editor most plausibly reads to check currency.
|
||
⚠️ **It was missing from this list and carried a stale date in the present
|
||
tense** (`adversarial-reviewer`, round 2).
|
||
⚠️ **NO RUNNING TALLY OF WHAT WAITS ON POUYA IS KEPT HERE ANY MORE.** This
|
||
line said "the only cutover item", was corrected to "one of three", and was
|
||
then rewritten as "the one item still waiting… is his own read-through" **in
|
||
the same change set that opened Q64** — wrong three times, in the same
|
||
direction each time: a count written while the change set was still adding
|
||
items. **The checkbox column is the tally.** Unticked items above are what
|
||
waits on him.
|
||
- [x] ✅ **THE SEVEN VOLATILE `docs/reference/` EXTRACTS RE-CHECKED — `AGENTS.md`
|
||
§12 R18, whose trigger is the same "before any cutover" event R10 uses.** ⚠️ **THIS ITEM DID NOT EXIST UNTIL 2026-09-02 AND THAT
|
||
WAS THE DEFECT**: R18 names a cutover as its trigger and the cutover
|
||
checklist carried no item to fire it, which is Q22's shape — a documented
|
||
control living somewhere it cannot run. R10 was on this list; R18 was not.
|
||
**Re-checked 2026-09-01 by Pouya. All seven hold and no shipped sentence
|
||
changed.** Three were re-verified against a source and four are held
|
||
unchanged on a cadence judgement rather than a fresh retrieval — the
|
||
distinction is stamped per limb in `AGENTS.md` §12 R18 and in each
|
||
extract, because "re-checked" and "not looked at, judged slow" are not the
|
||
same stamp and collapsing them is how OCNI lapsed.
|
||
**Re-fire this on the next republish that turns on one of them**, and note
|
||
that the fastest mover — Bill C-36 — needs no page edit while it sits at
|
||
second reading and needs one the day it does not.
|
||
- [ ] **The OBA sections stay listed; the LSO stays out** — a check that nobody
|
||
has tidied the two into one list, not an open question. `AGENTS.md` **Q51
|
||
answered 2026-08-28**: the Law Society is the **regulator**, so membership
|
||
*is* licensure; the OBA is a **voluntary association**, which admits
|
||
members it does not license. Structural, and independent of eligibility
|
||
details — which is what made the question unanswerable inside this repo
|
||
before the ruling. **R1 is still live**: same page, same subject, different
|
||
question
|
||
- [ ] No `TODO(pouya)` remains in any shipped page
|
||
- [ ] No matter counts, rates, dollar figures, or testimonials anywhere
|
||
- [ ] Q.Arb described as **held** everywhere it appears — `Q.Arb (ADRIC /
|
||
ADRIO)`, no acquisition date. **Every stage form is barred**: "commenced",
|
||
"in progress", "pathway", "not yet" (amended 2026-08-29). `npm run
|
||
check:claims` enforces the **stage words** and a **date near the
|
||
designation**, on `dist/`, in both deploy paths. It cannot catch a stage
|
||
expressed without naming the designation; that gap is `claims-auditor`'s to
|
||
close, and it is stated in the pattern itself
|
||
- [ ] **C.Med-Arb appears nowhere in `dist/`** — struck entirely 2026-08-29
|
||
- [ ] `/fees/` carries the rates confirmed in D14 and `docs/07-fees.md`, or the page does not ship. **And the reservation sentence sits adjacent to the overtime row** — Q59, ruled 2026-08-31: a reader who takes the $500 and skips *"a full day reserves the day; half-day overtime is subject to availability"* has read a different offer, and the arithmetic then makes the full-day rate look strictly worse
|
||
- [ ] Privacy policy matches the backend as actually built. **Three of its statements are derived and cannot drift** — the collected-data list from `INTAKE_FIELDS`, the retention period from the handler's own figure, the analytics paragraph from `ANALYTICS.installed`. **If analytics are installed, that flag flips and the policy's last-updated date moves on the same build**; a policy naming a processor that processes nothing is a false disclosure
|
||
- [ ] **`/insights/` — check which state it is in.** While no article is published the page emits `noindex` (derived from the collection) and `SiteHeader` keeps Insights out of the primary nav until **two** are live. Both clear themselves on publication. D9 means the flip is Pouya's: `src/content.config.ts` refuses `draft: false` without `reviewedByPouya: true`
|
||
- [ ] ⚠️ **AND PUBLISHING THE SECOND ARTICLE IS GATED — `AGENTS.md` R20, ruled
|
||
2026-09-01.** The flip that makes `/insights/` indexable is the same flip that
|
||
adds the **seventh nav item**, and with seven items under fallback font
|
||
metrics the masthead measures 141 px across 1056–1091 px: a 60 px layout shift
|
||
on every page and 44 px of `#main` behind the sticky header after the skip
|
||
link, both at the **default** text size. **No seventh nav item ships until
|
||
that is fixed**, so the fix is a prerequisite of article #2 — not a follow-up
|
||
to it. The measurements and the two candidate fixes are on the
|
||
seventh-nav-item item under **Technical** below
|
||
- [ ] **The five drafted articles are `draft: true` and therefore invisible to `check:claims`**, which reads `dist/`. They were checked once, on 2026-08-31, by temporarily publishing all five and running it — clean on 27 pages. **Re-run it that way before any of them publishes**, because a draft that changes in between is unchecked
|
||
|
||
**Technical**
|
||
- [ ] **`npm run lighthouse` on the full built site**, from a machine with
|
||
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).
|
||
✅ **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
|
||
- [ ] Sitemap generated and correct; `robots.txt` served, not 403
|
||
- [ ] Rich Results Test passes; OG previews render in LinkedIn and Slack
|
||
- [ ] **`npm run og:proof`** — every page's `og:image` resolves to a file in
|
||
`dist/`, and every generated card's headline is its page's own `<h1>`.
|
||
R15 is discharged (step 7b) so this is a regression check rather than a
|
||
blocker on the interim; it stays on the list because a broken preview image
|
||
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 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)
|
||
- [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'`,
|
||
so there is no cross-origin POST, so the endpoint id stays out of the
|
||
HTML, and so submitting the form from a laptop does nothing. Without
|
||
this behaviour the form 404s.
|
||
2. **Deploy `backend/intake/handler.mjs` + `backend/intake/fields.mjs`**,
|
||
replacing the hand-built `adr-intake-handler` §7 records. It needs
|
||
**SIX** variables — `INTAKE_TABLE`, `SITE_ORIGIN`, `NOTIFY_TO`,
|
||
`MAIL_FROM`, `RESPONSE_TIME` and `NO_RETAINER_NOTICE`; it throws at cold
|
||
start on any missing one, on purpose. ⚠️ **This list said five until
|
||
2026-08-31 and the handler required six.** `NO_RETAINER_NOTICE` became a
|
||
`requireEnv` in the same change set and was added to no document, so an
|
||
operator working this list would have set five, and the Lambda would
|
||
have thrown on **every** invocation — API Gateway answering 5xx and every
|
||
inquiry from counsel lost from the moment `/api/*` was wired. Loud in
|
||
CloudWatch, silent to Pouya, who has no reason to submit his own form.
|
||
Found by `adversarial-reviewer` round 2.
|
||
**Two of the six must be verbatim from `src/data/site.ts`:**
|
||
`RESPONSE_TIME` from `CONTACT.responseTime` and `NO_RETAINER_NOTICE`
|
||
from `NO_RETAINER_NOTICE`. Both are published commitments — the first is
|
||
§4/Q27's two-business-day response, the second is the no-retainer notice
|
||
`docs/01` §`/contact/` requires, **including its fourth clause about
|
||
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. **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**.
|
||
⚠️ **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
|
||
by the database rather than by someone remembering to do it"* after 24
|
||
months. The handler writes the `ttl` attribute; **TTL must also be enabled
|
||
on the table** — ⚠️ **§7 records whether it is, and it has not always
|
||
been**, which is why this item cannot be ticked from the handler code: the
|
||
handler only writes the attribute. On a privacy policy an unbacked
|
||
mechanism is a false statement to the public. `aws dynamodb
|
||
describe-time-to-live --table-name <the table §7 records>` and confirm
|
||
`TimeToLiveStatus: ENABLED` — then, because **`ENABLED` proves the setting
|
||
and not the behaviour**, write a record with a near-future `ttl` and
|
||
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
|
||
- [ ] **Both POST-redirect-GET landing pages reachable and `noindex`** —
|
||
`/contact/received/` and `/contact/could-not-send/`. Both are excluded from
|
||
the sitemap in `astro.config.mjs`
|
||
- [ ] **Regenerate and re-commit the bio PDF** — `npm run build && npm run bio:pdf`.
|
||
Nothing in the build does this, so a change to `/bio/`, to §4, to the rate
|
||
card or to the print styles leaves the committed PDF stale. It asserts one
|
||
page and writes nothing if the count is wrong (R16). ⚠️ **It is not
|
||
byte-reproducible** — Chrome stamps a `/CreationDate`, so two runs of
|
||
identical content differ in digest and every re-render is a binary diff.
|
||
Re-commit it when something actually changed, and say what in the message
|
||
- [ ] **`X-Robots-Tag: noindex` on `*.pdf`** — ⚠️ **WRITTEN 2026-09-03, NOT YET
|
||
APPLIED. It needs a `configure.mjs --apply` run, not a deploy.**
|
||
`infra/cloudfront/configure.mjs` §4 creates a response-headers policy
|
||
`adr-sml-pdf-noindex` and a `*.pdf` cache behaviour carrying it. ⚠️ **S3
|
||
OBJECT METADATA CANNOT DO THIS, which is the natural first reach and was
|
||
the instruction this was implemented against.** `aws s3 sync --metadata`
|
||
writes USER metadata, which S3 returns as `x-amz-meta-x-robots-tag` — a
|
||
header no crawler reads. Only a literal `X-Robots-Tag` counts and S3's REST
|
||
endpoint will not emit one, so the mechanism is the response-headers policy
|
||
this line has specified from the start. ⚠️ **THE POLICY CLONES THE
|
||
SECURITY HEADERS AT RUN TIME RATHER THAN RETYPING THEM** — a
|
||
response-headers policy REPLACES rather than merges, and all five
|
||
(`strict-transport-security`, `x-content-type-options`, `x-frame-options`,
|
||
`x-xss-protection`, `referrer-policy`) were measured arriving on the live
|
||
PDF 2026-09-03, so a hand-written policy would have silently dropped them.
|
||
Verify after applying with `docs/09` Part 3's header block, which counts
|
||
each of the six separately — an alternation `grep` exits 0 on any one match
|
||
and would call a partial clone a pass. **This is the PDF half of a decision
|
||
already taken for the page.**
|
||
`/bio/` is `noindex` and excluded from the sitemap because it is a condensed
|
||
duplicate of `/about/` and `/fees/`, and *"two URLs competing on the same
|
||
content is the one thing `docs/04` is most concerned with."* The committed
|
||
PDF is a rendering of that same page, it is linked from `/about/` which is
|
||
`index,follow`, `robots.txt` allows everything, and **Google indexes PDFs** —
|
||
so the duplicate the page-level `noindex` prevents arrives as a file
|
||
instead. A `Disallow` will not do it: a blocked URL can still be listed.
|
||
Found by `adversarial-reviewer`, 2026-08-31
|
||
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately
|
||
- [x] ⚠️ **THE HEADSHOT SHIPS SOFT, AND IT IS A DEFERRED DECISION RATHER THAN A
|
||
DEFECT — Pouya, 2026-09-03. NO CHANGE.** ✅ **Ticked because the decision is
|
||
taken, not because anything was done** — an item recording a decision *not*
|
||
to act can never be ticked on completion, and leaving it open would stop
|
||
this checklist ever reading clean. He raised it on the live site;
|
||
measured 2026-09-03 and the cause is not the master and not the delivery.
|
||
**The master is fine** (1600×1600, 4:4:4, real detail at full size — a
|
||
1/2-scale round trip is visibly softer than it is) and **the srcset ladder
|
||
is correct** (9 device profiles in Chrome: ratios **1.00–1.21, no
|
||
upscaling anywhere**, `sizes` 476 px matching the measured rendered width
|
||
exactly). **The cause is that Astro passes no `quality`, so sharp's
|
||
per-format defaults apply — AVIF 50, WebP 80, JPEG 80 — and
|
||
`formats={['avif','webp']}` puts AVIF first, so every modern browser gets
|
||
the quality-50 encode.** At 960 px it retains **55%** of the reference's
|
||
high-frequency energy; WebP retains 85% and JPEG 95%, and neither is
|
||
served. Sweep at 960 px: q60 → 76% at 33 KB, q65 → 80% at 39 KB, **q70 →
|
||
90% at 51 KB**, q80 → 94% at 77 KB, against today's **21 KB**.
|
||
⚠️ **IT IS DEFERRED BECAUSE IT IS A REAL TRADE, NOT BECAUSE IT IS SMALL:**
|
||
the portrait is the LCP element from 768 px up, and **§7's Lighthouse row
|
||
records `/` at LCP 2.03 s** `[verified 2026-08-31 — lcp-breakdown-insight]`,
|
||
so +30 KB needs a fresh `npm run lighthouse` before it ships.
|
||
⚠️ **THAT IS NOT A MEASUREMENT AGAINST `docs/04`'s BUDGET AND MUST NOT BE
|
||
READ AS ONE.** `docs/04`'s < 2.0 s is a **Slow 4G field** figure; 2.03 s is
|
||
a local run under loopback throttling, which is why `npm run lighthouse`
|
||
*reports* LCP and does not assert it (§7). The two are close enough to look
|
||
comparable and are not the same measurement — so this is a reason to
|
||
re-measure before adding bytes, not a recorded budget breach. **Three call sites would be affected
|
||
and none sets `quality`** — `src/pages/index.astro`, `src/pages/about.astro`
|
||
and `src/components/InfinityMark.astro`; the mark is line art and would
|
||
want a different number from the portrait, so this is not one edit.
|
||
- [x] ✅ **Favicon set complete, and REGENERATED 2026-09-02 — it had shipped with
|
||
no transparency at all.** Pouya's read-through finding. All three frames
|
||
(16/32/48) declared a 32-bit alpha channel and then carried `alpha = 255`
|
||
on every pixel, the ground opaque cream — so the tab icon showed as a cream
|
||
rectangle on any dark tab strip. `public/favicon.ico` is now transparent,
|
||
regenerated by `npm run icons` from the committed master and verified
|
||
programmatically and by eye, on dark grounds and light.
|
||
⚠️ **`public/apple-touch-icon.png` STAYS OPAQUE CREAM AND MUST NOT BE
|
||
"FIXED" TO MATCH.** The reason is a platform behaviour — iOS composites a
|
||
transparent touch icon onto black — **stated by Pouya on 2026-09-02 and not
|
||
re-tested on a handset**; the item directly below is where it would be. The
|
||
touch icon is byte-identical across this change.
|
||
⚠️ **AND THE CHANGE IS NOT FREE ON DARK.** The maroon half of the ribbon
|
||
effectively drops out against a dark tab strip; the champagne half carries
|
||
the mark. **The figures are deliberately NOT repeated here** — they live in
|
||
`docs/reference/brand-assets.md` §The icon set, with the method, and a copy
|
||
on this page had already gone stale within a day by quoting the 32 px row
|
||
as if it were the general case (`adversarial-reviewer`, round 2). Read them
|
||
there.
|
||
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
|
||
- [ ] Tested at 320 px and at 200% zoom
|
||
- [x] ✅ **THE 200%-TEXT NAV OVERFLOW IS FIXED, 2026-09-01 — THIS ITEM IS
|
||
DISCHARGED.** Pouya reopened the step-1 header decision and ruled *fix, do
|
||
not accept*. `flex-wrap: nowrap` is gone from `.header-inner` (the necessary
|
||
and sufficient one, measured) and from `.nav-list`, and the masthead is
|
||
sticky only while it is one row — a saturating `clamp()` gate in two bands,
|
||
66 rem and 76 rem. Measured across 22 pages × 16 widths, before and after on
|
||
the same grid: **root-style 32 px went from 175 of 352 page-widths over to
|
||
0**; minimum font size from 219 to 88, all of the remainder being the footer;
|
||
default font size stays 0. Nav items and CTA on-screen in **1408 of 1408**
|
||
measurements. The shipped rendering at the default size is
|
||
**geometry-identical — 0 differences on 352 page-widths**, header 81.00 px,
|
||
CTA on the content edge, with six nav items and with a seventh.
|
||
⚠️ **AND IT WAS VERIFIED ACROSS THE INTERMEDIATE ROOT SIZES, WHICH IS WHAT
|
||
ROUND 1 DID NOT DO.** WCAG 1.4.4 is a requirement up TO 200 %, not AT 200 %:
|
||
round 1 sampled root 16 and root 32 only, both clean, and shipped a gate that
|
||
left `#main` behind the header at **20 of 300** grid points across roots
|
||
18–30. **And round 2 found the gate was one-sided**: Chrome's "Font size" has
|
||
five presets (9 / 12 / 16 / 20 / 24 px) and only 16 had been swept, so at
|
||
**9 px the masthead wrapped for the opposite reason** — the `80rem` content
|
||
cap shrinks while the header's px minimums do not — leaving **65 px of `#main`
|
||
behind the header** on 21 of 22 pages at every width. A second gate term
|
||
(`1rem - 16px`) closes it, and closes a **pre-existing 12 px** shortfall on
|
||
all 22 pages at that setting as well. Now **0 failing across 762 points**:
|
||
roots 9–15 (70), roots 16–32 (300), the band seam (198), both thresholds
|
||
(144) and all five default-font-size presets (50). Both findings by
|
||
`adversarial-reviewer`; `docs/02` §Reflow carries all of it
|
||
- [x] ✅ **THE FOOTER REFLOW RESIDUAL IS FIXED, 2026-09-01.** Document overflow of
|
||
**14 px at 320, 38 px at 1024, 30 px at 1056 and 19 px at 1100 CSS px** on all
|
||
22 pages under Chrome's minimum-font-size setting, 0 from 1200 px up. One
|
||
cause at all four: `info@smlcompany.ca` has no break opportunity, so its
|
||
**min-content width is 310 px**. One declaration closes it —
|
||
`.footer-contact a[href^='mailto:'] { overflow-wrap: anywhere }` — measured
|
||
**88 of 352 → 0 of 352**, the other three enlargement methods unchanged at 0
|
||
of 352, and the normal-settings identity check at **0 differences on 352
|
||
page-widths across 8 metrics**. It also closed **57 cases of the link
|
||
overrunning its own box** at root style 26–32 px that the document-level
|
||
table reports as clean. ⚠️ **The earlier note here said the 320 px half "is a
|
||
column-sizing question and is not the same fix" — that was wrong**; the
|
||
column sizing at 320 px *is* that same 310 px min-content, arriving through
|
||
`.footer-grid`'s `auto` track instead of through text spilling out of a
|
||
`minmax(0, 1fr)` one. `docs/02` §Reflow carries both mechanisms, the negative
|
||
control proving `break-word` would not have worked, and the candidates
|
||
declined as broader than the cause
|
||
|
||
- [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
|
||
- [x] ✅ **THE `/med-arb/` GLOSS — RULED AND STRUCK, 2026-09-02.** Pouya:
|
||
*"strike the gloss sentence. The ADRIC-sourced material carries the page.
|
||
No replacement claim, no competence claim."* Applied: the definitional
|
||
gloss is struck. ⚠️ **AND SO IS THE DESIGNATIONS CLAUSE THAT THE FIRST
|
||
VERSION OF THIS ITEM SAID WAS "NOW THE LEDE" — corrected 2026-09-02,
|
||
`adversarial-reviewer` round 2, which found this line describing a page
|
||
state the same change set had deleted.** Round 1 of the closing claims
|
||
pass found that a bare `DESIGNATIONS_HELD_LINE` sitting directly beneath
|
||
§Rules' quotation of ADRIC requiring *"a high level of practitioner
|
||
competence"* read as meeting that bar. **§Why this practice now carries an
|
||
acceptance sentence and no credential claim at all**; `/about/` publishes
|
||
the designations and the JSON-LD carries them. The constraint is recorded
|
||
in the section's own comment in `src/pages/med-arb.astro`. **The original
|
||
finding is kept below unstruck**, because it is the reasoning behind a
|
||
paragraph that is deliberately thin, and a later reader who cannot see why
|
||
will fill it.
|
||
`claims-auditor`, D20 pass, finding 1: §Why this practice said *"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
|
||
overflow at any width, so it fails no success criterion and is cosmetic
|
||
today — filed because **7.7 px is one label rename away from two columns
|
||
colliding**, and nothing in the build measures it. `.footer-col a`
|
||
"Construction & Infrastructure" overruns its 176 px box by **24 px** at
|
||
640 px and by 4 px at 700 px (clearance 27.7 px); "Shareholder & Family
|
||
Business" by 2 px at 640 px, and — at widths that 12-column grid steps over —
|
||
**17 px at 660 px and 11 px at 680 px**. Clean at every other width sampled,
|
||
including 768 and 834 px; `minimumFontSize=32`, 22 pages × 12 widths = 264
|
||
rows. **A fourth case of the same family: `nav.crumbs > ol` on
|
||
`/practice/construction/` @320 px overruns its own 272 px box by 7 px**, also
|
||
with zero document overflow. **Rule on both together** — if
|
||
`.footer-col a { overflow-wrap: anywhere }` is ruled in, the same call covers
|
||
`.crumbs`; if it is declined, decline both and the record is complete.
|
||
**`.footer-col a { overflow-wrap: anywhere }` is measured to close it and to
|
||
be inert at normal settings**, and is not applied because it changes how six
|
||
practice-area names break on a marketing surface under enlargement — a
|
||
copy-adjacent call, not a technical one. ⚠️ **And it falsified the reason
|
||
given for scoping the email fix to the address** (*"every other footer string
|
||
has spaces"*): min-content is set by the longest **word**, not by whether a
|
||
space exists. Corrected in `docs/02` §Reflow. Raised by
|
||
`adversarial-reviewer`, 2026-09-01
|
||
|
||
- [ ] ⚠️ **THE SEVENTH NAV ITEM PLUS FALLBACK FONT METRICS — A LAYOUT SHIFT *AND*
|
||
A SKIP-LINK FAILURE, AT THE DEFAULT TEXT SIZE, ARMING ITSELF WHEN THE SECOND
|
||
INSIGHTS ARTICLE PUBLISHES.** No reader setting is involved: with every
|
||
`.woff2` blocked — what a reader on the Slow 4G profile `docs/04` budgets
|
||
against sees first, since the fonts are `font-display: swap` — and a seventh
|
||
nav item present, the header measures **141 px at every width from 1056 to
|
||
1091 px** (a contiguous **36 px** band) and 81 px from 1092 px up; with the
|
||
webfonts loaded it is 81 px throughout. Two consequences: **(i)** a **60 px
|
||
collapse** when Geist swaps in, on all 22 pages, against the CLS < 0.05
|
||
budget — *larger, not new*, since the previous build shifts ~5.6 px in the
|
||
same band; and **(ii) 44 px of `#main` behind the sticky header** after "Skip
|
||
to content", because 141 px exceeds the 97 px `scroll-padding-top` — **this
|
||
half IS new**, the previous build's 86.97 px stayed under 97 px and covered 0.
|
||
**Latent, not shipped:** six items never wrap, and `showInsights` turns the
|
||
seventh on at two published articles. Two candidate fixes, both Pouya's call
|
||
because both are outside "the header and its consumers": raise the desktop
|
||
breakpoint past 1091 px, which changes the normal-settings layout in that
|
||
band, or give Geist a metric-matched `size-adjust` fallback. Raised by
|
||
`adversarial-reviewer`, 2026-09-01; `docs/02` §Reflow carries the measurements.
|
||
|
||
⚠️ **THIS IS NOT A CUTOVER BLOCKER AND IT IS NOT TICKABLE HERE — IT IS A HARD
|
||
PRE-PUBLICATION GATE. Pouya's ruling, 2026-09-01: no seventh nav item ships
|
||
until the fallback-metrics defect is fixed**, which makes fixing it a
|
||
**prerequisite of publishing the second Insights article**, since that is what
|
||
adds the item. It is left unchecked on purpose: a checklist that fires once, at
|
||
cutover, cannot discharge a defect that arms itself later on an editorial
|
||
decision. ⚠️ **AND THE GATE IS A BUILD FAILURE, NOT THIS CHECKLIST AND NOT A
|
||
COMMENT.** `SiteHeader.astro` throws when `published.length >= 2`, with the
|
||
measurements and the instruction in the message, so it fires on both deploy
|
||
paths on the machine of whoever publishes. **Proven rather than assumed:** two
|
||
articles were temporarily flipped to `draft: false` and `npm run build` exited
|
||
**1** naming R20; the files were restored and the restoration verified by
|
||
`git diff --exit-code` and an unchanged `dist` digest. It was first written as
|
||
three prose cross-references and **did not gate** — the build and all five
|
||
checks passed while both defects shipped. **Owner: `AGENTS.md` R20**
|
||
|
||
**Infrastructure**
|
||
- [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`, 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**
|
||
- [ ] Sitemap submitted to Google Search Console and Bing Webmaster Tools
|
||
- [ ] Live site fetched as an anonymous crawler to confirm indexable content
|
||
- [ ] LinkedIn profile and ADRIC/ADRIO listings updated to point here
|
||
- [ ] Archive the old single-file build to `_archive/` — do not delete it
|
||
- [ ] `AGENTS.md` Change Log entry recording the cutover
|