# Contributing to Ktav

## Core rules

### 1. Every bug fix ships with a regression test

When you find a bug, **before fixing it**, write a test that reproduces
it — the test **must fail on `main`** and pass after the fix. Include
both in the same PR.

The test should live near related tests (e.g. in
`tests/edge_cases/<topic>.rs` or `tests/ser/<topic>.rs`). A short
comment on the test explains the failure mode so a reader ten months
from now understands *why* the case matters.

Why: silent regressions are the deadliest thing that can happen to a
library shipped to many users. A documented trip-wire costs nothing
long-term.

### 2. Performance-sensitive changes include before/after numbers

If a PR touches any of:

- `src/parser/` / `src/thin/parser.rs` — parsing hot path
- `src/ser/text_serializer.rs` / `src/render/` — serialization hot path
- `src/thin/deserializer.rs` / `src/de/` — deserialization hot path
- `src/value/` — the dynamic value type

… include a block in the PR description with **before/after** criterion
numbers for the affected benches:

```
parse_to_struct/100_upstreams_typed
  before: 275 µs
  after:  198 µs
  change: -28%
```

The goal isn't to be fastest at any cost — it's to make changes
*accountable*. A 5 % regression is fine if it buys clarity or
correctness, but it should be visible and justified.

Use the convenience script:

```
./bench.sh                # quick run (warmup 1s, measurement 2s, 20 samples)
./bench.sh full           # criterion defaults — longer, more accurate
./bench.sh parse          # filter: only parse benches
./bench.sh render         # filter: only render benches
./bench.sh "parse|render" # any criterion regex
```

Criterion stores the last run's numbers in `target/criterion/` and
automatically diffs against them on the next run, so you'll see
`change: +/- X %` in the output.

### 3. Public API changes note compatibility

If you touch anything under `pub` in `lib.rs`, in the PR description
say whether it's:

- **semver-compatible** (additions, looser bounds, doc changes); or
- **semver-breaking** (renamed / removed items, changed signatures,
  tightened bounds) — in which case the version bump goes into the
  next `MINOR` while we're pre-1.0.

Update `CHANGELOG.md` in the same PR under `## [Unreleased]`.

### 4. One concept per commit

Commits should be atomic: a bug fix and its test together, a new
feature and its tests together. A rename belongs in its own commit. A
refactor that happens to fix a bug should probably be two commits.

`git log --oneline` reads like a changelog. Write it that way.

## Getting the code

The spec conformance suite lives in the `spec/` git submodule
([`ktav-lang/spec`](https://github.com/ktav-lang/spec)). Clone with
submodules so `cargo test` can run it:

```
git clone --recurse-submodules https://github.com/ktav-lang/rust
```

If you already cloned without `--recurse-submodules`:

```
git submodule update --init
```

## Running tests

```
cargo test                         # all tests (includes spec conformance)
cargo test --test spec_conformance # language-agnostic conformance suite only
cargo test --test edge_cases       # one category
cargo test multiline               # by name filter
cargo test --doc                   # doc-tests only
```

## Pre-push local checks (mandatory)

Run **all four** of these locally before `git push`. CI runs the same
commands and a failure mid-pipeline costs more than a few seconds at
the keyboard — and in the case of a tag push that triggers a release,
a fix-and-force-tag-move dance.

```
cargo fmt --all -- --check          # canonical formatting
cargo clippy --release -- -D warnings  # zero warnings policy
cargo test --release                # full suite, release mode
cargo build --release               # final binary build
```

If any of these are red, the push is **not** ready. The CI in this
repo enforces all four (see `.github/workflows/ci.yml`); pushing a
broken commit guarantees a red main badge and, for tag pushes,
breaks the release pipeline.

This rule applies repo-wide, not just to this crate — every
`ktav-lang/*` binding has its own equivalent local-check incantation
documented in its own `CONTRIBUTING.md`.

## Releasing

A release is **fully automated** for this crate — push a `v*` tag and
the `Release` workflow handles everything:

```
git tag -a v0.1.X -m "v0.1.X"
git push origin v0.1.X
```

That triggers three sequential jobs in `.github/workflows/release.yml`:

1. **`verify`** — re-runs the four pre-push checks above (fmt /
   clippy / test / build) on the tagged commit. No green here, no
   publish.
2. **`publish`** — `cargo publish` with the `CARGO_REGISTRY_TOKEN`
   secret (gated to the `crates-io` environment). Confirms the tag
   matches `Cargo.toml` version before uploading.
3. **`github-release`** — creates a GitHub Release for the tag and
   pulls the matching `## [vX.Y.Z]` section out of `CHANGELOG.md` for
   the release notes. So **the CHANGELOG entry IS the release notes**:
   write the changelog before tagging.

What this means in practice:

- **Always write the CHANGELOG entry before tagging.** Skipping it
  leaves the GH Release with empty notes.
- **Never `cargo publish` from a maintainer's machine** — the
  workflow is the canonical path. (Manual publish would skip the
  verify gate and produce no GH Release.)
- If you need to retry a release without moving the tag, use
  `workflow_dispatch` with the tag name as the input.

Test categories:

- `src/**/tests.rs` — private unit tests per module.
- `tests/de/*` — deserialization by feature.
- `tests/ser/*` — serialization by feature.
- `tests/roundtrip/*` — round-trip (`T → text → T`).
- `tests/edge_cases/*` — combinatorial edge cases (paren literals,
  keywords in maps, deep nesting, special strings…).
- `tests/fixtures.rs` — end-to-end against real `.conf` files.
- `tests/spec_conformance.rs` — language-agnostic suite from
  `ktav-lang/spec` (valid fixtures match JSON oracle; invalid fixtures
  are rejected; valid fixtures survive lossless round-trip).

## Benchmarks

Source: `benches/parse.rs` (criterion). Scenarios cover:

- `parse_to_value` — raw parse into `Value` (the owned, public tree).
- `parse_to_struct` — parse via the thin path into a typed struct.
- `render` — serialize a typed struct to text.
- `roundtrip` — parse + render.
- `multiline_dedent` — multi-line string parsing under varying line
  counts.

## Code layout guide

Decomposition rule: **one exported item per file**. Private helpers
live with the type that uses them. Folders group closely-related
items (all of `src/thin/` is the zero-copy deserialization path).

```
src/
├── lib.rs                         public entry points
├── value/                         owned Value enum (public)
├── parser/                        Value-building parser
├── render/                        Value → text
├── thin/                          zero-copy de path (ThinValue → T)
├── ser/                           T → Value (public) + T → text (direct)
├── de/                            Value → T (via ValueDeserializer)
└── error/                         Error + serde impls
```

## Philosophy (what not to do)

Ktav's motto is "be the config's friend, not its examiner". Before
proposing a new feature, ask:

- Does this add a new rule the reader must hold in their head?
- Can a line still be understood without its neighbours?

New rules are always costly. Reject everything that doesn't pass those
two checks.
