Steps 7a through 10 as one authorised run. Nothing deployed (D11).
7a Lighthouse returns as `lighthouse@13.4.1` + `chrome-launcher`, NOT
`@lhci/cli`. AGENTS.md §7's advisory attribution was wrong: the carriers
were @lhci/cli's own `tmp` and @puppeteer/browsers' `extract-zip`, not
Lighthouse, which audits clean. A deliberate deviation from R11's literal
trigger, recorded with what it costs. Local gate; CI has no Chrome.
7b OG card generator (satori + sharp) discharges R15 — 20 typed cards plus
per-article cards; the portrait stays on / and /about/ by Q40. Insights
plumbing: ArticleCard, Prose, the index, the article route, articleGraph,
and /'s section 7. Card copy is constrained structurally because text in a
JPEG cannot be grepped by check:claims: every headline IS its page's <h1>,
enforced by `npm run og:proof`.
7c Five drafted launch articles, draft: true / reviewedByPouya: false. An
independent compliance audit returned 76 findings and 57 unsourced
assertions; all blocking and should-fix applied.
8 /contact/, the intake form, and backend/intake/ (undeployed). Plain HTML
POST to a same-origin /api/intake with a 303 redirect, so the form works
with zero JavaScript. docs/05 records three deliberate deviations.
9 /fees/ on Q59's ruling — overtime runs from the session cap, and the
reservation point ships adjacent to the rate. One-page PDF bio discharges
R16; /bio/ is its source, so the circulated artefact stays inside the
review apparatus.
10 /legal/privacy/ and /legal/terms/, written to the backend as built. Three
of the policy's statements are derived and cannot drift.
Also: /about/'s inverse credentials band (approved at step 6); Q59 closed;
R15 and R16 discharged; and a fix to shipped copy — /practice/energy/ asserted
the absence of a regulation the source extract says must not be asserted.
Review: adversarial-reviewer, two rounds (D20/D19). Round 1 returned 16
findings including two blocking — an invisible ghost button on /fees/ at
1.00:1 that Lighthouse scored 100, and a privacy policy that named one data
processor when there are two. All 16 acted on.
Lighthouse, 22 pages, mobile: performance 99-100, accessibility 100,
best practices 100, SEO 100 on every indexable page, CLS 0.000.
AGENTS.md entry (ah) has the detail, including four of my own verification
commands that were wrong and what each of them nearly caused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
143 lines
5.9 KiB
JavaScript
143 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Cross-checks the intake form's two field tables. `npm run check:intake`.
|
|
*
|
|
* WHY THERE ARE TWO TABLES AT ALL, because the obvious reaction to this script
|
|
* is to delete one of them and share an import. `docs/05-backend-spec.md`:
|
|
* *"Client-side validation is a convenience. **The Lambda re-validates
|
|
* everything.**"* A server that validates against a list the client shipped it
|
|
* is not validating — it is asking the caller what the rules are. And the Lambda
|
|
* is a separately deployed zip that cannot import from `src/` anyway.
|
|
*
|
|
* So the duplication is architectural, and what makes it safe is this check
|
|
* rather than a shared module: the two tables must agree on every field NAME, on
|
|
* which fields are REQUIRED, on every length CAP, and on every closed OPTION
|
|
* SET. If they disagree, the form offers something the handler rejects, or the
|
|
* handler accepts something the form never shows — and the first is a lost
|
|
* inquiry that looks like a bug in the browser.
|
|
*
|
|
* This is the one place in the repo where a duplicated fact is deliberate, and
|
|
* `AGENTS.md`'s standing rule about duplicated facts is why it needs a mechanism
|
|
* on top of a comment.
|
|
*
|
|
* Both files are read directly — Node strips the types out of the `.ts` — so
|
|
* this script holds no third copy of the list.
|
|
*/
|
|
import { INTAKE_FIELDS, HONEYPOT_FIELD } from '../src/data/intake.ts';
|
|
import {
|
|
FIELDS as SERVER_FIELDS,
|
|
HONEYPOT,
|
|
} from '../backend/intake/fields.mjs';
|
|
|
|
/**
|
|
* BOTH TABLES ARE IMPORTED, NOT PARSED. The first version of this script read
|
|
* `handler.mjs` as text, sliced out the `const FIELDS = [ … ]` literal, munged
|
|
* quotes and commas into JSON, and guarded the result with a regex meant to
|
|
* refuse anything executable.
|
|
*
|
|
* **That guard rejected the table on the word `process`, which is a FIELD NAME.**
|
|
* A guard that fires on the data it exists to protect is worse than no guard,
|
|
* and the munging underneath it would have broken on the first apostrophe or
|
|
* URL in a label. The fix was not a better regex: the server's table moved into
|
|
* `backend/intake/fields.mjs`, which has no module-scope side effects and can
|
|
* simply be imported. The independence that matters is that the SERVER's table
|
|
* lives with the server and the handler trusts nothing from `src/` — not that a
|
|
* check script refuses to load it.
|
|
*/
|
|
|
|
const problems = [];
|
|
const server = SERVER_FIELDS;
|
|
|
|
const clientNames = INTAKE_FIELDS.map((f) => f.name);
|
|
const serverNames = server.map((f) => f.name);
|
|
|
|
for (const name of clientNames) {
|
|
if (!serverNames.includes(name)) {
|
|
problems.push(
|
|
`"${name}" is on the form but the handler does not accept it — the ` +
|
|
'inquirer would fill it and it would be silently dropped.',
|
|
);
|
|
}
|
|
}
|
|
for (const name of serverNames) {
|
|
if (!clientNames.includes(name)) {
|
|
problems.push(
|
|
`"${name}" is validated by the handler but is not on the form.`,
|
|
);
|
|
}
|
|
}
|
|
if (HONEYPOT !== HONEYPOT_FIELD) {
|
|
problems.push(
|
|
`honeypot name differs: form "${HONEYPOT_FIELD}", handler "${HONEYPOT}". ` +
|
|
'A bot fills the field the form renders; the handler checks the one it ' +
|
|
'knows about, so a mismatch disables the honeypot silently.',
|
|
);
|
|
}
|
|
if (serverNames.includes(HONEYPOT_FIELD)) {
|
|
problems.push(
|
|
`the honeypot "${HONEYPOT_FIELD}" is in the handler's FIELDS table; it must ` +
|
|
'be checked separately, or a bot filling it would just fail validation ' +
|
|
'instead of being sent to the success page.',
|
|
);
|
|
}
|
|
|
|
for (const clientField of INTAKE_FIELDS) {
|
|
const serverField = server.find((f) => f.name === clientField.name);
|
|
if (!serverField) continue;
|
|
|
|
if (Boolean(clientField.required) !== Boolean(serverField.required)) {
|
|
problems.push(
|
|
`"${clientField.name}": form required=${Boolean(clientField.required)}, ` +
|
|
`handler required=${Boolean(serverField.required)}. A field the form ` +
|
|
'marks optional and the handler requires is a rejection the inquirer ' +
|
|
'cannot see the reason for.',
|
|
);
|
|
}
|
|
|
|
/* LABELS TOO, since 2026-08-31. The handler now renders `f.label` into the
|
|
confirmation email the inquirer keeps, so a label that drifts from the
|
|
form's own wording means the receipt describes fields by names the form
|
|
never showed. One more comparison; the duplication stays mechanical. */
|
|
if (clientField.label !== serverField.label) {
|
|
problems.push(
|
|
`"${clientField.name}": labels differ.\n` +
|
|
` form: ${JSON.stringify(clientField.label)}\n` +
|
|
` handler: ${JSON.stringify(serverField.label ?? null)}\n` +
|
|
' The handler renders its label into the confirmation email.',
|
|
);
|
|
}
|
|
|
|
if ((clientField.max ?? null) !== (serverField.max ?? null)) {
|
|
problems.push(
|
|
`"${clientField.name}": form max=${clientField.max ?? 'none'}, ` +
|
|
`handler max=${serverField.max ?? 'none'}. The form's maxlength stops ` +
|
|
'typing; a lower cap in the handler rejects a submission that looked fine.',
|
|
);
|
|
}
|
|
|
|
const clientOptions = clientField.options ? [...clientField.options] : null;
|
|
const serverOptions = serverField.options ? [...serverField.options] : null;
|
|
if (JSON.stringify(clientOptions) !== JSON.stringify(serverOptions)) {
|
|
problems.push(
|
|
`"${clientField.name}": option sets differ.\n` +
|
|
` form: ${JSON.stringify(clientOptions)}\n` +
|
|
` handler: ${JSON.stringify(serverOptions)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`check:intake — ${clientNames.length} form fields, ${serverNames.length} ` +
|
|
'handler fields, compared on name, label, requiredness, cap and option set.',
|
|
);
|
|
if (problems.length > 0) {
|
|
console.error(`\nINTAKE TABLE MISMATCH — ${problems.length}:`);
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
console.error(
|
|
'\ndocs/05: the handler re-validates everything. The two tables are ' +
|
|
'independent on purpose; they still have to agree.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log('OK — the form and the handler agree.');
|