honest framework

honest-gherkin

A feature file is executable, and stays honest: steps fold over an immutable context, so no scenario passes on another's residue.

You know pytest-bdd. honest-gherkin keeps the feature files but threads context immutably — no module-level state leaking between scenarios.

You write a feature file: *Given a cart with two items, When I remove one, Then the cart shows one item.* It reads beautifully in the planning doc. Then the step definitions accumulate their own world — a module-level context object each step mutates — and by the time three scenarios have run, step seven is passing because step two in an *earlier* scenario left something behind. The prose says one thing; the shared mutable state underneath says another. The feature file has become decoration.

honest-gherkin makes the feature file executable, and keeps it honest by refusing the shared mutable world. Parse a feature, compile its step patterns, match each step against a registry, then fold the scenario over an immutable context: each step takes the context and returns the next one. There is no context object for a stray step to corrupt, so a scenario cannot pass on residue from the one before it.

Steps are data; the run is a fold

A step definition is a pattern plus a function from context to context. Matching a step to its definition is a lookup in a registry, not a scan of decorators with side effects. Running a scenario is a fold: the immutable context threads through the matched steps in order, and the result is a report — passed, failed, with the failing step named. A fault is a value in that report, not an exception thrown from deep in a step body.

The whole engine is pure but for a single I/O boundary: run_feature_file reads the file and the CLI prints the report. Everything between — parse, compile, match, fold — is a function of its inputs.

Why the immutable fold is a poka-yoke

Shared mutable context between steps is the one bug BDD frameworks reliably ship: order-dependent scenarios that pass in isolation and fail in a suite, or fail alone and pass in a suite, depending on what ran first. The cause is a world any step can write. Remove the shared world — thread the context as an immutable value — and the category is gone. A scenario's result depends only on its own steps, so "it passed because a different scenario ran first" cannot happen.

honest-test runs on honest-gherkin: the framework's own behavioural specifications are feature files, executed through this engine, so the same discipline that gates the modules gates their descriptions.