From cfae90514a71aa18afad12eb737ec3b5494197e1 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Mon, 3 Aug 2026 13:17:32 -0400 Subject: [PATCH 1/4] Engagement scaffold: gitignore + deploy guard --- .gitignore | 8 ++++++++ scripts/engagement_guard.py | 25 +++++++++++++++++++++++++ scripts/git-hooks/pre-push | 24 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 scripts/engagement_guard.py create mode 100755 scripts/git-hooks/pre-push diff --git a/.gitignore b/.gitignore index e6b130a..74e4b08 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,11 @@ secrets.* # UI-review tool output (regenerated each run) screenshots/ + +# --- 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/scripts/engagement_guard.py b/scripts/engagement_guard.py new file mode 100644 index 0000000..ec77677 --- /dev/null +++ b/scripts/engagement_guard.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Lailara engagement deploy guard (Python, stdlib-only). + +Exit 2 if an ACTIVE (non-demo) client engagement.yml is present in the current +directory. No-op otherwise, so demo builds and clean CI checkouts are unaffected. +Self-contained (no dependency on the installed lailara_engagement package) so it can +run in any repo's deploy/build environment. +""" +import os +import re +import sys + +for _f in ("engagement.yml", "engagement.yaml"): + if os.path.isfile(_f): + with open(_f, encoding="utf-8-sig") as _fh: + _txt = _fh.read() + if re.search(r"^\s*demo:\s*true\s*$", _txt, re.M): + continue # demo config -> safe + sys.stderr.write( + f"ENGAGEMENT GUARD: active client engagement config present ({_f}). " + "Client mode is runtime-only and must never deploy. Deactivate it " + "(set 'demo: true', or use engagement.demo.yml) before deploying.\n" + ) + raise SystemExit(2) +raise SystemExit(0) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 0000000..9be9de9 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,24 @@ +#!/bin/sh +# Lailara engagement deploy guard (git pre-push hook). +# +# Refuses to push while an ACTIVE (non-demo) client engagement.yml is present in +# the working tree. Every tool repo auto-deploys on push, so blocking the push +# blocks the deploy — client mode is runtime-only and must never ship. +# +# No-op when no engagement.yml exists (demo builds and clean CI checkouts push +# normally), so demo behavior is unchanged. +# +# Activated per repo with: git config core.hooksPath scripts/git-hooks +set -e +for f in engagement.yml engagement.yaml; do + if [ -f "$f" ]; then + if grep -Eq '^[[:space:]]*demo:[[:space:]]*true[[:space:]]*$' "$f"; then + continue # demo config -> safe + fi + echo "ENGAGEMENT GUARD: active client engagement config present ($f)." >&2 + echo "Client mode is runtime-only and must never deploy. Deactivate it" >&2 + echo "(set 'demo: true', or remove/rename to engagement.demo.yml) before pushing." >&2 + exit 2 + fi +done +exit 0 From 557a9a28ae6f22571648a6eaebe2bbe56c449854 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Wed, 5 Aug 2026 16:54:32 -0400 Subject: [PATCH 2/4] fix: CSV loader keeps date-like cells as strings so mixed formats surface (P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RE-AUDIT P1: the CSV loader coerced date-like strings to datetime on load, erasing the format before analyze_mixed_dates could see it — so a column mixing '2026-01-01' and '01/02/2026' was silently reconciled and never flagged, the exact silent coercion datascope exists to catch. Reproduced (0 findings on a mixed-format CSV), fixed (date-like CSV cells stay strings), regression-locked (test_dates_stay_strings_so_mixed_formats_can_surface). Excel date cells arrive typed from openpyxl and are unaffected. Also: bump 2.3.2 -> 2.4.0 (staged, not published); README missing-value threshold 40% -> 10% to match _DEFAULT_THRESHOLD_PCT; CHANGELOG entry. The published CLI still runs (now reports the mixed-date finding). Full suite 364 passed. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- datascope/__init__.py | 2 +- datascope/loaders/csv_loader.py | 34 +++++++++++---------------------- pyproject.toml | 2 +- tests/test_loaders.py | 19 +++++++++++++----- 6 files changed, 36 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc280b..1e45188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to datascope are documented here. +## [2.4.0] — 2026-08-05 + +### Fixed +- **Mixed date formats are now detected in CSV files.** The CSV loader coerced date-like strings to `datetime` on load, erasing the raw format before the mixed-date analyzer could see it — so a column mixing `2026-01-01` and `01/02/2026` was silently reconciled and never flagged, the exact silent coercion datascope exists to surface. Date-like CSV cells are now kept as strings (a CSV has no type metadata; a date is text), so `analyze_mixed_dates` sees the raw formats and reports the inconsistency. Excel date cells, which arrive already typed from openpyxl, are unaffected. + +### Docs +- README missing-value threshold now reads 10% to match the code default (`_DEFAULT_THRESHOLD_PCT`), correcting a stale "40%". + ## [2.3.2] — 2026-07-27 ### Fixed diff --git a/README.md b/README.md index bb6b8fc..41320e3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ datascope finds these problems, explains what's wrong in plain English, and tell |---|---|---| | **Mixed types** | 485 numbers + 15 strings in a "numeric" column | Critical | | **Sentinel values** | "N/A", "TBD", "pending" hiding in numeric data | Critical | -| **Missing values** | 40% of a column is blank — aggregations silently exclude those rows | Warning | +| **Missing values** | 10% of a column is blank — aggregations silently exclude those rows | Warning | | **Leading-zero inconsistency** | "00123" alongside "456" — keys that won't match | Warning | | **Mixed date formats** | "01/15/2026" and "2026-01-15" in the same column | Warning | | **Suspected duplicate IDs** | 98% unique in an ID column — the other 2% will fan out joins | Warning | diff --git a/datascope/__init__.py b/datascope/__init__.py index 744e11b..78f7ade 100644 --- a/datascope/__init__.py +++ b/datascope/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "2.3.2" +__version__ = "2.4.0" diff --git a/datascope/loaders/csv_loader.py b/datascope/loaders/csv_loader.py index a021394..d6db8d0 100644 --- a/datascope/loaders/csv_loader.py +++ b/datascope/loaders/csv_loader.py @@ -12,12 +12,10 @@ from __future__ import annotations import csv -from datetime import datetime from pathlib import Path import pandas as pd -from datascope.analyzers.format_check import DATE_LIKE_RE from datascope.loaders.base import dedupe_headers from datascope.models import LoaderResult @@ -30,17 +28,6 @@ # it must stay a string rather than becoming ``inf``/``NaN``. _NON_FINITE_STRS = frozenset({"inf", "infinity", "nan"}) -# Date/time formats tried in order (most specific first). -_DATETIME_FMTS = ( - "%Y-%m-%dT%H:%M:%S", # ISO 8601 - "%Y-%m-%d %H:%M:%S", # space-separated - "%Y-%m-%d", # date only - "%m/%d/%Y %H:%M:%S", - "%m/%d/%Y", - "%d/%m/%Y", - "%Y/%m/%d", -) - def _infer_cell(raw: str) -> object: """Infer a single cell's Python value from its raw CSV string. @@ -50,8 +37,14 @@ def _infer_cell(raw: str) -> object: 2. Integer 3. Float 4. Boolean (true/false/yes/no, case-insensitive) - 5. Datetime (common formats) - 6. String fallback + 5. String fallback + + Date-like cells are DELIBERATELY left as strings. A CSV has no type + metadata, so a date is text; coercing it to ``datetime`` here would erase the + very format evidence the mixed-date analyzer needs, silently hiding a + mixed-format column ("2026-01-01" vs "01/02/2026") — exactly the silent + coercion datascope exists to surface. (Excel dates arrive already typed from + openpyxl, so the Excel loader keeps its datetime cells.) """ stripped = raw.strip() if not stripped: @@ -92,15 +85,10 @@ def _infer_cell(raw: str) -> object: if lower in _BOOL_FALSE: return False - # --- datetime ----------------------------------------------------- - if DATE_LIKE_RE.match(stripped): - for fmt in _DATETIME_FMTS: - try: - return datetime.strptime(stripped, fmt) - except ValueError: - continue - # --- string fallback ---------------------------------------------- + # Date-like strings intentionally fall through to here (see docstring): + # kept as text so analyze_mixed_dates can see the raw format and flag a + # mixed-format column instead of it being silently coerced away. return stripped diff --git a/pyproject.toml b/pyproject.toml index b1d158a..866f450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "datascope-dq" -version = "2.3.2" +version = "2.4.0" description = "Data quality diagnostics for tabular datasets — surfaces hidden problems in plain English" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_loaders.py b/tests/test_loaders.py index 7681494..0f575c3 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -3,7 +3,6 @@ from __future__ import annotations import textwrap -from datetime import datetime from pathlib import Path import pytest @@ -178,15 +177,25 @@ def test_boolean_inference(self, tmp_path): vals = list(result.dataframe["flag"]) assert vals == [True, False, True, False] - def test_datetime_inference_iso(self, tmp_path): + def test_dates_stay_strings_so_mixed_formats_can_surface(self, tmp_path): + # P1 (RE-AUDIT): the CSV loader must NOT coerce date-like strings to + # datetime — that erased the format evidence and hid mixed-format columns + # (the silent coercion datascope exists to catch). Date-like CSV cells + # stay as strings; the mixed-date analyzer then sees the raw formats. + from datascope.analyzers.format_check import analyze_mixed_dates + csv_path = _write_csv(tmp_path, """\ ts 2024-01-15 - 2024-06-30T12:00:00 + 01/15/2024 + 2024/06/30 """) result = load_csv(csv_path) - assert all(t is datetime for t in result.cell_types["ts"]) - assert result.dataframe.at[0, "ts"] == datetime(2024, 1, 15) + assert all(t is str for t in result.cell_types["ts"]) + assert result.dataframe.at[0, "ts"] == "2024-01-15" # not coerced + findings = analyze_mixed_dates(result) + assert len(findings) == 1 # mixed formats now fire on CSV + assert set(findings[0].evidence["formats_found"]) == {"%Y-%m-%d", "%m/%d/%Y", "%Y/%m/%d"} def test_bom_handled_transparently(self, tmp_path): """UTF-8 BOM should not corrupt the first header.""" From 9120414fb970ca13f255bdf7a57c86f55658b47d Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Wed, 5 Aug 2026 20:59:59 -0400 Subject: [PATCH 3/4] ci: add server-side engagement guard to deploy workflow(s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt 6 step 0.e. core.hooksPath (the local pre-push guard) is inert on a fresh clone, so a force-added active engagement.yml could otherwise reach a deploy. Add an 'Engagement guard' step (python3 scripts/engagement_guard.py — python3 is preinstalled on ubuntu-latest) right after checkout in the deploy path, so the guard runs server-side regardless of local git config. No-op for demo/clean checkouts (engagement.yml is gitignored). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 092c452..a828c32 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,9 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Engagement guard + run: python3 scripts/engagement_guard.py + - name: Set up Python uses: actions/setup-python@v6 with: From c24210fbe301145417d16d85617074bee78bc14a Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Wed, 5 Aug 2026 21:29:30 -0400 Subject: [PATCH 4/4] Align README example output to shipped sample; regenerate samples at v2.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README "Example Output" block was stale: it claimed 200 rows x 6 columns and 4 findings (2 Critical / 1 Warning / 1 Info) with a nonexistent `status` finding. The shipped sample_mixed_types.xlsx is 200 x 4 and the current CLI reports 2 findings, both Critical on revenue_mixed. Updated the block to the verbatim current run. Also regenerated the committed sample artifacts (HTML/PDF for both samples, annotated Excel for mixed_types) which were still emitted at v2.3.2 — refreshed to v2.4.0 (version string, print-CSS palette tokens, sales health-assessment copy). Finding counts unchanged (mixed 2, sales 5). Docs/artifacts only; no engine or version changes. Co-Authored-By: Claude Opus 4.8 --- README.md | 12 +++++------- .../output/sample_mixed_types_annotated.xlsx | Bin 11286 -> 11289 bytes .../output/sample_mixed_types_diagnostic.html | 12 ++++++------ .../output/sample_mixed_types_diagnostic.pdf | Bin 34558 -> 34561 bytes samples/output/sample_sales_diagnostic.html | 14 +++++++------- samples/output/sample_sales_diagnostic.pdf | Bin 36733 -> 36781 bytes 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 41320e3..d4a9ab2 100644 --- a/README.md +++ b/README.md @@ -103,16 +103,14 @@ datascope huge_file.csv --max-rows 100000 ``` datascope: Analyzing sample_mixed_types.xlsx... - 200 rows x 6 columns + 200 rows x 4 columns -Found 4 findings: - 2 Critical ######## - 1 Warning #### - 1 Info #### +Found 2 findings: + 2 Critical ######## Top critical findings: - * revenue_mixed: 15 non-numeric values hiding in an otherwise numeric column - * status: Sentinel values 'N/A' and 'TBD' in numeric data + * revenue_mixed: However, 15 str values were found among 200 non-null values (the majority type covers 92.5%). Examples of unexpected values: 'N/A', 'N/A', 'N/A', and 2 more. + * revenue_mixed: However, 7.5% of values (1 distinct sentinel string) are placeholder text rather than real data: 'N/A' (15 times). Report saved: reports/sample_mixed_types_diagnostic.pdf ``` diff --git a/samples/output/sample_mixed_types_annotated.xlsx b/samples/output/sample_mixed_types_annotated.xlsx index 3acef1a197ee51f279be1590ec1635ba33834d29..c3b2294289c770455a057906d5bbf6758e014fad 100644 GIT binary patch delta 3975 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 bl0ve7lK!c%fB%W2kwdJ?o@0oS{`K;IYZ+uu diff --git a/samples/output/sample_mixed_types_diagnostic.html b/samples/output/sample_mixed_types_diagnostic.html index 7543e35..44e8f3c 100644 --- a/samples/output/sample_mixed_types_diagnostic.html +++ b/samples/output/sample_mixed_types_diagnostic.html @@ -3,8 +3,8 @@ - - + + datascope diagnostic — sample_mixed_types.xlsx