Skip to content

Commit 6a294ec

Browse files
feat: repair limits, snapshot control, deterministic outliers, CI quality lanes
feat: repair limits, snapshot control, deterministic outliers, CI quality lanes
2 parents 641a9b9 + 779ec3f commit 6a294ec

21 files changed

Lines changed: 394 additions & 58 deletions

‎.github/workflows/ci.yml‎

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,38 @@ on:
44
push:
55
branches: [main]
66
pull_request:
7+
schedule:
8+
- cron: "0 3 * * *"
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
714

815
jobs:
9-
test:
16+
quality-fast:
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 20
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
cache: pip
25+
- name: Install
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install -e ".[dev,ml]"
29+
- name: Lint
30+
run: ruff check src tests
31+
- name: Typecheck
32+
run: mypy src/freshdata
33+
- name: Test (required fast lane)
34+
run: pytest -m "not online and not large"
35+
36+
test-matrix:
1037
runs-on: ubuntu-latest
38+
timeout-minutes: 30
1139
strategy:
1240
fail-fast: false
1341
matrix:
@@ -30,15 +58,31 @@ jobs:
3058
pip install -e ".[dev,ml]"
3159
if [ -n "${{ matrix.pandas }}" ]; then pip install "${{ matrix.pandas }}"; fi
3260
if [ -n "${{ matrix.numpy }}" ]; then pip install "${{ matrix.numpy }}"; fi
33-
- name: Lint
34-
run: ruff check src tests
35-
- name: Typecheck
36-
run: mypy src/freshdata
37-
- name: Test
38-
run: pytest
61+
- name: Test (fast marker set)
62+
run: pytest -m "not online and not large"
63+
64+
nightly-online-large:
65+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
66+
runs-on: ubuntu-latest
67+
timeout-minutes: 45
68+
continue-on-error: true
69+
steps:
70+
- uses: actions/checkout@v4
71+
- uses: actions/setup-python@v5
72+
with:
73+
python-version: "3.12"
74+
cache: pip
75+
- name: Install
76+
run: |
77+
python -m pip install --upgrade pip
78+
pip install -e ".[dev,ml]"
79+
- name: Nightly online/large drift checks
80+
run: pytest -m "online or large or tier1"
3981

4082
build:
83+
needs: quality-fast
4184
runs-on: ubuntu-latest
85+
timeout-minutes: 10
4286
steps:
4387
- uses: actions/checkout@v4
4488
- uses: actions/setup-python@v5
@@ -54,8 +98,9 @@ jobs:
5498
# Publish a self-hosted shields endpoint badge to the `badges` branch
5599
# (no third-party account needed). Runs only on main.
56100
if: github.ref == 'refs/heads/main'
57-
needs: test
101+
needs: quality-fast
58102
runs-on: ubuntu-latest
103+
timeout-minutes: 15
59104
permissions:
60105
contents: write
61106
steps:

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ concurrency:
2020
jobs:
2121
build-deploy:
2222
runs-on: ubuntu-latest
23+
timeout-minutes: 15
2324
steps:
2425
- uses: actions/checkout@v4
2526
with:

‎.github/workflows/release.yml‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ name: Release
33
# Publish freshdata-cleaner to PyPI when a version tag is pushed (e.g. v0.5.0),
44
# or manually via the Actions tab.
55
#
6-
# Authentication uses an API token stored in the repository secret
7-
# PYPI_API_TOKEN (a PyPI project/account token, username __token__). `skip-existing`
8-
# makes a re-run on an already-published version a no-op success. To switch to PyPI
9-
# Trusted Publishing instead, configure a trusted publisher on the project and replace
10-
# the `password:` line with `permissions: id-token: write`.
6+
# Authentication uses PyPI Trusted Publishing (OIDC). Configure a trusted
7+
# publisher on PyPI for this repository/workflow/environment.
118

129
on:
1310
push:
@@ -18,10 +15,39 @@ on:
1815
permissions:
1916
contents: read
2017

18+
concurrency:
19+
group: release-${{ github.ref }}
20+
cancel-in-progress: false
21+
2122
jobs:
23+
preflight:
24+
name: Release quality gate
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 30
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
cache: pip
33+
- name: Install
34+
run: |
35+
python -m pip install --upgrade pip
36+
pip install -e ".[dev,ml,docs]"
37+
- name: Lint
38+
run: ruff check src tests
39+
- name: Typecheck
40+
run: mypy src/freshdata
41+
- name: Test (release fast lane)
42+
run: pytest -m "not online and not large"
43+
- name: Docs strict
44+
run: mkdocs build --strict
45+
2246
build:
2347
name: Build distributions
48+
needs: preflight
2449
runs-on: ubuntu-latest
50+
timeout-minutes: 10
2551
steps:
2652
- uses: actions/checkout@v4
2753
- uses: actions/setup-python@v5
@@ -41,11 +67,13 @@ jobs:
4167
name: Publish to PyPI
4268
needs: build
4369
runs-on: ubuntu-latest
70+
timeout-minutes: 10
4471
environment:
4572
name: pypi
4673
url: https://pypi.org/project/freshdata-cleaner/
4774
permissions:
4875
contents: read
76+
id-token: write
4977
steps:
5078
- uses: actions/download-artifact@v4
5179
with:
@@ -54,5 +82,4 @@ jobs:
5482
- name: Publish
5583
uses: pypa/gh-action-pypi-publish@release/v1
5684
with:
57-
password: ${{ secrets.PYPI_API_TOKEN }}
5885
skip-existing: true

‎QUALITY_OPS.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Quality Operations Runbook
2+
3+
This runbook defines the weekly quality cadence and the minimum metrics to track
4+
for release readiness.
5+
6+
## Weekly cadence
7+
8+
- **Monday (scope/risk review)**
9+
- Review open PR risk levels (engine changes, fixture changes, workflow changes).
10+
- Confirm required CI lane status and top failure causes from last week.
11+
- **Wednesday (midweek quality check)**
12+
- Review skip counts and flaky failures.
13+
- Verify nightly online/large job output and open drift issues.
14+
- **Friday (merge gate)**
15+
- Merge only PRs with passing required checks.
16+
- Validate release readiness scorecard before tagging.
17+
18+
## Scorecard metrics
19+
20+
Track these weekly (rolling 4-week trend):
21+
22+
- Required CI flake rate (% reruns needed to pass).
23+
- Median required CI duration (minutes).
24+
- `pytest` skip count in required lane.
25+
- Nightly online/large failures (count + top 3 causes).
26+
- Golden snapshot updates merged (count) with diff summaries attached.
27+
- Release gate pass/fail rate.
28+
29+
## Exit criteria for a release candidate
30+
31+
- Required lane flake rate is near zero for the last 2 weeks.
32+
- No unresolved deterministic regression in outlier/repair tests.
33+
- No unexplained golden drift.
34+
- Release workflow preflight passes on tag candidate.
35+
36+
## Operational commands
37+
38+
Required lane locally:
39+
40+
```bash
41+
ruff check src tests
42+
mypy src/freshdata
43+
pytest -m "not online and not large"
44+
```
45+
46+
Nightly lane locally:
47+
48+
```bash
49+
pytest -m "online or large or tier1"
50+
```

‎RELEASE.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ backward-compatible fixes.
1717

1818
## Release checklist
1919

20-
1. **Green main** — `pytest`, `ruff check .`, `mypy src/freshdata`, and
20+
1. **Green main** — `pytest -m "not online and not large"`, `ruff check .`, `mypy src/freshdata`, and
2121
`mkdocs build --strict` all pass.
2222
2. **Bump the version** in `pyproject.toml` and `src/freshdata/__init__.py`.
2323
3. **Update `CHANGELOG.md`** — move `Unreleased` notes under a new
2424
`## [X.Y.Z] - YYYY-MM-DD` heading.
2525
4. **Commit & PR** — merge to `main`.
2626
5. **Build & validate** locally (see below).
2727
6. **Publish to TestPyPI**, smoke-test the install.
28-
7. **Publish to PyPI** (or push the tag and let CI do it).
28+
7. **Publish to PyPI** (push the tag and let CI do it through trusted publishing).
2929
8. **Tag & GitHub release** — `git tag vX.Y.Z` and create the release with
3030
notes from the changelog.
3131
9. **Verify** — `pip install freshdata-cleaner` in a clean environment imports
@@ -42,10 +42,11 @@ twine check dist/* # validates metadata + long-description renderin
4242

4343
## Publish
4444

45-
### Option A — automated (recommended)
45+
### Option A — automated (required)
4646

47-
Push a version tag; the `Release` workflow builds and publishes via PyPI
48-
**Trusted Publishing** (OIDC, no stored token):
47+
Push a version tag; the `Release` workflow runs a quality gate first
48+
(lint, typecheck, `pytest -m "not online and not large"`, docs strict),
49+
then builds and publishes via PyPI **Trusted Publishing** (OIDC, no stored token):
4950

5051
```bash
5152
git tag v0.5.0
@@ -56,7 +57,7 @@ One-time setup: add a trusted publisher at
5657
<https://pypi.org/manage/project/freshdata-cleaner/settings/publishing/>
5758
(workflow `release.yml`, environment `pypi`).
5859

59-
### Option B — manual with `twine`
60+
### Option B — manual with `twine` (fallback only)
6061

6162
```bash
6263
# 1. TestPyPI first
@@ -69,8 +70,9 @@ python -c "import freshdata as fd; print(fd.__version__)"
6970
twine upload dist/*
7071
```
7172

72-
Use a [PyPI API token](https://pypi.org/help/#apitoken) (username `__token__`).
73-
Never commit tokens; prefer `~/.pypirc` or the `TWINE_PASSWORD` env var.
73+
Use a [PyPI API token](https://pypi.org/help/#apitoken) (username `__token__`) only
74+
for emergency/manual fallback releases. Never commit tokens; prefer `~/.pypirc`
75+
or the `TWINE_PASSWORD` env var.
7476

7577
## Naming
7678

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ ignore = [
184184
[tool.ruff.lint.per-file-ignores]
185185
"tests/*" = ["PLR2004", "SIM117"]
186186
"src/freshdata/engine/model_select.py" = ["PLR0915"]
187+
"src/freshdata/engine/outliers.py" = ["PLR0915"]
187188
"src/freshdata/adapters/polars.py" = ["PLC0415", "PLW0603"]
188189
"scripts/debug_datasets.py" = ["PLR0915"]
189190
# Enterprise layer: lazy optional imports (PLC0415) keep `import freshdata` cheap;

‎scripts/debug_dataset_sweep.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020

2121
_REPO = Path(__file__).resolve().parents[1]
2222
_TESTS = _REPO / "tests"
23-
_LOG_PATH = _REPO.parent / ".cursor" / "debug-17298c.log"
24-
_SESSION = "17298c"
23+
_LOG_PATH = _REPO.parent / ".cursor" / "debug_dataset_sweep.log"
2524

2625
if str(_TESTS) not in sys.path:
2726
sys.path.insert(0, str(_TESTS))
@@ -46,7 +45,6 @@
4645
def _log(hypothesis_id: str, dataset: str, check: str, passed: bool, detail: str = "") -> None:
4746
_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
4847
payload = {
49-
"sessionId": _SESSION,
5048
"hypothesisId": hypothesis_id,
5149
"location": "debug_dataset_sweep.py",
5250
"message": check,

‎src/freshdata/api.py‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ def plan(
136136
df: pd.DataFrame,
137137
*,
138138
mode: str = "suggest",
139+
max_patches: int | None = None,
140+
max_cells_scanned: int | None = None,
141+
retain_snapshots: bool = True,
139142
config: CleanConfig | None = None,
140143
**options: object,
141144
) -> RepairPlan:
@@ -147,7 +150,15 @@ def plan(
147150
deterministic representation repairs by disabling statistical engine
148151
actions.
149152
"""
150-
return build_repair_plan(to_pandas(df), mode=mode, config=config, **options)
153+
return build_repair_plan(
154+
to_pandas(df),
155+
mode=mode,
156+
max_patches=max_patches,
157+
max_cells_scanned=max_cells_scanned,
158+
retain_snapshots=retain_snapshots,
159+
config=config,
160+
**options,
161+
)
151162

152163

153164
def repair(
@@ -156,6 +167,9 @@ def repair(
156167
mode: str = "repair_safe",
157168
approved_patch_ids: set[str] | None = None,
158169
return_plan: bool = False,
170+
max_patches: int | None = None,
171+
max_cells_scanned: int | None = None,
172+
retain_snapshots: bool = True,
159173
config: CleanConfig | None = None,
160174
**options: object,
161175
) -> pd.DataFrame | tuple[pd.DataFrame, RepairPlan]:
@@ -164,7 +178,15 @@ def repair(
164178
``mode="repair_reviewed"`` applies only ``approved_patch_ids``. Other
165179
modes apply every patch proposed by the plan.
166180
"""
167-
repair_plan = build_repair_plan(to_pandas(df), mode=mode, config=config, **options)
181+
repair_plan = build_repair_plan(
182+
to_pandas(df),
183+
mode=mode,
184+
max_patches=max_patches,
185+
max_cells_scanned=max_cells_scanned,
186+
retain_snapshots=retain_snapshots,
187+
config=config,
188+
**options,
189+
)
168190
approved = set(approved_patch_ids or ()) if mode == "repair_reviewed" else None
169191
repaired = from_pandas(repair_plan.apply(approved), df)
170192
if return_plan:

0 commit comments

Comments
 (0)