Skip to content

Type-aware partial masking: last-N per PII type (and initials for names) instead of falling back to [REDACTED] #8

Description

@prPMDev

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions