feat(legal): phase 6 — the privacy policy and the terms
All checks were successful
PR checks / checks (pull_request) Successful in 55s

PLAN.md §9. Builds /privacy and /terms, links them from the footer on every page,
and generates the Play Data Safety notes from the same inventory the policy renders.

Four decisions taken by the org lead before either page was written, recorded in
§9 under "How phase 6 built the legal pages":

  D30  DNS-only records, so the reverse proxy on the host keeps the only access
       log. Described qualitatively — the retention belongs to the proxy, and a
       policy that quotes a number the deployment does not enforce is worse than
       one that does not.
  D31  Eighteen or older. Above the children's-consent threshold everywhere in the
       EEA, so consent works with no parental-consent machinery this form could not
       honestly operate. Four surfaces render it from src/data/legal.mjs, and every
       one says plainly that nothing verifies it.
  D32  No governing-law clause. Nothing of value is contracted for here.
  D33  PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in
       CI, so the published policy and the answers given to Google cannot drift.

/privacy is three separately-scoped sections because "we" means three different
parties: this site (one form, no cookies, no third-party requests), the Android app
(we operate no server it talks to — the rows are what the DEVICE holds), and a
self-hosted deployment (the operator is the controller, not us). Every row names the
file it was read out of, because a policy is the document most likely to be written
from a template and least likely to be re-read against the software.

/terms governs only what we run: this site, the beta list, and the APK we publish.
The software is governed by its licence, and a community's deployment by that
community — a terms page claiming authority over every install of a GPL program is
the thing a generated template gets wrong.

Also here:
  - the age clause changed CONSENT_TEXT, so CONSENT_VERSION gained a suffix; rows
    written from now on carry the new sentence and older rows keep theirs
  - PLANNED_ROUTES is now empty — these were its last two entries, and its reverse
    check is what forced the deletion; the list stays for phases 7 and 8
  - test/legal.test.mjs asserts the structural promises no build check can see,
    including that every mapped Play row still answers "not collected, not shared"
  - --check normalises line endings: the repo has no .gitattributes and Windows
    checkouts are CRLF, so a byte comparison would fail for every Windows developer
    while passing in CI

Verified: npm run verify green end to end (tokens, brand, data safety, astro check,
36 tests, build, 214 links, 19 facts), both pages walked in a browser, and neither
overflows at 390px. One defect the checks could not see and a look could: the
retention line was being pushed to the foot of the tallest card in its row, opening
a void in the middle of the short ones.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-24 04:21:47 -05:00
parent 29c96d0b21
commit a2faf07104
15 changed files with 1810 additions and 18 deletions

61
PLAN.md
View File

@@ -236,7 +236,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
somewhere other than the thing it decided. The count of record is **twenty-five**:
somewhere other than the thing it decided. The count of record is **thirty-three**:
| # | Where | What it settled |
|---|---|---|
@@ -244,6 +244,7 @@ somewhere other than the thing it decided. The count of record is **twenty-five*
| D17D19 | §10, "How phase 3 built the homepage" | The data-path diagram, all five groups on the homepage, the emblem-led hero |
| D20D25 | §10, "How phase 4 built the marketing pages" | `/features/` as the same list with detail, `/architecture/` as reasons not reference, the absences as data, the two absorbed scope items, `needsModule`, the demo deep links |
| D26D29 | §8, "How phase 5 built the app and the beta" | The screenshot slot reserved for phase 9, the demo as the tester target, `/beta` handling its own POST, equal billing for the APK and the beta |
| D30D33 | §9, "How phase 6 built the legal pages" | One logging hop and no edge provider, eighteen or older, no governing-law clause, the Data Safety notes as a generated document |
---
@@ -593,6 +594,64 @@ because it cannot.
Both pages are linked from the footer on every page, and `/privacy` is the URL given to Play.
### How phase 6 built the legal pages
Four decisions taken before either page was written (org lead, 2026-08-24).
**D30 — One hop in front of the site, and the page says so.** §9 requires `/privacy` to state the
access logs and their retention, which needed a fact rather than a guess. The domain's DNS is on
Cloudflare but the records are **DNS-only**: no edge provider terminates the connection, so the
reverse proxy on the org lead's own host keeps the only access log there is — IP, path, user agent,
timestamp — read when something is broken or being attacked, rotated on the proxy's own schedule.
The page describes it qualitatively rather than quoting a retention number, because the number
belongs to the proxy's configuration and a policy that states one the deployment does not enforce is
worse than one that does not. **If the record is ever proxied, this section is wrong and has to be
rewritten** — an edge provider that terminates TLS is a processor, and D9's "no third-party
requests" would still be true of the browser while ceasing to be the whole story.
**D31 — Eighteen or older.** Play asks, and the answer decides whether consent alone is a lawful
basis in the EEA. Eighteen was chosen over thirteen (Google's own account minimum, but below the
children's-consent threshold in several EEA states, so a 1315 year old's consent would need a
parent's — which this form cannot obtain) and over sixteen (sufficient, but no simpler to state).
The number lives in `src/data/legal.mjs` because four surfaces render it: `/terms`, `/privacy`, the
eligibility list on `/beta`, and the consent sentence itself. **Nothing verifies it and no surface
implies otherwise** — the pages say in as many words that ticking the box is the whole of it, which
is both accurate and the only claim the code supports.
Adding the clause changed `CONSENT_TEXT`, which is stored per row rather than versioned — so rows
written from now on carry the new sentence and older ones keep theirs. `CONSENT_VERSION` gained a
suffix rather than a new date, because the change landed on the day the original wording was
written and two different sentences must not share the label an operator groups a CSV by.
**D32 — No governing-law clause.** Nothing of value is contracted for on this site: it sells
nothing, the software is free under a licence that carries its own terms, and the beta is a list of
addresses people asked to be on. A jurisdiction clause here would be decoration, and §9's standard
for these pages is that accurate and specific beats boilerplate. It stays available: adding one
later is a clause, not a rewrite.
**D33 — The Play Data Safety notes are a generated repository document.** §9 says the declaration is
"filled from section 2, and section 2 is written knowing that is what it is for" — so the two are
one array, `src/data/collection.mjs`, rendered by `/privacy` as prose and by
`scripts/playDataSafety.mjs` as the console's own questions into a committed
`PLAY_DATA_SAFETY.md`. `--check` regenerates and fails if the committed copy differs, and CI runs
it, so a hand edit is a red build that names the data file to edit instead. The document is
operator-facing rather than published: it is a form's worth of console vocabulary no visitor is
looking for, and `/privacy` already says the same things in prose.
Two properties of that file are worth keeping. **It does not pretend to know Play's current
definitions** — there is no API to read them from and the requirements have changed more than once
(the same reason `playPolicy` carries a `verifiedOn` date), so it holds the facts arranged as the
console arranges its questions, with the answer each fact supports and why; a person reads the
console's wording against them. And **a test asserts that every mapped row answers "not collected,
not shared"**, failing with the reason rather than a diff: "we operate no server the app talks to"
is the premise of the whole section, and a telemetry endpoint added later must not be able to
produce a row that quietly contradicts the lede three inches above it.
**What phase 6 also closed.** `/privacy/` and `/terms/` were the last two entries in
`checkLinks.mjs`'s `PLANNED_ROUTES`; building them emptied the list, and its reverse check is what
forced the deletion. The list itself stays, because §10's documentation routes land in phases 7 and 8
under the same convention.
---
## 10. Information architecture