feat: price med-arb by phase, attest the conflicts undertaking, and answer the first real spam
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
This commit is contained in:
Pouya Lajevardi
2026-09-04 10:06:37 -04:00
co-authored by Claude Opus 5
parent 02739adac9
commit 3c3ba5dc6e
21 changed files with 2443 additions and 141 deletions
+116 -9
View File
@@ -508,9 +508,15 @@ Then invalidate `/*`.
> §7.1 stops before any write and any email by design, and that is `docs/09`
> §7.2, the real-submission test Pouya has in progress. The disclosures are
> unblocked; the end-to-end confirmation is still owed.
> 3. ⚠️ **THE D20 CLAIMS PASS RETURNED FAIL WITH 20 CONFIRMED FINDINGS; 15 ARE
> NOW FIXED, 2 REFUTED, 3 OUTSTANDING — updated 2026-09-03, and the three
> numbers partition the twenty.** Fixed under Pouya's rule *"the gloss may say
> 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
> 19, 1418 and 20 — the whole gloss class, plus `/bio/`'s role verb.
> ⚠️ **15 FINDINGS, 14 DISTINCT EDITS: findings 4 and 15 quote the same
@@ -518,14 +524,42 @@ Then invalidate `/*`.
> findings 12 and 19, the two backend disclosures, with item 2 above.
> **OUTSTANDING — findings 10, 11 and 13, and each is outstanding for a
> different reason:**
> **(10) NEEDS A RULING.** `/fees/`'s *"Every figure is on this page"* against
> §4's **Med-Arb** offering, which `docs/07-fees.md` prices nowhere. Either a
> med-arb fee term or a scoped promise; it cannot be closed by narrowing.
> **(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) NEEDS HIM TO HAVE SAID IT.** `/legal/privacy/`'s *"if a conflicts
> **(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`.
@@ -753,6 +787,17 @@ the decision is re-readable rather than re-litigated.
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
@@ -1020,7 +1065,29 @@ the decision is re-readable rather than re-litigated.
- [ ] Security headers present (`securityheaders.com` A or better)
- [x] **SES identities verified for sending** — `VerifiedForSendingStatus: true`, `DkimAttributes.Status: SUCCESS`, signing enabled, and no custom MAIL FROM (so DMARC rests on DKIM alignment, which is what §7 records) `[re-verified 2026-09-01 — sesv2 get-email-identity]`
- [x] ✅ **SES bounce/complaint alarms DO notify someone — R9 DISCHARGED, 2026-09-01.** `aws sns list-subscriptions-by-topic` on `ses-alerts` returns the email subscription to `info@smlcompany.ca` with a **real subscription ARN**, not `PendingConfirmation`. §7 recorded it as pending, and §12 R9 said *"this is the first thing to check if `/contact/` ships"* — it had been confirmed at some point before this reading and the record had not moved, which is the same staleness in the safe direction. *(SES production access itself is granted — Q19 closed.)*
- [ ] **THE INTAKE FORM DOES NOT WORK YET, AND THREE THINGS HAVE TO HAPPEN BEFORE
- [ ] 🛑 **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
@@ -1141,8 +1208,48 @@ the decision is re-readable rather than re-litigated.
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.**
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