nattydate 0.1.7

Lightweight, deterministic natural language date/time preprocessor — no ML, no clock fragility.
Documentation
# Contributing to NattyDate

Thanks for considering a contribution. NattyDate is intentionally small and deterministic — every change should preserve that character.

---

## Quick Start

```bash
git clone https://github.com/troy74/nattydate.git
cd nattydate
cargo build
cargo test          # runs all 104 test cases
cargo run -- test --verbose
```

Minimum Rust version: **1.85** (Rust 2024 edition). Use `rustup update stable` if `cargo build` fails.

---

## Project Layout

```
src/
  lib.rs      — full parser: normalize, tokenize, classify, score, resolve, format
  main.rs     — CLI wrapper + test runner
tests.json    — 104-case test suite with fixed mock_now
SKILL.md      — user-facing quick-reference
CLAUDE.md     — codebase guide for AI agents
AGENT.md      — same as CLAUDE.md
```

---

## How to Add a Dictionary Word

All recognized words live in `get_dict()` in `src/lib.rs`. Each entry is a `(&str, KnownToken)` pair.

**Example — adding "forthcoming" as a synonym for `Next`:**

```rust
("forthcoming", KnownToken::Modifier(Modifier::Next)),
```

Entries are matched case-insensitively (input is lowercased before lookup). Fuzzy matching handles minor misspellings automatically; explicit aliases like `"tmrw"` are for common abbreviations the fuzzy engine might not score high enough.

Add a corresponding test case in `tests.json` before opening the PR.

---

## How to Add a Holiday

1. Add a variant to the `Holiday` enum in `src/lib.rs`.
2. Add a lookup entry in `get_dict()`:
   ```rust
   ("anzacday", KnownToken::Holiday(Holiday::AnzacDay)),
   ```
3. Add phrase normalization in `normalize()` if needed:
   ```rust
   ("anzac day", "anzacday"),
   ```
4. Add a resolution arm in `resolve_holiday()`:
   ```rust
   Holiday::AnzacDay => NaiveDate::from_ymd_opt(year, 4, 25).unwrap(),
   ```
5. Add at least one test case in `tests.json`.

---

## How to Add a New Modifier Behaviour

Modifier resolution lives in `resolve()` in `src/lib.rs`. The `Weekday` arm is the primary place modifiers are consumed. `preprocess_ago_patterns()` handles the `N unit ago` pattern before the main loop.

To add a new structural pattern (e.g. `"in N days"`):
- Add the keyword to the dict if it's not already there
- Either handle it in `preprocess_ago_patterns()` or add a new pre-pass

---

## Scoring Constants

All magic numbers are named constants at the top of `src/lib.rs`. Before changing any threshold, run the full test suite and check that no cases regress. The `THRESHOLD_MARGIN` value (`0.09` rather than `0.10`) exists specifically to survive `f32` rounding — do not raise it above `0.099`.

---

## Test Cases

All changes that affect parsing behaviour **must** be accompanied by a test case in `tests.json`. Format:

```json
{ "input": "the string to parse", "expected": "formatted output", "format": "YYYY-MM-DD" }
```

The `mock_now` at the top of the file is `2026-03-18` (a Wednesday). Use this when computing expected values for relative expressions.

Run the suite:

```bash
cargo test                           # via Rust unit test
cargo run -- test --verbose          # via CLI runner
cargo run -- test --test-file path   # custom file
```

The CLI runner exits with code `1` on failure, so it is safe to use in CI.

---

## Code Style

- `cargo fmt` — enforced in CI
- `cargo clippy -- -D warnings` — enforced in CI
- Keep functions focused; the pipeline stages (`normalize`, `tokenize`, `classify`, `resolve`, `format`) should remain distinct
- Avoid adding dependencies without discussion — the small dep tree is intentional

---

## Pull Request Checklist

- [ ] `cargo fmt` passes
- [ ] `cargo clippy -- -D warnings` passes
- [ ] `cargo test` passes (all 104 cases)
- [ ] New behaviour is covered by at least one test case in `tests.json`
- [ ] CHANGELOG.md entry added under `[Unreleased]`
- [ ] Public API changes reflected in README.md and SKILL.md

---

## Licensing

By submitting a pull request you agree that your contribution is licensed under the same terms as NattyDate: **MIT OR Apache-2.0**.