Zum Inhalt springen
Farbschema wählenSprache wählen

Delivery-Spec-Format

Eine Delivery-Spec beschreibt das Verhalten einer Domäne eines Produkts als strukturiertes, maschinell prüfbares YAML: Klauseln in EARS-Form mit stabilen IDs, Tuning-Parameter mit einem freigegebenen Bereich, Szenarien als Test-Orakel, das Domänenmodell, die Designentscheidungen mit den Antworten des Owners und die offenen Rückfragen. Work Items binden sich an ihre Klauseln, und Gates schließen auf Felder, statt Prosa zu parsen. Diese Seite ist die Referenz für die Formatversion 0.2, in der das Format nichts mehr über ein einzelnes Projekt weiß: Ein Projektprofil deklariert den Anforderungskatalog, die ID-Formen, die ein Verweis annehmen darf, und die Einheiten, die ein Parameter verwenden darf — ein zweites Projekt übernimmt das Format also unverändert. Warum es das Format gibt und wie es in die Lieferung passt, steht in Spec-getriebene Lieferung; die Work-Item-Seite und die Bereitschaft stehen im Spec-Bereitschaftsvertrag; was SupaCloud darum herum baut, steht in Wie SupaCloud sich in ein spec-getriebenes Projekt einfügt.

Ein Verzeichnis pro Domäne unter specs/. Nur spec.yaml ist Pflicht.

specs/
project.yaml das Projektprofil: Katalog, ID-Muster, Entscheidungsakten, Einheiten
_schema/ JSON Schemas (Draft 2020-12) und versions.lock.json
_template/ die vollständige Beispieldomäne (TPL) — kopieren, um eine neue zu beginnen
<domain>/ ein Verzeichnis pro Domäne, kleingeschrieben, z. B. econ/
spec.yaml Klauseln, Scope, Glossar, Zustandsautomaten
parameters.yaml Tuning-Werte mit freigegebenem Bereich
scenarios/*.yaml Given/When/Then-Beispiele; das Test-Orakel
model.yaml Entitäten, Commands, Events
decisions.yaml Designentscheidungen und die Antworten des Owners
questions.yaml offene Rückfragen
README.md Erzählung; nicht normativ
Regel Folge
Eine Domäne enthält nur die sechs YAML-Dateitypen oben jede andere *.yaml/*.yml im Domänenverzeichnis wird abgelehnt; ein neuer Dateityp beginnt also immer mit einem Schema
Szenariodateien haben die Endung .yaml eine Datei scenarios/*.yml wird abgelehnt
_schema ist keine Domäne der Validator überspringt es
project.yaml ist das Profil, keine Domäne es ist Pflicht; fehlt es oder ist es fehlerhaft, schlagen Validator und Spec-Gate fehl — siehe Das Projektprofil
Ein Verzeichnis, dessen Name mit _ beginnt (das Template) wird wie eine Domäne validiert und gegated, besitzt aber keine Katalog-ID und macht nie ein Work Item bereit
README.md ist nur Erzählung; nichts darin ist normativ, und es zitiert Klausel-IDs, statt sie zu wiederholen

specs/project.yaml (Schema project/0.2) macht das Format projektneutral. Die JSON Schemas nennen keine projektspezifische ID-Form und keine Einheitenliste; das Profil deklariert sie. Ein zweites Projekt übernimmt das Format also, indem es sein eigenes Profil schreibt, nicht indem es die Schemas ändert. Pfade darin sind relativ zur Repository-Wurzel.

Feld Deklariert
catalogue.source die Katalogdatei, JSON oder YAML, mit benannten ID-Listen
catalogue.lists[] je Liste den key und was sie enthält (holds): requirements, acceptance_criteria oder decisions
catalogue.patterns[] die ID-Formen, die ein satisfies-Verweis annehmen darf: name, pattern und ein example
decision_records[] optional: Aktendateien (glob), deren Dateinamen mit einer ID beginnen (pattern), damit eine nach dem Katalog-Snapshot erfasste Entscheidung ebenfalls als Katalogeintrag zählt
units[] jede Einheit, die ein Parameter verwenden darf: ein name in snake_case und ihre meaning
  • Ein Listeneintrag ist ein ID-String oder ein Objekt mit einer id als String.
  • Ein Muster muss eine vollständige ID treffen, und sein eigenes example muss darauf passen.
  • Jede ID, die der Katalog und die Entscheidungsakten enthalten, muss auf ein deklariertes Muster passen — ein Muster kann also nicht stillschweigend einen Teil des Katalogs ausschließen.
  • Validator, Spec-Gate und die Tiefenprüfung der Work Items lesen den Katalog über diese eine Datei.

Es gibt keinen Standard und keinen Rückfall. Ohne Profil oder mit einem fehlerhaften schlagen Validator, Spec-Gate und die Tiefenprüfung der Work Items fehl und nennen Datei und Feld. Nichts fällt auf den Katalog oder die Einheiten der Referenzimplementierung zurück.

Dass das Format wirklich projektneutral ist, ist ein Test, keine Behauptung: In der Referenzimplementierung wird die Template-Domäne gegen ein zweites, erfundenes Profil validiert, dessen IDs US-<n> und DR-<nnnn> lauten und dessen einzige Einheit cent ist. Dort besteht sie, gegen das Profil der Referenzimplementierung schlägt sie fehl, und dabei werden jeder fremde Verweis und die fremde Einheit benannt (tests/test_spec_schema.py::test_a_domain_validates_against_another_projects_profile).

<DOM> ist der Domänencode: 2 bis 8 Zeichen, ein Großbuchstabe, gefolgt von Großbuchstaben oder Ziffern (^[A-Z][A-Z0-9]{1,7}$). Jede Datei deklariert ihn im Feld domain, und er muss dem Code in spec.yaml entsprechen. IDs sind stabil: Einmal gemergt, wird eine ID nie für etwas anderes wiederverwendet.

Element Muster Beispiel
Klausel <DOM>-R-### ECON-R-012
Parameter <DOM>-P-### ECON-P-003
Szenario <DOM>-S-### ECON-S-021
Zustandsautomat <DOM>-SM-## ECON-SM-01
Entscheidung DEC-<DOM>-### DEC-ECON-002
Rückfrage Q-<DOM>-### Q-ECON-004

Jede ID trägt das Präfix ihrer eigenen Domäne und kommt über alle Dateien der Domäne genau einmal vor.

Die sechs ID-Formen oben gehören zum Format und stehen fest in den Schemas.

Katalog-Verweise. satisfies-Felder zeigen auf den Anforderungskatalog des Projekts — Anforderungen, Abnahmekriterien und Entscheidungsakten — und diese Formen sind Projektdaten, nicht Format. Das Schema verlangt nur ein generisches Token (^[A-Za-z0-9]+(?:[._:-][A-Za-z0-9]+)*$, höchstens 64 Zeichen); das Projektprofil deklariert die Muster, die ein Verweis treffen darf, und der Validator prüft jeden Verweis gegen sie und gegen den Katalog. Ein Verweis, der auf kein deklariertes Muster passt, wird abgelehnt, und ebenso einer, der auf ein Muster passt, aber keinen Katalogeintrag hat. Eine Klausel darf nur eine ID erfüllen, die ihre eigene Spec in scope.satisfies führt.

In der Referenzimplementierung sind die deklarierten Formen Anforderungen (LH-F-023, PH-WLD-006), Abnahmekriterien (LA-17, PA-03, A09-16) und ADRs (ADR-004); ein anderes Projekt deklariert seine eigenen und ändert kein Schema.

Feld Typ Regeln
schema Konstante spec/0.2
domain String der Domänencode
title String mindestens 3 Zeichen
status Enum draft, design-approved, implementing, verified — siehe Status
owner String die Rolle, der die Entscheidungen der Domäne gehören, z. B. project-owner
summary String mindestens 40 Zeichen
scope.satisfies Liste von Katalog-IDs Pflicht, mindestens eine, eindeutig: alles, wofür die Domäne verantwortlich ist
scope.paths Liste von Globs optional: repository-relative Glob-Muster des Codes, der die Domäne umsetzt, im Dialekt der allowed_paths eines Work Items (src/Fm.Economy/**); absolute Pfade, ./, Laufwerksbuchstaben, ..-Segmente und Backslashes werden abgelehnt
scope.excludes Liste von Strings optionale bewusste Nicht-Ziele, je mindestens 10 Zeichen, mit dem, der sie stattdessen besitzt
glossary[] term, definition Begriff mindestens 2, Definition mindestens 10 Zeichen
clauses[] Klausel mindestens eine — siehe Klauseln
state_machines[] Zustandsautomat optional — siehe Zustandsautomaten

Eine Klausel darf nur eine ID erfüllen, die ihre Spec in scope.satisfies führt, und das Spec-Gate verlangt, dass jede ID in scope.satisfies von mindestens einer Klausel erfüllt wird.

Eine Klausel ist eine testbare Aussage in strukturierter EARS-Form. kind legt fest, welche Bedingungsfelder sie trägt; shall ist immer die Reaktion.

kind Bedingungsfelder Liest sich als
ubiquitous keine — when, while, if und where werden abgelehnt Das System soll …
event when Pflicht Wenn …, soll das System …
state while Pflicht Solange …, soll das System …
unwanted if Pflicht Falls …, dann soll das System …
optional where Pflicht Wo …, soll das System …
complex mindestens zwei von when, while, if, where Solange …, wenn …, soll das System …
Feld Regeln
id <DOM>-R-###
kind eine der sechs Arten oben
when, while, if, where je mindestens 3 Zeichen, wie die Art es verlangt
shall mindestens 10 Zeichen: was das System tun soll
satisfies mindestens eine Katalog-ID, eindeutig, jede in scope.satisfies geführt
parameters optionale Parameter-IDs, von denen die Klausel abhängt; jede muss in parameters.yaml existieren
decisions optionale Entscheidungs-IDs, von denen die Klausel abhängt; jede muss in decisions.yaml existieren
rationale optionaler Freitext

Eine Zahl in einer Klausel ist ein Parameter, nie ein Literal im Text. Weil eine Klausel die Parameter und Entscheidungen nennt, von denen sie abhängt, lässt sich eine Änderung an ihnen zu jeder betroffenen Klausel zurückverfolgen.

Feld Regeln
id <DOM>-SM-##
name snake_case
states mindestens zwei, eindeutig, snake_case
initial einer der Zustände
final optionale Liste von Zuständen; ein Übergang darf einen Endzustand nicht verlassen
transitions[] mindestens einer: from und to (Zustände), trigger (ein Command oder Event in PascalCase), optional guard-Text und optional clause (eine Klausel dieser Spec)

Der Übergangsschlüssel heißt trigger statt on, weil YAML 1.1 einen nackten Schlüssel on als Boolean liest.

Tuning-Werte mit freigegebenem Bereich. Werte sind Ganzzahlen, passend zu einem Determinismusvertrag, der Gleitkommazahlen aus dem Zustand heraushält.

Feld Regeln
id <DOM>-P-###
name snake_case
description mindestens 10 Zeichen
unit ein Einheitenname in snake_case (^[a-z][a-z0-9_]*$), den das Projektprofil deklariert; im Schema steht keine feste Liste
type int oder fixed
scale Ganzzahl ≥ 1; Pflicht bei fixed: die gespeicherte Ganzzahl ist der Wert mal der Skala
default, min, max Ganzzahlen; mindefaultmax
status proposed oder approved
decided_by eine Entscheidung dieser Domäne; Pflicht bei approved
notes optional

Ein Verhältnis von 0,125, gespeichert als type: fixed mit scale: 1000, ist die Ganzzahl 125. min und max sind der freigegebene Bereich: Innerhalb davon ist Tuning frei und braucht niemanden; eine Änderung des Bereichs selbst ist eine Designentscheidung.

Ein Szenario ist ein konkretes Beispiel mit Fixture-Werten. Szenarien sind das Test-Orakel: Aus ihnen werden Tests generiert, die erwarteten Werte kommen also aus der Spec und nicht von dem Agenten, der sie umsetzt. In der Referenzimplementierung ist der Generator geplant (bw-fm27 WI-SPEC-004); das Format ist fertig.

Feld Regeln
schema scenarios/0.1
scenarios[].id <DOM>-S-###
title mindestens 5 Zeichen
covers mindestens eine Klausel-ID dieser Domäne, eindeutig
given ein Objekt: der Zustand vor dem Schritt, als Daten
when[] mindestens ein Schritt: command (PascalCase) und optional ein args-Objekt, in Reihenfolge
then[] mindestens eine Prüfung: path (gepunkteter Pfad in den Ergebniszustand oder das Command-Ergebnis), op und value

op ist eines von eq, ne, lt, lte, gt, gte, contains, absent, rejected. value ist bei jedem op außer absent und rejected Pflicht.

Entitäten, Commands und Events mit Nutzlastfeldern. Eine additive Änderung erweitert die Verträge des Projekts; eine brechende Änderung ist eine Architekturentscheidung.

Feld Regeln
entities[] name (PascalCase), optional description, fields (mindestens eines), optional invariants (je mindestens 10 Zeichen)
commands[], events[] name (PascalCase), optional description, fields, optional emits (Event-Namen dieses Modells), optional clause (eine Klausel dieser Spec)
fields[] name (snake_case), type, optional description, optional das Flag optional

Ein Feld-type ist int, fixed, bool, text, date, tick, money, id<Entity>, enum<a|b|c> oder list<…>. money ist eur_cent als 64-Bit-Ganzzahl; id<Entity> ist eine typisierte stabile ID. Namen sind über Entitäten, Commands und Events eindeutig, Feldnamen innerhalb eines Elements.

Eine offene Designfrage, die Empfehlung und die Antwort des Owners.

Feld Regeln
id DEC-<DOM>-###
title mindestens 3 Zeichen
question mindestens 10 Zeichen
blocking optionaler Boolean: Die Umsetzung kann nicht beginnen, bevor dies beantwortet ist
options[] 2 bis 12, jede mit key (snake_case, eindeutig), title, description
recommended der Schlüssel einer Option
rationale mindestens 10 Zeichen: warum die Empfehlung
references optionale Liste von Strings
status proposed, approved oder superseded
answer Pflicht bei approved: choice (ein Optionsschlüssel), answered_by, answered_at (Datum und Uhrzeit), optional note und source (wo sie gegeben wurde, z. B. ein Fragebogen oder eine Question-Gate-ID)
superseded_by Pflicht bei superseded: eine andere Entscheidung dieser Domäne

Die Empfehlung gehört zum Datensatz, ist aber nicht die Antwort. Eine Entscheidung ist erst beantwortet, wenn der Owner answer ausgefüllt hat — eine vorausgewählte Empfehlung, die niemand bestätigt hat, bleibt proposed.

Eine Lücke, die beim Spezifizieren oder Umsetzen auffällt. Sie wird hier aufgeworfen und nie durch eine Annahme im Code erledigt.

Feld Regeln
id Q-<DOM>-###
question mindestens 10 Zeichen
context optional
clauses optionale Klausel-IDs dieser Domäne, die die Rückfrage betrifft
raised_by, raised_at wer sie gestellt hat, und wann (Datum und Uhrzeit)
status open, answered oder withdrawn
answer, decision eine answered-Rückfrage nennt ihre Antwort, die Entscheidung, die sie geklärt hat, oder beides

spec.yaml trägt genau einen Status:

Status Bedeutung Was das Gate durchsetzt
draft in Arbeit; nicht bindend nichts über die Gültigkeit hinaus
design-approved der Owner hat die Designentscheidungen getroffen; Work Items dürfen abgeleitet werden keine Entscheidung proposed, keine Rückfrage open
implementing Work Items setzen sie um wie design-approved
verified die Umsetzung ist gegen sie nachgewiesen wie design-approved

Eine Entscheidung, die wieder geöffnet, oder eine Rückfrage, die gestellt wird, während eine Spec design-approved oder weiter ist, färbt das Spec-Gate rot, bis sie beantwortet ist oder die Spec auf draft zurückgeht. Die Bereitschaft folgt denselben Fakten: Ein Work Item, dessen Entscheidung nicht mehr freigegeben ist oder dessen Spec wieder bei draft steht, ist nicht mehr bereit. Noch setzt keine Regel einen Unterschied zwischen implementing und verified durch; das ist Aufgabe einer späteren Schemaversion.

Der Validator weist nach, dass eine Domäne wohlgeformt ist. Das Spec-Gate fügt die Regeln hinzu, die eine Spec an ihre Anforderungen, ihre Tests und ihren Code binden. In der Referenzimplementierung läuft es in fmctl validate all und allein als fmctl validate specs; jeder Befund nennt Datei, Feld, Domäne und die beteiligten IDs.

Regel Schlägt fehl, wenn
Abdeckung eine ID in scope.satisfies von keiner Klausel der Domäne erfüllt wird
Szenarien eine Klausel kein abdeckendes Szenario hat oder ein covers-Verweis keine Klausel der Domäne nennt
Status eine Spec bei design-approved oder weiter noch eine Entscheidung proposed oder eine Rückfrage open enthält
Drift eine Änderung unter scope.paths ohne Commit-Nachricht kommt, die eine Klausel der Domäne nennt
Keine Spec es gar kein Domänenverzeichnis gibt und das Gate sonst bestünde, weil es nichts trifft
Profil specs/project.yaml fehlt oder ist fehlerhaft, sodass kein Verweis und keine Einheit geprüft werden konnte

Katalog-IDs, die keine Domäne in scope.satisfies führt, werden mit ID gemeldet. Das ist eine Information, kein Fehler, aber ihre Anzahl ist in docs/requirements/spec-coverage-baseline.json festgeschrieben — und diese Baseline muss der aktuellen Anzahl gleichen, sie nicht nur begrenzen. Eine Anzahl über der Baseline ist ein Rückschritt; eine Baseline über der Anzahl ist Spielraum, der den nächsten Rückschritt unbemerkt durchlassen würde. Ein Pull Request, der einer Domäne neue Katalog-IDs gibt, schreibt die Baseline deshalb in derselben Änderung mit --write-baseline neu. (Die übrigen Baselines der Referenzimplementierung — unbelegte Anforderungen und Abnahmekriterien, flache und nicht migrierte Work Items — dürfen ohne diese Gleichheitsregel nur schrumpfen: Sie dürfen frei fallen, und nur eine anzuheben braucht eine benannte menschliche Entscheidung.)

Drift vergleicht eine Basis mit HEAD: die Dateien aus git diff <base>...HEAD und die Klausel-IDs in den Nachrichten von git log <base>..HEAD. Die Basis ist --base <ref>, sonst die Umgebungsvariable FM_BASE_REF, sonst die Pull-Request-Basis in GITHUB_BASE_REF. Eine explizite Basis, die nicht auflöst, schlägt fehl; ohne Basis oder wenn einem flachen Checkout die Pull-Request-Basis fehlt, wird die Drift-Regel übersprungen, und das Gate sagt, warum. Ein Commit, der Domänencode ändert, nennt die Klausel, die er umsetzt, z. B. feat(econ): pay wages weekly (ECON-R-012).

Terminal-Fenster
uv run python tools/qa/check_specs.py # das Spec-Gate, wie validate all es ausführt
uv run python tools/qa/check_specs.py --base origin/main # mit Drift gegen main
uv run python tools/qa/check_specs.py --write-baseline # nachdem eine Domäne Katalog-IDs übernimmt
uv run python tools/specs/validate.py # jede Domäne plus der Versions-Lock
uv run python tools/specs/validate.py specs/econ # eine Domäne
uv run python tools/specs/validate.py --write-lock # nach einer Schemaänderung
uv run python tools/specs/validate.py --init-lock # nur, wenn es noch keinen Lock gibt

Über die JSON Schemas hinaus prüft der Validator, was ein Schema nicht ausdrücken kann: IDs, die zu ihrer Domäne gehören und einmal vorkommen, Verweise, die auflösen (Katalog, Parameter, Entscheidungen, Klauseln, Events), mindefaultmax, Zustandsautomaten, deren Zustände existieren, recommended und answer.choice, die echte Optionsschlüssel nennen, und superseded_by, das eine andere Entscheidung nennt.

Versionierung. Jedes Schema verlangt seine eigene Version im Feld schema. In Format 0.2 sind das spec/0.2, parameters/0.2 und project/0.2; scenarios, model, decisions und questions haben sich nicht geändert und bleiben bei scenarios/0.1, model/0.1, decisions/0.1 und questions/0.1. Ein Dokument, das eine ältere Version nennt, wird mit der anzuwendenden Migration abgelehnt — der Befund zeigt auf Migration, nicht auf den Fehler des Schemas selbst.

Der Versions-Lock. specs/_schema/versions.lock.json hält die akzeptierende Oberfläche jedes Schemas pro Schema und Version als sortierte Token-Liste fest. Was er sieht:

Token Hält fest
clauses[].id ein Feld
clauses[].id!required ein immer verpflichtendes Feld
clauses[].kind=event einen Enum-Wert
schema==spec/0.2 einen Konstantenwert
domain~^[A-Z][A-Z0-9]{1,7}$ ein Muster
summary@minLength=40 eine Längen-, Anzahl- oder Zahlengrenze
clauses@type=array einen Typ oder eine Typliste
questions[].raised_at@format=date-time ein Format
clauses[].decisions@uniqueItems=true Elemente, die eindeutig sein müssen
clauses[]@additionalProperties=false die Regel für Felder, die das Schema nicht nennt
clauses[].when!required if kind==event ein unter einer if-Bedingung verpflichtendes Feld
clauses[]!if kind==complex then anyOf(…) jede weitere Regel, die ein if/then- oder else-Zweig hinzufügt

Grenzen sind minLength, maxLength, minItems, maxItems, minimum und maximum; ein else-Zweig liest sich als if not(…). Beschreibungen gehören nicht zur Oberfläche.

Welche Richtung brechend ist. Eine Änderung, die ein Dokument ablehnen kann, das die Version davor angenommen hat, ist brechend und braucht eine Versionserhöhung; --write-lock weigert sich sonst, sie festzuhalten:

Brechend — braucht eine Versionserhöhung Additiv — braucht nur eine Lock-Aktualisierung
ein Feld entfernt oder umbenannt ein neues Feld
ein Enum-Wert entfernt ein neuer Enum-Wert
ein Feld verpflichtend gemacht, immer oder unter einer Bedingung ein verpflichtendes Feld optional gemacht
eine neue bedingte Regel (if/then, anyOf, not) eine bedingte Regel entfernt
ein Enum oder eine Konstante auf einem Feld, das keine hatte; eine geänderte Konstante ein Enum oder eine Konstante entfernt
ein erhöhtes Minimum, ein gesenktes Maximum ein gesenktes Minimum, ein erhöhtes Maximum
jede Änderung eines Musters — Verschärfen lässt sich allgemein nicht von Lockern unterscheiden ein Muster entfernt
ein verengter Typ oder ein Typ auf einem Feld, das keinen hatte ein erweiterter Typ
ein neues oder geändertes Format ein Format entfernt
neues uniqueItems uniqueItems entfernt
ein Objekt, das unbenannte Felder ausschließt oder eine Regel für sie bekommt ein Objekt wieder geöffnet

Eine Einschränkung, die zusammen mit einem neuen Feld kommt, ist additiv, weil die Version davor das Feld überhaupt abgelehnt hat.

Der Lock selbst lässt sich nicht waschen. Ein fehlender Lock lässt die Validierung fehlschlagen. --write-lock aktualisiert nur einen bestehenden Lock; den Lock zu löschen kann eine brechende Änderung also nicht in einen frischen, unschuldig wirkenden verwandeln. --init-lock schreibt den ersten Lock und weigert sich, einen bestehenden zu überschreiben.

Die Schemas nennen keinen Anbieter, kein Modell und kein Werkzeug, eine Spec bindet Arbeit also nie an einen Provider.

  1. specs/project.yaml anlegen, wenn das Repository keines hat: die Katalogquelle und ihre Listen, ein Muster je ID-Form, die satisfies-Verweise verwenden, die Entscheidungsakten, falls es sie gibt, und jede Einheit, die die Parameter verwenden. Das Profil der Referenzimplementierung ist ein vollständiges Beispiel.
  2. In jeder spec.yaml schema: spec/0.2 setzen. Sonst ändert sich nichts: satisfies-Verweise behalten ihre IDs, die nun gegen das Profil statt gegen ein festes Muster geprüft werden.
  3. In jeder parameters.yaml schema: parameters/0.2 setzen. Einheiten sind keine feste Liste mehr; jede, die ein Parameter verwendet, muss im Profil deklariert sein.
  4. scenarios-, model-, decisions- und questions-Dateien bei 0.1 lassen.
  5. Den Validator laufen lassen; er meldet jede Datei, die noch auf einer alten Version steht, und jeden Verweis und jede Einheit, die das Profil nicht deklariert.

Das Template modelliert ein Hauptbuch mit Konten und einem Überweisungs-Command, sodass jeder Dateityp einmal mit echtem Inhalt vorkommt. Um eine Domäne zu beginnen, kopiere es nach specs/<domain>/, ersetze den Code TPL in jeder Datei und jeder ID und schreibe dann die Klauseln, ein Szenario pro Verhalten, die Parameter und das Modell. Richte scope.paths auf den Code, der die Domäne umsetzt. Jede offene Wahl kommt mit einer empfohlenen Option in decisions.yaml, jede später gefundene Lücke in questions.yaml. Der Inhalt der Dateien ist Englisch, wie jedes Artefakt im Repository.

Das Profil kommt zuerst, denn jeder satisfies-Verweis und jede Einheit unten wird dagegen geprüft. Dieses hier ist das der Referenzimplementierung, gekürzt auf die Einheiten, die das Template verwendet:

specs/project.yaml
schema: project/0.2
catalogue:
source: docs/requirements/catalog.json
lists:
- key: requirements
holds: requirements
- key: acceptance_criteria
holds: acceptance_criteria
- key: architecture_decisions
holds: decisions
patterns:
- name: requirement
pattern: "(PH|LH)-[A-Z]{1,4}-\\d{3}"
example: PH-WLD-006
- name: acceptance_criterion
pattern: "(LA|PA|A09)-\\d{2}"
example: LA-17
- name: architecture_decision
pattern: "ADR-\\d{3}"
example: ADR-004
decision_records:
# Ein nach dem Katalog-Snapshot erfasster ADR zählt weiterhin als Katalogeintrag.
- glob: docs/adr/ADR-*.md
pattern: "ADR-\\d{3}"
units:
- name: eur_cent
meaning: Money as an integer number of euro cents.
specs/_template/spec.yaml
schema: spec/0.2
domain: TPL
title: Template domain - a two-account ledger
status: draft
owner: project-owner
summary: >-
A minimal but complete example of a domain spec. It models a ledger with accounts and a
transfer command, so every file type of the format appears once with real content. Copy this
directory to specs/<domain>/, change the domain code everywhere, and replace the content.
scope:
satisfies:
- PH-WLD-006
- ADR-004
# The code that implements this domain. The template has none, so the glob names a project
# that does not exist; a real domain lists its kernel and test projects here.
paths:
- src/Fm.TemplateLedger/**
excludes:
- Currency conversion, which a real economy domain decides in its own decisions.yaml.
glossary:
- term: Posting
definition: One balanced booking that debits one account and credits another by the same amount.
clauses:
- id: TPL-R-001
kind: ubiquitous
shall: The ledger shall keep the sum of all account balances equal to the money created minus the money destroyed.
satisfies:
- PH-WLD-006
- id: TPL-R-002
kind: event
when: a TransferMoney command names two existing accounts and a positive amount
shall: the ledger shall debit the source and credit the target by exactly that amount in one posting.
satisfies:
- PH-WLD-006
parameters:
- TPL-P-001
- id: TPL-R-003
kind: unwanted
if: a transfer would take the source account below its overdraft limit
shall: then the ledger shall reject the command with the code overdraft_exceeded and leave both balances unchanged.
satisfies:
- PH-WLD-006
parameters:
- TPL-P-001
decisions:
- DEC-TPL-001
- id: TPL-R-004
kind: ubiquitous
shall: The ledger shall represent every amount as an integer number of euro cents.
satisfies:
- ADR-004
state_machines:
- id: TPL-SM-01
name: posting
states:
- requested
- posted
- rejected
initial: requested
final:
- posted
- rejected
transitions:
- from: requested
to: posted
trigger: TransferMoney
guard: within the overdraft limit
clause: TPL-R-002
- from: requested
to: rejected
trigger: TransferMoney
guard: beyond the overdraft limit
clause: TPL-R-003
specs/_template/parameters.yaml
schema: parameters/0.2
domain: TPL
parameters:
- id: TPL-P-001
name: overdraft_limit
description: How far below zero an account may go before transfers out of it are rejected.
unit: eur_cent
type: int
default: 0
min: 0
max: 100000000
status: approved
decided_by: DEC-TPL-001
specs/_template/scenarios/transfers.yaml
schema: scenarios/0.1
domain: TPL
scenarios:
- id: TPL-S-001
title: A transfer moves money and keeps the ledger balanced
covers:
- TPL-R-001
- TPL-R-002
- TPL-R-004
given:
accounts:
club: 50000
sponsor: 0
overdraft_limit: 0
when:
- command: TransferMoney
args:
from: club
to: sponsor
amount: 12500
then:
- path: accounts.club
op: eq
value: 37500
- path: accounts.sponsor
op: eq
value: 12500
- path: ledger.total
op: eq
value: 50000
- id: TPL-S-002
title: A transfer beyond the overdraft limit is rejected and changes nothing
covers:
- TPL-R-003
given:
accounts:
club: 1000
sponsor: 0
overdraft_limit: 0
when:
- command: TransferMoney
args:
from: club
to: sponsor
amount: 1001
then:
- path: result
op: rejected
- path: result.code
op: eq
value: overdraft_exceeded
- path: accounts.club
op: eq
value: 1000
specs/_template/model.yaml
schema: model/0.1
domain: TPL
entities:
- name: Account
description: A ledger account owned by one party.
fields:
- name: id
type: id<Account>
- name: balance
type: money
- name: overdraft_limit
type: money
invariants:
- A balance never falls below the negative of the account's overdraft limit.
commands:
- name: TransferMoney
description: Move an amount from one account to another in a single posting.
fields:
- name: from
type: id<Account>
- name: to
type: id<Account>
- name: amount
type: money
emits:
- MoneyTransferred
clause: TPL-R-002
events:
- name: MoneyTransferred
fields:
- name: from
type: id<Account>
- name: to
type: id<Account>
- name: amount
type: money
specs/_template/decisions.yaml
schema: decisions/0.1
domain: TPL
decisions:
- id: DEC-TPL-001
title: Overdraft
question: May an account go below zero, and if so, how far?
blocking: true
options:
- key: no_overdraft
title: No overdraft
description: Every transfer that would take an account below zero is rejected.
- key: limited_overdraft
title: Limited overdraft per account
description: Each account carries a limit; the default limit is a parameter with an approved range.
recommended: limited_overdraft
rationale: A limit keeps the rule data-driven and covers both cases, because a limit of zero is the same as no overdraft.
references:
- PH-WLD-006
status: approved
answer:
choice: limited_overdraft
answered_by: project-owner
answered_at: "2026-09-22T00:00:00Z"
source: template example
specs/_template/questions.yaml
schema: questions/0.1
domain: TPL
questions:
- id: Q-TPL-001
question: Does a rejected transfer still produce an event for the audit trail?
context: Raised while writing TPL-R-003; the clause says the balances stay unchanged but not whether anything is recorded.
clauses:
- TPL-R-003
raised_by: spec author
raised_at: "2026-09-22T00:00:00Z"
status: answered
answer: No domain event; the rejection is returned to the caller and logged by the command pipeline.

Zusammen gelesen zeigt das Beispiel jeden Verweis, dem die Gates folgen: TPL-R-003 hängt vom Parameter TPL-P-001 und der Entscheidung DEC-TPL-001 ab; der Parameter wurde durch eben diese Entscheidung freigegeben; TPL-S-002 deckt die Klausel ab und ist das Orakel für ihren Test; der Zustandsautomat nennt die Klauseln hinter seinen Übergängen; und Q-TPL-001 hält eine Lücke fest, die beantwortet statt angenommen wurde.