Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions artifacts/architecture.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ artifacts:
classifier-kind: type
aadl-file: arch/rivet_system.aadl:49
source-ref: arch/rivet_system.aadl:49-54
diagram: "root: RivetSystem::Rivet.Impl"

- id: ARCH-SYS-002
type: aadl-component
Expand Down
9 changes: 9 additions & 0 deletions artifacts/decisions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ artifacts:
alternatives: >
Per-tool REST adapters (Polarion REST, DOORS DNG API). Rejected
because each requires separate auth, pagination, and schema mapping.
diagram: |
graph LR
A[Rivet Core] -->|OSLC| B[Polarion]
A -->|OSLC| C[DOORS]
A -->|OSLC| D[codebeamer]
style A fill:#e8f4fd,stroke:#0550ae
style B fill:#f0f0f0,stroke:#666
style C fill:#f0f0f0,stroke:#666
style D fill:#f0f0f0,stroke:#666

- id: DD-002
type: design-decision
Expand Down
27 changes: 27 additions & 0 deletions docs/srs.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ audit, deny, vet, coverage).

[[REQ-011]] pins Rust edition 2024 with MSRV 1.85.

### 3.7 Traceability Flow

The following diagram shows the traceability chain from stakeholder needs
through to verification evidence:

```mermaid
graph TD
REQ[Requirements] -->|satisfies| DD[Design Decisions]
REQ -->|allocated-to| ARCH[Architecture]
DD -->|implemented-by| FEAT[Features]
FEAT -->|verified-by| TEST[Test Artifacts]
TEST -->|evidence| RES[Test Results]
style REQ fill:#e8f4fd,stroke:#0550ae
style ARCH fill:#f0e6ff,stroke:#6639ba
style TEST fill:#e6ffe6,stroke:#15713a
```

### 3.8 Key Requirement Details

The following requirement is the cornerstone of the system:

{{artifact:REQ-001}}

And the design decision that shapes tool integration:

{{artifact:DD-001}}

## 4. Glossary

See the glossary panel below (defined in document frontmatter).
60 changes: 60 additions & 0 deletions rivet-cli/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::process::Command;

fn main() {
// Emit git metadata as compile-time environment variables.
println!("cargo:rerun-if-changed=../.git/HEAD");
println!("cargo:rerun-if-changed=../.git/index");

let git = |args: &[&str]| -> String {
Command::new("git")
.args(args)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
};

let commit = git(&["rev-parse", "--short=8", "HEAD"]);
let branch = git(&["rev-parse", "--abbrev-ref", "HEAD"]);
let dirty = !git(&["status", "--porcelain"]).is_empty();

// Count uncommitted changes by category
let status_output = git(&["status", "--porcelain"]);
let mut staged = 0u32;
let mut modified = 0u32;
let mut untracked = 0u32;
for line in status_output.lines() {
if line.len() < 2 {
continue;
}
let index = line.as_bytes()[0];
let worktree = line.as_bytes()[1];
if line.starts_with("??") {
untracked += 1;
} else {
if index != b' ' && index != b'?' {
staged += 1;
}
if worktree != b' ' && worktree != b'?' {
modified += 1;
}
}
}

let build_date = Command::new("date")
.arg("+%Y-%m-%d")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "unknown".to_string());

println!("cargo:rustc-env=RIVET_GIT_COMMIT={commit}");
println!("cargo:rustc-env=RIVET_GIT_BRANCH={branch}");
println!("cargo:rustc-env=RIVET_GIT_DIRTY={dirty}");
println!("cargo:rustc-env=RIVET_GIT_STAGED={staged}");
println!("cargo:rustc-env=RIVET_GIT_MODIFIED={modified}");
println!("cargo:rustc-env=RIVET_GIT_UNTRACKED={untracked}");
println!("cargo:rustc-env=RIVET_BUILD_DATE={build_date}");
}
131 changes: 131 additions & 0 deletions rivet-cli/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ const TOPICS: &[DocTopic] = &[
category: "Reference",
content: JSON_DOC,
},
DocTopic {
slug: "documents",
title: "Documents — markdown with frontmatter, images, and diagrams",
category: "Reference",
content: DOCUMENTS_DOC,
},
DocTopic {
slug: "schema/common",
title: "Common base fields and link types",
Expand Down Expand Up @@ -329,6 +335,131 @@ rivet validate --format json | jq -e '.errors == 0' > /dev/null && echo "PASS" |
```
"#;

const DOCUMENTS_DOC: &str = r#"# Documents

Rivet treats markdown files as first-class project documents. Documents are
loaded from directories listed under `docs:` in `rivet.yaml`, parsed for
YAML frontmatter, and scanned for artifact references.

## Directory Layout

```yaml
# rivet.yaml
docs:
- docs # loads docs/*.md recursively
- arch # loads arch/*.md recursively
```

Each `.md` file becomes a document in the dashboard's Documents view.

## Frontmatter

Every document should start with a YAML frontmatter block:

```yaml
---
id: DOC-SRS
title: Software Requirements Specification
type: specification
status: approved
tags: [requirements, safety]
---
```

| Field | Required | Description |
|--------|----------|------------------------------------------|
| id | yes | Unique document identifier |
| title | yes | Display title |
| type | no | Document type (specification, plan, etc.) |
| status | no | Lifecycle status (draft, approved, etc.) |
| tags | no | Categorization tags |

## Artifact References

Use `[[ID]]` syntax to reference artifacts anywhere in the document body:

```markdown
The latency requirement [[REQ-001]] is satisfied by design decision [[DD-005]].
```

These are rendered as clickable links in the dashboard and tracked in the
document-artifact linkage view. Broken references (IDs not found in the
artifact store) are visually flagged.

## Images

Embed images using standard markdown syntax:

```markdown
![Architecture diagram](images/arch-overview.png)
![Sequence flow](images/flow.svg)
```

Images are resolved relative to the document's `docs:` directory.
Place images in a subdirectory (e.g. `docs/images/`) and reference them
with a relative path.

Supported formats: PNG, JPEG, GIF, SVG, WebP.

In the dashboard, image paths are served via `/docs-asset/` — e.g.
`images/arch.png` in a doc becomes `/docs-asset/images/arch.png`.

## Mermaid Diagrams

Embed diagrams using fenced code blocks with the `mermaid` language tag:

````markdown
```mermaid
graph TD
REQ-001 -->|satisfies| FEAT-001
REQ-001 -->|derives-from| SYS-REQ-001
DD-005 -->|implements| REQ-001
```
````

Mermaid diagrams are rendered client-side in the dashboard. Supported
diagram types include:

- **flowchart / graph** — dependency and flow diagrams
- **sequence** — interaction sequences
- **state** — state machines
- **class** — structure diagrams
- **gantt** — timeline views
- **C4** — architecture (C4 model)

### Tips

- Use artifact IDs as node names to match traceability
- Keep diagrams focused (10-20 nodes max) for readability
- The `mermaid` block is passed through as-is in CLI text output

## AADL Diagrams

If you have spar (AADL parser) integration, use `aadl` code blocks:

````markdown
```aadl
root: flight_controller
```
````

These are rendered as interactive architecture diagrams via the WASM runtime.

## Sections and TOC

Headings (`##`, `###`, etc.) are parsed into sections. Documents with more
than two sections automatically get a table of contents in the dashboard.
Section-level artifact reference counts are shown in the TOC.

## Validation

Documents participate in validation:

- **Broken references**: `[[ID]]` pointing to nonexistent artifacts are warnings
- **Coverage**: The doc-linkage view shows which artifacts are referenced in docs
- **Orphan detection**: Artifacts never referenced in any document are flagged
"#;

// ── Public API ──────────────────────────────────────────────────────────

/// List all available documentation topics.
Expand Down
45 changes: 44 additions & 1 deletion rivet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,43 @@ mod docs;
mod schema_cmd;
mod serve;

fn build_version() -> &'static str {
use std::sync::LazyLock;
static VERSION: LazyLock<String> = LazyLock::new(|| {
let version = env!("CARGO_PKG_VERSION");
let commit = env!("RIVET_GIT_COMMIT");
let branch = env!("RIVET_GIT_BRANCH");
let dirty: bool = env!("RIVET_GIT_DIRTY").parse().unwrap_or(false);
let staged: u32 = env!("RIVET_GIT_STAGED").parse().unwrap_or(0);
let modified: u32 = env!("RIVET_GIT_MODIFIED").parse().unwrap_or(0);
let untracked: u32 = env!("RIVET_GIT_UNTRACKED").parse().unwrap_or(0);
let date = env!("RIVET_BUILD_DATE");

let mut s = format!("{version} ({commit} {branch} {date})");
if dirty {
let mut parts = Vec::new();
if staged > 0 {
parts.push(format!("{staged} staged"));
}
if modified > 0 {
parts.push(format!("{modified} modified"));
}
if untracked > 0 {
parts.push(format!("{untracked} untracked"));
}
if parts.is_empty() {
s.push_str(" [dirty]");
} else {
s.push_str(&format!(" [{}]", parts.join(", ")));
}
}
s
});
&VERSION
}

#[derive(Parser)]
#[command(name = "rivet", about = "SDLC artifact traceability and validation")]
#[command(name = "rivet", about = "SDLC artifact traceability and validation", version = build_version())]
struct Cli {
/// Path to the project directory (containing rivet.yaml)
#[arg(short, long, default_value = ".")]
Expand Down Expand Up @@ -323,6 +358,7 @@ fn run(cli: Cli) -> Result<bool> {
project_name,
project_path,
schemas_dir,
doc_dirs,
) = load_project_full(&cli)?;
let rt = tokio::runtime::Runtime::new().context("failed to create tokio runtime")?;
rt.block_on(serve::run(
Expand All @@ -334,6 +370,7 @@ fn run(cli: Cli) -> Result<bool> {
project_name,
project_path,
schemas_dir,
doc_dirs,
port,
))?;
Ok(true)
Expand Down Expand Up @@ -1621,6 +1658,7 @@ fn load_project_full(
String,
PathBuf,
PathBuf,
Vec<PathBuf>,
)> {
let config_path = cli.project.join("rivet.yaml");
let config = rivet_core::load_project_config(&config_path)
Expand All @@ -1643,8 +1681,12 @@ fn load_project_full(

// Load documents
let mut doc_store = DocumentStore::new();
let mut doc_dirs = Vec::new();
for docs_path in &config.docs {
let dir = cli.project.join(docs_path);
if dir.is_dir() {
doc_dirs.push(dir.clone());
}
let docs = document::load_documents(&dir)
.with_context(|| format!("loading docs from '{docs_path}'"))?;
for doc in docs {
Expand Down Expand Up @@ -1675,6 +1717,7 @@ fn load_project_full(
project_name,
project_path,
schemas_dir,
doc_dirs,
))
}

Expand Down
Loading
Loading