# rusket Agent Guidelines

## Project Overview
rusket is a blazing fast library and a drop-in replacement for `mlxtend`'s FP-Growth and Association Rules. It uses:
- **Rust** (via PyO3 + maturin) for the core high-performance mining algorithms.
- **Python** (3.10+) for the public API surface and data validation.
- **uv** for Python package management and virtual environment execution.
- **Rayon** for parallel processing in Rust.
- **zero-copy buffers** from Pandas (dense/sparse) and Polars via NumPy arrays.

## Critical Rules

### Always Use `uv`
- **All Python commands MUST use `uv run`** — never use `.venv/bin/python` or bare `python`.
- Tests: `uv run pytest tests/ -x -q`
- Benchmarks: `uv run pytest tests/test_benchmark.py -v -s`
- Type checking: `uv run pyright`
- Smoke test: `uv run python -c "import rusket; print('rusket imported successfully')"`

### Build Workflow
1. `cargo check` — fast compilation check for the Rust codebase.
2. `uv run maturin develop --release` — build optimized extension and inject it into the `.venv`.

### Pre-Commit Checklist
1. `cargo check` — no Rust errors.
2. `uv run maturin develop --release` — build extension.
3. `uv run pytest tests/ -x -q` — all tests pass (always run tests before commit).
4. `uv run pyright` — type checking passes.
*(Run e2e tests/benchmarks after implementing new features)*

### General Development Guidelines
- **File Size Limit**: No file should exceed 1000 lines of code.
- **Branching**: This project operates **exclusively on the `main` branch**. Do not create feature branches under any circumstances. All commits and pushes must go directly to `main`.
- **Temporary / scratch files**: Never drop throwaway files (e.g. `test_*.rs`, `scratch_*.py`) in the repo root or anywhere tracked by git. If you need a sandbox file while investigating something, put it inside `.temp/` — that directory is in `.gitignore` and will never be committed.

## YOLO (Main-Branch Workflow)

This project works directly on **`main`** — there is no PR/branch review step. **YOLO** means: `git add -A && git commit && git push origin main` **and** release a new version — all in one go.

Steps:
1. `git add -A`
2. `git commit -m "<message>"`
3. Bump version in `Cargo.toml` and `pyproject.toml`, then `git commit -am "release: vX.Y.Z"`
4. `git tag vX.Y.Z`
5. `git push origin main && git push origin vX.Y.Z`

CI takes it from there. 🚀

## Releasing a New Version

The CI automatically builds wheels for all platforms, generates a changelog, and publishes to PyPI when a **git tag** is pushed.

### Steps
1. **Bump version** in both `Cargo.toml` (under `[package]`) and `pyproject.toml` (under `[project]`).
2. **Commit** the version bump: `git commit -am "release: v0.X.Y"`
3. **Tag** the commit: `git tag v0.X.Y`
4. **Push** the tag: `git push origin main && git push origin v0.X.Y`
5. CI will:
   - Run tests on all defined Python versions.
   - Build wheels for all specified platforms.
   - Generate changelog from conventional commits (via `git-cliff`).
   - Publish to PyPI.
   - Create a GitHub Release with the changelog and wheel assets.

### Commit Convention
Use [Conventional Commits](https://www.conventionalcommits.org/) for automatic changelog categorization:

| Prefix | Category | Example |
|--------|----------|---------|
| `feat:` | 🚀 Features | `feat: support null values in sparse matrices` |
| `fix:` | 🐛 Bug Fixes | `fix: handle edge cases in confidence calculation` |
| `perf:` | ⚡ Performance | `perf: zero-copy conversion for PyArrow arrays` |
| `refactor:` | 🔧 Refactoring | `refactor: split association rules module` |
| `docs:` | 📖 Documentation | `docs: add polars usage to README` |
| `ci:` | 🔄 CI/CD | `ci: add python 3.12 target` |
| `chore:` | 📦 Miscellaneous | `chore: update dependencies` |
| `release:` | _(skipped)_ | `release: v0.1.1` |

The changelog is generated by `git-cliff` using `cliff.toml` config and appears as the GitHub Release body.

### Generate Changelog Locally
```bash
# Install git-cliff
cargo install git-cliff

# Preview changelog for unreleased changes
git cliff --unreleased

# Full changelog
git cliff
```

### Verify before releasing
```bash
cargo check
uv run maturin develop --release
uv run pytest tests/ -x -q
uv run pyright
```

## Class Hierarchy & API Conventions

rusket has two major class families: **Miners** and **Recommenders**. All share common patterns.

### Base Classes

```
BaseModel (ABC)
├── Miner ─── FPGrowth, Eclat, FIN, LCM, FPGrowth (+ RuleMinerMixin)
│             PrefixSpan, HUPM (specialized miners)
├── ImplicitRecommender ─── ALS, BPR, EASE, ItemKNN, SVD, LightGCN
├── SequentialRecommender ─── FPMC, SASRec
└── FM (standalone, uses explicit feature matrices)
```

### Common Patterns (all classes)

| Convention | Rule |
|---|---|
| `from_transactions(data, transaction_col, item_col, verbose, **kwargs)` | Class method on every class for DataFrame-based init |
| `from_pandas()`, `from_polars()`, `from_spark()` | Aliases that delegate to `from_transactions()` |
| `verbose: int` | `0` = silent, `1`+ = progress. **Never** use `bool`. Cast to `bool()` at Rust FFI boundaries |
| `seed: int` | Random seed parameter. **Never** use `random_state` |
| `fitted: bool` | Attribute set to `True` after `.fit()` or `.mine()` |
| `__repr__` | Every class must have a `__repr__` showing key hyperparameters |
| Docstrings | **NumPy-style** everywhere (`Parameters`, `Returns`, `Raises`) |

### Miner API

All miners inherit `Miner` + `RuleMinerMixin`. Common interface:

```python
miner = FPGrowth(data, min_support=0.05, use_colnames=True)
# or
miner = FPGrowth.from_transactions(df, transaction_col="tid", item_col="item")

freq = miner.mine()                        # → pd.DataFrame (support, itemsets)
rules = miner.association_rules()          # → pd.DataFrame (antecedents, consequents, metrics)
recs  = miner.recommend_for_cart(items)    # → list[Any] (cart-based recommendations)
```

- `recommend_for_cart()` is the primary method for cart recommendations
- `recommend_items()` on miners is **deprecated** (backward-compat alias for `recommend_for_cart`)

### Recommender API

All recommenders expose:

```python
model = ALS.from_transactions(df, user_col="user", item_col="item", factors=64)
# or
model = ALS(factors=64, seed=42)
model.fit(csr_matrix)

ids, scores = model.recommend_items(user_id=0, n=10, exclude_seen=True)
```

| Method | Available on |
|---|---|
| `fit(interactions)` | All recommenders |
| `recommend_items(user_id, n, exclude_seen)` | All recommenders |
| `recommend_users(item_id, n)` | Base default raises `NotImplementedError`; ALS, SVD implement it |
| `batch_recommend(n, exclude_seen, format)` | ALS, SVD |
| `user_factors` / `item_factors` | ALS, BPR, SVD, LightGCN (properties) |
| `similar_items(item_id, n)` | All `ImplicitRecommender` subclasses |
| `export_factors()` / `export_user_factors()` | All `ImplicitRecommender` subclasses |

### Sequential Recommenders (FPMC, SASRec)

These work on ordered sequences rather than a flat interaction matrix:

```python
model = SASRec.from_transactions(df, user_col="user", item_col="item", timestamp_col="ts")
ids, scores = model.recommend_items(user_id=0, n=10)
# SASRec also accepts raw sequences:
ids, scores = model.recommend_items([1, 2, 3], n=10)
```

### FM (standalone)

FM requires explicit feature matrices — `from_transactions()` raises `NotImplementedError`:

```python
model = FM(factors=8, seed=42)
model.fit(X_train, y_train)
probs = model.predict_proba(X_test)
```

## Architecture Notes
- **Rust Library Root**: `src/lib.rs`
- **Core FP-Growth mining**: `src/fpgrowth.rs` (builds the FP-Tree and mines itemsets)
- **Core Association Rules**: `src/association_rules.rs` (Rayon-parallelized metrics generation)
- **Python Thin Wrappers**: `rusket/fpgrowth.py`, `rusket/association_rules.py`
- **Type Stubs**: `rusket/_rusket.pyi`
- **Data Flow**: Python inputs (Pandas/Polars) are validated and converted into raw NumPy/CSR arrays (`fpgrowth_from_dense` or `fpgrowth_from_csr`) allowing Rust to process them without round-trips or Python loops.

## CI/CD Workflows (`.github/workflows/`)

### `ci.yml` — Tests, Builds & PyPI Release
- **Triggers**: push to `main`, tags (`v*`), PRs, manual dispatch.
- **Test matrix**: Python 3.10–3.14 (including free-threaded `3.13t`/`3.14t`).
- **Wheel builds** (linux, musllinux, windows, macos) only run on **tag pushes** or **workflow_dispatch**.
- **PyPI publish** uses `PYPI_TOKEN` secret (already configured in repo settings).
- **GitHub Release** is auto-created with a `git-cliff` changelog.

### `docs.yml` — Zensical → GitHub Pages
- **Triggers**: push to `main` (only `docs/**` or `zensical.toml` changes), manual dispatch.
- **Key**: uses `uv sync --all-extras --dev` (Zensical is in the `docs` extra and `dev` group), then `uv run zensical build`. The `site/` output is deployed via `peaceiris/actions-gh-pages`.
- **Published at**: `https://bmsuisse.github.io/rusket`

### Important CI/CD Rules
- **Never commit `site/`** — it's the Zensical build output and is in `.gitignore`. `peaceiris/actions-gh-pages` pushes it to the `gh-pages` branch automatically.
- **GitHub Pages must be explicitly enabled** — if the site is 404 after the first deploy, run: `gh api repos/bmsuisse/rusket/pages --method POST -f 'source[branch]=gh-pages' -f 'source[path]=/'`
- **All repo references must use `bmsuisse/rusket`** — the old name was `fpgrowth-pyo3`. Check `zensical.toml`, docs, and README if renaming.
- **Action versions**: keep `actions/checkout`, `actions/setup-python`, `astral-sh/setup-uv` in sync across both workflows.
- **uv consistency**: always use `uv run` — never `uv pip install --system` (deps won't be visible to `uv run`).
