sift-lint 1.0.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.
- sift_lint-1.0.0/.gitattributes +3 -0
- sift_lint-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- sift_lint-1.0.0/.github/dependabot.yml +7 -0
- sift_lint-1.0.0/.github/scripts/check_distributions.py +93 -0
- sift_lint-1.0.0/.github/scripts/check_release.py +33 -0
- sift_lint-1.0.0/.github/workflows/README.md +46 -0
- sift_lint-1.0.0/.github/workflows/ci.yml +83 -0
- sift_lint-1.0.0/.github/workflows/release.yml +68 -0
- sift_lint-1.0.0/.gitignore +25 -0
- sift_lint-1.0.0/BENCHMARK.md +40 -0
- sift_lint-1.0.0/CHANGELOG.md +30 -0
- sift_lint-1.0.0/CODES.md +127 -0
- sift_lint-1.0.0/CONTRIBUTING.md +46 -0
- sift_lint-1.0.0/LICENSE +21 -0
- sift_lint-1.0.0/PKG-INFO +463 -0
- sift_lint-1.0.0/README.md +427 -0
- sift_lint-1.0.0/REAL-DATA.md +130 -0
- sift_lint-1.0.0/SECURITY.md +37 -0
- sift_lint-1.0.0/SETUP.md +67 -0
- sift_lint-1.0.0/bench/__init__.py +0 -0
- sift_lint-1.0.0/bench/corruptions.py +329 -0
- sift_lint-1.0.0/bench/run.py +198 -0
- sift_lint-1.0.0/demo/README.md +76 -0
- sift_lint-1.0.0/demo/demo.tape +66 -0
- sift_lint-1.0.0/demo/orders.csv +11 -0
- sift_lint-1.0.0/docs/PROJECT_GUIDE.md +1253 -0
- sift_lint-1.0.0/examples/customers.csv +5 -0
- sift_lint-1.0.0/examples/orders.parquet +0 -0
- sift_lint-1.0.0/examples/orders.xlsx +0 -0
- sift_lint-1.0.0/examples/orders_baseline.csv +61 -0
- sift_lint-1.0.0/examples/orders_de.csv +4 -0
- sift_lint-1.0.0/examples/orders_messy.csv +41 -0
- sift_lint-1.0.0/examples/orders_with_refs.csv +6 -0
- sift_lint-1.0.0/pyproject.toml +57 -0
- sift_lint-1.0.0/sample-report.html +143 -0
- sift_lint-1.0.0/sift.toml +36 -0
- sift_lint-1.0.0/sift_lint/__init__.py +59 -0
- sift_lint-1.0.0/sift_lint/checks.py +1194 -0
- sift_lint-1.0.0/sift_lint/cli.py +591 -0
- sift_lint-1.0.0/sift_lint/config.py +151 -0
- sift_lint-1.0.0/sift_lint/drift.py +160 -0
- sift_lint-1.0.0/sift_lint/findings.py +114 -0
- sift_lint-1.0.0/sift_lint/impact.py +348 -0
- sift_lint-1.0.0/sift_lint/inference.py +380 -0
- sift_lint-1.0.0/sift_lint/initialize.py +232 -0
- sift_lint-1.0.0/sift_lint/loading.py +270 -0
- sift_lint-1.0.0/sift_lint/profiling.py +388 -0
- sift_lint-1.0.0/sift_lint/references.py +165 -0
- sift_lint-1.0.0/sift_lint/repair.py +472 -0
- sift_lint-1.0.0/sift_lint/reporting.py +342 -0
- sift_lint-1.0.0/sift_lint/sources.py +336 -0
- sift_lint-1.0.0/sift_lint/text.py +72 -0
- sift_lint-1.0.0/tests/test_sift.py +2621 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Report an incorrect finding, crash, or other Sift problem
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- For a false positive — Sift reporting something that is actually fine —
|
|
7
|
+
please include the smallest CSV that reproduces it. That case usually
|
|
8
|
+
becomes a permanent regression test. -->
|
|
9
|
+
|
|
10
|
+
**What Sift reported**
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
paste the finding here
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**What the data actually is**
|
|
17
|
+
|
|
18
|
+
**Smallest file that reproduces it**
|
|
19
|
+
|
|
20
|
+
```csv
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Version** — output of `sift-lint --version`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Check built distributions in clean environments outside the source tree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import venv
|
|
12
|
+
import zipfile
|
|
13
|
+
|
|
14
|
+
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
15
|
+
DIST = ROOT / "dist"
|
|
16
|
+
EXAMPLE = ROOT / "examples" / "orders_messy.csv"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(*args: str, cwd: pathlib.Path) -> None:
|
|
20
|
+
subprocess.run(args, cwd=cwd, check=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def single(pattern: str) -> pathlib.Path:
|
|
24
|
+
matches = sorted(DIST.glob(pattern))
|
|
25
|
+
if len(matches) != 1:
|
|
26
|
+
raise RuntimeError(f"Expected one {pattern} in {DIST}, found {len(matches)}")
|
|
27
|
+
return matches[0]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_contents(wheel: pathlib.Path, sdist: pathlib.Path) -> None:
|
|
31
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
32
|
+
members = set(archive.namelist())
|
|
33
|
+
if "sift_lint/__init__.py" not in members or "sift_lint/cli.py" not in members:
|
|
34
|
+
raise RuntimeError("Wheel is missing the Sift package")
|
|
35
|
+
if any(name.startswith("sift/") for name in members):
|
|
36
|
+
raise RuntimeError("Wheel contains the old import package")
|
|
37
|
+
|
|
38
|
+
with tarfile.open(sdist, "r:gz") as archive:
|
|
39
|
+
members = {name.split("/", 1)[-1] for name in archive.getnames()}
|
|
40
|
+
for expected in ("sift_lint/__init__.py", "README.md", "LICENSE"):
|
|
41
|
+
if expected not in members:
|
|
42
|
+
raise RuntimeError(f"Source distribution is missing {expected}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def smoke(artifact: pathlib.Path, workspace: pathlib.Path, label: str) -> None:
|
|
46
|
+
env = workspace / label
|
|
47
|
+
venv.EnvBuilder(with_pip=True).create(env)
|
|
48
|
+
scripts = env / ("Scripts" if os.name == "nt" else "bin")
|
|
49
|
+
python = scripts / ("python.exe" if os.name == "nt" else "python")
|
|
50
|
+
executable = scripts / ("sift-lint.exe" if os.name == "nt" else "sift-lint")
|
|
51
|
+
|
|
52
|
+
run(str(python), "-m", "pip", "install", "--no-deps", str(artifact), cwd=workspace)
|
|
53
|
+
run(
|
|
54
|
+
str(python),
|
|
55
|
+
"-c",
|
|
56
|
+
(
|
|
57
|
+
"from importlib.metadata import version; "
|
|
58
|
+
"from pathlib import Path; "
|
|
59
|
+
"import sift_lint, sys; "
|
|
60
|
+
"assert sift_lint.__version__ == version('sift-lint'); "
|
|
61
|
+
"assert not Path(sift_lint.__file__).resolve().is_relative_to(Path(sys.argv[1]))"
|
|
62
|
+
),
|
|
63
|
+
str(ROOT),
|
|
64
|
+
cwd=workspace,
|
|
65
|
+
)
|
|
66
|
+
run(str(executable), "--version", cwd=workspace)
|
|
67
|
+
run(str(executable), "--help", cwd=workspace)
|
|
68
|
+
run(
|
|
69
|
+
str(executable),
|
|
70
|
+
"check",
|
|
71
|
+
str(EXAMPLE),
|
|
72
|
+
"--no-config",
|
|
73
|
+
"--fail-on",
|
|
74
|
+
"none",
|
|
75
|
+
cwd=workspace,
|
|
76
|
+
)
|
|
77
|
+
run(str(python), "-m", "pip", "check", cwd=workspace)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> None:
|
|
81
|
+
wheel = single("*.whl")
|
|
82
|
+
sdist = single("*.tar.gz")
|
|
83
|
+
check_contents(wheel, sdist)
|
|
84
|
+
run(sys.executable, "-m", "twine", "check", "--strict", str(wheel), str(sdist), cwd=ROOT)
|
|
85
|
+
|
|
86
|
+
with tempfile.TemporaryDirectory(prefix="sift-package-") as directory:
|
|
87
|
+
workspace = pathlib.Path(directory)
|
|
88
|
+
smoke(wheel, workspace, "wheel")
|
|
89
|
+
smoke(sdist, workspace, "sdist")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Reject release tags that do not match the package version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from sift_lint import __version__
|
|
9
|
+
|
|
10
|
+
TAG_PATTERN = re.compile(r"v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_tag(tag: str, version: str) -> None:
|
|
14
|
+
if not TAG_PATTERN.fullmatch(tag):
|
|
15
|
+
raise ValueError(f"Expected a stable release tag like v1.0.0, got {tag!r}")
|
|
16
|
+
if int(tag[1:].split(".", 1)[0]) == 0:
|
|
17
|
+
raise ValueError("Public releases start at version 1.0.0")
|
|
18
|
+
if tag[1:] != version:
|
|
19
|
+
raise ValueError(f"Release tag {tag!r} does not match package version {version!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
if len(sys.argv) != 2:
|
|
24
|
+
raise SystemExit("Usage: python .github/scripts/check_release.py vX.Y.Z")
|
|
25
|
+
try:
|
|
26
|
+
check_tag(sys.argv[1], __version__)
|
|
27
|
+
except ValueError as exc:
|
|
28
|
+
raise SystemExit(str(exc)) from exc
|
|
29
|
+
print(f"Release tag {sys.argv[1]} matches package version {__version__}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
`ci.yml` runs on every push to `main` and every pull request.
|
|
4
|
+
|
|
5
|
+
**test** — runs Ruff, the full test suite and the recall benchmark on Ubuntu
|
|
6
|
+
with Python 3.11–3.14, plus Windows and macOS with Python 3.14. The format
|
|
7
|
+
extras are installed for every test job. Python 3.15 will be added when
|
|
8
|
+
`actions/setup-python` provides it for the selected runner.
|
|
9
|
+
|
|
10
|
+
**package** — builds a wheel and source distribution on Linux, Windows and
|
|
11
|
+
macOS. Both archives undergo strict metadata checks and are installed
|
|
12
|
+
separately in clean virtual environments. Each installed package is tested
|
|
13
|
+
outside the repository for version consistency, imports, the CLI, a real CSV
|
|
14
|
+
check and `pip check`.
|
|
15
|
+
|
|
16
|
+
**data-gate** — runs Sift against this repository's own example files, the way
|
|
17
|
+
it is meant to be used in a pipeline: `check` on a folder, `diff` against a
|
|
18
|
+
baseline, `fix` as a dry run, and `impact` on a known-bad column. These use
|
|
19
|
+
`--fail-on none` because the example data is deliberately broken; in a real
|
|
20
|
+
pipeline you would drop that flag and let a bad file fail the build.
|
|
21
|
+
|
|
22
|
+
## Supply-chain controls
|
|
23
|
+
|
|
24
|
+
Workflow actions are pinned to full commit SHAs rather than mutable version
|
|
25
|
+
tags. Checkout does not persist the repository token because these jobs never
|
|
26
|
+
push back to the repository, and the workflow token is explicitly limited to
|
|
27
|
+
read-only repository contents.
|
|
28
|
+
|
|
29
|
+
## Release publishing
|
|
30
|
+
|
|
31
|
+
`release.yml` runs only when a GitHub Release is published. It ignores prereleases
|
|
32
|
+
and rejects tags that do not match the package's stable version or point outside
|
|
33
|
+
`main`. The unprivileged build job runs tests and the benchmark, then builds and
|
|
34
|
+
validates a wheel and source distribution. The separate publish job downloads
|
|
35
|
+
those artifacts and publishes through PyPI Trusted Publishing.
|
|
36
|
+
|
|
37
|
+
The publish job uses the `pypi` GitHub environment and has only the OIDC
|
|
38
|
+
permission needed for publishing. No long-lived PyPI token is stored. Creating
|
|
39
|
+
a branch, pushing a tag, or merging a pull request never triggers publishing.
|
|
40
|
+
|
|
41
|
+
Before using **Publish release**, configure the `pypi` environment and the
|
|
42
|
+
matching Trusted Publisher on PyPI, then complete the maintainer's release
|
|
43
|
+
checklist. Publishing a release is the deliberate production publishing action.
|
|
44
|
+
|
|
45
|
+
`.github/dependabot.yml` proposes weekly updates for GitHub Actions. Review the
|
|
46
|
+
updated commit SHA pins before merging those pull requests.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: test (${{ matrix.os }}, Python ${{ matrix.python-version }})
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
include:
|
|
19
|
+
- os: ubuntu-24.04
|
|
20
|
+
python-version: "3.11"
|
|
21
|
+
- os: ubuntu-24.04
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
- os: ubuntu-24.04
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
- os: ubuntu-24.04
|
|
26
|
+
python-version: "3.14"
|
|
27
|
+
- os: windows-2025
|
|
28
|
+
python-version: "3.14"
|
|
29
|
+
- os: macos-15
|
|
30
|
+
python-version: "3.14"
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
33
|
+
with:
|
|
34
|
+
persist-credentials: false
|
|
35
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
- run: python -m pip install -e ".[dev]"
|
|
39
|
+
- run: ruff check .
|
|
40
|
+
- run: pytest -q
|
|
41
|
+
# Preserve the benchmark's corruption-detection and clean-control gates.
|
|
42
|
+
- run: python -m bench.run
|
|
43
|
+
|
|
44
|
+
package:
|
|
45
|
+
name: package (${{ matrix.os }})
|
|
46
|
+
runs-on: ${{ matrix.os }}
|
|
47
|
+
strategy:
|
|
48
|
+
fail-fast: false
|
|
49
|
+
matrix:
|
|
50
|
+
os: [ubuntu-24.04, windows-2025, macos-15]
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
53
|
+
with:
|
|
54
|
+
persist-credentials: false
|
|
55
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.14"
|
|
58
|
+
- name: Install build validation tools
|
|
59
|
+
run: python -m pip install build twine
|
|
60
|
+
- name: Build wheel and source distribution
|
|
61
|
+
run: python -m build
|
|
62
|
+
- name: Validate and smoke-test distributions
|
|
63
|
+
run: python .github/scripts/check_distributions.py
|
|
64
|
+
|
|
65
|
+
# Sift is also used as a data gate against its own examples.
|
|
66
|
+
data-gate:
|
|
67
|
+
runs-on: ubuntu-24.04
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
70
|
+
with:
|
|
71
|
+
persist-credentials: false
|
|
72
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.12"
|
|
75
|
+
- run: pip install -e ".[all]"
|
|
76
|
+
- name: Lint the example files
|
|
77
|
+
run: sift-lint check examples/ --fail-on none
|
|
78
|
+
- name: Compare against the baseline
|
|
79
|
+
run: sift-lint diff examples/orders_baseline.csv examples/orders_messy.csv --fail-on none
|
|
80
|
+
- name: Repair what is safely repairable
|
|
81
|
+
run: sift-lint fix examples/orders_messy.csv --force --dry-run
|
|
82
|
+
- name: Report the blast radius
|
|
83
|
+
run: sift-lint impact examples/orders_messy.csv --sum order_total --group-by region
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
|
|
13
|
+
runs-on: ubuntu-24.04
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
+
with:
|
|
19
|
+
ref: ${{ github.event.release.tag_name }}
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.14"
|
|
25
|
+
- name: Verify release tag and source
|
|
26
|
+
env:
|
|
27
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
PYTHONPATH=. python .github/scripts/check_release.py "$RELEASE_TAG"
|
|
31
|
+
git fetch --no-tags origin main
|
|
32
|
+
git merge-base --is-ancestor HEAD origin/main
|
|
33
|
+
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
|
|
34
|
+
- name: Install build and validation tools
|
|
35
|
+
run: python -m pip install -e ".[dev]" build twine
|
|
36
|
+
- name: Run release checks
|
|
37
|
+
run: |
|
|
38
|
+
ruff check .
|
|
39
|
+
pytest -q
|
|
40
|
+
python -m bench.run
|
|
41
|
+
- name: Build distributions
|
|
42
|
+
run: python -m build
|
|
43
|
+
- name: Validate installed distributions
|
|
44
|
+
run: python .github/scripts/check_distributions.py
|
|
45
|
+
- name: Upload distributions
|
|
46
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
47
|
+
with:
|
|
48
|
+
name: release-distributions
|
|
49
|
+
path: dist/
|
|
50
|
+
if-no-files-found: error
|
|
51
|
+
|
|
52
|
+
publish:
|
|
53
|
+
if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
|
|
54
|
+
needs: build
|
|
55
|
+
runs-on: ubuntu-24.04
|
|
56
|
+
environment:
|
|
57
|
+
name: pypi
|
|
58
|
+
url: https://pypi.org/p/sift-lint
|
|
59
|
+
permissions:
|
|
60
|
+
id-token: write
|
|
61
|
+
steps:
|
|
62
|
+
- name: Download validated distributions
|
|
63
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
64
|
+
with:
|
|
65
|
+
name: release-distributions
|
|
66
|
+
path: dist/
|
|
67
|
+
- name: Publish distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.ruff_cache/
|
|
5
|
+
.coverage
|
|
6
|
+
.coverage.*
|
|
7
|
+
htmlcov/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
*.whl
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
.DS_Store
|
|
15
|
+
report.html
|
|
16
|
+
|
|
17
|
+
# Artefacts the examples in the README produce when you run them
|
|
18
|
+
examples/orders_repaired.csv
|
|
19
|
+
examples/repair-log.csv
|
|
20
|
+
examples/*-report.txt
|
|
21
|
+
examples/learned-sift.toml
|
|
22
|
+
|
|
23
|
+
# Recording artefacts (the finished GIF is committed; working files are not)
|
|
24
|
+
demo/clean.csv
|
|
25
|
+
demo/*.cast
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Benchmark
|
|
2
|
+
|
|
3
|
+
Generated by `python -m bench.run`. A clean file is generated, verified
|
|
4
|
+
silent, then corrupted one way at a time; each corruption declares the
|
|
5
|
+
finding code that should catch it.
|
|
6
|
+
|
|
7
|
+
**Recall: 28/28 (100%)**
|
|
8
|
+
|
|
9
|
+
**Noise on the clean control file: 0**
|
|
10
|
+
|
|
11
|
+
| Corruption | Expected finding | Result |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| -999 placeholders | `numeric-sentinel` | caught |
|
|
14
|
+
| CRLF and LF mixed | `mixed-line-endings` | caught |
|
|
15
|
+
| European decimal separators | `european-numbers` | caught |
|
|
16
|
+
| N/A instead of empty | `disguised-null` | caught |
|
|
17
|
+
| UTF-8 BOM | `bom` | caught |
|
|
18
|
+
| ambiguous date order | `ambiguous-dates` | caught |
|
|
19
|
+
| bad encoding round-trip | `mojibake` | caught |
|
|
20
|
+
| column collapsed to one value | `constant-column` | caught |
|
|
21
|
+
| column emptied entirely | `empty-column` | caught |
|
|
22
|
+
| column with no name | `unnamed-column` | caught |
|
|
23
|
+
| currency formatting | `number-as-text` | caught |
|
|
24
|
+
| dates in the future | `future-dates` | caught |
|
|
25
|
+
| epoch placeholder dates | `sentinel-dates` | caught |
|
|
26
|
+
| extreme outlier | `outliers` | caught |
|
|
27
|
+
| half the column emptied | `missing-values` | caught |
|
|
28
|
+
| headers differing by case | `similar-column-names` | caught |
|
|
29
|
+
| key value repeated | `key-not-unique` | caught |
|
|
30
|
+
| mixed casing | `label-variants` | caught |
|
|
31
|
+
| mixed decimal conventions | `conflicting-number-formats` | caught |
|
|
32
|
+
| repeated rows | `duplicate-rows` | caught |
|
|
33
|
+
| row with an extra field | `ragged-rows` | caught |
|
|
34
|
+
| single misspelling | `near-duplicate-labels` | caught |
|
|
35
|
+
| text in a number column | `mixed-types` | caught |
|
|
36
|
+
| thousands separators | `number-as-text` | caught |
|
|
37
|
+
| trailing spaces | `whitespace` | caught |
|
|
38
|
+
| two columns named the same | `duplicate-column-name` | caught |
|
|
39
|
+
| two date formats mixed | `conflicting-date-formats` | caught |
|
|
40
|
+
| zero-padded codes | `leading-zeros` | caught |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable user-visible changes to Sift are recorded here. Releases use
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## 1.0.0
|
|
7
|
+
|
|
8
|
+
### Included in the first stable distribution
|
|
9
|
+
|
|
10
|
+
- `sift-lint` CLI with `check`, `diff`, `fix`, `impact`, `init`, and `profile`
|
|
11
|
+
commands; a small Python API is available as `sift_lint`.
|
|
12
|
+
- Dependency-free CSV linting, with optional Excel and Parquet readers
|
|
13
|
+
provided through the `excel`, `parquet`, and `all` extras.
|
|
14
|
+
- Stable finding codes with text, JSON, and standalone HTML reports;
|
|
15
|
+
cross-file reference checks and configuration through `sift.toml`.
|
|
16
|
+
- Conservative repair with an audit log, baseline drift analysis, and
|
|
17
|
+
quantified aggregate-impact reporting.
|
|
18
|
+
- Terminal-control escaping and sensitive-data redaction safeguards.
|
|
19
|
+
|
|
20
|
+
### Release readiness
|
|
21
|
+
|
|
22
|
+
- Cross-platform CI covers Python 3.11–3.14 on Linux and Python 3.14 on
|
|
23
|
+
Windows/macOS; wheel and source-distribution smoke tests run on all three.
|
|
24
|
+
- The controlled corruption benchmark detects 28 of 28 planted defects
|
|
25
|
+
with zero findings on its clean control. This is not a general recall claim.
|
|
26
|
+
- Secure release-only PyPI publishing uses GitHub Actions and short-lived
|
|
27
|
+
Trusted Publishing credentials; GitHub Actions dependencies use pinned SHAs
|
|
28
|
+
with Dependabot updates.
|
|
29
|
+
- Public installation instructions, package metadata, contribution guidance,
|
|
30
|
+
and a security-reporting policy accompany the distribution.
|
sift_lint-1.0.0/CODES.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Finding codes
|
|
2
|
+
|
|
3
|
+
Every code Sift can emit. These strings are the stable interface — they appear in `--format json`, and they are what you name to silence a check:
|
|
4
|
+
|
|
5
|
+
```toml
|
|
6
|
+
[sift]
|
|
7
|
+
ignore = ["outliers"]
|
|
8
|
+
|
|
9
|
+
[sift.ignore_columns]
|
|
10
|
+
high-cardinality = ["email"]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Silencing per column is almost always better than silencing globally: a `notes` column is *supposed* to be free text, but the same finding on `customer_id` means an identifier leaked into a category field.
|
|
14
|
+
|
|
15
|
+
`sift-lint init` writes these exemptions for you, with the reason attached.
|
|
16
|
+
|
|
17
|
+
A test asserts this file lists exactly the codes the source can emit, so it cannot drift out of date.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
| Code | Severity | Meaning |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `ragged-rows` | error | Rows without the expected number of fields — usually an unescaped delimiter or an unclosed quote. |
|
|
24
|
+
| `no-data-rows` | error | A header and nothing under it. Readers get an empty result rather than an error. |
|
|
25
|
+
| `null-bytes` | error | NUL bytes in a text file — binary content with the wrong extension, or a truncated write. |
|
|
26
|
+
| `encoding` | error | The file is not valid UTF-8. Read as Latin-1 to continue. |
|
|
27
|
+
| `duplicate-column-name` | error | The same column name appears twice; most readers silently keep one. |
|
|
28
|
+
| `bom` | warning | A UTF-8 byte order mark that some readers fold into the first column name. |
|
|
29
|
+
| `mixed-line-endings` | warning | CRLF and LF in the same file, which makes diffs and checksums noisy. |
|
|
30
|
+
| `unnamed-column` | warning | A column with no name in the header. |
|
|
31
|
+
| `padded-column-name` | warning | A header with surrounding whitespace, so lookups by name miss it. |
|
|
32
|
+
| `similar-column-names` | warning | Two headers differing only by case or spacing. |
|
|
33
|
+
| `no-trailing-newline` | info | Concatenating this file with another would join two rows into one. |
|
|
34
|
+
|
|
35
|
+
## Types and numbers
|
|
36
|
+
|
|
37
|
+
| Code | Severity | Meaning |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `mixed-types` | error | A column that is mostly numbers or dates, with values that are neither. |
|
|
40
|
+
| `conflicting-number-formats` | error | The column mixes English and European decimal conventions; one of them is wrong by a thousand. |
|
|
41
|
+
| `ambiguous-decimal-separator` | error | Values like `1.234` differ by 1000x between conventions and nothing settles which was meant. |
|
|
42
|
+
| `european-numbers` | warning | Comma decimal, dot thousands. An English-locale reader misreads these silently. |
|
|
43
|
+
| `number-as-text` | warning | Numbers written with currency symbols, separators, percent signs or accounting parentheses. |
|
|
44
|
+
| `leading-zeros` | warning | An all-digit column whose zeros a numeric read would destroy. |
|
|
45
|
+
| `numeric-sentinel` | warning | A repeated placeholder like `-999` that will be averaged as a real value. |
|
|
46
|
+
| `outliers` | info | Values far from the median by modified z-score. Silent on skewed columns. |
|
|
47
|
+
| `unexpected-negative` | info | Negative values in a column named like a quantity. |
|
|
48
|
+
| `compact-date-or-number` | info | Every value is 8 digits, equally readable as `YYYYMMDD` or a plain number. |
|
|
49
|
+
|
|
50
|
+
## Dates
|
|
51
|
+
|
|
52
|
+
| Code | Severity | Meaning |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `conflicting-date-formats` | error | Some rows can only be day-first and others only month-first. Unrepairable. |
|
|
55
|
+
| `ambiguous-dates` | error | Every value reads both ways and nothing in the column settles the order. |
|
|
56
|
+
| `weak-date-evidence` | warning | An order was inferred, but only a small share of the column proves it. |
|
|
57
|
+
| `mixed-date-formats` | warning | ISO dates mixed with slash-separated ones. |
|
|
58
|
+
| `future-dates` | warning | Dates after today. |
|
|
59
|
+
| `implausible-dates` | warning | Dates before 1900. |
|
|
60
|
+
| `sentinel-dates` | warning | Placeholders such as `1970-01-01` or `9999-12-31` treated as real dates by range filters. |
|
|
61
|
+
|
|
62
|
+
## Missing data
|
|
63
|
+
|
|
64
|
+
| Code | Severity | Meaning |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `missing-values` | error / warning | Null rate above the configured threshold. Error above `null_error`, warning above `null_warn`. |
|
|
67
|
+
| `disguised-null` | warning | Missing values written as text (`N/A`, `unknown`, `-`) so they load as strings. |
|
|
68
|
+
| `empty-column` | warning | A column with no values at all. |
|
|
69
|
+
| `constant-column` | info | One value in every row — check whether a filter upstream collapsed it. |
|
|
70
|
+
|
|
71
|
+
## Values
|
|
72
|
+
|
|
73
|
+
| Code | Severity | Meaning |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `formula-injection` | warning | Values starting with `=`, `+` or `@`, which a spreadsheet executes as a formula rather than showing as text. |
|
|
76
|
+
| `mojibake` | error | Text mangled by a bad encoding round-trip. Not repairable; re-export the source. |
|
|
77
|
+
| `label-variants` | warning | The same category spelled several ways by case or spacing. |
|
|
78
|
+
| `whitespace` | warning | Stray, doubled or non-breaking whitespace inside values. |
|
|
79
|
+
| `near-duplicate-labels` | info | A rare misspelling of a spelling the rest of the column agrees on. |
|
|
80
|
+
| `high-cardinality` | info | A text column that is almost entirely unique — free text or an identifier, not a category. |
|
|
81
|
+
|
|
82
|
+
## Keys and references
|
|
83
|
+
|
|
84
|
+
| Code | Severity | Meaning |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `key-not-unique` | error | A declared key repeats, so joining on it multiplies rows. |
|
|
87
|
+
| `key-null` | error | A declared key is empty in some rows. |
|
|
88
|
+
| `missing-key-column` | error | A declared key column is not in the file. |
|
|
89
|
+
| `orphaned-reference` | error | Values pointing at rows that do not exist in the referenced file. |
|
|
90
|
+
| `reference-not-unique` | error | The far side of the relationship repeats, so it cannot identify a row. |
|
|
91
|
+
| `reference-matches-after-cleaning` | warning | Orphans that would match if case and spacing were normalised — dirty keys, not missing records. |
|
|
92
|
+
| `reference-null` | warning | The referencing column is empty, so those rows join to nothing. |
|
|
93
|
+
| `candidate-key` | info | A unique, complete column that could be enforced with `--key`. |
|
|
94
|
+
|
|
95
|
+
## Sensitive data
|
|
96
|
+
|
|
97
|
+
Nothing here is a defect — the file is fine. These say *this file contains personal or financial data*, which matters when it is about to be emailed, committed, or fed into a model. **No finding in this group ever includes example values**: a tool that prints someone's card number into a CI log has made the problem worse.
|
|
98
|
+
|
|
99
|
+
| Code | Severity | Meaning |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `sensitive-data` | warning | A column holds email addresses, phone numbers, payment card numbers or IBANs. Cards require an issuer prefix, a valid length and a Luhn check; phone numbers require an international prefix, or a local shape in a column named like a phone field. |
|
|
102
|
+
| `sensitive-column-name` | info | A column is *named* like a sensitive field (`ssn`, `date_of_birth`, `iban`). The values were not inspected. |
|
|
103
|
+
|
|
104
|
+
## Excel and Parquet
|
|
105
|
+
|
|
106
|
+
| Code | Severity | Meaning |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `formula-error` | error | `#REF!`, `#N/A` and similar exported as literal text. |
|
|
109
|
+
| `partial-excel-dates` | error | Some cells are real dates and the rest are text that looks identical on screen. |
|
|
110
|
+
| `preamble-rows` | warning | Title or spacer rows above the header, written for a human reader. |
|
|
111
|
+
| `loose-schema` | warning | A Parquet column declared as string whose values are all numbers. |
|
|
112
|
+
| `unread-sheets` | info | Other worksheets in the workbook that nothing checked. |
|
|
113
|
+
|
|
114
|
+
## Drift, from `sift-lint diff`
|
|
115
|
+
|
|
116
|
+
| Code | Severity | Meaning |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `column-removed` | error | A baseline column is gone. |
|
|
119
|
+
| `type-changed` | error | A column's inferred type changed between the two files. |
|
|
120
|
+
| `new-category` | warning | Categories not present in the baseline. |
|
|
121
|
+
| `null-rate-jump` | warning | A column became substantially emptier. |
|
|
122
|
+
| `distribution-shift` | warning | The median moved enough to suggest a unit change. |
|
|
123
|
+
| `column-order-changed` | warning | Harmless for named access, fatal for anything reading by position. |
|
|
124
|
+
| `row-count-shift` | warning | The file grew or shrank dramatically. |
|
|
125
|
+
| `column-added` | info | A column not present in the baseline. |
|
|
126
|
+
| `missing-category` | info | Baseline categories that no longer appear. |
|
|
127
|
+
| `duplicate-rows` | warning | Exact repeated rows, which inflate any total. |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Contributing to Sift
|
|
2
|
+
|
|
3
|
+
Bug reports, reproducible false positives, documentation corrections and
|
|
4
|
+
focused pull requests are welcome. For security vulnerabilities, follow
|
|
5
|
+
[SECURITY.md](SECURITY.md) rather than filing a public issue.
|
|
6
|
+
|
|
7
|
+
## Before proposing a change
|
|
8
|
+
|
|
9
|
+
Check existing [issues](https://github.com/N0tDeb/sift/issues). A useful data
|
|
10
|
+
quality bug report includes a small, non-sensitive input file, the command
|
|
11
|
+
used, the expected result and the actual finding or error. Do not attach
|
|
12
|
+
real customer data or secrets.
|
|
13
|
+
|
|
14
|
+
Sift's priority is accurate findings with few false positives. Before adding
|
|
15
|
+
a rule, explain what downstream damage it detects and when correct input
|
|
16
|
+
could produce a false alarm. Repairs must not guess when evidence is unclear.
|
|
17
|
+
|
|
18
|
+
## Development
|
|
19
|
+
|
|
20
|
+
Sift needs Python 3.11 or newer. After cloning, install development extras
|
|
21
|
+
in a suitable environment:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install -e '.[dev]'
|
|
25
|
+
ruff check .
|
|
26
|
+
pytest -q
|
|
27
|
+
python -m bench.run
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `dev` extra supplies pytest, Ruff and both optional file readers. No
|
|
31
|
+
special local release tooling is required; distribution checks run in GitHub
|
|
32
|
+
Actions. See [SETUP.md](SETUP.md) for the CI and maintainer workflow.
|
|
33
|
+
|
|
34
|
+
## Pull requests
|
|
35
|
+
|
|
36
|
+
- Branch from the latest `main` and keep changes focused.
|
|
37
|
+
- Add a regression test for behavioral fixes, including cases where Sift
|
|
38
|
+
should produce **no** finding.
|
|
39
|
+
- For new checks, extend the known-corruption benchmark where applicable and
|
|
40
|
+
update [CODES.md](CODES.md) when finding codes change.
|
|
41
|
+
- Update README examples and [Project Guide](docs/PROJECT_GUIDE.md) when
|
|
42
|
+
public commands, supported formats or expectations change.
|
|
43
|
+
- Make sure CI passes on Windows, macOS and Linux before requesting a merge.
|
|
44
|
+
|
|
45
|
+
The public command is `sift-lint`; the Python package is `sift_lint`.
|
|
46
|
+
See the [MIT license](LICENSE) for contribution licensing.
|
sift_lint-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sift contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|