feat: build steps 7a-10 — the site is complete and reviewable at 22 pages

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
This commit is contained in:
Pouya Lajevardi
2026-08-31 10:56:54 -04:00
co-authored by Claude Opus 5
parent 6cfe69033f
commit 210bc25a26
53 changed files with 8589 additions and 177 deletions
+343
View File
@@ -0,0 +1,343 @@
---
/**
* `/legal/privacy/` — build step 10. Spec: docs/01 §`/legal/*`,
* docs/05-backend-spec.md §Privacy policy must state.
*
* ⚠️ **THE GOVERNING INSTRUCTION IS "WRITTEN TO MATCH WHAT IS ACTUALLY BUILT,
* NOT WHAT IS TYPICAL" — docs/05 — AND THAT IS WHY THIS PAGE IS BUILT LAST IN
* THE ORDER.** `docs/01`: *"/legal/* — written to match the backend as actually
* built."* On this page a sentence that describes an intended control rather than
* a real one is a false statement to the public in a legal document, and it is
* the kind that fails silently: nothing breaks, and the sentence reads correctly.
*
* So three things are DERIVED rather than written, and each closes a specific
* way this page could quietly become untrue:
*
* 1. **The list of what is collected is rendered from `INTAKE_FIELDS`** — the
* same array `/contact/` builds the form from. A field added to the form
* appears here on the same build. A hand-written list is the copy nobody
* re-reads, which is the SES-DKIM shape in a document with legal weight.
* 2. **The retention period is rendered from `RETENTION_MONTHS`**, which is the
* figure `backend/intake/handler.mjs` writes into the `ttl` attribute.
* docs/05: *"Whatever number ships must match `/legal/privacy/` exactly."*
* 3. **The analytics paragraph is rendered from `ANALYTICS.installed`.** D15
* decided Plausible; §7 records that no script is on any page. Deciding is
* not installing, and a policy naming a processor that processes nothing is
* a false disclosure. Today it says there are none.
*
* ⚠️ **WHAT THIS PAGE DELIBERATELY DOES NOT CLAIM, AND THE OMISSIONS ARE THE
* POINT.** docs/05 specifies a customer-managed KMS key, point-in-time recovery,
* and DynamoDB TTL. `AGENTS.md` §7 verifies the table's name and region and
* **does not verify any of those three as enabled**. So:
*
* - "Encrypted at rest" IS stated, because DynamoDB encrypts every table at
* rest unconditionally — it is true whether or not the customer-managed key
* in docs/05 has been configured.
* - The customer-managed key and point-in-time recovery are NOT mentioned.
* Neither is a fact a reader needs, and neither is verified.
* - **Automatic deletion IS stated, and it is the one promise on this page
* that depends on a control nobody has verified.** The handler writes the
* `ttl` attribute; TTL must also be ENABLED on the table, which §7 does not
* record. docs/05's definition of done carries "TTL set and verified by test
* record" and `docs/06`'s cutover checklist now names this page as what that
* item is protecting. It must be verified before this page is public.
*
* ⚠️ **NO LICENSURE CLAIM AND NO ANSWER TO THE CAPACITY QUESTION.** A privacy
* policy is where "legal advice" phrasing arrives by convention. §4 records
* licence status as `[unestablished]` and instructs this repository to answer
* neither way; `docs/03`'s ratified pattern is role, then consequence for the
* reader, and no verb of capacity. Applied throughout.
*/
import BaseLayout from '../../layouts/BaseLayout.astro';
import Eyebrow from '../../components/Eyebrow.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../../assets/og-portrait.jpg';
import { pageGraph } from '../../data/schema';
import { ANALYTICS, CONTACT, SITE } from '../../data/site';
import { INTAKE_FIELDS } from '../../data/intake';
const ldImage = await getImage({
src: ogDefault,
format: 'jpeg',
width: 1200,
height: 630,
});
const graph = pageGraph(new URL(ldImage.src, Astro.site).href);
/**
* ⚠️ MUST MATCH `RETENTION_MONTHS` IN `backend/intake/handler.mjs`, which is
* the figure written into the record's `ttl`. docs/05: "Whatever number ships
* must match /legal/privacy/ exactly." The handler is a separately deployed
* artefact and cannot be imported here, so this is a second copy — and unlike
* the intake field tables there is no `check:` script over it. Treat a change to
* either as a change to both, and see docs/06's cutover checklist.
*/
const RETENTION_MONTHS = 24;
/** Bump this on ANY substantive edit. A privacy policy with a stale date is a
* policy a reader cannot tell they are reading an old version of. */
const LAST_UPDATED = '31 August 2026';
/* Rendered from the form's own field list, so the two cannot drift. `consent`
and the honeypot are absent from `INTAKE_FIELDS` deliberately and are
described in prose below instead — one is not information about the inquirer,
and the other is not information at all. */
const COLLECTED = INTAKE_FIELDS.map((field) => field.label);
---
<BaseLayout
title="Privacy Policy · Intake and Data Handling · Pouya Lajevardi"
description="What the intake form collects, why, where it is stored, how long it is kept, who can see it, and how to have it deleted. Written to match what is built."
jsonLd={graph}
noindex
>
<section class="section hero">
<div class="wrap">
<Eyebrow dot>Privacy</Eyebrow>
<h1 class="display hero-h">
What the intake form collects, and for how long.
</h1>
<p class="hero-lede">
This describes what actually happens to what you send me, not what is
typical. Last updated {LAST_UPDATED}.
</p>
</div>
</section>
<section class="section legal-body">
<div class="wrap">
<div class="prose">
<h2>What is collected</h2>
<p>
One form on this site collects personal information: the intake form
on the <a href="/contact/">contact page</a>. It asks for the
following, and the fields marked required on the form are the only
ones that must be completed.
</p>
<ul>
{COLLECTED.map((label) => <li>{label}</li>)}
</ul>
<p>
Submitting the form also records the date and time, your IP address
and your browser's user-agent string. Those three are kept for
investigating abuse of the form and are not used for anything else.
</p>
<p>
Nothing else on this site collects personal information. There is no
newsletter, no account, no comment form and no upload.
</p>
<h2>Information about other people</h2>
<p>
The form asks for the other parties to the dispute and their counsel.
That is information about people who have not filled in the form and
may not know it was sent. It is asked for one reason: I cannot accept
an appointment before conflicts are checked, and the check needs
names.
</p>
<p>
Please give names and nothing more about them. The form asks you not
to include privileged or confidential detail anywhere in it, and the
summary field says so directly. There is deliberately no field for
amounts in dispute and no way to attach a document.
</p>
<h2>Why it is collected, and on what basis</h2>
<p>
To reply to your inquiry and to run a conflicts check. The basis is
your consent, which the form asks for explicitly with an unchecked box
you have to tick. The wording you agree to is on the form itself.
</p>
<p>
It is not used for marketing. It is not sold, rented or shared with
anyone for their own purposes.
</p>
<h2>Where it is stored</h2>
<p>
In a DynamoDB table in Amazon Web Services' Canada Central region, in
Canada. It is encrypted at rest. Two emails are sent when you submit
the form — a notification to me and a confirmation to you — using
Amazon Simple Email Service, also in the same Canadian region.
</p>
{
/* ⚠️ THIS PARAGRAPH REPLACED A FALSE ONE, AND IT IS THE MOST SERIOUS
THING FOUND IN THE STEP 710 REVIEW. It read: *"Amazon Web Services
is therefore a processor for this information. **No other third party
receives it.**"*
`AGENTS.md` §7 records mail hosting as **Google Workspace**, and D18
sends the notification to `info@smlcompany.ca`. So Google receives and
stores every submission — including the names of opposing parties and
their counsel, which is the most sensitive thing this form collects —
as a mail processor. The page's own next section already admitted it:
*"The notification sits in my mailbox."* That mailbox is Google's.
A reader making a PIPEDA access request was being told there was one
processor when there are two. This page's header comment sets the
standard the sentence failed: a statement that describes an intended
control rather than a real one is a false statement to the public in
a legal document, and it fails silently, because nothing breaks and
the sentence reads correctly.
Found by `adversarial-reviewer`, 2026-08-31. §7 is cited rather than
restated — no MX record here. */
}
<p>
Two companies therefore process it, and both are named because a
reader asking for a copy or a deletion needs to know where it went. <strong
>Amazon Web Services</strong
> stores the submission and sends the two emails, in Canada. <strong
>Google</strong
> receives the notification email, because my own mail is on Google Workspace
— so a copy of what you send, including any names you give me, sits in that
mailbox. If you reply to the confirmation, that reply goes there too.
</p>
<p>
The confirmation sent to you is delivered to whoever runs your email.
That is your provider rather than mine, and I have no control over
what they keep.
</p>
<p>
No one else receives it. There is no CRM, no mailing list, no
analytics on the submission, and no assistant or outside
administrator.
</p>
<h2>How long it is kept</h2>
<p>
<strong>{RETENTION_MONTHS} months from the date you send it</strong>,
after which the record is deleted automatically by the database rather
than by someone remembering to do it. That period is long enough to
run a conflicts check across the normal life of a matter and no longer
than necessary for that purpose.
</p>
<p>
Emails are a separate matter. The notification sits in my mailbox and
the confirmation sits in yours, and neither is deleted by that
mechanism.
</p>
<h2>Who can see it</h2>
<p>
I can. The table is reachable by the function that writes to it and by
one administrative account, which is mine. Nobody else has access, and
there is no team, no assistant and no external administrator.
</p>
<h2>Cookies and analytics</h2>
{
ANALYTICS.installed ? (
<p>
Visits are counted using{' '}
{ANALYTICS.provider === 'plausible' ? 'Plausible' : 'Fathom'},
which is cookieless and collects no personal information and no
cross-site identifiers. There is nothing to consent to and no
banner, because nothing is stored on your device.
</p>
) : (
<p>
<strong>This site sets no cookies and runs no analytics.</strong>
There is no tracking script on any page, nothing is stored on your
device, and there is therefore nothing to consent to and no
banner. If that changes, this page changes on the same day and its
last updated date moves with it.
</p>
)
}
<p>
There are no third-party scripts of any kind on this site, no embedded
video, no web fonts fetched from another company's servers, and no
social media widgets. The pages you are reading make no request to
anyone but this site.
</p>
<h2>Asking for a copy, or asking me to delete it</h2>
<p>
Email <a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a> and ask. You
can ask for a copy of what you sent, ask me to correct it, or ask me to
delete it before the {RETENTION_MONTHS} months are up.
{' '}{CONTACT.responseTime}
</p>
<p>
Deletion removes the record. It does not retract the emails already
sent, and if a conflicts check has already been run I will tell you
what its outcome was rather than pretending the inquiry did not
happen.
</p>
<h2>What an inquiry is not</h2>
<p>
Sending the form does not create a retainer, does not appoint me as a
neutral in your matter, and does not itself establish a mediatorparty
relationship. It also does not, by itself, complete a conflicts check
— it gives me what I need to run one.
</p>
<h2>Changes to this page</h2>
<p>
If what happens to your information changes, this page is edited on
the same day and the date at the top moves. There is no archive of
previous versions.
</p>
<h2>Contact</h2>
<p>
Questions about any of the above:
<a href={`mailto:${CONTACT.email}`}>{CONTACT.email}</a>. The site is
{' '}{SITE.url}, and correspondence is by email — {CONTACT.location}.
</p>
</div>
</div>
</section>
</BaseLayout>
<style>
.hero {
padding-block-start: var(--space-9);
}
.hero-h {
margin-block: var(--space-4) var(--space-5);
/* --text-4xl, not --text-6xl. A legal page's job is to be read rather than
to land; at 96px this headline takes four lines before the reader reaches
the date they came to check. */
font-size: var(--text-4xl);
max-inline-size: 30ch;
}
.hero-lede {
max-inline-size: 58ch;
font-size: var(--text-lg);
line-height: var(--leading-body);
color: var(--text-secondary);
}
/* NOT `.reveal`. A legal document is the one page class where content must be
at full opacity the moment it renders, whatever the reader's scroll position
or motion setting — and where a reader may well arrive via Cmd-F. */
.legal-body {
padding-block-start: var(--space-7);
}
/* `global.css`'s `.prose` supplies the measure and paragraph spacing. These
are the two element types this page introduces that no other page's prose
block uses: headings inside a document, and a plain list. */
.prose h2 {
margin-block-start: var(--space-8);
font-family: var(--font-serif);
font-size: var(--text-2xl);
line-height: var(--leading-tight);
}
.prose h2:first-child {
margin-block-start: 0;
}
.prose ul {
margin-block-start: var(--space-4);
padding-inline-start: var(--space-6);
max-inline-size: var(--width-prose);
line-height: var(--leading-body);
color: var(--text-secondary);
}
.prose li + li {
margin-block-start: var(--space-2);
}
</style>