Requirements rarely start (or end) inside reqmd. Two halves of the same problem:
- Code — the functions, types, and methods that implement your spec need proxy requirements so traces resolve end to end.
- ALM tools — the DOORS or codeBeamer items that product managers review need to land in the spec tree as generated, reviewed-as-diffs documents.
Both are handled the same way: a small producer emits one JSON exchange
format (reqmd-import/v1), and a single renderer (reqmd-import) writes the
actual reqmd documents. Nobody but the renderer writes attr blocks or
schema.yaml.
DOORS / codeBeamer / CSV ──▶ your script ──▶ reqmd-import/v1 JSON ──┐
├─▶ reqmd-import ──▶ .md + schema.yaml ──▶ reqmd check
source code (tree-sitter) ─▶ reqmd-code ─────▶ reqmd-import/v1 JSON ─┘Why one renderer: a second Markdown writer in Python (or Java, or DXL) would drift from the quoting, heading, and schema rules. The JSON format is the stable contract; the renderer is the only code that knows the file syntax.
Source code: reqmd-code#
Walks your C, C++, Go, Python, or Rust code, parses each file with a tree-sitter grammar, and emits every symbol as a proxy requirement. The source path is recorded as provenance, the symbol kind is preserved, and the doc comment becomes the requirement body. The result is a spec tree where code and spec share an ID space.
# Build (both halves)
go build -o reqmd-code ./reqmd-code/cmd/reqmd-code
go build -o reqmd-import ./reqmd-import/cmd/reqmd-import
# Extract code and render it into the spec tree
reqmd-code ./internal | reqmd-import - -o spec/03-software/imported
# Validate the whole tree, generated documents included
reqmd check spec/Requirements are rendered one .md file per package (package.md), under an
IMP-<package>- ID prefix. Given internal/cli/check_test.go:
# Package: cli
This file is auto-generated by reqmd-code. Do not edit by hand.
## IMP-cli-BenchmarkRunValidationPipeline-80e6286: BenchmarkRunValidationPipeline
```attr
status: approved
trace: []
x-reqmd.imported: true
x-reqmd.source-file: internal/cli/check_test.go
x-reqmd.source-line: 19
x-reqmd.symbol-kind: function
```
BenchmarkRunValidationPipeline measures the cost of the full validation
pipeline against the spec/ fixture.The ID format is IMP-<package>-<symbol>-<7charhash>. The hash is computed over
(package, symbol) so IDs survive line-number shifts — a refactor that moves
code around does not invalidate the trace.
Options#
reqmd-code ./internal # bundle to stdout
reqmd-code ./internal -o imported.json # bundle to a file
reqmd-code --lang go ./internal # restrict to one language
reqmd-code --heuristic-traces ./internal # also mine bare IDs from prose
reqmd-code --id-prefix MY- ./src # custom ID prefix (default IMP-)--heuristic-traces looks for bare IDs in doc comments and prose (e.g.
// implements SYS-001) and records them under x-reqmd.heuristic-trace
— a separate attribute from the hand-asserted trace:, so a guess never
masquerades as a claim.
Languages#
| Language | Status | Tree-sitter grammar | Notes |
|---|---|---|---|
| Go | Stable | tree-sitter-go | Functions, methods, types, constants, variables. Method receivers are part of the ID (Foo.Bar). |
| Python | Stable | tree-sitter-python | Functions, classes, methods, constants. Package falls back to the module (file) name. |
| C | Stable | tree-sitter-c | Functions, struct/enum/union types, typedefs, global variables, #define macros. Function prototypes and function-local declarations are skipped. |
| C++ | Stable | tree-sitter-cpp | Functions, class/struct member functions (inline and declared), class/struct/enum/union types, using aliases, typedefs, globals, macros. Method receivers carry the enclosing class. |
| Rust | Stable | tree-sitter-rust | Functions, impl/trait methods, struct/enum/trait/union types, type aliases, constants, statics. Method receivers carry the impl’d type or trait. |
| Zig | Not implemented | — | The registry slot is reserved; contributions welcome. |
To add a language: implement the lang.Language interface in a new package
under reqmd-code/internal/lang/<lang>/ and self-register it via
lang.Register(&impl{}) in an init() block. The blank import in
cmd/reqmd-code/main.go pulls all plugins into the binary.
ALM tools and anything else: the exchange format#
Any tool that can print JSON can feed reqmd. A minimal bundle:
{
"format": "reqmd-import/v1",
"documents": [
{
"dir": "brakes",
"file": "package.md",
"title": "Brakes Module",
"level": "stakeholder-requirements",
"id-prefix": "DOORS-",
"external": true,
"properties": {"priority": {"type": "string", "enum": ["High", "Low"]}},
"requirements": [
{"native-id": "17", "title": "Brake request",
"body": "The pedal shall request braking.", "attrs": {"priority": "High"}},
{"native-id": "42", "title": "Brake pressure", "parent": "17",
"native-trace": ["17"], "trace": ["SYS-BRAKE-001"],
"attrs": {"priority": "Low"}}
]
}
]
}reqmd-import brakes.json -o spec/00-doors/ --checkThe format deliberately keeps the source tool’s stable IDs:
native-idis the DOORS object number or codeBeamer item ID. The reqmd ID becomes<id-prefix><native-id>(DOORS-17). Re-imports never churn IDs, so hand-writtentrace:links keep resolving. It must not contain whitespace: a key likeItem 42would produce an ID that notrace:could ever reference.parentnests by native ID to any depth (each level adds one#); the renderer reorders children under their parents, so a producer can emit items in any order.traceholds fully-qualified reqmd IDs.native-traceholds native IDs of other items in the bundle — the renderer rewrites them to full IDs for you.attrsare the remaining attribute-block entries. Values take strings, numbers, booleans, or flat lists of those; a JSON string stays a string even when it reads like a number,null, ortrue, so send a real number or boolean when you mean one. Names may only contain letters, digits,_,-and..statusandtraceare reserved (they have dedicated fields), andstatusdefaults toapproved, so imported items count as upstream coverage providers.bodyis Markdown and may contain headings — they become info items under the requirement. It must not put a fence whose info string isattrdirectly under a heading, because in reqmd that pair is a requirement: an imported description could otherwise add requirements of its own.properties/requireddescribe the attr block’s JSON Schema, which the renderer writes into the document’sschema.yaml.external: truemarks the document a proxy for content authored elsewhere.
Titles and notices must be single-line, and file must be a plain file name.
Anything that would silently corrupt a document — a line break in a title, an
attribute name containing a colon, an ID with a space in it — is rejected with a
precise message instead of being written.
Emitted files carry a <!-- reqmd: generated --> marker. A re-run rewrites
generated files only: hand-authored content in the same tree is never
clobbered, and unchanged output is not rewritten at all.
Writing an importer in Python#
pip install ./python gives you reqmd_io, a dependency-free helper that only
builds the exchange JSON — it contains no YAML or Markdown logic, so it cannot
drift from the renderer.
from reqmd_io import Bundle, Document, Requirement
doc = Document(id_prefix="CB-", dir="codebeamer", level="stakeholder-requirements",
title="codeBeamer Requirements", external=True,
properties={"priority": {"type": "string"}})
for item in my_codebeamer_pull(): # your REST/DXL/CSV code
doc.add(Requirement(native_id=item.id, title=item.name, body=item.description,
parent=item.parent_id,
native_trace=[link.target for link in item.links],
attrs={"priority": item.priority}))
Bundle([doc]).write("codebeamer.json")reqmd-import codebeamer.json -o spec/00-codebeamer/A complete, runnable reader for DOORS and codeBeamer CSV exports ships at
python/examples/csv_import.py;
Quickstart 15 walks it end to end.
The authority rule#
Import mirrors content down (ALM → reqmd) into generated, external: true
documents. It is deliberately one-way: reqmd is authoritative for its own
downstream layers and does not write prose back into a database-backed tool.
What is worth pushing back are recomputed facts (coverage, verdicts, current
downstream versions), because those are idempotent and cannot conflict.
The trace gap#
Plain Markdown traceability has a directional problem. A software requirement
can trace: [system-requirements/SYS-001] and reqmd validates that SYS-001
exists upstream. But the implementation — the function that actually
implements the requirement — has no native concept of a reqmd ID. Without
help you maintain a mapping table by hand, use an IDE plugin, or give up and
let the spec stop at the test layer.
reqmd-code produces that mapping automatically. It emits the symbols as an
exchange bundle; reqmd-import renders the bundle, and the IDs then exist in
the spec tree. The agent and the human can grep them, link them, and
CI enforces that they still resolve. When a function is renamed or moved, the
next run updates the file — the ID is a hash of (package, symbol), not a
position, so the spec tree still points at the same requirement.
Where it fits in the workflow#
# 1. Author the spec by hand
reqmd init my-feature/
$EDITOR my-feature/stakeholder/goals.md
$EDITOR my-feature/system/features.md
reqmd check my-feature/ # pass
# 2. Import the upstream requirements from the ALM (optional)
python my_doors_import.py -o doors.json
reqmd-import doors.json -o my-feature/00-doors/
# 3. Write the code, then extract proxies and re-validate
reqmd-code ./src | reqmd-import - -o my-feature/03-software/imported
reqmd check my-feature/ # still pass
# 4. CI runs the same check on every PR
# - run: reqmd check spec/The agent — and the human — can then answer questions like:
- What functions implement SYS-001? —
grep -r "SYS-001" spec/03-software/imported/ - What spec IDs does
foo/bar.gocorrespond to? —reqmd ls spec/ | grep "bar.go" - What code is untested? — load CTRF results with
reqmd check spec/ --results ctrf.json; themissing-verdictcheck fires on measures with no test.
What it doesn’t do#
- Guess traceability. Generated items start with
trace: []. You annotate the source, run--heuristic-traces, declare links in your importer, or hand-fill the attr block. - Refresh on every commit. You re-run the producer when you want new output. Incremental sync is a per-tool concern that would add drift for a workflow that runs once per PR or release.
- Read binary artefacts.
reqmd-codereads source, not compiled binaries or runtime behaviour; add a runtime instrumentation step if you need that. - Sync content back into the ALM. See the authority rule above.
Architecture#
walkdir (per-file) JSON bundle (reqmd-import/v1)
→ language plugin (tree-sitter) │
→ symbols ▼
→ trace inference reqmd-import
→ JSON bundle ────────────────▶ ├─ schema.yaml per document
├─ .md per document (hierarchical headings)
└─ idempotent write-if-changed, marker-guardedEach stage is independently testable. The renderer’s idempotency guarantee
(byte-identical output for unchanged input, hand-authored files untouched) is
what makes both import paths safe to re-run in CI. The tree-sitter grammars are
loaded once at startup and per-file parsing is dispatched to a worker pool
sized to runtime.NumCPU().
See also#
- Quickstart — including step 15, a custom CSV importer.
- Use cases — where the importers sit in the engineering workflow.
- Compare — how reqmd compares to other tools.
- Sources:
reqmd-code,reqmd-import,python/.