Academy · Chapter 4 of 11 · ~120 minutes

Specs and EARS: from criterion to test.

An agent builds what is written down and fills in what is missing. This chapter shows how to write down what you want so that people can approve it and machines can check it: as a product spec with acceptance criteria in EARS, backed by tests and traceable all the way to the commit.

~120 min 5 EARS templates 4 exercises · 10 questions

Learning goals

This chapter is the heart of the Academy and also its longest. Take your time with it, do the exercises, and only read the model answers after you have formulated an answer yourself. After this chapter you can:

  • explain why a coding agent needs a spec, and why tests alone only prove that code is good, not that it does the right thing;
  • build a product spec: frontmatter, goal, non-goals, context, acceptance criteria with stable IDs, edge cases, open questions;
  • apply the five EARS templates (ubiquitous, event-driven, state-driven, unwanted behaviour, optional feature) and their combinations with confidence;
  • spot bad requirements and rewrite them: vague words, compound requirements, implementation instead of behaviour, untestable statements;
  • place Given/When/Then as an alternative and choose it deliberately;
  • derive tests from every acceptance criterion (positive, negative, boundary) and link them to the spec via the AC ID, in Java/JUnit and TypeScript;
  • understand traceability and spec drift: commit trailers, the bound version, the why-trace, outdated work after a spec change;
  • in the factory, create a spec from the template, approve it (from MAINTAINER), choose the spec mode and have specs worked out by a spec run or a reverse spec.
Prerequisites: Chapters 2 (run, adapter, phases) and 3 (project definition, artifacts, AGENTS.md) help. Programming knowledge in Java or TypeScript is useful for section 4.7, but not required.

4.1 Why specs: good is not the same as right

Imagine you give a coding agent the task: “Build a CSV export for the order list.” Twenty minutes later the build is green, all tests pass, the code is cleanly structured, the static security reviewer finds nothing. Is the task done?

You don’t know. Does the function export all orders or only the filtered ones? Comma or semicolon as the delimiter (a real difference in German-locale spreadsheets)? Which encoding? What happens with a million rows? May technicians export, or only dispatchers? What about a customer name that starts with an equals sign and would be executed as a formula in the spreadsheet? The agent has answered every one of these questions somehow, and it did so silently. The tests it delivered check exactly its own assumptions. They are green because they share the same understanding as the code they test.

That is the core of the problem, and with agents it grows rather than shrinks. A human developer would have asked, or at least mentioned in the daily stand-up that they went with the semicolon. In the factory an agent works headless: nobody answers questions during the run (see Chapter 2). That is why the engineering guardrails explicitly require surfacing assumptions instead of asking (“surface, don’t ask”). But a surfaced assumption is still an assumption that somebody has to check afterwards.

Two questions, two tools

Quality assurance answers two different questions, and they need different tools:

QuestionClassic termToolsPossible without a spec?
Is the code good? (compiles, secure, maintainable, consistent with the architecture)Verification: “Are we building the product right?”Build, tests, linters, static analysis, architecture rules, reviewersYes
Does the code do the right thing? (does it fulfil what was wanted)Validation: “Are we building the right product?”Comparison against an agreed, checkable description of what is wantedNo. Without a description there is nothing to check against.

For a long time the factory answered the first question very thoroughly: build gate, six reviewers, architecture rules, dependency scan. The foundational decision on spec-driven development (ADR-0027) describes the gap itself like this: The quality gate checks whether code is good, not whether it does the right thing. Before version 0.35, acceptance criteria simply did not reach the quality gate; the field for them in the review request was hard-wired to empty. The spec closes this gap by putting what is wanted into a form that people can approve and machines can check.

What a spec is and what it is not

“Spec” is an overloaded word. In this Academy, as in the factory, we mean the product spec: a short, versioned document that describes from the user’s point of view what a feature should do, what is explicitly not part of it and how you can tell that it is done. How it differs from neighbouring artifacts:

ArtifactAnswersLifetimeIn the factory
Project definitionWhat is the whole product about? Stack, architecture, guardrailsProject lifetime13 fields, from which PROJECT.md, INSTRUCTIONS.md are generated (Chapter 3)
Product specWhat should a feature do, and how do we measure it?As long as the feature exists, versionedspecs/SPEC-<nnn>-<slug>/spec.md, database is authoritative
Plan / backlog itemHow and in what order do we implement it?Until implementedplans/NNNN-*.md, backlog (Chapter 5)
Design/architecture decisionWhy this technical solution?Long-livedADRs in the project repo
TestIs a specific criterion met in a specific case?With the codeIn the repo, linked to the spec via the AC ID

So a spec is not an 80-page requirements document, and it is not a one-sentence ticket. A good product spec fits on one to three screens. It is solution-neutral (it does not prescribe which class produces the export), but precise about behaviour (it says which delimiter applies).

Why especially with agents

  • Agents are literal. Whatever is not written down gets filled in, with what is statistically most plausible rather than what is right for the domain. A spec moves decisions from “some time during the run, implicitly” to “beforehand, explicitly, approved”.
  • Agents are fast. When code becomes cheap, clarity about which code should be written becomes the bottleneck. An hour of spec work then saves not one hour of programming but several correction loops and reviews.
  • Agents work in parallel. When three child runs build one feature (Chapter 7), they need a shared, immutable point of reference. Otherwise each one builds against its own interpretation.
  • Agent results must be provable. In regulated environments “the build was green” is not enough. You have to be able to show which approved requirement was built against and accepted (Chapter 9).
Rule of thumb: The spec is the contract between you and the agent, and between the agents themselves. Tests are the executable form of that contract. The AC ID is the link that binds the two together.

In the factory

Spec-driven development: enabling it and the spec mode

The factory treats the product spec as the single source of truth for requirements (ADR-0027). The SDD roadmap states the guiding idea as a chain:

SDD chain in six steps: idea (goal), spec and approval (AC IDs), plan and tasks (backlog), build (code), quality gate (verification), acceptance (evidence). SDD chain in six steps: idea (goal), spec and approval (AC IDs), plan and tasks (backlog), build (code), quality gate (verification), acceptance (evidence).

Open full-size graphicOpen full-size graphic

Idea → Spec (approval) → Plan/tasks → Build (parallel) → Quality gate against spec → Acceptance. The factory’s SDD flow, derived from its motto “You describe. The factory builds. You approve.”

Enabling

Specs are opt-in. For the Specs button to appear on the project page, two things are needed:

  • the feature flag softwarefabrik.spec.enabled (environment variable SOFTWAREFABRIK_SPEC_ENABLED=true, off by default), and
  • with an active license system, the Professional or Enterprise tier. Without an active license system the flag alone decides. Community sees no spec interface.

If a license expires, existing specs remain readable; creating, editing and approving are then locked. If the flag is off, the platform behaves exactly as it did before the extension.

The spec mode per project

On the project’s spec overview (/projects/{id}/specs) you set the spec mode from role DEVELOPER: AUS (off), LEICHT (light) or STRENG (strict). The project wizard asks for it in step 5 as soon as SDD is enabled. Every change is audited as PROJECT_SPEC_MODUS_SET.

ModeEffectWhen it makes sense
AUS (default)Current behaviour. Specs can be created but are not projected into the workspace.Trying things out, existing projects without SDD
LEICHTRuns project specs into specs/, agent changes are taken over as drafts, plan runs write spec and AC references, runs with an approved spec are bound and traceable. A missing approval does not block; it produces hints.Solo work, getting started with SDD
STRENGIn addition, an approved spec is a start condition: backlog items without an approved spec or with unknown AC IDs cannot be activated, and workflow plans with writing tasks without a spec reference cannot be approved. The spec-conformance reviewer becomes a mandatory reviewer, missing AC coverage is a CRITICAL finding, and the quality gate runs in blocking mode, even if the global gate mode is advisory or off.Teams, regulated environments, anything that requires approval
Since when: Projection, takeover as a draft, spec reference in the backlog, the activation lock and the plan-approval lock have been available since v0.35/v0.36, the spec-conformance reviewer with its blocking effect in mode STRENG since v0.38.0 (SPEC-003).

Database authoritative, file as projection

An important design decision: the database is authoritative, the file in the workspace is a projection. Every version is an immutable version with a SHA-256 content hash. In mode LEICHT/STRENG every run writes the approved version to specs/SPEC-<nnn>-<slug>/spec.md (the latest draft if there is no approval); if there is a newer draft alongside an approval, it is placed next to it as spec.draft.md. If an agent changes a file under specs/, the factory takes the change over after the build as a new draft, never as an approval. If someone deletes the file, the next run restores it from the database.

Why this way? Because immutability, signability and tenant separation can be enforced cleanly in a database, while the file keeps agents and people in their usual workflow: an agent reads specs/ like any other file in the repo.

4.2 Structure of a product spec

A product spec in the factory is Markdown with YAML frontmatter. The template lives at specs/_vorlage/spec.md. The mandatory sections are Ziel (goal), Nicht-Ziele (non-goals) and Akzeptanzkriterien (acceptance criteria); context, edge cases and open questions are optional but almost always helpful in practice.

The frontmatter

---
id: SPEC-012                 # SPEC-<nnn>, sequential per project
titel: CSV export of maintenance orders
version: 1                   # incremented by the factory for each new version
status: DRAFT                # DRAFT | APPROVED | SUPERSEDED — only the factory sets APPROVED
owner: k.berger              # accountable on the business side
contracts: []                # optional: names of linked contracts in the contract registry
ersetzt: null                # optional: SPEC-<nnn> that this spec replaces
---
Frontmatter according to the factory template. The factory maintains version and status, not you.
  • id is the stable identifier. It reappears in every AC ID (AC-012-01), in plans, commits and the audit trail.
  • version and status are managed by the factory: every change creates a new version with status DRAFT; only approval by a human sets APPROVED.
  • owner is the person accountable on the business side, not the agent that wrote the text.
  • contracts links the spec to contracts in the contract registry, for example an OpenAPI description (Chapter 7).
  • ersetzt (“replaces”) refers to an older spec that this one replaces, when a feature is fundamentally re-cut.

Goal

One to three paragraphs from the user’s point of view: Who gets what, and why? The goal is not an acceptance criterion but the rationale you weigh things against when in doubt. If an edge case is not covered, the goal helps you decide.

Dispatchers export the currently filtered order list as CSV in order to analyse it in a spreadsheet or pass it on to subcontractors. Today they copy the list by hand from the browser, losing umlauts and columns in the process.

Non-goals

The most underrated section. Non-goals state what the spec explicitly does not cover. They prevent scope creep, and they make “uncovered behaviour” checkable: if an agent adds an Excel export on the side even though “no Excel format” is listed as a non-goal, that is a clear finding, not a matter of taste. Good non-goals are concrete and name obvious extensions that you deliberately do not want:

  • No export in Excel format (.xlsx).
  • No scheduled export and no export sent by email.
  • No import of CSV files.
  • No change to the filter logic of the order list.

Context

Optional: starting situation, affected modules and files, dependencies, linked contracts. Concrete paths are allowed here, because the context helps the agent find the right place in the code without prescribing the behaviour. The factory’s own specs list, for instance, affected classes and the next free migration number there.

Acceptance criteria

The heart of the spec. Each criterion has an ID following the scheme AC-<nnn>-<nn>, where <nnn> is the spec number. IDs are unique within the spec and stable across versions: a criterion keeps its ID even when its text is refined. A removed ID is never reused, otherwise an old test would suddenly “prove” a completely different criterion. Criteria are preferably written in EARS (section 4.4); Given/When/Then is allowed (section 4.6).

Edge cases

Boundary and negative cases that must not be forgotten. The template recommends turning them into a separate AC where needed. The rule of thumb: if an edge case requires observable behaviour, it belongs in the spec as an AC so that it gets an ID and is tested. If it is only a hint for the implementation, it can stay here.

Open questions

What has not been decided yet. A spec run writes here what it cannot decide (section 4.10). The template is unambiguous: A spec with open questions should not be approved. Open questions are not a flaw; they are the most honest part of a draft.

The lifecycle of a spec

  create / edit / agent changes specs/…
                 │
                 ▼
            ┌─────────┐   approval (from MAINTAINER,   ┌──────────┐  new version     ┌────────────┐
            │  DRAFT  │ ── validates mandatory     ──▶│ APPROVED │ ── approved ─────▶│ SUPERSEDED │
            └─────────┘    sections and AC IDs)       └──────────┘                   └────────────┘
                 ▲                                         │
                 └──── every change = new version ◀────────┘   (APPROVED stays unchanged)
DRAFT → APPROVED → SUPERSEDED. Approved versions are immutable; each spec has at most one APPROVED version.

Three rules follow from this. First: an approved version never changes; anyone who wants to change something creates a new draft version. Second: approving a new version sets the previously approved one to SUPERSEDED. Third: because every version has a content hash, you can later prove exactly which text was built against.

A complete example

The following example accompanies us through the chapter. It is deliberately realistic: not perfect, but ready for approval.

---
id: SPEC-012
titel: CSV export of maintenance orders
version: 3
status: DRAFT
owner: k.berger
contracts: []
ersetzt: null
---

# SPEC-012 — CSV export of maintenance orders

## Ziel

Dispatchers export the currently filtered order list as CSV in order to
analyse it in a spreadsheet or pass it on to subcontractors. Today they
copy the list by hand, losing umlauts and columns in the process.

## Nicht-Ziele

- No export in Excel format (.xlsx).
- No scheduled export and no export sent by email.
- No import of CSV files.
- No change to the filter logic of the order list.

## Context

- Order list: io.beispiel.maint.auftrag.web.AuftragListeController
- Filter: AuftragFilter (existing, unchanged)
- Roles: ADMIN, DISPONENT (see PROJECT.md, section Security)

## Akzeptanzkriterien

- **AC-012-01** — When a dispatcher selects “Export as CSV” in the order list,
  the system shall deliver a CSV file containing exactly the orders that
  match the currently set filter.
- **AC-012-02** — The system shall generate CSV files in UTF-8 with a byte
  order mark, a semicolon as the delimiter and a header row with the
  column names.
- **AC-012-03** — If the filter yields more than 50,000 orders, then the
  system shall reject the export and display the message “Please narrow
  the filter (at most 50,000 rows)”.
- **AC-012-04** — While an export is running for a user, the system shall
  disable the export button for that user.
- **AC-012-05** — Where the “Cost centres” module is enabled, the system
  shall include the column “Cost centre” as the last column of the export.
- **AC-012-06** — If a field value contains a semicolon, a quotation mark
  or a line break, then the system shall enclose the field in quotation
  marks and double any quotation marks it contains.
- **AC-012-07** — If a field value starts with =, +, - or @, then the
  system shall prefix it with an apostrophe.
- **AC-012-08** — If a user without the role DISPONENT or ADMIN calls the
  export, then the system shall respond with HTTP 403 and deliver no
  order data.

## Edge cases

- Empty result set: file with header row and no data rows (AC-012-01).
- Exactly 50,000 orders: export allowed (boundary of AC-012-03).
- Umlauts and special characters in customer names (AC-012-02).

## Open questions

- (none)
Example spec SPEC-012, version 3. AC-012-07 protects against formula injection in spreadsheets: security requirements belong in the spec as checkable criteria, not only in the reviewers’ heads (more on security in Chapter 8).

Note what is not here: no CsvExportService class, no library, no streaming technique. The spec describes observable behaviour. How the agent implements it is up to the agent, within the bounds of the project definition, guardrails and architecture rules.

In the factory

Creating, editing and comparing a spec

  1. Open the spec overview. On the project page, choose the Specs button (route /projects/{id}/specs). The overview lists all specs with ID, title, latest version, approved version and path in the workspace, with the spec mode setting above.
  2. New spec from the template. In the “New spec from template” area, enter a meaningful title (for example “CSV export of maintenance orders”) and choose Create spec. The factory assigns the next sequential ID, stores the spec as version 1 with status DRAFT and fills it with the template. Creating is allowed from role DEVELOPER.
  3. Edit. On the detail page, choose Edit (new draft version). Every saved change creates a new version; nothing is overwritten.
  4. Read the validation. For each version, the detail page shows whether it is ready for approval (“Validation passed”) or which findings stand in the way, plus the recognised acceptance criteria as a table (ID and text). If the factory does not recognise a criterion, the ID spelling is usually wrong.
  5. Compare. Once an approved version exists, the detail page offers a comparison with it (/projects/{id}/specs/{specId}/vergleich). It works at the level of acceptance criteria: new, removed, changed.
Letting agents write specs: Even without a spec run, a normal run in mode LEICHT/STRENG can create or change files under specs/, for example with the objective “Read SPEC-012 and add missing edge cases as acceptance criteria”. The factory takes the result over as a new draft version, which you review and approve like any other. Two runs in a row produce two drafts; neither overwrites the other.

Other tenants requesting a spec get “not found” (HTTP 404), not “forbidden”: the existence of foreign data is not even confirmed.

4.3 What makes a good acceptance criterion

Before we get to sentence form, it is worth looking at the properties a single criterion must have. The list is based on the quality characteristics that the requirements engineering standard ISO/IEC/IEEE 29148 describes for individual requirements, translated into practice with agents:

PropertyMeaningCheck question
NecessaryWithout this criterion, something the goal requires is missing.What would be lost if I deleted it?
UnambiguousThere is exactly one reasonable reading.Would two developers write the same test?
Singular (atomic)It asks for exactly one thing.Is there an “and” or “as well as” between two responses?
VerifiableA test can confirm or refute it.Can I write down a test case with an expected result?
Solution-neutralIt describes behaviour, not implementation.Would it still be true if the code were built completely differently?
ConsistentIt contradicts neither other criteria nor the non-goals.Is there a situation in which two criteria demand opposite things?
FeasibleIt can be implemented with reasonable effort.Does it hold up against the constraints of the project definition and architecture?

How many criteria?

There is no magic number. As a guide: a spec with two criteria is usually too coarse (the edge cases are missing), one with forty is usually too broad (really several features). Five to fifteen criteria per spec is a good range. If it grows beyond that, check whether the spec can be split along the non-goals. The factory itself still lists the question “one spec per feature, or an epic with sub-specs” as open; its roadmap recommends one spec per feature.

The right altitude

A criterion is at the right altitude when it is observable from the outside: through the UI, an interface, a file, a response, a log entry that someone is meant to see. “The system shall generate the export via a stream” is too low (implementation). “The system shall make dispatchers’ work easier” is too high (a goal, not testable). “If the filter yields more than 50,000 orders, then the system shall reject the export and display the message …” is exactly right: you can trigger it from the outside and check the result.

Non-functional requirements can be captured as AC too, as long as they apply to this feature and are measurable: “The system shall deliver an export of 50,000 orders within at most 10 seconds.” Project-wide requirements such as “p95 under 300 ms”, on the other hand, belong in the project definition (field nonfunctionalRequirements, Chapter 3), not in every spec.

4.4 EARS: five sentence templates for precise requirements

EARS stands for Easy Approach to Requirements Syntax. Alistair Mavin and colleagues developed the method at Rolls-Royce to make requirements for engine control systems less ambiguous, and presented it in 2009 at the IEEE International Requirements Engineering Conference. The idea is strikingly simple: instead of writing requirements in free prose, you use a small number of templates with fixed keywords and a fixed order of clauses. This reduces typical problems of natural language (ambiguity, vagueness, missing conditions, omissions) without having to learn a formal language.

Every EARS requirement has the same basic structure:

[precondition(s)]  [trigger]  the <system>  shall  <response>
     optional        optional    mandatory   mandatory  mandatory
Basic structure according to Mavin et al.: zero or more preconditions, zero or one trigger, exactly one system, one or more responses. For testability, one response per criterion is recommended (section 4.5).

The keywords show which clause plays which role. Five basic patterns result from this. Below, each pattern is shown with the English original, alongside the German template that the factory’s spec template uses.

The five EARS patterns: ubiquitous (The system shall …), event-driven (When X, then Y), state-driven (While X, Y shall …), optional feature (Where feature enabled …) and unwanted behaviour (If X, then Y). The five EARS patterns: ubiquitous (The system shall …), event-driven (When X, then Y), state-driven (While X, Y shall …), optional feature (Where feature enabled …) and unwanted behaviour (If X, then Y).

Open full-size graphicOpen full-size graphic

1. Ubiquitous

Requirements that always apply, with no trigger and no state. They describe invariants and fundamental properties.

English:   The <system name> shall <system response>.
German:    Das System soll <Reaktion>.
The “ubiquitous” template.
  • AC-012-02 — The system shall generate CSV files in UTF-8 with a byte order mark, a semicolon as the delimiter and a header row with the column names.
  • The system shall keep every order under a unique, immutable order number.
  • The system shall output all timestamps in the API in ISO 8601 format with a time zone offset.
  • From the factory itself (AC-001-07): The system shall keep approved versions immutable; a change shall create a new version with status DRAFT.

Careful: the ubiquitous template is the most convenient and therefore the most overused. For every ubiquitous requirement, check whether it really applies always. Often a condition is hiding (“… when the user is logged in”), and then one of the other templates is the right one.

2. Event-driven

Requirements that describe a response to an event: a user action, an incoming message, a point in time, a signal. The event is a moment, not a lasting condition.

English:   When <trigger>, the <system name> shall <system response>.
German:    Wenn <Auslöser>, dann soll das System <Reaktion>.
The “event-driven” template, keyword When.
  • AC-012-01 — When a dispatcher selects “Export as CSV” in the order list, the system shall deliver a CSV file containing exactly the orders that match the currently set filter.
  • When an order receives the status “Done”, the system shall display a notification to the order’s dispatcher.
  • When the daily maintenance planning starts at 02:00, the system shall mark all orders whose due date has passed as “overdue”.
  • From the factory (AC-002-02): When a run with a spec reference starts, the system shall write RUN_SPEC_GEBUNDEN … of the version approved at that moment into the audit chain.

3. State-driven

Requirements that apply while a state persists. The difference from an event: a state has a duration, and the response applies for its entire duration.

English:   While <precondition(s)>, the <system name> shall <system response>.
German:    Solange <Zustand>, soll das System <Reaktion>.
The “state-driven” template, keyword While.
  • AC-012-04 — While an export is running for a user, the system shall disable the export button for that user.
  • While the application is in maintenance mode, the system shall answer write API calls with HTTP 503.
  • While an order is assigned to a technician, the system shall prevent its assignment to further technicians.
  • From the factory (AC-001-10): While a project is in spec mode streng, the system shall reject the activation of a PlanItem whose spec has no approved version.

4. Unwanted behaviour

Requirements for handling errors, faults and unwanted situations: invalid input, missing permissions, outages, limits being exceeded, attacks. These are exactly the requirements most often missing from free-text tasks, and exactly where an agent guesses the most.

English:   If <trigger>, then the <system name> shall <system response>.
German:    Falls <unerwünschte Situation>, dann soll das System <Reaktion>.
The “unwanted behaviour” template, keywords If … then.
  • AC-012-03 — If the filter yields more than 50,000 orders, then the system shall reject the export and display the message “Please narrow the filter (at most 50,000 rows)”.
  • AC-012-08 — If a user without the role DISPONENT or ADMIN calls the export, then the system shall respond with HTTP 403 and deliver no order data.
  • If the database becomes unreachable during an export, then the system shall abort the export, deliver no incomplete file and display the message “Export failed, please try again later”.
  • From the factory (AC-001-03): If a spec contains duplicate AC IDs, AC IDs outside the scheme or no Akzeptanzkriterien section, then the system shall reject the approval with an understandable validation message (HTTP 400, not a server error).
“When” or “If”? In English, EARS separates When (expected event) from If … then (unwanted situation). In German, “wenn” is ambiguous: it can mean “as soon as” or “in case”. The factory template resolves this with a clear convention: “Wenn” introduces an expected event, “Falls” an unwanted situation. Whichever language you write in, apply this distinction consistently. Then the very first word tells you whether a criterion describes the normal case or an error case, and you can count whether the error cases are sufficiently covered.

5. Optional feature

Requirements that apply only where a particular feature is present: an enabled module, a product variant, a license tier, a configuration, a hardware feature.

English:   Where <feature is included>, the <system name> shall <system response>.
German:    Wo <Merkmal vorhanden>, soll das System <Reaktion>.
The “optional feature” template, keyword Where.
  • AC-012-05 — Where the “Cost centres” module is enabled, the system shall include the column “Cost centre” as the last column of the export.
  • Where sign-in via SSO is configured, the system shall display the button “Sign in with company account” on the login page.
  • Where the device has a camera, the app shall offer capturing photos for an order.
  • From the factory (AC-002-09): Where the FeatureGate enables SDD, the system shall offer the choice of spec mode in the project wizard and store it on the created project.

The difference from the state-driven form: an optional feature is a property of the system’s configuration (setup, variant) that typically does not change during use. A state changes during operation (export running, maintenance mode active).

Complex forms

Real requirements often have several conditions. EARS allows the keywords to be combined. The usual order: optional feature, then state, then trigger, then system and response.

English:   Where <feature>, while <precondition>, when <trigger>, the <system> shall <response>.
           While <precondition>, if <trigger>, then the <system> shall <response>.

German:    Solange <Zustand>, wenn <Auslöser>, dann soll das System <Reaktion>.
           Wo <Merkmal>, solange <Zustand>, soll das System <Reaktion>.
           Solange <Zustand>, falls <unerwünschte Situation>, dann soll das System <Reaktion>.
Complex forms. Each condition remains a separate, clearly marked clause.
  • While an export is running for a user, when the same user reloads the page, the system shall display the progress of the running export instead of starting a second export.
  • Where the “Cost centres” module is enabled, if no cost centre is assigned to an order, then the system shall leave the column “Cost centre” empty for that order.
  • While the application is in maintenance mode, when an administrator signs in, the system shall grant them full access.
  • From the factory (AC-001-01), a state-driven requirement with two linked conditions: While softwarefabrik.spec.enabled is off or an active license system reports the Community tier, the system shall offer no spec interface …

Complex forms are powerful, but they come at a price: each additional condition roughly doubles the number of cases you should test. If a sentence has three conditions, check whether it would be better as two or three criteria.

Overview

PatternKeywordEnglish templateTypical content
Ubiquitous(none)The <system> shall <response>.Invariants, formats, fundamental properties
Event-drivenWhenWhen <trigger>, the <system> shall <response>.User actions, messages, points in time
State-drivenWhileWhile <state>, the <system> shall <response>.Modes, ongoing operations, locks
Unwanted behaviourIf … thenIf <condition>, then the <system> shall <response>.Errors, missing permissions, limits, outages, attacks
Optional featureWhereWhere <feature is included>, the <system> shall <response>.Modules, variants, license tiers, configuration
ComplexseveralWhere …, while …, when/if …, (then) the <system> shall …Responses with several conditions

“shall”, “must” and the subject

In requirements, the English shall expresses a binding obligation. The factory template renders it in German as “soll”, and that is how it is meant in factory specs: binding. Worth knowing: some organisations use a graded convention, in German requirements engineering for example “muss” (mandatory), “sollte” (recommended) and “kann” (optional) following Chris Rupp’s sentence templates, in English similarly shall/should/may. If your organisation works that way, decide deliberately and write the convention into the project definition. Mixing is the real mistake: a spec in which “shall” is sometimes meant as binding and sometimes as recommended is not unambiguous.

In the template, the subject is “the system”. In larger products you may be more precise (“the order API shall …”, “the web UI shall …”), as long as it remains clear which part is responsible. Avoid, on the other hand, passive sentences without a subject (“It shall be possible to export”): then it is missing who does what.

In the factory

EARS in factory specs: recommended, not enforced

On approval the factory checks the structure, not the sentence form (ADR-0027, decision E4). The parser requires:

  • the mandatory sections Ziel, Nicht-Ziele and Akzeptanzkriterien,
  • AC IDs following the scheme AC-<nnn>-<nn>,
  • uniqueness of the IDs within the spec.

Whether a criterion starts with “When” or “If”, the factory does not check. The rationale in ADR-0027: precise requirements without a formalism hurdle; Given/When/Then remains possible for test-oriented criteria. But that also means: the quality of the wording is up to you and to the person who approves. Approval is the moment when a human checks whether the criteria are unambiguous, verifiable and complete.

How to write AC that the parser recognises

## Akzeptanzkriterien

- **AC-012-01** — When …, the system shall ….
- **AC-012-02** — The system shall ….
- **AC-012-03** — *Given* … *When* … *Then* ….
The template’s format: bullet list, ID in bold, em dash, criterion.

The factory dogfoods the approach: the spec features themselves are described as SPEC-001 to SPEC-005 in the repository, and their acceptance criteria consistently follow the EARS templates. If you are looking for examples, the AC-001-… and AC-002-… quoted above are real criteria from these specs.

Typical approval mistake: AC-12-1 instead of AC-012-01, or an AC ID with the wrong spec number (AC-011-03 in SPEC-012). The detail page then shows a finding, and the approval is rejected with a validation message (HTTP 400), not with a server error.

4.5 Typical mistakes and how to rewrite them

Most bad requirements fail because of a handful of recurring patterns. Once you have seen them, you will find them in every ticket. For each pattern: how to recognise it, why it is particularly costly with agents, and a rewrite.

Vague words

Telltale signs: fast, user-friendly, intuitive, appropriate, robust, flexible, modern, secure, as far as possible, if applicable, as a rule, etc., and so on, sufficient, promptly. Each of these words invites the agent to insert its own definition, and every definition passes the test the agent writes for it.

VaguePrecise
The export should be fast.When a dispatcher starts an export of up to 50,000 orders, the system shall deliver the file within 10 seconds.
Error messages should be user-friendly.If an export fails, then the system shall display a message that states the cause in one sentence without an error code and includes a recommended action.
Large exports should be handled appropriately.If the filter yields more than 50,000 orders, then the system shall reject the export and display the message “Please narrow the filter (at most 50,000 rows)”.
Exported are order number, customer, status, etc.The system shall output the columns order number, customer, status, due date and technician in the export, in this order.

Compound requirements

Telltale signs: two independent responses joined by “and”, “as well as”, “also”, “/”. The problem: a test can turn only one of them red, and traceability becomes blurred. If a commit carries “Implements: AC-012-09”, which half is meant?

CompoundSplit
When an export is complete, the system shall download the file and record the export in the log.AC-a — When an export is complete, the system shall start the download of the file in the browser.
AC-b — When an export is complete, the system shall record user, time, filter and row count in the audit log.

Not every “and” is a mistake. “… reject the export and display the message” describes one coherent response to a situation: rejection without a message would be incomplete, a message without rejection pointless. The check question is: could the two parts sensibly be implemented, prioritised or tested separately? Then split them.

Self-criticism of the example: AC-012-02 in the version above bundles encoding, byte order mark, delimiter and header row. That is borderline. All four properties are part of “valid CSV for German-locale spreadsheets”, but a test for the header row says nothing about the encoding. In a fourth version you would list the header row as a separate criterion.

Implementation instead of behaviour

Telltale signs: class, table or library names, algorithms, data structures in the criterion. The problem: the criterion becomes true as soon as the technique is used, regardless of whether the behaviour is right; and it rules out better solutions.

ImplementationBehaviour
The system shall generate the export with Apache Commons CSV in a StreamingResponseBody.When a dispatcher starts an export of 50,000 orders, the system shall begin the download within 2 seconds.
The system shall set a column export_lock in the table app_user.While an export is running for a user, the system shall disable the export button for that user.

Technical requirements still have their place: project-wide in the project definition (technology and architecture preferences), for a single feature in the spec’s context section. There they are constraints, not acceptance criteria.

Not verifiable

Telltale signs: negations without bounds (“shall never crash”), absolutes (“all”, “always”, “at any time”) without a reference set, subjective judgements, a missing metric. The problem: no test can confirm it, so it is never proven, so in the factory it becomes a permanent finding without a resolution.

Not verifiableVerifiable
The export shall never fail.If an error occurs during an export, then the system shall deliver no incomplete file and display the message “Export failed, please try again later”.
The export shall work with all spreadsheet applications.The system shall generate CSV files that the spreadsheet applications named in PROJECT.md open without an import dialog, with correct umlauts and columns.
The system shall be secure against manipulation.If a field value starts with =, +, - or @, then the system shall prefix it with an apostrophe.

Other common patterns

  • Hidden condition. “The system shall disable the export button.” When? A ubiquitous requirement that is really state-driven. Rewrite with “While …”.
  • Missing error case. Every “When” comes with the question: what if it goes wrong? Count the “If” criteria in a spec. If there are none, something is very probably missing.
  • Pronouns without a referent. “When he starts the export, it shall notify him.” Who is he, what is it, whom does it notify? Spell out every role.
  • Weak modal verbs. “The system can …”, “should, if possible, …”. Either it is a requirement, in which case phrase it as binding, or it is a wish, in which case it does not belong in the acceptance criteria.
  • Solution disguised as a goal. “Goal: an export button in the toolbar.” The goal names the solution; the actual goal (passing on data, analysing it) is missing. Without a real goal there is no yardstick for edge cases.
  • Contradiction with a non-goal. A criterion demands sending by email although “no export sent by email” is listed as a non-goal. Before approval, read criteria and non-goals against each other.
  • Indefinite quantities and limits. “With many orders …” How many? And is the limit included or not? “More than 50,000” is unambiguous, “50,000 or more” is too, and so is “up to 50,000”, but you have to decide on one.

Checklist before approval

  • Every criterion has an ID following the scheme AC-<nnn>-<nn> and exactly one response.
  • Every criterion starts with the matching keyword (or is deliberately ubiquitous).
  • No criterion contains vague words, implementation details or indefinite quantities.
  • For every event, at least one error case has been considered.
  • Every limit is unambiguous (inclusive or exclusive).
  • Criteria and non-goals do not contradict each other.
  • For every criterion you can name a test case in one sentence.
  • The “open questions” section is empty or contains only questions that will deliberately be resolved after approval.

4.6 Given/When/Then as an alternative

Given/When/Then (in German often Angenommen/Wenn/Dann) comes from behaviour-driven development and became widely known through the Gherkin language, which tools such as Cucumber execute. Instead of a general rule, Given/When/Then describes a concrete example: starting situation, action, expected result.

Given  a dispatcher has filtered the order list to status “Open”
  And  the filter yields 3 orders
When   they select “Export as CSV”
Then   they receive a file with a header row and 3 data rows
  And  every data row has the status “Open”
A Given/When/Then scenario for AC-012-01. Concrete values instead of a general rule.

EARS and Given/When/Then compared

AspectEARSGiven/When/Then
Kind of statementGeneral ruleConcrete example
StrengthCompleteness, brevity, clear conditionsVividness, direct proximity to the test, concrete data
WeaknessAbstract; domain experts have to come up with examples themselvesOne example does not cover the rule; many scenarios needed
Good forRules, limits, states, error handling, variantsMulti-step flows, calculations, UI behaviour
Mapping“While/Where” ≈ Given; “When/If” ≈ When; “the system shall” ≈ Then

The two forms are not mutually exclusive. A proven practice: the rule in EARS as the acceptance criterion, and two or three Given/When/Then examples below it as test cases or edge cases. The factory template allows Given/When/Then directly as a criterion:

- **AC-012-09** — *Given* an order with the customer “Müller; Söhne” *When* it
  is exported *Then* the customer field contains the value "Müller; Söhne" in
  quotation marks.
Given/When/Then as an acceptance criterion in the template’s format. The ID is mandatory here too.

Scenario tables

Gherkin has scenario templates with example tables (Scenario Outline with Examples). They are excellent for boundary values because they make the equivalence classes visible:

Scenario Outline: Upper limit of the export (AC-012-03)
  Given the filter yields <count> orders
  When  a dispatcher selects “Export as CSV”
  Then  the result is <result>

  Examples:
    | count  | result                             |
    | 0      | File with header row, no data      |
    | 49999  | File with 49999 data rows          |
    | 50000  | File with 50000 data rows          |
    | 50001  | Rejection with message             |
Scenario table for AC-012-03: the limit of 50,000 is allowed, 50,001 is not.
When to use which? Write the spec primarily in EARS. Reach for Given/When/Then when a criterion only becomes understandable through an example, or when your organisation executes Gherkin scenarios anyway. What matters is not the form but that every criterion has an ID and is verifiable.

4.7 From acceptance criteria to tests

An acceptance criterion is a promise. A test is the evidence that it is being kept, and not just once but with every build. The basic rule is simple: every acceptance criterion is backed by at least one test, and that test names the AC ID. In practice one test is almost never enough. A good criterion needs:

  • at least one positive test showing that the required response occurs;
  • at least one negative test showing that the response does not occur when the condition is not met;
  • boundary tests where the criterion contains a limit (quantities, lengths, times, state transitions).

Why the negative test matters so much: an implementation that always disables the export button passes the positive test of AC-012-04 (“while an export is running, the button is disabled”) without any trouble. Only the negative test (“when no export is running, the button is enabled”) exposes it. Agents produce such overly broad solutions not out of malice, but because the positive test was the only signal they had.

Deriving test cases systematically

Two classic test design techniques fit EARS perfectly:

  • Equivalence classes: divide the possible inputs into classes for which the system should respond in the same way, and test one representative per class. For AC-012-03: “0 orders”, “1 to 50,000”, “more than 50,000”.
  • Boundary value analysis: bugs sit at the edges. Test the values right at and next to every limit: 49,999, 50,000, 50,001. A “greater than” instead of “greater than or equal” in the code only shows up this way.

Each EARS form also suggests a particular test strategy:

EARS formPositive testNegative or boundary test
UbiquitousProperty holds for typical inputsProperty also holds for extreme inputs (empty, very large, special characters); property-based testing where appropriate
Event-drivenTrigger occurs, response followsNo trigger, no response; trigger by the wrong role or in the wrong context
State-drivenIn the state, the response appliesOutside the state, it does not; transition into and out of the state
Unwanted behaviourBring about the error situation (exceed the limit, make a dependency fail), check the responseJust below the error threshold, no error response
Optional featureWith the feature: response presentWithout the feature: response absent, everything else unchanged

The AC ID in the test

So that people and tools can tell which test backs which criterion, the test names the AC ID. Three places have proven themselves, and the factory template names all three: in the test name, in an annotation such as @Spec("AC-012-03"), or in a comment. Choose one convention for your project and stick to it; write it into the project definition or the project memory so that the agent knows it too.

Java has no standard annotation for this. Create it once in the test code:

package io.beispiel.maint.testsupport;

import java.lang.annotation.*;

/** Links a test to acceptance criteria of a product spec. */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface Spec {
    /** AC IDs, e.g. "AC-012-03". */
    String[] value();
}
A custom @Spec annotation in the target project’s test code.

Example in Java with JUnit 5

package io.beispiel.maint.auftrag.export;

import io.beispiel.maint.testsupport.Spec;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.*;

@DisplayName("SPEC-012 CSV export")
class AuftragCsvExportTest {

    private final AuftragTestdaten daten = new AuftragTestdaten();
    private final AuftragCsvExport export = new AuftragCsvExport(daten.repository());

    // AC-012-03: upper limit of 50,000 rows — boundary values at and next to the limit
    @Spec("AC-012-03")
    @ParameterizedTest(name = "AC-012-03: {0} orders → allowed = {1}")
    @CsvSource({ "0, true", "49999, true", "50000, true", "50001, false" })
    void ac_012_03_obergrenze(int anzahl, boolean erlaubt) {
        var filter = daten.filterMitTreffern(anzahl);

        if (erlaubt) {
            assertThat(export.erzeuge(filter).datenzeilen()).hasSize(anzahl);
        } else {
            assertThatThrownBy(() -> export.erzeuge(filter))
                .isInstanceOf(ExportZuGrossException.class)
                .hasMessage("Please narrow the filter (at most 50,000 rows)");
        }
    }

    // AC-012-06: escaping — positive (special characters) and negative (normal field)
    @Test
    @Spec("AC-012-06")
    @DisplayName("AC-012-06: field with semicolon and quotation marks is escaped")
    void ac_012_06_maskiert_sonderzeichen() {
        daten.auftragMitKunde("Müller; \"Söhne\"");
        var zeile = export.erzeuge(daten.filterAlle()).datenzeilen().getFirst();
        assertThat(zeile).contains("\"Müller; \"\"Söhne\"\"\"");
    }

    @Test
    @Spec("AC-012-06")
    @DisplayName("AC-012-06: field without special characters stays unchanged")
    void ac_012_06_normales_feld_unveraendert() {
        daten.auftragMitKunde("Meier GmbH");
        var zeile = export.erzeuge(daten.filterAlle()).datenzeilen().getFirst();
        assertThat(zeile).contains(";Meier GmbH;").doesNotContain("\"Meier GmbH\"");
    }

    // AC-012-07: formula injection — all four leading characters and one harmless case
    @Spec("AC-012-07")
    @ParameterizedTest(name = "AC-012-07: \"{0}\" → \"{1}\"")
    @CsvSource(delimiter = '|', quoteCharacter = '"', value = {
        "=SUM(A1)|'=SUM(A1)", "+49 123|'+49 123", "-5|'-5", "@cmd|'@cmd", "Customer A|Customer A" })
    void ac_012_07_formel_injection(String eingabe, String erwartet) {
        assertThat(AuftragCsvExport.zelle(eingabe)).isEqualTo(erwartet);
    }
}
JUnit 5: AC ID in the annotation, test name and comment. Positive, negative and boundary cases for each criterion. AuftragTestdaten is a project-specific helper class.

Note two details. First, the AC ID appears three times; this redundancy is deliberate, so that it is immediately visible in test reports (via the display name), in code search (via the annotation) and when reading (via the comment). Second, the test for AC-012-07 also covers the harmless case: without it, an implementation that prefixes every field with an apostrophe would pass.

Example in TypeScript with Vitest

For the UI, for example the state-driven AC-012-04, a component test is a good fit. In TypeScript test frameworks such as Vitest or Jest, the easiest way is to put the AC ID into the description of describe and it:

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ExportButton } from './ExportButton';

// SPEC-012, AC-012-04: While an export is running for a user,
// the system shall disable the export button for that user.
describe('AC-012-04 export button during a running export', () => {
  it('AC-012-04: is disabled while an export is running', () => {
    render(<ExportButton exportLaeuft={true} />);
    expect(screen.getByRole('button', { name: 'Export as CSV' })).toBeDisabled();
  });

  it('AC-012-04: is enabled when no export is running', () => {
    render(<ExportButton exportLaeuft={false} />);
    expect(screen.getByRole('button', { name: 'Export as CSV' })).toBeEnabled();
  });

  it('AC-012-04: becomes enabled again as soon as the export ends', () => {
    const { rerender } = render(<ExportButton exportLaeuft={true} />);
    rerender(<ExportButton exportLaeuft={false} />);
    expect(screen.getByRole('button', { name: 'Export as CSV' })).toBeEnabled();
  });
});
Vitest with Testing Library: state on, state off, transition. The AC ID is in the comment and in every test description. The matchers toBeDisabled/toBeEnabled come from @testing-library/jest-dom (setup import @testing-library/jest-dom/vitest).

And in Python?

With pytest the same pattern works: AC ID in a comment or docstring and in the test name, for example via the ids of a parametrisation. If you want an annotation, you can register your own pytest marker (for instance @pytest.mark.spec("AC-012-03")); a comment with the ID should still be there, so that simple text searches find it too.

import pytest

# AC-012-03: upper limit of 50,000 rows
@pytest.mark.parametrize(
    "anzahl, erlaubt",
    [(0, True), (49_999, True), (50_000, True), (50_001, False)],
    ids=["AC-012-03 empty", "AC-012-03 just below", "AC-012-03 limit", "AC-012-03 above"],
)
def test_ac_012_03_obergrenze(export, filter_mit_treffern, anzahl, erlaubt):
    ...
pytest: AC ID in the comment and in the test names.

At which level to test?

Not every criterion needs an end-to-end test. Choose the lowest level at which the criterion can still be fully verified: escaping rules (AC-012-06, AC-012-07) as fast unit tests, the permission check (AC-012-08) as an integration test against the web endpoint, the button state (AC-012-04) as a component test. A single end-to-end test for the main flow (AC-012-01) additionally ensures that the parts work together.

When no test can be written

If you cannot think of a test for a criterion, the criterion is almost always the problem, not the test. Go back to section 4.5: usually there is a vague word, a missing limit or a compound requirement in it. The reverse holds too and is a powerful tool while writing: for every criterion, immediately formulate a test case in one sentence. If you cannot, the criterion is not finished yet.

Connection to the guardrails: The factory’s fourth engineering guardrail (“Make it verifiable”) requires the agent to translate the task into concrete success criteria before programming and to deliver the reproducing test. A spec with good acceptance criteria does that translation for it: the success criteria are already there, with IDs.

In the factory

Acceptance criteria in the quality gate and the spec-conformance reviewer

Today: the reviewers know the criteria

Since v0.35, a build run started from a backlog item with a spec reference passes the associated acceptance criteria (ID and text) of the approved version to all reviewers of the quality gate in the review request. Before that, the field was hard-wired to empty. This allows the LLM reviewers, for the first time, to judge whether the diff implements what was wanted, not just whether it looks good. Since v0.36, the agent itself also receives the bound criteria in its objective, with the explicit request to check at the end that each one is met and backed by at least one test (section 4.8).

Since v0.38.0: rule-based coverage check

Since v0.38.0 (SPEC-003) there is a dedicated reviewer spec-conformance of category REQUIREMENTS. It works rule-based, without an agent CLI (and therefore without an isolation proof) and only kicks in for runs bound to an approved spec version (spec mode LEICHT or STRENG). It checks:

  • AC coverage: every bound acceptance criterion must be referenced in at least one test in the repository. A reference is the AC ID in a test’s name or file name, in an annotation (@Spec("AC-…")) or in a comment. If the reference is missing, it reports a finding with the AC ID.
  • Unknown IDs: if a test references an AC ID of the same spec that does not exist in the bound version (a typo or a removed criterion), it reports a hint.
  • Uncovered behaviour: if the diff changes production files that are not assigned to any criterion (neither via a commit with an Implements: trailer that changes the file nor via path hints in the spec), it reports a hint with the file paths. For very large diffs it lists a limited number of paths and states the total.

Detection of test files and AC references is configurable via patterns (softwarefabrik.spec.conformance.*), with defaults for Java, TypeScript/JavaScript and Python. In mode LEICHT the findings are advisory (missing coverage as MEDIUM, hints as LOW). In mode STRENG, spec-conformance is a mandatory reviewer, missing coverage is CRITICAL, and the gate runs in blocking mode: the factory raises the global gate mode to blocking for this and notes it in the run log. A repository without tests does not cause an abort but reports all bound criteria as findings. An AC ID in a disabled test method counts as referenced; whether tests actually run is checked by the build reviewers, not this one.

Be aware of the limits: The check is a heuristic. It establishes that a test names an AC ID, not that it checks the criterion well. A test that carries @Spec("AC-012-03") and asserts nothing passes it. That is why human review of the tests remains mandatory, whether at approval before the merge or in the pull request (Chapter 6). The factory roadmap itself names false positives of the coverage heuristic as a residual risk; the reviewer is therefore advisory in mode LEICHT and blocking only in mode STRENG.

In enterprise environments a policy can additionally require a minimum coverage of the bound criteria (minAcAbdeckung, target state per SPEC-005, see Chapter 9).

4.8 Traceability: from requirement to commit

Traceability means: for every code change you can say which requirement it implements, and for every requirement which changes and tests belong to it. That sounds like bureaucracy, but it is the foundation for three very practical things: impact analysis (what is affected if AC-012-03 changes?), review (does this diff match what it is supposed to implement?) and evidence (which requirement was built against and accepted?).

Traceability scheme: spec SPEC-012 contains acceptance criterion AC-012-01; implementation and test evidence refer to the same AC ID, which stays visible across every stage. Traceability scheme: spec SPEC-012 contains acceptance criterion AC-012-01; implementation and test evidence refer to the same AC ID, which stays visible across every stage.

Open full-size graphicOpen full-size graphic

Scheme: the AC ID connects requirement, implementation and test evidence. TASK-007 and TEST-01 are placeholders; the stations the factory actually links are shown in the chain below.

IDs instead of text

Traceability via text does not work: texts get refined, reworded, translated. The factory therefore consistently tracks IDs (ADR-0027, decision E6). The AC ID is the thread that runs through all artifacts:

SPEC-012 v3 (APPROVED, sha256 …)
   └─ AC-012-03 ─┬─ Plan:     plans/0007-csv-export.md      (Spec: SPEC-012 / AC: AC-012-01, AC-012-03)
                 ├─ Backlog:  item with spec and AC reference
                 ├─ Run:      RUN_SPEC_GEBUNDEN spec=SPEC-012;version=3;hash=…;ac=AC-012-01,AC-012-03
                 ├─ Commit:   Implements: AC-012-01, AC-012-03
                 ├─ Test:     @Spec("AC-012-03") ac_012_03_obergrenze
                 ├─ Review:   acceptance criteria in the review request of all reviewers
                 └─ Trace:    the run’s why-trace shows spec, version, hash, AC IDs
The traceability chain of an AC ID through the factory.

Commit trailers

Git knows trailers: lines of the form Key: Value in the last paragraph of a commit message. Well-known examples are Signed-off-by: and Co-authored-by:. Tools such as git interpret-trailers or git log --format='%(trailers)' read them programmatically. For SDD the factory uses two trailers:

feat(export): CSV export of the filtered order list

Delivers the filtered order list as CSV (UTF-8, semicolon) and rejects
exports of more than 50,000 rows with an understandable message.

Spec: SPEC-012
Implements: AC-012-01, AC-012-03
Commit message with the SDD trailers Spec: and Implements: in the last paragraph.

This lets you ask at any time: Which commits implement AC-012-03? A git log --grep="AC-012-03" is enough to start with.

In the factory

Binding, trailers, why-trace: how the factory tracks an AC ID

Everything in this box applies only if SDD is enabled and the project’s spec mode is not AUS. Otherwise the factory behaves exactly as before.

  1. The plan run writes references. In mode LEICHT/STRENG the plan directive requires the lines Spec: SPEC-<nnn> and AC: AC-<nnn>-<nn>, … in every plan and lists the available specs with their approval status for the agent. When indexing, the factory stores the spec ID and AC IDs on the backlog item.
  2. The backlog warns. The backlog (/projects/{id}/backlog) shows the spec reference and hints when the spec is missing, has no approved version or AC IDs are unknown. In mode STRENG, activation rejects such items (Chapter 5).
  3. Binding at start. When a run with a spec reference starts, the factory binds the version approved at that moment and writes RUN_SPEC_GEBUNDEN with spec=…;version=N;hash=…;ac=… into the signed audit chain. If (in mode LEICHT) there is no approved version yet, the run starts without a binding, with a hint in the run log.
  4. Objective header. The agent receives the bound criteria (ID and text) directly after the engineering guardrails in its objective, the same for every adapter (verified for Claude, Codex and Gemini). Only the bound criteria, not the whole spec: that lives as a file in the workspace. The guardrails themselves and their hash do not change as a result.
  5. Spec index in AGENTS.md. In addition, the factory projects a marked “spec index” section with ID, title, status and path of all specs into the workspace’s AGENTS.md.
  6. Trailers on the factory commit. The factory’s commit and the pull request text carry Spec: SPEC-<nnn> and Implements: AC-…, even if the agent set no trailers in its own commits.
  7. Why-trace. Under /runs/{id}/trace the why-trace shows the spec ID, bound version, content hash and AC IDs, read from exactly the RUN_SPEC_GEBUNDEN event.

This is what the header the agent receives looks like (abridged, shown here in English):

## Spec binding: SPEC-012 v3 (sha256 4f1c…)

This task implements the following acceptance criteria (full spec:
specs/SPEC-012-csv-export/spec.md). Only these criteria are bound; check at
the end that each one is met and backed by at least one test:
- AC-012-01 — When a dispatcher …
- AC-012-03 — If the filter yields more than 50,000 orders, …

Commit rule: append the trailer lines `Spec: SPEC-012` and
`Implements: AC-012-01, AC-012-03` to every commit message.
The “spec binding” objective header (structure per the factory code, content from the example).

Parallel agents: the same version for everyone

In the workflow layer (Chapter 7) you declare the spec and AC IDs per task, when adding it or later via “Declare spec reference”. When the child run starts, the factory binds the approved version to the task (WORKFLOW_TASK_SPEC_GEBUNDEN). All child runs of a workflow work against the same version, even if the spec has been re-approved in the meantime. In mode STRENG a workflow plan can only be approved once every writing task names an approved spec.

In addition, every spec approval mirrors the acceptance-criteria block as contract SPEC-<nnn>-ac of kind ACCEPTANCE_CRITERIA into the contract registry (under Contracts). An unchanged block does not create a new contract version.

4.9 Spec drift: when spec, plan and code diverge

Spec drift is the divergence between spec, plan and code. It arises in two ways:

  • The spec changes, the work does not. A criterion is refined or removed while a backlog item or a running task is still planned against the old version. Whoever builds now builds against a requirement that no longer applies.
  • The code changes, the spec does not. A bug fix, a “small” extension, an optimisation changes observable behaviour without anyone updating the spec. The spec then quietly lies, and the next person (or the next agent) who relies on it builds on sand.

Against the second path, discipline helps most: change the spec first, then the code. A change in behaviour starts with a new draft version, gets approved and is then implemented. The hints about “uncovered behaviour” (section 4.7) make violations visible. Against the first path, technology helps: if every piece of work is bound to a specific version and versions can be compared at AC level, a system can calculate which work a change makes outdated.

What “changed” means

A comparison at AC level distinguishes three cases: a criterion is new (no existing work is affected, but someone has to implement it), removed (work that implements it is superfluous or even harmful) or changed (work must be reviewed and perhaps adjusted). Because IDs stay stable and are never reused, this comparison is unambiguous.

Disambiguation: In the factory documentation, “drift” is also used for other things, for example the deviation of the model actually used from the expected one. Spec drift refers exclusively to spec, plan and code diverging.

In the factory

Detecting spec drift

Today the spec detail page shows the comparison of two versions at the level of acceptance criteria (new, removed, changed). Use it before every approval of a new version: it shows you which criteria have moved relative to the approved version, and therefore which backlog items and tests you should look at. Because every run binds its version with version number and hash, you can also check in the why-trace at any time which state was built against.

Since v0.38.0 (SPEC-003) the factory takes over the impact analysis itself:

  • When a new version is approved, the factory compares it at AC level with the previously approved one and writes the result as SPEC_DRIFT with the affected AC IDs (new, removed, changed) into the audit chain. On a spec’s first approval there is no comparison.
  • If the new version removes or changes a criterion, the factory marks open backlog items (not DONE or REJECTED) bound to that criterion as outdated: the backlog shows them as VERALTET with a reason; an item without AC IDs counts for all criteria of the spec. Activation clears the mark, because the new build run binds the then approved version. Bound workflow tasks that are not finished move to WAITING_FOR_REPLAN; the workflow view lists them under “Outdated basis”.
  • Contracts of kind ACCEPTANCE_CRITERIA are compared structurally at AC level; the contract registry reports a removed criterion as a breaking change (HIGH) and a changed criterion text as a hint.
  • If the current version of a contract listed in the frontmatter under contracts: carries a breaking change, the factory flags the spec in the spec overview as “needs review” and lists the findings.

4.10 Working out specs: alone, in a team, with the agent

A spec is rarely written in one go. A four-step approach has proven itself:

  1. Goal and non-goals first. Two paragraphs of goal, three to six non-goals. If there is no agreement here, there is no point discussing criteria yet.
  2. Main flow as criteria. The event-driven criteria of the normal case (“When …, the system shall …”).
  3. Ask systematically about errors and states. For each event: what if the input is invalid, the role is missing, a limit is exceeded, a dependency fails? Which states influence the behaviour? Which variants exist? This produces the “If”, “While” and “Where” criteria.
  4. Review with a tester’s eye. For every criterion, a test case in one sentence. Whatever cannot be tested gets rewritten. Whatever is open moves to “open questions”.

Step 3 in particular is a strength of language models: they are good at listing edge cases for a rule that you had not thought of yourself. Their weakness is that they make plausible but wrong decisions if you let them decide. The right division of labour is therefore: the agent proposes and asks, the human decides and approves.

Reverse spec: specs for existing code

Existing systems often have no spec, but code and tests that implicitly define the behaviour. A reverse spec derives a draft from them. Its value lies less in the finished text than in the questions that come up along the way: is this behaviour intended, or a bug? A reverse spec draft must therefore explicitly mark assumptions, non-goals and open questions. Approving it unchecked would mean declaring the current state, bugs included, to be the requirement.

In the factory

Spec run and reverse spec

Since v0.38.0 (SPEC-004) the factory helps work out specs. Writing them by hand or with a normal run that writes under specs/ (section 4.2) still works.

  • A dedicated run type SPEC. Alongside PLAN and BUILD there is a spec run, which you start from the spec interface from role DEVELOPER: for a new spec from an idea in free text (the factory creates the spec from the template for this), or to sharpen an existing one. The prerequisite is enablement (section 4.1); the project’s spec mode may be AUS, because a spec run is an explicit request and not a side effect of a build run.
  • Only specs/ is taken over. At the end of a spec run the factory takes over exclusively changes under specs/ and discards all others (the counterpart to the plan takeover in a plan run). This produces new draft versions, never approvals. If the agent writes outside specs/, the run log lists the discarded paths. A spec run does not build, create a branch or merge.
  • Its own directive. The spec run requires goal, non-goals, acceptance criteria with IDs following the template’s scheme, negative and boundary cases for each criterion, and an “open questions” section. The rule “Do not ask questions” from INSTRUCTIONS.md, which still applies unchanged to plan and build runs, is replaced for spec runs only by “ask questions in ## Offene Fragen”.
  • Questions without a live dialog. If a spec run leaves questions in the “open questions” section of the new version, the factory reads them, stores them individually and ends the run in status WAITING_FOR_INPUT. This state does not occupy a capacity slot, since no process is running. The spec detail page shows the questions under “Open questions from spec runs” one by one for answering (from role DEVELOPER) and records who answered what and when. You can continue (follow-up pass) only once no question is open any more, i.e. all are answered or explicitly marked as “not relevant”. The factory then starts a new spec run linked to its predecessor, which receives your answers as input and again creates a new draft version; the waiting run is closed as COMPLETED. Without open questions, the spec run ends normally as COMPLETED.
  • Reverse spec. For a project with an imported repository you start a reverse spec on the spec overview: the factory creates a spec “Reverse-Spec: <project>” from the template and starts a spec run with an import directive that derives a draft from code and tests, names the source for each criterion and explicitly marks assumptions, non-goals and open questions. Without an imported repository (import source on the project), the start is rejected with a hint.
  • Robust against format errors. If a spec run produces a file that the parser rejects, it is still taken over as a draft and the findings appear on the version and in the run log, instead of the run failing.
  • Model choice and vendor. Spec runs get their own routing role SPEC with its own override run.routing.spec-model (without an override, as for plan runs); the resolution is attested as RUN_MODELL_AUFGELOEST with rolle=SPEC (Chapter 10). The directive and the question rule travel via the objective and INSTRUCTIONS.md and are therefore vendor-neutral, verified with the Claude and the Codex adapter.
Never approved automatically: ADR-0027 names it as a residual risk: generated specs can be plausible and wrong. That is why a spec run produces drafts only. Approval is always a human decision.

In the factory

Approving a spec

Approval is a separate, audited gate, distinct from run approval:

  1. On the spec detail page, review the draft version: content, recognised acceptance criteria, validation result and, if applicable, the comparison with the previously approved version.
  2. Go through the checklist from section 4.5. Open questions? Then do not approve; resolve them first.
  3. Choose Approve on the version. Allowed from role MAINTAINER, the same level as run approval.

What the factory does in the process:

  • It validates mandatory sections, ID scheme and uniqueness. Errors appear as a validation message (HTTP 400), and the status remains unchanged.
  • It sets the version to APPROVED and a previously approved version of the same spec to SUPERSEDED.
  • It records the decision in the approval history (visible on the detail page) and writes SPEC_APPROVED with spec ID, version and content hash into the signed audit chain.
  • It publishes the AC block as contract SPEC-<nnn>-ac in the contract registry.
  • It locks the spec header in the database, so that even two simultaneous approvals leave at most one APPROVED version.

Anyone with a role lower than MAINTAINER gets HTTP 403, and the status remains unchanged. An agent can never approve a spec: its changes are always taken over as a draft.

Solo operation: If you work alone, you approve your own spec. That is not a contradiction: even then, approval is the deliberate moment in which you check the draft against the checklist, and it produces the evidence of which version was built against. In teams and regulated environments, a policy will be able to make approval mandatory (pflichtSpecFreigabe, target state per SPEC-005, Chapter 9).

Summary

  • Tests and reviews check whether code is good. Whether it does the right thing can only be checked against an agreed, verifiable description: the spec.
  • A product spec has frontmatter, goal, non-goals and acceptance criteria with stable IDs AC-<nnn>-<nn>; context, edge cases and open questions complement it. Lifecycle: DRAFT → APPROVED → SUPERSEDED; approved versions are immutable.
  • EARS has five templates: ubiquitous (The system shall), event-driven (When), state-driven (While), unwanted behaviour (If … then), optional feature (Where), plus complex forms.
  • Vague words, compound requirements, implementation instead of behaviour and untestable statements are the most common mistakes; each has a standard rewrite.
  • Given/When/Then describes examples; it complements EARS, especially for flows and boundary value tables.
  • Every criterion needs positive, negative and boundary tests that carry the AC ID in their name, in an annotation or in a comment.
  • The factory tracks AC IDs from the plan through the binding (RUN_SPEC_GEBUNDEN), the objective header and the commit trailers into the why-trace; since v0.38.0, spec-conformance checks coverage and spec drift marks outdated work.
  • People and agents write specs together; approving (from MAINTAINER) is always done by a human.

Exercise

Exercises: from vague requirement to criteria and tests

Four exercises that build on one another. Work through them in writing before you open the solution sketches. For exercise 4 you need a running factory with SDD enabled (feature flag softwarefabrik.spec.enabled) and the mock adapter, so that no costs are incurred. The quickest way is the demo instance: SDD is switched on there; its data is reset every night.

Exercise 1: Translating a vague requirement into EARS

A business department writes to you: “After several wrong passwords, the account should be locked for a while for security reasons. The admin should of course be able to lift that, and the user ought to be informed.”

  1. Mark all vague words and all hidden compound requirements.
  2. Note down the questions you need to ask the business department.
  3. Using plausible values explicitly marked as assumptions, write four to six acceptance criteria in EARS, with IDs of a fictitious SPEC-020. Use at least three different templates.
Solution sketch

Vague: “several”, “for a while”, “of course”, “informed” (how, when, about what?), “ought to”. Compound: locking, lifting by the admin and notification are three independent requirements.

Questions: How many failed attempts, within what time period? How long does the lock last? Does the counter reset after a successful sign-in? Through which channel is the user informed? Is the admin notified? What does an attacker see (may the message reveal that the account exists)?

Criteria (assumptions: 5 attempts in 15 minutes, 30-minute lock):

  • AC-020-01 — If 5 sign-in attempts with a wrong password occur for an account within 15 minutes, then the system shall lock the account for 30 minutes.
  • AC-020-02 — While an account is locked, the system shall reject sign-in attempts for that account, even with the correct password.
  • AC-020-03 — If a sign-in is rejected, then the system shall display the same message “Sign-in failed”, regardless of whether the account is locked, unknown or the password is wrong.
  • AC-020-04 — When an account is locked, the system shall send the account holder an email stating when the lock started and when it ends.
  • AC-020-05 — When a user with the role ADMIN lifts the lock on an account, the system shall unlock the account immediately and reset the failed-attempt counter.
  • AC-020-06 — When a sign-in succeeds, the system shall reset the account’s failed-attempt counter.

Note AC-020-03: it is a security requirement that was not in the original text at all, but follows from the question “What does an attacker see?”. Exactly these kinds of criteria emerge when you systematically ask about unwanted situations. Until they are clarified, the assumptions belong in “open questions”.

Exercise 2: Determining the EARS form and finding mistakes

Determine the EARS form of each criterion. Find the three that are faulty and rewrite them.

  1. When a technician completes an order, the system shall notify the dispatcher.
  2. The system shall be performant and secure.
  3. While the mobile app is offline, the system shall store recorded times locally.
  4. Where multilingual support is enabled, the system shall use the browser’s language.
  5. If an uploaded photo is larger than 10 MB, then the system shall reject the upload and display the message “File too large (at most 10 MB)”.
  6. The system shall sort the orders by due date using a Hibernate query.
  7. The system shall save the order and send a PDF confirmation to the customer.
Solution sketch

a) event-driven, correct (more precise would be: which dispatcher, through which channel). c) state-driven, correct. d) optional feature, correct. e) unwanted behaviour, correct; “reject and display a message” is one coherent response.

Faulty: b) vague and compound; split into measurable criteria, for example “When a dispatcher opens the order list, the system shall display it with up to 500 entries within 2 seconds” and a concrete security requirement. f) implementation instead of behaviour; better “The system shall sort the order list by due date in ascending order by default.” g) compound requirement with a hidden trigger; better two event-driven criteria: “When a dispatcher saves an order, the system shall …” and “When an order is saved for the first time, the system shall send the customer a confirmation as a PDF by email.”

Exercise 3: Deriving tests from a criterion

Take AC-020-01 from exercise 1 (5 failed attempts in 15 minutes lead to a 30-minute lock). Form the equivalence classes, determine the boundary values and write the list of test cases with expected results. Write at least two of them as a JUnit or Vitest test with the AC ID.

Solution sketch

Dimensions: number of failed attempts (4 / 5 / 6), time window (all 5 within 15 minutes / the first was 15 minutes and 1 second ago), lock duration (sign-in after 29:59 rejected / possible after 30:00), counter behaviour (a successful sign-in in between resets it, strictly AC-020-06).

  • 4 failed attempts in 15 minutes → not locked (negative, boundary below).
  • 5 failed attempts in 15 minutes → locked (positive, boundary).
  • 5 failed attempts, the first 15:01 minutes ago → not locked (time window boundary; clarify beforehand whether 15:00 is included).
  • Locked, sign-in after 29:59 → rejected; after 30:00 → possible (lock duration boundaries; strictly speaking AC-020-02).
@Spec("AC-020-01")
@ParameterizedTest(name = "AC-020-01: {0} failed attempts → locked = {1}")
@CsvSource({ "4, false", "5, true", "6, true" })
void ac_020_01_sperre_nach_fehlversuchen(int versuche, boolean gesperrt) {
    var uhr = new VerstellbareUhr();
    var konto = kontoService(uhr).neuesKonto("anna");
    for (int i = 0; i < versuche; i++) {
        konto.anmelden("falsch");
        uhr.vor(Duration.ofMinutes(2));
    }
    assertThat(konto.istGesperrt()).isEqualTo(gesperrt);
}
A parameterised test covers the 4/5 boundary. The controllable clock makes time windows testable.

The exercise also shows that a precise criterion immediately places a testability requirement on the code: without a controllable clock, 15-minute windows cannot be tested sensibly. That belongs as a note in the spec’s context.

Exercise 4: The flow in the factory

Play through the SDD flow once from start to finish, preferably with the mock adapter:

  1. In a practice project, create a new spec from the template under Specs and transfer the criteria from exercise 1.
  2. Deliberately introduce a mistake (for example AC-20-1 instead of AC-020-01, or a duplicate ID) and try to approve. Read the validation message.
  3. Fix the mistake (new draft version), approve the spec and then compare a further change with the approved version.
  4. Set the spec mode to LEICHT, start a plan run and check in the backlog which spec and AC references the items carry.
  5. Set the mode to STRENG and try to activate an item without an approved spec.
  6. Activate an item with a spec reference and look at the why-trace after the run.
What to look out for
  • Step 2: The approval is rejected with an understandable validation message, not with an error page. The detail page already shows the finding on the version beforehand.
  • Step 3: After approval the version is immutable; “Edit” creates a new draft version. Your name appears in the approval history. The comparison shows changed criteria at AC level.
  • Step 4: With the mock adapter, the content of the plans depends on the mock; with a real adapter the plans carry the Spec: and AC: lines. Watch for the hints in the backlog.
  • Step 5: Activation is rejected. That is exactly the point of STRENG.
  • Step 6: The why-trace shows spec ID, version, hash and AC IDs. Compare the hash with the approved version on the detail page.

Chapter questions

  1. An agent delivers a green build, 95 % test coverage and no findings from the static reviewers. Why does that not show that it has fulfilled the task?

    Model answer

    Build, coverage and reviewers answer the verification question: is the code good? Whether it does the right thing (validation) can only be checked against an agreed description of what is wanted. The tests come from the same agent and check its own assumptions; they are green because they share the same understanding as the code. Only acceptance criteria with IDs approved by a human, and tests that back exactly those criteria, close the gap.

  2. Which three sections of a product spec are mandatory in the factory, and why are the non-goals more than a formality?

    Model answer

    Goal, non-goals and acceptance criteria are mandatory (Ziel, Nicht-Ziele, Akzeptanzkriterien). Non-goals delimit the scope, prevent scope creep and make uncovered behaviour checkable: if an agent builds something explicitly listed as a non-goal, that is a clear finding rather than a matter of taste. They also prevent an agent from “helpfully” adding obvious extensions that nobody ordered.

  3. Classify: “While maintenance mode is active …”, “Where the cost centres module is enabled …”, “If the database is unreachable …”, “When an order is completed …”. How do a state and an optional feature differ?

    Model answer

    State-driven, optional feature, unwanted behaviour, event-driven. A state changes during operation and has a duration (maintenance mode on/off). An optional feature describes the system’s configuration, that is setup, variant, module or license tier, and typically does not change during use. Accordingly, you test states with transitions into and out of them, and optional features with and without the feature.

  4. Rewrite: “The upload should work quickly and reliably and handle large files appropriately.” Which mistake patterns does it contain?

    Model answer

    Vague words (quickly, reliably, large, appropriately) and a threefold requirement. A rewrite, for example: “When a user uploads a file of up to 10 MB, the system shall confirm the upload within 5 seconds.” – “If an upload is incomplete because the connection drops, then the system shall store no partial file and display the message ….” – “If a file is larger than 10 MB, then the system shall reject the upload and display the message ‘File too large (at most 10 MB)’.” The concrete values are assumptions and belong in the open questions until clarified.

  5. Why is a single positive test not enough for AC-012-04 (“While an export is running, the system shall disable the export button”)? Which tests belong with it?

    Model answer

    An implementation that always disables the button would pass the positive test. What is needed: button disabled while an export is running (positive); button enabled when no export is running (negative); button becomes enabled again when the export ends (transition). Given “for that user”, additionally: an export by user A does not disable user B’s button.

  6. Why must AC IDs never be reused after a criterion has been removed?

    Model answer

    Tests, commits (Implements:), backlog items, audit events and why-traces refer to the criterion via its ID. If the ID were reused for a new criterion, an old test would suddenly back a completely different criterion, and historical evidence would change its meaning. Stable, never-reused IDs also make the comparison of two versions (new, removed, changed) and drift detection unambiguous.

  7. In a build run, an agent changed the file specs/SPEC-012-csv-export/spec.md and “refined” a criterion. What happens to this change in the factory, and what do you have to do?

    Model answer

    The factory takes the change over after the build as a new draft version (DRAFT), never as an approval; the approved version remains unchanged and continues to apply to bound runs. You review the draft, ideally with the comparison against the approved version, and decide: approve (from MAINTAINER), revise or discard. An agent can never approve a spec itself.

  8. Your project is in spec mode LEICHT. A backlog item refers to SPEC-012, which exists only as a draft. What happens on activation, and what would be different in STRENG?

    Model answer

    In LEICHT the backlog shows a hint, and the build run starts anyway, but without a spec binding: RUN_SPEC_GEBUNDEN is not written, the run log contains a hint, and the agent receives no objective header with bound criteria. In STRENG the factory rejects the activation, because an approved spec is a start condition.

  9. Three child runs of a workflow are building SPEC-012. While the second is running, someone approves version 4. Which version do the child runs work against, and why is that intended? What changes through spec drift detection (since v0.38.0)?

    Model answer

    All child runs work against the version that was bound to the task at start; the factory guarantees that all child runs of a workflow receive the same bound version, even if the spec has been re-approved in the meantime. Otherwise parallel agents would build against different requirements and the integration result would be contradictory. Since v0.38.0, the factory compares version 4 with version 3 at AC level, writes SPEC_DRIFT into the audit chain, marks open backlog items bound to changed or removed criteria as outdated and moves bound tasks that are not finished to WAITING_FOR_REPLAN. A human then decides on replanning.

  10. Transfer: you take over an existing repository without specs and want to switch to STRENG. How do you proceed, and what danger does a reverse spec carry?

    Model answer

    Import the repo, set the spec mode to LEICHT first. Work out specs for the most important areas: by hand, with a normal run that writes under specs/, or with a reverse spec (since v0.38.0) that derives a draft from code and tests. Review every draft from a domain perspective: is the described behaviour intended, or a bug? Resolve assumptions and open questions, then approve. Gradually add AC IDs to existing tests. Only switch to STRENG once the relevant areas have approved specs. The danger of a reverse spec: it describes the current state, bugs included. Approved unchecked, bugs would be declared requirements. That is why it marks assumptions explicitly and is never approved automatically.