Skip to content

Commit ed74ac0

Browse files
feat(compliance): add regulatory compliance report generators
Add an additive `freshdata.compliance` module that maps a CleanReport's actions to five named regulatory control frameworks and emits standards-grade audit artifacts: - 21 CFR §11.10(e): time-stamped, attributable, non-obscuring change log - GDPR Article 30 (record of processing) + Article 17 (erasure log) - ALCOA+ data-integrity attestation - SOX-404 transformation-control evidence pack - HIPAA Safe Harbor 18-identifier coverage (45 CFR §164.514(b)(2)) The entry point `generate_compliance_report(report, frameworks, config=...)` returns a ComplianceBundle exposing to_dict/to_json/to_frame/summary; each framework report carries passed/warnings/errors and a non-negotiable advisory caveat. The existing public API is untouched. A normalization adapter maps the real `Action.step` vocabulary onto the frameworks' concepts; optional `dataframe=` and `enterprise_result=` arguments enrich the artifacts with inferred column roles (infer_roles), the 0-100 Data Trust Score, and PII-masking / fuzzy- cluster lineage, degrading gracefully when those are not supplied. The 21 CFR audit's treatment of normalising rewrites is configurable via `ComplianceConfig.strict_cfr_normalization` (default False keeps lossless normalisations non-obscuring; True treats any rewrite without a retained pre-image as obscuring, failing the gate). Tests: tests/test_compliance/ (40 tests). Full suite green; total coverage 94.95% (gate 93%). ruff + mypy clean. Developed with AI tooling assistance.
1 parent 5cb7ea7 commit ed74ac0

21 files changed

Lines changed: 2070 additions & 0 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ adheres to [Semantic Versioning](https://semver.org/).
77
## [Unreleased]
88

99
### Added
10+
- New `freshdata.compliance` subpackage that maps a `CleanReport` onto regulatory
11+
control frameworks and emits standards-grade audit artifacts via
12+
`generate_compliance_report(report, frameworks=[...]) -> ComplianceBundle`.
13+
Five frameworks ship: `21cfr_11` (21 CFR §11.10(e) audit trail), `gdpr_30`
14+
(Article 30 + 17), `alcoa_plus` (ALCOA+ data integrity), `sox_404`
15+
(transformation controls), and `hipaa_safe_harbor` (18-identifier coverage).
16+
Reports are purely additive and report-only (never mutate the input). Optional
17+
`dataframe=` recovers column roles/missing ratios via `infer_roles`, and
18+
`enterprise_result=` folds in the Data Trust Score, PII-masking events, and
19+
clustering lineage. `ComplianceConfig.strict_cfr_normalization` (default
20+
`False`) toggles whether lossless normalising rewrites count as obscuring for
21+
the 21 CFR gate.
1022
- Four new domain validator packs: `healthcare` (FHIR/US Core — `Patient`,
1123
`Observation`, `Encounter` with `fhir_resource=`/auto-detection), `education`
1224
(Ed-Fi), `agriculture` (ADAPT, with area/yield unit coercion), and `media`

‎docs/api-reference.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,13 @@ The `freshdata.enterprise` subpackage is documented in the
6161
```python
6262
from freshdata.enterprise import clean_enterprise, EnterpriseConfig
6363
```
64+
65+
## Compliance
66+
67+
The `freshdata.compliance` subpackage maps a `CleanReport` onto regulatory control
68+
frameworks; it is documented in the [compliance reports guide](compliance.md).
69+
Import its symbols lazily:
70+
71+
```python
72+
from freshdata.compliance import generate_compliance_report, ComplianceConfig
73+
```

‎docs/compliance.md‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
title: Compliance reports
3+
description: >-
4+
Map a freshdata CleanReport onto regulatory control frameworks — 21 CFR Part 11,
5+
GDPR, ALCOA+, SOX-404, and HIPAA Safe Harbor — and emit standards-grade audit
6+
artifacts.
7+
keywords: compliance report python, 21 cfr part 11 audit trail, gdpr article 30, hipaa safe harbor, sox 404 data controls, alcoa+
8+
---
9+
10+
# Compliance reports
11+
12+
The `freshdata.compliance` subpackage turns a [`CleanReport`](cleaning-engine.md)
13+
into a regulatory audit artifact. Each *generator* maps the transformations
14+
freshdata applied — imputations, outlier handling, normalisations, deduplication,
15+
PII masking — onto a named control framework and emits a structured report you can
16+
attach to a data-governance workflow.
17+
18+
The generators are **purely additive**: they never mutate the input report or
19+
DataFrame. They read what freshdata already recorded and re-express it against a
20+
framework's controls.
21+
22+
!!! warning "Report generation, not certification"
23+
A compliance artifact **supports** a compliance workflow; it does not
24+
**constitute** a legal determination of compliance. Every report carries a
25+
verbatim caveat ([`GENERAL_CAVEAT`](#caveat)) to that effect, and review by a
26+
qualified compliance professional is required.
27+
28+
## Quickstart
29+
30+
```python
31+
import freshdata as fd
32+
from freshdata.compliance import generate_compliance_report, ComplianceConfig
33+
34+
cleaned, report = fd.clean(df, return_report=True)
35+
36+
bundle = generate_compliance_report(
37+
report,
38+
frameworks=["21cfr_11", "hipaa_safe_harbor"],
39+
config=ComplianceConfig(operator_id="svc-1", retention_days=2555),
40+
dataframe=df, # optional: recovers column roles + missing ratios
41+
)
42+
43+
bundle.summary()
44+
# {'21cfr_11': {'passed': True, 'warnings': [], 'errors': []},
45+
# 'hipaa_safe_harbor': {'passed': True, 'warnings': [...], 'errors': []}}
46+
47+
bundle["hipaa_safe_harbor"].passed # -> True / False
48+
print(bundle.to_json()) # full audit artifact as JSON
49+
```
50+
51+
`generate_compliance_report` also accepts an enterprise result directly:
52+
`generate_compliance_report(enterprise_result, frameworks=[...])` (or
53+
`report` exposing `clean_report` + `trust_after`) folds in the embedded report
54+
and trust/mask data automatically.
55+
56+
## Frameworks
57+
58+
Pass one or more of these keys in `frameworks=`:
59+
60+
| Key | Framework | What it documents |
61+
| --- | --- | --- |
62+
| `21cfr_11` | 21 CFR §11.10(e) | A tamper-evident audit trail: one entry per action, each marked non-obscuring (a pre-image was retained or the change cannot hide prior data). |
63+
| `gdpr_30` | GDPR Article 30 + Article 17 | Record of processing activities plus erasure (right-to-be-forgotten) evidence. |
64+
| `alcoa_plus` | ALCOA+ | Data-integrity attributes (Attributable, Legible, Contemporaneous, Original, Accurate, +). |
65+
| `sox_404` | SOX-404 transformation control | Internal-control evidence over data transformations, gated on the Data Trust Score. |
66+
| `hipaa_safe_harbor` | HIPAA Safe Harbor | Coverage of the 18 Safe Harbor identifiers, flagging any that remain unmasked. |
67+
68+
Unknown keys raise `ValueError` listing the valid keys.
69+
70+
## Configuration
71+
72+
`ComplianceConfig` carries the caller-supplied context; every field has a sensible
73+
default, so `ComplianceConfig()` is valid. The most useful knobs:
74+
75+
| Field | Default | Purpose |
76+
| --- | --- | --- |
77+
| `operator_id` | `None` (`"system"`) | Operator recorded on each 21 CFR audit entry. |
78+
| `retention_days` | `2555` (7 years) | Retention period stamped on audit entries. |
79+
| `masked_columns` | `[]` | Columns known to be PII-masked (report-only path; merged with any enterprise mask report). |
80+
| `trust_score` | `None` | 0–100 trust score for the SOX / 21 CFR gates when no enterprise result is supplied. |
81+
| `fail_on_hipaa_gap` | `False` | Raise [`ComplianceGapError`](#errors) instead of warning when Safe Harbor gaps remain. |
82+
| `strict_cfr_normalization` | `False` | See below. |
83+
| `controller_name` / `controller_contact` / `processing_purpose` / `legal_basis` / `data_subject_categories` | — | GDPR Article 30 record fields. |
84+
85+
### `strict_cfr_normalization`
86+
87+
Controls how the 21 CFR audit classifies *normalising rewrites* (whitespace
88+
trims, sentinel canonicalisation):
89+
90+
- **`False` (default)** — these lossless normalisations are treated as
91+
**non-obscuring**: trimming `" Ann "` to `"Ann"` cannot hide prior information,
92+
so the gate stays green. Only genuine value rewrites with no retained pre-image
93+
(outlier capping, fuzzy clustering) fail the gate.
94+
- **`True`** — any rewrite that did not retain a pre-image, *including*
95+
normalisation, is treated as **obscuring**: the entry's
96+
`original_value_class` becomes `"not_captured"`, `non_obscuring_guarantee`
97+
becomes `False`, a warning is emitted, and the gate fails. Use this when your SOP
98+
requires an independently recorded pre-image for every value change.
99+
100+
## Richer evidence
101+
102+
Two optional, keyword-only arguments add evidence when available:
103+
104+
- **`dataframe=`** — the source frame. Recovers per-column roles and missing
105+
ratios via [`freshdata.infer_roles`](api-reference.md), sharpening the HIPAA and
106+
ALCOA reports.
107+
- **`enterprise_result=`** — an [enterprise](feature-overview.md) result supplying
108+
the 0–100 Data Trust Score, PII-masking events, and fuzzy-clustering lineage.
109+
110+
## Output
111+
112+
`generate_compliance_report` returns a `ComplianceBundle` — a mapping of framework
113+
key to `FrameworkReport`:
114+
115+
| On `ComplianceBundle` | Returns |
116+
| --- | --- |
117+
| `bundle.summary()` | `{key: {"passed", "warnings", "errors"}}` for a quick gate check. |
118+
| `bundle[key]` | The `FrameworkReport` for one framework. |
119+
| `key in bundle`, `len(bundle)`, `list(bundle)` | Membership, count, and the framework keys. |
120+
| `bundle.to_dict()` / `bundle.to_json(indent=2)` | The full artifact as a dict / JSON string. |
121+
| `bundle.to_frame()` | All reports flattened into a single `pandas.DataFrame`. |
122+
123+
Each `FrameworkReport` exposes `framework_key`, `framework_name`, `passed` (bool),
124+
`warnings`, `errors`, and `data` (the framework-specific payload, e.g. the 21 CFR
125+
`audit_entries` or the HIPAA identifier coverage), plus its own `to_dict()`,
126+
`to_json()`, and `to_frame()`.
127+
128+
## Errors {#errors}
129+
130+
- `ValueError` — an unknown framework key was requested.
131+
- `ComplianceGapError` — HIPAA Safe Harbor gaps remain **and**
132+
`config.fail_on_hipaa_gap=True`. Otherwise gaps surface as warnings on the
133+
report and `passed` is `False`.
134+
135+
## Caveat {#caveat}
136+
137+
Every report embeds `GENERAL_CAVEAT` (importable from `freshdata.compliance`)
138+
unless a framework defines its own verbatim caveat. It states plainly that the
139+
artifact documents freshdata's transformations and their mapping to the named
140+
control framework, but is not a certified compliance system and does not
141+
constitute a legal determination — professional review is required.

‎docs/feature-overview.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ print(result.quality.to_markdown())
4848
assert result.passed_gate
4949
```
5050

51+
## Compliance reports
52+
53+
The `freshdata.compliance` subpackage turns a `CleanReport` into a regulatory
54+
audit artifact, mapping freshdata's transformations onto named control
55+
frameworks — 21 CFR Part 11, GDPR (Art. 30/17), ALCOA+, SOX-404, and HIPAA Safe
56+
Harbor. The generators are purely additive and report-only. See the
57+
[compliance reports guide](compliance.md).
58+
5159
## Polars support
5260

5361
```python

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ nav:
106106
- Cleaning engine: cleaning-engine.md
107107
- Data profiling: data-profiling.md
108108
- Feature overview: feature-overview.md
109+
- Compliance reports: compliance.md
109110
- Examples: examples.md
110111
- Benchmarks: benchmarks.md
111112
- API Reference: api-reference.md

‎src/freshdata/__init__.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
from .profile import ColumnProfile, Profile
3131
from .report import Action, CleanReport
3232

33+
# Compliance report generators (additive — Phase 1 roadmap). Light import:
34+
# only stdlib + pandas at load; the enterprise layer is touched lazily at call time.
35+
from .compliance import ComplianceBundle, ComplianceConfig, generate_compliance_report
36+
3337
__version__ = "1.0.0"
3438

3539
__all__ = [
@@ -40,13 +44,16 @@
4044
"Cleaner",
4145
"ColumnPlan",
4246
"ColumnProfile",
47+
"ComplianceBundle",
48+
"ComplianceConfig",
4349
"ExplainReport",
4450
"Profile",
4551
"__version__",
4652
"clean",
4753
"compare_clean",
4854
"compare_plans",
4955
"explain_clean",
56+
"generate_compliance_report",
5057
"infer_roles",
5158
"profile",
5259
"suggest_plan",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""21 CFR §11.10(e) — computer-generated, time-stamped audit trails."""
2+
3+
from __future__ import annotations
4+
5+
from ._adapter import ComplianceContext, NormalizedAction
6+
from ._base import (
7+
GENERAL_CAVEAT,
8+
ComplianceConfig,
9+
FrameworkReport,
10+
new_entry_id,
11+
)
12+
13+
FRAMEWORK_KEY = "21cfr_11"
14+
FRAMEWORK_NAME = "21 CFR §11.10(e)"
15+
16+
#: Verbatim regulatory text the audit trail is designed to satisfy.
17+
CONTROL_TEXT = (
18+
"Use of computer-generated, time-stamped audit trails to independently "
19+
"record the date and time of operator entries and actions that create, "
20+
"modify, or delete electronic records. Record changes shall not obscure "
21+
"previously recorded information and computer-generated audit trails shall "
22+
"be retained for a period at least as long as that required for the subject "
23+
"electronic records."
24+
)
25+
26+
27+
def _is_obscuring(action: NormalizedAction, *, strict: bool) -> bool:
28+
"""Whether an action could obscure previously recorded information.
29+
30+
Deletions are inherently non-obscuring once logged, and imputation/
31+
structural/intentional-mask changes retain (or never had) a prior value. A
32+
value overwrite with no retained pre-image (``not_captured``) always
33+
obscures. Normalising rewrites (whitespace trim, sentinel canonicalisation)
34+
obscure only when ``strict`` is set.
35+
"""
36+
if action.record_action_type == "DELETE":
37+
return False
38+
if action.original_basis == "not_captured":
39+
return True
40+
return strict and action.original_basis == "normalized"
41+
42+
43+
def generate_21cfr11(ctx: ComplianceContext, config: ComplianceConfig) -> FrameworkReport:
44+
"""Emit a session header plus one audit entry per normalized action."""
45+
operator_id = config.operator_id or "system"
46+
strict = config.strict_cfr_normalization
47+
warnings: list[str] = []
48+
audit_entries: list[dict] = []
49+
50+
for action in ctx.actions:
51+
obscuring = _is_obscuring(action, strict=strict)
52+
audit_entries.append(
53+
{
54+
"entry_id": new_entry_id("CFR11"),
55+
"session_id": ctx.session_id,
56+
"timestamp_utc": ctx.timestamp,
57+
"system_actor": config.system_actor,
58+
"operator_id": operator_id,
59+
"record_action_type": action.record_action_type,
60+
"column_affected": action.column,
61+
"column_role": action.column_role,
62+
"rows_affected": action.row_count,
63+
"original_value_class": "not_captured" if obscuring else "preserved",
64+
"original_value_basis": action.original_basis,
65+
"change_description": action.rationale or action.description,
66+
"risk_level": action.risk,
67+
"confidence": action.confidence,
68+
"non_obscuring_guarantee": not obscuring,
69+
"retention_days": config.retention_days,
70+
}
71+
)
72+
# A MODIFY that overwrote an existing value without retaining a pre-image
73+
# is the only way the audit trail could obscure prior information.
74+
if obscuring:
75+
warnings.append(
76+
f"{action.record_action_type} on {action.column!r} did not retain a "
77+
f"pre-image ({action.action_type}); prior value not independently recorded."
78+
)
79+
80+
session_header = {
81+
"session_id": ctx.session_id,
82+
"session_start_utc": ctx.timestamp,
83+
"system_actor": config.system_actor,
84+
"operator_id": operator_id,
85+
"total_actions": len(audit_entries),
86+
"data_trust_score": ctx.trust_score,
87+
"framework": FRAMEWORK_NAME,
88+
"retention_days": config.retention_days,
89+
}
90+
91+
passed = not warnings
92+
data = {
93+
"regulation": "21 CFR §11.10(e)",
94+
"control_text": CONTROL_TEXT,
95+
"session_header": session_header,
96+
"audit_entries": audit_entries,
97+
"caveat": GENERAL_CAVEAT,
98+
}
99+
return FrameworkReport(
100+
framework_key=FRAMEWORK_KEY,
101+
framework_name=FRAMEWORK_NAME,
102+
passed=passed,
103+
warnings=warnings,
104+
errors=[],
105+
data=data,
106+
)

0 commit comments

Comments
 (0)