cleanframe-engine 0.3.1__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/.gitignore +3 -0
- cleanframe_engine-0.5.0/CHANGELOG.md +506 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/CONTRIBUTING.md +45 -3
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/PKG-INFO +153 -31
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/README.md +139 -28
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/SECURITY.md +13 -6
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/__init__.py +8 -0
- cleanframe_engine-0.5.0/cleanframe/_numparse.py +233 -0
- cleanframe_engine-0.5.0/cleanframe/_textparse.py +110 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/_util.py +78 -3
- cleanframe_engine-0.5.0/cleanframe/_version.py +1 -0
- cleanframe_engine-0.5.0/cleanframe/agent_tools.py +1335 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/api.py +169 -27
- cleanframe_engine-0.5.0/cleanframe/checks.py +127 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/cli.py +375 -23
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/codegen.py +226 -125
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/dataio.py +291 -6
- cleanframe_engine-0.5.0/cleanframe/decisions.py +1465 -0
- cleanframe_engine-0.5.0/cleanframe/demo.py +115 -0
- cleanframe_engine-0.5.0/cleanframe/demo_data/next_month_customers.csv +5 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/categories.py +161 -34
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/currency.py +69 -14
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/dates.py +7 -1
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/dedup.py +11 -1
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/text.py +41 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/diff.py +27 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/drift.py +56 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/executor.py +32 -5
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/llm.py +86 -24
- cleanframe_engine-0.5.0/cleanframe/mcp_server.py +193 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/ops.py +385 -158
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/planner.py +1 -0
- cleanframe_engine-0.5.0/cleanframe/plugins.py +87 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/quality.py +10 -3
- cleanframe_engine-0.5.0/cleanframe/readfix.py +310 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/recipe.py +30 -4
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/result.py +8 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/streaming.py +253 -60
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/types.py +6 -4
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/validate.py +9 -85
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/workbook.py +37 -2
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/README.md +7 -1
- cleanframe_engine-0.5.0/docs/agents.md +230 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/api-reference.md +46 -12
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/architecture.md +10 -3
- cleanframe_engine-0.5.0/docs/benchmark.md +58 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/cli.md +68 -3
- cleanframe_engine-0.5.0/docs/decisions.md +183 -0
- cleanframe_engine-0.5.0/docs/detectors-and-ops.md +111 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/faq.md +3 -1
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/getting-started.md +13 -1
- cleanframe_engine-0.5.0/docs/github-action.md +194 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/installation.md +26 -5
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/llm.md +27 -11
- cleanframe_engine-0.5.0/docs/plugins.md +119 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/production.md +73 -24
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/recipe-spec.md +43 -18
- cleanframe_engine-0.5.0/examples/customer.schema.yaml +31 -0
- cleanframe_engine-0.5.0/examples/github-action/data-ci.yml +57 -0
- cleanframe_engine-0.5.0/examples/messy_customers.csv +7 -0
- cleanframe_engine-0.5.0/examples/next_month_customers.csv +5 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/pyproject.toml +24 -2
- cleanframe_engine-0.5.0/tests/test_agent_tools.py +539 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_audit_hardening.py +51 -7
- cleanframe_engine-0.5.0/tests/test_bench.py +280 -0
- cleanframe_engine-0.5.0/tests/test_cli_json_logging.py +63 -0
- cleanframe_engine-0.5.0/tests/test_decisions.py +534 -0
- cleanframe_engine-0.5.0/tests/test_demo.py +54 -0
- cleanframe_engine-0.5.0/tests/test_docs_in_sync.py +49 -0
- cleanframe_engine-0.5.0/tests/test_fix_llm_trust.py +149 -0
- cleanframe_engine-0.5.0/tests/test_fix_numbers_and_parity.py +430 -0
- cleanframe_engine-0.5.0/tests/test_fix_reading.py +287 -0
- cleanframe_engine-0.5.0/tests/test_fix_streaming.py +148 -0
- cleanframe_engine-0.5.0/tests/test_github_action.py +579 -0
- cleanframe_engine-0.5.0/tests/test_mcp_server.py +117 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_ops.py +14 -0
- cleanframe_engine-0.5.0/tests/test_playground.py +258 -0
- cleanframe_engine-0.5.0/tests/test_plugins.py +149 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_profile_detectors.py +61 -0
- cleanframe_engine-0.5.0/tests/test_properties.py +214 -0
- cleanframe_engine-0.5.0/tests/test_review_round2.py +257 -0
- cleanframe_engine-0.5.0/tests/test_unique_mapping.py +91 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave5_streaming.py +5 -2
- cleanframe_engine-0.3.1/CHANGELOG.md +0 -298
- cleanframe_engine-0.3.1/cleanframe/_version.py +0 -1
- cleanframe_engine-0.3.1/cleanframe/readfix.py +0 -160
- cleanframe_engine-0.3.1/docs/detectors-and-ops.md +0 -76
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/LICENSE +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/__main__.py +0 -0
- {cleanframe_engine-0.3.1/examples → cleanframe_engine-0.5.0/cleanframe/demo_data}/customer.schema.yaml +0 -0
- {cleanframe_engine-0.3.1/examples → cleanframe_engine-0.5.0/cleanframe/demo_data}/messy_customers.csv +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/__init__.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/base.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/contacts.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/nulls.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/outliers.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/schema_mapping.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/detectors/units.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/errors.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/fingerprint.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/issues.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/profile.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/py.typed +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/report.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/cleanframe/schema.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/concepts.md +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/docs/schema-spec.md +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/examples/customer.recipe.yaml +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/conftest.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_api_report_cli.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_cross_platform.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_drift_codegen.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_executor_diff.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_llm_schema.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_planner.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_production_safety.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_recipe.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_release_hardening.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_validate.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave1_codegen.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave1_correctness.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave1_hardening.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave1_lineage.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave1_llm.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave2_scaling.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave2_vectorization.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave3_selection.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave3_workbook.py +0 -0
- {cleanframe_engine-0.3.1 → cleanframe_engine-0.5.0}/tests/test_wave4_readfix.py +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to CleanFrame are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.0] — 2026-10-18
|
|
11
|
+
|
|
12
|
+
Decision models, agents, a playground and data CI. CleanFrame can now ask a typed decision model
|
|
13
|
+
(TypeSafe's Jev, an open "System One" model, or any LLM through an adapter) the judgment calls rules
|
|
14
|
+
leave open, and freeze the confident answers into the recipe. Replay still makes zero AI calls. This
|
|
15
|
+
release also fixes two pandas 3 bugs and a rules merge that could fold one real value into another.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Decision models: `clean(decide=...)` / `cleanframe clean --decide SPEC`** (`cleanframe/decisions.py`).
|
|
20
|
+
CleanFrame asks four kinds of typed questions: is `BLR` another name for `Bangalore` (choice); is a typo
|
|
21
|
+
merge the rules made really the same category (yes/no, and a "no" **vetoes** the merge); does an ambiguous
|
|
22
|
+
token such as `none`/`-`/`unknown` mean missing in *this* column (yes/no); which source column holds a
|
|
23
|
+
weakly-matched schema field (choice).
|
|
24
|
+
- Backends: `typesafe/<model>` (`TYPESAFE_API_KEY`), `openrouter/typesafe/<model>` (`OPENROUTER_API_KEY`),
|
|
25
|
+
`systemone/<model>` for self-hosted Jev-compatible servers (`SYSTEMONE_BASE_URL`), `rules` (offline
|
|
26
|
+
heuristics), any LLM spec through an adapter (recorded as `calibrated: false`), or a custom object with
|
|
27
|
+
`.decide()`. The System One wire protocol is implemented with the standard library: no new dependency.
|
|
28
|
+
- Policy: an answer that changes data needs `p ≥ max(mode threshold, decision_threshold)` (0.9 by default).
|
|
29
|
+
Below that it is a recorded suggestion, never executed. Undoing a rules merge only needs `p > 0.5`.
|
|
30
|
+
Confirming every typo merge in a column lifts it to 0.9 confidence so it survives `strict`.
|
|
31
|
+
- Privacy follows `llm_exposure`: under `metadata` only schema questions (names, types, patterns) and
|
|
32
|
+
null-token questions (tokens from CleanFrame's fixed vocabulary) are asked. Category questions need
|
|
33
|
+
`sample` and are only asked about columns whose values repeat.
|
|
34
|
+
- Every answer is recorded in `recipe.meta.decisions` (model, calibrated, threshold, counts, records);
|
|
35
|
+
`CleanResult.decisions` and the CLI's `--json` summary carry the same. Questions are built from sorted
|
|
36
|
+
distinct values, so row order cannot change them. `max_decisions` caps cost, 429/5xx are retried with
|
|
37
|
+
backoff, and a failing backend falls back to the rules plan with a warning (`--no-llm-fallback` raises).
|
|
38
|
+
- New flags: `--decide`, `--decision-threshold`, `--max-decisions`.
|
|
39
|
+
- **`cleanframe demo`.** A guided run on sample data that now ships inside the wheel
|
|
40
|
+
(`cleanframe/demo_data/`): profile, clean into a recipe, watch next month's drifted file **stop**, then
|
|
41
|
+
replay the drafted patch. It works right after `pip install`, with no clone and no API key. Also
|
|
42
|
+
`cleanframe.demo.demo_file()` / `run_demo()`. Before this, the README's first command needed a clone.
|
|
43
|
+
- `examples/next_month_customers.csv`: the drifted sample the demo uses.
|
|
44
|
+
- **MCP server for AI agents**: `cleanframe-mcp`, `cleanframe mcp` or `python -m cleanframe.mcp_server`
|
|
45
|
+
(extra `[mcp]`, `mcp>=2.3`). Seven tools: `profile_data`, `suggest_recipe`, `apply_recipe`,
|
|
46
|
+
`check_drift`, `explain_recipe`, `list_capabilities`, `export_code`.
|
|
47
|
+
- Errors come back structured (`ok`/`kind`) and flagged `isError`. Output is bounded JSON, with every cap reported.
|
|
48
|
+
- The input file is never written, and drift stops a replay by default.
|
|
49
|
+
- `CLEANFRAME_AGENT_HIDE_VALUES=1` replaces quoted cell values with `"<hidden>"`.
|
|
50
|
+
- The same tools are plain functions in `cleanframe.agent_tools`, with no `mcp` dependency.
|
|
51
|
+
- **Agent Skill** `skills/cleanframe/SKILL.md`, installed with `npx skills add inboxpraveen/Cleanframe`. It
|
|
52
|
+
teaches Claude Code, Codex, Cursor and other agents the recipe workflow and its hard rules, instead of
|
|
53
|
+
throwaway pandas.
|
|
54
|
+
- **GitHub Action "CleanFrame Data CI"** (`action/`, `uses: inboxpraveen/Cleanframe/action@v0.5.0`).
|
|
55
|
+
- Replays committed recipes over data files on pull requests or a schedule, and fails the check on schema
|
|
56
|
+
drift (exit 3) or validation failure (exit 4).
|
|
57
|
+
- Writes a per-file job-summary table and annotations.
|
|
58
|
+
- Outputs: `drift`, `changed-cells`, `quarantined`, `failed-files`, `exit-code`, `results-file`.
|
|
59
|
+
- Inputs reach its scripts only through environment variables, and CleanFrame runs in an isolated virtualenv.
|
|
60
|
+
- See `docs/github-action.md` and `examples/github-action/data-ci.yml`.
|
|
61
|
+
- **In-browser playground** (`playground/`, https://inboxpraveen.github.io/Cleanframe/).
|
|
62
|
+
- Drop a CSV/TSV/XLSX and get a Mess Score (100 − quality score), the detected problems, the YAML recipe, a
|
|
63
|
+
cell-level diff, the generated pandas code and both HTML reports.
|
|
64
|
+
- A shareable score card shows counts only, never names or values.
|
|
65
|
+
- CleanFrame runs client-side in Pyodide 314.0.7 in a Web Worker. A Content-Security-Policy limits the page
|
|
66
|
+
to its own site and the pinned Pyodide CDN path, so files never leave the browser.
|
|
67
|
+
- Deployed by `.github/workflows/pages.yml` from a wheel built from `main`; `python playground/dev_server.py`
|
|
68
|
+
serves it locally.
|
|
69
|
+
- **CleanBench** (`bench/`, not shipped in the wheel or sdist): a benchmark for data-cleaning decisions.
|
|
70
|
+
- It runs `decide=` backends through CleanFrame's real pipeline on 60 hand-labelled messy tables
|
|
71
|
+
(299 questions: aliases, typo merges, null tokens, schema mapping).
|
|
72
|
+
- Gold is written as equivalence classes and checked by an offline oracle pass. Every scenario lists the
|
|
73
|
+
questions it must produce.
|
|
74
|
+
- Metrics: safe-apply precision (of the changes a system makes, how many are right), coverage@0.9, macro
|
|
75
|
+
accuracy with scenario-bootstrap CIs, ECE, Brier, cells fixed vs corrupted, latency, $ per 1k decisions.
|
|
76
|
+
- `python -m bench.cleanbench {validate,run,leaderboard,export,hash}`. `export` writes standalone System One
|
|
77
|
+
requests with gold, for other leaderboards.
|
|
78
|
+
- A private held-out split is committed only as a SHA-256.
|
|
79
|
+
- The rules baseline is on `bench/LEADERBOARD.md` with its misses visible. Docs: `docs/benchmark.md`.
|
|
80
|
+
- CI: a required **pandas 3** job (pandas 3.0 shipped in January; the existing job only tried
|
|
81
|
+
pre-releases), and an **action smoke** job that runs the action on Linux, Windows and macOS runners.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **pandas 3: `dedup` with `ignore_case` silently kept case-variant duplicates.** The key builder only
|
|
86
|
+
case-folded `object` columns, and pandas 3 reads text as dtype `str`. It now folds `str`/`string`/categorical
|
|
87
|
+
columns too, in the executor and in exported code alike.
|
|
88
|
+
- **pandas 3.1: `fill_na` warns, and pandas 4 would make it fail,** when the fill value doesn't fit the
|
|
89
|
+
column: `empty` or a text value into a number or date column, `zero` into text (pandas 3's default
|
|
90
|
+
text dtype). pandas used to turn such a column into `object` silently; 3.1 deprecates that. The op and
|
|
91
|
+
exported code now cast explicitly first, so results are unchanged and nothing warns. Caught by the
|
|
92
|
+
pandas pre-release CI job on 3.1.0rc0.
|
|
93
|
+
- **A typo merge could fold one real value into another.** `Austria` (rare) was merged into `Australia`
|
|
94
|
+
in `review` and `auto` mode because the names score 0.875 similar. A typo merge now also needs the two
|
|
95
|
+
spellings to be one edit apart, so look-alikes and opposites two letters apart stay separate
|
|
96
|
+
(`Hypothyroidism`/`Hyperthyroidism`, `Prednisone`/`Prednisolone`). Real typos (`Banglore`, `Kolkatta`,
|
|
97
|
+
`Califronia`) still merge; rarer two-edit typos are left to a human or a decision model.
|
|
98
|
+
- **Categories that differ only in `+`, `-` or `#` were merged as case variants** (confidence 0.9, applied
|
|
99
|
+
in every mode): blood groups `A-` → `A+`, `O-` → `O+`; `C++`/`C#` → `C`. These marks now always count.
|
|
100
|
+
A hyphen between letters (`New-York`) is still cosmetic.
|
|
101
|
+
- **`similarity()` scored any two non-Latin strings as identical** (both reduce to an empty ASCII key), so
|
|
102
|
+
`दिल्ली` matched `मुंबई` perfectly in schema mapping and drift. Such pairs are now compared as text.
|
|
103
|
+
- Negation is detected inside phrases too (`Very dissatisfied` / `Very satisfied`).
|
|
104
|
+
- **The quality score ignored date-format and money-as-text problems.** Those issues report `unparsed: 0`
|
|
105
|
+
when every value parses, and the scorer read that as "no rows affected". So a column in three date
|
|
106
|
+
formats cost nothing. The detectors now report an explicit `affected` count, and the scorer prefers it.
|
|
107
|
+
Scores for such files drop (the bundled sample goes from 87 to 80); that is the fix, not a regression.
|
|
108
|
+
- `docs/plugins.md` showed `cleanframe --plugin X apply …`, which is a usage error; global flags go after
|
|
109
|
+
the subcommand.
|
|
110
|
+
- Two tests assumed pandas 2 string semantics and failed on pandas 3 without a library bug behind them;
|
|
111
|
+
they now accept either representation of a missing text cell.
|
|
112
|
+
|
|
113
|
+
### Changed
|
|
114
|
+
|
|
115
|
+
- `pyproject.toml` and `CITATION.cff` name the maintainer (Praveen Kumar); PyPI keywords and project
|
|
116
|
+
URLs (Playground, Funding) added.
|
|
117
|
+
- `docs/cli.md`: the `--json` summary documents `usage_error` and the `decisions` field.
|
|
118
|
+
|
|
119
|
+
## [0.4.0] — 2026-09-30
|
|
120
|
+
|
|
121
|
+
A correctness release. An empirical review (26 hard-input probes, a 1M/3M-row benchmark and a
|
|
122
|
+
docs audit that ran every example) found places where CleanFrame produced a **wrong number, a
|
|
123
|
+
wrong merge or a wrong file with no warning**. Every one is fixed, each has a regression test, and
|
|
124
|
+
the invariants that should have caught them now have property-based tests.
|
|
125
|
+
|
|
126
|
+
### Fixed — silent wrong results
|
|
127
|
+
|
|
128
|
+
- **A new number format no longer replays silently.** Replaying a `.`-decimal recipe on `€1.200,50`
|
|
129
|
+
produced `1.2005`, exit 0, no warning. Numbers are now parsed by structure (below), and `apply_recipe`
|
|
130
|
+
raises a `number_format_drift` finding (exit 3) when digit-bearing values stop fitting the recipe's
|
|
131
|
+
`decimal`/`thousands` convention (a small share is INFO, so a stray `12ab34` is not a stop).
|
|
132
|
+
- **Signs were dropped.** `-$5`, `-€3.50`, `-₹500` and `$(8)` parsed as positives. `parse_number` now finds
|
|
133
|
+
the sign wherever it is written (`-5`, `-$5`, `$-5`, `5-`, `(5)`, `$(5)`, unicode minus).
|
|
134
|
+
- **`€2,5` became 25 and `€999,99` became 99999** whenever one value in the column looked US-grouped.
|
|
135
|
+
Grouping is now validated (3-digit or Indian lakh) and the currency detector infers comma-decimal only
|
|
136
|
+
from unambiguous evidence; values that fit neither convention become a counted, warned null.
|
|
137
|
+
- **The category detector merged different numbers**, in every mode including `strict`: `-1,200`→`1,200`,
|
|
138
|
+
`12.00`→`1,200`, `inf`→`-inf`, `A1`→`A-1`. Values that differ in sign, decimal or grouping are never one category.
|
|
139
|
+
- **`normalize_unit` read `1,500 g` as 1.5 g** (1000x off). Ambiguous numbers are refused (null + warning);
|
|
140
|
+
`1,5 kg` and `0.500 kg` still parse, and `1.5e3 g` now does too.
|
|
141
|
+
- **`cast: int` rounded IDs above 2**53** through float64; integer text is now converted exactly.
|
|
142
|
+
- **The cell diff missed precision loss** (`9007199254740993` → `...992.0`) and `1` → `True`, because numpy
|
|
143
|
+
compared `int64` with `float64` after promotion. Both are now reported.
|
|
144
|
+
- **`R$`, `A$`, `C$`, `HK$`, `NZ$`, `S$` were all labelled USD**: symbols are matched longest-first, and `₺ ₫ ₱ ₪ ฿` were added.
|
|
145
|
+
- **Auto-dedup treated `1`, `True` and `1.0` as the same value**, and collapsed a one-column frame to its
|
|
146
|
+
distinct values. Booleans stay distinct from numbers, and one-column frames are reported, not deduplicated.
|
|
147
|
+
- **Offset timestamps were shifted to the UTC calendar day** (`2024-01-01T02:00+05:30` → `2023-12-31`); a
|
|
148
|
+
single-offset column now keeps its wall-clock date.
|
|
149
|
+
- **`title_case` produced `3Rd Street` and `Don'T`**, and **`normalize_phone` turned `0044…` into `+91442…` and `12345` into
|
|
150
|
+
`+9112345`**. Ordinals and apostrophes are handled; `00` is read as `+`; numbers too short to be phones never get a country code invented.
|
|
151
|
+
- **Any non-UTF-8 file was accepted as latin-1** (Shift-JIS became mojibake with exit 0). The encoding ladder
|
|
152
|
+
can now refuse: a file that is neither UTF-8, cp1252 nor plausibly a single-byte Latin text raises with the likely
|
|
153
|
+
encodings and `--encoding` advice.
|
|
154
|
+
- **`read_frame` silently truncated `a\x00b` to `a`**; it now raises like `clean`.
|
|
155
|
+
- **Generated code diverged from the executor**: `05/01/2024` read as Jan 5 instead of May 1, offset timestamps
|
|
156
|
+
crashed it, `to_na case_insensitive: false` was ignored, `normalize_values` skipped non-string cells, integer column
|
|
157
|
+
labels and missing columns raised `KeyError`. Exported pandas now **embeds the executor's own helper source**
|
|
158
|
+
for numbers, units, ints, dates and title case, and skips missing columns like the executor.
|
|
159
|
+
- **`stream_apply` leaked raw pandas errors** on a late chunk that did not fit the recorded dtypes (exit 70), and its
|
|
160
|
+
output could differ from `apply_recipe` (`2` vs `2.0`, drift missed beyond the first 200 rows). It now makes a first
|
|
161
|
+
bounded-memory pass, checks drift across every row, and writes byte-identical output or refuses with a named error.
|
|
162
|
+
Streaming also stops writing a quarantine file nobody asked for.
|
|
163
|
+
- **`suggest --update` "learned" a competing reading of dates that already parse** (`03/03/2026` → `%m/%d/%Y`) and could
|
|
164
|
+
not fix the README's own `Jan 5, 26` example. It now learns only from values the recipe cannot parse.
|
|
165
|
+
- **YAML 1.1 traps in hand-written recipes.** `yes:`, `no:`, `null:`, `010:`, `12:30:` keys were read as `True`, `False`,
|
|
166
|
+
`None`, `8`, `750` and the column was skipped with only a warning. Plain keys now stay text; `to_na: [no, null]` is refused
|
|
167
|
+
with a "quote it" hint.
|
|
168
|
+
- **`cleanframe clean` silently overwrote a hand-edited recipe.** It now refuses to replace a recipe that differs from
|
|
169
|
+
the one just planned unless `--overwrite` is given (re-running over unchanged input is still fine).
|
|
170
|
+
- `Recipe.from_yaml(r.to_yaml()) == r` was `False` for every generated recipe: equality now compares the canonical serialisation.
|
|
171
|
+
- **Money in `1.234` shape is never guessed.** With no other evidence `€1.234` could be 1234 (German) or 1.234 (Irish); the currency detector now reports `ambiguous_number_format` and proposes nothing, instead of picking one and being 1000x wrong.
|
|
172
|
+
- **Currency codes:** an explicit code beats a symbol (`$5 MXN` is pesos), `$`-family symbols glued to letters are no longer mislabelled (`AR$`, `MX$`, `NT$`, `CL$`, `CO$` added; an unknown `XY$5` is "no currency", not USD), `Rs` is INR, and more ISO codes are recognised.
|
|
173
|
+
- **`normalize_unit` recognises the spellings people write** (`lbs`, `pounds`, `kgs`, `ounces`, `inches`, `feet`, `gallons`, …).
|
|
174
|
+
- **A deliberate `to_na` was reported as "values became missing because an op could not parse them"**; it is now logged as intentional and only real parse failures warn.
|
|
175
|
+
- **A derived column could be named `None_currency`** when `extract_currency` followed an op that returned an unnamed Series; op results keep the column name.
|
|
176
|
+
- **`stream_apply` silently dropped the extra field of a row wider than the header** (pandas' chunked reader truncates it); streaming now refuses such a file, as `apply_recipe` does.
|
|
177
|
+
- `dedup` / `drop_columns` accept numeric-looking column names (`subset: [2024]`); `on_fail: null` explains the YAML trap; the Excel writer warns when it removes illegal control characters; `read_frame` on a `;`-delimited file with commas in the text says "semicolon-delimited" instead of suggesting `header_row`; a lone `-`/`+` is no longer formula-escaped; a percent or currency symbol keeps `10 %` distinct from `10` in category clustering.
|
|
178
|
+
|
|
179
|
+
### Security & trust
|
|
180
|
+
|
|
181
|
+
- **`OPENAI_API_KEY` is no longer sent to other providers.** Each provider reads only its own key variable.
|
|
182
|
+
- **LLM plans can never contain `fill_na` or `drop_columns`** (any mode); they are stripped with a warning, closing a
|
|
183
|
+
hole in the "nothing silently imputed or dropped" invariant. An LLM recipe is also dry-run on the first rows and falls back to the rules planner if it cannot run.
|
|
184
|
+
- `sample` exposure is documented as what it is (values partly redacted, no approval step) and warns which columns are sent; `llm_exposure` typos raise a clear error; `max_tokens_budget` without `llm=` warns.
|
|
185
|
+
- ReDoS-prone `replace` patterns and regex back-references to missing groups are refused **at load**.
|
|
186
|
+
|
|
187
|
+
### Added
|
|
188
|
+
|
|
189
|
+
- **Plugins that travel.** `cleanframe.plugins`: entry points (`cleanframe.plugins` group), `CLEANFRAME_PLUGINS`,
|
|
190
|
+
`--plugin MODULE`, `cf.load_plugins()`, and lazy discovery when a recipe names an unknown op.
|
|
191
|
+
`register_op(codegen=, helpers=, streamable=)` lets a custom op export as standalone pandas and stream (default-deny). See the new plugin guide.
|
|
192
|
+
- **`--json`**: one machine-readable summary object on stdout (human output to stderr) for every command, on success and failure — including usage errors (`status: usage_error`, exit 2), and `ops`/`detectors` list themselves. A workbook `apply --chunksize` is now refused instead of ignored.
|
|
193
|
+
- **`normalize_unicode` op + `unicode` detector**: NFC-normalises text and removes zero-width spaces, word joiners, BOMs and non-breaking spaces (zero-width joiners are kept). It fixes `café` typed two ways splitting a category, and invisible characters that `strip` leaves behind.
|
|
194
|
+
- **Logging**: the library logs through `logging.getLogger("cleanframe")` (silent `NullHandler`); `--verbose` enables INFO.
|
|
195
|
+
- **`header_row=` / `--header-row`** for files with title rows above the header, with a suggested value when the header looks misplaced; read-time warnings for ragged rows and `Total` footer rows; hidden Excel sheets stay hidden on write-back; warnings for uncached formulas and merged cells.
|
|
196
|
+
- **Property-based tests (Hypothesis)** for the parser, generated-code parity, determinism, recipe round-trip, "nothing silently dropped" and streaming parity; `scripts/sync_wiki.py` + a test that keeps `docs/` and `wiki/` identical.
|
|
197
|
+
- More date formats (`%b %d, %y`, `%B %d, %y`, `%d %b, %Y`, `%d.%m.%y`, `%Y.%m.%d`) — appended, so existing recipes are unaffected.
|
|
198
|
+
- A `detect` extra (`charset-normalizer`) that ranks candidate encodings in the refusal message.
|
|
199
|
+
- Unknown `options=` keys warn with a did-you-mean.
|
|
200
|
+
|
|
201
|
+
### Performance
|
|
202
|
+
|
|
203
|
+
- Pure text ops (`parse_number`, `normalize_unit`, `normalize_phone`, `normalize_values`, `extract_currency`, explicit-format `parse_date`, and the email/url/phone/regex validators) now run **once per distinct value** of an all-string column and fan the results out — identical output, 15-25x faster on repetitive columns (1M rows: `parse_number` 4.5 s → 0.17 s, `normalize_unit` 3.8 s → 0.26 s). Measured end to end on a realistic 18-column, 1M-row file: `clean()` 42 s → 29 s, replay 28 s → 16 s.
|
|
204
|
+
- Peak memory is unchanged and is now documented honestly: the frame plus the op-touched columns, about 2x when most columns are cleaned. Use `stream_apply` for row-independent recipes on files that do not fit.
|
|
205
|
+
|
|
206
|
+
### Changed
|
|
207
|
+
|
|
208
|
+
- **Behaviour changes worth knowing when upgrading:**
|
|
209
|
+
- Numbers with invalid or ambiguous grouping now become a warned null instead of a guess (`2,5` under `thousands=","`, `1,500 g`).
|
|
210
|
+
- Streaming reads the file twice, and writes a quarantine file only when `quarantine_path` is given.
|
|
211
|
+
- Offset timestamps keep their wall-clock date; mixed-offset columns are converted to UTC.
|
|
212
|
+
- `title_case` no longer capitalises after apostrophes or digits; `normalize_phone` treats `00` as `+`.
|
|
213
|
+
- `to_na` tokens, `replace` pattern/repl and `parse_number` separators must be text; `version: true` and unknown option keys are rejected.
|
|
214
|
+
- `Recipe` defines `__eq__` on its canonical form (it was already unhashable).
|
|
215
|
+
- Streaming `apply` measured at ~270 MB peak for 600k rows at `chunksize=50_000` (docs updated with the baseline).
|
|
216
|
+
- The validation check registry moved into `cleanframe.checks` (internal; removes the last import cycle).
|
|
217
|
+
|
|
218
|
+
## [0.3.1] — 2026-09-04
|
|
219
|
+
|
|
220
|
+
First follow-up to the 0.3.0 release, from the CI and code-scanning results that
|
|
221
|
+
only appear once a project is public.
|
|
222
|
+
|
|
223
|
+
### Fixed
|
|
224
|
+
|
|
225
|
+
- **`Severity` comparisons were alphabetical, not ordered.** Only `__lt__` was
|
|
226
|
+
defined, so the `str` mixin answered everything else: `Severity.ERROR >
|
|
227
|
+
Severity.INFO` returned `False` because `"error" < "info"` as text. All four
|
|
228
|
+
comparisons are now defined on the severity rank. Library code always used
|
|
229
|
+
`.rank` explicitly, so nothing internal was affected, but any caller comparing
|
|
230
|
+
severities directly got wrong answers. Found by CodeQL's incomplete-ordering
|
|
231
|
+
query.
|
|
232
|
+
- **`mypy` failed in CI** with `numpy/__init__.pyi:737: error: Type statement is
|
|
233
|
+
only supported in Python 3.12 and greater`. The `python_version = "3.10"` pin
|
|
234
|
+
also governs how dependency stubs are parsed, and numpy 2.5's stubs use syntax
|
|
235
|
+
that needs 3.12. Removed the pin; minimum-version support is still checked by
|
|
236
|
+
ruff's `target-version` and by the Python 3.10 test job.
|
|
237
|
+
- **The wiki sync workflow could not push.** GitHub Actions' built-in token can
|
|
238
|
+
clone a wiki but not write to it, so the job committed and then failed on
|
|
239
|
+
authentication. It now requires the `WIKI_TOKEN` secret and stops up front with
|
|
240
|
+
an explanatory message. `wiki/README.md` claimed no token was needed; corrected.
|
|
241
|
+
- Two module-level import cycles removed: `profile` with `ops`, and `recipe` with
|
|
242
|
+
`validate`. `COMMON_DATE_FORMATS` now lives in `ops`, which owns date parsing,
|
|
243
|
+
and is re-exported from `profile` so every existing import keeps working.
|
|
244
|
+
`validate` imports `ValidationRule` for annotations only.
|
|
245
|
+
- Empty exception handlers in `fingerprint` and `report` replaced with a helper
|
|
246
|
+
that returns whether a cell is missing, so the intent is in the code rather
|
|
247
|
+
than in a comment beside `pass`.
|
|
248
|
+
- Redundant function-level `import re` in the dates detector removed; it now
|
|
249
|
+
reuses the module's compiled pattern instead of recompiling per call.
|
|
250
|
+
|
|
251
|
+
### Changed
|
|
252
|
+
|
|
253
|
+
- Workflow actions updated: checkout to v7, setup-python to v7, upload-artifact
|
|
254
|
+
to v7, download-artifact to v8, codeql-action to v4. Clears the Node 20
|
|
255
|
+
deprecation warnings.
|
|
256
|
+
- Dependabot groups the `github-actions` ecosystem, so action bumps arrive as one
|
|
257
|
+
pull request instead of one per action.
|
|
258
|
+
- CodeQL analyses the library and skips `tests`. Test fixtures are deliberately
|
|
259
|
+
hostile (catastrophic-backtracking patterns, injection payloads, handlers that
|
|
260
|
+
assert something must not raise) and are not part of the wheel, so reporting
|
|
261
|
+
them buries real findings. Configuration lives in
|
|
262
|
+
`.github/codeql/codeql-config.yml`.
|
|
263
|
+
- Protocol methods on `LLMClient` and `Planner` carry a docstring instead of a
|
|
264
|
+
bare `...`.
|
|
265
|
+
- Installation docs link the package page, show how to pin a version, and mention
|
|
266
|
+
`cleanframe --version`. `CHANGELOG.md` gained the Keep a Changelog link
|
|
267
|
+
definitions now that releases are tagged.
|
|
268
|
+
|
|
269
|
+
### Verified
|
|
270
|
+
|
|
271
|
+
335 tests pass on Python 3.10 with the declared minimum pins (pandas 1.5.3,
|
|
272
|
+
numpy 1.23.5), on Python 3.13 with pandas 2.3.3, and on Python 3.13 with pandas
|
|
273
|
+
3.0.5 and numpy 2.5.2, which is what a fresh `pip install` resolves today.
|
|
274
|
+
|
|
275
|
+
## [0.3.0] — 2026-09-04
|
|
276
|
+
|
|
277
|
+
Release-readiness pass driven by a full audit of the CLI, the Python API, packaging and
|
|
278
|
+
the docs. Several fixes change behaviour on purpose: cases that used to pass silently
|
|
279
|
+
now fail loudly, and a few transforms that quietly corrupted data no longer run.
|
|
280
|
+
|
|
281
|
+
### Security
|
|
282
|
+
|
|
283
|
+
- **Generated code no longer trusts column names.** `generate_code` interpolated the
|
|
284
|
+
source column into a comment and the validation label into a `raise` without escaping,
|
|
285
|
+
so a CSV header containing a newline produced an exported `clean(df)` that executed
|
|
286
|
+
arbitrary statements. Both are now sanitised.
|
|
287
|
+
- **CSV/Excel exports sanitise header labels**, not just cells: a column literally named
|
|
288
|
+
`=CMD()` was written as a live formula.
|
|
289
|
+
- The formula sanitiser no longer escapes plain signed numbers, so a normalised phone
|
|
290
|
+
number (`+919876543210`) and a negative amount (`-1.5`) survive export unchanged.
|
|
291
|
+
- Recipes and schemas reject **duplicate YAML keys** instead of silently keeping the last.
|
|
292
|
+
|
|
293
|
+
### Fixed — silent data corruption
|
|
294
|
+
|
|
295
|
+
- `normalize_phone` on a numeric column (one blank cell makes pandas read the column as
|
|
296
|
+
float) appended a spurious trailing digit; `9876543210.0` became `98765432100`.
|
|
297
|
+
Trailing extensions (`ext 12`) are no longer fused into the number either.
|
|
298
|
+
- ISO timestamps and dashed identifiers were classified as phone numbers and stripped to
|
|
299
|
+
digits. Phone classification now needs a phone-ish column name or an explicit `+`/`(`
|
|
300
|
+
dial prefix, and clock times and ISO dates are never phone-like.
|
|
301
|
+
- European decimals were parsed with the US convention: `€1.200,50` became `1.2005` while
|
|
302
|
+
the issue reported nothing unparseable. The currency detector now infers the grouping.
|
|
303
|
+
- A fuzzy category merge folded `Unapproved` into `Approved` (and `Unverified` into
|
|
304
|
+
`Verified`). A spelling that is another plus a negation prefix is never merged.
|
|
305
|
+
- A ragged CSV row (one field too many, typically a trailing delimiter) shifted every
|
|
306
|
+
column left because pandas promoted the first field to the index. Reads now pass
|
|
307
|
+
`index_col=False`.
|
|
308
|
+
- `in [Yes, No]` rejected every row: YAML 1.1 read the values as booleans. Membership
|
|
309
|
+
values are parsed as text.
|
|
310
|
+
- Mixed day-first and month-first dates reported `unparsed: 0` while nulling the values
|
|
311
|
+
that no longer matched after the conflicting format was dropped. The count is now taken
|
|
312
|
+
after reconciliation.
|
|
313
|
+
- Punctuation-only values (`-`, `?`) were collapsed into `""` by category clustering.
|
|
314
|
+
- `NA` in a short all-caps code column (Namibia) is treated as ambiguous, not a null.
|
|
315
|
+
- Category canonicalisation prefers short all-caps codes (`CA` over `ca`).
|
|
316
|
+
- A user column named `_cf_quarantine_reason` was overwritten in the quarantine frame.
|
|
317
|
+
- `skiprows` meant different things per format and could eat the CSV header. It now
|
|
318
|
+
counts **data rows** everywhere: an int drops that many leading records, a list names
|
|
319
|
+
1-based data rows.
|
|
320
|
+
- `profile_dataframe` no longer takes O(n²) time on long text values, so a column holding
|
|
321
|
+
a notes field or a JSON blob does not stall `clean()`. It also survives unhashable
|
|
322
|
+
cells (lists/dicts) and categorical columns.
|
|
323
|
+
- A validation rule written `values: [Yes, No]` matched nothing, because YAML 1.1 reads
|
|
324
|
+
those as booleans. Every spelling of the boolean is now matched, in the executor and in
|
|
325
|
+
the exported code alike.
|
|
326
|
+
- `round: {decimals: 2}` — the documented parameter form — failed to load; only the bare
|
|
327
|
+
`round: 2` worked.
|
|
328
|
+
- The schema dtype alias `str` was listed but not accepted.
|
|
329
|
+
- `excel_sheet_names` left the workbook file handle open, so a later write to the same
|
|
330
|
+
path could fail with a permission error on Windows.
|
|
331
|
+
|
|
332
|
+
### Fixed — errors that reached users as tracebacks
|
|
333
|
+
|
|
334
|
+
- The CLI has a catch-all: an unexpected exception prints one line plus an invitation to
|
|
335
|
+
re-run with `--debug` (or `CLEANFRAME_DEBUG=1`) instead of a traceback.
|
|
336
|
+
- Clean messages replace tracebacks for: a missing or directory input path on
|
|
337
|
+
`clean`/`report` (the format detector opened the file before the existence check); an
|
|
338
|
+
output path that is a directory, read-only, or locked; malformed recipe YAML through
|
|
339
|
+
`apply`; malformed schema YAML; a recipe or schema in a non-UTF-8 encoding; a
|
|
340
|
+
non-integer recipe `version`; a non-mapping `meta`; a malformed `source_fingerprint`; a
|
|
341
|
+
validation rule with an unsupported parameter; a bad `mode`, `on_drift` or `chunksize`;
|
|
342
|
+
a wrongly-typed `options`, `planner` or `recipe` argument.
|
|
343
|
+
- The encoding ladder ends at latin-1, which cannot fail. Byte values cp1252 leaves
|
|
344
|
+
undefined (a Shift-JIS export, for instance) no longer raise mid-detection.
|
|
345
|
+
- UTF-16/32 files are detected by byte-order mark instead of being reported as binary.
|
|
346
|
+
- Excel writes strip the control characters openpyxl refuses and check its 32,767-character
|
|
347
|
+
cell limit up front, so a failed write no longer leaves a truncated workbook behind.
|
|
348
|
+
|
|
349
|
+
### Changed
|
|
350
|
+
|
|
351
|
+
- **The PyPI distribution is now `cleanframe-engine`** (the name `cleanframe` belongs to
|
|
352
|
+
an unrelated project). The import package, the console script and every API are
|
|
353
|
+
unchanged: `pip install cleanframe-engine`, then `import cleanframe as cf`.
|
|
354
|
+
- **Exit codes are distinct:** `0` success, `1` data/recipe/output error, `2` usage,
|
|
355
|
+
`3` stopped on drift, `4` validation failed, `70` internal error, `130` interrupted.
|
|
356
|
+
Previously everything but a usage error exited `1`.
|
|
357
|
+
- **Writing output over the input file is refused** unless `overwrite=True` /
|
|
358
|
+
`--overwrite`. A single-sheet clean written back over its own workbook used to leave
|
|
359
|
+
one renamed sheet and delete the rest.
|
|
360
|
+
- Every write goes through a temporary file and is moved into place — cleaned data,
|
|
361
|
+
quarantine, streamed output, recipes, schemas, generated code and HTML reports.
|
|
362
|
+
- `stream_apply` accepts `overwrite=True` (CLI `--overwrite`), which it previously had no
|
|
363
|
+
way to express.
|
|
364
|
+
- **Unknown and misspelled op parameters are rejected at load time** with the list of
|
|
365
|
+
valid names. `parse_date: {format: ...}`, `dedup: {case_insensitive: true}` and
|
|
366
|
+
`to_na: {token: ...}` used to load and do nothing. `cast` targets, `normalize_unit`
|
|
367
|
+
units, `dedup keep` and validation check names are validated at load too.
|
|
368
|
+
- Wrongly-typed parameters are refused rather than coerced: `remove_symbols: 5` used to
|
|
369
|
+
delete every digit 5, and a bare string where a list belongs used to be read one
|
|
370
|
+
character at a time.
|
|
371
|
+
- Schema dtype aliases (`int`, `number`, `text`, `bool`, `id`) are normalised, so they now
|
|
372
|
+
drive the same casts as their canonical spellings instead of being silently ignored.
|
|
373
|
+
- Flags that were silently ignored now error: `--code`/`--report`/`--quarantine` and the
|
|
374
|
+
file-only read flags in workbook mode, `--report` and the selection flags in streaming
|
|
375
|
+
mode. `--chunksize 0` was accepted and quietly ran a whole-frame apply.
|
|
376
|
+
- `llm_exposure="none"` makes no network call at all; it plans with rules and warns.
|
|
377
|
+
Previously it sent the same metadata payload as `metadata` exposure.
|
|
378
|
+
- `on_drift` is validated: a typo used to disable the drift guard silently.
|
|
379
|
+
- Streaming pins column types from the recipe's fingerprint, so a streamed replay renders
|
|
380
|
+
numbers the same way a whole-frame replay does (`2` vs `2.0`), falling back to text with
|
|
381
|
+
a warning if the file no longer matches.
|
|
382
|
+
- `suggest` re-applies the recipe's recorded `read:` binding and accepts the selection
|
|
383
|
+
flags, so the command the drift message recommends now works on workbooks and on
|
|
384
|
+
`;`-separated files instead of reporting every column as drifted.
|
|
385
|
+
- `generate_code` raises when a recipe uses an op or check it cannot reproduce, instead of
|
|
386
|
+
emitting a silently incomplete module (`allow_partial=True` restores the old output).
|
|
387
|
+
- Every warning uses the new `CleanFrameWarning` category and prints as one line from the
|
|
388
|
+
CLI. Filter them with `warnings.simplefilter("ignore", cleanframe.CleanFrameWarning)`.
|
|
389
|
+
- `Mode.coerce` raises `CleanFrameError` rather than `ValueError`.
|
|
390
|
+
- Unsupported input extensions are refused instead of being parsed as CSV.
|
|
391
|
+
- Duplicate and blank CSV header names are refused instead of being renamed by pandas to
|
|
392
|
+
`name.1` / `Unnamed: 3`.
|
|
393
|
+
- Writing legacy `.xls` is refused (pandas emits `.xlsx` bytes under the name).
|
|
394
|
+
|
|
395
|
+
### Added
|
|
396
|
+
|
|
397
|
+
- `text=True` / `--text` reads every field verbatim, keeping leading zeros, literal `NA`
|
|
398
|
+
and `1e5` exactly as the file has them, and records the choice in the recipe. Without
|
|
399
|
+
it, `clean`/`report` compare a bounded verbatim re-read and warn naming the columns
|
|
400
|
+
pandas' type inference changed.
|
|
401
|
+
- Values an op could not parse are counted per column, logged and warned about.
|
|
402
|
+
- `python -m cleanframe` as an alias for the console script.
|
|
403
|
+
- CLI: `--debug`, `--verbose`, `--sep`, `--encoding`, `--text`, `--no-llm-fallback`,
|
|
404
|
+
`clean --out-dir`, and `-o` on every subcommand that has `--out`.
|
|
405
|
+
- `clean(..., llm_fallback=False)` makes an LLM failure raise instead of degrading.
|
|
406
|
+
- **A model-written step that cannot load no longer costs the whole plan.** It is dropped,
|
|
407
|
+
warned about, and listed in `recipe.meta["llm_dropped"]`, so the rest of the model's
|
|
408
|
+
recipe still runs. Previously one stray parameter, or a `cast` with no target, discarded
|
|
409
|
+
the entire LLM recipe and silently fell back to rules. A response that is not a recipe at
|
|
410
|
+
all still falls back. Ops a mode forbids are recorded in `recipe.meta["llm_blocked_ops"]`.
|
|
411
|
+
- Recipe loading joins an op and its single argument written as two list items
|
|
412
|
+
(`["extract_currency", "cast", "float"]`), a shape models emit regularly. Two real op
|
|
413
|
+
names in a row are never joined.
|
|
414
|
+
- `sep=` / `encoding=` on `clean`, `report`, `apply_recipe` and `infer_schema`;
|
|
415
|
+
`blank_lines=` and `text=` on `read_frame`; `source=`/`overwrite=` on `write_frame`.
|
|
416
|
+
- `OutputError` and `CleanFrameWarning` in the public API.
|
|
417
|
+
- `mypy` runs clean over the package and is part of the dev extra and CI.
|
|
418
|
+
- Release workflow (PyPI trusted publishing), Dependabot, CodeQL, issue and pull-request
|
|
419
|
+
templates, a code of conduct, and a citation file.
|
|
420
|
+
|
|
421
|
+
## [0.2.0] — 2026-07-19
|
|
422
|
+
|
|
423
|
+
### Added — production-hardening upgrade (multi-sheet, scaling, selection, format-correction, streaming)
|
|
424
|
+
|
|
425
|
+
- **Multi-sheet Excel workbooks**: `clean_workbook` / `apply_workbook` clean every tab
|
|
426
|
+
independently (one recipe + diff per sheet), collected in a `WorkbookResult` with a
|
|
427
|
+
single reviewable `WorkbookRecipe` (`sheets:` block). `read_frame` now *refuses* to
|
|
428
|
+
silently read only sheet 1 of a multi-sheet workbook — pass `sheet=` or use the
|
|
429
|
+
workbook API. Write-back preserves untouched sheets but **refuses in-place overwrite
|
|
430
|
+
of the source** (formulas/formatting are lost on pandas re-emit) unless `overwrite=True`.
|
|
431
|
+
The `cleanframe clean/apply` CLI auto-routes a multi-sheet `.xlsx`.
|
|
432
|
+
- **Selective ingestion**: `sheet` / `columns` / `nrows` / `skiprows` on
|
|
433
|
+
`read_frame`/`clean`/`report`/`apply`/`infer-schema` and the CLI, recorded in the recipe's
|
|
434
|
+
new `read:` section (recipe v2, backward-compatible) so `apply` re-reads the same slice.
|
|
435
|
+
- **Read-time format auto-correction** (`correct_format=True`, default; `--no-correct` to
|
|
436
|
+
opt out): deterministic encoding fallback (utf-8 → cp1252) and header-consensus delimiter
|
|
437
|
+
detection for CSV-family files, pinned into the recipe `read:` section for replay; refuses
|
|
438
|
+
on an ambiguous delimiter.
|
|
439
|
+
- **Out-of-core streaming replay**: `stream_apply(recipe, in, out, chunksize=)` and
|
|
440
|
+
`cleanframe apply --chunksize N` process files larger than RAM. Row-independent recipes
|
|
441
|
+
stream with byte-identical values and bounded (chunk-sized) memory; global ops
|
|
442
|
+
(`dedup`, aggregate `fill_na`, `cast` to category/datetime, format-less `parse_date`, the
|
|
443
|
+
`unique` validator) are **refused with a clear, named error**. Streaming also honours the
|
|
444
|
+
refuse-on-drift guarantee (checked on a bounded head sample).
|
|
445
|
+
- **In-RAM scaling**: ~44× faster diff extraction (bulk slicing), the executor snapshots only
|
|
446
|
+
op-touched columns (peak memory ≈ input, not 2×), a vectorised `.str` fast-path for text ops
|
|
447
|
+
(byte-identical), and deduplicated profiler signal computation.
|
|
448
|
+
|
|
449
|
+
### Fixed — correctness & robustness (from a full empirical audit)
|
|
450
|
+
|
|
451
|
+
- **Silent data loss / lineage**: an emitted derived column that overwrites an existing column
|
|
452
|
+
is now tracked in the diff (was reported as add+remove with the change lost); a value rewrite
|
|
453
|
+
on a row later dropped by dedup/validation keeps its change provenance; two ops emitting the
|
|
454
|
+
same column now raise instead of silently clobbering.
|
|
455
|
+
- **Detectors**: ambiguous DD/MM vs MM/DD dates are resolved from the data (no silent day/month
|
|
456
|
+
swap); fuzzy category clustering no longer merges two both-frequent look-alikes
|
|
457
|
+
(`insured`/`uninsured`); datetimes keep their time-of-day; disguised-null tokens that are often
|
|
458
|
+
legitimate (`none`, `-`, `unknown`) are surfaced for review, not auto-converted.
|
|
459
|
+
- **`parse_number`** rejects fused digit groups (`"12ab34"` → NaN) and handles scientific
|
|
460
|
+
notation; format-less `parse_date`/`cast(datetime)` are now deterministic (order-independent).
|
|
461
|
+
- **Crash-hardening**: non-UTF-8 CSVs, ragged/empty files, directories, mislabeled extensions,
|
|
462
|
+
malformed YAML/op params, duplicate/non-string/MultiIndex column names, and a Windows cp1252
|
|
463
|
+
console now raise a clean `CleanFrameError` / render safely instead of a raw traceback.
|
|
464
|
+
- **LLM planner**: any failure (bad JSON, malformed recipe, provider/network error) now falls
|
|
465
|
+
back to the deterministic rules planner as documented — previously some outputs crashed the
|
|
466
|
+
pipeline with an uncaught `KeyError`. Array-form ops are parsed leniently; the `METADATA`
|
|
467
|
+
exposure no longer leaks a raw cell value via a detector message.
|
|
468
|
+
- **Codegen**: generated standalone pandas now reproduces the executor exactly (currency symbols,
|
|
469
|
+
number sign/unicode-minus, NA tokens, unit aliases, `cast` bool/date, validation row-filtering,
|
|
470
|
+
and case-insensitive dedup), rendered from a single source of truth in `cleanframe.ops`.
|
|
471
|
+
- **Schema**: an unknown/typo'd dtype (`"flaot"`) is rejected instead of silently ignored.
|
|
472
|
+
|
|
473
|
+
### Added — production safety guards for large datasets and untrusted exports
|
|
474
|
+
|
|
475
|
+
- Detector scans sample at most 50,000 non-null values per column (`sample_non_null`)
|
|
476
|
+
- Cell-level diffs cap stored detail at 100,000 changes by default (`max_diff_changes`)
|
|
477
|
+
- CSV/TSV writes escape spreadsheet formula injection by default (`sanitize_csv=True`)
|
|
478
|
+
- Recipe regexes (`replace`, `matches:`) reject oversized / nested-quantifier patterns
|
|
479
|
+
- LLM HTTP clients use a 60s timeout; SAMPLE exposure no longer materialises entire columns
|
|
480
|
+
- Missing recipe columns and LLM fallbacks emit `warnings.warn` (not silent skips)
|
|
481
|
+
- Optional `parquet` extra (`pyarrow`)
|
|
482
|
+
- `cleanframe/py.typed` marker (PEP 561)
|
|
483
|
+
- Example schema + recipe under `examples/`
|
|
484
|
+
- CI workflow (pytest + ruff on Python 3.10–3.13)
|
|
485
|
+
- Full documentation under `docs/` and GitHub Wiki pages under `wiki/`
|
|
486
|
+
|
|
487
|
+
### Fixed
|
|
488
|
+
|
|
489
|
+
- Units detector threshold now compares against the sampled column size, not the full row count
|
|
490
|
+
- Cross-platform IO: UTF-8 (+ BOM-tolerant reads), LF-only text/CSV writes, auto-create parent dirs
|
|
491
|
+
- Quarantine / `save_all` CSV exports go through `write_frame` (same sanitise + encoding path)
|
|
492
|
+
- Pandas 3 compatibility: detectors recognise default ``str`` / ``string`` dtypes (not only ``object``)
|
|
493
|
+
|
|
494
|
+
## [0.1.0] — 2026-07-11
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
|
|
498
|
+
- Initial public release: profiler, detectors, rules + optional LLM planner, recipe YAML,
|
|
499
|
+
deterministic executor, validation/quarantine, cell-level diff, schema drift, HTML reports,
|
|
500
|
+
codegen, and CLI.
|
|
501
|
+
|
|
502
|
+
[Unreleased]: https://github.com/inboxpraveen/Cleanframe/compare/v0.3.1...HEAD
|
|
503
|
+
[0.3.1]: https://github.com/inboxpraveen/Cleanframe/releases/tag/v0.3.1
|
|
504
|
+
[0.3.0]: https://github.com/inboxpraveen/Cleanframe/releases/tag/v0.3.0
|
|
505
|
+
[0.2.0]: https://github.com/inboxpraveen/Cleanframe/commits/main
|
|
506
|
+
[0.1.0]: https://github.com/inboxpraveen/Cleanframe/commits/main
|
|
@@ -9,7 +9,7 @@ than no change at all. The rules below exist to protect it.
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
## The
|
|
12
|
+
## The six invariants (do not break these)
|
|
13
13
|
|
|
14
14
|
1. **Determinism.** The same input must always produce the same recipe, the same
|
|
15
15
|
cleaned frame, the same diff — on any machine, in any process. That means:
|
|
@@ -27,6 +27,17 @@ than no change at all. The rules below exist to protect it.
|
|
|
27
27
|
raw cell values by default, and its output must go through `Recipe.from_dict`
|
|
28
28
|
(which validates it) — never straight to the executor.
|
|
29
29
|
|
|
30
|
+
The same holds for **decision models** (`decisions.py`, `clean(decide=...)`): they
|
|
31
|
+
answer typed questions while planning, and accepted answers become ordinary recipe
|
|
32
|
+
ops, so replay never calls them. Under the default `metadata` exposure a question may
|
|
33
|
+
carry column names, types, value *patterns* and — the one exception — a null token,
|
|
34
|
+
because the token always comes from CleanFrame's fixed vocabulary
|
|
35
|
+
(`ops.DEFAULT_NA_TOKENS`), never from free-form data. Category values are sent only
|
|
36
|
+
under `sample`. An answer that *changes* data needs `p ≥ max(mode threshold,
|
|
37
|
+
decision_threshold)`; below that it is a recorded suggestion, never executed.
|
|
38
|
+
Questions must be built from sorted distinct values so row order cannot change them
|
|
39
|
+
(`tests/test_decisions.py` checks this).
|
|
40
|
+
|
|
30
41
|
3. **Recipes round-trip losslessly and idempotently.** `Recipe.from_yaml(r.to_yaml())`
|
|
31
42
|
must equal `r`, and applying `to_yaml` twice must be byte-identical. If you add
|
|
32
43
|
an op with parameters, you must add a `coerce`/`compact` pair such that
|
|
@@ -43,11 +54,27 @@ than no change at all. The rules below exist to protect it.
|
|
|
43
54
|
name it in the op's signature (or in `aliases=`) so the loader accepts it — a
|
|
44
55
|
parameter the loader silently ignores is a data-loss bug.
|
|
45
56
|
|
|
57
|
+
It applies to *guessing* as well: when a value is ambiguous (`1,500 g` — 1500 g or
|
|
58
|
+
1.5 g?) or malformed for the declared convention (`2,5` under `thousands=","`), the
|
|
59
|
+
result is a counted, warned null, never a plausible-looking number. Wrong numbers
|
|
60
|
+
are worse than missing ones. Likewise a clustering/merge rule must never fold together
|
|
61
|
+
values that differ in sign, decimal or grouping (`-300`/`300`), and a *typo* merge must
|
|
62
|
+
be a typo: one edit apart, so look-alike words such as `Austria`/`Australia` or
|
|
63
|
+
`Hypothyroidism`/`Hyperthyroidism` stay apart.
|
|
64
|
+
|
|
46
65
|
5. **Every changed cell is tracked.** The executor assigns a stable row id and
|
|
47
66
|
tracks column lineage so `CellDiff` can attribute every change. If you add a
|
|
48
67
|
transform that adds/removes/reorders columns or rows, make sure the lineage in
|
|
49
68
|
`executor.py` stays correct and the diff still reconciles.
|
|
50
69
|
|
|
70
|
+
6. **Exported code equals the executor — because it is the same code.** The pandas that
|
|
71
|
+
`result.code` writes must produce exactly what `apply_recipe` produces. Do not
|
|
72
|
+
re-implement an op's logic in `codegen.py`: put shared scalar logic in a small
|
|
73
|
+
self-contained module (`_numparse.py`, `_textparse.py`) or an existing `ops.py` helper
|
|
74
|
+
and *embed its source* with `inspect.getsource`, as codegen already does. Two
|
|
75
|
+
implementations drift; one implementation cannot. `tests/test_properties.py` fuzzes
|
|
76
|
+
this, and `tests/test_fix_numbers_and_parity.py` pins the known cases.
|
|
77
|
+
|
|
51
78
|
If a change would weaken one of these, open an issue to discuss it first.
|
|
52
79
|
|
|
53
80
|
---
|
|
@@ -74,7 +101,13 @@ DataFrame ─▶ profile.py ─▶ detectors/ ─▶ planner.py ─▶ Recipe (r
|
|
|
74
101
|
renames, and — critically — orders ops via `OP_ORDER`.
|
|
75
102
|
- **`executor.py`** — replays a recipe in fixed phases with lineage tracking.
|
|
76
103
|
- **`recipe.py` / `schema.py`** — the durable, human-reviewable artifacts.
|
|
104
|
+
- **`checks.py`** — the validation check registry, plus the guard that rejects an
|
|
105
|
+
unknown check name at load time. It lives apart from `validate.py` so `recipe.py`
|
|
106
|
+
can use the guard without an import cycle.
|
|
77
107
|
- **`llm.py`** — optional; writes a recipe from metadata only.
|
|
108
|
+
- **`decisions.py`** — optional; asks a decision model (Jev, an open System One model,
|
|
109
|
+
or an LLM via an adapter) the judgment calls rules leave open, and turns confident
|
|
110
|
+
answers into ordinary proposals for the planner.
|
|
78
111
|
|
|
79
112
|
The dependency direction is strictly downward (detectors import ops, planner
|
|
80
113
|
imports detectors, etc.). Don't introduce an upward import — it will create a
|
|
@@ -218,6 +251,14 @@ you.
|
|
|
218
251
|
- **Every PR keeps `pytest` green.** New behavior needs a new test.
|
|
219
252
|
- **Test the invariant, not just the happy path.** For anything order-sensitive, add
|
|
220
253
|
a determinism assertion. For a new op with params, add the round-trip.
|
|
254
|
+
- **Fuzz the invariants.** `tests/test_properties.py` uses [Hypothesis](https://hypothesis.readthedocs.io/)
|
|
255
|
+
to check parser totality/sign, generated-code == executor, determinism, recipe round-trip,
|
|
256
|
+
"nothing silently dropped" and streaming == whole-frame. CI runs a small deterministic
|
|
257
|
+
sample; to hunt for new bugs locally run
|
|
258
|
+
`CLEANFRAME_PROP_EXAMPLES=2000 CLEANFRAME_PROP_RANDOM=1 pytest tests/test_properties.py`.
|
|
259
|
+
If it finds one, add the minimal case as a normal example test as well.
|
|
260
|
+
- **New op?** Decide `codegen=` / `streamable=` deliberately (see the [plugin guide](docs/plugins.md));
|
|
261
|
+
both default to "refuse", which is safe.
|
|
221
262
|
- Prefer small, focused tests using the fixtures in `tests/conftest.py`.
|
|
222
263
|
- `ruff check cleanframe tests` and `mypy` must both pass — CI runs them.
|
|
223
264
|
|
|
@@ -227,8 +268,9 @@ you.
|
|
|
227
268
|
docstrings that explain *why* (the reader can see *what*).
|
|
228
269
|
- Target Python 3.10+. Keep the core dependencies minimal (pandas, numpy, pyyaml,
|
|
229
270
|
jinja2). Anything heavier goes in an optional extra in `pyproject.toml`.
|
|
230
|
-
- No `print` in library code — return data
|
|
231
|
-
|
|
271
|
+
- No `print` in library code — return data, add to the run `log`, or use
|
|
272
|
+
`logging.getLogger(__name__)` (the package installs a `NullHandler`). `print` is for the
|
|
273
|
+
CLI only, and under `--json` the CLI keeps stdout for the one JSON summary object.
|
|
232
274
|
|
|
233
275
|
## Pull requests
|
|
234
276
|
|