Privacy, PII, Deletion & Right to Be Forgotten · module 6 of 9 · Time, Scale, Multiplicity, Trust · lesson 66 of 84 · ~45 min
$
uv run python curriculum/advanced/redaction-and-minimization/lab/lab.py
Redaction and Minimization¶
In one line. You can throw away the street address entirely, coarsen every other kind, and the exam still passes — only the health value is load-bearing.
Where this sits¶
Stage: govern · Level: advanced · ~45 min
You need first: PII on the Write Path
Concepts assumed: Personal Data · Label, Not Permission · Element Cost
This unlocks: Deletion That Actually Deletes
The problem¶
pii-on-the-write-path measured that refusing personal data breaks the system. Redaction is the middle option — keep the fact, drop the detail — and the only honest way to choose a level is to measure what each one costs.
address
full Priya lives at 47 Halloway Road, Bristol
coarse Priya lives in Bristol
tokenised Priya — <address>
health
full Priya was diagnosed with a gluten intolerance last week
coarse Priya has a gluten intolerance last week
tokenised Priya — <health condition>
| level | kinds | exam |
|---|---|---|
| full | all four | True |
| coarse | all four | True |
| coarse | contact only | True |
| coarse | health only | True |
| tokenised | all four | False |
| tokenised | contact only | True |
| tokenised | health only | False |
Coarsening every kind costs nothing. Tokenising the contact details — destroying the address and the phone number outright — also costs nothing. The only redaction that breaks anything is tokenising health, because the exam needs the word gluten.
Why this isn't RAG¶
Minimisation in a retrieval system is a decision about the corpus, made once, by whoever assembled it. You cannot minimise what you did not write, and the index has no opinion — it will happily return whatever the documents contain.
A memory layer writes every record itself, so minimisation is available at every write and free to choose per record. That is an advantage and a trap: the choice is available so often that it never gets made deliberately, and the default is to store the sentence exactly as it arrived.
Mechanism¶
Coarsening is per kind, because "less precise" is not one operation.
| kind | what coarsening throws away |
|---|---|
| address | the street; keeps the city |
| phone | the number; keeps that one exists |
| health | the diagnosis event; keeps the condition |
| third_party_health | — no useful middle; falls through to a token |
The health rule is the instructive one. "Priya was diagnosed with a gluten intolerance last week" carries a date and a clinical encounter; "Priya has a gluten intolerance" carries the dietary constraint. A question about food needs the second and never the first, and the difference is a regex.
Third-party health has no coarse form, and that is a finding rather than a gap. "Sam is a nurse at St. Aubyn's" coarsened to "Sam is a nurse" still identifies a person's occupation and employer; there is no version of it that is useful to Priya's assistant and not a fact about Sam. It falls through to a token, which is the honest answer: keep that something was said, keep nothing of what.
Redaction changes the id. Memory.id is content-addressed, so a redacted record is a different record — correct, and load-bearing, because a full-detail copy and a redacted copy sharing an id is exactly how the original survives a redaction.
flowchart LR
FCT["<b>a labelled fact</b>"] --> CRS{"<b>coarsen</b><br/><i>per kind — 'less precise'<br/>is not one operation</i>"}
CRS -->|"address"| ADR["keeps the city<br/><i>drops the street</i>"]
CRS -->|"phone"| PHN["keeps that one exists<br/><i>drops the number</i>"]
CRS -->|"health"| HLT["keeps the condition<br/><i>drops the clinical event</i>"]
CRS -->|"third-party health"| TPH["<b>no useful middle</b><br/><i>falls through to a token</i>"]
ADR --> NID["<b>a new id</b><br/><i>content-addressed, so the<br/>redaction is a different record</i>"]
PHN --> NID
HLT --> NID
TPH --> NID
SAM["<b>one id, both copies</b><br/><i>how the full-detail original<br/>survives its own redaction</i>"]:::bad
NID -.->|"never"| SAM
style CRS fill:#f9e79f,stroke:#b7950b,stroke-width:2px
style TPH fill:#aed6f1,stroke:#2874a6
style NID fill:#aed6f1,stroke:#2874a6
classDef bad fill:#f5b7b1,stroke:#c0392b,stroke-dasharray: 4
What the table is really saying¶
Every argument against minimisation is that it might cost something. Here it costs nothing at three of the four kinds, at the most aggressive level available. The address can simply be destroyed. The system that knows where she lives is not more useful than the one that does not — measured against the question it is asked, it is identical.
Design decisions¶
Why measure with the exam rather than by counting what was removed? Because the amount removed is a measure of how much you did, not of what it cost — and here the two point the same way and still tell you nothing. Tokenising the phone number drops 5 tokens and changes nothing; tokenising the health fact drops 7 and breaks the answer. Two tokens apart, opposite outcomes. The only unit that discriminates is whether the system still works.
Why not tokenise everything and accept the loss? Because the loss is the exam, and the exam is the reason the store exists. The point of measuring per kind is that "minimise aggressively" and "keep what is needed" are not in conflict here — they disagree about exactly one kind, and it is knowable which.
Why keep FULL as a level at all? So that the comparison is in the code rather than in a commit message. A redaction policy with no null option cannot be evaluated, and the first question anyone asks about one is "what does it cost?".
Lab¶
You'll implement: redact and apply.
Run:
uv run python curriculum/advanced/redaction-and-minimization/lab/lab.py
Expected output: each kind at all three levels, then the nine-row table — coarse/all four True, tokenised/all four False, tokenised/contact only True.
Stretch: coarsen the health memory and check the id. It changes, because content is the id — so the redacted record does not replace the original unless something explicitly retires it. A redaction that adds a record instead of replacing one has doubled the data it was meant to reduce.
What this adds to the capstone¶
memlab.privacy.redact — Level, redact, apply, and the per-kind coarsening table. Consumes the labels from privacy.classify and enforces nothing on its own; choosing a level is a deployment decision this module prices rather than makes.
Failure modes¶
| Symptom | Cause | How to detect | Mitigation |
|---|---|---|---|
| Redaction breaks answers | Uniform level across all kinds | Run the exam per kind, per level | Choose per kind |
| Full-detail copy survives | Redacted record added, not swapped | Compare ids before and after | Retire the original |
| Third-party data half-hidden | Coarsening applied where none exists | Read the coarse form aloud | Token, not a middle |
| "We minimise" with no evidence | Measured in characters removed | Ask what it cost the answer | Measure with the exam |
| Nothing ever minimised | Choice available at every write, made at none | Check the default level | A null level to compare against |
Check yourself¶
Tokenising the address destroys it entirely and the exam still passes. Why keep the address at all?
Because the exam is one question, and "what's my address?" is another that this corpus does not ask. What the measurement establishes is that the address is not load-bearing for the questions being asked — which is the argument for coarsening it by default and the reason the answer is a per-deployment decision rather than a course-wide one.
Why does health have a coarse form when third-party health does not?
Because there is a version of the health fact that is useful and less revealing — the condition without the diagnosis event. There is no equivalent for "Sam is a nurse at St. Aubyn's": strip the employer and you still have a named person's occupation, and no amount of coarsening makes it a fact about the account holder. When there is no useful middle, the honest form is a token.
The redacted memory has a different id. Is that a problem?
It is the mechanism working. Ids are content-addressed, so changing content produces a new record — which means a redaction must explicitly retire the original, and a policy that only adds the redacted form has doubled the data. The id changing is what makes that failure visible instead of silent.
Connections¶
Stage: govern · Level: advanced · ~45 min
You need first: PII on the Write Path
Concepts assumed: Personal Data · Label, Not Permission · Element Cost
This unlocks: Deletion That Actually Deletes