DEV Community

Cover image for Write the Boring Scenarios First: A Spec Checklist Before Your AI Coder Writes C#
MICHAEL-MAURICE
MICHAEL-MAURICE

Posted on

Write the Boring Scenarios First: A Spec Checklist Before Your AI Coder Writes C#

The scenario I almost didn't write was the dull one: a customer tries a coupon on a 50.00 cart, gets rejected because the minimum is 100.00, then adds items and tries again at 150.00. Should the second attempt work? Obviously yes. But "obviously" is not in any prompt, and code that records a redemption before checking the rules will happily use up the coupon on the failed attempt.

That's the part of spec-driven development I want to focus on here. Not the tooling or the commands, just the spec itself: what does a set of acceptance scenarios need before you hand it to an AI coding agent, so the agent has nothing left to guess?

I worked through this on a small .NET 10 feature, coupon redemption at checkout, with the spec written in GitHub Spec Kit's file layout and every scenario running as an xUnit test (37 tests in total). Code below is trimmed to the shape; omitted parts are marked.

Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10. Complete, working, tested .NET projects with full source and PDF guides are inside Tech Skill Builder.

If you want the whole spec-driven workflow explained end to end, I also have a PDF guide on it: Spec Driven Ai Development ($10). More at the end.

The checklist

  1. Every scenario has an ID a test can find
  2. Every boundary is written from both sides
  3. The clock and the calendar are named
  4. Rounding is stated, once
  5. Failures say what they do NOT change
  6. Every rejection has a name
  7. No open question is left in the file

Here's why each one is on the list.

1. Every scenario has an ID a test can find

A scenario without an ID is a paragraph. A scenario with an ID like AS-2.3, citing requirements like FR-006, is something a test can be matched against. I keep each scenario as one row of data, Given / When / Then:

public sealed record Scenario(
    string Id, string[] Covers,                       // "AS-2.3", ["FR-006", "FR-009"]
    DateTimeOffset Now, PriorAttempt[] Before,        // Given
    string Customer, decimal Subtotal, string Code,   // When
    RedemptionResult Expected);                       // Then

// AcceptanceScenarios.All = [ ...one row per AS-x.y in spec.md... ]
// (all 14 rows in the complete project)
Enter fullscreen mode Exit fullscreen mode

Then a test reads spec.md itself and fails if the IDs in the file and the IDs in the test data drift apart:

[Fact]
public void Every_scenario_in_the_spec_has_a_test_case_and_vice_versa()
{
    // parse **AS-x.y** markers out of spec.md, compare with AcceptanceScenarios.All
    // (regexes and the FR coverage checks are in the complete project)
}
Enter fullscreen mode Exit fullscreen mode

That one test is what turns the spec from documentation into a contract. If the agent (or you) adds a scenario to the spec and forgets the test, the build goes red.

2. Every boundary is written from both sides

"Orders of 100.00 or more qualify" sounds complete. It isn't, until you write the two rows that pin the line:

**AS-2.3** Given a 10% coupon MIN100 with a 100.00 minimum, When it is applied to
99.99, Then it is rejected with BelowMinimumSubtotal.
**AS-2.4** Given the same coupon, When it is applied to exactly 100.00,
Then it is accepted with a 10.00 discount.
Enter fullscreen mode Exit fullscreen mode

The first row alone passes with > or >=. Only the pair catches the wrong one. I checked that by flipping >= to > in the implementation: exactly one test failed, AS-2.4, and everything else stayed green.

3. The clock and the calendar are named

"Valid until October 23" has three hidden questions: inclusive or not, in which time zone, and whose clock. My spec answers them in a clarification (inclusive, until the end of that date in the store's time zone, Africa/Cairo) and then pins it with another boundary pair:

  • 2026-10-23 20:59:59 UTC (23:59:59 in Cairo): accepted
  • 2026-10-23 21:00:00 UTC (midnight in Cairo): rejected as Expired

The constitution adds the rule that makes this testable at all: domain code reads time from an injected TimeProvider, never DateTime.UtcNow. That also lets a test cover daylight saving. Egypt leaves summer time on Oct 29, 2026, so a coupon valid until Oct 31 must still work at 21:59:59 UTC and be expired at 22:00 UTC that day. If the spec never names the time zone, no agent will write that test for you.

4. Rounding is stated, once

A 15% coupon on 10.30 is 1.545 off. Is that 1.54 or 1.55? In .NET, Math.Round(1.545m, 2) returns 1.54, because the default is round-half-to-even (banker's rounding). If the business expects 1.55, the spec has to say "2 decimals, midpoint away from zero", and the constitution says where rounding is allowed to happen: one Money.Round method, nowhere else.

public static decimal Round(decimal amount) =>
    decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
Enter fullscreen mode Exit fullscreen mode

That one is complete; it really is that small. The hard part was writing the sentence in the spec, not the code.

5. Failures say what they do NOT change

This is the scenario from the intro, and it's the one I see missing most often. Specs describe what success does. They rarely describe what a failure must leave untouched. My spec has two rules for that:

  • a rejection never changes the total (it equals the subtotal)
  • a failed attempt does not use up the coupon (AS-3.3: rejected at 50.00, then accepted at 150.00)

A domain test backs it up: two rejected attempts, zero entries in the redemption log; one valid attempt, one entry. And because "once per customer" is also a concurrency rule, another test fires 50 parallel redemptions for the same customer and expects exactly one applied and 49 AlreadyRedeemed.

6. Every rejection has a name

"Return an error if the coupon can't be used" lets the agent pick one generic failure. Naming the reasons (UnknownCode, Expired, BelowMinimumSubtotal, AlreadyRedeemed) forces two useful decisions: the UI can tell the customer what actually happened, and someone has to decide the order of the checks (an expired coupon on a small cart: which reason wins?). The plan fixes that order; the spec only has to demand a specific reason.

7. No open question is left in the file

Spec Kit's template lets a first draft mark gaps with [NEEDS CLARIFICATION: ...], and its clarify step asks about them and writes the answers back into the spec. The cheap trick is to make leftovers fail the build:

[Fact]
public void Spec_has_no_open_clarifications() =>
    Assert.DoesNotContain("[NEEDS CLARIFICATION", Spec);
// Spec = spec.md copied to the test output (setup in the complete project)
Enter fullscreen mode Exit fullscreen mode

Does the checklist actually catch anything?

To find out, I kept a second, "just prompted" implementation next to the real one. To be clear, I wrote it by hand to mirror what a one-sentence prompt leaves open (exact string match, UTC dates, >, default rounding, no cap, no clamp, no redemption log); it is not captured AI output. Run against the same 14 scenarios, it missed 7 of them. Every miss is a decision the one-sentence prompt never made: case and spaces in the code, rounding, the cap, a negative total, the minimum boundary, the time zone, and the second redemption. Three of them (boundary, time zone, rounding) are exactly the items from this checklist that the clarification step answered.

The implementation written from the finished spec passed all 14 scenarios on its first test run. The full suite is 37 tests: 14 acceptance scenarios, 5 traceability checks, 2 comparison tests and 16 domain tests, on .NET SDK 10.0.401 with xUnit v3, with -warnaserror on and 0 warnings.

What I didn't verify

I wrote the spec, plan and tasks by hand in Spec Kit's layout and template format. I did not run the Specify CLI or an AI agent through the commands for this example. I also didn't test the Africa/Cairo time zone lookup on Windows, or any persistence beyond in-memory stores.

Short version

  • [ ] Every scenario has an ID, and a test fails if spec and tests drift
  • [ ] Every "at least", "until" and "up to" has a row on each side of the line
  • [ ] The spec names the time zone, and the code reads an injected clock
  • [ ] Rounding mode is written down, and lives in one method
  • [ ] Failures state what they leave unchanged
  • [ ] Every rejection reason has a name
  • [ ] [NEEDS CLARIFICATION] can't survive into a green build

Go further

If you want the full spec-driven workflow (Constitution, Specify, Clarify, Plan, Tasks, Analyze, Implement) explained step by step, Spec Driven Ai Development is my $10 PDF guide on it, with review checklists, reusable prompt patterns and guidance for existing projects. It's an independent guide, not an official GitHub publication, and the coupon example from this post isn't part of it.

And if you learn best from complete code, Tech Skill Builder is where I put complete, working, tested .NET + AI projects with full source code and step-by-step PDF guides, with new ones added regularly. Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10. Join Tech Skill Builder

Which "obvious" scenario did your AI coder get wrong because nobody wrote it down?

Top comments (0)