feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Build and deploy / build-and-deploy (push) Failing after 4s

Five items of Pouya's production run, 2026-09-01.

Q61 — scroll-padding-top becomes a max() ramp on `10lh - 83px`, with the
plain calc() first as the fallback for engines without `lh`. Hidden focus
stops under minimumFontSize=32: 290 of 1,455 -> 0, control build still
290. Default settings byte-identical (0 differences over 352 page-widths x
17 fields). The 12 residual cells at minimumFontSize=16/20 are pre-existing
and unchanged-or-better; reported, not widened, per instruction.

Intake backend + CloudFront — docs/09-cutover-runbook.md is the
copy-paste sequence for admin execution: every command followed by its
verification and expected output, rollback per part, and Part 10 is Q60's
TTL test. infra/cloudfront/router.js is the trailing-slash function
(30-case suite; 8 fail against the pre-review version, incl. a
protocol-relative open redirect). infra/cloudfront/configure.mjs is
dry-run-by-default and idempotent. scripts/intake-env.mjs emits the six
Lambda env vars from src/data/site.ts.

Four launch blockers found by reading the running system:
  - handler.mjs wrote pk/sk; the live table's key is submissionId with no
    sort key, so every submission would have failed validation silently
  - the Lambda invoke permission is scoped to the old route path
  - 22 of 23 pages 403 without the router function
  - there was no 404 page; src/pages/404.astro adds it

Claims audit (D20 cutover pass) — five gloss over-reaches corrected on
/practice/energy/, /practice/insurance/ (x2), /practice/technology/ and
/med-arb/. Three findings left open for Pouya: Q62, the /med-arb/ gloss,
and Q60.

Q62 — one frozen-tripwire pattern added under the freeze's own breach
exception, with a probe and four negative fixtures. check:claims exits 1
until the false /legal/privacy/ sentence is corrected, so both deploy
paths are blocked by a mechanism rather than by memory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-02 06:52:20 -04:00
co-authored by Claude Opus 5
parent ca1c2524e1
commit bd282aa47d
30 changed files with 3256 additions and 143 deletions
+40
View File
@@ -257,6 +257,31 @@ const PATTERNS = [
'reached a public page once.',
re: /anyone (may|can) be appointed an arbitrator|nothing in law gates|no (licence|license|designation) is (required|needed) to (be appointed|arbitrate|act as an arbitrator)/gi,
},
{
id: 'sole-administrator-q62',
rule: 'Q62 — /legal/privacy/ claims sole administrative access to the intake table, and that is FALSE.',
incident:
'The page shipped "nobody else has access to the table. There is no team, ' +
'no assistant and no external administrator." The AWS account has an ' +
'`admins` IAM group carrying AdministratorAccess with TWO members, and ' +
'`simulate-principal-policy` returns allowed for dynamodb:GetItem, Query ' +
'and Scan on the table for both ' +
'[verified 2026-09-01 — docs/reference/intake-table-access-verification.md]. ' +
'THE REACHED-DIST CONDITION IS WHY THIS PATTERN EXISTS AT ALL: the ' +
'sentence was in dist/legal/privacy/index.html, `build`, `check` and this ' +
'script all exited 0 over it, and the only thing between it and a live ' +
'privacy policy was a TODO(pouya) in a JSX comment — which Astro strips, ' +
'so `grep -rn "TODO(pouya)" dist/` returned nothing. The gate was human ' +
'memory. Found by `adversarial-reviewer`, 2026-09-01. ' +
'DELETE THIS PATTERN when Q62 is ruled: either the access is removed and ' +
'the sentence becomes true, or the sentence is rewritten. It is a tripwire ' +
'on one specific published falsehood, not a rule about a class.',
/* `\s+`, NOT LITERAL SPACES: `compressHTML` collapses whitespace between
tags and leaves it alone inside a text node, so the published bytes read
"nobody else has access to\n the table". A literal-space version
runs, prints `ok`, and exits 0 with the breach in `dist/`. */
re: /nobody\s+else\s+has\s+access\s+to\s+the\s+table|there\s+is\s+no\s+team,\s+no\s+assistant\s+and\s+no\s+external\s+administrator/gi,
},
];
/**
@@ -341,6 +366,11 @@ const FIXTURES = {
'C.Med-Arbitrators',
'C.Med-Arbitration',
],
'sole-administrator-q62': [
/* The two published clauses, verbatim from dist/legal/privacy/. */
'nobody else has access to the table',
'There is no team, no assistant and no external administrator.',
],
'struck-universal-q39': [
'Anyone may be appointed an arbitrator in Ontario',
'nothing in law gates the role',
@@ -348,6 +378,16 @@ const FIXTURES = {
},
/* Every one of these is real published or spec-approved copy on this site. */
mustNotMatch: [
/* NEGATIVE FIXTURES FOR `sole-administrator-q62` — the approved copy nearest
to the two clauses it catches. The pattern is deliberately anchored on the
two published sentences rather than on the ideas in them, because the
replacement wording is not yet decided and a looser pattern would fire on
whatever Q62's ruling produces. These four are what "nearest" means: the
same page's true sentences about the same subject. */
'The table is reachable by the function that writes to it.',
'Two accounts hold administrative access to the AWS account, and the function that writes to the table cannot read it.',
'Nobody else has access to my mailbox.',
'There is no team. Every inquiry is read by me.',
'I act as a neutral. I do not act for a party in a matter I take, and each party should have their own legal advice.',
'I run a process, I do not run a case for anybody in it.',
'I will not run a process whose shape nobody agreed to in advance.',
+21 -6
View File
@@ -69,7 +69,7 @@ case "$caller" in
echo >&2
echo "REFUSING: that is the broadly-permissioned personal user." >&2
echo "AGENTS.md §10 — never use user/pouya to deploy. Use the scoped" >&2
echo "deploy user (Q22: not yet created)." >&2
echo "deploy user, adr-sml-deploy — PROVISIONED, AGENTS.md §7." >&2
exit 1
;;
esac
@@ -157,7 +157,7 @@ location=$(curl -sS -o /dev/null -w '%{redirect_url}' -X POST \
-H "Origin: https://adr.smlcompany.ca" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'deploy-route-probe=1' \
"https://adr.smlcompany.ca/api/intake" 2>/dev/null || true)
"https://adr.smlcompany.ca/api/intake" || true)
if [ "$rc" -ne 0 ]; then
echo >&2
echo "WARNING: the POST to /api/intake did not complete (curl exit $rc)." >&2
@@ -169,10 +169,25 @@ else
echo >&2
echo "WARNING: POST /api/intake returned $code (expected 303 to" >&2
echo "/contact/could-not-send/); redirect was '${location:-none}'." >&2
echo "404 means the CloudFront /api/* behaviour is missing. 403 can mean the" >&2
echo "same thing — CloudFront rejecting a method the default behaviour does" >&2
echo "not allow — or the handler refusing the Origin. Either way the form is" >&2
echo "not verified working. See docs/06-deployment.md's cutover checklist." >&2
# 404 IS AMBIGUOUS BETWEEN THREE CAUSES and the distribution's custom error
# response hides the one string that would separate them: API Gateway's
# {"message":"Not Found"} is replaced by /404.html, because custom error
# responses are distribution-wide. So name the causes and the one command that
# tells them apart. Corrected 2026-09-01 by `adversarial-reviewer`; the earlier
# text named only the CloudFront behaviour.
echo "404 means one of three things, and \`aws apigatewayv2 get-routes" >&2
echo "--api-id <id> --query 'Items[].RouteKey'\` separates them in one call:" >&2
echo " - the CloudFront /api/* behaviour is missing (docs/09 Part 3);" >&2
echo " - the POST /api/intake route is missing or misspelled (Part 6.2);" >&2
echo " - the route exists and the distribution's 404 mapping is showing you" >&2
echo " /404.html instead of the API's own body." >&2
echo "403 means CloudFront rejected the method, or the handler refused the" >&2
echo "Origin — check the behaviour uses Managed-AllViewerExceptHostHeader," >&2
echo "because a policy that drops Origin turns every real submission into a" >&2
echo "403. 500 means the Lambda invoke permission for this route is missing" >&2
echo "(Part 6.1) — the function is never entered, so CloudWatch is silent." >&2
echo "Either way the form is not verified working. See docs/09-cutover-" >&2
echo "runbook.md Part 7.1 and docs/06's cutover checklist." >&2
fi
echo "==> Deployed to https://adr.smlcompany.ca ($(git rev-parse --short HEAD))"
+103
View File
@@ -0,0 +1,103 @@
/**
* Prints the intake Lambda's six environment variables as the JSON that
* `aws lambda update-function-configuration --environment` takes.
*
* ⚠️ THIS EXISTS SO THAT TWO PUBLISHED COMMITMENTS ARE NEVER RETYPED INTO A
* SHELL COMMAND. `RESPONSE_TIME` and `NO_RETAINER_NOTICE` are read from
* `src/data/site.ts` — the same constants `/contact/` renders — because a
* hand-typed copy of the notice inside the handler had already dropped a clause
* once (`docs/05`, and the handler's own comment on the constant). A deploy
* procedure that asks an operator to paste a sentence is the same defect one
* step further out, and the notice contains an EN DASH in "mediatorparty",
* which is exactly the character a retype loses.
*
* Resource names come from `AGENTS.md` §7 and are passed in, not defaulted from
* a second copy here — except the two that are pure site facts.
*
* usage: node scripts/intake-env.mjs --table <name> --notify <addr> --from <addr>
* node scripts/intake-env.mjs ... --shell # export lines instead
*/
import { CONTACT, NO_RETAINER_NOTICE, SITE } from '../src/data/site.ts';
const args = process.argv.slice(2);
const flag = (name) => {
const i = args.indexOf(`--${name}`);
return i === -1 ? undefined : args[i + 1];
};
const table = flag('table');
const notify = flag('notify');
const from = flag('from');
const missing = [
['--table', table],
['--notify', notify],
['--from', from],
]
.filter(([, v]) => !v)
.map(([k]) => k);
if (missing.length > 0) {
console.error(`missing: ${missing.join(' ')}`);
console.error(
'usage: node scripts/intake-env.mjs --table <dynamodb-table> ' +
'--notify <address> --from <ses-verified-address> [--shell]',
);
console.error('Resource names are in AGENTS.md §7.');
process.exit(2);
}
/* The site origin is not a deploy-time choice: the handler compares the request
Origin against it and redirects to pages ON it, so it must be the canonical
origin `astro.config.mjs` builds against. */
const origin = SITE.url.replace(/\/$/, '');
const vars = {
INTAKE_TABLE: table,
SITE_ORIGIN: origin,
NOTIFY_TO: notify,
MAIL_FROM: from,
RESPONSE_TIME: CONTACT.responseTime,
NO_RETAINER_NOTICE,
};
/* Guards, not decoration. Each one is a failure this project has already had or
has written down as the next one. */
for (const [k, v] of Object.entries(vars)) {
if (typeof v !== 'string' || v.trim() === '') {
throw new Error(
`${k} resolved empty — the handler throws at cold start on that`,
);
}
}
if (!/^https:\/\//.test(origin)) {
throw new Error(`SITE_ORIGIN must be an https origin, got ${origin}`);
}
/* The clause a hand-copy dropped. `docs/01` §/contact/ requires it, so its
absence is a published-disclosure defect rather than a typo. */
if (!NO_RETAINER_NOTICE.includes('create a conflict check')) {
throw new Error(
'NO_RETAINER_NOTICE is missing its fourth clause about not itself creating ' +
'a conflict check — docs/01 §/contact/ requires it. Do not deploy this.',
);
}
if (!//.test(NO_RETAINER_NOTICE)) {
throw new Error(
'NO_RETAINER_NOTICE no longer contains the en dash in "mediatorparty". ' +
'Either the constant changed deliberately, or something re-typed it.',
);
}
if (!/\btwo business days\b/.test(CONTACT.responseTime)) {
throw new Error(
`RESPONSE_TIME is "${CONTACT.responseTime}" — AGENTS.md §4/Q27 is a ` +
'two-business-day commitment. If the commitment changed, /contact/, the ' +
'bio and this all move together.',
);
}
if (args.includes('--shell')) {
for (const [k, v] of Object.entries(vars)) {
console.log(`export ${k}=${JSON.stringify(v)}`);
}
} else {
console.log(JSON.stringify({ Variables: vars }));
}
+10
View File
@@ -89,6 +89,16 @@ async function collectPages(dir = DIST) {
const rel = relative(DIST, dir).split(sep).filter(Boolean).join('/');
out.push(rel ? `/${rel}/` : '/');
}
/* ⚠️ `index.html` ALONE MISSED THE 404 PAGE, so the budget was measured on
22 pages of 23 while the header above claims it enumerates the site.
`404.astro` is emitted as `dist/404.html`, outside `build.format:
'directory'`. The path pushed here is a URL this script SERVES, so it is
`/404.html` — the form CloudFront's custom error response fetches — and
`resolveFile()` resolves it on the `extname` branch. `og-proof.mjs` needs
the `OG_CARDS` key `/404/` for the same file; the two differ on purpose. */
else if (dir === DIST && entry.name.endsWith('.html')) {
out.push(`/${entry.name}`);
}
}
return out.sort();
}
+12
View File
@@ -67,6 +67,18 @@ async function pages(dir = DIST) {
const rel = relative(DIST, dir).split(sep).filter(Boolean).join('/');
out.push({ path: rel ? `/${rel}/` : '/', file: full });
}
/* ⚠️ `index.html` ALONE MISSED A WHOLE PAGE. `build.format: 'directory'`
puts every route at `<dir>/index.html` — except the ones Astro emits
outside the convention, and `404.astro` becomes `dist/404.html`. So this
script enumerated 22 pages of 23, and the symptom was backwards: it
reported the 404 page's card as ORPHANED ("generated, but no built page
references it") rather than reporting the page as unchecked. `path` here
is an `OG_CARDS` key, which is `Astro.url.pathname` — `/404/`, not
`/404.html`. `scripts/lighthouse.mjs` had the same blind spot and needs
the URL form instead; see the note there. */
else if (dir === DIST && entry.name.endsWith('.html')) {
out.push({ path: `/${entry.name.replace(/\.html$/, '')}/`, file: full });
}
}
return out.sort((a, b) => a.path.localeCompare(b.path));
}