symfonic.tools.corpus_scrub¶
corpus_scrub ¶
symfonic.tools.corpus_scrub -- PII redaction for Phase 12 corpus exports.
Public adopter-facing module for scrubbing real-production episodic data BEFORE it leaves the adopter's environment. Required for the v7.9.0 LLM-extractor flip: the symfonic-side eval harness consumes scrubbed JSONL files; raw production data MUST NOT cross the boundary.
Design contract (v7.8.0):
- Scrubbing happens ADOPTER-SIDE. symfonic-core ships the regex patterns + tokeniser; the adopter runs them locally and uploads only the scrubbed output.
- Opaque-PII tokens are STABLE per-export: the same input string
always maps to the same
<REDACTED-EMAIL-N>within one corpus so the LLM-extractor can still learn co-occurrence patterns ("user mentionstwice in a conversation"). - Drop-record-on-doubt: rows with more than
unresolved_thresholdunresolved matches (suspicious values that look like PII but don't fit a known pattern) are dropped from the output. - Amazon-specific: ASINs and marketplace-IDs are STRUCTURAL identifiers (not PII) and are KEPT. Seller-IDs and account-IDs are PII and are REDACTED.
Usage::
from symfonic.tools.corpus_scrub import CorpusScrubber, scrub_jsonl_file
scrubber = CorpusScrubber()
scrub_jsonl_file(
input_path=Path("dryrun_corpus.jsonl"),
output_path=Path("dryrun_corpus.scrubbed.jsonl.gz"),
scrubber=scrubber,
)
Or via the CLI::
python -m symfonic.tools.corpus_scrub \
--input dryrun_corpus.jsonl \
--output dryrun_corpus.scrubbed.jsonl.gz
v7.8.0 (Jarvio v7.9.0 corpus-flip ask).
CorpusScrubber ¶
CorpusScrubber(
*,
value_credential_patterns: Iterable[
tuple[str, Pattern[str]]
] = DEFAULT_VALUE_CREDENTIAL_PATTERNS,
pii_patterns: Iterable[
tuple[str, Pattern[str]]
] = DEFAULT_PII_PATTERNS,
amazon_keep_patterns: Iterable[
tuple[str, Pattern[str]]
] = AMAZON_KEEP_PATTERNS,
amazon_redact_patterns: Iterable[
tuple[str, Pattern[str]]
] = AMAZON_REDACT_PATTERNS,
unresolved_threshold: int = 3,
)
Apply redaction rules to a corpus row.
Token-mapping state is instance-scoped. Construct one scrubber
per corpus export so co-occurrence tokens (<REDACTED-EMAIL-1>)
are stable within the export and not leaked across exports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value_credential_patterns
|
Iterable[tuple[str, Pattern[str]]]
|
Credential-shaped substring
patterns applied to VALUES. Default:
:data: |
DEFAULT_VALUE_CREDENTIAL_PATTERNS
|
pii_patterns
|
Iterable[tuple[str, Pattern[str]]]
|
Generic-PII patterns. Default:
:data: |
DEFAULT_PII_PATTERNS
|
amazon_keep_patterns
|
Iterable[tuple[str, Pattern[str]]]
|
Amazon structural identifiers to KEEP
as-is. Default: :data: |
AMAZON_KEEP_PATTERNS
|
amazon_redact_patterns
|
Iterable[tuple[str, Pattern[str]]]
|
Amazon account-id patterns to REDACT.
Default: :data: |
AMAZON_REDACT_PATTERNS
|
unresolved_threshold
|
int
|
Maximum unresolved substring count
allowed per row before |
3
|
Source code in src/symfonic/tools/corpus_scrub/scrubber.py
scrub_row ¶
Scrub a single corpus row.
When drop_on_doubt is True and the unresolved count
exceeds unresolved_threshold, the row is dropped and
ScrubResult.scrubbed is None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
Any
|
The JSON-shaped row to scrub. Typically a dict. |
required |
drop_on_doubt
|
bool
|
When True (default), enforce
|
True
|
Source code in src/symfonic/tools/corpus_scrub/scrubber.py
scrub_string ¶
Redact a single string.
Returns:
| Type | Description |
|---|---|
tuple[str, dict[str, int], int]
|
|
Algorithm
- Find all keep-pattern spans; mark them as protected.
- For each redact pattern, replace matches OUTSIDE the protected spans with stable per-label tokens.
- Unresolved count is held at 0 in v7.8.0 -- adopters who want heuristic unresolved detection subclass and override.
Source code in src/symfonic/tools/corpus_scrub/scrubber.py
scrub_value ¶
Recursively scrub a JSON-shaped value.
Strings are passed to :meth:scrub_string. Dicts and lists
recurse. Other types (int, float, bool, None) pass through.
Source code in src/symfonic/tools/corpus_scrub/scrubber.py
ScrubResult
dataclass
¶
ScrubResult(
scrubbed: Any | None,
match_counts: dict[str, int] = dict(),
unresolved: int = 0,
dropped_reason: str | None = None,
)
Per-row scrub outcome.
Attributes:
| Name | Type | Description |
|---|---|---|
scrubbed |
Any | None
|
The redacted row, or |
match_counts |
dict[str, int]
|
Mapping |
unresolved |
int
|
Count of unresolved suspicious substrings (drop
threshold check is the caller's responsibility -- the
scrubber returns the count but only enforces threshold on
|
dropped_reason |
str | None
|
Human-readable reason when |
scrub_jsonl_file ¶
scrub_jsonl_file(
*,
input_path: Path,
output_path: Path,
scrubber: CorpusScrubber | None = None,
drop_on_doubt: bool = True,
) -> dict[str, Any]
Stream a JSONL file through :class:CorpusScrubber.
Input may be plain .jsonl or gzipped .jsonl.gz. Output
extension determines compression (.gz -> gzip). Per architect
spec, the canonical output shape is .jsonl.gz so the file is
transferable without further compression.
Returns a summary dict
{
"input_rows": int,
"output_rows": int,
"dropped_rows": int,
"match_counts": dict[str, int],
}
The summary is also the natural shape for a manifest sidecar.