(motir-marketing) The 30-day erasure promise loses its guard with content/legal/ — no repository asserts that the Privacy Policy §6 number is the one ACCOUNT_ERASURE_WINDOW_DAYS runs on
Opened by Zhu Yue ·
Type: code · Parent: MOTIR-3875 (the discovery epic — the EDGE TEST says no: nothing under MOTIR-4101 is blocked_by this fix, and the deletion card ships whether or not it lands) · Discovered in: MOTIR-4103, while giving every file in the referrer sweep a disposition · Repos: motir-marketing — ONE PR. ⚠️ AMENDED at claim time: the motir-core half is delivered by MOTIR-4103 itself — see Recorded amendment below.
The defect
motir-core promises users a 30-day erasure window in code (ACCOUNT_ERASURE_WINDOW_DAYS) and publishes the same number as a contractual promise in the Privacy Policy. Until now those two were coupled by a test that read the policy off disk:
// motir-core tests/users/dataSubjectRequests.test.ts:56-70 (at f8fb84d80)
const privacy = readFileSync(path.join(process.cwd(), 'content/legal/privacy.md'), 'utf8');
const match = privacy.match(/we erase or anonymise within \*\*(\d+) days\*\*/);
expect(match, 'privacy.md §6 no longer states the erasure window …').not.toBeNull();
expect(Number(match![1])).toBe(ACCOUNT_ERASURE_WINDOW_DAYS);
Its own comment states what it is for: "This assertion is what stops the copy and the behaviour drifting apart in either direction — an edit to the constant with no edit to the policy fails here, and so does the reverse."
MOTIR-4103 deletes content/legal/ from motir-core, so the file that assertion reads is gone and the assertion goes with it. After that PR merges, the number can move on either side and nothing anywhere goes red.
Root cause — VERIFIED, not read off a card
It is not that the guard was forgotten. It is that the coupling it asserted became UNASSERTABLE, because the two things it compared now live in different repositories:
| the half | where it lives now |
|---|---|
ACCOUNT_ERASURE_WINDOW_DAYS = 30 | motir-core lib/users/dataSubjectRequests.ts:43 |
| "we erase or anonymise within 30 days" | motir-marketing content/legal/privacy.md:133 |
Neither repository can read the other at test time, so the assertion has no host. That is a different defect from MOTIR-4133 and is not covered by it: that card re-homed the materiality guard (the semver ⇒ material/non-material convention over a document SET), and motir-marketing's tests/legal/legalMateriality.test.ts says in its own header exactly which two assertions it carries — the exclusion set and the REVIEWED_BASELINE tripwire. Neither is about a NUMBER inside a document being equal to a constant in an application.
The absence, with its scope and its count. In motir-marketing at origin/main — RE-MEASURED at claim time (2026-09-02, origin/main @ e263dbc), and it still holds:
$ git grep -c -E 'erase or anonymise within|ACCOUNT_ERASURE_WINDOW|erasure window' origin/main -- tests lib app
(no output; exit 1 — ZERO matches)
$ git grep -n 'erase or anonymise within' origin/main # the WHOLE repo
content/legal/privacy.md:133: … we erase or anonymise within **30 days**, except where something below applies
The sentence exists in that repository exactly once, in the document itself, and nothing reads it.
Why it matters more than an ordinary lost test
The number is a published commitment under GDPR Art. 13, not an implementation detail. The two failure directions are both silent and both are the kind nobody finds by reading code:
- Counsel revises §6 to 14 days on
motir-marketing;motir-corekeeps erasing at 30 and the company is out of compliance with its own published policy. - Somebody tunes
ACCOUNT_ERASURE_WINDOW_DAYSinmotir-core; the policy still promises 30 and the product quietly under- or over-delivers against a contract.
MOTIR-3705's own history is the precedent for the first direction: §6 has already been revised once by counsel (PR #2427), and the guard is what made that revision safe to make.
⚠️ Recorded amendment (2026-09-02, at claim time) — the motir-core half is ALREADY WRITTEN, by MOTIR-4103
The card's premise held; its SCOPE did not. motir run MOTIR-4233's prior-art read found the second acceptance criterion already delivered — unlanded and uncommitted — on the live worktree of MOTIR-4103, the very card this bug was filed from. git -C motir-core-subtask-4103 diff -- tests/users/dataSubjectRequests.test.ts shows that card re-pointing the assertion exactly as criterion 2 asked:
+ it('equals the number the published Privacy Policy §6 states to users', () => {
+ // https://motir.co/legal/privacy §6 ("How long we keep it"), verbatim:
+ // "After you delete it, we erase or anonymise within **30 days**, …"
+ const PUBLISHED_ERASURE_WINDOW_DAYS = 30;
+ expect(ACCOUNT_ERASURE_WINDOW_DAYS).toBe(PUBLISHED_ERASURE_WINDOW_DAYS);
});
and its comment already names this card as owing the other half ("bug MOTIR-4233, which owes motir-marketing the guard that reads §6"). It also re-points the ACCOUNT_ERASURE_WINDOW_DAYS doc-comment from content/legal/privacy.md to motir.co/legal/privacy.
Two consequences, both applied to this card rather than written down and left:
- Criterion 2 is CUT. Rebuilding it here would mean branching off
origin/mainand re-writing a sibling's in-flight work into a conflicting diff on the same file — the one thing the prior-art rule forbids outright. It is not deferred and it is not unowned: it is delivered under MOTIR-4103's key, which is a card in a ready set rather than a paragraph. targetReposis narrowed to["motir-marketing"]— a field edit, not a sentence. The repository set is a completion GATE: left at both, this card would hang at In Review forever, deferred on amotir-corepull request that will never be opened for it, because that work ships under MOTIR-4103's.
No blocked_by edge is owed to MOTIR-4103, and that is a decision rather than an omission. Nothing in the surviving scope consumes it: the guard below reads a document in this repository and pins a literal, and its cross-reference names ACCOUNT_ERASURE_WINDOW_DAYS and its test file — both of which are true before MOTIR-4103 lands and after. (MOTIR-4103 also sits under story MOTIR-4101 while this card sits under epic MOTIR-3875, so a leaf-to-leaf edge across those containers is the shape gate 7 refuses anyway.)
Sized down with the scope: 3 points / 45 min → 2 points / 30 min.
Filed as a planning bug under MOTIR-1465.
Fix direction — chosen: (1), the two-halves repair
The coupling cannot be restored as one assertion, so it is restored as two halves that each name the other, and the honest cost is that neither half can see its counterpart:
motir-marketing— a guard besidetests/legal/legalMateriality.test.tsthat readscontent/legal/privacy.md, extracts the retention window from §6 with the same regex, and asserts it against a named constant in that repository (PUBLISHED_ERASURE_WINDOW_DAYS = 30). This is the half with the reviewable DIFF: a counsel edit to §6 goes red here. ← THIS CARD.motir-core— re-pointtests/users/dataSubjectRequests.test.tssoACCOUNT_ERASURE_WINDOW_DAYSis asserted against the published literal, quoting the sentence and its URL (https://motir.co/legal/privacy§6) rather than a file path. A constant edit goes red there. ← DELIVERED BY MOTIR-4103, see the amendment above.
A THIRD option was weighed and NOT taken: put the number in the manifest. MOTIR_LEGAL_DOCUMENTS is already the seam through which motir-core learns about published documents (MOTIR-4007), and a retentionDays field on the privacy entry would make the coupling a single runtime assertion again rather than two literals that agree by convention. It is the only option that makes the coupling machine-checkable again — and it costs a manifest schema change plus a re-provision of the deployment (MOTIR-4012's act), which is operator work outside both repositories and outside a 30-minute bug fix. The reasoning is recorded in the PR body so the option stays live rather than being silently dropped.
Whichever is chosen, say in both repositories' comments that the other half exists and where. Two independent literals with no cross-reference is how this defect recurs.
Acceptance criteria
motir-marketingcarries a test that reads the retention window out ofcontent/legal/privacy.md§6 and fails when that number changes without the guard's own constant changing — quoted in the PR body with the failing output from a deliberate edit.— CUT at claim time: delivered by MOTIR-4103. See Recorded amendment.motir-core'stests/users/dataSubjectRequests.test.tsassertsACCOUNT_ERASURE_WINDOW_DAYSagainst the published number- The
motir-marketingguard's comment names themotir-corehalf — the constant, its file, and the test that pins it — so a reader of either half can find the other in one grep. (Themotir-coreside of the cross-reference is already written by MOTIR-4103, which names this card by key.) - The PR body states which of the three fix directions was taken and why the other two were not — in particular, whether the manifest was extended, since that is the only one that makes the coupling machine-checkable again.
- Coverage: ≥90% on any file modified; the files
vitest.config.mtspins stay at their thresholds.
Resolution
open
Context refs
motir-core/lib/users/dataSubjectRequests.ts:43—ACCOUNT_ERASURE_WINDOW_DAYSmotir-core/tests/users/dataSubjectRequests.test.ts— the guard as it stood atf8fb84d80, and where its replacement goes (delivered by MOTIR-4103)motir-marketing/content/legal/privacy.md:133— the published sentencemotir-marketing/tests/legal/legalMateriality.test.ts— MOTIR-4133's re-homed guard, and its header stating what it does NOT cover; this card's guard is modelled on itmotir-core/lib/legal/documents.ts— the manifest reader, if the third fix direction is ever taken- MOTIR-4103 — the deletion that removes the guard's subject, and the card that delivers the
motir-corehalf - MOTIR-3705 — the counsel revision of §6 that this guard made safe. ⚠️ The original card's link for this ref carried the cuid of MOTIR-3667, an unrelated research card; corrected here.
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.