diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3f865bb..876899c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,9 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Engagement guard + run: python3 scripts/engagement_guard.py + - uses: actions/setup-python@v6 with: python-version: "3.12" diff --git a/.gitignore b/.gitignore index 6e114d8..7375ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,11 @@ credentials.* .env.* *.pem secrets.* + +# --- lailara engagement scaffold --- +# Client engagement data is runtime-only: never commit it, never deploy it. +client-data/ +client-output/ +/engagement.yml +/engagement.yaml +# (engagement.demo.yml and engagement.example.yml stay committable) diff --git a/CHANGELOG.md b/CHANGELOG.md index d607c3f..98ccb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- README "What It Detects" now states **9** date patterns (was 6), matching + `DATE_PATTERNS` in `detection.py` (7 phone / 6 currency were already correct). + +### Changed +- Regenerated the committed `samples/output/` reports from current source so the + shipped showcase artifacts reflect the 1.3.0 tool. + +## [1.3.0] - 2026-08-05 + +### Added +- `AuditResult.severity_counts` — a High/Medium/Low breakdown that counts **every** + issue type (findings, duplicates, fuzzy duplicates, schema violations), so it + reconciles to `total_issues`. The findings-only `high_issues` / `medium_issues` + / `low_issues` properties undercounted, because duplicates and schema violations + carry a severity but are not findings — a severity breakdown drawn from them did + not foot to the headline total. `severity_counts` is the reconciling view. + ## [1.2.1] - 2026-07-28 ### Fixed diff --git a/README.md b/README.md index fe0e91e..b3d326b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A single run produces three reports tailored to three audiences: an **HTML repor ## What It Detects -**Mixed Formats** — Identifies dates, phone numbers, and currency values stored in inconsistent formats within the same column. For example, `2023-01-15` alongside `Jan 15, 2023` and `01/15/2023` in one date field. The auditor recognizes 6 date patterns, 7 phone patterns, and 6 currency patterns. +**Mixed Formats** — Identifies dates, phone numbers, and currency values stored in inconsistent formats within the same column. For example, `2023-01-15` alongside `Jan 15, 2023` and `01/15/2023` in one date field. The auditor recognizes 9 date patterns, 7 phone patterns, and 6 currency patterns. **Misused Fields** — Flags data stored in the wrong column: reference codes in name fields, free text in currency columns, invalid email addresses, and mixed boolean representations (`Y/N` vs `1/0` vs `Active/Inactive` in the same field). diff --git a/data_hygiene_auditor/api.py b/data_hygiene_auditor/api.py index ae62ec9..c6bcd7d 100644 --- a/data_hygiene_auditor/api.py +++ b/data_hygiene_auditor/api.py @@ -192,6 +192,29 @@ def medium_issues(self) -> List[Finding]: def low_issues(self) -> List[Finding]: return [f for f in self.findings if f.is_low] + @property + def severity_counts(self) -> Dict[str, int]: + """Issue counts by severity across EVERY issue type — findings, + duplicates, fuzzy duplicates, and schema violations — so the breakdown + foots to ``total_issues``. + + ``high_issues`` / ``medium_issues`` / ``low_issues`` are the *findings* + view (Finding objects only); duplicates and schema violations carry a + severity too, so a High/Medium/Low breakdown drawn only from findings + undercounts and does not reconcile to the headline total. This property + is the reconciling breakdown: ``sum(severity_counts.values()) == + total_issues``. + """ + counts = {"High": 0, "Medium": 0, "Low": 0} + for s in self.sheets: + for collection in (s.findings, s.duplicates, s.fuzzy_duplicates, + s.schema_violations): + for issue in collection: + sev = getattr(issue, "severity", None) + if sev in counts: + counts[sev] += 1 + return counts + def to_dict(self) -> Dict[str, Any]: """Return the raw audit results dict.""" return self._raw diff --git a/data_hygiene_auditor/reporting/html.py b/data_hygiene_auditor/reporting/html.py index 7e0845b..c514854 100644 --- a/data_hygiene_auditor/reporting/html.py +++ b/data_hygiene_auditor/reporting/html.py @@ -87,6 +87,15 @@ def _render_fix(fix: dict[str, str]) -> str: ) +# Static one-liner lifted out of the score-hero f-string so no source line exceeds +# the 120-char lint limit; implicit concatenation reproduces the exact same markup. +_SCORE_SCALE_HTML = ( + '
sample_messy_data.xlsx — 2026-07-27 16:58:34
+sample_messy_data.xlsx — 2026-08-05 20:45:07
df["_LastName_review"] = (
df["LastName"] == "Doe"
)df["_Email_review"] = (
df["Email"] == "test@test.com"
)df["_Phone_review"] = (
df["Phone"] == "555-555-5555"
)df["_JoinDate_review"] = (
df["JoinDate"] == "2023-01-15"
)df["_AccountBalance_review"] = (
df["AccountBalance"] == "$1,250.00"
)df["ShipDate"] = df["ShipDate"].fillna(df["ShipDate"].mode()[0])
| Format | Count |
|---|---|
| YYYY-MM-DD | 5 |
| MM/DD/YYYY | 1 |
| Mon DD, YYYY | 1 |
| M/D/YYYY | 1 |
df["ShipDate"] = pd.to_datetime(
df["ShipDate"], format="mixed", dayfirst=False
).dt.strftime("%Y-%m-%d")df = df.drop_duplicates(keep="first").reset_index(drop=True)