# Developing `ski`
How to set up, build, lint, and test the `ski` crate (the engine behind
skill-inject). For the design, see [PLAN.md](./PLAN.md); for a usage overview, see
[README.md](./README.md).
## 1. Toolchain
`ski` targets stable Rust (developed on 1.96). Install via [rustup](https://rustup.rs)
and add the lint components:
```sh
rustup component add clippy rustfmt
```
`pre-commit` runs the lint/test gate (see §5):
```sh
pipx install pre-commit # or: uv tool install pre-commit / pip install pre-commit
```
Everything below runs from the repo root (the crate dir).
## 2. Layout
```
src/
main.rs # clap CLI: index / why / hook / observe / session-start
lib.rs # module wiring
observe.rs # PostToolUse: record model-loaded skills (Read SKILL.md / Skill)
session_start.rs # SessionStart: incremental reindex + re-arm on compaction
config.rs # Config + defaults (model, thresholds, roots)
text.rs # tokenize + FNV hashes (deterministic — see "Determinism")
skill.rs # SKILL.md discovery + frontmatter parse
embed/
mod.rs # Embedder trait + EmbedKind + build() backend selector
bow.rs # offline bag-of-words backend (`--no-default-features`)
fast.rs # fastembed (bge/MiniLM) backend — the default `fastembed` feature
index.rs # persisted, incrementally-refreshed embedding index
rank.rs # cosine + keyword hybrid scoring; select() guardrails
tests/
golden.rs # prompt -> expected skill, against the real repo skills
```
## 3. Build & run
```sh
cargo build --release # default: real embedder + reranker (model downloads once)
cargo run -- index # build index at $XDG_DATA_HOME/ski/index.json
cargo run -- why "set up pre-commit hooks" --top 5 # rank + scores (tuning aid)
```
Offline bag-of-words build (no deps, no model/network):
```sh
cargo build --no-default-features
```
## 4. Test
```sh
cargo test --no-default-features # unit + golden tests (offline, network-free)
cargo test # default: same, against the real bge model (network on first run)
```
- **Unit tests** live next to the code (`#[cfg(test)] mod tests`): tokenizer,
hashing, bow determinism/normalization, cosine, keyword scoring, frontmatter.
- **Golden tests** (`tests/golden.rs`) discover the self-contained fixture skills
under `tests/fixtures/skills/` and assert the top-ranked skill for representative
prompts (e.g. `"bootstrap a new python project with uv"` → `uv-setup`). Run them with
`--no-default-features` to use the offline bag-of-words backend (no model, network-free,
deterministic); they depend on nothing outside this repo. When you add or rename a
fixture, update the golden cases; use
`cargo run -- why "<prompt>"` to pick prompts and read the scores.
- **Live compliance probe** (`scripts/probe-compliance.py`) — the one thing no
offline eval can answer: given ski's injection, does a *real* host actually
invoke the skill? Runs `claude -p` A/B/C (no injection / directive / directive
+ evidence clause) against a throwaway uv project, N runs each, and scores the
`Skill` tool_use in the transcripts. Costs real model calls; results to date
are recorded in `src/inject.rs` (`directive_block` docs). Re-run it whenever
the directive wording changes — the wording is tuned against this probe, not
the ranking evals.
## 5. Lint & format
```sh
cargo fmt --all # apply formatting
cargo fmt --all -- --check # CI check (no changes)
cargo clippy --all-targets -- -D warnings
```
Install the git hooks so this runs automatically on commit (config lives at the repo
root, scoped to `*.rs`):
```sh
pre-commit install
pre-commit run --all-files # run on demand
```
The hooks are `ski-fmt`, `ski-clippy`, `ski-test`. They pin `--no-default-features`
(offline, no model download); run the default `fastembed` lane separately in CI.
## 6. Adding an embedding backend
1. Implement `embed::Embedder` (`id()` + `embed(texts, kind)`); honor `EmbedKind`
if the model is asymmetric (bge prefixes queries, not documents).
2. Register it in `embed::build()` so a config `model` id selects it.
3. Pick a unique `id()` — the index is tagged with it, so a change forces a full,
automatic reindex. Don't reuse another backend's tag.
## 7. Determinism (important)
Persisted embeddings and content hashes must reproduce byte-for-byte across runs
and machines. Use the fixed FNV hashes in `text.rs` — **never** `std`'s
`DefaultHasher`/`RandomState` (seeded/unspecified) for anything that lands in the
index or on disk.
## 8. Conventions
- Keep an offline, dependency-light lane behind `--no-default-features` (bag-of-words,
no ONNX/network) — it's what the tests and pre-commit hooks run. Heavy deps (ONNX) stay
gated behind the `fastembed` feature, which is now on by default.
- Fail open on the hook path: a ranking error must never block the user's prompt.
- Match the surrounding style; `cargo fmt` + clippy `-D warnings` is the bar.