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#

LanguageStatusTree-sitter grammarNotes
GoStabletree-sitter-goFunctions, methods, types, constants, variables. Method receivers are part of the ID (Foo.Bar).
PythonStabletree-sitter-pythonFunctions, classes, methods, constants. Package falls back to the module (file) name.
CStabletree-sitter-cFunctions, struct/enum/union types, typedefs, global variables, #define macros. Function prototypes and function-local declarations are skipped.
C++Stabletree-sitter-cppFunctions, class/struct member functions (inline and declared), class/struct/enum/union types, using aliases, typedefs, globals, macros. Method receivers carry the enclosing class.
RustStabletree-sitter-rustFunctions, impl/trait methods, struct/enum/trait/union types, type aliases, constants, statics. Method receivers carry the impl’d type or trait.
ZigNot 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/ --check

The format deliberately keeps the source tool’s stable IDs:

  • native-id is 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-written trace: links keep resolving. It must not contain whitespace: a key like Item 42 would produce an ID that no trace: could ever reference.
  • parent nests 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.
  • trace holds fully-qualified reqmd IDs. native-trace holds native IDs of other items in the bundle — the renderer rewrites them to full IDs for you.
  • attrs are 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, or true, so send a real number or boolean when you mean one. Names may only contain letters, digits, _, - and .. status and trace are reserved (they have dedicated fields), and status defaults to approved, so imported items count as upstream coverage providers.
  • body is Markdown and may contain headings — they become info items under the requirement. It must not put a fence whose info string is attr directly under a heading, because in reqmd that pair is a requirement: an imported description could otherwise add requirements of its own.
  • properties / required describe the attr block’s JSON Schema, which the renderer writes into the document’s schema.yaml.
  • external: true marks 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.go correspond to? — reqmd ls spec/ | grep "bar.go"
  • What code is untested? — load CTRF results with reqmd check spec/ --results ctrf.json; the missing-verdict check 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-code reads 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-guarded

Each 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#