Metadata-Version: 2.4
Name: staleness
Version: 0.1.0
Summary: Deterministic freshness and drift linter for RAG knowledge bases: catch stale chunks, missing/future timestamps, age-threshold breaches, temporal clustering, and embed-model age before they degrade your retrieval.
Author: Asmit Dash
License: MIT
Project-URL: Homepage, https://github.com/asmitdash/staleness
Project-URL: Issues, https://github.com/asmitdash/staleness/issues
Keywords: rag,llm,freshness,drift,staleness,lint,knowledge-base
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pdf
Requires-Dist: fpdf2>=2.7; extra == "pdf"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: fpdf2>=2.7; extra == "dev"
Dynamic: license-file

# staleness

**Deterministic freshness and drift linter for RAG knowledge bases.** One import, one call. Catches the silent decay bugs that turn a once-fresh corpus into a wrong-answer machine: chunks too old to be authoritative, missing or corrupted timestamps, future-dated bugs from clock skew, severe age skew, recent-window coverage gaps, and embedding-model-vs-corpus age mismatches.

```bash
pip install staleness          # core (Python stdlib only)
pip install staleness[pdf]     # adds PDF report support (fpdf2)
```

```python
import staleness
from datetime import datetime, timezone

chunks = [
    {"text": "...", "timestamp": "2024-01-15T00:00:00Z"},
    {"text": "...", "timestamp": "2026-05-20T10:30:00Z"},
    ...
]

report = staleness.check_corpus(
    chunks,
    max_age_days=365,
    embed_model_name="text-embedding-3-small",
    now=datetime.now(timezone.utc),
)
print(report)
if not report.ok():
    raise SystemExit("Refresh stale chunks before relying on this corpus.")
```

That's the whole API. staleness **does not** call any LLM, embed chunks, or hit a vector DB — it inspects timestamps and metadata. Pure Python stdlib. Sub-second on 100k chunks.

---

## Why this exists

A RAG corpus that was fresh on launch day quietly rots. Six months in:

- A documentation chunk says "the API takes parameter X" — but parameter X was renamed in the last release.
- News retrieval returns last year's market summary because no one indexed this quarter.
- A chunk's timestamp is silently `None` — the model treats stale and current content as equivalent.
- Clock-skew bugs in the ingestion pipeline produce future-dated chunks that artificially rank well.
- Your embed model was trained in 2021. Your corpus is mostly 2025. Half your domain language doesn't embed well.

**These are operational bugs, not algorithm bugs.** No retriever fixes them. No reranker fixes them. They need to be caught at ingestion time, deterministically, before retrieval ever runs.

---

## What it catches

| Code | Severity | What it catches |
|------|----------|-----------------|
| `ST001` | critical / warning | Chunks with missing or unparsable timestamps (critical if ≥5% of corpus) |
| `ST002` | critical / warning | Chunks older than `max_age_days` (critical if ≥25% of timestamped corpus) |
| `ST003` | critical | Future-dated chunks (clock-skew / parser bugs) |
| `ST004` | warning | Severe age skew — 80% of chunks fall inside <10% of the time span |
| `ST005` | warning | Coverage gap in the recent window (`recent_window_days`) |
| `ST006` | critical / warning | Embed model is much older than the corpus median date |
| `ST007` | warning | Topic-localized staleness (one topic's median age is much older than overall) |

Each finding lists affected chunk indices and a concrete fix.

---

## Demo

The repo ships [`examples/demo.py`](examples/demo.py) — a 30-chunk corpus with all the bugs baked in:
- 2 chunks with missing timestamps
- 8 chunks past the 365-day age threshold (one is 1500 days old)
- 1 future-dated chunk
- ~70% of timestamped chunks clustered inside a 30-day window (severe skew)
- 0 chunks in the most recent 60 days (coverage gap)

Run it:

```bash
cd examples
python demo.py
```

Expected: ST001/002/003 fire critical, ST004/005 fire warning, `report.ok() == False`.

---

## Use it in CI

```python
import staleness, sys

report = staleness.check_corpus(chunks, max_age_days=365)
sys.exit(0 if report.ok() else 1)
```

---

## API reference

```python
staleness.check_corpus(
    chunks,                            # list[dict] each with a timestamp field
    *,
    timestamp_field="timestamp",       # field name in each chunk
    now=None,                          # defaults to datetime.now(timezone.utc)
    max_age_days=365,                  # ST002 threshold
    recent_window_days=30,             # ST005 window
    min_recent_pct=5.0,                # ST005 threshold
    embed_model_name=None,             # enables ST006
    topic_field=None,                  # enables ST007 if set (e.g. "topic")
) -> Report
```

Timestamps can be: `datetime` (naive treated as UTC), epoch seconds (int/float, ms auto-detected), or ISO 8601 strings.

`Report`:

- `report.ok()` — `True` if no critical findings.
- `report.findings`, `report.critical`, `report.warnings`, `report.infos` — lists of `Finding`.
- `report.cleaned_chunks(chunks)` — drops chunks flagged by any critical finding.
- `print(report)` — human-readable terminal summary.
- `report.to_dict()` — JSON-serializable.

---

## Known embed-model cutoffs

staleness ships approximate training cutoffs for OpenAI text-embedding-3-{small,large,ada-002}, Cohere embed-english-v3.0, Voyage voyage-3, all-MiniLM-L6-v2, all-mpnet-base-v2, BAAI/bge-{small,base,large}-en-v1.5. Models not in the registry skip ST006 silently.

---

## See also

- **[chaffer](https://github.com/asmitdash/chaffer)** — sibling library: lints corpus for retrieval-quality bugs.
- **[redoubt](https://github.com/asmitdash/redoubt)** — sibling library: scans corpus for prompt-injection signatures.
- **[corroborate](https://github.com/asmitdash/corroborate)** — sibling library: deterministic answer-grounding after generation.
- **[dash-mlguard](https://github.com/asmitdash/dash-mlguard)** — same author, same form factor, but for ML training pipelines.

If you ship RAG to production, you probably want chaffer + redoubt + staleness + corroborate. Each takes a different swing at the corpus or its outputs.

---

## License

MIT — see [LICENSE](LICENSE).
