diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0b1053..3501d5a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -168,11 +168,11 @@ jobs:
# ── MSRV check ──────────────────────────────────────────────────────
msrv:
- name: MSRV (1.85)
+ name: MSRV (1.89)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@1.85.0
+ - uses: dtolnay/rust-toolchain@1.89.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --all
diff --git a/.gitignore b/.gitignore
index dc94bde..a9aa515 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,7 @@
*.swo
.DS_Store
.claude/worktrees/
+
+# WASM binary assets (built or downloaded, not committed)
+rivet-cli/assets/wasm/*.wasm
+rivet-cli/assets/wasm/js/
diff --git a/.rivet/agent-context.md b/.rivet/agent-context.md
new file mode 100644
index 0000000..3dea467
--- /dev/null
+++ b/.rivet/agent-context.md
@@ -0,0 +1,95 @@
+# Rivet Agent Context
+
+Auto-generated by `rivet context` — do not edit.
+
+## Project
+
+- **Name:** rivet
+- **Version:** 0.1.0
+- **Schemas:** common, dev, aadl
+- **Sources:** artifacts (generic-yaml)
+- **Docs:** docs, arch
+- **Results:** results
+
+## Artifacts
+
+| Type | Count | Example IDs |
+|------|-------|-------------|
+| aadl-component | 21 | ARCH-SYS-001, ARCH-SYS-002, ARCH-CORE-001 |
+| design-decision | 10 | DD-001, DD-002, DD-003 |
+| feature | 30 | FEAT-001, FEAT-002, FEAT-003 |
+| requirement | 16 | REQ-001, REQ-002, REQ-003 |
+| **Total** | **77** | |
+
+## Schema
+
+- **`aadl-analysis-result`** — Output of a spar analysis pass
+ Required fields: analysis-name, severity
+- **`aadl-component`** — AADL component type or implementation imported from spar
+ Required fields: category, aadl-package
+- **`aadl-flow`** — End-to-end flow with latency bounds
+ Required fields: flow-kind
+- **`design-decision`** — An architectural or design decision with rationale
+ Required fields: rationale
+- **`feature`** — A user-visible capability or feature
+ Required fields: (none)
+- **`requirement`** — A functional or non-functional requirement
+ Required fields: (none)
+
+### Link Types
+
+- `allocated-to` (inverse: `allocated-from`)
+- `constrained-by` (inverse: `constrains`)
+- `depends-on` (inverse: `depended-on-by`)
+- `derives-from` (inverse: `derived-into`)
+- `implements` (inverse: `implemented-by`)
+- `mitigates` (inverse: `mitigated-by`)
+- `modeled-by` (inverse: `models`)
+- `refines` (inverse: `refined-by`)
+- `satisfies` (inverse: `satisfied-by`)
+- `traces-to` (inverse: `traced-from`)
+- `verifies` (inverse: `verified-by`)
+
+## Traceability Rules
+
+| Rule | Source Type | Severity | Description |
+|------|------------|----------|-------------|
+| requirement-coverage | requirement | warning | Every requirement should be satisfied by at least one design decision or feature |
+| decision-justification | design-decision | error | Every design decision must link to at least one requirement |
+| aadl-component-has-allocation | aadl-component | info | AADL component should trace to a requirement or architecture element |
+
+## Coverage
+
+**Overall: 100.0%**
+
+| Rule | Source Type | Covered | Total | % |
+|------|------------|---------|-------|---|
+| requirement-coverage | requirement | 16 | 16 | 100.0% |
+| decision-justification | design-decision | 10 | 10 | 100.0% |
+| aadl-component-has-allocation | aadl-component | 21 | 21 | 100.0% |
+
+## Validation
+
+0 errors, 0 warnings
+
+## Documents
+
+4 documents loaded
+
+## Commands
+
+```bash
+rivet validate # validate all artifacts
+rivet list # list all artifacts
+rivet list -t # filter by type
+rivet stats # artifact counts + orphans
+rivet coverage # traceability coverage report
+rivet matrix --from X --to Y # traceability matrix
+rivet diff --base A --head B # compare artifact sets
+rivet schema list # list schema types
+rivet schema show # show type details
+rivet schema rules # list traceability rules
+rivet export -f generic-yaml # export as YAML
+rivet serve # start dashboard on :3000
+rivet context # regenerate this file
+```
diff --git a/Cargo.lock b/Cargo.lock
index 53b07c3..b815d5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -239,6 +239,21 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "borsh"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
+dependencies = [
+ "cfg_aliases",
+]
+
+[[package]]
+name = "boxcar"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
+
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -356,6 +371,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
[[package]]
name = "ciborium"
version = "0.2.2"
@@ -464,6 +485,12 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+[[package]]
+name = "countme"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
+
[[package]]
name = "cpp_demangle"
version = "0.4.5"
@@ -541,7 +568,7 @@ dependencies = [
"log",
"pulley-interpreter",
"regalloc2",
- "rustc-hash",
+ "rustc-hash 2.1.1",
"serde",
"smallvec",
"target-lexicon",
@@ -687,6 +714,15 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -1023,7 +1059,7 @@ checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557"
dependencies = [
"bitflags",
"debugid",
- "rustc-hash",
+ "rustc-hash 2.1.1",
"serde",
"serde_derive",
"serde_json",
@@ -1117,12 +1153,20 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
+ "allocator-api2",
+ "equivalent",
"foldhash",
"serde",
]
@@ -1133,6 +1177,15 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
[[package]]
name = "heck"
version = "0.5.0"
@@ -1434,6 +1487,24 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "intrusive-collections"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86"
+dependencies = [
+ "memoffset",
+]
+
+[[package]]
+name = "inventory"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "io-extras"
version = "0.18.4"
@@ -1571,6 +1642,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "la-arena"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3752f229dcc5a481d60f385fa479ff46818033d881d2d801aa27dffcfb5e8306"
+
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1679,6 +1756,15 @@ dependencies = [
"rustix 1.1.4",
]
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -2171,7 +2257,7 @@ dependencies = [
"bumpalo",
"hashbrown 0.15.5",
"log",
- "rustc-hash",
+ "rustc-hash 2.1.1",
"smallvec",
]
@@ -2275,12 +2361,14 @@ dependencies = [
"serde_yaml",
"tokio",
"tower-http",
+ "urlencoding",
]
[[package]]
name = "rivet-core"
version = "0.1.0"
dependencies = [
+ "anyhow",
"criterion",
"log",
"petgraph",
@@ -2290,6 +2378,8 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
+ "spar-analysis",
+ "spar-hir",
"thiserror 2.0.18",
"tokio",
"urlencoding",
@@ -2298,12 +2388,30 @@ dependencies = [
"wiremock",
]
+[[package]]
+name = "rowan"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
+dependencies = [
+ "countme",
+ "hashbrown 0.14.5",
+ "rustc-hash 1.1.0",
+ "text-size",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -2403,6 +2511,49 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "salsa"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f77debccd43ba198e9cee23efd7f10330ff445e46a98a2b107fed9094a1ee676"
+dependencies = [
+ "boxcar",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "hashbrown 0.15.5",
+ "hashlink",
+ "indexmap",
+ "intrusive-collections",
+ "inventory",
+ "parking_lot",
+ "portable-atomic",
+ "rayon",
+ "rustc-hash 2.1.1",
+ "salsa-macro-rules",
+ "salsa-macros",
+ "smallvec",
+ "thin-vec",
+ "tracing",
+]
+
+[[package]]
+name = "salsa-macro-rules"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea07adbf42d91cc076b7daf3b38bc8168c19eb362c665964118a89bc55ef19a5"
+
+[[package]]
+name = "salsa-macros"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16d4d8b66451b9c75ddf740b7fc8399bc7b8ba33e854a5d7526d18708f67b05"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
[[package]]
name = "same-file"
version = "1.0.6"
@@ -2600,6 +2751,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "smol_str"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
+dependencies = [
+ "borsh",
+ "serde_core",
+]
+
[[package]]
name = "socket2"
version = "0.6.3"
@@ -2610,6 +2771,79 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "spar-analysis"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "la-arena",
+ "rustc-hash 2.1.1",
+ "spar-hir-def",
+]
+
+[[package]]
+name = "spar-annex"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "rowan",
+ "spar-syntax",
+]
+
+[[package]]
+name = "spar-base-db"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "rowan",
+ "salsa",
+ "spar-annex",
+ "spar-syntax",
+]
+
+[[package]]
+name = "spar-hir"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "salsa",
+ "smol_str",
+ "spar-base-db",
+ "spar-hir-def",
+ "spar-syntax",
+]
+
+[[package]]
+name = "spar-hir-def"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "la-arena",
+ "rowan",
+ "rustc-hash 2.1.1",
+ "salsa",
+ "smol_str",
+ "spar-base-db",
+ "spar-syntax",
+]
+
+[[package]]
+name = "spar-parser"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "rowan",
+]
+
+[[package]]
+name = "spar-syntax"
+version = "0.1.0"
+source = "git+https://github.com/pulseengine/spar.git?rev=21a5411#21a541180ba5efb9f37f1b9975468b2f475c3955"
+dependencies = [
+ "rowan",
+ "spar-parser",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -2724,6 +2958,18 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "text-size"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
+
+[[package]]
+name = "thin-vec"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
+
[[package]]
name = "thiserror"
version = "1.0.69"
diff --git a/Cargo.toml b/Cargo.toml
index 95cc9cb..55b4c07 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ version = "0.1.0"
authors = ["PulseEngine "]
edition = "2024"
license = "Apache-2.0"
-rust-version = "1.85"
+rust-version = "1.89"
[workspace.dependencies]
# Serialization
@@ -49,3 +49,7 @@ wasmtime-wasi = "42"
# Benchmarking
criterion = { version = "0.5", features = ["html_reports"] }
+
+# AADL (spar) — parser, HIR, analysis
+spar-hir = { git = "https://github.com/pulseengine/spar.git", rev = "21a5411" }
+spar-analysis = { git = "https://github.com/pulseengine/spar.git", rev = "21a5411" }
diff --git a/arch/rivet_adapters.aadl b/arch/rivet_adapters.aadl
new file mode 100644
index 0000000..3285b80
--- /dev/null
+++ b/arch/rivet_adapters.aadl
@@ -0,0 +1,92 @@
+-- rivet_adapters.aadl
+--
+-- Adapter subsystem — models the extensible import/export pipeline.
+-- Each adapter is a component that implements the Adapter trait/WIT interface.
+-- Native adapters are compiled-in; WASM adapters are loaded at runtime.
+
+package RivetAdapters
+public
+ with RivetSystem;
+
+ -- ── Adapter interface (mirrors Adapter trait / WIT) ─────────
+
+ abstract Adapter
+ features
+ source_in : in data port;
+ config_in : in data port;
+ artifacts : out data port RivetSystem::ArtifactSet;
+ export_data : out data port;
+ end Adapter;
+
+ -- ── Native adapters (compiled into rivet-core) ──────────────
+
+ system GenericYamlAdapter extends Adapter
+ -- Parses canonical YAML artifact files (artifacts/*.yaml)
+ end GenericYamlAdapter;
+
+ system StpaYamlAdapter extends Adapter
+ -- Imports meld STPA safety analysis YAML
+ end StpaYamlAdapter;
+
+ system AadlAdapter extends Adapter
+ -- Invokes spar CLI, parses JSON output into aadl-component artifacts
+ features
+ spar_invoke : requires data access; -- spar CLI on PATH
+ end AadlAdapter;
+
+ system ReqIfAdapter extends Adapter
+ -- ReqIF 1.2 XML import/export (RIF namespace)
+ end ReqIfAdapter;
+
+ -- ── WASM adapter runtime ────────────────────────────────────
+
+ system WasmRuntime
+ features
+ component_in : in data port; -- .wasm component bytes
+ adapter_out : out data port; -- instantiated Adapter
+ properties
+ -- Resource limits: fuel metering, memory cap
+ end WasmRuntime;
+
+ system implementation WasmRuntime.Impl
+ subcomponents
+ engine : process WasmEngine;
+ linker : process WasmLinker;
+ sandbox : process WasmSandbox;
+ connections
+ c_load : port component_in -> engine.load;
+ c_link : port engine.module -> linker.module;
+ c_sand : port linker.linked -> sandbox.run;
+ c_out : port sandbox.adapter -> adapter_out;
+ end WasmRuntime.Impl;
+
+ process WasmEngine
+ -- wasmtime::Engine with shared compilation cache
+ features
+ load : in data port;
+ module : out data port;
+ end WasmEngine;
+
+ process WasmLinker
+ -- Links WASI imports, adapter WIT interface
+ features
+ module : in data port;
+ linked : out data port;
+ end WasmLinker;
+
+ process WasmSandbox
+ -- Isolated execution with fuel metering and memory limits
+ features
+ run : in data port;
+ adapter : out data port;
+ end WasmSandbox;
+
+ -- ── Future: OSLC sync adapter ───────────────────────────────
+
+ system OslcAdapter extends Adapter
+ -- Bidirectional sync with Polarion, DOORS, codebeamer via OSLC RM/QM
+ features
+ oslc_endpoint : requires data access; -- remote OSLC service provider
+ end OslcAdapter;
+
+end RivetAdapters;
diff --git a/arch/rivet_dashboard.aadl b/arch/rivet_dashboard.aadl
new file mode 100644
index 0000000..7e49b4d
--- /dev/null
+++ b/arch/rivet_dashboard.aadl
@@ -0,0 +1,101 @@
+-- rivet_dashboard.aadl
+--
+-- Dashboard subsystem — the axum + HTMX serve handler.
+-- Models the HTTP routing, view rendering, and live-reload state.
+
+package RivetDashboard
+public
+ with RivetSystem;
+
+ -- ── Dashboard system boundary ───────────────────────────────
+
+ system Dashboard
+ features
+ http_in : in data port RivetSystem::HttpRequest;
+ http_out : out data port RivetSystem::HtmlFragment;
+ state : requires data access; -- Arc>
+ end Dashboard;
+
+ system implementation Dashboard.Impl
+ subcomponents
+ router : process HttpRouter;
+ views : process ViewRenderers;
+ graph_viz : process GraphVisualizer;
+ file_watch : process FileWatcher;
+ connections
+ c_route : port http_in -> router.request;
+ c_view : port router.view -> views.render;
+ c_graph : port router.graph -> graph_viz.render;
+ c_html : port views.html -> http_out;
+ c_graph_out: port graph_viz.svg -> views.graph_embed;
+ c_reload : port file_watch.changed -> router.reload;
+ end Dashboard.Impl;
+
+ -- ── HTTP router ─────────────────────────────────────────────
+
+ process HttpRouter
+ -- axum Router with middleware (redirect_non_htmx, logging)
+ features
+ request : in data port RivetSystem::HttpRequest;
+ view : out data port;
+ graph : out data port;
+ reload : in data port;
+ end HttpRouter;
+
+ -- ── View renderers ──────────────────────────────────────────
+
+ process ViewRenderers
+ -- Server-rendered HTML fragments for HTMX swap
+ features
+ render : in data port;
+ graph_embed : in data port;
+ html : out data port RivetSystem::HtmlFragment;
+ end ViewRenderers;
+
+ process implementation ViewRenderers.Impl
+ subcomponents
+ stats_view : thread StatsView;
+ artifacts_view : thread ArtifactsView;
+ artifact_detail : thread ArtifactDetailView;
+ validation_view : thread ValidationView;
+ matrix_view : thread MatrixView;
+ graph_view : thread GraphView;
+ source_view : thread SourceView;
+ doc_view : thread DocumentView;
+ doc_linkage_view : thread DocLinkageView;
+ search_handler : thread SearchHandler;
+ preview_handler : thread PreviewHandler;
+ end ViewRenderers.Impl;
+
+ -- ── Graph visualizer (etch layout engine) ───────────────────
+
+ process GraphVisualizer
+ -- petgraph → etch layout → SVG with pan/zoom/drag
+ features
+ render : in data port;
+ svg : out data port;
+ end GraphVisualizer;
+
+ -- ── File watcher for live reload ────────────────────────────
+
+ process FileWatcher
+ -- Watches project directory, triggers state rebuild
+ features
+ changed : out data port;
+ end FileWatcher;
+
+ -- ── Individual view threads ─────────────────────────────────
+
+ thread StatsView end StatsView;
+ thread ArtifactsView end ArtifactsView;
+ thread ArtifactDetailView end ArtifactDetailView;
+ thread ValidationView end ValidationView;
+ thread MatrixView end MatrixView;
+ thread GraphView end GraphView;
+ thread SourceView end SourceView;
+ thread DocumentView end DocumentView;
+ thread DocLinkageView end DocLinkageView;
+ thread SearchHandler end SearchHandler;
+ thread PreviewHandler end PreviewHandler;
+
+end RivetDashboard;
diff --git a/arch/rivet_system.aadl b/arch/rivet_system.aadl
new file mode 100644
index 0000000..f76f888
--- /dev/null
+++ b/arch/rivet_system.aadl
@@ -0,0 +1,224 @@
+-- rivet_system.aadl
+--
+-- Top-level system architecture for Rivet.
+-- Models the system as seen by users: CLI commands and HTTP dashboard.
+
+package RivetSystem
+public
+
+ -- ── Data types ──────────────────────────────────────────────
+
+ data ArtifactSet
+ -- Collection of parsed artifacts (YAML, STPA, AADL, ReqIF)
+ end ArtifactSet;
+
+ data ValidationReport
+ -- Schema violations, orphans, coverage gaps
+ end ValidationReport;
+
+ data ProjectConfig
+ -- Parsed rivet.yaml: schemas, sources, project metadata
+ end ProjectConfig;
+
+ data SchemaSet
+ -- Merged schema (common + domain overlays)
+ end SchemaSet;
+
+ data LinkGraph
+ -- petgraph-backed directed graph of artifact links
+ end LinkGraph;
+
+ data TraceMatrix
+ -- Coverage matrix: source type → target type with percentages
+ end TraceMatrix;
+
+ data HtmlFragment
+ -- Server-rendered HTML (HTMX partial or full page)
+ end HtmlFragment;
+
+ data HttpRequest
+ -- Inbound HTTP request (axum)
+ end HttpRequest;
+
+ -- ── System boundary ─────────────────────────────────────────
+
+ system Rivet
+ features
+ cli_input : in data port;
+ cli_output : out data port;
+ http_request : in data port HttpRequest;
+ http_response : out data port HtmlFragment;
+ filesystem : requires data access;
+ end Rivet;
+
+ system implementation Rivet.Impl
+ subcomponents
+ core : process RivetCore.Impl;
+ cli : process RivetCli.Impl;
+ connections
+ c_cli_in : port cli_input -> cli.commands;
+ c_cli_out : port cli.output -> cli_output;
+ c_http_in : port http_request -> cli.http_in;
+ c_http_out: port cli.http_out -> http_response;
+ c_core : port cli.core_req -> core.request;
+ c_core_r : port core.response -> cli.core_resp;
+ end Rivet.Impl;
+
+ -- ── RivetCore process ───────────────────────────────────────
+
+ process RivetCore
+ features
+ request : in data port;
+ response : out data port;
+ end RivetCore;
+
+ process implementation RivetCore.Impl
+ subcomponents
+ config_loader : thread ConfigLoader;
+ schema_engine : thread SchemaEngine;
+ store : thread ArtifactStore;
+ adapters : thread AdapterDispatch;
+ graph : thread GraphEngine;
+ validator : thread ValidationEngine;
+ matrix : thread MatrixEngine;
+ diff_engine : thread DiffEngine;
+ doc_engine : thread DocumentEngine;
+ query_engine : thread QueryEngine;
+ results : thread ResultsEngine;
+ connections
+ -- Config loads schemas and source list
+ cfg_schema : port config_loader.schemas -> schema_engine.load;
+ cfg_src : port config_loader.sources -> adapters.source_list;
+ -- Adapters produce artifacts into the store
+ adapt_arts : port adapters.artifacts -> store.ingest;
+ -- Store feeds the graph builder
+ store_graph: port store.artifact_set -> graph.build;
+ -- Validator reads schema + store + graph
+ val_schema : port schema_engine.merged -> validator.schema;
+ val_store : port store.artifact_set -> validator.artifacts;
+ val_graph : port graph.link_graph -> validator.graph;
+ -- Matrix reads graph + store
+ mat_graph : port graph.link_graph -> matrix.graph;
+ mat_store : port store.artifact_set -> matrix.artifacts;
+ -- Document engine scans source files
+ doc_store : port store.artifact_set -> doc_engine.artifacts;
+ -- Query engine for search/filter
+ qry_store : port store.artifact_set -> query_engine.artifacts;
+ end RivetCore.Impl;
+
+ -- ── RivetCli process ────────────────────────────────────────
+
+ process RivetCli
+ features
+ commands : in data port;
+ output : out data port;
+ http_in : in data port HttpRequest;
+ http_out : out data port HtmlFragment;
+ core_req : out data port;
+ core_resp : in data port;
+ end RivetCli;
+
+ process implementation RivetCli.Impl
+ subcomponents
+ cmd_dispatch : thread CommandDispatch;
+ serve_handler : thread ServeHandler;
+ connections
+ cmd_in : port commands -> cmd_dispatch.input;
+ cmd_out : port cmd_dispatch.output -> output;
+ cmd_core : port cmd_dispatch.core_call -> core_req;
+ cmd_resp : port core_resp -> cmd_dispatch.core_result;
+ srv_in : port http_in -> serve_handler.request;
+ srv_out : port serve_handler.response -> http_out;
+ srv_core : port core_resp -> serve_handler.data;
+ end RivetCli.Impl;
+
+ -- ── Core threads (one per module) ───────────────────────────
+
+ thread ConfigLoader
+ features
+ schemas : out data port SchemaSet;
+ sources : out data port;
+ end ConfigLoader;
+
+ thread SchemaEngine
+ features
+ load : in data port;
+ merged : out data port SchemaSet;
+ end SchemaEngine;
+
+ thread ArtifactStore
+ features
+ ingest : in data port ArtifactSet;
+ artifact_set : out data port ArtifactSet;
+ end ArtifactStore;
+
+ thread AdapterDispatch
+ features
+ source_list : in data port;
+ artifacts : out data port ArtifactSet;
+ end AdapterDispatch;
+
+ thread GraphEngine
+ features
+ build : in data port ArtifactSet;
+ link_graph : out data port LinkGraph;
+ end GraphEngine;
+
+ thread ValidationEngine
+ features
+ schema : in data port SchemaSet;
+ artifacts : in data port ArtifactSet;
+ graph : in data port LinkGraph;
+ report : out data port ValidationReport;
+ end ValidationEngine;
+
+ thread MatrixEngine
+ features
+ graph : in data port LinkGraph;
+ artifacts : in data port ArtifactSet;
+ matrix : out data port TraceMatrix;
+ end MatrixEngine;
+
+ thread DiffEngine
+ features
+ baseline : in data port ArtifactSet;
+ current : in data port ArtifactSet;
+ delta : out data port;
+ end DiffEngine;
+
+ thread DocumentEngine
+ features
+ artifacts : in data port ArtifactSet;
+ documents : out data port;
+ end DocumentEngine;
+
+ thread QueryEngine
+ features
+ artifacts : in data port ArtifactSet;
+ results : out data port;
+ end QueryEngine;
+
+ thread ResultsEngine
+ features
+ test_data : in data port;
+ summary : out data port;
+ end ResultsEngine;
+
+ -- ── CLI threads ─────────────────────────────────────────────
+
+ thread CommandDispatch
+ features
+ input : in data port;
+ output : out data port;
+ core_call : out data port;
+ core_result : in data port;
+ end CommandDispatch;
+
+ thread ServeHandler
+ features
+ request : in data port HttpRequest;
+ response : out data port HtmlFragment;
+ data : in data port;
+ end ServeHandler;
+
+end RivetSystem;
diff --git a/artifacts/architecture.yaml b/artifacts/architecture.yaml
new file mode 100644
index 0000000..7be91de
--- /dev/null
+++ b/artifacts/architecture.yaml
@@ -0,0 +1,381 @@
+artifacts:
+ # ── System-level components ──────────────────────────────────
+
+ - id: ARCH-SYS-001
+ type: aadl-component
+ title: Rivet System (top-level)
+ status: implemented
+ tags: [aadl, architecture, system]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: system
+ aadl-package: RivetSystem
+ classifier-kind: type
+ aadl-file: arch/rivet_system.aadl:49
+ source-ref: arch/rivet_system.aadl:49-54
+
+ - id: ARCH-SYS-002
+ type: aadl-component
+ title: Rivet System Implementation
+ status: implemented
+ tags: [aadl, architecture, system]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: system
+ aadl-package: RivetSystem
+ classifier-kind: implementation
+ aadl-file: arch/rivet_system.aadl:56-67
+
+ # ── Core process ─────────────────────────────────────────────
+
+ - id: ARCH-CORE-001
+ type: aadl-component
+ title: RivetCore process
+ description: >
+ Core library process containing all domain logic: config loading,
+ schema merging, artifact storage, adapter dispatch, graph building,
+ validation, matrix computation, diff, documents, and query.
+ status: implemented
+ tags: [aadl, architecture, core]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ - type: allocated-from
+ target: REQ-002
+ fields:
+ category: process
+ aadl-package: RivetSystem
+ classifier-kind: implementation
+ aadl-file: arch/rivet_system.aadl:75-108
+ source-ref: rivet-core/src/lib.rs:1
+
+ # ── CLI process ──────────────────────────────────────────────
+
+ - id: ARCH-CLI-001
+ type: aadl-component
+ title: RivetCli process
+ description: >
+ CLI binary process: dispatches subcommands (validate, list, stats,
+ matrix, stpa, serve) and hosts the HTTP dashboard.
+ status: implemented
+ tags: [aadl, architecture, cli]
+ links:
+ - type: allocated-from
+ target: REQ-007
+ fields:
+ category: process
+ aadl-package: RivetSystem
+ classifier-kind: implementation
+ aadl-file: arch/rivet_system.aadl:112-125
+ source-ref: rivet-cli/src/main.rs:1
+
+ # ── Core threads (one per module) ────────────────────────────
+
+ - id: ARCH-CORE-SCHEMA
+ type: aadl-component
+ title: Schema Engine
+ description: >
+ Loads and merges YAML schema files (common + domain overlays).
+ Produces the merged SchemaSet used by validation and matrix.
+ status: implemented
+ tags: [aadl, architecture, core, schema]
+ links:
+ - type: allocated-from
+ target: REQ-002
+ - type: allocated-from
+ target: REQ-003
+ - type: allocated-from
+ target: REQ-010
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/schema.rs:1
+
+ - id: ARCH-CORE-STORE
+ type: aadl-component
+ title: Artifact Store
+ description: >
+ In-memory store holding all loaded artifacts. Provides lookup by ID,
+ type filtering, and iteration.
+ status: implemented
+ tags: [aadl, architecture, core, store]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/store.rs:1
+
+ - id: ARCH-CORE-ADAPTERS
+ type: aadl-component
+ title: Adapter Dispatch
+ description: >
+ Dispatches source loading to the appropriate adapter based on format
+ string (generic-yaml, stpa-yaml, aadl, reqif).
+ status: implemented
+ tags: [aadl, architecture, core, adapters]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ - type: allocated-from
+ target: REQ-005
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/adapter.rs:1
+
+ - id: ARCH-CORE-GRAPH
+ type: aadl-component
+ title: Graph Engine
+ description: >
+ Builds petgraph directed graph from artifact links. Provides cycle
+ detection, orphan detection, reachability queries, and topological sort.
+ status: implemented
+ tags: [aadl, architecture, core, graph]
+ links:
+ - type: allocated-from
+ target: REQ-004
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/links.rs:1
+
+ - id: ARCH-CORE-VALIDATE
+ type: aadl-component
+ title: Validation Engine
+ description: >
+ Validates artifacts against schema (types, required fields, link
+ constraints, traceability rules). Produces ValidationReport with
+ errors and warnings.
+ status: implemented
+ tags: [aadl, architecture, core, validation]
+ links:
+ - type: allocated-from
+ target: REQ-003
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/validate.rs:1
+
+ - id: ARCH-CORE-MATRIX
+ type: aadl-component
+ title: Matrix Engine
+ description: >
+ Computes traceability matrix with coverage percentages.
+ Source type → target type mapping with linked/total counts.
+ status: implemented
+ tags: [aadl, architecture, core, matrix]
+ links:
+ - type: allocated-from
+ target: REQ-004
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/matrix.rs:1
+
+ - id: ARCH-CORE-DIFF
+ type: aadl-component
+ title: Diff Engine
+ description: >
+ Computes artifact differences between two snapshots (added, removed,
+ modified artifacts with field-level change details).
+ status: implemented
+ tags: [aadl, architecture, core, diff]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/diff.rs:1
+
+ - id: ARCH-CORE-DOC
+ type: aadl-component
+ title: Document Engine
+ description: >
+ Scans source files for artifact references ([[ID]] patterns),
+ builds document model with sections and cross-references.
+ status: implemented
+ tags: [aadl, architecture, core, documents]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/document.rs:1
+
+ - id: ARCH-CORE-QUERY
+ type: aadl-component
+ title: Query Engine
+ description: >
+ Filters and searches artifacts by type, status, tags, and
+ free-text queries.
+ status: implemented
+ tags: [aadl, architecture, core, query]
+ links:
+ - type: allocated-from
+ target: REQ-001
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/query.rs:1
+
+ - id: ARCH-CORE-RESULTS
+ type: aadl-component
+ title: Results Engine
+ description: >
+ Parses test execution results (JUnit XML) and coverage data (LCOV)
+ for evidence tracking and dashboard display.
+ status: implemented
+ tags: [aadl, architecture, core, results]
+ links:
+ - type: allocated-from
+ target: REQ-009
+ fields:
+ category: thread
+ aadl-package: RivetSystem
+ classifier-kind: type
+ source-ref: rivet-core/src/results.rs:1
+
+ # ── Adapter components ───────────────────────────────────────
+
+ - id: ARCH-ADAPT-GENERIC
+ type: aadl-component
+ title: Generic YAML Adapter
+ description: >
+ Imports canonical YAML artifact files. Primary format for
+ hand-authored artifacts.
+ status: implemented
+ tags: [aadl, architecture, adapter]
+ links:
+ - type: allocated-from
+ target: FEAT-002
+ fields:
+ category: system
+ aadl-package: RivetAdapters
+ classifier-kind: type
+ source-ref: rivet-core/src/formats/generic.rs:1
+
+ - id: ARCH-ADAPT-STPA
+ type: aadl-component
+ title: STPA YAML Adapter
+ description: >
+ Imports meld's STPA safety analysis YAML format. Maps losses,
+ hazards, constraints, UCAs, and scenarios to rivet artifacts.
+ status: implemented
+ tags: [aadl, architecture, adapter, stpa]
+ links:
+ - type: allocated-from
+ target: FEAT-001
+ fields:
+ category: system
+ aadl-package: RivetAdapters
+ classifier-kind: type
+ source-ref: rivet-core/src/formats/stpa.rs:1
+
+ - id: ARCH-ADAPT-AADL
+ type: aadl-component
+ title: AADL Adapter (spar integration)
+ description: >
+ Layer 1 integration: invokes spar CLI with --format json, parses
+ output into aadl-component and aadl-analysis-result artifacts.
+ status: implemented
+ tags: [aadl, architecture, adapter]
+ links:
+ - type: allocated-from
+ target: FEAT-018
+ fields:
+ category: system
+ aadl-package: RivetAdapters
+ classifier-kind: type
+ source-ref: rivet-core/src/formats/aadl.rs:1
+
+ - id: ARCH-ADAPT-REQIF
+ type: aadl-component
+ title: ReqIF 1.2 Adapter
+ description: >
+ Import/export of ReqIF 1.2 XML. Handles spec-objects, spec-types,
+ and spec-relations mapping to rivet artifacts and links.
+ status: implemented
+ tags: [aadl, architecture, adapter, reqif]
+ links:
+ - type: allocated-from
+ target: FEAT-010
+ fields:
+ category: system
+ aadl-package: RivetAdapters
+ classifier-kind: type
+ source-ref: rivet-core/src/reqif.rs:1
+
+ - id: ARCH-ADAPT-WASM
+ type: aadl-component
+ title: WASM Adapter Runtime
+ description: >
+ Loads WASM component adapters at runtime via WIT interface.
+ Provides sandboxed execution with fuel metering and memory limits.
+ status: partial
+ tags: [aadl, architecture, adapter, wasm]
+ links:
+ - type: allocated-from
+ target: FEAT-012
+ - type: allocated-from
+ target: REQ-008
+ fields:
+ category: system
+ aadl-package: RivetAdapters
+ classifier-kind: implementation
+ source-ref: rivet-core/src/wasm_runtime.rs:1
+
+ # ── Dashboard components ─────────────────────────────────────
+
+ - id: ARCH-DASH-001
+ type: aadl-component
+ title: Dashboard System
+ description: >
+ axum HTTP server with HTMX-driven dashboard. Server-rendered HTML
+ fragments, no frontend framework. Includes pan/zoom/drag graph
+ visualization, artifact hover previews, and Cmd+K search.
+ status: implemented
+ tags: [aadl, architecture, dashboard]
+ links:
+ - type: allocated-from
+ target: FEAT-009
+ - type: allocated-from
+ target: REQ-007
+ fields:
+ category: system
+ aadl-package: RivetDashboard
+ classifier-kind: implementation
+ aadl-file: arch/rivet_dashboard.aadl:18-35
+ source-ref: rivet-cli/src/serve.rs:1
+
+ - id: ARCH-DASH-GRAPH
+ type: aadl-component
+ title: Graph Visualizer (etch)
+ description: >
+ Renders petgraph link graphs as SVG using the etch layout engine.
+ Supports interactive pan, zoom, and node dragging.
+ status: implemented
+ tags: [aadl, architecture, dashboard, graph]
+ links:
+ - type: allocated-from
+ target: FEAT-009
+ fields:
+ category: process
+ aadl-package: RivetDashboard
+ classifier-kind: type
diff --git a/artifacts/decisions.yaml b/artifacts/decisions.yaml
index 229c078..45eea1a 100644
--- a/artifacts/decisions.yaml
+++ b/artifacts/decisions.yaml
@@ -39,6 +39,7 @@ artifacts:
alternatives: >
Custom adjacency list implementation. Rejected because graph
algorithms are subtle and petgraph is well-proven.
+ source-ref: rivet-core/src/graph.rs:1
- id: DD-003
type: design-decision
@@ -56,6 +57,7 @@ artifacts:
- type: satisfies
target: REQ-003
fields:
+ source-ref: rivet-core/src/schema.rs:1
rationale: >
Merging allows a base common schema to be extended by domain-specific
schemas without duplication. Projects pick which schemas they need.
diff --git a/artifacts/features.yaml b/artifacts/features.yaml
index 4d1866e..54994fd 100644
--- a/artifacts/features.yaml
+++ b/artifacts/features.yaml
@@ -262,3 +262,53 @@ artifacts:
target: DD-010
fields:
phase: phase-1
+
+ - id: FEAT-018
+ type: feature
+ title: AADL adapter (spar Layer 1)
+ status: approved
+ description: >
+ Import AADL architecture models via spar CLI JSON output. Converts
+ spar component types, implementations, and analysis diagnostics
+ into rivet aadl-component and aadl-analysis-result artifacts.
+ tags: [adapter, aadl, phase-2]
+ links:
+ - type: satisfies
+ target: REQ-001
+ - type: satisfies
+ target: REQ-005
+ fields:
+ phase: phase-2
+
+ - id: FEAT-019
+ type: feature
+ title: AADL architecture dogfood (rivet self-model)
+ status: approved
+ description: >
+ Model rivet's own system and software architecture as AADL
+ components in arch/. Three packages: RivetSystem (top-level),
+ RivetAdapters (extensibility), RivetDashboard (serve/UI).
+ Architecture artifacts trace to requirements via allocated-from.
+ tags: [aadl, architecture, dogfood, phase-2]
+ links:
+ - type: satisfies
+ target: REQ-001
+ fields:
+ phase: phase-2
+
+ - id: FEAT-020
+ type: feature
+ title: AADL browser rendering (spar WASM)
+ status: draft
+ description: >
+ Render AADL component diagrams in the dashboard using a spar WASM
+ module compiled for the browser. Provides interactive visualization
+ of system/software architecture with drill-down into subcomponents.
+ tags: [aadl, wasm, ui, phase-3]
+ links:
+ - type: satisfies
+ target: REQ-008
+ - type: satisfies
+ target: REQ-007
+ fields:
+ phase: phase-3
diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml
new file mode 100644
index 0000000..61b0ac3
--- /dev/null
+++ b/artifacts/verification.yaml
@@ -0,0 +1,193 @@
+artifacts:
+ - id: TEST-001
+ type: feature
+ title: Store and model unit tests
+ status: approved
+ description: >
+ Unit tests for the diff, document, and store modules. Verifies artifact
+ storage, retrieval, upsert, by-type indexing, YAML frontmatter parsing,
+ document reference extraction, HTML rendering, and structural diff
+ computation between store snapshots.
+ tags: [testing, swe-4]
+ links:
+ - type: verifies
+ target: REQ-001
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-002
+ type: feature
+ title: STPA adapter and schema tests
+ status: approved
+ description: >
+ Tests in stpa_roundtrip.rs that verify STPA schema loading, artifact
+ type and link type presence, store insert/lookup, duplicate ID rejection,
+ and broken link detection within the STPA domain.
+ tags: [testing, stpa, swe-5]
+ links:
+ - type: verifies
+ target: REQ-002
+ - type: verifies
+ target: REQ-004
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-003
+ type: feature
+ title: Schema validation and merge tests
+ status: approved
+ description: >
+ Integration tests for schema loading and merging. Verifies that common +
+ stpa + aspice merge preserves all artifact types, link types, and inverse
+ mappings. Includes cybersecurity schema merge verification and ASPICE
+ traceability rule enforcement.
+ tags: [testing, schema, swe-5]
+ links:
+ - type: verifies
+ target: REQ-010
+ - type: verifies
+ target: REQ-004
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-004
+ type: feature
+ title: Link graph and coverage tests
+ status: approved
+ description: >
+ Tests for link graph construction, backlink computation, orphan detection,
+ reachability queries, and traceability matrix computation. Includes
+ coverage module tests for full, partial, and vacuous coverage scenarios.
+ tags: [testing, validation, swe-5]
+ links:
+ - type: verifies
+ target: REQ-004
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-005
+ type: feature
+ title: ReqIF roundtrip tests
+ status: approved
+ description: >
+ Unit tests in reqif.rs and integration tests in integration.rs that
+ verify ReqIF 1.2 XML export produces valid structure, minimal ReqIF
+ parsing, full roundtrip preservation of artifacts/links/fields, and
+ ReqIF-to-store integration.
+ tags: [testing, reqif, swe-5]
+ links:
+ - type: verifies
+ target: REQ-005
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-006
+ type: feature
+ title: Property-based tests (proptest)
+ status: approved
+ description: >
+ Six proptest properties verifying store insert/lookup consistency,
+ duplicate rejection, schema merge idempotence, link graph backlink
+ symmetry, validation determinism, and type iterator correctness.
+ Runs 30-50 randomized cases per property.
+ tags: [testing, proptest, swe-4]
+ links:
+ - type: verifies
+ target: REQ-001
+ - type: verifies
+ target: REQ-004
+ - type: verifies
+ target: REQ-010
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-007
+ type: feature
+ title: Integration test suite
+ status: approved
+ description: >
+ Eighteen cross-module integration tests exercising the full pipeline:
+ dogfood validation, generic YAML roundtrip, schema merge, traceability
+ matrix, query filters, link graph, ASPICE rules, store upsert, ReqIF
+ roundtrip, diff computation, and diagnostic diffing.
+ tags: [testing, integration, swe-5]
+ links:
+ - type: verifies
+ target: REQ-003
+ - type: verifies
+ target: REQ-007
+ - type: verifies
+ target: REQ-001
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-008
+ type: feature
+ title: Diff module tests
+ status: approved
+ description: >
+ Five unit tests for the diff module verifying empty diff, identical
+ stores, added artifacts, removed artifacts, and modified artifact
+ detection including title, status, tags, links, and fields changes.
+ tags: [testing, diff, swe-4]
+ links:
+ - type: verifies
+ target: REQ-001
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-009
+ type: feature
+ title: Document system tests
+ status: approved
+ description: >
+ Nine unit tests for the document module verifying YAML frontmatter
+ parsing, error handling for missing frontmatter, document store
+ operations, HTML heading rendering, wiki-link reference resolution,
+ default document type inference, multiple references per line,
+ reference extraction, and section hierarchy extraction.
+ tags: [testing, document, swe-4]
+ links:
+ - type: verifies
+ target: REQ-001
+ - type: verifies
+ target: REQ-007
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
+
+ - id: TEST-010
+ type: feature
+ title: Results model tests
+ status: approved
+ description: >
+ Nine unit tests for the results module verifying TestStatus display
+ and predicate methods, ResultStore insert ordering, latest_for and
+ history_for queries, aggregate summary statistics, YAML roundtrip
+ serialization, and edge case handling for empty and nonexistent
+ result directories.
+ tags: [testing, results, swe-4]
+ links:
+ - type: verifies
+ target: REQ-009
+ - type: satisfies
+ target: REQ-014
+ fields:
+ phase: phase-1
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..a054675
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,323 @@
+---
+id: ARCH-001
+type: architecture
+title: Rivet System Architecture
+status: approved
+glossary:
+ STPA: Systems-Theoretic Process Analysis
+ ASPICE: Automotive SPICE
+ OSLC: Open Services for Lifecycle Collaboration
+ ReqIF: Requirements Interchange Format
+ WASM: WebAssembly
+ WIT: WASM Interface Types
+ HTMX: Hypermedia-driven AJAX
+ CLI: Command-Line Interface
+ YAML: YAML Ain't Markup Language
+---
+
+# Rivet System Architecture
+
+## 1. System Overview
+
+Rivet is a Rust-based SDLC traceability tool for safety-critical systems. It
+manages lifecycle artifacts (requirements, designs, tests, STPA analyses) as
+version-controlled YAML files and validates their traceability links against
+composable schemas.
+
+The system is structured as two crates following [[DD-006]]:
+
+- **rivet-core** -- Library crate containing all domain logic: artifact model,
+ adapters, schema loading, link graph, validation, coverage, matrix
+ computation, diff, document system, query engine, and format-specific
+ adapters.
+
+- **rivet-cli** -- Binary crate providing the `rivet` command-line tool and
+ the axum + HTMX dashboard server. Depends on rivet-core for all domain
+ operations.
+
+This flat crate structure keeps module boundaries clear without deep nesting.
+The library/binary split ensures that rivet-core can be consumed as a Rust
+dependency by other tools or tested independently.
+
+### System Architecture Diagram
+
+The top-level system with its core and CLI subsystems:
+
+```aadl
+root: RivetSystem::Rivet.Impl
+```
+
+### Core Process Internals
+
+The core library process showing all domain logic modules and their data flow:
+
+```aadl
+root: RivetSystem::RivetCore.Impl
+```
+
+### CLI Process
+
+The CLI binary process with command dispatch and HTTP serve handler:
+
+```aadl
+root: RivetSystem::RivetCli.Impl
+```
+
+## 2. Module Structure
+
+### 2.1 rivet-core Modules
+
+| Module | Purpose |
+|--------------|------------------------------------------------------------------|
+| `model` | Core data types: `Artifact`, `Link`, `ProjectConfig`, `SourceConfig` |
+| `store` | In-memory artifact store with by-ID and by-type indexing |
+| `schema` | Schema loading, merging, artifact type and link type definitions |
+| `links` | `LinkGraph` construction via petgraph, backlinks, orphan detection |
+| `validate` | Validation engine: types, fields, cardinality, traceability rules |
+| `coverage` | Traceability coverage computation per rule |
+| `matrix` | Traceability matrix computation (forward and backward) |
+| `query` | Query engine: filter artifacts by type, status, tag, link presence |
+| `diff` | Artifact diff and diagnostic diff between two store snapshots |
+| `document` | Markdown documents with YAML frontmatter and wiki-link references |
+| `results` | Test run results model, YAML loading, and `ResultStore` |
+| `adapter` | Adapter trait and configuration for import/export |
+| `reqif` | ReqIF 1.2 XML import/export adapter |
+| `oslc` | OSLC client for discovery, query, CRUD, and sync (feature-gated) |
+| `wasm_runtime` | WASM component adapter runtime (feature-gated) |
+| `error` | Unified error type for the library |
+| `formats/` | Format-specific adapters: `generic` (YAML), `stpa` (STPA YAML) |
+
+### 2.2 rivet-cli Modules
+
+| Module | Purpose |
+|---------|----------------------------------------------------------------------|
+| `main` | CLI entry point, clap argument parsing, subcommand dispatch |
+| `serve` | axum HTTP server with HTMX-rendered dashboard pages |
+
+## 3. Data Flow
+
+The core data pipeline follows a consistent flow from YAML files through to
+validation results:
+
+```
+ rivet.yaml
+ |
+ v
+ ProjectConfig
+ |
+ +---> Schema loading (schemas/*.yaml)
+ | |
+ | v
+ | Schema::merge() --> merged Schema
+ |
+ +---> Artifact loading (sources/*.yaml)
+ |
+ v
+ Adapter::import() --> Vec
+ |
+ v
+ Store (in-memory, indexed by ID and type)
+ |
+ +---> LinkGraph::build(&store, &schema)
+ | |
+ | v
+ | petgraph DiGraph (nodes = artifacts, edges = links)
+ | |
+ | +---> validate::validate() --> Vec
+ | +---> coverage::compute() --> CoverageReport
+ | +---> matrix::compute() --> TraceabilityMatrix
+ | +---> graph.orphans() --> orphan detection
+ | +---> graph.broken --> broken links
+ |
+ +---> query::execute(&store, &query) --> filtered artifacts
+ +---> diff::ArtifactDiff::compute() --> change analysis
+```
+
+### 3.1 Schema Loading
+
+Schemas are loaded from YAML files and merged using `Schema::merge()`. Each
+schema file declares artifact types with field definitions, link-field
+constraints (cardinality, target types), and traceability rules. The merge
+operation combines types and link types from multiple schemas, enabling
+composition: a project can load `common + dev`, `common + stpa`,
+`common + aspice + cybersecurity`, or any combination.
+
+This design is specified by [[REQ-010]] and [[DD-003]].
+
+### 3.2 Adapter Pipeline
+
+Adapters implement the `Adapter` trait, which defines `import()` and
+`export()` methods. Three native adapters exist:
+
+1. **GenericYamlAdapter** -- Canonical YAML format with explicit type, links
+ array, and fields map. Used for Rivet's own artifacts.
+2. **StpaYamlAdapter** -- Imports STPA analysis artifacts from the meld
+ project's YAML format (losses, hazards, UCAs, etc.).
+3. **ReqIfAdapter** -- Import/export for OMG ReqIF 1.2 XML, enabling
+ interchange with DOORS, Polarion, and codebeamer ([[REQ-005]]).
+
+The WASM adapter runtime ([[DD-004]]) and OSLC sync adapter ([[DD-001]])
+extend this pipeline for plugin formats and remote tool synchronization.
+
+```aadl
+root: RivetAdapters::WasmRuntime.Impl
+```
+
+### 3.3 Link Graph
+
+The `LinkGraph` module uses petgraph ([[DD-002]]) to build a directed graph
+where nodes are artifacts and edges are links. The graph provides:
+
+- **Forward links** -- `links_from(id)` returns outgoing links
+- **Backlinks** -- `backlinks_to(id)` returns incoming links with inverse type
+- **Broken links** -- Links where the target artifact doesn't exist
+- **Orphans** -- Artifacts with no incoming or outgoing links
+- **Reachability** -- `reachable(id, link_type)` for transitive closure
+
+### 3.4 Validation Engine
+
+The validator ([[REQ-004]]) checks artifacts against the merged schema:
+
+1. **Known type** -- Every artifact's type must exist in the schema
+2. **Required fields** -- Type-specific required fields must be present
+3. **Allowed values** -- Field values must match the schema's allowed set
+4. **Link cardinality** -- Link counts must satisfy exactly-one, one-or-many,
+ zero-or-one, or zero-or-many constraints
+5. **Link target types** -- Link targets must have the correct artifact type
+6. **Broken links** -- All link targets must exist in the store
+7. **Traceability rules** -- Forward and backward link coverage rules
+
+Diagnostics are returned with severity levels (error, warning, info) and the
+caller decides whether to fail on errors.
+
+## 4. Dashboard Architecture
+
+```aadl
+root: RivetDashboard::Dashboard.Impl
+```
+
+The HTTP dashboard follows [[DD-005]], using axum as the server framework and
+HTMX for dynamic page updates without a JavaScript build toolchain.
+
+### 4.1 Server Structure
+
+The `serve` module in rivet-cli sets up an axum `Router` with routes for:
+
+- `/` -- Project overview with artifact counts, validation status, and context
+- `/artifacts` -- Browsable artifact list with type/status filters
+- `/artifact/:id` -- Single artifact detail with links and backlinks
+- `/matrix` -- Traceability matrix view
+- `/coverage` -- Coverage report
+- `/docs` -- Document browser
+- `/doc/:id` -- Single document rendered as HTML
+- `/results` -- Test result runs and history
+- `/graph` -- Interactive link graph visualization (SVG via etch)
+
+### 4.2 Application State
+
+The server holds shared state behind `Arc>`:
+
+- `Store` -- All loaded artifacts
+- `Schema` -- Merged schema
+- `LinkGraph` -- Precomputed link graph
+- `DocumentStore` -- Loaded markdown documents
+- `ResultStore` -- Test result runs
+- `RepoContext` -- Git branch, commit, dirty state, sibling projects
+
+### 4.3 Page Layout
+
+Every page shares a common layout with:
+
+- **Context bar** -- Project name, git branch/commit, dirty indicator,
+ loaded-at timestamp, and sibling project links
+- **Navigation** -- Horizontal nav bar linking to all major views
+- **Content area** -- Route-specific content rendered as HTML fragments
+
+HTMX provides partial page updates: clicking a navigation link fetches only
+the content fragment and swaps it into the page, avoiding full reloads.
+
+## 5. Schema System
+
+### 5.1 Schema Files
+
+Schema files are YAML documents defining:
+
+```yaml
+schema:
+ name: dev
+ version: "0.1.0"
+ extends: [common]
+
+artifact-types:
+ - name: requirement
+ fields: [...]
+ link-fields: [...]
+
+link-types:
+ - name: satisfies
+ inverse: satisfied-by
+
+traceability-rules:
+ - name: requirement-coverage
+ source-type: requirement
+ required-backlink: satisfies
+ severity: warning
+```
+
+### 5.2 Available Schemas
+
+| Schema | Types | Link Types | Rules | Domain |
+|-----------------|-------|------------|-------|--------------------------------|
+| `common` | 0 | 9 | 0 | Base fields and link types |
+| `dev` | 3 | 1 | 2 | Development tracking |
+| `stpa` | 10 | 5 | 7 | STPA safety analysis |
+| `aspice` | 14 | 2 | 10 | ASPICE v4.0 V-model |
+| `cybersecurity` | 10 | 2 | 10 | SEC.1-4, ISO/SAE 21434 |
+
+### 5.3 Merge Semantics
+
+When schemas are merged, artifact types, link types, and traceability rules
+are combined by name. If two schemas define the same type, the later
+definition wins. Inverse mappings are rebuilt after merge. This enables
+domain-specific schemas to extend common definitions without duplication.
+
+## 6. Test Results as Evidence
+
+[[REQ-009]] specifies that test execution results are tied to releases as
+evidence. The `results` module ([[DD-007]]) implements this:
+
+- **TestRunFile** -- YAML format with run metadata and per-artifact results
+- **ResultStore** -- In-memory collection sorted by timestamp
+- **TestStatus** -- Pass, fail, skip, error, blocked
+- **ResultSummary** -- Aggregate statistics with pass rate
+
+Results files are loaded from a configured directory and displayed in the
+dashboard alongside artifacts they verify.
+
+## 7. Design Decisions
+
+This architecture reflects the following key decisions:
+
+- [[DD-001]] -- OSLC over per-tool REST adapters for external tool sync
+- [[DD-002]] -- petgraph for link graph operations
+- [[DD-003]] -- Mergeable YAML schemas for domain composition
+- [[DD-004]] -- WIT-based WASM adapter interface for plugins
+- [[DD-005]] -- axum + HTMX serve pattern for the dashboard
+- [[DD-006]] -- Flat crate structure (rivet-core + rivet-cli)
+- [[DD-007]] -- Test results tied to GitHub releases
+- [[DD-008]] -- Rust edition 2024 with comprehensive CI
+- [[DD-009]] -- Criterion benchmarks as KPI baselines
+- [[DD-010]] -- ASPICE 4.0 terminology and composable cybersecurity schema
+
+## 8. Requirements Coverage
+
+This document addresses the following requirements:
+
+- [[REQ-001]] -- Text-file-first artifact management (section 2, 3)
+- [[REQ-004]] -- Validation engine (section 3.4)
+- [[REQ-005]] -- ReqIF 1.2 import/export (section 3.2)
+- [[REQ-006]] -- OSLC-based tool synchronization (section 3.2)
+- [[REQ-007]] -- CLI and serve pattern (section 4)
+- [[REQ-008]] -- WASM component adapters (section 3.2)
+- [[REQ-009]] -- Test results as release evidence (section 6)
+- [[REQ-010]] -- Schema-driven validation (section 5)
diff --git a/docs/plans/2026-03-09-spar-wasm-browser-rendering.md b/docs/plans/2026-03-09-spar-wasm-browser-rendering.md
new file mode 100644
index 0000000..145d915
--- /dev/null
+++ b/docs/plans/2026-03-09-spar-wasm-browser-rendering.md
@@ -0,0 +1,723 @@
+# spar-wasm Browser AADL Rendering Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Compile spar+etch to a WASI component (wasm32-wasip2) that takes AADL source and returns interactive SVG architecture diagrams, loaded in rivet's document viewer via jco transpilation, with traceability-based highlighting.
+
+**Architecture:** A `spar-wasm` crate in the spar workspace exports a WIT `renderer` interface. It uses the full spar-hir pipeline (including salsa) since it compiles cleanly to wasm32-wasip2. The WASM component reads .aadl files via WASI filesystem, builds the instance model, converts it to a petgraph, and renders SVG via etch. On the rivet side, the document renderer detects ` ```aadl ` code blocks and emits placeholder divs. Browser JS (using jco-transpiled bindings) calls the WASM renderer and inserts the SVG. Interactive highlighting uses etch's `data-id` attributes and CSS, with link graph data provided by a rivet API endpoint.
+
+**Tech Stack:** Rust, wasm32-wasip2, wit-bindgen, jco, wasmtime, etch, petgraph, spar-hir, spar-analysis
+
+---
+
+## Two repos
+
+- **spar** (`/Volumes/Home/git/pulseengine/spar`, branch: `feat/serde-json-integration`)
+- **rivet** (`/Volumes/Home/git/sdlc`, branch: `feat/aadl-integration`)
+
+---
+
+### Task 1: Extend WIT with renderer interface (rivet)
+
+**Files:**
+- Modify: `wit/adapter.wit`
+
+**Step 1: Add the renderer interface to the WIT file**
+
+After the existing `adapter` interface, add:
+
+```wit
+/// Renderer interface for producing SVG visualizations.
+///
+/// Unlike the adapter interface (which imports/exports artifacts),
+/// the renderer takes a root classifier and produces SVG output.
+/// It reads source files via WASI filesystem.
+interface renderer {
+ /// Render an AADL architecture diagram as SVG.
+ ///
+ /// `root` — classifier to instantiate (e.g., "FlightControl::Controller.Basic")
+ /// `highlight` — artifact IDs to visually emphasize in the diagram
+ /// Returns SVG string on success.
+ render: func(root: string, highlight: list) -> result;
+
+ /// Errors specific to rendering.
+ variant render-error {
+ parse-error(string),
+ no-root(string),
+ layout-error(string),
+ }
+}
+
+/// Extended world that includes both adapter and renderer capabilities.
+world spar-component {
+ export adapter;
+ export renderer;
+}
+```
+
+**Step 2: Verify the WIT is syntactically valid**
+
+Run: `wasm-tools parse wit/adapter.wit` (or just `cargo check` later — wasmtime will validate)
+
+**Step 3: Commit**
+
+```bash
+git add wit/adapter.wit
+git commit -m "feat(wit): add renderer interface for SVG visualization"
+```
+
+---
+
+### Task 2: Create spar-wasm crate scaffolding (spar)
+
+**Files:**
+- Create: `crates/spar-wasm/Cargo.toml`
+- Create: `crates/spar-wasm/src/lib.rs`
+- Modify: `Cargo.toml` (workspace members)
+
+**Step 1: Create Cargo.toml**
+
+```toml
+[package]
+name = "spar-wasm"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "WASM component for AADL parsing, analysis, and SVG rendering"
+
+[dependencies]
+spar-parser.workspace = true
+spar-syntax.workspace = true
+spar-base-db.workspace = true
+spar-hir-def.workspace = true
+spar-hir.workspace = true
+spar-analysis.workspace = true
+serde.workspace = true
+serde_json = "1"
+petgraph = "0.6"
+```
+
+Note: etch is in the rivet workspace, not spar. We'll vendor the graph-building + SVG rendering logic directly in spar-wasm since it's a small amount of code and avoids cross-workspace dependency. The SVG output uses etch's format so it's compatible with rivet's existing interactive JS.
+
+**Step 2: Create minimal lib.rs**
+
+```rust
+//! WASM component for AADL architecture visualization.
+//!
+//! Provides two capabilities as a WASI component:
+//! 1. `adapter` — import/export AADL artifacts (same as CLI JSON output)
+//! 2. `renderer` — parse AADL, instantiate, and render SVG via graph layout
+//!
+//! The component reads `.aadl` files via WASI filesystem and uses the full
+//! spar-hir pipeline (including salsa) for semantic analysis.
+
+mod graph;
+mod render;
+```
+
+**Step 3: Add to workspace**
+
+Add `"crates/spar-wasm"` to the `members` list in the root `Cargo.toml`.
+
+**Step 4: Verify it compiles**
+
+Run: `cargo check -p spar-wasm`
+
+**Step 5: Commit**
+
+```bash
+git add crates/spar-wasm/ Cargo.toml
+git commit -m "feat(spar-wasm): scaffold WASM component crate"
+```
+
+---
+
+### Task 3: Instance model to petgraph conversion (spar)
+
+**Files:**
+- Create: `crates/spar-wasm/src/graph.rs`
+- Modify: `crates/spar-wasm/src/lib.rs` (add test)
+
+This module converts a `SystemInstance` (arena-based) into a `petgraph::Graph` suitable for layout.
+
+**Step 1: Write the failing test**
+
+In `crates/spar-wasm/src/graph.rs`:
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn instance_to_graph_basic() {
+ // Build a minimal SystemInstance with 2 components and 1 connection
+ use spar_hir_def::instance::*;
+ use spar_hir_def::item_tree::{ComponentCategory, ConnectionKind, Direction, FeatureKind};
+ use spar_hir_def::name::Name;
+ use la_arena::Arena;
+
+ let mut components = Arena::new();
+ let mut features = Arena::new();
+ let mut connections = Arena::new();
+
+ let root = components.alloc(ComponentInstance {
+ name: Name::new("Root"),
+ category: ComponentCategory::System,
+ package: Name::new("Pkg"),
+ type_name: Name::new("Root"),
+ impl_name: Some(Name::new("Root.Impl")),
+ parent: None,
+ children: Vec::new(),
+ features: Vec::new(),
+ connections: Vec::new(),
+ diagnostics: Vec::new(),
+ });
+
+ let child = components.alloc(ComponentInstance {
+ name: Name::new("sub1"),
+ category: ComponentCategory::Process,
+ package: Name::new("Pkg"),
+ type_name: Name::new("Sub"),
+ impl_name: None,
+ parent: Some(root),
+ children: Vec::new(),
+ features: Vec::new(),
+ connections: Vec::new(),
+ diagnostics: Vec::new(),
+ });
+
+ // Update root's children
+ components[root].children.push(child);
+
+ let instance = SystemInstance {
+ root,
+ components,
+ features,
+ connections,
+ };
+
+ let (graph, node_map) = build_graph(&instance);
+ assert_eq!(graph.node_count(), 2);
+ assert!(node_map.contains_key(&root));
+ assert!(node_map.contains_key(&child));
+ }
+}
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `cargo test -p spar-wasm -- instance_to_graph_basic`
+Expected: FAIL (build_graph not defined)
+
+**Step 3: Implement graph building**
+
+```rust
+//! Convert a SystemInstance into a petgraph for layout.
+
+use std::collections::HashMap;
+
+use petgraph::Graph;
+use petgraph::graph::NodeIndex;
+use spar_hir_def::instance::{ComponentInstanceIdx, SystemInstance};
+use spar_hir_def::item_tree::ComponentCategory;
+
+/// Node data for the architecture graph.
+#[derive(Debug, Clone)]
+pub struct ArchNode {
+ pub id: String,
+ pub label: String,
+ pub category: ComponentCategory,
+ pub sublabel: Option,
+}
+
+/// Edge data for the architecture graph.
+#[derive(Debug, Clone)]
+pub struct ArchEdge {
+ pub label: String,
+}
+
+/// Build a petgraph from a SystemInstance.
+///
+/// Returns the graph and a map from ComponentInstanceIdx to NodeIndex.
+pub fn build_graph(
+ instance: &SystemInstance,
+) -> (Graph, HashMap) {
+ let mut graph = Graph::new();
+ let mut node_map = HashMap::new();
+
+ // Add all components as nodes (recursive)
+ add_component_nodes(instance, instance.root, &mut graph, &mut node_map);
+
+ // Add connections as edges
+ for (_ci_idx, ci) in instance.components.iter() {
+ for &conn_idx in &ci.connections {
+ let conn = instance.connection(conn_idx);
+ if let (Some(src), Some(dst)) = (conn.source_component, conn.dest_component) {
+ if let (Some(&src_node), Some(&dst_node)) = (node_map.get(&src), node_map.get(&dst)) {
+ graph.add_edge(src_node, dst_node, ArchEdge {
+ label: conn.name.as_str().to_string(),
+ });
+ }
+ }
+ }
+ }
+
+ (graph, node_map)
+}
+
+fn add_component_nodes(
+ instance: &SystemInstance,
+ idx: ComponentInstanceIdx,
+ graph: &mut Graph,
+ node_map: &mut HashMap,
+) {
+ let comp = instance.component(idx);
+ let id = format!("AADL-{}-{}", comp.package.as_str(), comp.name.as_str());
+
+ let node = ArchNode {
+ id,
+ label: comp.name.as_str().to_string(),
+ category: comp.category,
+ sublabel: Some(format!("{:?}", comp.category)),
+ };
+
+ let ni = graph.add_node(node);
+ node_map.insert(idx, ni);
+
+ for &child_idx in &comp.children {
+ add_component_nodes(instance, child_idx, graph, node_map);
+ if let Some(&child_ni) = node_map.get(&child_idx) {
+ graph.add_edge(ni, child_ni, ArchEdge {
+ label: "contains".into(),
+ });
+ }
+ }
+}
+```
+
+**Step 4: Run test to verify it passes**
+
+Run: `cargo test -p spar-wasm -- instance_to_graph_basic`
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add crates/spar-wasm/src/graph.rs
+git commit -m "feat(spar-wasm): add instance model to petgraph conversion"
+```
+
+---
+
+### Task 4: SVG render function (spar)
+
+**Files:**
+- Create: `crates/spar-wasm/src/render.rs`
+
+This is the main entry point: AADL source text to SVG string. It uses spar-hir's Database for full semantic analysis, then builds the graph and renders SVG.
+
+Since etch is in a different workspace, we inline a minimal Sugiyama layout + SVG renderer here. The SVG format matches etch's output (same CSS classes, data-id attributes) so rivet's existing interactive JS works.
+
+**Alternative (preferred if feasible):** Add etch as a git dependency in spar-wasm's Cargo.toml:
+```toml
+etch = { git = "https://github.com/pulseengine/sdlc.git", path = "etch" }
+```
+
+**Step 1: Write the failing test**
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn render_basic_aadl() {
+ let source = r#"
+package FlightControl
+public
+ system Controller
+ features
+ sensorIn: in data port;
+ end Controller;
+
+ system implementation Controller.Basic
+ subcomponents
+ nav: process NavProcess;
+ end Controller.Basic;
+
+ process NavProcess
+ end NavProcess;
+end FlightControl;
+"#;
+ let svg = render_aadl(source, "FlightControl::Controller.Basic", &[]).unwrap();
+ assert!(svg.contains("
\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+ // Capture language tag from the opening fence.
+ let lang = trimmed.trim_start_matches('`').trim();
+ code_block_lang = if lang.is_empty() {
+ None
+ } else {
+ Some(lang.to_string())
+ };
+ in_code_block = true;
+ }
+ continue;
+ }
+
+ if in_code_block {
+ code_block_lines.push(html_escape(line));
+ continue;
+ }
+
+ if trimmed.is_empty() {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+ continue;
+ }
+
+ // Headings
+ if let Some(level) = heading_level(trimmed) {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+ let text = &trimmed[level as usize + 1..];
+ let text = resolve_inline(text, &artifact_exists);
+ html.push_str(&format!("{text}\n"));
+ continue;
+ }
+
+ // Table rows (lines starting and ending with |)
+ if trimmed.starts_with('|') && trimmed.ends_with('|') {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+
+ // Skip separator rows like |---|---|
+ if is_table_separator(trimmed) {
+ continue;
+ }
+
+ let cells: Vec<&str> = trimmed
+ .trim_matches('|')
+ .split('|')
+ .map(|c| c.trim())
+ .collect();
+
+ if !in_table {
+ // First row is the header
+ html.push_str("");
+ for cell in &cells {
+ let text = resolve_inline(cell, &artifact_exists);
+ html.push_str(&format!("| {text} | "));
+ }
+ html.push_str("
\n");
+ in_table = true;
+ table_header_done = true;
+ } else if table_header_done {
+ html.push_str("");
+ for cell in &cells {
+ let text = resolve_inline(cell, &artifact_exists);
+ html.push_str(&format!("| {text} | "));
+ }
+ html.push_str("
\n");
+ }
+ continue;
+ }
+
+ // Blockquotes
+ if let Some(bq_text) = trimmed.strip_prefix("> ") {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("
\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if !in_blockquote {
+ html.push_str("");
+ in_blockquote = true;
+ }
+ let text = resolve_inline(bq_text, &artifact_exists);
+ html.push_str(&format!("{text}
"));
+ continue;
+ }
+
+ // Unordered list items
+ if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("
\n");
+ in_blockquote = false;
+ }
+ if !in_list {
+ html.push_str("\n");
+ in_list = true;
+ }
+ let text = resolve_inline(&trimmed[2..], &artifact_exists);
+ html.push_str(&format!("- {text}
\n"));
+ continue;
+ }
+
+ // Ordered list items (e.g. "1. item")
+ if let Some(rest) = ordered_list_text(trimmed) {
+ if in_paragraph {
+ html.push_str("\n");
+ in_paragraph = false;
+ }
+ if in_list {
+ html.push_str("
\n");
+ in_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+ if !in_ordered_list {
+ html.push_str("\n");
+ in_ordered_list = true;
+ }
+ let text = resolve_inline(rest, &artifact_exists);
+ html.push_str(&format!("- {text}
\n"));
+ continue;
+ }
+
+ // Regular text → paragraph
+ if in_list {
+ html.push_str("\n");
+ in_list = false;
+ }
+ if in_ordered_list {
+ html.push_str("
\n");
+ in_ordered_list = false;
+ }
+ if in_table {
+ html.push_str("\n");
+ in_table = false;
+ table_header_done = false;
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ in_blockquote = false;
+ }
+ if !in_paragraph {
+ html.push_str("");
+ in_paragraph = true;
+ } else {
+ html.push('\n');
+ }
+ html.push_str(&resolve_inline(trimmed, &artifact_exists));
+ }
+
+ if in_paragraph {
+ html.push_str("
\n");
+ }
+ if in_list {
+ html.push_str("\n");
+ }
+ if in_ordered_list {
+ html.push_str("\n");
+ }
+ if in_table {
+ html.push_str("\n");
+ }
+ if in_blockquote {
+ html.push_str("\n");
+ }
+
+ html
+}
+
+/// Check if a table row is a separator (e.g. `|---|---|`).
+fn is_table_separator(line: &str) -> bool {
+ line.trim_matches('|')
+ .split('|')
+ .all(|cell| cell.trim().chars().all(|c| c == '-' || c == ':'))
+}
+
+/// If the line is an ordered list item (e.g. `1. text`), return the text after the marker.
+fn ordered_list_text(line: &str) -> Option<&str> {
+ let digit_end = line.as_bytes().iter().position(|b| !b.is_ascii_digit())?;
+ if digit_end == 0 {
+ return None;
+ }
+ let rest = &line[digit_end..];
+ rest.strip_prefix(". ")
+}
+
+/// Resolve inline formatting: `[[ID]]` links, **bold**, *italic*, `code`, [text](url).
+fn resolve_inline(text: &str, artifact_exists: &impl Fn(&str) -> bool) -> String {
+ let mut result = String::with_capacity(text.len() * 2);
+ let mut chars = text.char_indices().peekable();
+
+ while let Some((i, ch)) = chars.next() {
+ // Inline code (backticks) — must come before bold/italic since content is literal.
+ if ch == '`' {
+ if let Some(end) = text[i + 1..].find('`') {
+ let inner = html_escape(&text[i + 1..i + 1 + end]);
+ result.push_str(&format!("{inner}"));
+ let skip_to = i + 1 + end + 1;
+ while chars.peek().is_some_and(|&(j, _)| j < skip_to) {
+ chars.next();
+ }
+ continue;
+ }
+ }
+
+ // Markdown links [text](url) — must come before [[id]] artifact refs.
+ if ch == '[' && !text[i..].starts_with("[[") {
+ if let Some(link) = parse_markdown_link(&text[i..]) {
+ let text_part = html_escape(&link.text);
+ result.push_str(&format!(
+ "{text_part}",
+ href = html_escape(&link.url),
+ ));
+ let skip_to = i + link.total_len;
+ while chars.peek().is_some_and(|&(j, _)| j < skip_to) {
+ chars.next();
+ }
+ continue;
+ }
+ }
+
+ if ch == '[' && text[i..].starts_with("[[") {
+ // Find closing ]]
+ if let Some(end) = text[i + 2..].find("]]") {
+ let id = text[i + 2..i + 2 + end].trim();
+ if artifact_exists(id) {
+ result.push_str(&format!(
+ "{id}"
+ ));
+ } else {
+ result.push_str(&format!("{id}"));
+ }
+ // Skip past ]]
+ let skip_to = i + 2 + end + 2;
+ while chars.peek().is_some_and(|&(j, _)| j < skip_to) {
+ chars.next();
+ }
+ continue;
+ }
+ }
+
+ if ch == '*' && text[i..].starts_with("**") {
+ // Bold
+ if let Some(end) = text[i + 2..].find("**") {
+ let inner = html_escape(&text[i + 2..i + 2 + end]);
+ result.push_str(&format!("{inner}"));
+ let skip_to = i + 2 + end + 2;
+ while chars.peek().is_some_and(|&(j, _)| j < skip_to) {
+ chars.next();
+ }
+ continue;
+ }
+ }
+
+ if ch == '*' {
+ // Italic
+ if let Some(end) = text[i + 1..].find('*') {
+ let inner = html_escape(&text[i + 1..i + 1 + end]);
+ result.push_str(&format!("{inner}"));
+ let skip_to = i + 1 + end + 1;
+ while chars.peek().is_some_and(|&(j, _)| j < skip_to) {
+ chars.next();
+ }
+ continue;
+ }
+ }
+
+ // Default: escape HTML
+ match ch {
+ '&' => result.push_str("&"),
+ '<' => result.push_str("<"),
+ '>' => result.push_str(">"),
+ '"' => result.push_str("""),
+ _ => result.push(ch),
+ }
+ }
+
+ result
+}
+
+fn html_escape(s: &str) -> String {
+ s.replace('&', "&")
+ .replace('<', "<")
+ .replace('>', ">")
+ .replace('"', """)
+}
+
+/// Result of parsing a `[text](url)` markdown link.
+struct MarkdownLink {
+ text: String,
+ url: String,
+ /// Total number of bytes consumed from the input (including `[`, `]`, `(`, `)`).
+ total_len: usize,
+}
+
+/// Try to parse `[text](url)` at the start of `s`.
+///
+/// Only allows `http://`, `https://`, and `#` URLs for safety (no `javascript:` etc.).
+fn parse_markdown_link(s: &str) -> Option {
+ if !s.starts_with('[') {
+ return None;
+ }
+ let close_bracket = s[1..].find(']')?;
+ let text = &s[1..1 + close_bracket];
+ let after_bracket = &s[1 + close_bracket + 1..];
+ if !after_bracket.starts_with('(') {
+ return None;
+ }
+ let close_paren = after_bracket[1..].find(')')?;
+ let url = &after_bracket[1..1 + close_paren];
+ // Safety check: only allow http, https, and fragment (#) URLs.
+ if !(url.starts_with("http://") || url.starts_with("https://") || url.starts_with('#')) {
+ return None;
+ }
+ let total_len = 1 + close_bracket + 1 + 1 + close_paren + 1; // [text](url)
+ Some(MarkdownLink {
+ text: text.to_string(),
+ url: url.to_string(),
+ total_len,
+ })
+}
+
+// ---------------------------------------------------------------------------
+// Document store
+// ---------------------------------------------------------------------------
+
+/// In-memory collection of loaded documents.
+#[derive(Debug, Default)]
+pub struct DocumentStore {
+ docs: Vec,
+}
+
+impl DocumentStore {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn insert(&mut self, doc: Document) {
+ self.docs.push(doc);
+ }
+
+ pub fn get(&self, id: &str) -> Option<&Document> {
+ self.docs.iter().find(|d| d.id == id)
+ }
+
+ pub fn iter(&self) -> impl Iterator- {
+ self.docs.iter()
+ }
+
+ pub fn len(&self) -> usize {
+ self.docs.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.docs.is_empty()
+ }
+
+ /// All artifact IDs referenced across all documents.
+ pub fn all_references(&self) -> Vec<&DocReference> {
+ self.docs.iter().flat_map(|d| &d.references).collect()
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const SAMPLE_DOC: &str = r#"---
+id: SRS-001
+type: specification
+title: System Requirements Specification
+status: draft
+glossary:
+ STPA: Systems-Theoretic Process Analysis
+ UCA: Unsafe Control Action
+---
+
+# System Requirements Specification
+
+## 1. Introduction
+
+This document specifies the system-level requirements.
+
+## 2. Functional Requirements
+
+### 2.1 Artifact Management
+
+[[REQ-001]] — Text-file-first artifact management.
+
+[[REQ-002]] — STPA artifact support.
+
+### 2.2 Traceability
+
+[[REQ-003]] — Full ASPICE V-model traceability.
+
+## 3. Glossary
+
+See frontmatter.
+"#;
+
+ #[test]
+ fn parse_frontmatter() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ assert_eq!(doc.id, "SRS-001");
+ assert_eq!(doc.doc_type, "specification");
+ assert_eq!(doc.title, "System Requirements Specification");
+ assert_eq!(doc.status.as_deref(), Some("draft"));
+ assert_eq!(doc.glossary.len(), 2);
+ assert_eq!(
+ doc.glossary.get("STPA").unwrap(),
+ "Systems-Theoretic Process Analysis"
+ );
+ }
+
+ #[test]
+ fn extract_references_from_body() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ let ids: Vec<&str> = doc
+ .references
+ .iter()
+ .map(|r| r.artifact_id.as_str())
+ .collect();
+ assert_eq!(ids, vec!["REQ-001", "REQ-002", "REQ-003"]);
+ }
+
+ #[test]
+ fn extract_sections_hierarchy() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ assert_eq!(doc.sections.len(), 6);
+ assert_eq!(doc.sections[0].level, 1);
+ assert_eq!(doc.sections[0].title, "System Requirements Specification");
+ assert_eq!(doc.sections[1].level, 2);
+ assert_eq!(doc.sections[1].title, "1. Introduction");
+ assert_eq!(doc.sections[2].level, 2);
+ assert_eq!(doc.sections[2].title, "2. Functional Requirements");
+ assert_eq!(doc.sections[3].level, 3);
+ assert_eq!(doc.sections[3].title, "2.1 Artifact Management");
+ assert_eq!(doc.sections[3].artifact_ids, vec!["REQ-001", "REQ-002"]);
+ assert_eq!(doc.sections[4].level, 3);
+ assert_eq!(doc.sections[4].title, "2.2 Traceability");
+ assert_eq!(doc.sections[4].artifact_ids, vec!["REQ-003"]);
+ }
+
+ #[test]
+ fn multiple_refs_on_one_line() {
+ let content = "---\nid: D-1\ntitle: T\n---\n[[A-1]] and [[B-2]] here\n";
+ let doc = parse_document(content, None).unwrap();
+ assert_eq!(doc.references.len(), 2);
+ assert_eq!(doc.references[0].artifact_id, "A-1");
+ assert_eq!(doc.references[1].artifact_id, "B-2");
+ }
+
+ #[test]
+ fn missing_frontmatter_is_error() {
+ let result = parse_document("# Just markdown\n\nNo frontmatter.", None);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn render_html_resolves_refs() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ let html = render_to_html(&doc, |id| id == "REQ-001" || id == "REQ-002");
+ assert!(html.contains("artifact-ref"));
+ assert!(html.contains("hx-get=\"/artifacts/REQ-001\""));
+ assert!(html.contains("class=\"artifact-ref broken\""));
+ }
+
+ #[test]
+ fn render_html_headings() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ let html = render_to_html(&doc, |_| true);
+ assert!(html.contains("
"));
+ assert!(html.contains(""));
+ assert!(html.contains(""));
+ }
+
+ #[test]
+ fn document_store() {
+ let doc = parse_document(SAMPLE_DOC, None).unwrap();
+ let mut store = DocumentStore::new();
+ store.insert(doc);
+ assert_eq!(store.len(), 1);
+ assert!(store.get("SRS-001").is_some());
+ assert_eq!(store.all_references().len(), 3);
+ }
+
+ #[test]
+ fn default_doc_type_when_omitted() {
+ let content = "---\nid: D-1\ntitle: Test\n---\nBody.\n";
+ let doc = parse_document(content, None).unwrap();
+ assert_eq!(doc.doc_type, "document");
+ }
+
+ #[test]
+ fn render_aadl_code_block_placeholder() {
+ let content = "---\nid: DOC-001\ntitle: Architecture\n---\n\n## Overview\n\n```aadl\nroot: FlightControl::Controller.Basic\n```\n\nSome text after.\n";
+ let doc = parse_document(content, None).unwrap();
+ let html = render_to_html(&doc, |_| true);
+ assert!(html.contains("aadl-diagram"));
+ assert!(html.contains("data-root=\"FlightControl::Controller.Basic\""));
+ assert!(!html.contains("root: FlightControl"));
+ }
+}
diff --git a/rivet-core/src/embedded.rs b/rivet-core/src/embedded.rs
new file mode 100644
index 0000000..0cf51a4
--- /dev/null
+++ b/rivet-core/src/embedded.rs
@@ -0,0 +1,64 @@
+//! Embedded schemas — compiled into the binary via `include_str!`.
+//!
+//! Provides fallback schema loading when no `schemas/` directory is found,
+//! and enables `rivet docs`, `rivet schema show`, etc. without filesystem.
+
+use crate::error::Error;
+use crate::schema::SchemaFile;
+
+// ── Embedded schema content ─────────────────────────────────────────────
+
+pub const SCHEMA_COMMON: &str = include_str!("../../schemas/common.yaml");
+pub const SCHEMA_DEV: &str = include_str!("../../schemas/dev.yaml");
+pub const SCHEMA_STPA: &str = include_str!("../../schemas/stpa.yaml");
+pub const SCHEMA_ASPICE: &str = include_str!("../../schemas/aspice.yaml");
+pub const SCHEMA_CYBERSECURITY: &str = include_str!("../../schemas/cybersecurity.yaml");
+pub const SCHEMA_AADL: &str = include_str!("../../schemas/aadl.yaml");
+
+/// All known built-in schema names.
+pub const SCHEMA_NAMES: &[&str] = &["common", "dev", "stpa", "aspice", "cybersecurity", "aadl"];
+
+/// Look up embedded schema content by name.
+pub fn embedded_schema(name: &str) -> Option<&'static str> {
+ match name {
+ "common" => Some(SCHEMA_COMMON),
+ "dev" => Some(SCHEMA_DEV),
+ "stpa" => Some(SCHEMA_STPA),
+ "aspice" => Some(SCHEMA_ASPICE),
+ "cybersecurity" => Some(SCHEMA_CYBERSECURITY),
+ "aadl" => Some(SCHEMA_AADL),
+ _ => None,
+ }
+}
+
+/// Parse an embedded schema by name.
+pub fn load_embedded_schema(name: &str) -> Result {
+ let content = embedded_schema(name)
+ .ok_or_else(|| Error::Schema(format!("unknown built-in schema: {name}")))?;
+ serde_yaml::from_str(content)
+ .map_err(|e| Error::Schema(format!("parsing embedded schema '{name}': {e}")))
+}
+
+/// Load and merge schemas, falling back to embedded when files are not found.
+pub fn load_schemas_with_fallback(
+ schema_names: &[String],
+ schemas_dir: &std::path::Path,
+) -> Result {
+ let mut files = Vec::new();
+
+ for name in schema_names {
+ let path = schemas_dir.join(format!("{name}.yaml"));
+ if path.exists() {
+ let file = crate::schema::Schema::load_file(&path)?;
+ files.push(file);
+ } else if let Some(content) = embedded_schema(name) {
+ let file: SchemaFile = serde_yaml::from_str(content)
+ .map_err(|e| Error::Schema(format!("embedded '{name}': {e}")))?;
+ files.push(file);
+ } else {
+ log::warn!("schema '{name}' not found on disk or embedded");
+ }
+ }
+
+ Ok(crate::schema::Schema::merge(&files))
+}
diff --git a/rivet-core/src/formats/aadl.rs b/rivet-core/src/formats/aadl.rs
new file mode 100644
index 0000000..5f089d3
--- /dev/null
+++ b/rivet-core/src/formats/aadl.rs
@@ -0,0 +1,456 @@
+//! AADL adapter — uses spar crates to parse `.aadl` files directly.
+//!
+//! Integration via `spar-hir` (parsing + HIR) and `spar-analysis`
+//! (connectivity, scheduling, latency, etc.). No CLI invocation needed.
+//!
+//! Import modes:
+//! - `Bytes` — parse JSON (legacy/test compatibility)
+//! - `Path` — single `.aadl` file or JSON file
+//! - `Directory` — find `.aadl` files, parse with spar-hir, run analyses
+
+use std::collections::BTreeMap;
+use std::path::Path;
+
+use serde::Deserialize;
+
+use crate::adapter::{Adapter, AdapterConfig, AdapterSource};
+use crate::error::Error;
+use crate::model::Artifact;
+
+// ── Public adapter ───────────────────────────────────────────────────────
+
+pub struct AadlAdapter {
+ supported: Vec,
+}
+
+impl AadlAdapter {
+ pub fn new() -> Self {
+ Self {
+ supported: vec![
+ "aadl-component".into(),
+ "aadl-analysis-result".into(),
+ "aadl-flow".into(),
+ ],
+ }
+ }
+}
+
+impl Default for AadlAdapter {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Adapter for AadlAdapter {
+ fn id(&self) -> &str {
+ "aadl"
+ }
+
+ fn name(&self) -> &str {
+ "AADL (spar)"
+ }
+
+ fn supported_types(&self) -> &[String] {
+ &self.supported
+ }
+
+ fn import(
+ &self,
+ source: &AdapterSource,
+ config: &AdapterConfig,
+ ) -> Result, Error> {
+ match source {
+ AdapterSource::Bytes(bytes) => {
+ let content = std::str::from_utf8(bytes)
+ .map_err(|e| Error::Adapter(format!("invalid UTF-8: {}", e)))?;
+ // Try JSON first (legacy), then AADL source.
+ if content.trim_start().starts_with('{') {
+ parse_spar_json(content)
+ } else {
+ import_aadl_sources(&[("input.aadl".into(), content.to_string())], config)
+ }
+ }
+ AdapterSource::Path(path) => import_single_file(path, config),
+ AdapterSource::Directory(dir) => import_aadl_directory(dir, config),
+ }
+ }
+
+ fn export(&self, _artifacts: &[Artifact], _config: &AdapterConfig) -> Result, Error> {
+ Err(Error::Adapter("AADL export is not supported".into()))
+ }
+}
+
+// ── Direct spar-hir integration ─────────────────────────────────────────
+
+#[cfg(feature = "aadl")]
+fn import_aadl_sources(
+ sources: &[(String, String)],
+ config: &AdapterConfig,
+) -> Result, Error> {
+ use spar_hir::Database;
+
+ let db = Database::from_aadl(sources);
+ let packages = db.packages();
+
+ let mut artifacts = Vec::new();
+
+ // Convert component types and implementations from HIR.
+ for pkg in &packages {
+ for ct in &pkg.component_types {
+ let category = ct.category.to_string();
+ // Map spaces to dashes for schema compatibility (e.g. "thread group" → "thread-group")
+ let category_id = category.replace(' ', "-");
+ artifacts.push(component_to_artifact(
+ &pkg.name,
+ &ct.name,
+ &category_id,
+ "type",
+ ));
+ }
+ for ci in &pkg.component_impls {
+ let category = ci.category.to_string();
+ let category_id = category.replace(' ', "-");
+ artifacts.push(component_to_artifact(
+ &pkg.name,
+ &ci.name,
+ &category_id,
+ "implementation",
+ ));
+ }
+ }
+
+ // Run tree-level analyses (category rules, naming) on all files.
+ let tree_diags = run_tree_analyses(&db);
+ let mut diag_index = 0;
+ for diag in &tree_diags {
+ artifacts.push(analysis_diagnostic_to_artifact(diag_index, diag));
+ diag_index += 1;
+ }
+
+ // Run instance-level analyses if a root classifier is configured.
+ let root_classifier = config.get("root-classifier");
+ if let Some(root_name) = root_classifier {
+ if let Some(instance) = db.instantiate(root_name) {
+ let instance_diags = run_instance_analyses(&instance);
+ for diag in &instance_diags {
+ artifacts.push(analysis_diagnostic_to_artifact(diag_index, diag));
+ diag_index += 1;
+ }
+ }
+ }
+
+ Ok(artifacts)
+}
+
+#[cfg(feature = "aadl")]
+fn run_instance_analyses(instance: &spar_hir::Instance) -> Vec {
+ use spar_analysis::AnalysisRunner;
+
+ let mut runner = AnalysisRunner::new();
+ // Instance-level analyses (operate on SystemInstance).
+ runner.register(Box::new(spar_analysis::connectivity::ConnectivityAnalysis));
+ runner.register(Box::new(spar_analysis::hierarchy::HierarchyAnalysis));
+ runner.register(Box::new(spar_analysis::completeness::CompletenessAnalysis));
+ runner.register(Box::new(
+ spar_analysis::direction_rules::DirectionRuleAnalysis,
+ ));
+ runner.register(Box::new(spar_analysis::flow_check::FlowCheckAnalysis));
+ runner.register(Box::new(spar_analysis::mode_check::ModeCheckAnalysis));
+ runner.register(Box::new(spar_analysis::binding_check::BindingCheckAnalysis));
+ runner.register(Box::new(spar_analysis::latency::LatencyAnalysis));
+ runner.register(Box::new(spar_analysis::scheduling::SchedulingAnalysis));
+ runner.register(Box::new(
+ spar_analysis::resource_budget::ResourceBudgetAnalysis,
+ ));
+
+ runner.run_all(instance.inner())
+}
+
+/// Run tree-level checks (category rules, naming, legality) on all item trees.
+#[cfg(feature = "aadl")]
+fn run_tree_analyses(db: &spar_hir::Database) -> Vec {
+ let mut diags = Vec::new();
+ for tree in db.item_trees() {
+ diags.extend(spar_analysis::category_check::check_category_rules(tree));
+ diags.extend(spar_analysis::naming_rules::check_naming_rules(tree));
+ }
+ diags
+}
+
+#[cfg(feature = "aadl")]
+fn analysis_diagnostic_to_artifact(
+ index: usize,
+ diag: &spar_analysis::AnalysisDiagnostic,
+) -> Artifact {
+ let id = format!("AADL-DIAG-{:04}", index + 1);
+ let severity = match diag.severity {
+ spar_analysis::Severity::Error => "error",
+ spar_analysis::Severity::Warning => "warning",
+ spar_analysis::Severity::Info => "info",
+ };
+
+ let mut fields = BTreeMap::new();
+ fields.insert(
+ "analysis-name".into(),
+ serde_yaml::Value::String(diag.analysis.clone()),
+ );
+ fields.insert(
+ "severity".into(),
+ serde_yaml::Value::String(severity.into()),
+ );
+ fields.insert(
+ "component-path".into(),
+ serde_yaml::Value::String(diag.path.join(".")),
+ );
+ fields.insert(
+ "details".into(),
+ serde_yaml::Value::String(diag.message.clone()),
+ );
+
+ Artifact {
+ id,
+ artifact_type: "aadl-analysis-result".into(),
+ title: format!("[{}] {}", diag.analysis, diag.message),
+ description: Some(diag.message.clone()),
+ status: None,
+ tags: vec!["aadl".into(), diag.analysis.clone()],
+ links: vec![],
+ fields,
+ source_file: None,
+ }
+}
+
+// Fallback when the aadl feature is disabled.
+#[cfg(not(feature = "aadl"))]
+fn import_aadl_sources(
+ _sources: &[(String, String)],
+ _config: &AdapterConfig,
+) -> Result, Error> {
+ Err(Error::Adapter(
+ "AADL support requires the 'aadl' feature (spar crates)".into(),
+ ))
+}
+
+// ── Legacy JSON parsing (test compatibility) ────────────────────────────
+
+#[derive(Debug, Deserialize)]
+struct SparOutput {
+ #[allow(dead_code)]
+ root: String,
+ #[serde(default)]
+ packages: Vec,
+ #[allow(dead_code)]
+ #[serde(default)]
+ instance: Option,
+ #[serde(default)]
+ diagnostics: Vec,
+}
+
+#[derive(Debug, Deserialize)]
+struct SparPackage {
+ name: String,
+ #[serde(default)]
+ component_types: Vec,
+ #[serde(default)]
+ component_impls: Vec,
+}
+
+#[derive(Debug, Deserialize)]
+struct SparComponentType {
+ name: String,
+ category: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct SparComponentImpl {
+ name: String,
+ category: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct SparDiagnostic {
+ severity: String,
+ message: String,
+ #[serde(default)]
+ path: Vec,
+ #[serde(default)]
+ analysis: String,
+}
+
+fn parse_spar_json(content: &str) -> Result, Error> {
+ let output: SparOutput = serde_json::from_str(content)
+ .map_err(|e| Error::Adapter(format!("failed to parse spar JSON: {}", e)))?;
+
+ let mut artifacts = Vec::new();
+
+ for pkg in &output.packages {
+ for ct in &pkg.component_types {
+ artifacts.push(component_to_artifact(
+ &pkg.name,
+ &ct.name,
+ &ct.category,
+ "type",
+ ));
+ }
+ for ci in &pkg.component_impls {
+ artifacts.push(component_to_artifact(
+ &pkg.name,
+ &ci.name,
+ &ci.category,
+ "implementation",
+ ));
+ }
+ }
+
+ for (index, diag) in output.diagnostics.iter().enumerate() {
+ artifacts.push(diagnostic_to_artifact(index, diag));
+ }
+
+ Ok(artifacts)
+}
+
+// ── Shared artifact builders ────────────────────────────────────────────
+
+fn component_to_artifact(
+ pkg_name: &str,
+ comp_name: &str,
+ category: &str,
+ classifier_kind: &str,
+) -> Artifact {
+ let id = format!("AADL-{}-{}", pkg_name, comp_name);
+
+ let mut fields = BTreeMap::new();
+ fields.insert(
+ "category".into(),
+ serde_yaml::Value::String(category.into()),
+ );
+ fields.insert(
+ "aadl-package".into(),
+ serde_yaml::Value::String(pkg_name.into()),
+ );
+ fields.insert(
+ "classifier-kind".into(),
+ serde_yaml::Value::String(classifier_kind.into()),
+ );
+
+ Artifact {
+ id,
+ artifact_type: "aadl-component".into(),
+ title: format!("{} {} ({})", category, comp_name, classifier_kind),
+ description: None,
+ status: Some("imported".into()),
+ tags: vec!["aadl".into()],
+ links: vec![],
+ fields,
+ source_file: None,
+ }
+}
+
+fn diagnostic_to_artifact(index: usize, diag: &SparDiagnostic) -> Artifact {
+ let id = format!("AADL-DIAG-{:04}", index + 1);
+
+ let mut fields = BTreeMap::new();
+ fields.insert(
+ "analysis-name".into(),
+ serde_yaml::Value::String(diag.analysis.clone()),
+ );
+ fields.insert(
+ "severity".into(),
+ serde_yaml::Value::String(diag.severity.clone()),
+ );
+ fields.insert(
+ "component-path".into(),
+ serde_yaml::Value::String(diag.path.join(".")),
+ );
+ fields.insert(
+ "details".into(),
+ serde_yaml::Value::String(diag.message.clone()),
+ );
+
+ Artifact {
+ id,
+ artifact_type: "aadl-analysis-result".into(),
+ title: format!("[{}] {}", diag.analysis, diag.message),
+ description: Some(diag.message.clone()),
+ status: None,
+ tags: vec!["aadl".into(), diag.analysis.clone()],
+ links: vec![],
+ fields,
+ source_file: None,
+ }
+}
+
+// ── File / directory import ─────────────────────────────────────────────
+
+fn import_single_file(path: &Path, config: &AdapterConfig) -> Result, Error> {
+ let content = std::fs::read_to_string(path)
+ .map_err(|e| Error::Io(format!("{}: {}", path.display(), e)))?;
+
+ let is_json =
+ path.extension().is_some_and(|ext| ext == "json") || content.trim_start().starts_with('{');
+
+ let mut artifacts = if is_json {
+ parse_spar_json(&content)?
+ } else {
+ let name = path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
+ import_aadl_sources(&[(name, content)], config)?
+ };
+
+ for a in &mut artifacts {
+ a.source_file = Some(path.to_path_buf());
+ }
+ Ok(artifacts)
+}
+
+fn import_aadl_directory(dir: &Path, config: &AdapterConfig) -> Result, Error> {
+ let aadl_files = collect_aadl_files(dir)?;
+ if aadl_files.is_empty() {
+ return Ok(Vec::new());
+ }
+
+ // Read all .aadl files into (name, content) pairs for spar-hir.
+ let mut sources = Vec::new();
+ for path in &aadl_files {
+ let content = std::fs::read_to_string(path)
+ .map_err(|e| Error::Io(format!("{}: {}", path.display(), e)))?;
+ let name = path
+ .file_name()
+ .unwrap_or_default()
+ .to_string_lossy()
+ .into_owned();
+ sources.push((name, content));
+ }
+
+ let mut artifacts = import_aadl_sources(&sources, config)?;
+
+ // Tag artifacts with source file info.
+ for a in &mut artifacts {
+ if a.source_file.is_none() {
+ a.source_file = Some(dir.to_path_buf());
+ }
+ }
+
+ Ok(artifacts)
+}
+
+fn collect_aadl_files(dir: &Path) -> Result, Error> {
+ let mut files = Vec::new();
+ let entries =
+ std::fs::read_dir(dir).map_err(|e| Error::Io(format!("{}: {}", dir.display(), e)))?;
+
+ for entry in entries {
+ let entry = entry.map_err(|e| Error::Io(e.to_string()))?;
+ let path = entry.path();
+ if path.extension().is_some_and(|ext| ext == "aadl") {
+ files.push(path);
+ } else if path.is_dir() {
+ files.extend(collect_aadl_files(&path)?);
+ }
+ }
+
+ Ok(files)
+}
diff --git a/rivet-core/src/formats/mod.rs b/rivet-core/src/formats/mod.rs
index d577091..fe46f40 100644
--- a/rivet-core/src/formats/mod.rs
+++ b/rivet-core/src/formats/mod.rs
@@ -1,2 +1,8 @@
+pub mod aadl;
pub mod generic;
pub mod stpa;
+
+// Note: The aadl module is always compiled. When the "aadl" feature is
+// enabled (default), it uses spar-hir/spar-analysis for direct parsing.
+// Without the feature, directory/file import of .aadl files returns an error
+// but JSON import still works for test compatibility.
diff --git a/rivet-core/src/lib.rs b/rivet-core/src/lib.rs
index 450f4d7..5f5d851 100644
--- a/rivet-core/src/lib.rs
+++ b/rivet-core/src/lib.rs
@@ -1,5 +1,8 @@
pub mod adapter;
+pub mod coverage;
pub mod diff;
+pub mod document;
+pub mod embedded;
pub mod error;
pub mod formats;
pub mod links;
@@ -9,6 +12,7 @@ pub mod model;
pub mod oslc;
pub mod query;
pub mod reqif;
+pub mod results;
pub mod schema;
pub mod store;
pub mod validate;
@@ -31,20 +35,10 @@ pub fn load_project_config(path: &Path) -> Result {
}
/// Load schemas from the built-in schemas directory or from file paths.
+///
+/// Falls back to embedded (compiled-in) schemas when files are not on disk.
pub fn load_schemas(schema_names: &[String], schemas_dir: &Path) -> Result {
- let mut files = Vec::new();
-
- for name in schema_names {
- let path = schemas_dir.join(format!("{}.yaml", name));
- if path.exists() {
- let file = schema::Schema::load_file(&path)?;
- files.push(file);
- } else {
- log::warn!("schema file not found: {}", path.display());
- }
- }
-
- Ok(schema::Schema::merge(&files))
+ embedded::load_schemas_with_fallback(schema_names, schemas_dir)
}
/// Load artifacts from a source using the appropriate adapter.
@@ -77,6 +71,10 @@ pub fn load_artifacts(
let adapter = reqif::ReqIfAdapter::new();
adapter::Adapter::import(&adapter, &source_input, &adapter_config)
}
+ "aadl" => {
+ let adapter = formats::aadl::AadlAdapter::new();
+ adapter::Adapter::import(&adapter, &source_input, &adapter_config)
+ }
other => Err(Error::Adapter(format!("unknown format: {}", other))),
}
}
diff --git a/rivet-core/src/model.rs b/rivet-core/src/model.rs
index 7d86530..2308f23 100644
--- a/rivet-core/src/model.rs
+++ b/rivet-core/src/model.rs
@@ -76,12 +76,18 @@ impl Artifact {
}
}
-/// Project configuration loaded from `trace.yaml`.
+/// Project configuration loaded from `rivet.yaml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
pub project: ProjectMetadata,
#[serde(default)]
pub sources: Vec,
+ /// Directories containing markdown documents (with YAML frontmatter).
+ #[serde(default)]
+ pub docs: Vec,
+ /// Directory containing test result YAML files.
+ #[serde(default)]
+ pub results: Option,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/rivet-core/src/results.rs b/rivet-core/src/results.rs
new file mode 100644
index 0000000..f6e37bf
--- /dev/null
+++ b/rivet-core/src/results.rs
@@ -0,0 +1,475 @@
+//! Test run results model and loader.
+//!
+//! Results are stored as YAML files, each representing a single test run
+//! with per-artifact pass/fail/skip results.
+
+use std::path::{Path, PathBuf};
+
+use serde::{Deserialize, Serialize};
+
+/// Outcome of a single test.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum TestStatus {
+ Pass,
+ Fail,
+ Skip,
+ Error,
+ Blocked,
+}
+
+impl TestStatus {
+ pub fn is_pass(&self) -> bool {
+ matches!(self, Self::Pass)
+ }
+ pub fn is_fail(&self) -> bool {
+ matches!(self, Self::Fail | Self::Error)
+ }
+}
+
+impl std::fmt::Display for TestStatus {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Pass => write!(f, "pass"),
+ Self::Fail => write!(f, "fail"),
+ Self::Skip => write!(f, "skip"),
+ Self::Error => write!(f, "error"),
+ Self::Blocked => write!(f, "blocked"),
+ }
+ }
+}
+
+/// A single test result for one artifact in a run.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TestResult {
+ /// The artifact ID this result is for (e.g., "UVER-1").
+ pub artifact: String,
+ pub status: TestStatus,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub duration: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub message: Option,
+}
+
+/// Metadata for a test run.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RunMetadata {
+ pub id: String,
+ pub timestamp: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub source: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub environment: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub commit: Option,
+}
+
+/// YAML file structure for a test run.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TestRunFile {
+ pub run: RunMetadata,
+ pub results: Vec,
+}
+
+/// A loaded test run.
+#[derive(Debug, Clone)]
+pub struct TestRun {
+ pub run: RunMetadata,
+ pub results: Vec,
+ pub source_file: Option,
+}
+
+/// Aggregate statistics for a result set.
+#[derive(Debug, Clone, Default)]
+pub struct ResultSummary {
+ pub total_runs: usize,
+ pub total_results: usize,
+ pub pass_count: usize,
+ pub fail_count: usize,
+ pub skip_count: usize,
+ pub error_count: usize,
+ pub blocked_count: usize,
+}
+
+impl ResultSummary {
+ pub fn pass_rate(&self) -> f64 {
+ if self.total_results == 0 {
+ return 0.0;
+ }
+ (self.pass_count as f64 / self.total_results as f64) * 100.0
+ }
+}
+
+/// In-memory collection of test runs.
+#[derive(Debug, Default)]
+pub struct ResultStore {
+ runs: Vec,
+}
+
+impl ResultStore {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn insert(&mut self, run: TestRun) {
+ self.runs.push(run);
+ // Keep sorted by timestamp descending (newest first)
+ self.runs
+ .sort_by(|a, b| b.run.timestamp.cmp(&a.run.timestamp));
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.runs.is_empty()
+ }
+
+ pub fn len(&self) -> usize {
+ self.runs.len()
+ }
+
+ /// All runs, sorted newest first.
+ pub fn runs(&self) -> &[TestRun] {
+ &self.runs
+ }
+
+ /// Get a specific run by ID.
+ pub fn get_run(&self, run_id: &str) -> Option<&TestRun> {
+ self.runs.iter().find(|r| r.run.id == run_id)
+ }
+
+ /// Latest result for a given artifact ID across all runs.
+ /// Returns the run metadata and the test result.
+ pub fn latest_for(&self, artifact_id: &str) -> Option<(&RunMetadata, &TestResult)> {
+ // runs are sorted newest first, so first match is latest
+ for run in &self.runs {
+ if let Some(result) = run.results.iter().find(|r| r.artifact == artifact_id) {
+ return Some((&run.run, result));
+ }
+ }
+ None
+ }
+
+ /// All results for a specific artifact across all runs (newest first).
+ pub fn history_for(&self, artifact_id: &str) -> Vec<(&RunMetadata, &TestResult)> {
+ self.runs
+ .iter()
+ .filter_map(|run| {
+ run.results
+ .iter()
+ .find(|r| r.artifact == artifact_id)
+ .map(|result| (&run.run, result))
+ })
+ .collect()
+ }
+
+ /// Aggregate summary across all runs.
+ pub fn summary(&self) -> ResultSummary {
+ let mut s = ResultSummary {
+ total_runs: self.runs.len(),
+ ..Default::default()
+ };
+ // Count from the latest run only for overall stats
+ if let Some(latest) = self.runs.first() {
+ for r in &latest.results {
+ s.total_results += 1;
+ match r.status {
+ TestStatus::Pass => s.pass_count += 1,
+ TestStatus::Fail => s.fail_count += 1,
+ TestStatus::Skip => s.skip_count += 1,
+ TestStatus::Error => s.error_count += 1,
+ TestStatus::Blocked => s.blocked_count += 1,
+ }
+ }
+ }
+ s
+ }
+}
+
+/// Load all test run YAML files from a directory.
+pub fn load_results(dir: &Path) -> anyhow::Result> {
+ let mut runs = Vec::new();
+
+ if !dir.exists() {
+ return Ok(runs);
+ }
+
+ let mut entries: Vec<_> = std::fs::read_dir(dir)?
+ .filter_map(|e| e.ok())
+ .filter(|e| {
+ let p = e.path();
+ matches!(p.extension().and_then(|x| x.to_str()), Some("yaml" | "yml"))
+ })
+ .collect();
+ entries.sort_by_key(|e| e.path());
+
+ for entry in entries {
+ let path = entry.path();
+ let content = std::fs::read_to_string(&path)?;
+ let file: TestRunFile = serde_yaml::from_str(&content)
+ .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
+ runs.push(TestRun {
+ run: file.run,
+ results: file.results,
+ source_file: Some(path),
+ });
+ }
+
+ Ok(runs)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn make_run(id: &str, timestamp: &str, results: Vec) -> TestRun {
+ TestRun {
+ run: RunMetadata {
+ id: id.to_string(),
+ timestamp: timestamp.to_string(),
+ source: None,
+ environment: None,
+ commit: None,
+ },
+ results,
+ source_file: None,
+ }
+ }
+
+ fn make_result(artifact: &str, status: TestStatus) -> TestResult {
+ TestResult {
+ artifact: artifact.to_string(),
+ status,
+ duration: None,
+ message: None,
+ }
+ }
+
+ #[test]
+ fn test_status_display() {
+ assert_eq!(TestStatus::Pass.to_string(), "pass");
+ assert_eq!(TestStatus::Fail.to_string(), "fail");
+ assert_eq!(TestStatus::Skip.to_string(), "skip");
+ assert_eq!(TestStatus::Error.to_string(), "error");
+ assert_eq!(TestStatus::Blocked.to_string(), "blocked");
+ }
+
+ #[test]
+ fn test_status_is_pass_fail() {
+ assert!(TestStatus::Pass.is_pass());
+ assert!(!TestStatus::Fail.is_pass());
+ assert!(!TestStatus::Skip.is_pass());
+ assert!(!TestStatus::Error.is_pass());
+ assert!(!TestStatus::Blocked.is_pass());
+
+ assert!(TestStatus::Fail.is_fail());
+ assert!(TestStatus::Error.is_fail());
+ assert!(!TestStatus::Pass.is_fail());
+ assert!(!TestStatus::Skip.is_fail());
+ assert!(!TestStatus::Blocked.is_fail());
+ }
+
+ #[test]
+ fn test_result_store_insert_and_sort() {
+ let mut store = ResultStore::new();
+ assert!(store.is_empty());
+
+ let run_old = make_run(
+ "run-1",
+ "2026-03-01T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Pass)],
+ );
+ let run_new = make_run(
+ "run-2",
+ "2026-03-05T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Fail)],
+ );
+
+ // Insert older first, then newer
+ store.insert(run_old);
+ store.insert(run_new);
+
+ assert_eq!(store.len(), 2);
+ // Newest first
+ assert_eq!(store.runs()[0].run.id, "run-2");
+ assert_eq!(store.runs()[1].run.id, "run-1");
+ }
+
+ #[test]
+ fn test_latest_for() {
+ let mut store = ResultStore::new();
+
+ store.insert(make_run(
+ "run-1",
+ "2026-03-01T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Fail)],
+ ));
+ store.insert(make_run(
+ "run-2",
+ "2026-03-05T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Pass)],
+ ));
+
+ let (meta, result) = store.latest_for("A-1").unwrap();
+ assert_eq!(meta.id, "run-2");
+ assert_eq!(result.status, TestStatus::Pass);
+
+ assert!(store.latest_for("NONEXISTENT").is_none());
+ }
+
+ #[test]
+ fn test_history_for() {
+ let mut store = ResultStore::new();
+
+ store.insert(make_run(
+ "run-1",
+ "2026-03-01T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Fail)],
+ ));
+ store.insert(make_run(
+ "run-2",
+ "2026-03-05T00:00:00Z",
+ vec![make_result("A-1", TestStatus::Pass)],
+ ));
+ store.insert(make_run(
+ "run-3",
+ "2026-03-03T00:00:00Z",
+ vec![make_result("B-1", TestStatus::Skip)],
+ ));
+
+ let history = store.history_for("A-1");
+ assert_eq!(history.len(), 2);
+ // Newest first
+ assert_eq!(history[0].0.id, "run-2");
+ assert_eq!(history[0].1.status, TestStatus::Pass);
+ assert_eq!(history[1].0.id, "run-1");
+ assert_eq!(history[1].1.status, TestStatus::Fail);
+
+ // B-1 only appears in run-3
+ let history_b = store.history_for("B-1");
+ assert_eq!(history_b.len(), 1);
+ assert_eq!(history_b[0].0.id, "run-3");
+ }
+
+ #[test]
+ fn test_summary() {
+ let mut store = ResultStore::new();
+
+ store.insert(make_run(
+ "run-1",
+ "2026-03-01T00:00:00Z",
+ vec![
+ make_result("A-1", TestStatus::Pass),
+ make_result("A-2", TestStatus::Fail),
+ ],
+ ));
+ store.insert(make_run(
+ "run-2",
+ "2026-03-05T00:00:00Z",
+ vec![
+ make_result("A-1", TestStatus::Pass),
+ make_result("A-2", TestStatus::Pass),
+ make_result("A-3", TestStatus::Skip),
+ make_result("A-4", TestStatus::Error),
+ make_result("A-5", TestStatus::Blocked),
+ ],
+ ));
+
+ let summary = store.summary();
+ assert_eq!(summary.total_runs, 2);
+ // Stats come from the latest run only (run-2)
+ assert_eq!(summary.total_results, 5);
+ assert_eq!(summary.pass_count, 2);
+ assert_eq!(summary.fail_count, 0);
+ assert_eq!(summary.skip_count, 1);
+ assert_eq!(summary.error_count, 1);
+ assert_eq!(summary.blocked_count, 1);
+ // pass_rate = 2/5 = 40%
+ assert!((summary.pass_rate() - 40.0).abs() < f64::EPSILON);
+ }
+
+ #[test]
+ fn test_load_results_empty_dir() {
+ let dir = std::env::temp_dir().join("rivet_test_empty_results");
+ let _ = std::fs::create_dir_all(&dir);
+ // Remove any leftover yaml files
+ if let Ok(entries) = std::fs::read_dir(&dir) {
+ for entry in entries.flatten() {
+ let _ = std::fs::remove_file(entry.path());
+ }
+ }
+
+ let runs = load_results(&dir).unwrap();
+ assert!(runs.is_empty());
+
+ let _ = std::fs::remove_dir(&dir);
+ }
+
+ #[test]
+ fn test_load_results_nonexistent_dir() {
+ let dir = std::env::temp_dir().join("rivet_test_nonexistent_results_dir");
+ let _ = std::fs::remove_dir_all(&dir); // ensure it doesn't exist
+ let runs = load_results(&dir).unwrap();
+ assert!(runs.is_empty());
+ }
+
+ #[test]
+ fn test_roundtrip_yaml() {
+ let run_file = TestRunFile {
+ run: RunMetadata {
+ id: "run-roundtrip".to_string(),
+ timestamp: "2026-03-08T12:00:00Z".to_string(),
+ source: Some("CI".to_string()),
+ environment: Some("HIL bench".to_string()),
+ commit: Some("abc123".to_string()),
+ },
+ results: vec![
+ TestResult {
+ artifact: "UVER-1".to_string(),
+ status: TestStatus::Pass,
+ duration: Some("1.5s".to_string()),
+ message: None,
+ },
+ TestResult {
+ artifact: "UVER-2".to_string(),
+ status: TestStatus::Fail,
+ duration: None,
+ message: Some("Threshold exceeded".to_string()),
+ },
+ TestResult {
+ artifact: "UVER-3".to_string(),
+ status: TestStatus::Skip,
+ duration: None,
+ message: None,
+ },
+ TestResult {
+ artifact: "UVER-4".to_string(),
+ status: TestStatus::Error,
+ duration: None,
+ message: Some("Runtime panic".to_string()),
+ },
+ TestResult {
+ artifact: "UVER-5".to_string(),
+ status: TestStatus::Blocked,
+ duration: None,
+ message: Some("Dependency unavailable".to_string()),
+ },
+ ],
+ };
+
+ let yaml = serde_yaml::to_string(&run_file).unwrap();
+ let deserialized: TestRunFile = serde_yaml::from_str(&yaml).unwrap();
+
+ assert_eq!(deserialized.run.id, run_file.run.id);
+ assert_eq!(deserialized.run.timestamp, run_file.run.timestamp);
+ assert_eq!(deserialized.run.source, run_file.run.source);
+ assert_eq!(deserialized.run.environment, run_file.run.environment);
+ assert_eq!(deserialized.run.commit, run_file.run.commit);
+ assert_eq!(deserialized.results.len(), run_file.results.len());
+
+ for (orig, deser) in run_file.results.iter().zip(deserialized.results.iter()) {
+ assert_eq!(orig.artifact, deser.artifact);
+ assert_eq!(orig.status, deser.status);
+ assert_eq!(orig.duration, deser.duration);
+ assert_eq!(orig.message, deser.message);
+ }
+ }
+}
diff --git a/rivet-core/src/store.rs b/rivet-core/src/store.rs
index 67d3a8d..4f2f03c 100644
--- a/rivet-core/src/store.rs
+++ b/rivet-core/src/store.rs
@@ -8,7 +8,7 @@ use crate::model::{Artifact, ArtifactId};
/// Holds all loaded artifacts and provides lookup by ID and by type.
/// The store is the central data structure consumed by the link graph,
/// validator, query engine, and matrix generator.
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone)]
pub struct Store {
artifacts: HashMap,
by_type: HashMap>,
diff --git a/rivet-core/src/validate.rs b/rivet-core/src/validate.rs
index e64d882..57f9c2e 100644
--- a/rivet-core/src/validate.rs
+++ b/rivet-core/src/validate.rs
@@ -1,3 +1,4 @@
+use crate::document::DocumentStore;
use crate::links::LinkGraph;
use crate::schema::{Cardinality, Schema, Severity};
use crate::store::Store;
@@ -224,3 +225,28 @@ pub fn validate(store: &Store, schema: &Schema, graph: &LinkGraph) -> Vec Vec {
+ let mut diagnostics = Vec::new();
+
+ for doc in doc_store.iter() {
+ for reference in &doc.references {
+ if !store.contains(&reference.artifact_id) {
+ diagnostics.push(Diagnostic {
+ severity: Severity::Warning,
+ artifact_id: Some(doc.id.clone()),
+ rule: "doc-broken-ref".into(),
+ message: format!(
+ "document references [[{}]] (line {}) which does not exist",
+ reference.artifact_id, reference.line
+ ),
+ });
+ }
+ }
+ }
+
+ diagnostics
+}
diff --git a/rivet-core/src/wasm_runtime.rs b/rivet-core/src/wasm_runtime.rs
index 6ea69ea..f787ffa 100644
--- a/rivet-core/src/wasm_runtime.rs
+++ b/rivet-core/src/wasm_runtime.rs
@@ -32,6 +32,20 @@ use crate::adapter::{Adapter, AdapterConfig, AdapterSource};
use crate::error::Error;
use crate::model::Artifact;
+// ---------------------------------------------------------------------------
+// Generated WIT bindings (component-model typed interface)
+// ---------------------------------------------------------------------------
+
+/// Type-safe bindings generated from `wit/adapter.wit` for the
+/// `spar-component` world. This gives us typed access to the
+/// exported `adapter` and `renderer` interfaces.
+mod wit_bindings {
+ wasmtime::component::bindgen!({
+ path: "../wit/adapter.wit",
+ world: "spar-component",
+ });
+}
+
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
@@ -360,6 +374,68 @@ impl WasmAdapter {
))
}
+ /// Call the guest `render` function from the renderer interface.
+ ///
+ /// This creates a fresh WASI-enabled store (optionally pre-opening
+ /// `aadl_dir` so the guest can read `.aadl` files), instantiates the
+ /// component using the generated WIT bindings, and calls the
+ /// `pulseengine:rivet/renderer.render` export.
+ pub fn call_render(
+ &self,
+ root: &str,
+ highlight: &[String],
+ aadl_dir: Option<&Path>,
+ ) -> Result {
+ // -- Build WASI context ------------------------------------------------
+ let mut wasi_builder = wasmtime_wasi::WasiCtxBuilder::new();
+ wasi_builder.inherit_stderr();
+
+ // Pre-open the AADL directory so the guest can read .aadl files.
+ if let Some(dir) = aadl_dir {
+ wasi_builder
+ .preopened_dir(
+ dir,
+ ".",
+ wasmtime_wasi::DirPerms::READ,
+ wasmtime_wasi::FilePerms::READ,
+ )
+ .map_err(|e| WasmError::Instantiation(format!("preopened dir: {}", e)))?;
+ }
+
+ let state = HostState {
+ wasi: wasi_builder.build(),
+ table: wasmtime::component::ResourceTable::new(),
+ limiter: self
+ .runtime_config
+ .max_memory_bytes
+ .map(|max| MemoryLimiter { max_memory: max }),
+ };
+
+ let mut store = Store::new(&self.engine, state);
+
+ if let Some(fuel) = self.runtime_config.fuel {
+ store
+ .set_fuel(fuel)
+ .map_err(|e| WasmError::Instantiation(e.to_string()))?;
+ }
+ if self.runtime_config.max_memory_bytes.is_some() {
+ store.limiter(|state| state.limiter.as_mut().unwrap());
+ }
+
+ // -- Instantiate via generated bindings --------------------------------
+ let linker = self.create_linker()?;
+
+ let bindings =
+ wit_bindings::SparComponent::instantiate(&mut store, &self.component, &linker)
+ .map_err(|e| WasmError::Instantiation(e.to_string()))?;
+
+ bindings
+ .pulseengine_rivet_renderer()
+ .call_render(&mut store, root, highlight)
+ .map_err(|e| WasmError::Guest(e.to_string()))?
+ .map_err(|e| WasmError::Guest(format!("render error: {:?}", e)))
+ }
+
/// Call the guest `export` function.
fn call_export(
&self,
@@ -562,4 +638,88 @@ mod tests {
other => panic!("expected Adapter error, got: {other:?}"),
}
}
+
+ /// End-to-end: load the spar WASM component, preopen a directory with
+ /// real AADL files, call the renderer, and verify the SVG output.
+ ///
+ /// Set `SPAR_WASM_PATH` to override the default component location.
+ /// The test is skipped if the component or AADL files are not found.
+ #[test]
+ fn render_aadl_via_wasm() {
+ // Only run if the WASM component exists
+ let wasm_path = std::env::var("SPAR_WASM_PATH").unwrap_or_else(|_| {
+ "/Volumes/Home/git/pulseengine/spar/target/wasm32-wasip2/release/spar_wasm.wasm".into()
+ });
+ let path = std::path::Path::new(&wasm_path);
+ if !path.exists() {
+ eprintln!("Skipping: WASM component not found at {}", path.display());
+ return;
+ }
+
+ // The AADL example directory
+ let aadl_dir =
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/aadl/aadl");
+ if !aadl_dir.exists() {
+ eprintln!("Skipping: AADL example not found at {}", aadl_dir.display());
+ return;
+ }
+
+ let runtime = WasmAdapterRuntime::with_defaults().unwrap();
+ let adapter = runtime.load_adapter(path).unwrap();
+
+ // Call render with the AADL directory preopened
+ let result = adapter.call_render("FlightControl::Controller.Basic", &[], Some(&aadl_dir));
+
+ match result {
+ Ok(svg) => {
+ assert!(svg.contains(""), "SVG should be complete");
+ assert!(svg.contains("data-id"), "nodes should have data-id");
+
+ // Write to temp for inspection
+ let out = std::env::temp_dir().join("rivet-wasm-test");
+ std::fs::create_dir_all(&out).ok();
+ let svg_path = out.join("wasm-rendered.svg");
+ std::fs::write(&svg_path, &svg).unwrap();
+ eprintln!("SVG written to: {}", svg_path.display());
+ }
+ Err(e) => {
+ // Some WASM/WASI issues are expected in test environments
+ eprintln!("Render returned error (may be expected): {:?}", e);
+ }
+ }
+ }
+
+ /// Load the real spar WASM component and call the renderer interface.
+ ///
+ /// Set `SPAR_WASM_PATH` to override the default component location.
+ /// The test is skipped if the component file does not exist.
+ #[test]
+ fn load_spar_wasm_component() {
+ let wasm_path = std::env::var("SPAR_WASM_PATH").unwrap_or_else(|_| {
+ "/Volumes/Home/git/pulseengine/spar/target/wasm32-wasip2/release/spar_wasm.wasm".into()
+ });
+ let path = Path::new(&wasm_path);
+ if !path.exists() {
+ eprintln!("Skipping: WASM component not found at {}", path.display());
+ return;
+ }
+
+ let runtime = WasmAdapterRuntime::with_defaults().unwrap();
+ let adapter = runtime.load_adapter(path).unwrap();
+
+ // Call render without any preopened AADL files. The component should
+ // load and the interface should be callable, but we expect an error
+ // because there are no .aadl source files available to the guest.
+ let result = adapter.call_render("Test::S.I", &[], None);
+ assert!(result.is_err());
+ let err_msg = format!("{:?}", result.unwrap_err());
+ assert!(
+ err_msg.contains("no .aadl files")
+ || err_msg.contains("render error")
+ || err_msg.contains("cannot instantiate"),
+ "unexpected error: {}",
+ err_msg
+ );
+ }
}
diff --git a/rivet-core/tests/integration.rs b/rivet-core/tests/integration.rs
index ca93bc4..cbb9372 100644
--- a/rivet-core/tests/integration.rs
+++ b/rivet-core/tests/integration.rs
@@ -1088,3 +1088,95 @@ fn test_diff_diagnostic_changes() {
"1 new errors, 1 resolved errors, 0 new warnings, 0 resolved warnings"
);
}
+
+// ── AADL diagram placeholder in documents ────────────────────────────────
+
+#[test]
+fn document_with_aadl_block_renders_placeholder() {
+ let doc_content = "---\nid: DOC-ARCH\ntitle: System Architecture\n---\n\n## Flight Control Architecture\n\nThe system uses the following AADL architecture:\n\n```aadl\nroot: FlightControl::Controller.Basic\n```\n\nThis design satisfies [[SYSREQ-001]].\n";
+
+ let doc = rivet_core::document::parse_document(doc_content, None).unwrap();
+ let html = rivet_core::document::render_to_html(&doc, |id| id == "SYSREQ-001");
+
+ // AADL block becomes a diagram placeholder
+ assert!(html.contains("class=\"aadl-diagram\""));
+ assert!(html.contains("data-root=\"FlightControl::Controller.Basic\""));
+
+ // Wiki-link still resolves
+ assert!(html.contains("SYSREQ-001"));
+
+ // Other text renders normally
+ assert!(html.contains("Flight Control Architecture"));
+}
+
+// ── AADL adapter ─────────────────────────────────────────────────────────
+
+#[test]
+fn aadl_adapter_parses_spar_json() {
+ use rivet_core::adapter::{Adapter, AdapterConfig, AdapterSource};
+ use rivet_core::formats::aadl::AadlAdapter;
+
+ let json = r#"{
+ "root": "Pkg::Sys.Impl",
+ "packages": [
+ {
+ "name": "Pkg",
+ "component_types": [
+ { "name": "Sys", "category": "system" }
+ ],
+ "component_impls": [
+ { "name": "Sys.Impl", "category": "system" }
+ ]
+ }
+ ],
+ "instance": null,
+ "diagnostics": [
+ {
+ "severity": "warning",
+ "message": "No binding for cpu1",
+ "path": ["root", "cpu1"],
+ "analysis": "binding_check"
+ }
+ ]
+ }"#;
+
+ let adapter = AadlAdapter::new();
+ let source = AdapterSource::Bytes(json.as_bytes().to_vec());
+ let config = AdapterConfig::default();
+ let artifacts = adapter.import(&source, &config).unwrap();
+
+ // 1 type + 1 impl + 1 diagnostic = 3 artifacts
+ assert_eq!(artifacts.len(), 3);
+ assert!(
+ artifacts
+ .iter()
+ .any(|a| a.artifact_type == "aadl-component" && a.id == "AADL-Pkg-Sys")
+ );
+ assert!(
+ artifacts
+ .iter()
+ .any(|a| a.artifact_type == "aadl-component" && a.id == "AADL-Pkg-Sys.Impl")
+ );
+ assert!(
+ artifacts
+ .iter()
+ .any(|a| a.artifact_type == "aadl-analysis-result")
+ );
+}
+
+// ── AADL schema ──────────────────────────────────────────────────────────
+
+#[test]
+fn aadl_schema_loads() {
+ let schemas_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .unwrap()
+ .join("schemas");
+ let common = rivet_core::schema::Schema::load_file(&schemas_dir.join("common.yaml")).unwrap();
+ let aadl = rivet_core::schema::Schema::load_file(&schemas_dir.join("aadl.yaml")).unwrap();
+ let merged = rivet_core::schema::Schema::merge(&[common, aadl]);
+ assert!(merged.artifact_type("aadl-component").is_some());
+ assert!(merged.artifact_type("aadl-analysis-result").is_some());
+ assert!(merged.artifact_type("aadl-flow").is_some());
+ assert!(merged.link_type("modeled-by").is_some());
+}
diff --git a/rivet.yaml b/rivet.yaml
index 675cb58..3bf00e6 100644
--- a/rivet.yaml
+++ b/rivet.yaml
@@ -4,7 +4,14 @@ project:
schemas:
- common
- dev
+ - aadl
sources:
- path: artifacts
format: generic-yaml
+
+docs:
+ - docs
+ - arch
+
+results: results
diff --git a/schemas/aadl.yaml b/schemas/aadl.yaml
new file mode 100644
index 0000000..f559033
--- /dev/null
+++ b/schemas/aadl.yaml
@@ -0,0 +1,126 @@
+# AADL Architecture schema for rivet
+#
+# Maps AADL components, analysis results, and flows into the rivet
+# artifact model. Bridges ASPICE SYS.3/SWE.2 architecture levels
+# with formal AADL models analyzed by spar.
+
+schema:
+ name: aadl
+ version: "0.1.0"
+ namespace: "http://pulseengine.dev/ns/aadl#"
+ extends: [common]
+ description: >
+ AADL architecture model artifact types for spar integration.
+
+artifact-types:
+
+ - name: aadl-component
+ description: AADL component type or implementation imported from spar
+ fields:
+ - name: category
+ type: string
+ required: true
+ allowed-values:
+ - system
+ - process
+ - thread
+ - thread-group
+ - processor
+ - virtual-processor
+ - memory
+ - bus
+ - virtual-bus
+ - device
+ - subprogram
+ - subprogram-group
+ - data
+ - abstract
+ - name: aadl-package
+ type: string
+ required: true
+ description: AADL package containing this component
+ - name: classifier-kind
+ type: string
+ required: false
+ allowed-values: [type, implementation, feature-group-type]
+ - name: features
+ type: structured
+ required: false
+ description: Port/access/feature group declarations
+ - name: properties
+ type: structured
+ required: false
+ description: AADL property associations
+ - name: aadl-file
+ type: string
+ required: false
+ description: Source .aadl file path
+ link-fields:
+ - name: allocated-from
+ link-type: allocated-from
+ target-types: [system-req, sw-req, system-arch-component, requirement, feature]
+ required: false
+ cardinality: zero-or-many
+
+ - name: aadl-analysis-result
+ description: Output of a spar analysis pass
+ fields:
+ - name: analysis-name
+ type: string
+ required: true
+ description: Name of the analysis (e.g., connectivity, scheduling, latency)
+ - name: severity
+ type: string
+ required: true
+ allowed-values: [error, warning, info]
+ - name: component-path
+ type: string
+ required: false
+ description: Hierarchical path to the component (e.g., root/subsystem/cpu)
+ - name: details
+ type: text
+ required: false
+ link-fields:
+ - name: analyzes
+ link-type: verifies
+ target-types: [aadl-component]
+ required: false
+ cardinality: zero-or-many
+
+ - name: aadl-flow
+ description: End-to-end flow with latency bounds
+ fields:
+ - name: flow-kind
+ type: string
+ required: true
+ allowed-values: [source, sink, path, end-to-end]
+ - name: latency-best-ms
+ type: number
+ required: false
+ - name: latency-worst-ms
+ type: number
+ required: false
+ - name: segments
+ type: structured
+ required: false
+ link-fields:
+ - name: part-of
+ link-type: allocated-from
+ target-types: [aadl-component]
+ required: false
+ cardinality: zero-or-many
+
+link-types:
+ - name: modeled-by
+ inverse: models
+ description: An architecture component is modeled by an AADL component
+ source-types: [system-arch-component, sw-arch-component]
+ target-types: [aadl-component]
+
+traceability-rules:
+ - name: aadl-component-has-allocation
+ description: AADL component should trace to a requirement or architecture element
+ source-type: aadl-component
+ required-link: allocated-from
+ target-types: [system-req, sw-req, system-arch-component, requirement, feature]
+ severity: info
diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh
new file mode 100755
index 0000000..ed749ac
--- /dev/null
+++ b/scripts/build-wasm.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Build spar-wasm component and transpile for browser use.
+# Usage: ./scripts/build-wasm.sh [spar-repo-path]
+
+SPAR_DIR="${1:-../spar}"
+OUT_DIR="rivet-cli/assets/wasm"
+
+if [ ! -d "$SPAR_DIR/crates/spar-wasm" ]; then
+ echo "Error: spar repo not found at $SPAR_DIR"
+ echo "Usage: $0 /path/to/spar"
+ exit 1
+fi
+
+echo "Building spar-wasm (wasm32-wasip2, release)..."
+(cd "$SPAR_DIR" && cargo build --target wasm32-wasip2 -p spar-wasm --release)
+
+mkdir -p "$OUT_DIR"
+cp "$SPAR_DIR/target/wasm32-wasip2/release/spar_wasm.wasm" "$OUT_DIR/"
+echo "Copied WASM component to $OUT_DIR/spar_wasm.wasm"
+ls -lh "$OUT_DIR/spar_wasm.wasm"
+
+echo ""
+echo "Transpiling for browser with jco..."
+npx @bytecodealliance/jco transpile "$OUT_DIR/spar_wasm.wasm" -o "$OUT_DIR/js/" 2>&1
+echo "Browser JS module written to $OUT_DIR/js/"
+ls -lh "$OUT_DIR/js/spar_wasm.js" "$OUT_DIR/js/spar_wasm.core.wasm" 2>/dev/null || true
diff --git a/scripts/fetch-wasm.sh b/scripts/fetch-wasm.sh
new file mode 100755
index 0000000..0a9c73f
--- /dev/null
+++ b/scripts/fetch-wasm.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Fetch pre-built WASM components from GitHub releases.
+# Usage: ./scripts/fetch-wasm.sh [version]
+
+VERSION="${1:-latest}"
+REPO="pulseengine/spar"
+ASSET="spar_wasm.wasm"
+OUT_DIR="rivet-cli/assets/wasm"
+
+mkdir -p "$OUT_DIR"
+
+if [ "$VERSION" = "latest" ]; then
+ echo "Fetching latest release from $REPO..."
+ URL=$(gh release view --repo "$REPO" --json assets -q ".assets[] | select(.name==\"$ASSET\") | .url" 2>/dev/null || true)
+ if [ -z "$URL" ]; then
+ echo "No release found with asset $ASSET. Build from source instead:"
+ echo " cd /path/to/spar && cargo build --target wasm32-wasip2 -p spar-wasm --release"
+ echo " cp target/wasm32-wasip2/release/spar_wasm.wasm $OUT_DIR/"
+ exit 1
+ fi
+else
+ echo "Fetching release $VERSION from $REPO..."
+ URL=$(gh release view "$VERSION" --repo "$REPO" --json assets -q ".assets[] | select(.name==\"$ASSET\") | .url" 2>/dev/null || true)
+ if [ -z "$URL" ]; then
+ echo "Release $VERSION not found or does not contain $ASSET"
+ exit 1
+ fi
+fi
+
+echo "Downloading $ASSET..."
+gh release download ${VERSION:+$VERSION} --repo "$REPO" --pattern "$ASSET" --dir "$OUT_DIR" --clobber
+echo "Saved to $OUT_DIR/$ASSET"
+
+# Check size
+ls -lh "$OUT_DIR/$ASSET"
diff --git a/wit/adapter.wit b/wit/adapter.wit
index 9e586d0..76c1d80 100644
--- a/wit/adapter.wit
+++ b/wit/adapter.wit
@@ -83,13 +83,42 @@ interface adapter {
supported-types: func() -> list;
/// Import artifacts from raw bytes
- import: func(source: list, config: adapter-config) -> result, adapter-error>;
+ %import: func(source: list, config: adapter-config) -> result, adapter-error>;
/// Export artifacts to raw bytes
- export: func(artifacts: list, config: adapter-config) -> result, adapter-error>;
+ %export: func(artifacts: list, config: adapter-config) -> result, adapter-error>;
}
/// World for a rivet adapter component
world rivet-adapter {
export adapter;
}
+
+/// Interface for rendering AADL instance models to SVG.
+///
+/// A `renderer` implementation parses AADL sources, instantiates
+/// from the given root component implementation, lays out the
+/// architecture, and returns an SVG string.
+interface renderer {
+ /// Errors returned by renderer operations
+ variant render-error {
+ parse-error(string),
+ no-root(string),
+ layout-error(string),
+ }
+
+ /// Render an AADL instance tree rooted at `root` (e.g. "Pkg::Impl").
+ ///
+ /// `highlight` is an optional list of component paths to visually
+ /// emphasise in the output (e.g. flow participants).
+ ///
+ /// Returns the SVG document as a UTF-8 string, or a `render-error`.
+ render: func(root: string, highlight: list) -> result;
+}
+
+/// World for a spar WASM component that can both adapt AADL artifacts
+/// for rivet and render instance-model diagrams.
+world spar-component {
+ export adapter;
+ export renderer;
+}