Anatomy of a Good Spec: Acceptance Criteria, Edge Cases, Constraints
A spec is not documentation you write after the fact. It is the input you give an agent before any code exists, and it is the checklist you grade the result against afterward. A good spec is short, specific, and verifiable. A bad spec is either a vague wish or a 2,000-word essay the agent skims and ignores.
This lesson gives you a repeatable structure so you never face a blank page. Every spec you write for the rest of this course will use the same parts.
What You'll Learn
- The six parts of a complete spec
- How to write acceptance criteria that a test could check
- How to surface edge cases before the agent guesses them
- How to state constraints that fence the agent in
- A reusable template you can paste in front of any agent
The six parts of a spec
A complete spec answers six questions. You will not always need every part in full, but you should consciously consider each one.
- A complete spec
- Outcome — what done looks like
- Acceptance criteria — checkable statements
- Edge cases — the inputs that break naive code
- Constraints — what the agent may NOT do
- Prior decisions — context the agent can't infer
- Verification — how you'll prove it's correct
The first three define what to build. The next two fence in how. The last one is your contract for checking the result, which the verification lesson builds on directly.
Acceptance criteria: the checkable core
Acceptance criteria are the load-bearing part of a spec. Each one is a single statement that is unambiguously true or false about the finished code. If you cannot imagine writing a test for a criterion, it is not yet a criterion — it is still a wish.
Compare these:
| Wish (not checkable) | Acceptance criterion (checkable) |
|---|---|
| "Handle invalid input gracefully" | "A negative quantity returns a 400 with { error: 'quantity_must_be_positive' }" |
| "Make it fast" | "A lookup of 10,000 records returns in under 50ms on the test fixture" |
| "Format the date nicely" | "Dates render as YYYY-MM-DD in UTC, regardless of server timezone" |
The pattern: name the input, name the expected output or behavior, and leave no adjective undefined. "Gracefully", "nicely", and "fast" are adjectives the agent will define for you, usually not the way you meant.
A good test of a criterion: read it out loud and ask "could two reasonable engineers disagree about whether the code satisfies this?" If yes, tighten it.
Edge cases: name them before the agent guesses
Edge cases are the inputs that distinguish correct code from code that merely works on the happy path. The agent will handle the happy path almost every time. It is the empty list, the zero, the duplicate, the simultaneous request, and the malformed input where intent and guess diverge.
You do not need to enumerate every theoretical input. You need to name the ones where you have an opinion about the right behavior. Useful prompts to surface them:
- What happens with empty, null, or missing input?
- What happens at the boundary (zero, the maximum, the exact limit)?
- What happens with duplicates or repeated calls?
- What happens when two things happen at once?
- What is the failure mode, and is it an error, a default, or a silent no-op?
If you state the behavior, the agent implements it. If you stay silent, the agent picks, and you find out in production.
Constraints: fence the agent in
Constraints are the negative space of the spec. They tell the agent what it may not do. Agents are eager and will happily add a caching layer, pull in a new dependency, or refactor a neighboring file you never mentioned. Constraints keep the change scoped.
Common constraints worth stating explicitly:
- Dependencies: "No new packages" or "use only the standard library."
- Files: "Only modify
cart.ts; do not touch other modules." - Patterns: "Match the existing error-handling style in this file."
- Scope: "Do not add features beyond the criteria above. No speculative abstractions."
That last one matters more than it looks. Eager agents tend to over-engineer, adding flexibility for futures that may never arrive. A one-line "do the simplest thing that satisfies the criteria" constraint saves a lot of cleanup.
Prior decisions: context the agent cannot infer
The agent can read your code, but it cannot read your history. Prior decisions are the facts that already constrain the solution and would otherwise look arbitrary: "We use snake_case for API fields", "Auth tokens live in an httpOnly cookie, never localStorage", "This service must stay synchronous; the queue migration is a separate project."
State the decision, and ideally the one-line reason, so the agent connects the task to the real intent instead of inferring its own.
A reusable template
Paste this in front of any agent and fill the blanks. The headings do the heavy lifting — they prompt you to think about each part.
## Task
<one sentence: what done looks like>
## Acceptance criteria
- <input> produces <output/behavior>
- <input> produces <output/behavior>
## Edge cases
- <boundary or unusual input> -> <expected behavior>
## Constraints
- <what NOT to do: deps, files, scope, patterns>
## Prior decisions
- <fact the agent can't infer, + one-line reason>
## Verification
- <how I'll prove this is correct: tests, command to run>
Keep it tight. A spec for a single function might be eight lines. A spec for a feature might be thirty. If it is creeping past a page, you are probably specifying several features at once — split them, and run the loop on each.
Worked example
Task: add a discount code to a shopping cart.
## Task
Apply a percentage discount code to the cart total.
## Acceptance criteria
- applyCode("SAVE10") reduces the subtotal by 10% and returns the new total.
- An unknown code returns the unchanged total and { applied: false }.
- A valid code returns { applied: true, code, percentOff }.
## Edge cases
- Empty string or whitespace code -> treated as unknown, total unchanged.
- Code is case-insensitive ("save10" == "SAVE10").
- Only one code may be active; applying a second replaces the first.
## Constraints
- Only modify cart.ts. No new dependencies.
- Discount applies to subtotal before tax, not after.
## Prior decisions
- Codes and their percentOff live in the existing CODES constant; do not
hardcode new ones.
## Verification
- Unit tests cover each acceptance criterion and edge case.
- Run: npm test cart
Hand that to any agent and you will get a focused, checkable result. Stay vague — "add discount codes to the cart" — and you will get someone's guess about case sensitivity, tax order, and stacking.
Key Takeaways
- A complete spec has six parts: outcome, acceptance criteria, edge cases, constraints, prior decisions, and verification.
- Acceptance criteria must be checkable — name the input and the expected output, with no undefined adjectives.
- Surface the edge cases where you have an opinion; silence means the agent decides.
- Constraints fence the agent in: dependencies, files, scope, and patterns.
- Prior decisions give the agent context it cannot infer from the code alone.
- Use the template so you never start from a blank page, and split anything that grows past a page.

