Spec files in your repository
The folder layout, Markdown and graph.json formats SpecsGraph commits to your repository, and how to use spec files in code review and CI checks.
Folder layout
Every approved proposal updates one folder in the project's repository, specsgraph/ unless you chose another path. This is the layout for the Northwind Commerce Shop project, trimmed to the Orders context:
specsgraph/
README.md index of the model, generated
glossary.md every term with its definition
actors.md people, systems and agents
graph.json machine-readable graph for tools and CI
contexts/
orders/
context.md purpose, subdomain, services, relationships
requirements/
ORD-12.md one file per requirement, scenarios included
ORD-13.md
decisions/
0007-reserve-stock-before-payment.md
workstreams/
WS-3.mdA context's folder is named after its slug, which is set when the context is created. Renaming the context does not change the slug, so file paths stay the same. A requirement that moves to another context keeps its ID, and its file moves to that context's folder.
File formats
Context and requirement files are Markdown with YAML front matter. The front matter holds the fields a tool needs; the body is written for people and reads well in GitHub's file view, which shows front matter as a table. Decision and workstream files are plain Markdown, as shown in the decision log.
A requirement file
ORD-12 lives at specsgraph/contexts/orders/requirements/ORD-12.md. The scenarios sit in a fenced Gherkin block, so BDD tools and editors highlight them, and links are relative so they work on GitHub and in any clone.
---
id: ORD-12
title: Reserve stock for every line item at checkout
context: orders
service: checkout
state: active
terms: [reservation, customer, line-item]
actors: [customer]
related: [BIL-04]
decisions: ["0007"]
---
# ORD-12 Reserve stock for every line item at checkout
When a Customer starts checkout, the system creates a reservation for the
full quantity of every line item before payment is requested.
## Scenarios
```gherkin
@ORD-12
Feature: Reserve stock for every line item at checkout
Background:
Given the Catalog has 5 units of "Trail mug" in stock
And the Catalog has 1 unit of "Canvas tote" in stock
Scenario: Every line item gets a reservation
Given a Customer has a cart with 2 "Trail mug" and 1 "Canvas tote"
When the Customer starts checkout
Then a reservation holds 2 "Trail mug" for the order
And a reservation holds 1 "Canvas tote" for the order
And the Catalog shows 3 units of "Trail mug" available
Scenario: Checkout stops when a line item cannot be reserved
Given a Customer has a cart with 2 "Canvas tote"
When the Customer starts checkout
Then no reservation is created for the order
And the Customer is told that only 1 "Canvas tote" is left
And payment is not requested
```
## Links
- Term: [Reservation](../../../glossary.md#reservation)
- Actor: [Customer](../../../actors.md#customer)
- Requirement: [BIL-04](../../billing/requirements/BIL-04.md) Capture payment only once stock is reserved
- Decision: [0007 Reserve stock before payment](../../../decisions/0007-reserve-stock-before-payment.md)SpecsGraph writes a requirement file as the requirement will be once its proposal is published, so state is active or, once the requirement is removed from the spec, retired. The Proposed state never appears in a file. A retired requirement keeps its file, so an ID quoted in an old commit still leads somewhere.
graph.json
graph.json holds the same model as data. SpecsGraph writes it with a stable order, so a change to one requirement shows up as a small diff. An excerpt:
{
"schemaVersion": 1,
"project": { "key": "shop", "name": "Shop" },
"contexts": [
{ "key": "ORD", "slug": "orders", "name": "Orders", "subdomain": "core", "owner": "Checkout team" },
{ "key": "BIL", "slug": "billing", "name": "Billing", "subdomain": "supporting", "owner": "Billing team" }
],
"relationships": [
{ "upstream": "CAT", "downstream": "ORD", "patterns": ["customer-supplier"] },
{ "upstream": "ORD", "downstream": "BIL", "patterns": ["partnership"] }
],
"requirements": [
{
"id": "ORD-12",
"title": "Reserve stock for every line item at checkout",
"context": "ORD",
"state": "active",
"file": "contexts/orders/requirements/ORD-12.md",
"scenarios": 2,
"related": ["BIL-04"],
"decisions": ["0007"]
}
]
}Front matter reference
Scripts can rely on these keys. Values that point at other files use the same slugs and IDs as the folder layout.
Requirement files
Context files
Each context folder holds a context.md with the context's fields and its relationships. The body carries the purpose.
---
key: ORD
slug: orders
name: Orders
subdomain: core
owner: Checkout team
services: [checkout, order-history]
upstream:
- { context: CAT, patterns: [customer-supplier] }
- { context: IDN, patterns: [open-host-service] }
downstream:
- { context: BIL, patterns: [partnership] }
- { context: FUL, patterns: [published-language] }
- { context: ANL, patterns: [published-language] }
---
# Orders
Turns a cart into a confirmed order and holds stock while the Customer pays.
Does not ship anything.graph.json reference
Fields with a fixed set of values:
Glossary terms, actors, decisions and workstreams are published as Markdown only, in the files listed above. Scenario text lives in the requirement files; Run scenarios with a BDD tool shows how to extract it.
Treat the folder as generated
SpecsGraph writes these files from the graph on every publish. The graph is where the spec is edited; the folder is its output. If someone edits specsgraph/contexts/orders/requirements/ORD-12.md by hand, the graph does not learn about it, and the next publish that touches ORD-12 stops instead of overwriting the edit, as described in Conflicts and rebasing.
SpecsGraph notices commits to the spec folder that it did not make and shows a warning on the project with the files involved. It does not import them. To keep the change, make it in SpecsGraph as a proposal, so it gets a review, a place in history and links to the rest of the model.
Tip
Moving the folder
Change the spec folder in project settings rather than moving files in Git. The next publish writes to the new path; remove the old folder in an ordinary pull request. Workspaces and projects covers project settings.
Use the files in code review
- Put the rule next to the change. A pull request that changes checkout can link
specsgraph/contexts/orders/requirements/ORD-12.md. The reviewer reads the scenarios and checks the code against them without leaving GitHub. - Review spec changes as diffs. A pull request from
spec/checkout-reserveshows exactly which scenario lines changed. Branches and pull requests describes those pull requests. - Cite IDs. Commit messages and pull requests that name
ORD-12makegit log --grep ORD-12a list of every code change made for that rule. - Run the scenarios. Gherkin tags such as
@ORD-12let a BDD runner execute the scenarios of one requirement; see Run scenarios with a BDD tool.
Use the files in CI
Because graph.json is plain data, a few lines of shell can hold pull requests to the spec. This GitHub Actions workflow fails a pull request whose title and description cite no requirement ID, or cite one that does not exist. Spec pull requests are skipped, and a no-spec label opts out a change such as a dependency bump.
name: Requirement IDs
on:
pull_request:
types: [opened, edited, synchronize, labeled, unlabeled]
jobs:
cite-requirements:
if: ${{ !startsWith(github.head_ref, 'spec/') && !contains(github.event.pull_request.labels.*.name, 'no-spec') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check cited requirement IDs
env:
PR_TEXT: ${{ github.event.pull_request.title }} ${{ github.event.pull_request.body }}
run: |
keys=$(jq -r '.contexts[].key' specsgraph/graph.json | paste -sd '|' -)
ids=$(printf '%s' "$PR_TEXT" | grep -oE "\b($keys)-[0-9]+\b" | sort -u)
if [ -z "$ids" ]; then
echo "Cite at least one requirement ID, for example ORD-12."
exit 1
fi
for id in $ids; do
jq -e --arg id "$id" '.requirements[] | select(.id == $id)' specsgraph/graph.json > /dev/null \
|| { echo "Unknown requirement ID: $id"; exit 1; }
doneThe script builds its pattern from the context keys in graph.json, so ORD-12 counts as a citation and an unrelated token such as UTF-8 does not. The pull request text reaches the script through an environment variable instead of being pasted into it, which keeps a crafted title from running as shell code.
A second guard keeps hand edits out of the spec folder. It fails any pull request that touches specsgraph/ from a branch outside spec/:
name: Spec folder
on:
pull_request:
paths:
- "specsgraph/**"
jobs:
generated-only:
if: ${{ !startsWith(github.head_ref, 'spec/') }}
runs-on: ubuntu-latest
steps:
- run: |
echo "Files in specsgraph/ are generated by SpecsGraph. Propose the change there instead."
exit 1Run scenarios with a BDD tool
Scenarios are published inside the requirement files, in a fenced Gherkin block that already carries the Feature line and the ID tag. A few lines of shell turn them into .feature files that Cucumber and similar runners read, skipping retired requirements:
mkdir -p features
for f in specsgraph/contexts/*/requirements/*.md; do
grep -q '^state: retired' "$f" && continue
awk '/^```gherkin$/ { on = 1; next } /^```$/ { on = 0 } on' "$f" \
> "features/$(basename "$f" .md).feature"
done
# Run the scenarios of one requirement, next to your step definitions
npx cucumber-js features --tags "@ORD-12"Note
GitHub's documentation is the authority on workflow syntax and action versions, and your BDD runner's documentation on its flags. Adjust the examples to your own conventions.
Next steps
- Requirements and scenarios: what goes into a requirement file.
- Branches and pull requests: how spec changes reach your base branch.
- History and versions: read and roll back spec revisions.
- Working well with agents: get agents to cite requirement IDs.