ASK KNOX
beta
LESSON 430

Anatomy of a Good PRD

A PRD without testable acceptance criteria is a wish list — five required sections that convert it into a buildable contract.

8 min read·Spec-Driven Development: PRD → Ship

A PRD Without Acceptance Criteria Is a Wish List

The word "PRD" gets applied to everything from one-paragraph Notion notes to 40-page design documents. In the context of AI-assisted development, a PRD has a precise definition: a document that contains the minimum information required for an agent to produce verifiable output.

That definition has sharp implications. A PRD without testable acceptance criteria is not a PRD — it is a wish list that cannot be verified at the quality gate. A PRD without non-goals is not a PRD — it is an open-ended invitation for scope drift. A PRD without consumer contracts is not a PRD — it is a specification that will break existing callers.

The Six Sections

Five sections are required. One is optional (though rarely omitted in practice):

The most underbuilt section in practice is Consumer Contracts. Teams write goals, criteria, and even non-goals — but skip the step of grepping existing callers before finalizing output shape. This is the root cause of the "spec merged, existing callers broken" failure mode. The consumer contract section forces the research step.

The Acceptance Criteria Problem

Acceptance criteria are the most important section and the most commonly written wrong. The pattern failure is substituting subjective descriptions for binary conditions.

The test for a good acceptance criterion: can you write the failing test before implementation starts? If yes, the criterion is testable. If you need to see the implementation to know what to test, the criterion is vague.

The Given/When/Then format is the most reliable pattern for forcing testability:

  • Given a valid Supabase session, When the user calls GET /api/lessons, Then the response is 200 with the lessons array in the body.
  • Given no Supabase session, When the user calls GET /api/lessons, Then the response is 401.
  • Given a lesson merged to main in content/academy/, When the announce-lessons cron fires within 5 minutes, Then a message appears in Discord #lessons with the lesson title, excerpt, and link.

Each of these maps directly to a test case. The test can be written before the implementation exists. This is what makes the quality gate work: the gate checks whether all acceptance criteria have passing tests. Without testable criteria, the gate has nothing to check.

Non-Goals Are Underrated

The non-goals section receives less attention than it deserves. Most practitioners treat it as a courtesy — a brief acknowledgment that the feature is bounded. In AI-assisted development, it is a hard constraint.

Agents do not read scope as humans do. An agent building a Discord notification feature will infer that "comprehensive notification support" is a reasonable goal from the context of "add Discord notifications." The agent will add per-user DMs. It will add a preference system. It will add a notification history log. None of these were requested. All of them are "helpful" additions from the model's perspective.

Non-goals block this at the prompt level. When the dispatch prompt includes the non-goals section, the agent reads them as constraints on the solution space. "No per-user DMs" is not a suggestion — it is a requirement that the agent not implement per-user DMs.

The minimum viable non-goals section has three items:

  1. One capability to defer to a future PRD
  2. One system to not touch
  3. One user group not served by this feature

Three lines that prevent three classes of overreach.

Consumer Contracts: The Missing Section

Every PRD that changes an existing function's output shape needs a consumer contracts section. The section has three components:

Who calls this function:

grep -rn "getAllAcademyArticles\|formatLesson" $(git ls-files)

What shape they expect:

// Expected by app/(learn)/lesson/[slug]/page.tsx:44
// { lesson: number, title: string, track: string, excerpt: string }

What the new shape will be and why it is compatible:

// New: adds optional `notification_text?: string`
// Backward compatible — all existing callers unaffected

When this section is missing and you change the return type of getAllAcademyArticles(), you will find out what broke at CI time — or, if CI doesn't cover the caller, at runtime. The consumer contracts section moves this discovery to the spec review phase.

The Out of Scope Section (Optional)

Out of Scope is distinct from Non-Goals. Non-goals are things that will never be part of this feature. Out of Scope items are things that will eventually be built but are deferred.

The value of making this explicit: every Out of Scope item should have a follow-on ticket. The discipline of creating the ticket at spec-writing time (not "when we get to it") prevents the spec-impl decoupling gap covered in the “The Compounding Loop” lesson.

## Out of Scope
- [ ] Retry logic on Discord API failure → ticket #43
- [ ] Per-lesson announcement customization → ticket #44  
- [ ] Analytics on notification engagement → ticket #45

Three lines, three tickets, zero coverage gaps.

Now apply all of this at once: take a PRD skeleton with empty TODO sections and turn it into a contract verifiable enough that an agent could build against it without a single judgment call.

What's Next

The next lesson covers the decomposition of a master PRD into per-phase and per-feature PRDs. The structure you build in the previous lesson and this one is the input to /prd-writer --decompose. A well-structured master PRD decomposes cleanly. A vague one cannot be decomposed without writing it properly first.