fix: refute (ar)'s intake finding; fix the D20 gloss class; add X-Robots-Tag on *.pdf
Build and deploy / build-and-deploy (push) Failing after 4s

Pouya's rulings of 2026-09-03, in five parts.

1. THE INTAKE FORM IS NOT BROKEN. (ar) was wrong. docs/09 §7.1 verbatim —
POST /api/intake with an Origin header — returns 303 to /contact/could-not-send/
with access-control-allow-origin echoed; the same probe without Origin returns
403. A bare POST 403s BY DESIGN and §7.1 says so three lines below the probe it
prescribes: "403 means the Origin header did not arrive". The earlier finding
read a status code without reading the document that defines it. Second time in
two days. CLAUDE.md's instrument list goes eight to nine. D20 findings 12 and 19
fall with it; §7.2 (that both emails arrive) is still owed.

The correction is APPENDED as entry (as); (ar) stands unedited.

2. The privacy retention comment was stale, not a defect — superseded by his
decision to publish and confirm after launch, reading from 2026-09-04. Reworded;
the TODO(pouya) came off with the gate it enforced. The mechanism finding
survives: it was a JSX comment, stripped by Astro, so no build or deploy path
could see it. A publication gate that lives only in a stripped comment is not a
gate. §9 Q60 corrected.

3. The gloss class is fixed — 15 of the 20 D20 findings, 14 distinct edits across
9 files, under the rule "the gloss may say no more than the extract says; no new
claims, no new sources". Swept three unpublished insights drafts too, and
corrected the wrong CAA attribution at its source in docs/reference/, which is
where a fixed page re-seeds. /bio/ changed, so the committed PDF is regenerated
(89,549 B, 1 page asserted). Three findings outstanding: 10 needs a ruling, 11 is
ruled and owed via Q60, 13 needs him to have said it. R1 is not one of the twenty.

4. X-Robots-Tag cannot be done with S3 object metadata — --metadata writes user
metadata, returned as x-amz-meta-x-robots-tag, which no crawler reads. Built as
the CloudFront response-headers policy docs/06 has specified all along:
configure.mjs section 4. It needs a --apply run, not a deploy. The policy is
cloned from whatever is attached at run time and reconciled on every run, because
a response-headers policy replaces rather than merges.

5. Headshot deferred as an open non-defect. The master and the srcset ladder are
both fine; Astro passes no quality, so AVIF encodes at sharp's default 50 and is
served first.

Two review rounds, 29 findings, all resolved, none declined; stopped at two per
D19. NINE of round 2's fourteen were defects in round 1's own repairs — including
a fix that harmonised both /fees/ rows onto wording that was itself unregistered,
publishing an unsourced fee term twice where it had been once.

Gates, exit status read for each: check 0 (0 errors, 0 warnings, 0 hints),
build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, minifier
grep exit 1, router.test.mjs 30/30. Lighthouse NOT run. Nothing deployed and
nothing applied to the distribution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-03 17:23:20 -04:00
co-authored by Claude Opus 5
parent b9523817e2
commit 02739adac9
18 changed files with 764 additions and 101 deletions
+60 -7
View File
@@ -309,7 +309,7 @@ status, not the absence of an error.
---
## Part 3 — Apply the three distribution changes
## Part 3 — Apply the four distribution changes
One script, `infra/cloudfront/configure.mjs`, because the alternative is
hand-editing a 300-line JSON document and posting it back with an `IfMatch` ETag.
@@ -328,18 +328,39 @@ Part 0.3 records is exactly:
resolved Managed-CachingDisabled = 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
resolved Managed-AllViewerExceptHostHeader = b689b0a8-53d0-40ab-baf2-68738e2966ac
4 change(s) to distribution E1OK7G98KNKUTA (ETag …):
6 change(s) to distribution E1OK7G98KNKUTA (ETag …):
+ DefaultCacheBehavior.FunctionAssociations viewer-request -> arn:…:function/adr-sml-router
+ CustomErrorResponses += 404 -> /404.html with status 404
+ Origins += intake-api -> …execute-api… (https-only, TLSv1.2)
+ CacheBehaviors += /api/* -> intake-api, CachingDisabled, AllViewerExceptHostHeader, POST allowed
+ create response-headers policy adr-sml-pdf-noindex (SecurityHeadersConfig cloned from … + X-Robots-Tag: noindex)
+ CacheBehaviors += *.pdf -> <s3-origin>, default cache policy, adr-sml-pdf-noindex (policy id created in the same --apply pass)
DRY RUN — nothing was sent. Re-run with --apply to write it.
```
Fewer than four changes means part of this is already done — read which lines are
prefixed `·` (already present) and carry on. More than four, or a different set,
means the distribution is not in the state 0.3 recorded: stop and re-read it.
⚠️ **SECTION 4 CANNOT SHOW THE POLICY ID IN A DRY RUN, AND SAYS SO — IT IS
STILL ONE `--apply`.** The `*.pdf` behaviour has to carry the response-headers
policy's id, and on a first run that policy does not exist yet, so the dry run
prints the behaviour it *would* add with `(policy id created in the same --apply
pass)` where the id goes. **A single `--apply` creates the policy and adds the
behaviour in one call — do not run it twice.** The dry run reports both changes
either way; one that listed only the policy would hide the half that touches a
distribution serving 23 pages.
Fewer than six changes means part of this is already done — read which lines are
prefixed `·` (already present) and carry on. **On the live distribution as at
2026-09-03, changes 13 are applied and you should see exactly the last two.**
More than six, or a different set, means the distribution is not in the state 0.3
recorded: stop and re-read it.
⚠️ **AND `adr-sml-pdf-noindex` IS RECONCILED ON EVERY RUN, NOT ONLY CREATED.** A
response-headers policy **replaces** rather than merges, so the PDF policy has to
carry everything the default behaviour's policy carries. If they have diverged —
someone adds the `Content-Security-Policy` or `Permissions-Policy` that
`docs/05` specifies to one and not the other — the script **throws and names the
diff** rather than passing. That is deliberate: the failure it guards is the PDF
being served different headers from the pages, which is silent.
```bash
node infra/cloudfront/configure.mjs --dist "$DIST_ID" --api-domain "$API_DOMAIN" \
@@ -352,11 +373,43 @@ echo "deployed: $?"
```bash
aws cloudfront get-distribution-config --id "$DIST_ID" \
--query 'DistributionConfig.{Fn:DefaultCacheBehavior.FunctionAssociations.Items[].EventType,Err:CustomErrorResponses.Items[].{Code:ErrorCode,Page:ResponsePagePath,Status:ResponseCode},Beh:CacheBehaviors.Items[].{P:PathPattern,O:TargetOriginId,Methods:AllowedMethods.Items},Origins:Origins.Items[].Id}'
--query 'DistributionConfig.{Fn:DefaultCacheBehavior.FunctionAssociations.Items[].EventType,Err:CustomErrorResponses.Items[].{Code:ErrorCode,Page:ResponsePagePath,Status:ResponseCode},Beh:CacheBehaviors.Items[].{P:PathPattern,O:TargetOriginId,RHP:ResponseHeadersPolicyId,Fn2:FunctionAssociations.Items[].EventType,Methods:AllowedMethods.Items},Origins:Origins.Items[].Id}'
```
**Expect:** `Fn: ["viewer-request"]`; one error response `404 → /404.html → 404`;
one behaviour `/api/*``intake-api` with POST in its method list; two origins.
**two** behaviours — `/api/*``intake-api`, POST in its method list, **no
`RHP` and `Fn2: null`** (the association is withheld there deliberately: a 301
would turn the form's POST into a GET and drop the body), and `*.pdf` → the S3
origin **with an `RHP` id and `Fn2: ["viewer-request"]`**; two origins.
**Then verify the header actually arrives, because the config landing is not the
same fact:**
```bash
curl -D /tmp/pdf.h -o /dev/null "$SITE/pouya-lajevardi-bio.pdf"
echo "curl_exit=$?" # curl's OWN status, on its own line
for h in x-robots-tag strict-transport-security x-content-type-options \
x-frame-options x-xss-protection referrer-policy; do
printf '%-28s %s\n' "$h" "$(grep -ic "^$h:" /tmp/pdf.h)"
done
```
**Expect** `curl_exit=0` and **`1` against every one of the six** — the five
security headers *and* `x-robots-tag`.
⚠️ **THE SHAPE OF THIS BLOCK IS THE POINT, and its first version got all three
wrong.** It piped `curl -sI` into one `grep -E` with six alternatives and read
`$?`. That reports **grep's** status, not curl's, so a DNS failure, a TLS failure
and a 5xx all read as `exit=1` — indistinguishable from "the headers are
missing", with `-s` deleting the message that would have told them apart. And an
alternation exits **0 if ANY ONE** matches, so `exit=0` would not have meant the
five arrived, which is the only regression the block exists to catch. Counting
each header separately is what makes a partial clone visible. (`CLAUDE.md`: never
suppress stderr, never read a pipeline's status as its first command's, and a
uniform pass is the result that ends a check rather than starting one.)
⚠️ **If any of the five security headers reads `0`, the policy did not clone them
and the PDF has LOST headers it had before this change.**
---