Skip to content

Production-audit remediation: safe-by-default cleaning, validator-gated PII, honest release posture#154

Merged
JohnnyWilson16 merged 4 commits into
mainfrom
fix/prod-audit-remediation-jwd
Jul 20, 2026
Merged

Production-audit remediation: safe-by-default cleaning, validator-gated PII, honest release posture#154
JohnnyWilson16 merged 4 commits into
mainfrom
fix/prod-audit-remediation-jwd

Conversation

@JohnnyWilson16

@JohnnyWilson16 JohnnyWilson16 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Remediates the July 2026 "FreshData v1.2.0 Production Readiness Audit" (run against 497c2f6). Every finding was independently reproduced before fixing; one audit conclusion turned out to be stale bookkeeping rather than a code defect, and one gate-integrity problem the audit missed was found and fixed.

P0 — release posture

  • The four "KNOWN-RED" TruthBench gates were already green. A from-scratch fail-closed run (--profile release --backends pandas,polars,duckdb --require-backends --repeats 2 --check) passes 48/48 gates at the audited revision 497c2f6 and at main — the release-audit fixes that shipped in v1.2.0 fixed them, but the committed baseline.json still recorded the pre-fix state, which is what the audit read. The baseline is refreshed from a green run: any future failure of those gates is now a PR-ratchet regression, not a shrug.
  • Classifier downgraded to 4 - Beta. The project's own absolute gates have not yet held through a completed release (the v1.2.0 attempt stopped on the CleanBench T5 runtime gate), so Production/Stable was an overclaim.
  • Re-armed the neutered T5 gate. The temporary probes merged as probe: profile T5 default path on CI (temporary, never merged) #152/probe: profile T5 on green commit (temporary) #153 left || true on perf-regression.yml's CleanBench T5 step, silently disabling both the nightly gate and its failure alerting. Removed, along with the throwaway profiler block (release.yml's copy was never affected).

P1 — safe-by-default behavior (breaking by design, all documented in CHANGELOG)

Area Before Now
Duplicates removed by default (60-row frame silently became 2 rows) detected + reported; removal opt-in (drop_duplicates=True, CLI --drop-duplicates); duplicate_ratio_action="error" stops suspicious joins
Dates one 13/01/2023 flipped a whole column to day-first dayfirst="auto" never infers; ambiguous values quarantine into report.coerced_cells with an audit action; explicit dayfirst parses fully
Outliers aggressive winsorized (lognormal max 6.4M → 168k) auto flags under every strategy; explicit cap uses skew-aware log-space fences (widen-only)
Name heuristics amount preserved, xyz capped on identical data behind domain_sensitive_names=False; identical data → identical treatment
PII detection DOB/Aadhaar-like/UUID/licence → PHONE; IBAN → CREDIT_CARD post-match validators: phone shape+boundary, Luhn, IBAN mod-97; zero true positives lost (canary-verified); checksum findings report source="checksum"
anonymize() silent no-op with a warning raises ValueError without rules/detection config
CSV output formula injection passed through by default every first-party CSV sink sanitizes by default (incl. leading-whitespace triggers); --no-sanitize-formulas / sanitize_formulas=False opt out

Also closes a real invariant hole found during the work: the never-impute-coerced-cells guard only read the 1000-row-capped preview list, so casualties beyond the cap could be imputed (new uncapped report.coerced_rows).

Verification

  • 62 new regression tests (tests/test_audit_core_defaults.py, tests/test_audit_privacy_csv.py), each pinned to a confirmed audit repro; existing tests updated only where they encoded the old unsafe defaults (none weakened/skipped).
  • Full supported lane (-m "not online and not large", 4039 tests): pass. ruff (full tree): clean. mypy src (3.12 target locally): clean. Wheel + sdist build: clean, wheel METADATA carries the Beta classifier.
  • TruthBench: 48/48 gates, exit 0; refreshed baseline shows zero regressions. CleanBench T1–T5 --check-gates: all release gates pass (T5 runtime +6.4% vs ≤20% budget).
  • An independent fresh-context adversarial review of the full diff (own probes, test-gaming audit, golden-snapshot spot checks, canary regression hunt) confirmed the fixes hold; its one caveat — generic dash-grouped IDs (ISBN/GPS-style) can still match PHONE — predates this PR (the old regex matched them too, with no validator at all) and is left as a follow-up tightening.

Known residual: local coverage reads 91.9% because this environment lacks the optional extras CI installs (the unmodified base reads 91.94% in the same venv; the diff's delta is −0.02pt with ~1,900 lines added) — CI's 93% gate is the authoritative check on this PR.

Prepared with AI-assisted tooling; reviewed and directed by the author.

Summary by CodeRabbit

  • New Features

    • Duplicate rows are now detected and reported by default; removal requires explicit opt-in.
    • Outliers are flagged by default, while capping requires explicit configuration.
    • Ambiguous dates are quarantined for review instead of inferred automatically.
    • CSV and spreadsheet exports now sanitize formula-like content by default, with opt-out controls.
    • Added CLI options for duplicate removal and formula sanitization.
  • Bug Fixes

    • Improved PII detection accuracy and validation for phone numbers, cards, and IBANs.
    • Anonymization now fails clearly when no rules or detection settings are provided.

- drop_duplicates now False under every strategy: duplicates are detected
  and reported, never removed, until explicitly requested. New
  duplicate_ratio_action="warn"|"error" (DuplicateRatioError) escalation,
  shared with all native backends.
- dayfirst="auto" no longer infers a column-wide day/month order from
  sampled values; day/month-ambiguous dates quarantine into
  report.coerced_cells with an audit action, unless dayfirst is explicit.
- outlier_action="auto" flags under every strategy including aggressive;
  winsorizing requires an explicit cap and then uses skew-aware log-space
  fences (widen-only) so lawful heavy tails survive. Column-name outlier
  heuristics moved behind domain_sensitive_names=False.
- Closed a real invariant hole: the never-impute-coerced-cells guard read
  only the 1000-row-capped preview; new uncapped report.coerced_rows.
- Gold-oracle harness and TruthBench duplicate-case observation updated to
  the detection-only default; goldens regenerated (sanctioned diffs only).
- New regression suite tests/test_audit_core_defaults.py (21 tests).
…SV formula safety

- PHONE/CREDIT_CARD/IBAN regex matches must now pass post-match
  validators: structural/boundary plausibility for phones, Luhn (13-19
  digits) for cards, ISO 13616 mod-97 for IBANs (spaced and compact).
  Kills the date/UUID/licence/Aadhaar-like/semver -> PHONE and
  IBAN -> CREDIT_CARD false-positive classes; zero true positives lost
  (canary-verified). Checksum-verified findings report source=checksum.
- anonymize() with no rules and no detection_config raises ValueError
  instead of silently returning unmasked data.
- Every first-party CSV sink sanitizes spreadsheet formulas by default
  (clean_csv, clean/apply-plan CLI, streaming CLI incl. quarantine export,
  HTML-report ledger download), now also covering triggers behind leading
  whitespace; sanitize_formulas=False / --no-sanitize-formulas opt out.
- New CLI --drop-duplicates opt-in flag; fd.clean docstring updated to the
  detection-only duplicates default.
- New regression suite tests/test_audit_privacy_csv.py (41 tests).
… re-armed T5 gate

- Development Status classifier 5 - Production/Stable -> 4 - Beta: the
  project's own absolute release gates (CleanBench T5 on release
  infrastructure) have not yet held through a completed release.
- Refreshed benchmarks/truthbench/results/baseline.json: the four gates
  recorded KNOWN-RED (cleaning:raw_pii_leakage, cleaning:review_routing,
  cleaning:exact_repair, generated_code_sandbox) have passed since the
  v1.2.0 release-audit fixes; the stale record was itself the audit's
  P0-1 finding. All 48 gates green; any failure is now a PR-ratchet
  regression.
- perf-regression.yml: removed the '|| true' and throwaway profiler left
  by the temporary T5 probes (PRs #152/#153), restoring the nightly gate
  and its failure alerting.
- CHANGELOG/README/docs updated for the new safe defaults.
@strix-security

Copy link
Copy Markdown
Contributor

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@JohnnyWilson16, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 518b5929-e3eb-4027-8628-57c5e800fbbc

📥 Commits

Reviewing files that changed from the base of the PR and between 18b784c and e5ca53b.

📒 Files selected for processing (2)
  • src/freshdata/engine/context.py
  • tests/test_audit_core_defaults.py
📝 Walkthrough

Walkthrough

The PR changes cleaning safety defaults: duplicates are report-only unless explicitly removed, automatic outlier handling flags values, ambiguous dates are quarantined, PII detection is validated, and CSV exports sanitize formulas by default. Native backends, CLIs, reports, documentation, fixtures, tests, and quality gates are updated accordingly.

Changes

Production-readiness safety defaults

Layer / File(s) Summary
Duplicate detection and opt-in removal
src/freshdata/config.py, src/freshdata/steps/duplicates.py, src/freshdata/execution/..., src/freshdata/enterprise/cli.py, tests/...
Duplicates are detected and reported by default; removal requires drop_duplicates=True, with duplicate-ratio validation and backend coverage.
Ambiguous date quarantine and coercion tracking
src/freshdata/steps/dtypes.py, src/freshdata/report.py, src/freshdata/engine/missing.py, tests/...
dayfirst="auto" quarantines ambiguous values, preserves original cells, and records affected row keys.
Flag-only defaults and skew-aware capping
src/freshdata/engine/..., src/freshdata/steps/outliers.py, src/freshdata/execution/backends/..., tests/...
Automatic outlier handling flags without rewriting; explicit capping uses skew-aware widened fences, with domain-sensitive preservation opt-in.
PII validation and sanitized exports
src/freshdata/enterprise/privacy.py, src/freshdata/_util.py, src/freshdata/streaming/_cli.py, src/freshdata/render/renderers.py, tests/...
PII matches receive post-match and checksum validation, anonymization fails closed without configuration, and CSV formula sanitization defaults to enabled with opt-outs.
Quality gates and synchronized artifacts
.github/workflows/perf-regression.yml, benchmarks/..., CHANGELOG.md, README.md, docs/..., pyproject.toml, tests/fixtures/...
The T5 gate now fails on command failure, TruthBench baselines are refreshed, release/docs metadata describe the new behavior, and golden reports are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: johnnywilson-portfolio

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it misses required template sections like Description, Fixes #, Type of Change, and Checklist. Add the template headings, include an issue reference if applicable, select the change type, and complete the checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: safe-by-default cleaning, tighter PII validation, and release posture fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/prod-audit-remediation-jwd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

FreshData benchmark report — performance

  • freshdata: ?
  • python: ?
  • platform: ?
fixture n_rows n_cols p50 s p95 s peak MB repair % false-repair % preserve % trust monotonic export %

Authored-code reduction (Metric 6)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/cleaning-engine.md (1)

65-71: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Stale "Duplicates" section still says removed by default — contradicts line 25 and the new default.

This section wasn't updated alongside Layer 1's table entry (line 25) and still claims duplicates are removed automatically, which is no longer true (drop_duplicates now defaults to False; rows are detected/reported only).

📝 Proposed fix
 ### Duplicates

-Exact duplicates are removed by default (count and percentage reported).
+Exact duplicates are detected and reported by default, but not removed —
+pass `drop_duplicates=True` to remove them (count and percentage reported).
 Time-indexed frames never lose rows unless `allow_timeseries_duplicates=True`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/cleaning-engine.md` around lines 65 - 71, Update the “Duplicates”
section in cleaning-engine.md to reflect that drop_duplicates defaults to false:
duplicates are detected and reported by default, not removed. Preserve the
documented time-series exception and the duplicate_subset aggregation behavior,
adjusting only statements that imply automatic removal.
🧹 Nitpick comments (2)
docs/threat-model.md (1)

20-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using "who" instead of "that" when referring to a person.

For better readability, use "who" when referring to "a consumer".

💡 Proposed grammatical tweak
-JSONL and Parquet are never altered. **Residual risk:** a consumer that
+JSONL and Parquet are never altered. **Residual risk:** a consumer who
 opted out and opens the CSV in a spreadsheet re-accepts the injection risk.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/threat-model.md` around lines 20 - 34, Update the residual-risk sentence
in the threat-model documentation to use “who” instead of “that” when referring
to a consumer, without changing its meaning or any surrounding CSV safety
details.

Source: Linters/SAST tools

src/freshdata/steps/duplicates.py (1)

175-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant check_duplicate_ratio call on the detection-only branch.

high_ratio is computed unconditionally at line 178, but when drop_duplicates=False the code immediately calls report_detected_duplicates, which recomputes the identical check internally — the line-178 result is discarded. Not a correctness bug (an action="error" escalation still fires either way), just a wasted call and a slightly confusing dead value.

♻️ Suggested fix
     n_before = len(df)
     pct = 100.0 * n_dup / n_before
     where = f" (compared on {subset})" if subset else ""
-    high_ratio = check_duplicate_ratio(n_dup, n_before, config)
 
     if not config.drop_duplicates:
         report_detected_duplicates(n_dup, n_before, config, report, subset=subset)
         return df
 
+    high_ratio = check_duplicate_ratio(n_dup, n_before, config)
+
     if isinstance(df.index, pd.DatetimeIndex) and not config.allow_timeseries_duplicates:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/freshdata/steps/duplicates.py` around lines 175 - 182, Move the
check_duplicate_ratio call and high_ratio assignment below the
!config.drop_duplicates early-return branch in the duplicate-processing flow.
Keep report_detected_duplicates as the detection-only path, and compute
high_ratio only when the subsequent drop-duplicates logic uses it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/freshdata/enterprise/privacy.py`:
- Around line 946-956: Update the fail-closed guard in anonymize() to also raise
when no rules are provided and detection_config is disabled, matching the
downstream enabled check. Extend test_anonymize_raises_when_nothing_to_apply to
cover PIIDetectionConfig(enabled=False) while preserving the existing error
behavior.

---

Outside diff comments:
In `@docs/cleaning-engine.md`:
- Around line 65-71: Update the “Duplicates” section in cleaning-engine.md to
reflect that drop_duplicates defaults to false: duplicates are detected and
reported by default, not removed. Preserve the documented time-series exception
and the duplicate_subset aggregation behavior, adjusting only statements that
imply automatic removal.

---

Nitpick comments:
In `@docs/threat-model.md`:
- Around line 20-34: Update the residual-risk sentence in the threat-model
documentation to use “who” instead of “that” when referring to a consumer,
without changing its meaning or any surrounding CSV safety details.

In `@src/freshdata/steps/duplicates.py`:
- Around line 175-182: Move the check_duplicate_ratio call and high_ratio
assignment below the !config.drop_duplicates early-return branch in the
duplicate-processing flow. Keep report_detected_duplicates as the detection-only
path, and compute high_ratio only when the subsequent drop-duplicates logic uses
it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b506b56-bfd9-484b-9713-f05c158dfc9e

📥 Commits

Reviewing files that changed from the base of the PR and between ab56479 and 18b784c.

📒 Files selected for processing (65)
  • .github/workflows/perf-regression.yml
  • CHANGELOG.md
  • README.md
  • benchmarks/harness_metrics.py
  • benchmarks/truthbench/results/baseline.json
  • benchmarks/truthbench/runner.py
  • docs/cleaning-engine.md
  • docs/quickstart.md
  • docs/threat-model.md
  • pyproject.toml
  • src/freshdata/_util.py
  • src/freshdata/api.py
  • src/freshdata/cleaner.py
  • src/freshdata/config.py
  • src/freshdata/engine/missing.py
  • src/freshdata/engine/model_select.py
  • src/freshdata/engine/outliers.py
  • src/freshdata/enterprise/cli.py
  • src/freshdata/enterprise/privacy.py
  • src/freshdata/execution/_native_steps.py
  • src/freshdata/execution/_plan.py
  • src/freshdata/execution/backends/_duckdb.py
  • src/freshdata/execution/backends/_freshcore.py
  • src/freshdata/execution/backends/_polars.py
  • src/freshdata/execution/backends/_spark.py
  • src/freshdata/experimental/ai_copilot.py
  • src/freshdata/render/renderers.py
  • src/freshdata/report.py
  • src/freshdata/steps/dtypes.py
  • src/freshdata/steps/duplicates.py
  • src/freshdata/steps/outliers.py
  • src/freshdata/streaming/_cli.py
  • tests/fixtures/golden/aqi_sample.balanced.report.json
  • tests/fixtures/golden/duplicate_heavy.balanced.report.json
  • tests/fixtures/golden/fraud_signals.balanced.report.json
  • tests/fixtures/golden/large_panel.balanced.report.json
  • tests/fixtures/golden/sales_export.balanced.report.json
  • tests/fixtures/golden/sensor_timeseries.balanced.report.json
  • tests/fixtures/golden_diff_summary.jsonl
  • tests/fixtures/online/golden/air_quality_uci.balanced.report.json
  • tests/fixtures/online/golden/heart_cleveland.balanced.report.json
  • tests/fixtures/online/golden/iris.balanced.report.json
  • tests/fixtures/online/golden/wine_quality.balanced.report.json
  • tests/test_api.py
  • tests/test_audit_core_defaults.py
  • tests/test_audit_privacy_csv.py
  • tests/test_backend_honesty.py
  • tests/test_cleanbench.py
  • tests/test_compare_matrix.py
  • tests/test_config_tuple_coercion.py
  • tests/test_csv_formula_sanitize.py
  • tests/test_dtypes.py
  • tests/test_engine_duplicates.py
  • tests/test_engine_outliers.py
  • tests/test_enterprise_privacy.py
  • tests/test_execution/test_duckdb_engine.py
  • tests/test_execution/test_fallback_policy.py
  • tests/test_execution/test_plan_generator.py
  • tests/test_explain.py
  • tests/test_insight.py
  • tests/test_outliers.py
  • tests/test_peel_cli.py
  • tests/test_properties.py
  • tests/test_report.py
  • tests/test_rows.py

Comment thread src/freshdata/enterprise/privacy.py
The Validation Gauntlet caught a cascade from the detection-only duplicates
default: an injected exact-duplicate row broke the all-unique key check, so
a near-unique identifier (healthcare mrn) fell back to categorical and its
missing value was filled with 'Unknown' — a labelled corruption.

build_contexts now computes the full-row duplicate mask once; when the raw
all-unique check fails, the id role is re-tested on distinct rows only, and
only the upgrade to id is accepted. Unhashable payloads skip the mask.
Gauntlet gates green again; TruthBench 48/48 and the full fast lane pass.
@JohnnyWilson16
JohnnyWilson16 merged commit e2bfd64 into main Jul 20, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant