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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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