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"]
}
],
"reporters": [
{ "builtin": "html", "outFile": ".artifacts/specdown/report" },
{ "builtin": "json", "outFile": ".artifacts/specdown/report.json" }
],
"models": { "builtin": "alloy" }
}
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
mkdir -p .tmp-test
cat <<'SPEC' > .tmp-test/entry-test.spec.md
# My Feature
Some prose.
SPEC
printf '# My Project Title\n\n- [Feature](entry-test.spec.md)\n' > .tmp-test/entry-index.spec.md
cat <<'CFG' > .tmp-test/entry-test-cfg.json
{"entry":"entry-index.spec.md","adapters":[],"reporters":[{"builtin":"html","outFile":"entry-report"}]}
CFG
specdown run -config .tmp-test/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
mkdir -p .tmp-test
printf '# T\n\n- [S](builtin-shell-test.spec.md)\n' > .tmp-test/builtin-shell-index.spec.md
BT=$(printf '\140\140\140')
printf '%s\n' '# Builtin Shell' '' "$BT"'run:shell' '$ echo works' 'works' "$BT" > .tmp-test/builtin-shell-test.spec.md
printf '{"entry":"builtin-shell-index.spec.md","adapters":[]}' > .tmp-test/builtin-shell-cfg.json
specdown run -config .tmp-test/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. Can be omitted if not used |
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) |
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 .tmp-test/setup-test
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/setup-test/index.spec.md
printf '# S\n\nProse.\n' > .tmp-test/setup-test/s.spec.md
cat <<'CFG' > .tmp-test/setup-test/specdown.json
{"entry": "index.spec.md", "setup": "echo SETUP-RAN > setup-marker.txt"}
CFG
specdown run -config .tmp-test/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 .tmp-test/teardown-test
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/teardown-test/index.spec.md
BT=$(printf '\140\140\140')
printf '%s\n' '# S' '' "$BT"'run:shell' '$ echo hello' 'wrong-output' "$BT" > .tmp-test/teardown-test/s.spec.md
cat <<'CFG' > .tmp-test/teardown-test/specdown.json
{"entry": "index.spec.md", "setup": "echo SETUP-OK > marker.txt", "teardown": "echo TEARDOWN-RAN >> marker.txt"}
CFG
specdown run -config .tmp-test/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 .tmp-test/setup-fail-test
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/setup-fail-test/index.spec.md
printf '# S\n\nProse.\n' > .tmp-test/setup-fail-test/s.spec.md
cat <<'CFG' > .tmp-test/setup-fail-test/specdown.json
{"entry": "index.spec.md", "setup": "exit 1"}
CFG
! specdown run -config .tmp-test/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"]) |
checksDir |
Directory containing shell check scripts (default: "./checks") |
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
| 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) |
checksDir (adapter) |
"" (empty — must be set explicitly when using shell check scripts) |
An empty config {} is valid — all fields are defaulted.
Verify empty config applies defaults
mkdir -p .tmp-test/defaults-test
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/defaults-test/specs/index.spec.md
mkdir -p .tmp-test/defaults-test/specs
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/defaults-test/specs/index.spec.md
printf '# S\n\nProse.\n' > .tmp-test/defaults-test/specs/s.spec.md
echo '{}' > .tmp-test/defaults-test/specdown.json
cd .tmp-test/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 .tmp-test/no-config-test && mkdir -p .tmp-test/no-config-test/specs
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/no-config-test/specs/index.spec.md
printf '# S\n\nProse.\n' > .tmp-test/no-config-test/specs/s.spec.md
cd .tmp-test/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 6Two adapters with the same name must be rejected.
Reject duplicate adapter names
mkdir -p .tmp-test
cat <<'CFG' > .tmp-test/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 .tmp-test/dup-adapter.json 2>/dev/nullAn adapter with an empty name must be rejected.
Reject adapter with empty name
mkdir -p .tmp-test
printf '{"entry":"i.spec.md","adapters":[{"name":"","command":["true"],"blocks":["run:x"]}]}' > .tmp-test/empty-name.json
! specdown run -config .tmp-test/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"]}]}' > .tmp-test/no-cmd.json
! specdown run -config .tmp-test/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"]}]}' > .tmp-test/no-blocks.json
! specdown run -config .tmp-test/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"}}' > .tmp-test/bad-model.json
! specdown run -config .tmp-test/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 .tmp-test/relpath/specs
printf '# T\n\n- [S](s.spec.md)\n' > .tmp-test/relpath/specs/index.spec.md
printf '# S\n\nProse.\n' > .tmp-test/relpath/specs/s.spec.md
printf '{"entry":"specs/index.spec.md","adapters":[]}' > .tmp-test/relpath/specdown.json
specdown run -config .tmp-test/relpath/specdown.json -dry-run 2>&1 | grep 'spec(s)'