Configuration
Every project needs a specdown.json. It tells specdown where specs live,
which adaptersdepends to launch, and what
reporters to generate.
Place specdown.json at the project root, next to .git/. All paths
inside the config are resolved relative to the config file's directory,
so a root-level config can reference specs in any subdirectory (e.g.
"entry": "docs/specs/index.spec.md"). This also makes the config easy
to find for both humans and tools — specdown run looks for
specdown.json in the current directory by default.
The config is data-only JSON — no scripting, no language runtime dependency. For v1, a single file is sufficient.
{
"entry": "specs/index.spec.md",
"adapters": [
{
"name": "myapp",
"command": ["python3", "./tools/adapter.py"],
"blocks": ["run:myapp"],
"checks": ["user-exists"]
}
]
}
The models and reporters fields can be included to override defaults.
See Defaults.
Entry File
The entry field points to a Markdown file that serves as the starting point for recursive crawling.
If it has an H1 heading, that becomes the entry page title.
Markdown links to .md and .spec.md files are followed recursively to discover all pages.
Generate report from entry file and verify title
cat <<'SPEC' > entry-test.spec.md
# My Feature
Some prose.
SPEC
printf '# My Project Title\n\n- [Feature](entry-test.spec.md)\n' > entry-index.spec.md
cat <<'CFG' > entry-test-cfg.json
{"entry":"entry-index.spec.md","adapters":[],"reporters":[{"builtin":"html","outFile":"entry-report"}]}
CFG
specdown run -config entry-test-cfg.json 2>&1 || trueThe H1 heading from the entry file appears as that page's title.
Built-in Shell Adapter
The shell adapter is built into specdown. run:shell blocks
work without any adapter configuration.
Run a spec using the built-in shell adapter with no adapter config
printf '# T\n\n- [S](builtin-shell-test.spec.md)\n' > builtin-shell-index.spec.md
BT=$(printf '\140\140\140')
printf '%s\n' '# Builtin Shell' '' "$BT"'run:shell' '$ echo works' 'works' "$BT" > builtin-shell-test.spec.md
printf '{"entry":"builtin-shell-index.spec.md","adapters":[]}' > builtin-shell-cfg.json
specdown run -config builtin-shell-cfg.json 2>&1 || trueIf a user adapter explicitly claims a shell block (e.g., "blocks": ["run:shell"]),
the user adapter takes precedence over the built-in.
Built-in jq Check
The check:jq check is built into specdown. It evaluates
jq expressions against JSON data and
compares the result with an expected value. No adapter configuration is
required.
Columns (or check parameters):
| Column | Description |
|---|---|
input |
JSON string to evaluate against |
expr |
jq expression |
expected |
Expected result |
Use input as a check parameter when every row operates on the same JSON:
echo '{"name":"Alice","age":30,"tags":["admin","user"]}'| expr | expected |
|---|---|
.name | Alice |
.age | 30 |
.tags | ["admin","user"] |
Or use input as a column when rows have different inputs:
echo '{"city":"Seoul"}'| input | expr | expected |
|---|---|---|
{"name":"Alice","age":30,"tags":["admin","user"]} | .name | Alice |
{"city":"Seoul"} | .city | Seoul |
Full jq expressions are supported — pipes, filters, and boolean conditions all work:
| expr | expected |
|---|---|
.tags | length | 2 |
.age > 18 | true |
Array and object comparisons are whitespace-insensitive and key-order-insensitive.
If a user adapter explicitly claims check:jq, the user adapter takes
precedence over the built-in.
Config Fields
| Field | Description |
|---|---|
entry |
Path to the entry Markdown file. Starting point for recursive link crawling |
adapters |
List of adapters that handle executable blocks and checks |
reporters |
Output generators. html and json builtins provided |
models |
Alloy model verification (default: alloy). Accepted for explicit configuration; omit to use the default. Set models.jarPath to use a local Alloy JAR instead of auto-downloading |
ignorePrefixes |
List of code block prefixes to suppress unknown-prefix warnings for |
trace |
Traceability configuration. See Traceability |
toc |
Sidebar table-of-contents grouping. See TOC Grouping below |
setup |
Shell command to run once before any specs execute |
teardown |
Shell command to run once after all specs finish (runs even on failure) |
defaultTimeoutMsec |
Default adapter request timeout in milliseconds (default: 30000). 0 disables the time limit |
Global Setup and Teardown
The setup and teardown fields run shell commands before and after the
entire spec run. This is useful for managing test infrastructure — starting
databases, launching containers, seeding data, or cleaning up afterwards.
{
"setup": "docker compose up -d && sleep 2",
"teardown": "docker compose down",
"entry": "specs/index.spec.md",
"adapters": []
}
The setup command runs before any spec execution begins. If it fails, specdown exits immediately without running specs.
Verify setup runs before specs
mkdir -p setup-test
printf '# T\n\n- [S](s.spec.md)\n' > setup-test/index.spec.md
printf '# S\n\nProse.\n' > setup-test/s.spec.md
cat <<'CFG' > setup-test/specdown.json
{"entry": "index.spec.md", "setup": "echo SETUP-RAN > setup-marker.txt"}
CFG
specdown run -config setup-test/specdown.json 2>&1 || trueThe teardown command runs after specs complete, even if specs fail.
Verify teardown runs after specs (even on failure)
mkdir -p teardown-test
printf '# T\n\n- [S](s.spec.md)\n' > teardown-test/index.spec.md
BT=$(printf '\140\140\140')
printf '%s\n' '# S' '' "$BT"'run:shell' '$ echo hello' 'wrong-output' "$BT" > teardown-test/s.spec.md
cat <<'CFG' > teardown-test/specdown.json
{"entry": "index.spec.md", "setup": "echo SETUP-OK > marker.txt", "teardown": "echo TEARDOWN-RAN >> marker.txt"}
CFG
specdown run -config teardown-test/specdown.json 2>&1 || trueA failing setup command prevents spec execution and exits with an error.
Verify failing setup aborts the run
mkdir -p setup-fail-test
printf '# T\n\n- [S](s.spec.md)\n' > setup-fail-test/index.spec.md
printf '# S\n\nProse.\n' > setup-fail-test/s.spec.md
cat <<'CFG' > setup-fail-test/specdown.json
{"entry": "index.spec.md", "setup": "exit 1"}
CFG
! specdown run -config setup-fail-test/specdown.json 2>/dev/nullAdapter Fields
| Field | Description |
|---|---|
name |
Unique identifier for the adapter |
command |
Array of strings — the executable and its arguments |
blocks |
List of block prefixes this adapter handles (e.g. ["run:myapp"]) |
checks |
List of check names this adapter handles (e.g. ["user-exists"]) |
Reporter Fields
| Field | Description |
|---|---|
builtin |
Reporter type: "html" or "json" |
outFile |
Output path. For HTML, this is a directory; for JSON, a file path |
Models Fields
The entire models object is optional. When omitted, alloy is enabled
by default.
| Field | Description |
|---|---|
builtin |
Model checker: only "alloy" is supported |
Defaults
When fields are omitted from a config file, sensible defaults are applied:
| Field | Default |
|---|---|
entry |
specs/index.spec.md |
adapters |
[] (empty — built-in shell adapter handles run:shell) |
models.builtin |
"alloy" |
reporters |
[{"builtin":"html","outFile":"specs/report"}, {"builtin":"json","outFile":"specs/report.json"}] |
ignorePrefixes |
[] (empty) |
trace |
not set (traceability disabled) |
toc |
not set (auto-group by directory when subdirectories exist; flat otherwise) |
setup |
not set (no pre-run command) |
teardown |
not set (no post-run command) |
defaultTimeoutMsec |
30000 (30 seconds) |
An empty config {} is valid — all fields are defaulted.
Verify empty config applies defaults
mkdir -p defaults-test
printf '# T\n\n- [S](s.spec.md)\n' > defaults-test/specs/index.spec.md
mkdir -p defaults-test/specs
printf '# T\n\n- [S](s.spec.md)\n' > defaults-test/specs/index.spec.md
printf '# S\n\nProse.\n' > defaults-test/specs/s.spec.md
echo '{}' > defaults-test/specdown.json
cd defaults-test && specdown run -dry-run 2>&1 | grep 'spec(s)'specdown runs without a config file when specs/index.spec.md exists.
Verify specdown works with no config file
rm -rf no-config-test && mkdir -p no-config-test/specs
printf '# T\n\n- [S](s.spec.md)\n' > no-config-test/specs/index.spec.md
printf '# S\n\nProse.\n' > no-config-test/specs/s.spec.md
cd no-config-test && specdown run -dry-run 2>&1 | grep 'spec(s)'TOC Grouping
The toc field controls how documents are organized in the HTML report sidebar.
Each entry is either a string (standalone document) or a group object with a name
and a list of document paths.
{
"toc": [
{ "group": "Core", "docs": ["specs/syntax.spec.md", "specs/cli.spec.md"] },
{ "group": "Advanced", "docs": ["specs/alloy.spec.md", "specs/traceability.spec.md"] },
"specs/overview.spec.md"
]
}
String entries appear as ungrouped items. Group entries render as collapsible sections in the sidebar. The current document's group is expanded by default; others are collapsed.
Status propagation: if any document in a group has a failed test case, the group header shows a red status dot. Expected-fail propagates similarly.
Type badges: when a document has a frontmatter type field, a small
colored badge appears next to its title in the sidebar.
Auto-grouping fallback: documents not listed in toc are automatically
grouped by their directory. Documents in the same directory as the entry file
remain ungrouped; documents in subdirectories form groups named after the
directory (e.g., specs/stories/ becomes "Stories").
When toc is omitted entirely, auto-grouping by directory is applied if the
spec tree spans multiple directories. If all documents are in a single
directory, the sidebar renders as a flat list (backward-compatible).
Validation
The routing model guarantees that no block prefix is handled by more than one adapter. User adapters have exclusive claims, and the built-in adapter yields to any user adapter that claims the same prefix.
module routing
sig Prefix {}
sig Adapter { handles: set Prefix }
one sig Builtin extends Adapter {}
-- user adapters never overlap
fact exclusiveUserClaims {
all disj a1, a2: Adapter - Builtin |
no a1.handles & a2.handles
}
-- builtin yields when a user adapter claims the same prefix
fact builtinYields {
no p: Prefix |
p in Builtin.handles and p in (Adapter - Builtin).handles
}
-- every prefix is handled by at most one adapter
assert noConflict {
all p: Prefix | lone a: Adapter | p in a.handles
}
check noConflict for 6
pred sanityCheck {}
run sanityCheck {} for 6Two adapters claiming the same block prefix must be rejected.
Reject two adapters handling the same block prefix
cat <<'CFG' > dup-prefix.json
{
"entry": "index.spec.md",
"adapters": [
{"name": "a", "command": ["true"], "blocks": ["run:x"]},
{"name": "b", "command": ["true"], "blocks": ["run:x"]}
]
}
CFG
! specdown run -config dup-prefix.json 2>/dev/nullTwo adapters with the same name must be rejected.
Reject duplicate adapter names
cat <<'CFG' > dup-adapter.json
{
"entry": "index.spec.md",
"adapters": [
{"name": "a", "command": ["true"], "blocks": ["run:x"]},
{"name": "a", "command": ["true"], "blocks": ["run:y"]}
]
}
CFG
! specdown run -config dup-adapter.json 2>/dev/nullAn adapter with an empty name must be rejected.
Reject adapter with empty name
printf '{"entry":"i.spec.md","adapters":[{"name":"","command":["true"],"blocks":["run:x"]}]}' > empty-name.json
! specdown run -config empty-name.json 2>/dev/nullAn adapter without a command must be rejected.
Reject adapter with empty command
printf '{"entry":"i.spec.md","adapters":[{"name":"a","command":[],"blocks":["run:x"]}]}' > no-cmd.json
! specdown run -config no-cmd.json 2>/dev/nullAn adapter must declare at least one block or check.
Reject adapter with no blocks and no checks
printf '{"entry":"i.spec.md","adapters":[{"name":"a","command":["true"]}]}' > no-blocks.json
! specdown run -config no-blocks.json 2>/dev/nullOnly "alloy" is supported as a models builtin. Unknown values are rejected.
Reject unknown models builtin
printf '{"entry":"i.spec.md","adapters":[],"models":{"builtin":"unknown"}}' > bad-model.json
! specdown run -config bad-model.json 2>/dev/nullRelative Paths
All paths in the config are resolved relative to the config file's directory. This ensures the project works from any checkout location.
Verify relative entry path resolves from config directory
mkdir -p relpath/specs
printf '# T\n\n- [S](s.spec.md)\n' > relpath/specs/index.spec.md
printf '# S\n\nProse.\n' > relpath/specs/s.spec.md
printf '{"entry":"specs/index.spec.md","adapters":[]}' > relpath/specdown.json
specdown run -config relpath/specdown.json -dry-run 2>&1 | grep 'spec(s)'