feat(legal): phase 6 — the privacy policy and the terms #9

Merged
whitlocktech merged 1 commits from feat/phase-6-legal into main 2026-08-24 09:28:41 +00:00
Member

PLAN.md §9. /privacy, /terms, the footer links, and the Play Data Safety notes.

AI-assisted: written with Claude Code (Claude Opus 5).


The four decisions, taken before either page was written

Recorded in PLAN.md §9 under "How phase 6 built the legal pages" — the count of record is now thirty-three.

Decision Why
D30 DNS-only records; the reverse proxy on the host keeps the only access log. Described qualitatively — no retention number quoted. 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 becomes wrong: an edge provider that terminates TLS is a processor.
D31 Eighteen or older. Above the children's-consent threshold in every EEA state, so consent works as a basis with no parental-consent machinery this form could not honestly operate. Chosen over 13 (below that threshold in several states) and 16 (sufficient, no simpler).
D32 No governing-law clause. Nothing of value is contracted for: the site sells nothing, the software is free under a licence carrying its own terms, the beta is a list of addresses people asked to be on. Adding one later is a clause, not a rewrite.
D33 PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in CI. §9 says the declaration is "filled from section 2". One array, two renderers — the published policy and the answers given to Google cannot drift.

What the pages are

/privacy is three separately-scoped sections, because "we" means three different parties and §9 is explicit that conflating them "would be wrong in both directions":

  1. This site — one form. No cookies, no analytics, no third-party requests: not a promise, a description of what the pages load. The signup row, the salted IP hash (never the address), the truncated user agent, the access log.
  2. The Android appwe operate no server it talks to. The rows are mostly what the device holds, because we receive nothing. Includes the fact worth stating precisely: a push notification carries no content, so the relay learns nothing.
  3. Self-hosted deployments — the operator is the controller, not us. Written so an operator sees what they are taking on and a player never mistakes this page for the one governing their community's site.

Every row names the file it was read out of. checkFacts.mjs cannot verify prose, so the citation is what a reviewer uses instead — and a row that cannot name its source is a row somebody guessed.

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

Also in this PR

  • The age clause changed CONSENT_TEXT, which is stored per row rather than versioned — so rows written from now on carry the new sentence and older rows keep theirs. CONSENT_VERSION gained a suffix rather than a new date: the change landed on the day the original wording was written, and two sentences must not share the label an operator groups a CSV by.
  • PLANNED_ROUTES is now empty. These were its last two entries, and its reverse check is what forced the deletion. The list stays — §10's documentation routes land in phases 7 and 8 under the same convention.
  • test/legal.test.mjs asserts what no build check can see: every scope renders something (an empty section reads as a claim, not an omission), every app row maps to a console question, and every mapped row still answers "not collected, not shared" — failing with the reason, so a telemetry endpoint added later cannot produce a row that contradicts the lede three inches above it.
  • --check normalises line endings. The repo has no .gitattributes and Windows checkouts run core.autocrlf=true, so a byte comparison would fail for every Windows developer while passing in CI — the worst shape a check can have.

Verification

  • npm run verify green end to end: tokens · brand · data safety · astro check (0 errors) · 36 tests · build · 214 links · 19 facts.
  • Both pages walked in a real browser; neither overflows at 390px.
  • The check was proved in both directions: a CRLF copy passes, a one-word edit fails with the message naming the data file.
  • 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 that read as missing content. Same lesson as phase 4's cascade defect and phase 5's literal backticks.

🤖 Generated with Claude Code

PLAN.md §9. `/privacy`, `/terms`, the footer links, and the Play Data Safety notes. **AI-assisted:** written with Claude Code (Claude Opus 5). --- ## The four decisions, taken before either page was written Recorded in `PLAN.md` §9 under "How phase 6 built the legal pages" — the count of record is now **thirty-three**. | | Decision | Why | |---|---|---| | **D30** | **DNS-only records; the reverse proxy on the host keeps the only access log.** Described qualitatively — no retention number quoted. | 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 becomes wrong**: an edge provider that terminates TLS is a processor. | | **D31** | **Eighteen or older.** | Above the children's-consent threshold in every EEA state, so consent works as a basis with no parental-consent machinery this form could not honestly operate. Chosen over 13 (below that threshold in several states) and 16 (sufficient, no simpler). | | **D32** | **No governing-law clause.** | Nothing of value is contracted for: the site sells nothing, the software is free under a licence carrying its own terms, the beta is a list of addresses people asked to be on. Adding one later is a clause, not a rewrite. | | **D33** | **`PLAY_DATA_SAFETY.md` is generated from `src/data/collection.mjs` and checked in CI.** | §9 says the declaration is "filled from section 2". One array, two renderers — the published policy and the answers given to Google cannot drift. | ## What the pages are **`/privacy` is three separately-scoped sections**, because "we" means three different parties and §9 is explicit that conflating them "would be wrong in both directions": 1. **This site** — one form. No cookies, no analytics, no third-party requests: not a promise, a description of what the pages load. The signup row, the salted IP hash (never the address), the truncated user agent, the access log. 2. **The Android app** — **we operate no server it talks to.** The rows are mostly what the *device* holds, because we receive nothing. Includes the fact worth stating precisely: a push notification carries no content, so the relay learns nothing. 3. **Self-hosted deployments** — the operator is the controller, not us. Written so an operator sees what they are taking on and a player never mistakes this page for the one governing their community's site. Every row names the file it was read out of. `checkFacts.mjs` cannot verify prose, so the citation is what a reviewer uses instead — and a row that cannot name its source is a row somebody guessed. **`/terms` governs only what we run**: this site, the beta list, the APK we publish. The software is governed by its licence; a community's deployment by that community. A terms page claiming authority over every installation of a GPL program is both unenforceable and the single thing a generated template most reliably gets wrong. ## Also in this PR - **The age clause changed `CONSENT_TEXT`**, which is stored per row rather than versioned — so rows written from now on carry the new sentence and older rows keep theirs. `CONSENT_VERSION` gained a suffix rather than a new date: the change landed on the day the original wording was written, and two sentences must not share the label an operator groups a CSV by. - **`PLANNED_ROUTES` is now empty.** These were its last two entries, and its reverse check is what forced the deletion. The list stays — §10's documentation routes land in phases 7 and 8 under the same convention. - **`test/legal.test.mjs`** asserts what no build check can see: every scope renders something (an empty section reads as a claim, not an omission), every app row maps to a console question, and **every mapped row still answers "not collected, not shared"** — failing with the reason, so a telemetry endpoint added later cannot produce a row that contradicts the lede three inches above it. - **`--check` normalises line endings.** The repo has no `.gitattributes` and Windows checkouts run `core.autocrlf=true`, so a byte comparison would fail for every Windows developer while passing in CI — the worst shape a check can have. ## Verification - `npm run verify` green end to end: tokens · brand · **data safety** · `astro check` (0 errors) · **36 tests** · build · **214 links** · **19 facts**. - Both pages walked in a real browser; neither overflows at 390px. - The check was proved in both directions: a CRLF copy passes, a one-word edit fails with the message naming the data file. - **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 that read as missing content. Same lesson as phase 4's cascade defect and phase 5's literal backticks. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-08-24 09:22:22 +00:00
feat(legal): phase 6 — the privacy policy and the terms
All checks were successful
PR checks / checks (pull_request) Successful in 55s
a2faf07104
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>
whitlocktech merged commit 971fa9c032 into main 2026-08-24 09:28:41 +00:00
whitlocktech deleted branch feat/phase-6-legal 2026-08-24 09:28:41 +00:00
Sign in to join this conversation.
No description provided.