Route support text through detection, redaction, classification and deletion controls with a testable contract for every data sink.
Project: enforce a privacy-safe support-text pipeline
Define the path and owner
A customer submits a ticket that may contain an email, address and order reference. The support desk requires the original inside a restricted case system; analytics and model evaluation receive only approved representations. List every sink, purpose, access role and retention window. Assign an owner to changes in the sink list. Do not begin by training a detector: first remove needless copies of raw text.
Build the redaction boundary
Capture original text in the authorized store, run structured patterns and a reviewed contextual span detector, resolve overlaps, then create a redacted representation. Version the detector and offset policy. Quarantine a case if detection fails before an unrestricted sink. Test examples with mixed scripts, combining marks, several addresses and a false-positive product code. The redaction contract describes exact offsets and replacements.
Gate every output
Enforce field allowlists at analytics, search, debugging and training-export boundaries. Put synthetic canary values in a controlled ticket and verify they appear only where originals are authorized. Run deletion end to end, including caches, indexes and derived datasets; retain an audit of deletion completion without retaining the deleted text. Sink audit and retention supplies the test matrix.
Measure useful and safe behavior
Report PII recall by type on reviewed data, false redaction rate, failed-closed volume, queue-routing quality and deletion lag. Sample production failures under restricted access. A redaction policy that destroys every order reference may satisfy a leak check but break support. Keep the order ID only in the restricted workflow or replace it with a scoped surrogate where needed. Release detector, classifier and sink policy as one reviewed bundle with a rollback plan.
Implementation
def route_ticket_payload(case_id, original_text, redacted_text, sink_policies):
if not case_id or not original_text or not redacted_text:
raise ValueError("case and both representations are required")
outbound = {}
for sink_name, policy in sink_policies.items():
if policy == "restricted-original":
outbound[sink_name] = {"case_id": case_id, "text": original_text,
"representation": "original"}
elif policy == "redacted-only":
outbound[sink_name] = {"case_id": case_id, "text": redacted_text,
"representation": "redacted"}
else:
raise ValueError("unknown sink policy")
return outbound
delivery = route_ticket_payload("case-47", "Email [email protected]",
"Email [EMAIL]", {"case_store": "restricted-original", "analytics": "redacted-only"})
assert delivery["analytics"]["text"] == "Email [EMAIL]"
Performance and operating cost
Routing copies each representation once per configured sink, so local work is O(s·n) in the worst case for s sinks and n text characters. Actual cost includes detector inference, protected storage, deletion propagation and restricted review. Measure p95 intake latency alongside leak findings and deletion lag. For large fan-out, avoid needless payload duplication, but never replace explicit sink policies with an implicit shared raw event.
Common Mistakes
- Sending a raw event to all subscribers and relying on each to redact it.
- Keeping originals in broad analytics because support needs originals elsewhere.
- Treating canary tests as a substitute for reviewed PII recall.
- Forgetting caches and vector indexes during deletion.
