This post is part of the series about my private BeagleBone Black project. The project uses StrictDoc for requirements management — no Word document, no Jira ticket, just text files directly in the repository. What sounds odd turns out to be surprisingly sensible in practice.

What Is StrictDoc?

StrictDoc is an open-source tool for document-based requirements management. Requirements are written in .sdoc text files that live in the same repository as the code — and therefore in the same Git history.

Why not a classic ALM tool?

Jira, Azure DevOps, Polarion — all these tools share the same drawback: the requirements live separately from the code. A requirement changes, the code is adjusted, but nobody links the two explicitly. A year later, nobody knows anymore whether the implementation still matches the requirement.

StrictDoc forces the link to happen. A code comment references a requirement ID, and StrictDoc can check whether every requirement has at least one implementation reference.

StrictDoc is not a replacement for DOORS in safety-critical systems with regulatory requirements. For a private project, it’s exactly right.

Document Structure

StrictDoc documents are .sdoc text files with a simple syntax.

Basic Structure

[DOCUMENT]
TITLE: BeagleBone Black — GPIO Requirements

[SECTION]
TITLE: Functional Requirements

[REQUIREMENT]
UID: BBB-GPIO-001
STATUS: Active
TITLE: Read GPIO pin
STATEMENT: The system MUST be able to read the digital state of a configured GPIO pin.

[REQUIREMENT]
UID: BBB-GPIO-002
STATUS: Active
TITLE: Set GPIO pin
STATEMENT: The system MUST be able to set a configured GPIO pin to HIGH or LOW.
RELATIONS:
- TYPE: Refines
  VALUE: BBB-GPIO-001

Important fields:

UID

Unique ID — referenced in the code. Never change after the first commit.

STATUS

Active, Draft, Obsolete

STATEMENT

The actual requirement — using SHALL/MUST/SHOULD if you use EARS notation

RELATIONS

Link to other requirements (Refines, Implements, Verifies)

Hierarchy: System to Software

I separate requirements into three levels:

docs/requirements/
  system/
    gpio.sdoc       ← system requirements (what the system must be able to do)
  software/
    hal-gpio.sdoc   ← software requirements (how the HAL implements it)
    api-gpio.sdoc   ← API requirements (how the REST API exposes it)
  tests/
    gpio-tests.sdoc ← test cases (how it's verified)

Writing Requirements — GPIO Example

System Level (system/gpio.sdoc)

[DOCUMENT]
TITLE: System — GPIO

[REQUIREMENT]
UID: SYS-GPIO-001
TITLE: Digital output
STATEMENT: The system MUST provide at least 4 independent digital outputs.

[REQUIREMENT]
UID: SYS-GPIO-002
TITLE: Digital input
STATEMENT: The system MUST provide at least 4 independent digital inputs.

Software Level (software/hal-gpio.sdoc)

[DOCUMENT]
TITLE: HAL — GPIO

[REQUIREMENT]
UID: SW-GPIO-001
TITLE: Read GPIO via sysfs
STATEMENT: The HAL MUST read GPIO states via the Linux sysfs interface.
RELATIONS:
- TYPE: Implements
  VALUE: SYS-GPIO-002

[REQUIREMENT]
UID: SW-GPIO-002
TITLE: Write GPIO via sysfs
STATEMENT: The HAL MUST write GPIO states via the Linux sysfs interface.
RELATIONS:
- TYPE: Implements
  VALUE: SYS-GPIO-001

Traceability in the Code

The implementation reference sits directly in the source code as a comment.

C Driver

/**
 * gpio_read - Reads the state of a GPIO pin via sysfs.
 *
 * @req SW-GPIO-001
 */
int gpio_read(int pin, int *value) {
    char path[64];
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", pin);

    FILE *f = fopen(path, "r");
    if (!f) return -ENODEV;

    fscanf(f, "%d", value);
    fclose(f);
    return 0;
}

Rust HAL

/// GPIO read via the C driver.
/// @req SW-GPIO-001
pub fn gpio_read(pin: u32) -> Result<u8, HalError> {
    let mut value: i32 = 0;
    let ret = unsafe { c_gpio_read(pin as i32, &mut value) };
    if ret < 0 {
        return Err(HalError::GPIOReadFailed(ret));
    }
    Ok(value as u8)
}

Test

// TestGPIORead verifies SW-GPIO-001 and SYS-GPIO-002.
// @req SW-GPIO-001
// @req SYS-GPIO-002
func TestGPIORead(t *testing.T) {
    h := hal.NewMockHAL()
    h.SetGPIOValue(48, 1)

    val, err := h.GPIORead(48)
    assert.NoError(t, err)
    assert.Equal(t, 1, val)
}

The @req tag is a convention, not a StrictDoc feature. StrictDoc itself offers source-file tracing with a configurable pattern.

StrictDoc HTML Export

# Installation
pip install strictdoc

# HTML export of all requirement documents
strictdoc export docs/requirements/ --output-dir out/requirements

The HTML export shows:

  • All requirements with status and text

  • Traceability matrix: which requirement has which implementation

  • Gaps: requirements without a code reference (marked in red)

  • Coverage level per document

Gap Analysis

StrictDoc highlights requirements without an implementation reference. That’s the real value: not just documenting what’s implemented, but seeing what’s still missing.

strictdoc check docs/requirements/
# [ERROR] SW-GPIO-003: no implementation reference found

CI Integration

In the Drone pipeline:

steps:
  - name: requirements-check
    image: python:3.11-slim
    commands:
      - pip install strictdoc --quiet
      - strictdoc export docs/requirements/
          --output-dir out/requirements
          --formats=html
      - strictdoc check docs/requirements/

A failed StrictDoc check blocks the merge — just like a failed unit test.

Conclusion

Is StrictDoc worth it for a private project?

Yes, if:

  • you want to learn what requirements management looks like in practice

  • you want to see how code and requirements drift apart (they always do)

  • you want to show a portfolio project that goes beyond "I wrote some code"

No, if:

  • you want fast results and see documentation as overhead

  • the project only runs for a few weeks

For me, it’s the right choice. Professionally, I work in an environment where traceability is a regulatory requirement — this is my practice ground for understanding what that actually means, beyond the process documents.


Next post in the series: Drone CI with Podman