feat(pii): add v3 replace_pii config and tabular replacer - #672
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR replaces the legacy NER-based PII workflow with heuristic tabular discovery, declarative replacement plans, persona generation, scoped mappings, free-text propagation, and new preflight validation. It also removes obsolete NER and transformation modules. ChangesTabular PII replacement
Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
cf11160 to
4eb987c
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
d4d85dd to
5b7b975
Compare
7a35de1 to
76b1a7d
Compare
Greptile SummaryThe PR replaces the legacy NER/Jinja PII stack with heuristic, plan-driven tabular discovery and deterministic persona-aware replacement.
Confidence Score: 5/5The PR appears safe to merge with respect to the previously reported sampling issue. No blocking failure remains; discovery and validation now use the same deterministic evidence slice for identifier, phone, card, API-key, and date patterns, and focused regression coverage exercises the original late-format scenario. Important Files Changed
|
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (15)
src/nemo_safe_synthesizer/config/pii_replacement.py (1)
91-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd descriptions to all user-facing Pydantic fields.
The new plan and configuration fields omit required
Field(description=...)metadata. This removes field documentation from generated configuration help and API references.
src/nemo_safe_synthesizer/config/pii_replacement.py#L91-L92: Add descriptions for persona matching fields.src/nemo_safe_synthesizer/config/pii_replacement.py#L103-L111: Add descriptions for column-plan fields.src/nemo_safe_synthesizer/config/pii_replacement.py#L132-L147: Add descriptions for persona-set and plan fields.src/nemo_safe_synthesizer/config/pii_replacement.py#L153-L161: Add descriptions for LLM and replacement-setting fields.src/nemo_safe_synthesizer/config/pii_replacement.py#L216-L226: Add descriptions for top-level replacement configuration fields.As per coding guidelines, “Pydantic model fields must include
Field(description=...).”Source: Coding guidelines
src/nemo_safe_synthesizer/pii_replacer/core.py (1)
2205-2213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep one Luhn implementation.
_luhn_validduplicates_luhn_ok(lines 284-295). Both are used in this module:_luhn_okclassifies card values during detection, and_luhn_validverifies generated card values. Two checksum implementations can diverge, which would let detection and generation disagree about the same number.Delete one and call the survivor from both call sites.
src/nemo_safe_synthesizer/pii_replacer/discovery.py (1)
240-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a docstring to
discover_plan.
discover_planis the entry point thatplan.pycalls to build a plan. It has no docstring, so the generated API reference documents nothing about the arguments, the returned scope, or the effect ofconfig.llm_enhancement.Add a Google-style docstring that states what
group_keyselects, how the scope is derived, and that the returned plan is not yet validated.As per coding guidelines: "Use Google-style docstrings for public, nontrivial, or non-obvious functions and classes".
Source: Coding guidelines
src/nemo_safe_synthesizer/pii_replacer/persona.py (1)
122-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument that
assignmutates the instances in place.
assignis public and writessynthetic_personandsynthetic_person_sourceinto every dict of the caller's list. It returnsNone. Add a Google-style docstring that names the keys it sets and states the in-place side effect, because the API reference is generated from these docstrings.As per coding guidelines: "document arguments, returns, raises, side effects, thread safety, and idempotency as applicable".
Source: Coding guidelines
src/nemo_safe_synthesizer/pii_replacer/replacement.py (3)
102-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBind loop variables in
_append_instanceso ruff B023 does not failmise run check.Ruff flags
field_cols,col_set,match_persona_by, andpatterns_by_labelas unbound loop variables. The closure is only called inside the same iteration, so the behavior is correct today. The lint failure is still real, and the pattern breaks if the call ever moves out of the loop. Bind the values as default arguments, or move the helper out of the loop and pass the per-persona state explicitly.♻️ Proposed binding
def _append_instance( match: tuple, row: pd.Series, originals: dict, row_indices: list, + *, + col_set: PersonaColumnSet = col_set, + field_cols: dict[str, str] = field_cols, + match_persona_by: list[dict[str, str]] = match_persona_by, + patterns_by_label: dict[str, list[str]] = patterns_by_label, ) -> None:Source: Linters/SAST tools
476-481: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove
import reto module scope.
_presentimportsreon every call, and it runs once per row, per free-text column, per pair. The import is cheap after the first call, but the local import contradicts the repository import convention (standard library first, at module level). Addimport reat the top of the module and remove the local import.The ast-grep ReDoS hint on this line is a false positive:
re.escape(needle)removes all pattern metacharacters, and the lookarounds are literals.Source: Linters/SAST tools
346-356: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing Google-style docstrings on the new public replacement APIs. These three functions form the public replacement path, and none documents its contract. The shared root cause is that the new entry points were added without the docstrings the repository requires for public and nontrivial APIs; the API reference is generated from them.
src/nemo_safe_synthesizer/pii_replacer/replacement.py#L346-L356: document the keys of the returneddict[str, Any](replaced_df,structured_cols,free_text_applied,standalone_cols,changed_summary,free_text_entities) and thegroup_keyrequirement for group scope.src/nemo_safe_synthesizer/pii_replacer/replacement.py#L542-L549: document the returned tuple, the extra keys it adds (instances,standalone_maps,persona_backend_effective), and thepersona_engineoverride.src/nemo_safe_synthesizer/pii_replacer/replacer.py#L70-L77: document the argument, the mutated attributes (result,resolved_plan,elapsed_time), the plan-file side effect, and the raisedParameterError.As per path instructions: "Public APIs and nontrivial functions need Google-style docstrings."
Source: Path instructions
src/nemo_safe_synthesizer/pii_replacer/replacer.py (1)
66-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnotate the public attributes.
result,elapsed_time, andresolved_planform the public surface ofTabularPiiReplacer.library_builder.process_datareadsreplacer.resultandreplacer.elapsed_time, and it needsassert replacer.result is not Noneto narrow the type. Explicit annotations let the type checker see the optional types instead of inferringNone.♻️ Proposed annotations
- self.result = None - self.elapsed_time = 0.0 - self.resolved_plan = None + self.result: TransformResult | None = None + self.elapsed_time: float = 0.0 + self.resolved_plan: PiiReplacementPlan | None = Nonesrc/nemo_safe_synthesizer/preflight/checks/environment.py (1)
490-494: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the class docstring to match the new gate.
The gate now reads
replace_pii.llm_enhancement. The docstring above still says "When classification is enabled" and refers to "PII column classification", which no longer exists in the replacement path. Reword the docstring to referencereplace_pii.llm_enhancementand LLM-assisted replacement.Note the resulting interaction:
PiiReplacementConfigCheckreportspii_llm_not_implementedas an error wheneverllm_enhancement=True, so every finding from this check now accompanies a failing preflight run. That is acceptable for this release, but the docstring should say so.docs/product-overview/pii_replacement.md (1)
75-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winApply the Markdown emphasis and code-span conventions.
The changed text uses decorative bold for
notandentity type. Line 205 uses double backticks for examples. Use plain text or single-backtick code spans.As per coding guidelines, Markdown body text must not use decorative bold, and inline code uses single backticks.
Also applies to: 139-139, 205-205
Source: Coding guidelines
docs/user-guide/running.md (1)
655-657: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFix markdownlint MD046 violations for nested code blocks.
Static analysis flags fenced code blocks at Line 655-657, Line 667-673, and Line 682-692 as violations of MD046 (
code-block-style). Each fenced block sits inside a numbered list item (1. Obtain an NGC API key...,2. Download locales...,3. Install parquet files...). The linter expects an indented code block in this context. Convert each fenced block to an indented block, or confirm the project's markdownlint config permits fenced blocks inside list items before merging.Also applies to: 667-673, 682-692
Source: Linters/SAST tools
tests/pii_replacer/test_tabular_pii.py (2)
103-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winIsolate
PERSON_RANDOM_SEEDfor the whole module.
config_from_replace_piifalls back to thePERSON_RANDOM_SEEDenvironment variable whenreplacement.seedisNone(src/nemo_safe_synthesizer/pii_replacer/core.py:138-152). Most tests in this file buildReplacePiiConfig()without a seed, so their engine seed comes from the ambient environment. Onlytest_config_from_replace_pii_maps_user_fieldsclears that variable, and this test sets it to99for the process until monkeypatch teardown. If a developer shell or CI job exportsPERSON_RANDOM_SEED, statistical assertions such aschanged.mean() > 0.9at Line 1854 run against a different draw.Add an autouse fixture that removes the variable, and keep the explicit
setenvinside the one test that verifies the fallback.♻️ Proposed module-level isolation fixture
+@pytest.fixture(autouse=True) +def _isolate_person_random_seed(monkeypatch): + """Keep the engine seed independent of the ambient environment.""" + monkeypatch.delenv("PERSON_RANDOM_SEED", raising=False) + + `@pytest.fixture` def patient_df() -> pd.DataFrame:As per path instructions: "Check marker usage, fixture naming, tmp_path usage, determinism, and GPU/vLLM process-isolation requirements."
Source: Path instructions
54-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the
fixture_prefix for the dataset fixtures.
tests/TESTING.mdstates that dataset fixtures use thefixture_prefix. The dataset fixtures in this file (patient_df,dob_df,phone_df,contact_df,middle_name_df,numbered_email_df) do not follow it. Rename them so fixture discovery stays consistent with the rest of the suite.As per coding guidelines: "Use the
fixture_prefix for dataset and tokenizer fixtures".Also applies to: 808-809, 1145-1146, 1539-1540, 1671-1672, 1819-1820
Source: Coding guidelines
tests/sdk/test_builder.py (1)
72-89: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert that the inline
replacement_plansurvives builder resolution.The test now passes an inline
replacement_planthroughwith_replace_pii, but the only assertion isreplacement.locale == "en_US". The inline-plan branch ofReplacePiiConfig._resolve_replacement_planandfrom_config_sourceis executed and then discarded. If plan resolution dropped or flattenedpersona_backed_columns, this test would still pass. Add an assertion on the resolved plan.♻️ Proposed assertion on the resolved plan
assert builder._nss_config is not None assert builder._nss_config.replace_pii is not None assert builder._nss_config.replace_pii.replacement.locale == "en_US" + plan = builder._nss_config.replace_pii.inline_plan + assert plan is not None + assert [spec.column_name for spec in plan.persona_backed_columns[0].columns_to_replace] == ["name"]tests/smoke/test_pii_replacement_cpu.py (1)
42-55: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winSensitive Data Exposure (CWE-359)
Reachability: Internal
Assert the free-text propagation that the plan declares.
The plan marks
notesasfree_text, but the test does not verify that each note contains its row’s synthetic first name and no longer contains"Alice".Proposed assertion on the propagated note text
assert replacer.result is not None out = replacer.result.transformed_df assert "Alice" not in out["first_name"].tolist() assert out.loc[out["patient_id"] == "p1", "first_name"].nunique() == 1 + for first_name, note in zip(out["first_name"], out["notes"], strict=True): + assert first_name in note, (first_name, note) + assert "Alice" not in noteSource: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: e558206c-30fa-498d-8067-eedbc79097d0
📒 Files selected for processing (136)
.github/workflows/config/.secrets.baselineAGENTS.mddesign.mddocs/dev-notes/posts/introducing-nemo-safe-synthesizer.mddocs/developer-guide/architecture.mddocs/developer-guide/configuration_management.mddocs/product-overview/pii_replacement.mddocs/product-overview/pipeline.mddocs/tutorials/time-series-financial-transactions.ipynbdocs/user-guide/configuration.mddocs/user-guide/docker.mddocs/user-guide/environment.mddocs/user-guide/evaluating-data.mddocs/user-guide/getting-started.mddocs/user-guide/running.mddocs/user-guide/troubleshooting.mdsrc/nemo_safe_synthesizer/artifacts/base/fields.pysrc/nemo_safe_synthesizer/cli/run.pysrc/nemo_safe_synthesizer/cli/settings.pysrc/nemo_safe_synthesizer/cli/utils.pysrc/nemo_safe_synthesizer/config/__init__.pysrc/nemo_safe_synthesizer/config/parameters.pysrc/nemo_safe_synthesizer/config/patch.pysrc/nemo_safe_synthesizer/config/pii_replacement.pysrc/nemo_safe_synthesizer/config/replace_pii.pysrc/nemo_safe_synthesizer/config/unknown_fields.pysrc/nemo_safe_synthesizer/configurator/pydantic_click_options.pysrc/nemo_safe_synthesizer/data_processing/actions/data_actions.pysrc/nemo_safe_synthesizer/data_processing/actions/utils.pysrc/nemo_safe_synthesizer/data_processing/records/fragment.pysrc/nemo_safe_synthesizer/defaults.pysrc/nemo_safe_synthesizer/evaluation/assets/jinja/components/training_columns.j2src/nemo_safe_synthesizer/evaluation/assets/text/multi_modal_tooltips.pysrc/nemo_safe_synthesizer/pii_replacer/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/core.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/detect.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/edit.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/environment.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/filters.pysrc/nemo_safe_synthesizer/pii_replacer/data_editor/transform_test_utils.pysrc/nemo_safe_synthesizer/pii_replacer/discovery.pysrc/nemo_safe_synthesizer/pii_replacer/nemo_pii.pysrc/nemo_safe_synthesizer/pii_replacer/ner/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/ner/const.pysrc/nemo_safe_synthesizer/pii_replacer/ner/custom.pysrc/nemo_safe_synthesizer/pii_replacer/ner/datetime.pysrc/nemo_safe_synthesizer/pii_replacer/ner/entity.pysrc/nemo_safe_synthesizer/pii_replacer/ner/factory.pysrc/nemo_safe_synthesizer/pii_replacer/ner/fasttext.pysrc/nemo_safe_synthesizer/pii_replacer/ner/helpers.pysrc/nemo_safe_synthesizer/pii_replacer/ner/labels.pysrc/nemo_safe_synthesizer/pii_replacer/ner/metadata.pysrc/nemo_safe_synthesizer/pii_replacer/ner/model.pysrc/nemo_safe_synthesizer/pii_replacer/ner/models.pysrc/nemo_safe_synthesizer/pii_replacer/ner/ner.pysrc/nemo_safe_synthesizer/pii_replacer/ner/ner_mp.pysrc/nemo_safe_synthesizer/pii_replacer/ner/nlp.pysrc/nemo_safe_synthesizer/pii_replacer/ner/person_name.pysrc/nemo_safe_synthesizer/pii_replacer/ner/pipeline.pysrc/nemo_safe_synthesizer/pii_replacer/ner/predictor.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regex.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/aba_routing_number.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/age.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/credit_card.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/domain_name.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/email.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/facebook.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/generic_key.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/github.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/google.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/google_olc.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/iban.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/imei.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/ip_address.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/jwt.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/lat_lon.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/md5.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/race_ethnicity.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/sendgrid.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/sex_gender.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/sha256.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/sha512.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/slack.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/square.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/stripe.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/swift.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/twilio.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/url.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_phone.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_ssn.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_zipcode.pysrc/nemo_safe_synthesizer/pii_replacer/ner/regexes/uuid.pysrc/nemo_safe_synthesizer/pii_replacer/ner/report/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/ner/report/base.pysrc/nemo_safe_synthesizer/pii_replacer/ner/report/metadata.pysrc/nemo_safe_synthesizer/pii_replacer/ner/report/results.pysrc/nemo_safe_synthesizer/pii_replacer/ner/utils.pysrc/nemo_safe_synthesizer/pii_replacer/persona.pysrc/nemo_safe_synthesizer/pii_replacer/plan.pysrc/nemo_safe_synthesizer/pii_replacer/replacement.pysrc/nemo_safe_synthesizer/pii_replacer/replacer.pysrc/nemo_safe_synthesizer/pii_replacer/transform_result.pysrc/nemo_safe_synthesizer/preflight/checks/__init__.pysrc/nemo_safe_synthesizer/preflight/checks/environment.pysrc/nemo_safe_synthesizer/preflight/checks/pii.pysrc/nemo_safe_synthesizer/sdk/config_builder.pysrc/nemo_safe_synthesizer/sdk/library_builder.pysrc/nemo_safe_synthesizer/utils.pytests/TESTING.mdtests/cli/test_run.pytests/cli/test_settings.pytests/cli/test_utils.pytests/config/test_nss_config.pytests/config/test_parameters.pytests/config/test_patch.pytests/configurator/test_pydantic_click_options.pytests/conftest.pytests/evaluation/components/test_pii_replay.pytests/evaluation/conftest.pytests/evaluation/test_render.pytests/evaluation/test_render_assets.pytests/nss_pii_replacer_test.pytests/pii_replacer/test_detect.pytests/pii_replacer/test_edit.pytests/pii_replacer/test_filters.pytests/pii_replacer/test_nemo_pii.pytests/pii_replacer/test_tabular_pii.pytests/preflight/conftest.pytests/preflight/test_preflight.pytests/sdk/test_builder.pytests/sdk/test_config_builder.pytests/sdk/test_process_data.pytests/sdk/test_process_data_pii_regression.pytests/smoke/test_pii_replacement_cpu.py
💤 Files with no reviewable changes (72)
- src/nemo_safe_synthesizer/pii_replacer/data_editor/init.py
- src/nemo_safe_synthesizer/artifacts/base/fields.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/google.py
- src/nemo_safe_synthesizer/pii_replacer/ner/helpers.py
- src/nemo_safe_synthesizer/config/replace_pii.py
- src/nemo_safe_synthesizer/pii_replacer/ner/const.py
- src/nemo_safe_synthesizer/pii_replacer/ner/ner_mp.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/credit_card.py
- src/nemo_safe_synthesizer/pii_replacer/ner/labels.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/aba_routing_number.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/init.py
- src/nemo_safe_synthesizer/pii_replacer/data_editor/filters.py
- src/nemo_safe_synthesizer/pii_replacer/ner/report/base.py
- src/nemo_safe_synthesizer/pii_replacer/data_editor/detect.py
- src/nemo_safe_synthesizer/pii_replacer/ner/nlp.py
- src/nemo_safe_synthesizer/data_processing/records/fragment.py
- src/nemo_safe_synthesizer/pii_replacer/data_editor/edit.py
- src/nemo_safe_synthesizer/pii_replacer/nemo_pii.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_zipcode.py
- src/nemo_safe_synthesizer/pii_replacer/ner/custom.py
- src/nemo_safe_synthesizer/pii_replacer/ner/metadata.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/md5.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/sha512.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regex.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/lat_lon.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/twilio.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/slack.py
- src/nemo_safe_synthesizer/data_processing/actions/data_actions.py
- src/nemo_safe_synthesizer/pii_replacer/ner/factory.py
- src/nemo_safe_synthesizer/pii_replacer/ner/datetime.py
- tests/pii_replacer/test_edit.py
- src/nemo_safe_synthesizer/pii_replacer/ner/person_name.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/jwt.py
- src/nemo_safe_synthesizer/pii_replacer/ner/ner.py
- src/nemo_safe_synthesizer/pii_replacer/ner/report/metadata.py
- tests/nss_pii_replacer_test.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/email.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/age.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/generic_key.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/race_ethnicity.py
- src/nemo_safe_synthesizer/pii_replacer/data_editor/transform_test_utils.py
- src/nemo_safe_synthesizer/pii_replacer/ner/entity.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/github.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/square.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/sha256.py
- tests/pii_replacer/test_filters.py
- src/nemo_safe_synthesizer/pii_replacer/ner/report/init.py
- src/nemo_safe_synthesizer/pii_replacer/ner/init.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/sendgrid.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_ssn.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/sex_gender.py
- src/nemo_safe_synthesizer/pii_replacer/ner/utils.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/uuid.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/imei.py
- src/nemo_safe_synthesizer/pii_replacer/ner/report/results.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/ip_address.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/google_olc.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/domain_name.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/iban.py
- tests/pii_replacer/test_detect.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/stripe.py
- src/nemo_safe_synthesizer/pii_replacer/ner/predictor.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/url.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/us_phone.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/facebook.py
- src/nemo_safe_synthesizer/pii_replacer/ner/regexes/swift.py
- tests/pii_replacer/test_nemo_pii.py
- src/nemo_safe_synthesizer/pii_replacer/ner/pipeline.py
- src/nemo_safe_synthesizer/pii_replacer/ner/models.py
- src/nemo_safe_synthesizer/pii_replacer/ner/model.py
- src/nemo_safe_synthesizer/pii_replacer/data_editor/environment.py
- src/nemo_safe_synthesizer/pii_replacer/ner/fasttext.py
|
thanks @nina-xu - as a note for the rest of the team, i'd suggest we continue to let this PR bake for a while so we can properly dogfood and review it (+/- 10K loc and a whole new design), so no one needs to "approve" it right now. we can group reviews comments into a few buckets as well - pii config design/logic/semantics, structural implementation/code quality/maintainability, usability, and testing? |
|
@binaryaaron @zywind @nina-xu If we want this to be a longer-lived branch, then we should merge this into |
Replace the v2-oriented PII replacement path with a typed plan (persona-backed vs standalone columns, scope, patterns), auto-discovery with preflight validation, and a modular pii_replacer package (detection, patterns, planning, replacement, llm seams). Reject unsupported llm_enhancement=True at config validation. Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
2c092cf to
591c349
Compare
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
b81447d to
79f0a8b
Compare
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
|
Too many files changed for review (189 files, 100 file limit). Bypass the limit by tagging |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (17)
src/nemo_safe_synthesizer/pii_replacer/replacement/free_text.py (1)
228-228: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAnnotate the returned substituter.
build_text_substituterhas no return annotation, and the docstring states it returns a callable orNone. Add-> Callable[[object], object] | Noneso callers get checked types.As per path instructions: "use typed contracts and Protocols, native Python 3.11-compatible annotations" (STYLE_GUIDE.md).
Source: Path instructions
src/nemo_safe_synthesizer/pii_replacer/replacement/instances.py (3)
154-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBind the loop variables in the closure. Ruff reports B023 for
field_cols,col_set,match_persona_by, andpatterns_by_labelat lines 160-170. The current calls happen inside the same iteration, so behavior is correct today, but the closure is one refactor away from capturing the wrongcol_set. Pass the values as default arguments, or move_append_instanceto a module-level helper that takes them explicitly.Source: Linters/SAST tools
196-214: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeduplicate the record and default scope branches. The
case "record"andcase _arms differ only in whether repeated signatures are collapsed. Thecase _body also re-annotatessig_rowsandsig_first, which thecase "group"arm already annotated in the same function scope. Extract one helper that takes a "collapse duplicates" flag, or drop the duplicate annotations.
26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUntyped parameters in new library helpers. Several new helpers omit annotations on parameters and return values, so
tycannot check the call sites and callers cannot see the expected interface. The docstrings already name the concrete shapes, so the annotations are mechanical.
src/nemo_safe_synthesizer/pii_replacer/replacement/instances.py#L26-L34: annotate_instance_is_persondictionaries and every_make_instanceparameter (tuple,pd.Series,entities.Config,dict[str, list[str]] | None).src/nemo_safe_synthesizer/pii_replacer/patterns/value_templates.py#L55-L55: annotateshape_fnasCallable[[str], str], and giverngandvaluestypes inpattern_preserving_token,infer_value_pattern,generate_from_pattern, andconform_to_template.src/nemo_safe_synthesizer/pii_replacer/replacement/free_text.py#L228-L228: add theCallable[[object], object] | Nonereturn annotation tobuild_text_substituter.As per path instructions: "use typed contracts and Protocols, native Python 3.11-compatible annotations" (STYLE_GUIDE.md).
Source: Path instructions
tests/pii_replacer/replacement/test_personas.py (1)
199-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSet an explicit replacement seed for this statistical assertion. The test omits
replacement.seed; the resolver usesPERSON_RANDOM_SEEDor42. This leaves the assertion dependent on ambient CI environment state.tests/pii_replacer/detection/test_column_names.py (2)
28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated discovery setup into a fixture.
Ten call sites repeat
discover_plan(df, None, config_from_replace_pii(PiiReplacerConfig()), PiiReplacerConfig()). A small module fixture or helper (for examplefixture_discoverreturning a callable that takesdfand an optionalgroup_key) keeps the behavior under test visible and removes the duplicated wiring.tests/TESTING.mdasks for focusedfixture_-prefixed fixtures for shared setup.Also applies to: 47-47, 59-59, 80-80, 96-96, 123-123, 164-164
Source: Path instructions
212-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog assertions treat message prose as a contract. Three new assertions require full warning or info phrasing, so a harmless rewording of a log message fails the suite without any behavior regression. Assert only the contract-relevant tokens: column name, candidate labels, and chosen label.
tests/pii_replacer/detection/test_column_names.py#L212-L221: drop the"Review the replacement plan"and"bug report"conditions; keep the column name and the label names.tests/pii_replacer/detection/test_column_names.py#L239-L242: replace the"chose 'sex'"condition with an assertion on the returneddemo_labelplus the candidate label names in the message.tests/pii_replacer/detection/test_value_recognizers.py#L149-L149: assert"misc_col"and a short stable token instead of the full"Identified temporal column 'misc_col'"phrase.Source: Path instructions
tests/pii_replacer/detection/test_value_recognizers.py (1)
115-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the unused
caplogfixture or assert on the captured records.Both tests request
caplogand callcaplog.set_level(logging.WARNING), but neither asserts anything about the records. The scaffolding suggests an intended warning assertion that is missing. Either assert the expected warning (which would add real coverage for the skip decision) or drop the fixture and theloggingimport.Also applies to: 152-163
src/nemo_safe_synthesizer/pii_replacer/detection/column_names.py (1)
197-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove or drop the stale keyword comment.
This comment describes the curated
FUZZY_KEYWORDSspellings, but those now live inentities.pyasEntitySpec.fuzzy_keywords. In this file it reads as documentation forfuzzy_match_label, which does something else.♻️ Proposed cleanup
-# Curated, specific keyword spellings per label for the fuzzy backstop. These are -# deliberately NOT generic single words (no bare "name"/"date"/"id"), so a typo'd -# variant matches but unrelated columns (e.g. "event_name", "event_date") do not. def fuzzy_match_label(col: str, patterns: Mapping[str, list[str]], threshold: float) -> str | None:src/nemo_safe_synthesizer/pii_replacer/detection/free_text.py (1)
69-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLog the skip reason that
_free_text_eligibilityalready returns.
_free_text_eligibilitycomputes a reason per column, but line 69 discards it. The summary log then lists skipped columns without saying whether the dtype or the field classification excluded them. Users who ask why a text column was not scanned get no answer from the logs.♻️ Proposed change to keep the reason
- eligible, _reason = _free_text_eligibility(col, df[col]) + eligible, reason = _free_text_eligibility(col, df[col]) if eligible: text_fields.append(col) else: - not_scanned.append(col) + not_scanned.append(f"{col} ({reason})")src/nemo_safe_synthesizer/pii_replacer/entity_handlers.py (1)
43-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport
IpAddressHandlerfrom__all__.
IpAddressHandleris a public handler with the same role asCreditCardHandlerandDateOfBirthHandler, and_HANDLERSregisters it foripv4andipv6. It is missing from__all__, so the public surface is inconsistent.♻️ Proposed fix
__all__ = [ "CreditCardHandler", "DateOfBirthHandler", "DefaultHandler", "EntityHandler", + "IpAddressHandler", "get_handler", ]As per path instructions for
src/**/*.py, which require__all__for public APIs.Source: Path instructions
src/nemo_safe_synthesizer/pii_replacer/patterns/persona_templates.py (1)
204-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the missing return annotation on
split_title.
split_titleis public and returnstuple[str | None, str]. The function has no return annotation, so callers such aspersona_writteninreplacement/personas.pylose type information. The repository requires type-correct code checked byty.♻️ Proposed annotation
-def split_title(value: str): +def split_title(value: str) -> tuple[str | None, str]:Source: Coding guidelines
src/nemo_safe_synthesizer/pii_replacer/replacement/apply.py (1)
267-273: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCompute
cells_changedonly for columns the plan touched.
_cells_changedruns an element-wise Pythonmapover the original and replaced series for every column in the frame, including columns no replacement can reach. On wide frames this dominates the replacement cost and produces no extra information, because onlystructured_cols,standalone_cols, andfree_text_appliedcan change.♻️ Proposed narrowing
- changed_summary = [{"column": c, "cells_changed": _cells_changed(c)} for c in column_order] + touched = structured_cols | set(standalone_cols) | set(free_text_applied) + changed_summary = [{"column": c, "cells_changed": _cells_changed(c)} for c in column_order if c in touched] changed_summary = [d for d in changed_summary if d["cells_changed"]]src/nemo_safe_synthesizer/pii_replacer/replacement/demographics.py (2)
59-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winWiden the missing-value guard to pandas NA scalars.
The guard only recognizes
NoneandfloatNaN.instances.pycallsnorm_sex(row[cond["sex"]])with a raw cell value, so a nullable-dtype column yieldspd.NAand a datetime column yieldspd.NaT. Neither is afloat, so_norm_catconverts them to"na"and"nat"and the fuzzy scan runs on a sentinel string.🛡️ Proposed guard
- if value is None or (isinstance(value, float) and pd.isna(value)): + if value is None or (not isinstance(value, str) and pd.isna(value) is True): return None
288-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport
persona_match_mapand give its parameter a real type.
replacement/instances.pyimportspersona_match_map, but__all__(lines 17-23) omits it, so the module's declared public surface does not match its actual use. The parameter is also annotated as a barelist, and the body indexes entries as untyped mappings. The style guide asks for typed contracts instead of nested dictionary exchanges.Add
"persona_match_map"to__all__and annotate the parameter with the plan'smatch_persona_byentry model.Source: Path instructions
src/nemo_safe_synthesizer/pii_replacer/replacement/personas.py (1)
80-102: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy liftDo not install a global
sudachipystub, and replace thesetattrcalls.Two problems in this block:
Lines 83-99 insert a fake
sudachipymodule intosys.modulesfor the whole process and never remove it. Any other code in the same process that importssudachipyafterwards silently receives a stub whoseDictionary.create()returnsNone.sys.path.insertat line 82 is also permanent. The PGM backend is internal-only, but the side effect is process-wide and outlives the call.Ruff flags lines 96-97 (
B010):setattrwith a constant attribute name. Use direct assignment.If the stub must stay, restore
sys.modulesandsys.pathin atry/finallyblock, which the coding guidelines require for resource cleanup.♻️ Minimal change for the `setattr` calls
- _dict_mod = _types.ModuleType("sudachipy.dictionary") - setattr(_dict_mod, "Dictionary", _StubDict) - setattr(_stub, "dictionary", _dict_mod) + _dict_mod = _types.ModuleType("sudachipy.dictionary") + _dict_mod.Dictionary = _StubDict + _stub.dictionary = _dict_modSources: Coding guidelines, Linters/SAST tools
tests/sdk/test_builder.py (1)
87-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBuilder tests assert existence instead of the configured replacement values. Both tests supply a specific
replace_piipayload and then check only thatreplace_piiis notNoneor that one field survived. A regression that dropped the plan, the seed, or thellm_enhancementflag during resolution would still pass.
tests/sdk/test_builder.py#L87-L89: assert the resolved persona-backed plan contains thenamecolumn with entityfirst_name, and assertreplacement.seed == 42.tests/sdk/test_builder.py#L255-L268: assertllm_enhancement is Falseand that the replacement plan resolved to auto discovery from the YAML string.Source: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: e6c7e3a4-e3f9-4c65-87e7-5b1f46db2eea
📒 Files selected for processing (67)
docs/developer-guide/architecture.mddocs/product-overview/pii_replacement.mddocs/user-guide/configuration.mddocs/user-guide/running.mddocs/user-guide/troubleshooting.mdsrc/nemo_safe_synthesizer/config/__init__.pysrc/nemo_safe_synthesizer/config/parameters.pysrc/nemo_safe_synthesizer/config/patch.pysrc/nemo_safe_synthesizer/config/replace_pii.pysrc/nemo_safe_synthesizer/pii_replacer/detection/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.pysrc/nemo_safe_synthesizer/pii_replacer/detection/free_text.pysrc/nemo_safe_synthesizer/pii_replacer/detection/persona_grouping.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/llm/noop.pysrc/nemo_safe_synthesizer/pii_replacer/llm/not_implemented.pysrc/nemo_safe_synthesizer/pii_replacer/llm/protocol.pysrc/nemo_safe_synthesizer/pii_replacer/models.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/__init__.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/evidence.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/persona_templates.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/temporal.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/value_templates.pysrc/nemo_safe_synthesizer/pii_replacer/planning/discovery.pysrc/nemo_safe_synthesizer/pii_replacer/planning/io.pysrc/nemo_safe_synthesizer/pii_replacer/planning/validation.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/apply.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/demographics.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/free_text.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/instances.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/personas.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/scope.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/standalone.pysrc/nemo_safe_synthesizer/pii_replacer/replacer.pysrc/nemo_safe_synthesizer/preflight/checks/pii.pysrc/nemo_safe_synthesizer/sdk/config_builder.pytests/config/test_nss_config.pytests/config/test_parameters.pytests/config/test_patch.pytests/conftest.pytests/pii_replacer/detection/test_column_names.pytests/pii_replacer/detection/test_free_text_detect.pytests/pii_replacer/detection/test_persona_grouping.pytests/pii_replacer/detection/test_value_recognizers.pytests/pii_replacer/golden/patient_events.plan.yamltests/pii_replacer/helpers.pytests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/patterns/test_value_templates.pytests/pii_replacer/planning/test_discovery.pytests/pii_replacer/planning/test_validation.pytests/pii_replacer/replacement/test_free_text.pytests/pii_replacer/replacement/test_personas.pytests/pii_replacer/replacement/test_phone.pytests/pii_replacer/replacement/test_scope.pytests/pii_replacer/replacement/test_scope_consistency.pytests/pii_replacer/replacement/test_standalone.pytests/pii_replacer/test_config.pytests/pii_replacer/test_discovery_golden.pytests/pii_replacer/test_preflight.pytests/preflight/conftest.pytests/sdk/test_builder.pytests/sdk/test_config_builder.pytests/sdk/test_process_data.pytests/sdk/test_process_data_pii_regression.pytests/smoke/test_pii_replacement_cpu.py
🚧 Files skipped from review as they are similar to previous changes (30)
- tests/smoke/test_pii_replacement_cpu.py
- src/nemo_safe_synthesizer/pii_replacer/detection/init.py
- src/nemo_safe_synthesizer/pii_replacer/llm/noop.py
- tests/pii_replacer/test_discovery_golden.py
- tests/pii_replacer/detection/test_free_text_detect.py
- docs/user-guide/configuration.md
- tests/conftest.py
- src/nemo_safe_synthesizer/pii_replacer/llm/protocol.py
- tests/preflight/conftest.py
- docs/user-guide/troubleshooting.md
- src/nemo_safe_synthesizer/config/patch.py
- tests/pii_replacer/helpers.py
- tests/pii_replacer/replacement/test_free_text.py
- tests/sdk/test_config_builder.py
- src/nemo_safe_synthesizer/preflight/checks/pii.py
- tests/config/test_patch.py
- tests/pii_replacer/detection/test_persona_grouping.py
- tests/pii_replacer/test_preflight.py
- src/nemo_safe_synthesizer/pii_replacer/llm/not_implemented.py
- tests/pii_replacer/planning/test_discovery.py
- src/nemo_safe_synthesizer/pii_replacer/models.py
- src/nemo_safe_synthesizer/pii_replacer/planning/io.py
- docs/product-overview/pii_replacement.md
- src/nemo_safe_synthesizer/pii_replacer/detection/persona_grouping.py
- src/nemo_safe_synthesizer/pii_replacer/replacement/standalone.py
- tests/pii_replacer/planning/test_validation.py
- src/nemo_safe_synthesizer/pii_replacer/replacer.py
- tests/pii_replacer/golden/patient_events.plan.yaml
- src/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.py
- tests/pii_replacer/test_config.py
| === "Config reference" | ||
|
|
||
| ```yaml | ||
| replace_pii: | ||
| globals: | ||
| classify: | ||
| enable_classify: true | ||
| entities: ["email", "phone_number", "ssn"] | ||
| steps: | ||
| - rows: | ||
| update: | ||
| - condition: column.entity == "email" and not (this | isna) | ||
| value: column.entity | fake | ||
| - condition: column.entity == "phone_number" and not (this | isna) | ||
| value: column.entity | fake | ||
| - condition: column.entity == "ssn" and not (this | isna) | ||
| value: column.entity | fake | ||
| # "auto_discovery" (default), a path to a plan file, or an inline plan | ||
| replacement_plan: pii_replacement_plan.yaml | ||
| replacement: | ||
| locale: en_US | ||
| seed: 42 | ||
| person: | ||
| # managed (default) or faker | ||
| backend: faker | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect markdownlint config and the flagged block to confirm the MD046 finding.
set -euo pipefail
fd -H '.markdownlint*' -x cat
sed -n '555,625p' docs/user-guide/running.md | cat -A | sed -n '1,80p' | head -80Repository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 2491
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- markdownlint references ---'
rg -n -i 'markdownlint|mise run .*lint|lint.*docs' . --glob '!uv.lock' --glob '!*.lock' | head -120
printf '%s\n' '--- available local tools ---'
command -v markdownlint || true
command -v markdownlint-cli2 || true
command -v mise || true
printf '%s\n' '--- exact block and file status ---'
sed -n '560,625p' docs/user-guide/running.md | cat -n
git status --short -- docs/user-guide/running.md .markdownlint.json .markdownlint.yaml .markdownlint.ymlRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 3568
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- markdownlint result ---'
markdownlint-cli2 docs/user-guide/running.md
status=$?
printf 'markdownlint-cli2 exit status: %s\n' "$status"
exit 0Repository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 13916
Handle MD046 for MkDocs tab blocks. This block already matches the surrounding tab syntax, but markdownlint reports MD046 for all similarly indented tab blocks. Update the markdownlint configuration to account for MkDocs tabs instead of changing this block alone.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 611-611: Code block style
Expected: fenced; Actual: indented
(MD046, code-block-style)
Source: Linters/SAST tools
| ```bash | ||
| # United States (matches the default replace_pii.replacement.locale) | ||
| ngc registry resource download-version "nvidia/nemotron-personas/nemotron-personas-dataset-en_us" | ||
|
|
||
| ### LLM Column Classification | ||
| # Optional additional locales | ||
| ngc registry resource download-version "nvidia/nemotron-personas/<NGC package>" | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
ngc registry resource download-version command syntax version suffix
💡 Result:
The command syntax for downloading an NGC resource using the NGC CLI is: ngc registry resource download-version /[/]: [options] [1][2][3] In this syntax, the version suffix is specified by appending a colon (:) followed by the specific version number to the resource name [1][2][3]. For example: ngc registry resource download-version my-org/my-team/my-resource:1.0 [2][4] Key details: - Organization (org) and team (team) are part of the target identifier, where the team is optional [1][3]. - The version is a mandatory suffix after the colon [1][2][3]. - You can specify a destination directory using the --dest option [5][2][6]. For further help or to see specific options available for your version of the NGC CLI, you can run the following command: ngc registry resource download-version -h [5][7]
Citations:
- 1: https://archive.docs.nvidia.com/ace/resource-downloader-container/1.2/ngc-resource-downloader.html
- 2: https://docs.nvidia.com/brev/guides/inference-deployment/ngc-developer-guide
- 3: https://docs.nvidia.com/dgx/ngc-registry-cli-user-guide/index-bak.html
- 4: https://github.com/NVIDIA/nv-cloud-function-helpers/blob/main/skills/nvcf-ngc-cli-skill/references/registry.md
- 5: https://docs.ngc.nvidia.com/cli/cmd_registry.html
- 6: https://docs.nvidia.com/dgx/pdf/ngc-registry-cli-user-guide.pdf
- 7: https://docs.ngc.nvidia.com/cli/cmd.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- documented commands ---'
rg -n -C 3 'download-version|nemotron-personas-dataset-en_us|NGC package' docs/user-guide/running.md
printf '%s\n' '--- repository examples ---'
rg -n 'ngc registry resource download-version' . --glob '*.md' --glob '*.yaml' --glob '*.yml' --glob '*.sh' || trueRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 1605
Add a version suffix to both NGC commands. download-version requires <org>/<team>/<resource>:<version>; the current commands can fail with a usage error.
| @field_validator("replacement_plan", mode="before") | ||
| @classmethod | ||
| def _resolve_replacement_plan(cls, value: object) -> object: | ||
| """Resolve the plan/string union here so errors describe the plan, not the union. | ||
|
|
||
| Left to the union, a malformed inline plan reports the plan's own errors | ||
| *and* "input should be a valid string", which reads as though a file path | ||
| was expected. Validating a mapping as a plan up front keeps the report to | ||
| the fields the user actually got wrong. | ||
| """ | ||
| if isinstance(value, Mapping): | ||
| try: | ||
| return PiiReplacementPlan.model_validate(value) | ||
| except ValidationError as exc: | ||
| details = "; ".join( | ||
| f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}" for error in exc.errors() | ||
| ) | ||
| raise ParameterError(f"invalid inline replacement plan ({details})") from exc | ||
| if isinstance(value, Path): | ||
| return str(value) | ||
| if isinstance(value, str | PiiReplacementPlan): | ||
| return value | ||
| raise ParameterError( | ||
| f"replacement_plan must be {AUTO_DISCOVERY!r}, a path to a plan file, or an inline plan; " | ||
| f"got {type(value).__name__}" | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject a blank replacement_plan string here.
An empty or whitespace-only string passes this validator. plan_path then returns a falsy value, so resolve_plan in src/nemo_safe_synthesizer/pii_replacer/planning/io.py (Lines 185-190) treats the config as having no plan source and raises the generic "replacement_plan must be auto_discovery, a path, or an inline plan" error. The user sees a message that does not name the real problem. Reject blank strings at config validation so the error points at the field.
🛠️ Proposed fix
if isinstance(value, Path):
return str(value)
- if isinstance(value, str | PiiReplacementPlan):
+ if isinstance(value, str):
+ if not value.strip():
+ raise ParameterError(
+ f"replacement_plan must be {AUTO_DISCOVERY!r}, a path to a plan file, or an inline plan; "
+ "got an empty string"
+ )
+ return value
+ if isinstance(value, PiiReplacementPlan):
return value📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @field_validator("replacement_plan", mode="before") | |
| @classmethod | |
| def _resolve_replacement_plan(cls, value: object) -> object: | |
| """Resolve the plan/string union here so errors describe the plan, not the union. | |
| Left to the union, a malformed inline plan reports the plan's own errors | |
| *and* "input should be a valid string", which reads as though a file path | |
| was expected. Validating a mapping as a plan up front keeps the report to | |
| the fields the user actually got wrong. | |
| """ | |
| if isinstance(value, Mapping): | |
| try: | |
| return PiiReplacementPlan.model_validate(value) | |
| except ValidationError as exc: | |
| details = "; ".join( | |
| f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}" for error in exc.errors() | |
| ) | |
| raise ParameterError(f"invalid inline replacement plan ({details})") from exc | |
| if isinstance(value, Path): | |
| return str(value) | |
| if isinstance(value, str | PiiReplacementPlan): | |
| return value | |
| raise ParameterError( | |
| f"replacement_plan must be {AUTO_DISCOVERY!r}, a path to a plan file, or an inline plan; " | |
| f"got {type(value).__name__}" | |
| ) | |
| @field_validator("replacement_plan", mode="before") | |
| @classmethod | |
| def _resolve_replacement_plan(cls, value: object) -> object: | |
| """Resolve the plan/string union here so errors describe the plan, not the union. | |
| Left to the union, a malformed inline plan reports the plan's own errors | |
| *and* "input should be a valid string", which reads as though a file path | |
| was expected. Validating a mapping as a plan up front keeps the report to | |
| the fields the user actually got wrong. | |
| """ | |
| if isinstance(value, Mapping): | |
| try: | |
| return PiiReplacementPlan.model_validate(value) | |
| except ValidationError as exc: | |
| details = "; ".join( | |
| f"{'.'.join(str(part) for part in error['loc'])}: {error['msg']}" for error in exc.errors() | |
| ) | |
| raise ParameterError(f"invalid inline replacement plan ({details})") from exc | |
| if isinstance(value, Path): | |
| return str(value) | |
| if isinstance(value, str): | |
| if not value.strip(): | |
| raise ParameterError( | |
| f"replacement_plan must be {AUTO_DISCOVERY!r}, a path to a plan file, or an inline plan; " | |
| "got an empty string" | |
| ) | |
| return value | |
| if isinstance(value, PiiReplacementPlan): | |
| return value | |
| raise ParameterError( | |
| f"replacement_plan must be {AUTO_DISCOVERY!r}, a path to a plan file, or an inline plan; " | |
| f"got {type(value).__name__}" | |
| ) |
| random_seed: int = field(default_factory=lambda: int(os.environ.get("PERSON_RANDOM_SEED", "42") or "42")) | ||
| """Seed for persona/ID/Faker generation. | ||
|
|
||
| Env-overridable via ``PERSON_RANDOM_SEED`` so batched runs can give each | ||
| row-batch a distinct seed; otherwise every batch regenerates the same | ||
| unique-identifier sequence and values collide across batches. | ||
| """ |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
A non-numeric PERSON_RANDOM_SEED raises a bare ValueError.
int(os.environ.get("PERSON_RANDOM_SEED", "42") or "42") handles an empty value but not a malformed one. With PERSON_RANDOM_SEED=auto, the run fails with an unhandled ValueError instead of a project error that names the variable. The same expression is repeated at lines 114-116, so both call sites share the defect.
Extract one helper that parses the variable and raises ParameterError with the offending value, then call it from both sites.
| case "record": | ||
| data = { | ||
| cast(Hashable, idx): _unit_map(original_df.loc[[idx], col], cast(Hashable, idx)) | ||
| for idx in original_df.index | ||
| } | ||
| return ScopedValueMap("record", cast(dict[Hashable, object], data)) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether the replacement entry point normalizes the dataframe index.
set -euo pipefail
rg -n -C4 'reset_index|index\.is_unique|def run_replacement' --glob 'src/nemo_safe_synthesizer/pii_replacer/**/*.py'Repository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 166
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- candidate files ---'
git ls-files 'src/nemo_safe_synthesizer/pii_replacer/**'
printf '%s\n' '--- scope module outline ---'
ast-grep outline src/nemo_safe_synthesizer/pii_replacer/replacement/scope.py
printf '%s\n' '--- replacement call sites and index handling ---'
rg -n -C5 'ScopedValueMap|scope|original_df|reset_index|index' src/nemo_safe_synthesizer/pii_replacerRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 50385
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- replacement entry point ---'
sed -n '60,155p' src/nemo_safe_synthesizer/pii_replacer/replacer.py
printf '%s\n' '--- scope map implementation ---'
sed -n '120,225p' src/nemo_safe_synthesizer/pii_replacer/replacement/scope.py
printf '%s\n' '--- record-scope consumers ---'
rg -n -C6 'build_scoped_col_map|unit_key\(|row_indices|standalone_maps' \
src/nemo_safe_synthesizer/pii_replacer/replacement \
src/nemo_safe_synthesizer/pii_replacer/replacer.pyRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 42776
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- all build_scoped_col_map callers ---'
rg -n -C3 'build_scoped_col_map\(' --glob '*.py' .
printf '%s\n' '--- standalone map entry point ---'
sed -n '244,315p' src/nemo_safe_synthesizer/pii_replacer/replacement/standalone.py
printf '%s\n' '--- record-scope tests and index fixtures ---'
rg -n -C4 'record.scope|scope.*record|duplicate|non.unique|index' tests src/nemo_safe_synthesizer/pii_replacer --glob '*.py' | head -n 240
printf '%s\n' '--- pandas duplicate-label behavior probe ---'
python3 - <<'PY'
try:
import pandas as pd
except ImportError as exc:
print(f"pandas unavailable: {exc}")
else:
df = pd.DataFrame({"value": ["a", "b"]}, index=["same", "same"])
print("index:", list(df.index))
for idx in df.index:
selected = df.loc[[idx], "value"]
print(f"{idx!r}: selected={selected.tolist()!r}, unique_values={selected.dropna().unique().tolist()!r}")
data = {idx: df.loc[[idx], "value"].dropna().unique().tolist() for idx in df.index}
print("dict keys:", list(data.keys()))
print("dict result:", data)
PYRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 22392
🏁 Script executed:
set -euo pipefail
printf '%s\n' '--- public replacement APIs and duplicate-index boundary ---'
sed -n '270,340p' src/nemo_safe_synthesizer/pii_replacer/replacement/apply.py
sed -n '1,55p' src/nemo_safe_synthesizer/pii_replacer/replacement/__init__.py
rg -n -C5 'run_replacement\(|build_standalone_maps\(' \
src tests --glob '*.py' | head -n 220
printf '%s\n' '--- exact transform_df normalization context ---'
sed -n '75,90p' src/nemo_safe_synthesizer/pii_replacer/replacer.pyRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 20147
🏁 Script executed:
set -euo pipefail
sed -n '340,380p' tests/pii_replacer/replacement/test_scope.pyRepository: NVIDIA-NeMo/Safe-Synthesizer
Length of output: 1635
Handle duplicate indexes in direct replacement APIs. TabularPiiReplacer.transform_df resets non-unique indexes, but direct calls to run_replacement or build_standalone_maps do not. In those calls, record scope still collapses duplicate labels. Enforce a unique-index precondition or iterate rows positionally in build_scoped_col_map.
| def test_phone_replacement_without_a_pattern_uses_faker_phone(fixture_phone_df: pd.DataFrame): | ||
| """With no template, standalone phones get real Faker numbers (not character-class noise).""" | ||
| replaced = _replace_phones( | ||
| fixture_phone_df, | ||
| PiiColumnPlan(column_name="phone", entity_type=PiiEntity.phone_number), | ||
| standalone=False, | ||
| ) | ||
| assert (replaced != fixture_phone_df["phone"]).all() | ||
| assert replaced.nunique() == fixture_phone_df["phone"].nunique() | ||
| # Digits remain; shape need not match the original (unlike pattern_preserving_token). | ||
| assert all(re.search(r"\d", v) for v in replaced) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align the test name and docstring with the plan it builds. The name and docstring describe standalone phones, but the call passes standalone=False, so the plan puts phone in a persona set. With the Faker backend the column still routes through the standalone map, which is exactly what line 75 asserts, so the coverage is real but the intent is unclear. Either pass standalone=True, or state in the docstring that a Faker-backed persona set falls through to the standalone path.
| cfg = Config( | ||
| locale="en_US", | ||
| random_seed=7, | ||
| persona_backend="faker", | ||
| sdg_pgms_src="/tmp", | ||
| managed_assets_path=None, | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the hardcoded /tmp values that Ruff reports as errors.
Ruff reports S108 as an error on lines 140, 252, 269, and 329. Unless S108 is suppressed for tests/, lint fails in CI. The value is never read here, because every test uses the faker or managed backend, so a neutral placeholder works.
Set sdg_pgms_src from the tmp_path fixture, or use a clearly non-filesystem sentinel such as "unused-pgm-src", in all four places.
🧰 Tools
🪛 ast-grep (0.45.1)
[info] 139-139: Do not hardcode temporary file or directory names
Context: "/tmp"
Note: [CWE-377] Insecure Temporary File.
(hardcoded-tmp-file)
🪛 Ruff (0.16.1)
[error] 140-140: Probable insecure usage of temporary file or directory: "/tmp"
(S108)
Sources: Path instructions, Linters/SAST tools
| def test_card_replacement_keeps_the_columns_grouping_and_its_checksum(): | ||
| from nemo_safe_synthesizer.pii_replacer.patterns import luhn_valid | ||
|
|
||
| # Luhn-valid 16-digit numbers written the way the column writes them. | ||
| cards = ["4111-1111-1111-1111", "4012-8888-8888-1881", "4222-2222-2222-2220"] * 8 | ||
| df = pd.DataFrame({"full_name": [f"Person {i}" for i in range(24)], "card_number": cards}) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Suppress or allowlist the card-number literals so secret scanning does not fail.
OpenGrep reports three pii.credit-card-number-dashed errors on line 95. The values are the well-known public test numbers, so the finding is a false positive, but this PR also adds .github/workflows/config/.secrets.baseline, and an unsuppressed error can block CI.
Add the values to the baseline, or build them from parts in the test so no literal card-shaped string appears. Keep a short comment that records why the numbers are safe.
🧰 Tools
🪛 OpenGrep (1.26.0)
[ERROR] 95-95: Possible credit card number with dashes or spaces detected in source code. Credit card numbers should never be hardcoded or stored in source files. Use a secrets manager or tokenization service instead.
(coderabbit.pii.credit-card-number-dashed)
[ERROR] 95-95: Possible credit card number with dashes or spaces detected in source code. Credit card numbers should never be hardcoded or stored in source files. Use a secrets manager or tokenization service instead.
(coderabbit.pii.credit-card-number-dashed)
[ERROR] 95-95: Possible credit card number with dashes or spaces detected in source code. Credit card numbers should never be hardcoded or stored in source files. Use a secrets manager or tokenization service instead.
(coderabbit.pii.credit-card-number-dashed)
Source: Linters/SAST tools
| def test_pii_schema_version_refuses_an_unknown_value(): | ||
| """schema_version is the forward-compat gate: only the current release's value is accepted.""" | ||
| with pytest.raises(ValidationError): | ||
| PiiReplacerConfig.model_validate({"schema_version": 2}) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Assert which validation rule rejected schema_version: 2.
pytest.raises(ValidationError) also passes if the model rejects the payload for an unrelated reason, for example a required field that the partial dict omits. The test then no longer proves the forward-compatibility gate its docstring describes. Add match="schema_version" or assert exc_info.value.errors()[0]["loc"].
💚 Proposed fix
- with pytest.raises(ValidationError):
+ with pytest.raises(ValidationError, match="schema_version"):
PiiReplacerConfig.model_validate({"schema_version": 2})As per path instructions: "Use pytest.raises(..., match=...) for validation errors".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def test_pii_schema_version_refuses_an_unknown_value(): | |
| """schema_version is the forward-compat gate: only the current release's value is accepted.""" | |
| with pytest.raises(ValidationError): | |
| PiiReplacerConfig.model_validate({"schema_version": 2}) | |
| def test_pii_schema_version_refuses_an_unknown_value(): | |
| """schema_version is the forward-compat gate: only the current release's value is accepted.""" | |
| with pytest.raises(ValidationError, match="schema_version"): | |
| PiiReplacerConfig.model_validate({"schema_version": 2}) |
Source: Path instructions
zywind
left a comment
There was a problem hiding this comment.
The typed plan -> validate -> apply flow and the centralized entity registry
are good architectural directions. At the current head, however, I reproduced
two paths that silently retain planned PII: null group keys and readable but
malformed managed-persona assets. I also reproduced group identity instability
because discovery computes structural grain but does not use it, and explicit
persona formats being overridden at apply time.
The remaining comments separate smaller contract/test gaps from architectural
decisions. The PR's release plan already calls for the LLM follow-up before the
final merge, and #630, #631, and #632 remain open, so I am treating those tracked
items as follow-up rather than duplicating them as inline findings.
Agent-assisted review: generated by @zywind's Agent and reviewed by @zywind.
| # low-variety and can be mistaken for free text. ``scoped_column_stats`` | ||
| # recomputes ``unique_ratio`` per group. | ||
| stats = detection.scoped_column_stats(df, group_key, cfg.group_constancy_threshold) | ||
| discovery = detection.detect_structured_columns(df, stats, cfg) |
There was a problem hiding this comment.
🎯 Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift | 🧭 Human decision required
Use structural grain when allocating persona fields.
scoped_column_stats labels columns as group-constant or record-varying, but
detect_structured_columns never reads stats. In a grouped frame with one
constant full_name and 20 varying email values per group, discovery placed
both fields in person_1; apply then emitted 20 different synthetic names within
each group. Carry a typed structural-grain field into ColumnEvidence and use it
when deciding persona membership and instance identity. Please also rename the
internal scope stat (for example, to grain) so it is not confused with the
plan's record / group / dataframe replacement scope.
| title, rest = split_title(original) if label == "full_name" else (None, original) | ||
| parts = split_full_name(rest) if label == "full_name" else {label: rest} | ||
| own = infer_persona_pattern(rest, parts) | ||
| written = render_persona_pattern(own or pats[0], cast(Mapping[str, str], persona), rng=rng) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win | 👤 Author review required
Honor the plan's persona formatting patterns.
own or pats[0] always prefers a format inferred from the source value. As a
result, a hand-authored {LAST}, {First} pattern applied to John Smith emitted
Jane Doe rather than DOE, Jane. Treat supplied patterns as the allowed output
formats, using the source shape only to choose among that list when necessary,
and add an edited round-trip plan regression.
| case "group" if gk and gk in original_df.columns: | ||
| data: dict[Hashable, object] = { | ||
| cast(Hashable, gval): _unit_map(gdf[col], cast(Hashable, gval)) | ||
| for gval, gdf in original_df.groupby(gk, dropna=True) |
There was a problem hiding this comment.
🎯 Security & Privacy | 🔴 Critical | ⚡ Quick win | 🤖 Agent-fix candidate
Reject null group keys before group-scoped replacement.
groupby(..., dropna=True) omits the null bucket here and during persona
extraction, while the apply mask cannot select a null key. Through the public
TabularPiiReplacer path, a group-scoped explicit plan therefore left both a
name and an identifier unchanged on the null-key row. Reject null group keys at
this public boundary before extraction/map construction, and cover both persona
and standalone replacements in the regression test.
| if not path.exists(): | ||
| return None | ||
| try: | ||
| return pd.read_parquet(path) |
There was a problem hiding this comment.
🎯 Security & Privacy | 🔴 Critical | ⚡ Quick win | 🤖 Agent-fix candidate
Validate managed persona assets before accepting them.
Any readable parquet is accepted as a managed persona source. A file containing
only sex is sampled successfully, but it has no first_name or last_name, so
a planned full_name remains unchanged without an error or fallback. Validate
the required managed schema immediately after loading and route malformed assets
through the existing warning plus Faker fallback. Add a regression asserting
that planned persona-sourced PII is transformed.
| originals: dict, | ||
| row_indices: list, | ||
| ) -> None: | ||
| if not originals or not _instance_is_person(field_cols, originals): |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win | 📌 Follow-up acceptable
Reject unsupported scalar full_name values during validation.
An explicit plan assigning guardian_name as full_name validates, but this
apply-time discovery heuristic rejects Alice Smith and Bob Jones; that row
stays raw while an ordinary Jane Doe row is replaced. First-class repeated
personas can remain future work. For the current scalar contract, reject value
shapes that apply will not handle and tell the user to pre-split them instead of
silently passing the original PII through.
|
|
||
| from nemo_safe_synthesizer.pii_replacer.detection import column_names | ||
|
|
||
| entity_patterns = {"first_name": [r"sex|name"]} |
There was a problem hiding this comment.
🎯 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win | 🤖 Agent-fix candidate
Exercise multi-match warnings against the real registry.
The warn-and-continue behavior is covered only with monkeypatched pattern maps,
so changes to the real registry can bypass the agreed warning contract. Add
cases such as physician_gender and doctor_id using ENTITY_NAME_PATTERNS and
DEMO_LABEL_PATTERNS, and assert that the warning contains every candidate and
the chosen label. This tests the policy without hard-coding synthetic collisions
that production patterns never exercise.
| locale = replace_pii.replacement.locale | ||
| backend = replace_pii.person.backend | ||
|
|
||
| if backend == PiiPersonBackend.faker and not _faker_locale_supported(locale): |
There was a problem hiding this comment.
🎯 Stability & Availability | 🟠 Major | 🏗️ Heavy lift | 🧭 Human decision required
Resolve the managed-locale dependency on Faker.
Preflight checks Faker locale support only for the Faker backend, but managed
apply still constructs Faker for persona rendering and standalone maps. The
documented managed locales en_SG, hi_Deva_IN, and hi_Latn_IN are not
supported by the installed Faker version, so they pass preflight and then fail
with Faker's raw locale error. Decide whether those managed locales remain part
of the contract: either decouple managed rendering/randomness from Faker, or
validate every locale passed to Faker and update the documented support matrix.
|
|
||
| import pandas as pd | ||
|
|
||
| from .detection.persona_grouping import skip_reason_named_column |
There was a problem hiding this comment.
🎯 Maintainability & Code Quality | 🟡 Minor | 🏗️ Heavy lift | 🧭 Human decision required
Break the handler/detection dependency cycle.
entity_handlers imports skip_reason_named_column from detection, while
persona_grouping locally imports get_handler to avoid the reverse import at
module load. That keeps entity behavior split across the handler and detection
layers and makes the new handler seam shallower than intended. Move skip policy
behind the handler interface, or into a lower-level evidence/policy module that
both layers can depend on, so detection can consume handlers without a cycle.
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 88fb5ed8-e80f-48bc-88fa-d9dcd92fe01e
📒 Files selected for processing (11)
src/nemo_safe_synthesizer/evaluation/assets/jinja/components/training_columns.j2src/nemo_safe_synthesizer/pii_replacer/detection/column_names.pysrc/nemo_safe_synthesizer/pii_replacer/detection/persona_grouping.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/patterns/temporal.pysrc/nemo_safe_synthesizer/pii_replacer/replacement/personas.pysrc/nemo_safe_synthesizer/pii_replacer/replacer.pytests/pii_replacer/detection/test_column_names.pytests/pii_replacer/patterns/test_temporal.py
🚧 Files skipped from review as they are similar to previous changes (5)
- src/nemo_safe_synthesizer/evaluation/assets/jinja/components/training_columns.j2
- src/nemo_safe_synthesizer/pii_replacer/detection/persona_grouping.py
- src/nemo_safe_synthesizer/pii_replacer/replacer.py
- src/nemo_safe_synthesizer/pii_replacer/patterns/temporal.py
- src/nemo_safe_synthesizer/pii_replacer/replacement/personas.py
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
- GitHub Check: Unit Tests (3.11)
- GitHub Check: Unit Tests (3.14)
- GitHub Check: Unit Tests (3.12)
- GitHub Check: Unit Tests (3.13)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{md,markdown,py}
📄 CodeRabbit inference engine (.cursor/rules/agent-markdown-style.mdc)
**/*.{md,markdown,py}: Avoid decorative bold (**text**) in list items, body text, and docstrings; use structural cues (headers, list markers, colons, backticks) for emphasis instead
Use backticks for code identifiers, paths, and CLI commands in markdown and docstrings
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
tests/**
📄 CodeRabbit inference engine (.cursor/rules/repo-navigation.mdc)
tests/**: Mirrorsrc/directory structure intests/directory for test organization
Auto-mark tests by directory:tests/e2e/→e2e,tests/smoke/→smoke, otherwise default tounit
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
**/*.{py,pyi}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
**/*.{py,pyi}: Keep shared Python package code compatible with Python 3.11; do not use Python 3.12-only syntax such as PEP 695 type statements or bracketed generic parameters.
Use the repository's pinned Ruff tasks for Python formatting, import sorting, and linting rather than invoking unpinned tools directly.
Run the repository's pinnedtytype checker and maintain type-correct Python code.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
**/*.{py,sh,yaml,yml,md}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Include SPDX copyright headers in all source files, except files explicitly listed in
.copyrightignore.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
tests/**/*.py
📄 CodeRabbit inference engine (CONTRIBUTING.md)
All existing tests must pass before submitting a pull request; new features must include tests and bug fixes must include regression tests.
tests/**/*.py:__init__.pyfiles must never be added undertests/.
Usetmp_pathfixture for file operations, never write to the repo tree
Mock only external boundaries, not internal implementation details
Use@pytest.mark.parametrizefor testing multiple input combinations rather than copy-pasting similar tests
tests/**/*.py: Every test should have exactly one of the category markers:unit, smoke, e2e.
Optional dependencies: usepytest.importorskipto gate on packages that require specific extras.
Dataset/tokenizer fixtures use thefixture_prefix; CLI helpers use descriptive names (mock_workdir).
Faker: seed withfake.seed_instance(seed)andrandom.seed(seed)for reproducibility.
Naming: fixture names usefixture_prefix consistently (e.g.,fixture_iris_dataset).
print()is allowed in tests (ruffT201is suppressed fortests/). Use it freely for debug output in test functions.
Importing from another file undertests/, such astests/cli/helpers.pydoes not work due to how pytest operates.
Tests mirror source structure:tests/training/,tests/generation/, etc.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
⚙️ CodeRabbit configuration file
Review tests against tests/TESTING.md. Check marker usage, fixture naming, tmp_path usage, determinism, and GPU/vLLM process-isolation requirements. Flag slop tests that only check that code runs, assert result is not None when stronger invariants exist, over-mock internal implementation details, patch around the bug instead of reproducing it, or add broad snapshot/golden churn without a clear contract. Flag change detector tests that fail on harmless refactors, formatting, record ordering, incidental wording, or private implementation details without demonstrating a behavior regression. Prefer existing fixtures or focused new fixtures for repeated setup; keep tests DRY when reasonable without making the behavior under test opaque. print() is allowed in tests.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
**/*.{py,sh,yaml,yml,toml,md}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Use
misetasks with the repository's pinned tool versions for formatting, checking, and testing before submitting changes.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Use
uvfor everything -- neverpipor rawpython. Python 3.11–3.14 with modern syntax (X | Y,list[str],Self).
**/*.py: Use American English spelling: "initialize" not "initialise", "recognize" not "recognise", "color" not "colour".
Neverprint()for operational output. Approved alternatives:click.echo()for CLI output,sys.stdout.write()for raw output in tools.
X | YnotOptional[X]orUnion[X, Y]
list[str]notList[str],dict[str, int]notDict[str, int]
Selffor fluent method returns
Protocolfor structural subtyping when you need duck-typing boundaries
AvoidAny-- preferobject, generics, orProtocol
Prefermatch/casefor dispatch on types or tagged values. Not a blanket rule --if/elifis fine for simple boolean predicates.
Comprehensions over imperative loops where intent is clearer. No multipleforclauses -- optimize for readability, not conciseness (per Google Python Style Guide sec 2.7).
PascalCase classes, snake_case functions/variables, UPPER_SNAKE_CASE constants, leading_for private
Order of imports: 1) stdlib, 2) third-party, 3) local (enforced by ruff I001/I002)
from __future__ import annotations-- add to every module.
Google style is mandatory.
A docstring is mandatory for every function that has one or more of: being part of the public API, nontrivial size, or non-obvious logic.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
**/tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Testing gotchas:
asyncio_mode = autoinpytest.ini-- async tests work without@pytest.mark.asyncio. Theunit_testmarker is deprecated; useunit.
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
**/*
📄 CodeRabbit inference engine (STYLE_GUIDE.md)
**/*: Every source file requires an SPDX copyright header andmise run formathandles this automatically.
Newline at end of file, no trailing whitespace (enforced bypre-commit)
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
⚙️ CodeRabbit configuration file
**/*: Review as a senior maintainer for NeMo Safe Synthesizer. Prioritize issues that can change behavior, break user workflows, weaken privacy guarantees, hide failures, make tests unreliable, or create maintenance risk. Avoid generic style commentary unless it points to a concrete project convention that automated tools will not catch.
Comment only when the finding is actionable and tied to changed code. For each finding, state the impact, the condition that triggers it, and the smallest practical fix. Prefer one precise comment over broad advice. Do not ask for refactors outside the PR scope unless the changed code creates the problem.
Review type guidance: - Potential issue: use for correctness bugs, data loss, privacy leaks,
security risks, broken public APIs, invalid config behavior, missing
validation, hidden failures, nondeterministic tests, or CI breakage.
- Refactor suggestion: use for local maintainability problems introduced
by the diff when they have clear future cost, such as duplicated setup,
unclear boundaries, over-mocking, avoidable complexity, or opaque test
helpers.- Nitpick: avoid in chill mode. Do not emit formatting, import-order,
wording, or style-only comments unless automated tools cannot catch the
issue and it affects maintainability.Severity guidance: - Critical: security/privacy leaks, data loss, training/test/holdout
contamination, or broken release/package/core pipeline execution.
- Major: incorrect generation/training/evaluation behavior, broken
CLI/SDK public API, invalid config defaults or validators, or GPU/vLLM
cleanup and process-isolation bugs likely to fail CI or production
runs.- Minor: localized bugs, missing focused tests for changed behavior, or
bad test patterns that weaken regression coverage.- Trivial: small cleanup with no behavior impact. Usually suppress in
chill mode.- Info: context only. Avoid unless it helps reviewers understand risk.
Safe-Synthesizer-specific review focus: - Data ...
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
**/*.{py,md,sh,yaml,yml}
📄 CodeRabbit inference engine (STYLE_GUIDE.md)
Line length: 120 characters for code, comments, and docstrings (configured in ruff.toml)
Files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
src/**/*.py
📄 CodeRabbit inference engine (STYLE_GUIDE.md)
src/**/*.py: Every directory undersrc/that contains Python files must include an__init__.pyfile, even if empty.
Relative imports insrc/(from ..observability import get_logger), absolute imports intests/(from nemo_safe_synthesizer.observability import get_logger)
Files:
src/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
⚙️ CodeRabbit configuration file
Review library code against STYLE_GUIDE.md. Focus on behavior, API contracts, error handling, resource cleanup, typing, logging, and user-facing failures. Public APIs and nontrivial functions need Google-style docstrings.
Files:
src/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
src/nemo_safe_synthesizer/pii_replacer/**/*.py
⚙️ CodeRabbit configuration file
Treat PII replacement changes as high-risk. Check entity coverage, replacement determinism, leakage of original values, handling of empty or multilingual text, and compatibility with optional dependencies.
Files:
src/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: nina-xu
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 672
File: src/nemo_safe_synthesizer/pii_replacer/entities.py:586-603
Timestamp: 2026-08-12T19:04:29.192Z
Learning: For `src/nemo_safe_synthesizer/pii_replacer/entities.py` `unique_identifier` header detection, compact suffix headers such as `userid` and `orderid` are valid identifier candidates. Ambiguous suffix-only matches such as `valid` and `hybrid` require a dominant structured value pattern before the PII discovery pipeline adds them to the replacement plan.
Learnt from: CR
Repo: NVIDIA-NeMo/Safe-Synthesizer
Timestamp: 2026-08-12T19:30:40.415Z
Learning: Always use mise tasks or the wrapper scripts in `tools/` instead of running `ruff` or `ty` directly.
Learnt from: CR
Repo: NVIDIA-NeMo/Safe-Synthesizer
Timestamp: 2026-08-12T19:30:40.415Z
Learning: Do not commit unless the user asks for a commit or PR work.
Learnt from: CR
Repo: NVIDIA-NeMo/Safe-Synthesizer
Timestamp: 2026-08-12T19:30:40.415Z
Learning: When committing, all commits require DCO sign-off and GPG signing. Always use `git commit --signoff --gpg-sign` (or `-s -S`) -- never write the `Signed-off-by` trailer manually, and never pass `--no-gpg-sign`.
📚 Learning: 2026-05-27T22:20:37.354Z
Learnt from: kendrickb-nvidia
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 520
File: tests/generation/test_vllm_backend.py:556-587
Timestamp: 2026-05-27T22:20:37.354Z
Learning: In NVIDIA-NeMo/Safe-Synthesizer, `tests/conftest.py`’s `pytest_collection_modifyitems` hook applies pytest category markers automatically based on each test file’s path: tests under `/e2e/` get `pytest.mark.e2e`, tests under `/smoke/` get `pytest.mark.smoke`, and all other tests get `pytest.mark.unit`. Therefore, when reviewing pytest tests outside `tests/e2e/` and `tests/smoke/`, do not flag missing explicit `pytest.mark.unit` decorators on test classes/functions as an issue (the hook will add them during collection). If a new test directory/category is introduced, ensure the hook is updated so it’s categorized correctly.
Applied to files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
📚 Learning: 2026-07-27T22:07:22.590Z
Learnt from: binaryaaron
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 673
File: tests/pii_replacer/test_edit.py:327-327
Timestamp: 2026-07-27T22:07:22.590Z
Learning: When tests read structured logging context from Python `logging.LogRecord` instances, don’t access `record.ctx` directly (it isn’t declared on `LogRecord` and will break static typing). Instead, use `getattr(record, "ctx", default)` (or an appropriate fallback) to safely handle cases where `ctx` may or may not be attached. This applies even if Ruff rule `B009` isn’t enabled in the repo.
Applied to files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
📚 Learning: 2026-07-29T17:12:32.642Z
Learnt from: zywind
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 654
File: tests/config/test_parameters.py:116-118
Timestamp: 2026-07-29T17:12:32.642Z
Learning: In Pydantic validation tests (e.g., models configured with `from_attributes`), when asserting failures from `model_validate(...)`, assert the structured error details (such as `ValidationError.errors()[0]["type"]`, e.g. `"model_attributes_type"`) rather than relying on the human-readable error message text. This keeps tests stable even if wording changes, while still verifying the correct validation rule is triggered.
Applied to files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
📚 Learning: 2026-08-05T19:07:15.856Z
Learnt from: binaryaaron
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 679
File: tests/conftest.py:89-96
Timestamp: 2026-08-05T19:07:15.856Z
Learning: In the Safe Synthesizer test suite, treat CPU and CUDA installation profiles as the supported pytest collection profiles because they install PyTorch. A bare installation without PyTorch is an incomplete, unsupported profile. GPU-marked test modules may import PyTorch before tests/conftest.py::pytest_collection_modifyitems executes, so do not require that collection hook to prevent such imports.
Applied to files:
tests/pii_replacer/patterns/test_temporal.pytests/pii_replacer/detection/test_column_names.py
📚 Learning: 2026-08-12T19:04:29.192Z
Learnt from: nina-xu
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 672
File: src/nemo_safe_synthesizer/pii_replacer/entities.py:586-603
Timestamp: 2026-08-12T19:04:29.192Z
Learning: For `src/nemo_safe_synthesizer/pii_replacer/entities.py` `unique_identifier` header detection, compact suffix headers such as `userid` and `orderid` are valid identifier candidates. Ambiguous suffix-only matches such as `valid` and `hybrid` require a dominant structured value pattern before the PII discovery pipeline adds them to the replacement plan.
Applied to files:
tests/pii_replacer/detection/test_column_names.pysrc/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
📚 Learning: 2026-08-06T15:42:45.969Z
Learnt from: zywind
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 703
File: src/nemo_safe_synthesizer/pii_replacer/data_editor/detect.py:356-362
Timestamp: 2026-08-06T15:42:45.969Z
Learning: In NeMo Safe Synthesizer Python code, do not flag diagnostics solely because they report raw DataFrame column names. This is an established convention used in preflight warnings and validation errors. Flag such reporting only when an explicit repository-wide logging or privacy policy requires redaction, or when the diagnostic includes cell values, records, or other sensitive data.
Applied to files:
src/nemo_safe_synthesizer/pii_replacer/entities.pysrc/nemo_safe_synthesizer/pii_replacer/entity_handlers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/value_recognizers.pysrc/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
🪛 ast-grep (0.45.1)
src/nemo_safe_synthesizer/pii_replacer/detection/column_names.py
[warning] 43-43: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.search(pattern, name, re.IGNORECASE)
Note: [CWE-1333] Inefficient Regular Expression Complexity.
(redos-non-literal-regex-python)
🪛 Ruff (0.16.1)
src/nemo_safe_synthesizer/pii_replacer/entities.py
[warning] 612-612: Consider iterable unpacking instead of concatenation
Replace with iterable unpacking
(RUF005)
🔇 Additional comments (6)
src/nemo_safe_synthesizer/pii_replacer/entities.py (1)
171-177: LGTM!Also applies to: 191-201, 608-612, 713-784
src/nemo_safe_synthesizer/pii_replacer/entity_handlers.py (1)
8-425: LGTM!Also applies to: 466-571
tests/pii_replacer/detection/test_column_names.py (1)
265-311: LGTM!tests/pii_replacer/patterns/test_temporal.py (1)
85-100: LGTM!src/nemo_safe_synthesizer/pii_replacer/detection/column_names.py (2)
13-13: LGTM!
41-44: 🩺 Stability & AvailabilityUse repository-controlled patterns for
header_matches_patterns.
header_matches_patternsreceives only repository-controlledEntitySpec.strong_name_patternsvalues. User-configured plan patterns do not reach this function, and the registry patterns are valid regular expressions.> Likely an incorrect or invalid review comment.
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
Signed-off-by: nina-xu <19981858+nina-xu@users.noreply.github.com>
kendrickb-nvidia
left a comment
There was a problem hiding this comment.
Some higher-level comments from skimming the PR. Haven't done any detailed reading since I know there's still a lot of code iteration going on.
There was a problem hiding this comment.
question: Why is this diff here? Maybe an incorrect merge conflict resolution? Looks like possibly some other unexpected changes like pyproject.toml that are effectively reverting PRs from main.
If we do need to make changes to this secrets config, can we do it in a totally separate PR that just merges directly to main, and is not part of the feature branch changes?
| @@ -83,7 +83,7 @@ class SafeSynthesizerParameters(Parameters): | |||
|
|
|||
| replace_pii: PiiReplacerConfig | None = Field( | |||
There was a problem hiding this comment.
nit: Consider using a different name for the field, so it's clearer if using the old or new pii replacer config? If this is really the only and best name then we can keep it the same.
| def match_column_header( | ||
| col: str, | ||
| entity_patterns: Mapping[str, list[str]], | ||
| demo_patterns: Mapping[str, list[str]], |
There was a problem hiding this comment.
nit: Here and in a lot of other places where we're passing Mapping/dict and other constructed structures around, we should consider using a dataclass, pydantic model, or a regular python class to describe and constrain what's being passed around. E.g. here, the keys should always be values of an enum for both mappings. Encoding that in the typing helps catch bugs and is often easier to read.
| return [str(v) for v in vals[:k]] | ||
|
|
||
|
|
||
| def column_stats(df: pd.DataFrame) -> dict[str, dict]: |
There was a problem hiding this comment.
nit: Another great place for a dataclass for the stats instead of a generic and untypable dict. I'll stop commenting on these now :)
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Sequence | ||
| from typing import Protocol, runtime_checkable |
There was a problem hiding this comment.
question: @binaryaaron and @zywind are we using Protocol a lot in NSS, do we want to? I'd prefer explicit subclassing unless there's some particular reason not to?
|
|
||
|
|
||
| @runtime_checkable | ||
| class PiiEnhancer(Protocol): |
There was a problem hiding this comment.
question: Are other classes setup to do both discovery and replacement-time via one object? This seems a little strange and would be more logical to me to have 2 interfaces/protocols/superclasses that define standard methods each for Discover and Applier or something (with better names).
There was a problem hiding this comment.
suggestion: Can this be unified with existing datetime format inference in src/nemo_safe_synthesizer/data_processing/actions/dates.py (and maybe other places, that's one I know about)?
| cfg: entities.Config, | ||
| config: PiiReplacerConfig, | ||
| *, | ||
| enhancer: PiiEnhancer | None = None, |
There was a problem hiding this comment.
comment: I find the conceptualization of the heuristics as the base and a singular PiiEnhancer a little strange. There are many possible ways to do this discovery and we have/are implementing a few right now: LLMbased, hueristics based. Treating those as equals (and with an explicit priority or resolution path) seems better. And perhaps fits splitting out ExactRegexHeuristic, FuzzyRegexHeuristic, LLMHeuristic and then having the weighting, priority be explicit across all three?
I'm not into the full details in this PR so maybe this doesn't fit and would be more awkward, but a possible abstraction to consider.
| self.elapsed_time = 0.0 | ||
| self.resolved_plan: PiiReplacementPlan | None = None | ||
|
|
||
| def transform_df(self, df: pd.DataFrame) -> None: |
There was a problem hiding this comment.
question: Does this pattern match other steps in NSS and exhibit the abstractions we want? A "do_something" method that returns None and then separate methods that fetch the results (but thus return None or raise an exception if you haven't previously called transform_df or otherwise mean the typing status of the object is not as neatly defined).
We don't have to keep the same shape as the previous nemo_pii.py object, this is a great time to change it if we want to.
binaryaaron
left a comment
There was a problem hiding this comment.
I've taken a first pass with a few agent-powered sweeps for specific inline feedback. I'll take another pass at the broader structural revisions, likely as direct PRs to this branch early next week.
Agent-assisted review: generated by @binaryaaron's Agent and reviewed by @binaryaaron.
| detected_values: dict[str, set] = {} | ||
| else: | ||
| detected_counts = {entity_key: len(values)} if entity else {} | ||
| detected_values = {entity_key: set(str(v) for v in values)} if entity else {} |
There was a problem hiding this comment.
🎯 Stability & Availability | 🟠 Major | ⚡ Quick win | 🤖 Agent-fix candidate
Define the all-null replay result.
PII Replay divides by zero when a planned entity column contains only null values. A direct current-head probe reaches a zero unique-value denominator and raises ZeroDivisionError. Please define the empty-entity result and add an all-null regression before division.
| detected_counts: dict[str, int] = {} | ||
| detected_values: dict[str, set] = {} | ||
| else: | ||
| detected_counts = {entity_key: len(values)} if entity else {} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win | 🤖 Agent-fix candidate
Count detections by occurrence.
_build_column_statistics() counts dropna().unique() values and reports that number as detected_counts. Three rows with one repeated name report two detections. Please count occurrences for the detection metric and keep cardinality as a separate typed statistic.
| if label: | ||
| label_s = str(label) | ||
| existing.detected_entity_counts[label_s] = existing.detected_entity_counts.get(label_s, 0) + 1 | ||
| existing.detected_entity_values.setdefault(label_s, set()).add(str(ent.get("original"))) |
There was a problem hiding this comment.
🎯 Security & Privacy | 🟠 Major | 🏗️ Heavy lift | 👤 Author review required
Detect source PII embedded in free text.
Free-text PII Replay uses whole-cell Series.isin(). It reports no replay when the source entity is Ada and the generated text is Ada visited the clinic. Please compare detected spans or normalized substrings and add an embedded-entity regression.
| if not inst.synthetic_by_column: | ||
| continue | ||
| for col, syn in inst.synthetic_by_column.items(): | ||
| replaced_df.loc[inst.row_indices, col] = syn |
There was a problem hiding this comment.
🎯 Stability & Availability | 🟠 Major | ⚡ Quick win | 🤖 Agent-fix candidate
Support replacement in categorical columns.
Replacement writes synthetic strings directly into categorical columns. A value outside the existing categories raises TypeError: Cannot setitem on a Categorical with a new category. Please normalize the dtype or extend the categories at the core replacement entry point and add a persona-backed categorical regression.
| field = model_type.model_fields.get(name) | ||
| if field is None: | ||
| location = ".".join((*path, name)) | ||
| if path and path[-1] == "replace_pii" and name in _LEGACY_REPLACE_PII_KEYS: |
There was a problem hiding this comment.
🎯 Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift | 🧭 Human decision required
Define how legacy replacement plans load.
Saved configurations with replace_pii.globals or replace_pii.steps cannot resume. config.unknown_fields rejects those fields before the selected unknown-field policy can ignore them. Please define the supported saved-run window and add either a loader migration or a versioned compatibility error with tests.
| from .base import NSSBaseModel | ||
| from .types import OptionalListOrInt, OptionalListOrStr, OptionalStrList | ||
|
|
||
| __all__ = [ |
There was a problem hiding this comment.
🎯 Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift | 🧭 Human decision required
Define compatibility for removed public and serialized types.
This branch removes public config symbols and serialized data-action types without a compatibility shim or migration test. Saved artifacts and Python callers can fail before they receive a versioned diagnostic. Please define the support window, then add the required re-exports, read-only aliases, or explicit migration errors.
Summary
Suggested reading order for review
The finished design is one cohesive cutover (new engine + delete NER/
data_editor). Intermediate “split PRs” are not independently shippable onmain. Read the current tree in this order — the rest of this doc follows the same sequence:config/replace_pii.py→entities.py(EntitySpec) →models.pyplanning/validation.py→planning/io.py→preflight/checks/pii.pydetection/→patterns/→planning/discovery.pyentity_handlers.py(thin adapters for generate/skip/pattern rejection)replacement/(scope→instances→personas→standalone→free_text→apply)llm/(PiiEnhancer)replacer.py,sdk/library_builder.py, deletions, docstests/pii_replacer/(mirrors packages) + golden plansImplementation walkthrough (same order, with file paths):
pii-repl-v3-behavior-impl-notes.md
Release plan: we'll get this PR reviewed and ready for merging, but we won't merge just yet. We'll then review #665 , merge that into this branch; add the LLM mode into this branch. At that point we'll do a final check on the huge branch and merge & release.
What users experience
When
replace_piiis enabled (the default), the pipeline still runs PII replacement on the training dataframe before training. The mechanism is new:--validate/ early preflight).pii_replacement_plan.yamlinto the run directory so the discovered (or used) plan can be edited and reused.Turning it off is unchanged in spirit:
replace_pii: null,--no-replace-pii, or.with_replace_pii(enable=False).Config shape (
replace_pii)Old step/NER/classify YAML (
PiiReplacerConfig) is gone. The new top-level object isReplacePiiConfig:replacement_plan"auto_discovery"(default), a path to a plan YAML, or an inline plan (config file only)replacement.locale/seedperson.backendmanaged(default),faker, or internal-onlypgmperson.managed_assets_path/sdg_pgms_srcschema_version1— forward-compat gate for config shape (not plan YAML)llm_enhancementtrueis a preflight error and raisesParameterErrorfrom thePiiEnhancerseams when discovery/apply runllmThere is no
discovery.replace_group_key(removed). Whether a group key may be replaced is governed by protected columns (below), not a discovery toggle.A plan has:
scope:record|group|dataframe— how widely one original value keeps the same synthetic value (applies to personas and standalone identifiers/phones/cards/etc.)persona_backed_columns: named personas; columns filled from one synthetic identity; optionalmatch_persona_by(gender/ethnic_background) columns that are read onlystandalone_columns_to_replace: IDs, cards, free text, etc., replaced independentlyEach column entry is
column_name+entity_type+ optionalpatterns(formats for writing values).Important rule: the engine follows entity type, not YAML section. Putting an ID under a persona does not make it persona-sourced; putting a name only under standalone means it will not share a synthetic person with other name columns. Preflight/apply emit warnings for those mismatches; they do not rewrite the plan.
Protected columns (structural — never replaced)
Some columns define training structure and must stay intact:
data.group_training_examples_bytime_series.is_timeseries)data.order_training_examples_bytime_series.timestamp_columncolumns_to_replaceis a validation error (pii_plan_protected_column) — the plan is not silently rewritten.group_training_examples_byoutside TS mode, that column is not protected. Discovery may plan it asunique_identifier(unless other heuristics skip it, e.g. contiguous integer sequences). Scope can still begroupso replacements stay consistent within each group.Discovery (auto plan)
With
replacement_plan: auto_discovery, discovery:unique_identifier/national_idshapes).patient_first_name→ personapatient,provider_email→provider); unprefixed columns useperson_1,person_2, …. Same role → same pool; duplicate entity label or disagreeing name parts →patient_2/provider_2+ warning.first_name/last_name/full_nameonto one persona (~85% agreement over ≥3 comparable rows); otherwise split + warn.pgmphones) in standalone; marks long prose columns asfree_textwhen eligible.scopetogrouponly ifgroup_training_examples_byis set and that column exists in the dataframe; otherwisedataframe(+ warning if the key was set but missing).apply_path: identify_only): address partscity/state/zipcode(name-matched) and generic temporalsdate/datetime/time/duration(value-based — no name match needed, so oddly named date columns still qualify). These are logged, omitted from the emitted plan, and excluded from the free-text scan so their values are neither replaced nor propagated, because these columns should not contain any free-text PII.patternsfrom observed value shapes (names/emails as persona placeholders; DOB as strftime; IDs/phones as character templates). IPv4/IPv6, SSN, national_id, street_address, free_text get no templates.unique_identifier; gapped numeric IDs can still be planned. Numeric columns headedssn/national_idkeep that entity (they are not collapsed tounique_identifierand are not sequential-skipped).street_address.No LLM or NER model is used. Enabling
llm_enhancementfails at thePiiEnhancerseams (not a silent alternate path).Replacement (exact apply behavior)
Persona-sourced (from one synthetic identity when listed under a persona): names, email, street address. Phone is persona-sourced only under
pgm; undermanaged/fakerit is rebuilt from the column’s format like other identifiers.Entity-driven (scoped original→synthetic maps; standalone path always): unique IDs, cards, API keys, IPs, DOB (age-preserving perturbation keeping format), SSN, national_id, and non-
pgmphones.scopeis honored for these maps (dataframe/group/record). Underscope: record, one map is built per row — fine for typical samples; a warning fires above 25k rows.Standalone persona entities (name/email/etc. listed only under standalone): replaced with real Faker entity values (not character-class noise), without sharing a persona across columns. Placement advisories still warn.
Identified, not replaced (
city/state/zipcode/ generic temporals): pass through unchanged. If such a label somehow reaches apply (e.g. a hand-written plan), its standalone map is empty and the column is left as-is.entity_type: datein a user plan is a validation error (pii_plan_entity_type_invalid) — usedate_of_birthor drop the column.Patterns (user plans):
patternsare optional. Missing/empty lists are normal — persona columns use default persona formatting; standalone columns fall back to Faker/shape-preserving generation (or value-inferred DOB format). Wrong-family patterns (e.g.full_namewith%Y/%m/%d) are rejected at validation aspii_plan_pattern_invalid, not applied.Free text: does not replace the whole cell; substitutes values already replaced from persona-backed instances for that row (plus name tokens for partial mentions like “Dr. Smith”), and scoped standalone pairs for that row. Matching is case-insensitive; the synthetic is reshaped to the matched token’s case (upper / lower / title / else as stored). Word-boundary matching; punctuation around structured name columns is stripped for token aliases. Pairs are row-/instance-local (no merging every persona in a group into every note). A warning fires if a row exceeds 500 substitution pairs.
Persona backends:
managed— sampledatasets/{locale}.parquetunderNSS_MANAGED_ASSETS_PATHor~/.data-designer/managed-assets; missing assets fall back to Faker with a warning. Column stats report the effective backend after fallback.faker— Faker personas; locale must be supported. Auto-discovery omitsethnic_backgroundmatchers under faker; hand-written ethnic matchers are ignored with an advisory.pgm— localsdg_pgmscheckout; no fallback;en_USonly; only backend that supplies persona phone numbers.match_persona_byconditions which persona is drawn (e.g. sex→gender for name agreement). Condition columns are never replaced.Non-unique DataFrame indices are reset to a positional index (runtime warning) before apply.
After apply, column stats for the evaluation report record transform methods (
personas/Faker/PGM,pattern,perturbation,propagation).Validation and preflight
User-supplied plans are checked in early preflight (
PiiPlanValidityCheck) so--validatecatches bad plans without running replacement. Errors include unknown/duplicate columns, missingentity_type,entity_type: date, patterns that don’t match values or don’t apply to that entity, group scope without a group key, persona/matcher conflicts, and protected columns listed for replacement.Late full preflight skips
pii.plan_validity(already checked early on the full input) — it focuses on post-transform concerns (token budget, etc.).Auto-discovered plans are validated when replacement runs (after protected columns are stripped), not via the user-plan preflight check. Auto
validate_planfailures are reported as discovery/internal errors (message prefixed), except clearly user-actionable group-scope issues.Separate config checks (early + late):
llm_enhancement: true→ errorreplace_pii.globals/steps→ always rejected (even underunknown_fields: ignore)What was removed
NemoPII/data_editor(including Jinja/Faker template execution).replace_piistep/classify/NER config.GenExpression,GenFaker, etc.) that depended on that engine.fragment.pyand NER metadata on field features.discovery.replace_group_key(replaced by protected-column rules).low_card_max).Typed
ActionExecutoractions remain but are not on the default PII path.Net behavioral change vs
mainpii_replacement_plan.yamlround-tripDefault runs still replace PII automatically; the plan file in the run dir is the main new control surface for audit and override. Always review it before production / regulated use.
Auto-discovery MVP expectations
Works best on English, US-ish, well-named tabular schemas. Expect to hand-author plans for narrative-heavy data, international IDs/phones, opaque tokens, and oddly named columns (name match is required). Free text is not a redaction NER — it only propagates persona (and per-row standalone) substitutions.
Testing Plan
Paths below assume datasets live in
/root/datasetsand configs in/root/configs. Artifacts land undersafe-synthesizer-artifacts/<config>---<dataset>/<timestamp>/(plan aspii_replacement_plan.yaml, transformed data asdataset/transformed_training.csvwhen PII changed anything).For PII-only checks, prefer the SDK
TabularPiiReplacersnippets (no GPU/train). Use CLI--validatefor plan/preflight, and a tiny TinyLlama run only when you need end-to-end artifact wiring.Dataset map
/root/datasets/telco_churn.csvCustomerID,Name,Gender, city/state/zipfull_name+match_persona_bygender; ID patterns; dataframe scope; unprefixed →person_N/root/datasets/patient_events.csvpatient,provider); name-part agreement; DOB; free-text propagation/root/datasets/bike_sales.csv/root/datasets/crm/Contact.csv/root/datasets/pii_dataset_no_label.csvorpersonal_bios.csv/root/datasets/call_transcripts.csv/clinc_oos.csvSuggested matrix
--validatebad / protected plantests/pii_replacer/test_discovery_golden.pyWhat to inspect after each run
pii_replacement_plan.yaml— scope, role/person_Npersonas, matchers, patterns, free_text, protected omissionsPre-Review Checklist
Ensure that the following pass:
mise run format && mise run checkor via prek validation.mise run testpasses locallymise run test:e2epasses locallymise run test:ci-containerpasses locally (recommended)/syncon this PR to trigger a run (auto-triggers on ready-for-review)Pre-Merge Checklist
Other Notes
Summary by CodeRabbit
New Features
Bug Fixes
Documentation