Problem
"Mask" mode is meant to partially hide a value (reveal a recognizable sliver, like XXX-XX-6789). Today that only works for digit-shaped IDs. For every other PII type the pattern's mask is null, so Mask mode silently falls back to [REDACTED] — identical to Label mode.
Worse, the initials helper maskName ("Michael Thompson" → "M. T.") exists but is wired only to custom terms. So the same name behaves differently by detection source:
| How a name is detected |
Mask-mode output |
| Typed as a custom term |
M. T. (initials) |
| Caught by the GLiNER model (usual path) |
[REDACTED] |
| Caught by the Named Fields regex |
[REDACTED] |
A user who picks "Mask" expecting partial-everywhere gets full redaction for most names/addresses without any signal that it happened.
Current state (which patterns have a real partial mask)
- Have a partial mask: SSN/ITIN, Bank Account, Credit Card, Phone, Email, PAN, Labeled ID.
mask: null → fall back to [REDACTED]: Routing, EIN, Passport, Visa, Alien/USCIS#, Street Address, City/State ZIP, DOB, Named Fields (names), Dollar Amounts, Zip Code, and all GLiNER model labels (person, address, …).
Proposed enhancement
Give every PII type a type-appropriate Mask behavior — "reveal the last N relevant units for that type" — generalizing the SSN "last 4" idea:
- Number/ID types → keep the last N meaningful chars (extend to Routing, EIN, Passport, Visa, Alien#, etc.; pick N per type).
- Names (GLiNER person + Named Fields regex + custom terms) → initials (
M. T.). Reuse maskName and wire it to the model's person label and the regex name pattern, not just custom terms.
- Other types (address, DOB, amount, zip) → define a sensible partial (e.g. keep ZIP first 3 / city, keep DOB year, keep amount order-of-magnitude) or, where no meaningful partial exists, fall back to a typed tag (
[NAME], [ADDRESS]) rather than the generic [REDACTED], so the fallback is intentional and legible.
Principle
"Mask" should always mean partial, consistently — it should never silently equal full redaction. If a type truly can't be partially masked, say so explicitly with a typed placeholder.
Acceptance criteria
- Every active regex pattern and every GLiNER label has a defined Mask behavior (a partial fn, or an explicit typed fallback — no silent
[REDACTED]).
- A detected name masks to initials regardless of source (model / regex / custom).
- Per-type mask output covered by tests (
tests/test_engine.py + ui/src/engine.test.ts).
Pointers
ui/src/engine.ts: maskName (~L134), masks block (~L128–135), CUSTOM_PAT (~L246, the only maskName user), Named Fields mask: null (~L172), PATTERNS mask fields, MODES apply + [REDACTED] fallback (~L237–241).
ui/src/gliner-detector.ts: GlinerLabel.mask field (~L29), tagOf (~L34) — wire a mask onto name-like labels.
- Keep
redactabit.py (the Python engine) in sync — it mirrors the same masks/patterns.
Problem
"Mask" mode is meant to partially hide a value (reveal a recognizable sliver, like
XXX-XX-6789). Today that only works for digit-shaped IDs. For every other PII type the pattern'smaskisnull, so Mask mode silently falls back to[REDACTED]— identical to Label mode.Worse, the initials helper
maskName("Michael Thompson" → "M. T.") exists but is wired only to custom terms. So the same name behaves differently by detection source:M. T.(initials)[REDACTED][REDACTED]A user who picks "Mask" expecting partial-everywhere gets full redaction for most names/addresses without any signal that it happened.
Current state (which patterns have a real partial mask)
mask: null→ fall back to[REDACTED]: Routing, EIN, Passport, Visa, Alien/USCIS#, Street Address, City/State ZIP, DOB, Named Fields (names), Dollar Amounts, Zip Code, and all GLiNER model labels (person, address, …).Proposed enhancement
Give every PII type a type-appropriate Mask behavior — "reveal the last N relevant units for that type" — generalizing the SSN "last 4" idea:
M. T.). ReusemaskNameand wire it to the model's person label and the regex name pattern, not just custom terms.[NAME],[ADDRESS]) rather than the generic[REDACTED], so the fallback is intentional and legible.Principle
"Mask" should always mean partial, consistently — it should never silently equal full redaction. If a type truly can't be partially masked, say so explicitly with a typed placeholder.
Acceptance criteria
[REDACTED]).tests/test_engine.py+ui/src/engine.test.ts).Pointers
ui/src/engine.ts:maskName(~L134), masks block (~L128–135),CUSTOM_PAT(~L246, the onlymaskNameuser),Named Fieldsmask: null(~L172),PATTERNSmask fields,MODESapply +[REDACTED]fallback (~L237–241).ui/src/gliner-detector.ts:GlinerLabel.maskfield (~L29),tagOf(~L34) — wire amaskonto name-like labels.redactabit.py(the Python engine) in sync — it mirrors the same masks/patterns.