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 |
Das Projektprofil
Abschnitt betitelt „Das Projektprofil“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
idals String. - Ein Muster muss eine vollständige ID treffen, und sein eigenes
examplemuss 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.
spec.yaml
Abschnitt betitelt „spec.yaml“| 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.
Klauseln
Abschnitt betitelt „Klauseln“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.
Zustandsautomaten
Abschnitt betitelt „Zustandsautomaten“| 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.
parameters.yaml
Abschnitt betitelt „parameters.yaml“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; min ≤ default ≤ max |
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.
scenarios/*.yaml
Abschnitt betitelt „scenarios/*.yaml“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.
model.yaml
Abschnitt betitelt „model.yaml“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.
decisions.yaml
Abschnitt betitelt „decisions.yaml“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.
questions.yaml
Abschnitt betitelt „questions.yaml“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.
Das Spec-Gate
Abschnitt betitelt „Das Spec-Gate“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).
Validierung und Versions-Lock
Abschnitt betitelt „Validierung und Versions-Lock“uv run python tools/qa/check_specs.py # das Spec-Gate, wie validate all es ausführtuv run python tools/qa/check_specs.py --base origin/main # mit Drift gegen mainuv run python tools/qa/check_specs.py --write-baseline # nachdem eine Domäne Katalog-IDs übernimmtuv run python tools/specs/validate.py # jede Domäne plus der Versions-Lockuv run python tools/specs/validate.py specs/econ # eine Domäneuv run python tools/specs/validate.py --write-lock # nach einer Schemaänderunguv 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), min ≤ default ≤ max, 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.
Migration von 0.1 auf 0.2
Abschnitt betitelt „Migration von 0.1 auf 0.2“specs/project.yamlanlegen, wenn das Repository keines hat: die Katalogquelle und ihre Listen, ein Muster je ID-Form, diesatisfies-Verweise verwenden, die Entscheidungsakten, falls es sie gibt, und jede Einheit, die die Parameter verwenden. Das Profil der Referenzimplementierung ist ein vollständiges Beispiel.- In jeder
spec.yamlschema: spec/0.2setzen. Sonst ändert sich nichts:satisfies-Verweise behalten ihre IDs, die nun gegen das Profil statt gegen ein festes Muster geprüft werden. - In jeder
parameters.yamlschema: parameters/0.2setzen. Einheiten sind keine feste Liste mehr; jede, die ein Parameter verwendet, muss im Profil deklariert sein. scenarios-,model-,decisions- undquestions-Dateien bei 0.1 lassen.- 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.
Durchgearbeitetes Beispiel: die Template-Domäne
Abschnitt betitelt „Durchgearbeitetes Beispiel: die Template-Domäne“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:
schema: project/0.2catalogue: 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-004decision_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.schema: spec/0.2domain: TPLtitle: Template domain - a two-account ledgerstatus: draftowner: project-ownersummary: >- 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-004state_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-003schema: parameters/0.2domain: TPLparameters: - 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-001schema: scenarios/0.1domain: TPLscenarios: - 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: 1000schema: model/0.1domain: TPLentities: - 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-002events: - name: MoneyTransferred fields: - name: from type: id<Account> - name: to type: id<Account> - name: amount type: moneyschema: decisions/0.1domain: TPLdecisions: - 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 exampleschema: questions/0.1domain: TPLquestions: - 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.