Build and deploy / build-and-deploy (push) Failing after 4s
Pouya's rulings of 2026-09-03 (the last two D20 findings) and 2026-09-04 (the spam observation and four mitigations), in one change set. D20 finding 10 — med-arb is billed BY PHASE, each phase at the rates already published, so /fees/'s "Every figure is on this page" is true as written rather than narrowed. FEES.medArb is the single source; docs/07 §Med-arb carries the rule INTERIM against R5, and R5 now carries it back, because a derived price moves silently when a rate moves. D20 finding 13 — conduct undertaking (g), attested 2026-09-03, published as his wording verbatim on /legal/privacy/ and /contact/. The clause that raised the finding promised to DISCLOSE a conflicts check's outcome, which the attestation does not cover; it is struck. D20 now partitions 17 fixed / 2 refuted / 1 owed. Spam, 2026-09-04 — recorded in docs/05 §Observed abuse with the date and signature. A second honeypot (a decoy checkbox, own class, `hidden`, a label that tells a human not to tick it) and scoring that LABELS and never rejects: nothing is dropped, nothing new is stored, and only the operator notification changes. Q65 opens the WAF cost call. The timing floor could not be built: there is no timing check and never has been. docs/05 carries it struck, and every mechanism that would give a real per-visitor clock breaks zero-JS, handler-and-form-only, or D1. Q66. configure.mjs gains section 5 — a custom origin request policy forwarding CloudFront-Viewer-Address on /api/*. Written, dry-run against the live distribution, NOT applied. It reads the handler's own header reads and refuses to run if the whitelist omits one. And reading the live account to do it found four AGENTS.md §7 rows saying the intake backend was undeployed, two days after it went live — corrected against get-function-configuration, get-routes, get-stage, get-policy and the deployed zip, which was downloaded and read. Review: adversarial-reviewer only (claims-auditor is D20's cutover pass and has run). Round 1 five lenses, 56 findings, 7 blocking, 4 refuted by an independent refuter; round 2 four lenses, 36 findings, 33 of them defects in round 1's own repairs. Stopped at two per D19. Gates, exit status read for each: check 0 · build 0 (23 pages) · check:claims 0 · check:intake 0 · og:proof 0 · lint 0 · spam-score.test 39/39 with 6/6 mutations killed · router.test 30/30 · minifier grep 1 (clean) · lighthouse 0, no category below 95 · configure.mjs dry run 0, nothing written. Nothing deployed and nothing applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
1543 lines
106 KiB
Markdown
1543 lines
106 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; 17 ARE
|
||
> NOW FIXED, 2 REFUTED, 1 OWED — updated 2026-09-04, and the three numbers
|
||
> partition the twenty.** **Findings 10 and 13 were both RULED by Pouya on
|
||
> 2026-09-03 and are closed** (see below); **the one remaining is 11**, which
|
||
> is ruled and waiting on Q60's observation window rather than on a copy
|
||
> change. ⚠️ **THIS ITEM STAYS UNTICKED, AND NOT BECAUSE A CLAIM IS WRONG.**
|
||
> What is outstanding is a *confirmation that a record was seen to vanish*,
|
||
> not a sentence anyone disputes — tick it when Q60 closes. The previous
|
||
> tally follows. 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) ✅ RULED AND CLOSED 2026-09-03 — PRICED, NOT NARROWED.** Med-arb is
|
||
> billed **by phase**: the mediation phase at the published mediation rates,
|
||
> the arbitration phase (if it is reached) at the published arbitration
|
||
> rates; additional-party and cancellation terms apply to each phase as they
|
||
> apply to that process on its own; **there is no separate med-arb fee.**
|
||
> Pouya took the more expensive of the two fixes — the promise is unchanged
|
||
> and is now true, rather than being trimmed to fit. `FEES.medArb` is the
|
||
> single source, `/fees/` §4 renders it, `docs/07` §Med-arb carries the rule
|
||
> **marked INTERIM, set 2026-09-03, reviewed at §12 R5**. ⚠️ **It carries NO
|
||
> figure of its own and must not be given one** — a fourth price for a
|
||
> process priced twice would disagree with one of them. ⚠️ **AND BECAUSE IT
|
||
> IS DERIVED, MOVING A RATE AT R5 MOVES IT SILENTLY**, with no diff on the
|
||
> med-arb rule; R5 carries that. Verified by reading the built page.
|
||
> *(The original wording of this item follows.)* `/fees/`'s *"Every figure is
|
||
> on this page"* against §4's **Med-Arb** offering, which `docs/07-fees.md`
|
||
> priced 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) ✅ RULED AND CLOSED 2026-09-03 — HE SAID IT, AND THE PAGE SAID MORE
|
||
> THAN HE SAID.** Pouya attested that he runs a conflicts check on every
|
||
> inquiry before engaging. §4 gains **conduct undertaking (g)**, `[attested
|
||
> 2026-09-03]`, and `CONDUCT_UNDERTAKINGS` now holds **seven** strings, not
|
||
> six. ⚠️ **THE ATTESTATION DOES NOT COVER THE SENTENCE THAT RAISED THE
|
||
> FINDING.** Finding 13 quoted a promise to **disclose the outcome** —
|
||
> *"I will tell you what its outcome was"* — which is a different commitment
|
||
> from running the check, and his instruction was that the page *"may say no
|
||
> more than that attestation"*. So the clause is **struck**; the page now
|
||
> reads *"it does not undo a conflicts check that has already been run"*, and
|
||
> the undertaking itself ships through `<Undertaking>` in §Information about
|
||
> other people, replacing a hand-typed near-equivalent. ⚠️ **IT DOES NOT
|
||
> REVERSE Q57**, which refused an undertaking about what happens when a check
|
||
> turns something up; that one is still refused. *(The original wording of
|
||
> this item follows.)* **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.
|
||
|
||
⚠️ **THE SECOND CLASS WAS REFUTED — 2026-09-03, AND AGAIN BY DIRECT
|
||
MEASUREMENT 2026-09-04.** The backend **is** deployed; the 403 that founded
|
||
those two findings was a bare POST with no `Origin` header, which the
|
||
handler rejects by design. `docs/09` §7.1 run correctly returns **303**, and
|
||
on 2026-09-04 the function's own configuration and its deployed artefact
|
||
were read: `handler.handler`, six environment variables, both source files
|
||
byte-identical to commit `02739ad`. The paragraph above is preserved as what the pass
|
||
found; **only findings 10, 11 and 13 outlived it, and 10 and 13 are now
|
||
ruled** — see item 3 of the callout near the top of this file, which is the
|
||
current tally and this is not.
|
||
|
||
⚠️ **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 END-TO-END SUBMISSION TEST IS STILL OWED — `docs/09` §7.2.**
|
||
The route answers (§7.1 returns **303**), which is a different fact:
|
||
**§7.1 stops before any DynamoDB write and before any SES send, by
|
||
design.** What is unproven is that a real submission stores a record and
|
||
that **both** emails arrive — the notification and the inquirer's
|
||
confirmation, D18's whole point. ⚠️ **THIS ITEM DID NOT EXIST FOR ONE
|
||
ROUND.** Ticking "the intake form works" below removed the only unticked
|
||
line covering §7.2, so the one genuinely outstanding intake verification
|
||
lived inside an item marked done. Pouya has this in progress; §7.2 also
|
||
says to read `sourceIp` against `checkip` and to delete the test record
|
||
- [x] ✅ **THE INTAKE FORM WORKS — all three happened at cutover, 2026-09-02**,
|
||
and every one was re-verified against the live account on 2026-09-04:
|
||
`handler.handler` with six variables, one route `POST /api/intake`, and the
|
||
`/api/*` behaviour on the distribution. `docs/09` §7.1 returns **303**.
|
||
⚠️ **THIS ITEM READ "THE INTAKE FORM DOES NOT WORK YET" UNTIL 2026-09-04**,
|
||
unticked, near the top of the list an operator follows — the same staleness
|
||
as §7's two intake rows and from the same cause: the list was written under
|
||
D11 and never re-read after Part 5 ran. **What is still owed is §7.2**, the
|
||
real-submission test that proves both emails arrive; §7.1 stops before any
|
||
write and any send by design. **The original text follows, because the two
|
||
things it records are what made this hard and they are still true of the
|
||
code.**
|
||
**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
|
||
- [ ] 🛑 **THE SPAM MITIGATIONS ARE HALF-SHIPPED BY A DEPLOY, AND THE HALF THAT
|
||
MATTERS IS NOT — 2026-09-04.** `scripts/deploy-local.sh` does an S3 sync
|
||
and a CloudFront invalidation and **nothing else**: it contains no Lambda
|
||
step `[verified 2026-09-04 — read]`. So `npm run deploy` ships the second
|
||
honeypot, because that is markup in `dist/contact/index.html`, and ships
|
||
**neither the check that reads it nor the spam scoring**, because both are
|
||
in `backend/intake/`. **The handler needs `docs/09` Part 5** — 5.1, 5.2,
|
||
5.3, then **5.4, and 5.5 if 5.4 fires**, which it did at cutover.
|
||
⚠️ **`spam-score.mjs` IS A THIRD FILE IN THE ZIP, AND SINCE 2026-09-04 BOTH
|
||
5.1 AND 5.5 DERIVE THE LIST FROM THE DIRECTORY RATHER THAN NAMING IT** —
|
||
they were hand-typed in both, with nothing checking they agreed, until the
|
||
review found it. A zip missing a module fails at cold start with
|
||
`Runtime.ImportModuleError` and every submission then 500s. Run
|
||
`node backend/intake/spam-score.test.mjs` (**39 of 39**) before packaging.
|
||
**There is no ordering hazard either way**: a form ahead of the handler
|
||
renders a field nothing checks, and a handler ahead of the form checks a
|
||
field nothing renders. Both are inert, so the only cost of doing one and
|
||
not the other is that the mitigation is not yet in force
|
||
- [ ] **`CloudFront-Viewer-Address` forwarded on `/api/*`** — ⚠️ **WRITTEN
|
||
2026-09-04, NOT YET APPLIED. Same `configure.mjs --apply` run as the item
|
||
below; not a deploy.** `infra/cloudfront/configure.mjs` §5 creates a custom
|
||
origin request policy `adr-sml-api-viewer-address` and points the `/api/*`
|
||
behaviour at it. Pouya's ruling of 2026-09-04, after the first real spam:
|
||
forward it **so per-IP measures become possible later — measured, not yet
|
||
acted on**. 🛑 **THIS IS THE ONLY CHANGE IN `configure.mjs` THAT REPLACES
|
||
RATHER THAN ADDS, AND IT REPLACES THE POLICY ON THE PATH THE INTAKE FORM
|
||
POSTS TO.** AWS has no behaviour meaning *"all viewer headers except Host,
|
||
plus a CloudFront header"* — `allExcept` can only subtract, and
|
||
`allViewerAndWhitelistCloudFront` forwards `Host` and 403s at API Gateway
|
||
(derived from the API's own enum, 2026-09-04). A **whitelist** is forced,
|
||
so the five listed headers are load-bearing: the handler's four `headerOf`
|
||
reads plus the new one. **A missing header does not error — every
|
||
submission would validate short and land on `/contact/could-not-send/`,
|
||
which reads as the inquirer's own browser misbehaving.** So `docs/09`
|
||
Part 3's `303` probe and its one-field rollback are **mandatory** after
|
||
this, not advisory. ⚠️ **AND THE HANDLER STILL STORES THE EDGE ADDRESS.**
|
||
Forwarding is infrastructure; **storing** the viewer address is a
|
||
`/legal/privacy/` change governed by `docs/09` §7.2's decision table, and
|
||
it is deliberately not made here
|
||
- [ ] **`X-Robots-Tag: noindex` on `*.pdf`** — ⚠️ **WRITTEN 2026-09-03, NOT YET
|
||
APPLIED. It needs a `configure.mjs --apply` run, not a deploy** — the same
|
||
run as the item above; one `--apply` does both.
|
||
`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
|