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.
Files changed (53) hide show
  1. sift_lint-1.0.0/.gitattributes +3 -0
  2. sift_lint-1.0.0/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  3. sift_lint-1.0.0/.github/dependabot.yml +7 -0
  4. sift_lint-1.0.0/.github/scripts/check_distributions.py +93 -0
  5. sift_lint-1.0.0/.github/scripts/check_release.py +33 -0
  6. sift_lint-1.0.0/.github/workflows/README.md +46 -0
  7. sift_lint-1.0.0/.github/workflows/ci.yml +83 -0
  8. sift_lint-1.0.0/.github/workflows/release.yml +68 -0
  9. sift_lint-1.0.0/.gitignore +25 -0
  10. sift_lint-1.0.0/BENCHMARK.md +40 -0
  11. sift_lint-1.0.0/CHANGELOG.md +30 -0
  12. sift_lint-1.0.0/CODES.md +127 -0
  13. sift_lint-1.0.0/CONTRIBUTING.md +46 -0
  14. sift_lint-1.0.0/LICENSE +21 -0
  15. sift_lint-1.0.0/PKG-INFO +463 -0
  16. sift_lint-1.0.0/README.md +427 -0
  17. sift_lint-1.0.0/REAL-DATA.md +130 -0
  18. sift_lint-1.0.0/SECURITY.md +37 -0
  19. sift_lint-1.0.0/SETUP.md +67 -0
  20. sift_lint-1.0.0/bench/__init__.py +0 -0
  21. sift_lint-1.0.0/bench/corruptions.py +329 -0
  22. sift_lint-1.0.0/bench/run.py +198 -0
  23. sift_lint-1.0.0/demo/README.md +76 -0
  24. sift_lint-1.0.0/demo/demo.tape +66 -0
  25. sift_lint-1.0.0/demo/orders.csv +11 -0
  26. sift_lint-1.0.0/docs/PROJECT_GUIDE.md +1253 -0
  27. sift_lint-1.0.0/examples/customers.csv +5 -0
  28. sift_lint-1.0.0/examples/orders.parquet +0 -0
  29. sift_lint-1.0.0/examples/orders.xlsx +0 -0
  30. sift_lint-1.0.0/examples/orders_baseline.csv +61 -0
  31. sift_lint-1.0.0/examples/orders_de.csv +4 -0
  32. sift_lint-1.0.0/examples/orders_messy.csv +41 -0
  33. sift_lint-1.0.0/examples/orders_with_refs.csv +6 -0
  34. sift_lint-1.0.0/pyproject.toml +57 -0
  35. sift_lint-1.0.0/sample-report.html +143 -0
  36. sift_lint-1.0.0/sift.toml +36 -0
  37. sift_lint-1.0.0/sift_lint/__init__.py +59 -0
  38. sift_lint-1.0.0/sift_lint/checks.py +1194 -0
  39. sift_lint-1.0.0/sift_lint/cli.py +591 -0
  40. sift_lint-1.0.0/sift_lint/config.py +151 -0
  41. sift_lint-1.0.0/sift_lint/drift.py +160 -0
  42. sift_lint-1.0.0/sift_lint/findings.py +114 -0
  43. sift_lint-1.0.0/sift_lint/impact.py +348 -0
  44. sift_lint-1.0.0/sift_lint/inference.py +380 -0
  45. sift_lint-1.0.0/sift_lint/initialize.py +232 -0
  46. sift_lint-1.0.0/sift_lint/loading.py +270 -0
  47. sift_lint-1.0.0/sift_lint/profiling.py +388 -0
  48. sift_lint-1.0.0/sift_lint/references.py +165 -0
  49. sift_lint-1.0.0/sift_lint/repair.py +472 -0
  50. sift_lint-1.0.0/sift_lint/reporting.py +342 -0
  51. sift_lint-1.0.0/sift_lint/sources.py +336 -0
  52. sift_lint-1.0.0/sift_lint/text.py +72 -0
  53. sift_lint-1.0.0/tests/test_sift.py +2621 -0
@@ -0,0 +1,3 @@
1
+ # This fixture intentionally mixes CRLF/LF and contains trailing whitespace.
2
+ # Treat it as data so Git never normalizes the bytes on checkout or commit.
3
+ examples/orders_messy.csv -text
@@ -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,7 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 5
@@ -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.
@@ -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.
@@ -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.