Files
adr-sml/src/pages/legal/privacy.astro
T
Pouya LajevardiandClaude Opus 5 3c3ba5dc6e
Build and deploy / build-and-deploy (push) Failing after 4s
feat: price med-arb by phase, attest the conflicts undertaking, and answer the first real spam
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
2026-09-04 10:06:37 -04:00

460 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
/**
* `/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 is the register for whether each of the three
* is enabled, and **this comment does not restate what it says** — it did once,
* went stale within the day, and had to be pulled back (§12 R19). 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 asserts a MECHANISM rather than only
* a period** — the one promise here whose truth lives entirely outside this
* repository. The handler writes the `ttl` attribute, and ⚠️ **writing the
* attribute is not the mechanism**: TTL must also be enabled on the table,
* which §7 records — **and the setting being on still does not prove a
* record is ever deleted.** Only a record written with a near-future `ttl`
* and watched to vanish proves that. docs/05's definition of done carries
* "TTL set and verified by test record" and `docs/06`'s cutover checklist
* names this page as what that item protects. ⚠️ **This read "both halves
* before this page is public" and the page went public first — Pouya's
* ruling of 2026-09-03: publish, then confirm the deletion, reading from
* 2026-09-04.** So the second half is now owed rather than pending, which is
* a weaker position and is recorded as one. See the comment on the retention
* section below, and §9 Q60.
*
* ⚠️ **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 Undertaking from '../../components/Undertaking.astro';
import { getImage } from 'astro:assets';
import ogDefault from '../../assets/og-portrait.jpg';
import { pageGraph } from '../../data/schema';
import {
ANALYTICS,
CONDUCT_UNDERTAKINGS,
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 = '4 September 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>
{
/* ⚠️ DO NOT WRITE "your IP address" HERE, AND DO NOT CONCLUDE ANYTHING
ABOUT WHETHER THE ADDRESS IDENTIFIES ANYONE. The handler stores
`requestContext.http.sourceIp` — behind the `/api/*` behaviour that
is a CloudFront edge, so the expected value is Amazon's. **Expected,
not measured:** `docs/09` Part 7.2 measures it at cutover and
enumerates three outcomes, one of which is that the reader's own
address does land. This copy therefore states only why the field is
kept, which is true in all three. A previous form hedged "usually
not yours" and then asserted "not precise enough to identify you" —
false in exactly the branch the hedge admitted. `claims-auditor`,
then `adversarial-reviewer` round 2. */
}
<p>
Submitting the form also records the date and time and your browser's
user-agent string. Those two are kept for investigating abuse of the
form and are not used for anything else.
</p>
<p>
It also records the network address the request arrived from. This
site sits behind a content delivery network, so that address is
normally the network's own rather than your connection's — which is
why it is kept simply because it arrives with the request, and not as
a way of identifying anyone.
</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, and the reason
is a commitment rather than an observation.
</p>
{
/* `<Undertaking>` AND `CONDUCT_UNDERTAKINGS`, NEVER TYPED PROSE —
§4's third class, whose characteristic failure mode is that a
promise gets quietly smaller and nothing fails. Undertaking (g),
attested 2026-09-03.
⚠️ IT IS THE COMPONENT FOR THE REASON THE COMPONENT EXISTS: one
treatment on every page, so a reader can tell a promise from a
description. This shipped for one pass as an ordinary paragraph in
`&ldquo;`/`&rdquo;` — the only such entities in `src/`, and a
commitment set as body prose reads as another sentence about
process.
It REPLACED the hand-typed "I cannot accept an appointment before
conflicts are checked", which stated the same proposition as a
constraint; keeping both would have set the undertaking beside its
own paraphrase — the (e)/(f) treatment. */
}
<Undertaking>{CONDUCT_UNDERTAKINGS.conflictsCheck}</Undertaking>
<p>
The check needs names. 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, and
it names <strong>SML Company Ltd</strong>, the company that holds this
practice's systems.
</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 the practice and a confirmation to you —
using Amazon Simple Email Service, also in the same Canadian region.
</p>
<p>
The table sits in an Amazon Web Services account that also runs
systems unrelated to this practice.
</p>
{
/* ⚠️ TWO PROCESSORS, AND BOTH MUST BE NAMED. `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 opposing parties and their counsel, the most sensitive
thing this form collects. A reader making a PIPEDA access request
needs both names. This paragraph replaced one asserting *"No other
third party receives it"*; see entry (ao). §7 is cited, not 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 the practice's 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 is sent it. There is no CRM, no mailing list and no
analytics on the submission. Who can read what is stored is a
different question from who it is sent to, and it is answered under
"Who can see it" below.
</p>
<h2>How long it is kept</h2>
{
/* The sentence below asserts a MECHANISM, not just a period, and the
mechanism is still unobserved — AGENTS.md §9 Q60, open. **Pouya
ruled 2026-09-03 that the page publishes now and the deletion is
confirmed after launch**; the observation window opened 2026-09-02
and the earliest useful reading is 2026-09-04 (`docs/09` Part 10).
That decision is why this is no longer a `TODO(pouya)`. The table
setting lives in §7 and is deliberately not restated here — it was
once, and went stale within the day (§12 R19). Do not answer Q60
from the handler code, which only writes the attribute. */
}
<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 the practice's
mailbox and the confirmation sits in yours, and neither is deleted by
that mechanism.
</p>
<h2>Who can see it</h2>
{
/* ⚠️ THIS SECTION AND THE SENTENCES BELOW ANSWER THE SAME QUESTION
AND CHANGE TOGETHER — by OPENING PHRASE, never by count. **Here:**
"The record in the table", "The system that receives", "The
notification goes to", "The confirmation that went to you".
**§Where it is stored:** "In a DynamoDB table", "The table sits in an
Amazon Web Services account", "Two companies therefore process it",
"The confirmation sent to you", "No one else is sent it". **§How long
it is kept:** "Emails are a separate matter".
⚠️ **THIS SECTION STATES WHO, NOT HOW — Pouya's ruling, 2026-09-02.
NOT TO BE RESTORED HERE:** the measurement paragraph, the
root-credential sentence, the single-sign-on and federated-login
enumeration, the resource-policy clause, the "company that runs a
database" aside, the deploy-credential sentence and the three-copies
summary. All true, all still in `AGENTS.md` §7 and
`docs/reference/intake-table-access-verification.md`. **No human
headcount** — a simulation counts identities, not people.
⚠️ **SENTENCE 1 IS SCOPED TO THE STORED RECORD** (paragraph 3 names
administrative staff, who read the mailbox and **cannot** read the
table) **AND PREDICATED ON ADMINISTERING THE ACCOUNT** (what §7
measures). §4 carries the row and the bar: **never widen it to
running, founding, practising or acting.**
**TWO CLAIMS GO STALE ON THEIR OWN** — who administers the account,
and who reads `info@smlcompany.ca`. §7 holds both; §12 **R21** is the
trigger.
⚠️ **THERE ARE THREE COPIES AND THE THIRD IS THE READER'S OWN** — the
handler puts the whole submission into the confirmation it sends the
inquirer. Never write "the one other place a copy exists". */
}
<p>
The record in the table: me, and the small number of people who
administer the account it sits in with me.
</p>
<p>
The system that receives what you send
<strong
>can only add a record — it cannot read back what is stored.</strong
>
</p>
<p>
The notification goes to the practice's mailbox, which is read by me
and by administrative staff and is hosted on Google Workspace — so
Google holds a copy of whatever you send me.
</p>
<p>
The confirmation that went to you sits with whoever runs your email.
That copy is in your hands rather than mine.
</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 it sets no cookies and stores no identifier on
your device.
</p>
) : (
<p>
<strong>This site sets no cookies and runs no analytics.</strong>
There is no tracking script on any page, and there is therefore
nothing to consent to and no banner. If cookies or analytics are
ever introduced, this page changes on the same day and its last
updated date moves with it. Your browser does cache this site's
fonts, stylesheets and images for up to a year so a return visit
loads faster, and those are the same files for every visitor.
</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>
{
/* ⚠️ THE CLAUSE THAT WAS HERE PROMISED TO DISCLOSE THE OUTCOME OF A
CONFLICTS CHECK — *"I will tell you what its outcome was rather than
pretending the inquiry did not happen"* — and that is an UNDERTAKING,
which §4 may publish only where Pouya has made it in terms. He had
not. D20 finding 13, and it is closed by his attestation of
2026-09-03, which covers RUNNING the check and says nothing about
reporting it. The sentence now states what deletion does not undo and
stops there. Do not restore the promise without a second attestation:
it is a different commitment from the one he made. */
}
<p>
Deletion removes the record. It does not retract the emails already
sent, and it does not undo a conflicts check that has already been
run.
</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>