Skip to content

Commit 4de587e

Browse files
authored
Merge pull request #149 from DataFog/fix/4.5-release-review
fix: 4.5.0 release-review fixes — detection parity, changelog, release metadata
2 parents 76d1645 + d01a01e commit 4de587e

10 files changed

Lines changed: 114 additions & 32 deletions

File tree

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 4.4.0a5
2+
current_version = 4.5.0b5
33
commit = True
44
tag = True
55
tag_name = v{new_version}

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ repos:
3636
exclude: |
3737
(?x)^(
3838
.venv|
39+
docs/audit/.*|
3940
.*\.github/workflows/.*\.ya?ml$
4041
)$
4142

CHANGELOG.MD

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
# ChangeLog
22

3-
## [Unreleased]
3+
## [2026-07-02]
44

55
### `datafog-python` [4.5.0]
66

7+
#### Behavior Changes Since 4.4.0
8+
9+
- Agent guardrail helpers (`sanitize`, `scan_prompt`, `filter_output`,
10+
`create_guardrail`, and the `Guardrail` class) now default to
11+
`engine="regex"` instead of `engine="smart"`, matching the top-level
12+
`scan`/`redact`/`protect` defaults. This keeps the core install from
13+
probing optional NLP dependencies, but means NER-backed entities
14+
(PERSON, ORGANIZATION, LOCATION) are no longer detected by these
15+
helpers unless requested. **Migration:** pass `engine="smart"` to
16+
restore the 4.4.0 behavior (requires `datafog[nlp]` or
17+
`datafog[nlp-advanced]`).
18+
- `DataFog.detect()` / `DataFog.scan_text()` result dictionaries now
19+
contain keys only for the labels active under the configured locales:
20+
the seven base labels by default, plus the `DE_*` labels when
21+
constructed with `locales=["de"]`.
22+
- Supported Python versions are `>=3.10,<3.14`. Python 3.14 is not yet
23+
certified and is intentionally excluded from this release.
24+
725
#### Release Thesis
826

927
- Frames 4.5.0 as a focused, lightweight text PII screening release rather
@@ -25,11 +43,15 @@
2543

2644
- Adds regex-only German structured PII support without adding core
2745
dependencies.
28-
- Detects German VAT IDs and German IBANs by default because their country-code
29-
structure is precise enough for default screening.
30-
- Enables broader German identifiers only through `locales=["de"]` or explicit
31-
entity selection, including German tax IDs, pension insurance numbers,
32-
postal codes, passport numbers, and residence permit numbers.
46+
- All German identifiers are locale-gated: they activate only through
47+
`locales=["de"]` or explicit entity selection. This covers German VAT
48+
IDs, IBANs, tax IDs, pension insurance numbers, postal codes, passport
49+
numbers, and residence permit numbers. Default (no-locale) detection
50+
behavior is unchanged from 4.4.0.
51+
- When German locale support is active, overlapping matches (e.g. the
52+
nine-digit run inside a VAT ID also matching the generic SSN pattern)
53+
are resolved by engine-level span-overlap suppression in favor of the
54+
more specific German label.
3355

3456
#### Optional Profiles And Python 3.13
3557

@@ -61,9 +83,9 @@
6183
- Adds a 4.5 release-readiness checklist covering docs build, formatting,
6284
core no-network checks, install-profile smoke checks, German regex tests,
6385
broad non-slow tests, package build checks, and final CI status.
64-
- Clarifies the version alignment path: the development package remains
65-
`4.4.0a5` until stable release promotion, and the final stable release should
66-
publish as `4.5.0`.
86+
- Clarifies the version alignment path: development prereleases publish as
87+
`4.5.0aN`/`4.5.0bN` from `dev`, and the stable release publishes as
88+
`4.5.0` via the Release workflow's `stable` dispatch from `main`.
6789

6890
## [2026-02-13]
6991

datafog/agent.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ def filter(self, text: str) -> RedactResult:
4444

4545
@dataclass
4646
class Guardrail:
47-
"""Reusable text guardrail for wrapping LLM prompts and outputs."""
47+
"""Reusable text guardrail for wrapping LLM prompts and outputs.
48+
49+
Defaults to the lightweight regex engine (changed from "smart" in 4.5.0)
50+
so the core install never probes optional NLP dependencies; pass
51+
``engine="smart"`` to restore NER-backed detection.
52+
"""
4853

4954
entity_types: Optional[list[str]] = None
5055
locales: Optional[list[str]] = None
@@ -119,6 +124,10 @@ def sanitize(text: str, engine: str = "regex", **kwargs: Any) -> str:
119124
One-liner PII removal.
120125
121126
Returns the redacted text only.
127+
128+
Uses the lightweight regex engine by default (changed from "smart" in
129+
4.5.0); pass ``engine="smart"`` for NER-backed detection, which requires
130+
the optional NLP extras.
122131
"""
123132
result = scan_and_redact(text=text, engine=engine, **kwargs)
124133
return result.redacted_text
@@ -127,13 +136,19 @@ def sanitize(text: str, engine: str = "regex", **kwargs: Any) -> str:
127136
def scan_prompt(prompt: str, engine: str = "regex", **kwargs: Any) -> ScanResult:
128137
"""
129138
Scan an LLM prompt for PII without modifying the input text.
139+
140+
Uses the lightweight regex engine by default (changed from "smart" in
141+
4.5.0); pass ``engine="smart"`` for NER-backed detection.
130142
"""
131143
return scan(prompt, engine=engine, **kwargs)
132144

133145

134146
def filter_output(output: str, engine: str = "regex", **kwargs: Any) -> RedactResult:
135147
"""
136148
Scan and redact PII from model output before returning to users.
149+
150+
Uses the lightweight regex engine by default (changed from "smart" in
151+
4.5.0); pass ``engine="smart"`` for NER-backed detection.
137152
"""
138153
return scan_and_redact(output, engine=engine, **kwargs)
139154

datafog/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ def detect(self, text: str) -> dict:
184184
_start = _time.monotonic()
185185

186186
scan_result = scan(text=text, engine="regex", locales=self.locales)
187-
result = {label: [] for label in RegexAnnotator.LABELS}
187+
# Only pre-populate keys for labels active under the configured
188+
# locales so the default output shape matches v4.4.0 (no DE_* keys
189+
# unless German locale support is enabled).
190+
result = {label: [] for label in RegexAnnotator.active_labels_for(self.locales)}
188191
legacy_map = {"DATE": "DOB", "ZIP_CODE": "ZIP"}
189192
for entity in scan_result.entities:
190193
label = legacy_map.get(entity.type, entity.type)

datafog/processing/text_processing/regex_annotator/regex_annotator.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def __init__(
5353
enabled_labels: Iterable[str] | None = None,
5454
):
5555
self.locales = self._normalize_locales(locales)
56-
self.active_labels = self._resolve_active_labels(enabled_labels)
56+
self.active_labels = self.active_labels_for(self.locales, enabled_labels)
5757

5858
# Compile all patterns once at initialization
5959
all_patterns: Dict[str, Pattern] = {
@@ -100,20 +100,19 @@ def __init__(
100100
),
101101
# SSN pattern - U.S. Social Security Number
102102
# Supports dashed and no-dash formats.
103+
# Note: overlaps with locale-gated labels (e.g. the nine-digit run
104+
# inside a DE_VAT_ID) are resolved by the engine's span-overlap
105+
# suppression, not here, so default (EN) detection keeps v4.4.0
106+
# behavior even when German labels are active.
103107
"SSN": re.compile(
104108
r"""
109+
(?<!\d)
105110
(?:
106-
(?<!\d)
107111
(?!000|666)\d{3}-(?!00)\d{2}-(?!0000)\d{4}
108-
(?!\d)
109112
|
110-
(?<![A-Za-z0-9])
111-
(?<!DE)
112-
(?<!DE\s)
113-
(?<!DE-)
114113
(?!000|666)\d{3}(?!00)\d{2}(?!0000)\d{4}
115-
(?![A-Za-z0-9])
116114
)
115+
(?!\d)
117116
""",
118117
re.IGNORECASE | re.MULTILINE | re.VERBOSE,
119118
),
@@ -347,13 +346,19 @@ def _normalize_locales(cls, locales: str | Iterable[str] | None) -> tuple[str, .
347346
normalized.append(value)
348347
return tuple(dict.fromkeys(normalized))
349348

350-
def _resolve_active_labels(self, enabled_labels: Iterable[str] | None) -> list[str]:
351-
active = set(self.DEFAULT_LABELS)
352-
for locale in self.locales:
353-
active.update(self.LOCALE_LABELS[locale])
349+
@classmethod
350+
def active_labels_for(
351+
cls,
352+
locales: str | Iterable[str] | None = None,
353+
enabled_labels: Iterable[str] | None = None,
354+
) -> list[str]:
355+
"""Resolve the labels active for the given locales and explicit labels."""
356+
active = set(cls.DEFAULT_LABELS)
357+
for locale in cls._normalize_locales(locales):
358+
active.update(cls.LOCALE_LABELS[locale])
354359
if enabled_labels is not None:
355360
active.update(label.strip().upper() for label in enabled_labels)
356-
return [label for label in self.LABELS if label in active]
361+
return [label for label in cls.LABELS if label in active]
357362

358363
@staticmethod
359364
def _match_text(match: Match[str]) -> str:

docs/v45-release-readiness.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,13 @@ Use this framing for the GitHub release notes and package announcement:
4747

4848
Call out these user-facing points:
4949

50-
* German VAT IDs and German IBANs are detected by default in the regex engine.
51-
* Broader German identifiers such as tax IDs, postal codes, passport numbers,
52-
residence permit numbers, and pension insurance numbers require
53-
``locales=["de"]`` or explicit entity selection.
50+
* German structured identifiers — VAT IDs, IBANs, tax IDs, postal codes,
51+
passport numbers, residence permit numbers, and pension insurance numbers —
52+
are locale-gated and require ``locales=["de"]`` or explicit entity
53+
selection. Default (no-locale) detection behavior is unchanged from 4.4.0.
54+
* Guardrail helpers (``sanitize``, ``scan_prompt``, ``filter_output``,
55+
``create_guardrail``) now default to the regex engine; pass
56+
``engine="smart"`` to restore 4.4.0's NER-backed helper behavior.
5457
* OCR and Spark remain supported optional surfaces. They are not deprecated,
5558
but their broader overhaul is deferred beyond 4.5.
5659
* Telemetry remains disabled unless ``DATAFOG_TELEMETRY=1`` is set.

tests/test_agent_api.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22

33
from __future__ import annotations
44

5+
import inspect
6+
57
import pytest
68

79
import datafog
8-
from datafog.agent import GuardrailBlockedError
10+
from datafog.agent import Guardrail, GuardrailBlockedError
11+
12+
13+
def test_agent_helpers_default_to_regex_engine() -> None:
14+
"""4.5 lean-core contract: guardrail helpers stay on the regex path
15+
unless an engine is explicitly requested (changed from "smart" in 4.5.0;
16+
see CHANGELOG behavior changes)."""
17+
assert Guardrail().engine == "regex"
18+
assert datafog.create_guardrail().engine == "regex"
19+
for helper in (datafog.sanitize, datafog.scan_prompt, datafog.filter_output):
20+
assert inspect.signature(helper).parameters["engine"].default == "regex"
921

1022

1123
def test_sanitize_redacts_structured_pii() -> None:

tests/test_de_pii_regex.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,22 @@ def test_redaction_and_service_locale_support() -> None:
134134
def test_german_vat_redaction_suppresses_inner_generic_ssn_match(
135135
text: str, vat_text: str
136136
) -> None:
137+
# Default (no locale): v4.4.0 parity — the bare nine-digit run still
138+
# matches the generic SSN pattern even when prefixed by a country code.
137139
scan_result = scan(text, engine="regex")
138-
assert scan_result.entities == []
140+
assert [(entity.type, entity.text) for entity in scan_result.entities] == [
141+
("SSN", "123456789")
142+
]
139143

144+
# German locale: the longer DE_VAT_ID span wins via the engine's
145+
# span-overlap suppression, so the inner SSN match is dropped.
140146
locale_scan_result = scan(text, engine="regex", locales=["de"])
141147
assert [(entity.type, entity.text) for entity in locale_scan_result.entities] == [
142148
("DE_VAT_ID", vat_text)
143149
]
144150

145151
default_redaction = scan_and_redact(text, engine="regex")
146-
assert default_redaction.redacted_text == text
152+
assert default_redaction.redacted_text == text.replace("123456789", "[SSN_1]")
147153

148154
redaction = scan_and_redact(text, engine="regex", locales=["de"])
149155
assert redaction.redacted_text == text.replace(vat_text, "[DE_VAT_ID_1]")

tests/test_regex_annotator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,18 @@ def test_annotation_result_format():
371371

372372
assert len(ssn_spans) >= 1
373373
assert ssn_spans[0].text == "123-45-6789"
374+
375+
376+
def test_ssn_detection_keeps_v44_behavior_for_country_prefixed_digits():
377+
"""Regression guard: bare nine-digit runs after a country prefix must
378+
still match SSN when no locale is configured (v4.4.0 parity). The
379+
DE_VAT_ID overlap is resolved by engine-level span suppression only
380+
when German locale support is active, never by weakening the base
381+
SSN pattern."""
382+
annotator = RegexAnnotator()
383+
for text in (
384+
"Reference DE 123456789 was issued.",
385+
"Reference DE-123456789 was issued.",
386+
"Reference DE123456789 was issued.",
387+
):
388+
assert annotator.annotate(text)["SSN"] == ["123456789"], text

0 commit comments

Comments
 (0)