-
Notifications
You must be signed in to change notification settings - Fork 0
Core Concepts
The ideas Feature is built on, spelled out. If the Language Guide is how to write a spec, this page is why the language is shaped the way it is and what the toolchain guarantees in return.
Traditional assertions check what you thought to check. A prediction declares the complete observable footprint of an operation across every configured surface — and the test fails in both directions:
- Missing — you predicted it, it didn't happen.
- Unpredicted — it happened, you didn't predict it.
Worked example. Given this prediction:
predict success:
response 201 FlowCreatedResponse
eventStore has [ FlowCreated with FlowCreatedEvent ]
projectionStore has []
eventBroker has []
…an implementation that also writes a FlowAuditLogged event fails with:
SPEC-AUT-001 › "successful flow creation" › eventStore[1]: UNPREDICTED record FlowAuditLogged
That failure is the point. Either the audit write is intended — then the spec gains a line and the behavior becomes contractual — or it isn't, and a stray side effect just got caught before it shipped. Nothing observable is allowed to be unspecified. Three rules make this airtight:
-
Every configured service appears in every prediction (
INCOMPLETE_PREDICTIONotherwise).has []is a real assertion — "nothing happens here" — not an omission. - Rejections assert zero effects everywhere. A business rule that refuses must leave no trace on any surface.
-
Queries cannot express writes at all (
QUERY_SIDE_EFFECTis a parse error). Side-effect freedom for reads is grammar, not team discipline.
A spec serves two readers with opposite needs. The builder (an AI agent or a person) needs expressive instructions; the compiler needs unambiguous declarations. One file, two zones:
| Agent zone | Compiler zone | |
|---|---|---|
| Sections |
construct:, enforce:
|
contract:, scenario, predict
|
| Grammar | Freeform natural language; recognized keywords add structure; everything else is captured verbatim | Strict and typed; malformation is a parse error |
| Can it fail to parse? | Never — any instruction is valid | Yes — loudly, with a hint |
The boundary is the design. Instructions can say anything ("never call the payment provider
synchronously"); measurements can say only things a machine can check. A Then clause in prose
needs a human to interpret it — that interpretation step is where every spec-to-test approach
before this one leaked. Here there is no interpretation step: the compiler zone is the
assertion set.
Nothing in a spec references anything undeclared. Each space has a declaration site and a parse-time error:
| Space | Declared in | Error |
|---|---|---|
Service keys (eventStore has …) |
config services
|
UNKNOWN_SERVICE_KEY |
Schema names (with FlowCreatedEvent) |
the spec's contract: block |
UNDECLARED_SCHEMA |
Command names (when: CreateFlow) |
config response.commands
|
UNKNOWN_COMMAND |
Actor names ((as admin)) |
config response.actors
|
UNKNOWN_ACTOR |
Every error lists the valid names. The effect is type safety for specifications: typos,
renamed schemas, and phantom services die at feat parse, not in a 3 a.m. test run. It also
makes specs reviewable — a reader can trace every identifier to its declaration.
The spec is a two-party contract: the author states what must exist; the builder implements exactly that. Four mechanisms hold both sides honest:
-
Lifecycle —
draft → agreed → built → verified.agreedis the handshake: nothing is built before it. Tooling — never a human — flipsbuilt(suite green) andverified(green under generated tests in CI). Editing an agreed spec demotes it: changed contract, renewed handshake. -
Change boundaries —
touches <glob>declares the file footprint before building, and the build may not exceed it. Prediction inversion for the working tree. -
Rejection traceability — every rejection a scenario predicts must be justified by a
rejects <ID> when <reason>instruction, and every justification must be tested. Checked in both directions byfeat audit; untested rules and unexplained tests are both findings. - The ambiguity protocol — a builder that finds an instruction ambiguous halts and asks. It never guesses, never "interprets," never edits the spec to match what it built. Ambiguity count per spec is a quality signal for the spec itself; the target is zero.
The entire CI story rests on one property: same inputs, same bytes.
| Stage | Guarantee |
|---|---|
| parse | same spec + config → identical IR |
| derive | same IR + config → identical topology |
| emit | same topology + schemas → byte-identical test file |
To keep emit byte-stable, generated files contain no timestamps and no environment residue;
the header carries a sha256 of the inputs (spec text + contracts + the derivation-relevant config
slice) plus the language and emitter versions. feat verify is therefore a plain byte
comparison — cheap enough to run on every push, strict enough that any drift between specs and
committed tests fails the build. Upgrading the emitter intentionally changes the hash: a
toolchain upgrade forces a regenerate, which is correct, because a new emitter may change output.
Every test executes the same choreography:
reset adapters (fresh namespaces)
│
├─ given: execute / seed / clock ← BEFORE the window; effects never captured
│
├─ window opens
│ stimulus: when-command or deliver-events (delivered events excluded from capture)
│ …one shared wait for the scenario's eventual-consistency services…
│ capture sweep across every service
├─ window closes
│
└─ diff: captured reality vs prediction (response, records, state)
Two decisions worth knowing:
-
Preconditions are input, not output.
execute/seedeffects land before measurement so the prediction covers exactly the operation under test. -
One shared wait, not per-service waits. The runtime waits once for the longest
convergenceTimeoutamong the scenario's eventual services, then sweeps. Absence proofs (has []on an eventual service) ride the same wait;absenceTimeoutcan shorten it as a tuning knob for rejection-heavy suites.
Consistency models set capture timing per service: acid (immediate), strong
(post-replication), eventual (after the shared wait) — and set the default ordering semantics
(acid/strong ordered, eventual multiset).
Scenario names are unique per spec and become failure coordinates. Every assertion the runtime makes carries one:
SPEC-USR-002 › "unknown users are rejected" › row[2] › response › code:
expected "UNKNOWN_USER", got "NOT_FOUND"
Spec ID → scenario → outline row → surface → field. A red test is a pointer into the contract, not a stack trace to reverse-engineer. This is what makes the red→fix loop precise for a human and tractable for an agent.
equals fixture "<path>" asserts deep structural equality against a JSON document — validated
against the position's schema at compile time (type-safe goldens) and inlined into the generated
test (self-containment). Use it when the whole document is the assertion.
The repository applies the same idea to the language itself: corpus/ pairs each exemplar
.feat with its expected IR, and the parser's own spec asserts every pairing as a golden. The
corpus is simultaneously documentation, test data, and the conformance suite any future parser
must pass. The discipline that keeps it honest: a language change is incomplete until the
reference, a corpus exemplar, the implementation, and a full corpus re-validation land
together. That rule was earned — twice, features were documented but unexercised, and both
gaps surfaced only when something finally consumed them.
Generated suites are written next to their specs and committed, not produced at build time.
Three reasons: feat verify needs a committed artifact to compare against; reviewers see exactly
what will be asserted; and the test file is a durable record tying a spec version to its
assertions. Generated files are self-contained — schemas, goldens, and seed data inlined, one
runtime import — so they survive refactors of everything around them and run anywhere the
runner runs. The corollary: never edit one. Change the spec; regenerate.