chaffchaff

Failure modes

The ugly paths, named on purpose (SPEC §9): what chaff does when an input is bad, ambiguous, or missing, rather than guessing quietly.

  • A declared entry is missing or unreadable

    Exit 2, no score printed. A partial score is a wrong score, so chaff would rather tell you nothing than tell you a number computed from an incomplete set.

  • A glob matches nothing

    A warning, with the pattern echoed back, not an error. An empty .claude/rules/ directory is normal, not a mistake.

  • Unparseable markdown or invalid YAML frontmatter

    That entry becomes kind: unknown, excluded from the score denominator, and listed under “not analyzed” with the reason stated.

  • Unrecognized JSON shape in a tool-defs entry

    Skipped, with the sniffer’s reason printed. Never guessed.

  • An @-import target is missing, cyclic, or past 4 hops

    The matching import.* structural finding fires. A cycle is broken at the first repeat; each file is counted once regardless of how many times it is imported.

  • An @-import sits inside a code span or fenced code block

    Not an import. Matches Claude Code’s own documented behavior. Pinned by fixture both ways.

  • An @-import resolves outside the set root

    Resolved and materialized, but marked external: true and counted separately from the resident total. Claude Code gates these behind an approval dialog, so they may not actually be resident in practice.

  • A date-relative rule runs with no --today

    Uses the system date and prints it. Committed fixtures always pin --today so their output stays reproducible forever.

  • The calibration manifest is missing the requested model

    The Claude-count band is omitted; ref tokens still print. A warning names the manifest date. chaff never extrapolates a ratio it has not measured.

  • --tokenizer anthropic is requested without a key, offline, or rate-limited

    Falls back to o200k_base, with a printed downgrade line and a non-zero warning count. Never a silent substitution.

  • The set exceeds 5 MB or 500 entries

    Hard stop, exit 4. This is a context set, not a codebase.

  • A file is too large for the browser Worker

    Per-file 2 MB cap, the message names the file. The page never freezes the main thread while it decides.

  • Same input, different OS (CRLF/BOM/path separators)

    Identical findings and identical byte offsets after normalization, pinned by the determinism eval.

  • A measured rule's evidence goes stale

    CI fails. chaff never silently keeps claiming a behavioral delta it can no longer support with a fresh run.

Exit codes

Load-bearing for CI. 1 and 3 look similar (both mean “something is wrong”) but are deliberately distinct: different owner, different fix.

CodeMeaningWho fixes it
0Clean. No findings at or above the gate.Nobody. Nothing to do.
1Findings at/above the gate (--fail-on, default enforced).Whoever owns the context file: fix the finding, or ignore/fail-on it deliberately if it's a false positive.
2Input/parse failure. A declared entry is missing or unreadable.Whoever owns the config or context files: fix the path or the file.
3Resident-token budget exceeded.Whoever owns the budget: move content on-demand, or raise the budget deliberately.
4Usage/config error: bad flag, invalid config, a budget with no model named, an unknown rule id.Whoever invoked chaff: fix the CLI call or the config.
5Internal error. A bug in chaff itself.chaff’s maintainer. Please report it.

Cost safety

The product itself makes zero model calls at any traffic level and has zero server functions, so the abuse-case numbers are zero and there is no rate limiter to design. The repo’s eval tooling (the admission pipeline) is the only key path in the whole codebase, and it is local-only with per-run and program-wide USD caps that never run in CI.