diff --git a/Cargo.lock b/Cargo.lock index 53b07c3..94bccc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2281,6 +2281,7 @@ dependencies = [ name = "rivet-core" version = "0.1.0" dependencies = [ + "anyhow", "criterion", "log", "petgraph", diff --git a/docs/srs.md b/docs/srs.md new file mode 100644 index 0000000..3b12129 --- /dev/null +++ b/docs/srs.md @@ -0,0 +1,87 @@ +--- +id: SRS-001 +type: specification +title: System Requirements Specification +status: draft +glossary: + STPA: Systems-Theoretic Process Analysis + UCA: Unsafe Control Action + ASPICE: Automotive SPICE + OSLC: Open Services for Lifecycle Collaboration + ReqIF: Requirements Interchange Format + WASM: WebAssembly +--- + +# System Requirements Specification + +## 1. Purpose + +This document specifies the system-level requirements for **Rivet**, an SDLC +traceability tool for safety-critical systems. Rivet manages lifecycle +artifacts (requirements, designs, tests, STPA analyses) as version-controlled +YAML files and validates their traceability links against composable schemas. + +## 2. Scope + +Rivet targets Automotive SPICE, ISO 26262, and ISO/SAE 21434 workflows. It +replaces heavyweight ALM tools with a text-file-first, git-friendly approach. + +## 3. Functional Requirements + +### 3.1 Artifact Management + +[[REQ-001]] defines the core principle: artifacts live as human-readable YAML +files under version control. + +[[REQ-002]] extends this to STPA artifacts — losses, hazards, unsafe control +actions, causal factors, and loss scenarios. + +### 3.2 Traceability + +[[REQ-003]] requires full Automotive SPICE V-model traceability, from +stakeholder requirements down to unit verification and back. + +[[REQ-004]] mandates a validation engine that checks link integrity, +cardinality constraints, required fields, and traceability coverage. + +### 3.3 Schema System + +[[REQ-010]] requires schema-driven validation where artifact types, fields, +link types, and traceability rules are defined declaratively. + +[[REQ-015]] aligns schemas with ASPICE 4.0 terminology (verification replaces +test). + +[[REQ-016]] adds cybersecurity schema support for ISO/SAE 21434 and ASPICE +SEC.1-4. + +### 3.4 Interoperability + +[[REQ-005]] covers ReqIF 1.2 import/export for requirements interchange with +tools like DOORS, Polarion, and codebeamer. + +[[REQ-006]] specifies OSLC-based bidirectional synchronization rather than +per-tool REST adapters. + +[[REQ-008]] enables WASM component adapters for custom format plugins. + +### 3.5 User Interface + +[[REQ-007]] requires both a CLI and an HTTP serve pattern for the dashboard. + +### 3.6 Quality + +[[REQ-012]] mandates comprehensive CI quality gates (fmt, clippy, test, miri, +audit, deny, vet, coverage). + +[[REQ-013]] requires performance benchmarks with regression detection. + +[[REQ-014]] structures test artifacts to mirror the ASPICE SWE.4/5/6 levels. + +[[REQ-009]] ties test results to GitHub releases as evidence artifacts. + +[[REQ-011]] pins Rust edition 2024 with MSRV 1.85. + +## 4. Glossary + +See the glossary panel below (defined in document frontmatter). diff --git a/etch/src/svg.rs b/etch/src/svg.rs index ab9d30f..7700e26 100644 --- a/etch/src/svg.rs +++ b/etch/src/svg.rs @@ -143,10 +143,13 @@ fn write_style(svg: &mut String, options: &SvgOptions) { \x20 .node text {{ font-family: {font}; font-size: {fs}px; \ fill: #222; text-anchor: middle; dominant-baseline: central; }}\n\ \x20 .node .sublabel {{ font-size: {}px; fill: #666; }}\n\ - \x20 .edge path {{ fill: none; stroke: {ec}; stroke-width: 1.2; \ + \x20 .edge path {{ fill: none; stroke: {ec}; stroke-width: 1.4; \ marker-end: url(#arrowhead); }}\n\ + \x20 .edge .label-bg {{ fill: #fff; opacity: 0.85; rx: 3; }}\n\ \x20 .edge text {{ font-family: {font}; font-size: {}px; \ - fill: {ec}; text-anchor: middle; }}\n\ + fill: #555; text-anchor: middle; dominant-baseline: central; \ + font-weight: 500; }}\n\ + \x20 .node:hover rect {{ filter: brightness(0.92); }}\n\ \x20 \n", fs - 2.0, fs - 2.0, @@ -175,15 +178,25 @@ fn write_edges(svg: &mut String, layout: &GraphLayout) { writeln!(svg, " ").unwrap(); - // Edge label at midpoint. + // Edge label at midpoint with background pill. if !edge.label.is_empty() { let mid = edge.points.len() / 2; let (mx, my) = edge.points[mid]; + let label = xml_escape(&edge.label); + let text_y = my - 4.0; + // Approximate label width: ~6.5px per char at default font size. + let approx_w = edge.label.len() as f64 * 6.5 + 8.0; + let approx_h = 14.0; writeln!( svg, - " {}", - my - 4.0, - xml_escape(&edge.label), + " ", + mx - approx_w / 2.0, + text_y - approx_h / 2.0, + ) + .unwrap(); + writeln!( + svg, + " {label}", ) .unwrap(); } diff --git a/examples/aspice/artifacts/architecture.yaml b/examples/aspice/artifacts/architecture.yaml new file mode 100644 index 0000000..110cc90 --- /dev/null +++ b/examples/aspice/artifacts/architecture.yaml @@ -0,0 +1,176 @@ +artifacts: + # ── System Architecture (SYS.3) ────────────────────────────────────── + + - id: SYSARCH-1 + type: system-arch-component + title: Hydraulic Control Unit + status: approved + description: > + The HCU receives brake pressure commands from the ECU and drives + proportional solenoid valves to modulate brake line pressure + independently on each axle. Contains the valve block, pump motor, + and pressure sensors. + tags: [braking, hcu, hardware] + fields: + component-type: mixed + interfaces: + provided: + - name: pressure-command + protocol: CAN FD + description: Accepts 12-bit pressure demand per axle at 100 Hz + required: + - name: power-supply + description: 12 V nominal, 60 A peak during pump operation + links: + - type: allocated-from + target: SYSREQ-1 + - type: allocated-from + target: SYSREQ-2 + + - id: SYSARCH-2 + type: system-arch-component + title: ABS Electronic Control Unit + status: approved + description: > + The ABS ECU hosts the slip control software, reads wheel speed sensors + via the sensor interface, and commands pressure modulation through the + HCU. Includes the microcontroller, CAN FD transceiver, and power + management. + tags: [braking, abs, ecu] + fields: + component-type: mixed + interfaces: + provided: + - name: abs-status + protocol: CAN FD + description: ABS active flag, wheel speeds, slip ratios at 100 Hz + required: + - name: wheel-speed-input + protocol: analog + description: 4x wheel speed sensor signals (inductive, 48 teeth) + - name: hcu-command + protocol: CAN FD + description: Pressure build/hold/release commands to HCU + links: + - type: allocated-from + target: SYSREQ-3 + + # ── Software Architecture (SWE.2) ──────────────────────────────────── + + - id: SWARCH-1 + type: sw-arch-component + title: Brake Pressure Manager + status: approved + description: > + Software component responsible for computing brake pressure demands + for each axle. Reads pedal position and axle load estimates, applies + the load-dependent ratio, and outputs DAC commands to the HCU valve + driver. Runs in the 10 ms periodic task. + tags: [braking, ebd, software] + fields: + interfaces: + provided: + - name: pressure_demand_output + type: function + description: "fn pressure_demand(pedal: u16, speed: u16, ratio: f32) -> [u16; 2]" + required: + - name: axle_load_input + type: function + description: "fn get_axle_loads() -> (f32, f32)" + concurrency: single-threaded (10 ms cyclic task) + resource-budgets: + stack: 2 KiB + wcet: 200 us + links: + - type: allocated-from + target: SWREQ-1 + - type: allocated-from + target: SWREQ-2 + + - id: SWARCH-2 + type: sw-arch-component + title: ABS Slip Controller + status: approved + description: > + Software component implementing the wheel slip regulation algorithm. + Reads wheel speed sensor inputs at 500 Hz via the sensor abstraction + layer, computes individual wheel slip ratios, determines the pressure + modulation phase (build/hold/release), and issues commands to the HCU + driver. Runs in the 2 ms high-priority task. + tags: [braking, abs, software] + fields: + interfaces: + provided: + - name: slip_status + type: struct + description: "struct SlipStatus { slip_ratio: [f32; 4], phase: [Phase; 4], abs_active: bool }" + required: + - name: wheel_speed_input + type: function + description: "fn read_wheel_speeds() -> [u16; 4]" + - name: hcu_command + type: function + description: "fn set_pressure_phase(wheel: u8, phase: Phase)" + concurrency: single-threaded (2 ms cyclic task) + resource-budgets: + stack: 4 KiB + wcet: 400 us + links: + - type: allocated-from + target: SWREQ-3 + + # ── Software Detailed Design / Unit Construction (SWE.3) ───────────── + + - id: SWDD-1 + type: sw-detail-design + title: Pressure demand calculation function + status: approved + description: > + Implements the brake pressure demand calculation. Reads the 12-bit + ADC pedal position value, multiplies by the load-dependent front/rear + ratio from the axle load estimator, clamps the result to the + [0, 4095] DAC range, and writes to the HCU valve driver output buffer. + Includes a rate limiter (max 500 LSB/cycle) to prevent pressure + spikes. + tags: [braking, ebd, implementation] + fields: + unit: src/braking/pressure_demand.rs + function: calculate_pressure_demand + algorithm: > + 1. Read pedal ADC (12-bit, 0-4095). + 2. Read axle load ratio (front_ratio, rear_ratio) from estimator. + 3. front_demand = clamp(pedal * front_ratio, 0, 4095). + 4. rear_demand = clamp(pedal * rear_ratio, 0, 4095). + 5. Apply rate limiter: abs(demand - prev_demand) <= 500. + 6. Write to HCU output buffer. + links: + - type: refines + target: SWARCH-1 + + - id: SWDD-2 + type: sw-detail-design + title: Wheel slip ratio computation and phase selector + status: approved + description: > + Computes individual wheel slip ratios from raw wheel speed sensor + ticks and vehicle reference speed. Implements the ABS phase state + machine: NORMAL -> BUILD -> HOLD -> RELEASE -> NORMAL based on slip + threshold crossings with hysteresis. Transition thresholds are + calibratable parameters stored in NVM. + tags: [braking, abs, implementation] + fields: + unit: src/braking/slip_control.rs + function: compute_slip_and_select_phase + algorithm: > + 1. Convert wheel speed ticks to m/s using calibration factor. + 2. Estimate vehicle reference speed as max(wheel_speeds). + 3. slip[i] = (v_ref - v_wheel[i]) / v_ref (guard div-by-zero). + 4. Phase state machine per wheel: + - NORMAL: if slip > threshold_high -> BUILD + - BUILD: if slip > threshold_release -> HOLD + - HOLD: if slip < threshold_low -> RELEASE + - RELEASE: if slip < threshold_normal -> NORMAL + 5. Output phase commands to HCU driver. + links: + - type: refines + target: SWARCH-2 diff --git a/examples/aspice/artifacts/requirements.yaml b/examples/aspice/artifacts/requirements.yaml new file mode 100644 index 0000000..b498bda --- /dev/null +++ b/examples/aspice/artifacts/requirements.yaml @@ -0,0 +1,156 @@ +artifacts: + # ── Stakeholder Requirements (SYS.1) ────────────────────────────────── + + - id: STKH-1 + type: stakeholder-req + title: Electronic Brake Force Distribution + status: approved + description: > + The vehicle shall distribute braking force between front and rear axles + electronically, adapting to load conditions, to ensure stable and + predictable deceleration across all operating conditions. + tags: [braking, ebd] + fields: + priority: must + source: customer + + - id: STKH-2 + type: stakeholder-req + title: Anti-lock Braking System + status: approved + description: > + The vehicle shall prevent wheel lock-up during emergency braking on all + surface types to maintain steering control and reduce stopping distance, + compliant with ECE R13-H and FMVSS 135. + tags: [braking, abs] + fields: + priority: must + source: regulation + + # ── System Requirements (SYS.2) ─────────────────────────────────────── + + - id: SYSREQ-1 + type: system-req + title: Brake pressure modulation per axle + status: approved + description: > + The braking system shall independently modulate brake pressure on front + and rear axles within 10 ms control cycle time, using proportional + solenoid valves driven by the hydraulic control unit. + tags: [braking, ebd, hydraulics] + fields: + req-type: functional + priority: must + verification-criteria: > + Measure brake pressure response on a dynamometer at each axle during + step and ramp demand profiles; confirm independent modulation within + 10 ms cycle time. + links: + - type: derives-from + target: STKH-1 + + - id: SYSREQ-2 + type: system-req + title: Dynamic load-dependent brake force ratio + status: approved + description: > + The system shall compute the front-to-rear brake force ratio as a + function of estimated vehicle deceleration, axle load transfer, and + surface friction coefficient, updating the ratio every control cycle. + tags: [braking, ebd, control] + fields: + req-type: functional + priority: must + verification-criteria: > + Verify computed brake force ratio against reference model output for + a set of deceleration, load, and friction scenarios on a + hardware-in-the-loop bench. + links: + - type: derives-from + target: STKH-1 + + - id: SYSREQ-3 + type: system-req + title: Wheel slip regulation + status: approved + description: > + The ABS controller shall regulate individual wheel slip to the target + slip ratio (10-20 % depending on surface) by modulating brake pressure + through build, hold, and release phases, achieving regulation within + 3 pressure cycles after lock-up onset detection. + tags: [braking, abs, control] + fields: + req-type: functional + priority: must + verification-criteria: > + Execute full-vehicle ABS stops on low-mu, split-mu, and high-mu + surfaces; confirm wheel slip stays within target band and regulation + onset occurs within 3 pressure cycles. + links: + - type: derives-from + target: STKH-2 + + # ── Software Requirements (SWE.1) ───────────────────────────────────── + + - id: SWREQ-1 + type: sw-req + title: Brake pressure demand calculation + status: approved + description: > + The software shall calculate the target brake pressure for each axle + based on driver brake pedal input, vehicle speed, and the load-dependent + ratio, outputting a 12-bit DAC command to the hydraulic valve driver + every 10 ms. + tags: [braking, ebd, software] + fields: + req-type: functional + priority: must + verification-criteria: > + Unit test the pressure demand function with boundary and nominal pedal + input, speed, and ratio combinations; verify DAC output within +/- 1 LSB + of the reference model. + links: + - type: derives-from + target: SYSREQ-1 + + - id: SWREQ-2 + type: sw-req + title: Axle load estimator + status: approved + description: > + The software shall estimate front and rear axle loads using longitudinal + acceleration from the inertial measurement unit and static weight + distribution parameters, updating the estimate every 10 ms with a + first-order low-pass filter (time constant 50 ms). + tags: [braking, ebd, estimation] + fields: + req-type: functional + priority: must + verification-criteria: > + Inject known acceleration profiles and verify estimated axle loads + against a Simulink reference model; maximum steady-state error + shall not exceed 2 % of nominal axle load. + links: + - type: derives-from + target: SYSREQ-2 + + - id: SWREQ-3 + type: sw-req + title: ABS slip control algorithm + status: approved + description: > + The software shall implement a threshold-based ABS slip control + algorithm that reads wheel speed sensor inputs at 500 Hz, computes + individual wheel slip ratios, and commands pressure build, hold, or + release actions to maintain each wheel within the target slip window. + tags: [braking, abs, software] + fields: + req-type: functional + priority: must + verification-criteria: > + Execute model-in-the-loop tests with recorded wheel speed data from + ice, wet, and dry surfaces; verify that slip regulation commands + match the validated reference controller output. + links: + - type: derives-from + target: SYSREQ-3 diff --git a/examples/aspice/artifacts/verification.yaml b/examples/aspice/artifacts/verification.yaml new file mode 100644 index 0000000..146dc31 --- /dev/null +++ b/examples/aspice/artifacts/verification.yaml @@ -0,0 +1,316 @@ +artifacts: + # ── Unit Verification (SWE.4) ──────────────────────────────────────── + + - id: UVER-1 + type: unit-verification + title: Pressure demand calculation unit tests + status: approved + description: > + Automated unit tests for the pressure demand calculation function. + Covers nominal pedal inputs, boundary conditions (0 and 4095), + rate limiter activation, and axle load ratio extremes. + tags: [braking, ebd, unit-test] + fields: + method: automated-test + preconditions: + - Rust test harness with mock HCU output buffer + - Calibration constants loaded from test fixture + steps: + - step: 1 + action: Call calculate_pressure_demand with pedal=0, ratio=(0.6, 0.4) + expected: front_demand=0, rear_demand=0 + - step: 2 + action: Call with pedal=4095, ratio=(0.6, 0.4) + expected: front_demand=2457, rear_demand=1638 + - step: 3 + action: Call with pedal=4095 after previous pedal=0 (rate limiter test) + expected: Demand increases by at most 500 per cycle + - step: 4 + action: Call with pedal=2048, ratio=(1.0, 0.0) — full front bias + expected: front_demand=2048, rear_demand=0 + links: + - type: verifies + target: SWDD-1 + + - id: UVER-2 + type: unit-verification + title: Slip ratio and phase state machine unit tests + status: approved + description: > + Automated unit tests for the wheel slip computation and ABS phase + state machine. Tests nominal slip calculation, divide-by-zero guard, + and all state transitions with calibratable thresholds. + tags: [braking, abs, unit-test] + fields: + method: automated-test + preconditions: + - Rust test harness with mock wheel speed sensor inputs + - NVM calibration parameters loaded from test fixture + steps: + - step: 1 + action: Set all wheel speeds equal to reference speed + expected: Slip ratio = 0.0 for all wheels, phase = NORMAL + - step: 2 + action: Set one wheel speed to 80 % of reference (20 % slip) + expected: Slip ratio = 0.2, phase transitions to BUILD + - step: 3 + action: Set reference speed to 0 (vehicle stationary) + expected: Slip ratio clamped to 0.0, no divide-by-zero + - step: 4 + action: Simulate full ABS cycle (NORMAL -> BUILD -> HOLD -> RELEASE -> NORMAL) + expected: Each phase transition occurs at correct threshold crossings + links: + - type: verifies + target: SWDD-2 + + # ── Software Integration Verification (SWE.5) ──────────────────────── + + - id: SWINTVER-1 + type: sw-integration-verification + title: Brake Pressure Manager integration verification + status: approved + description: > + Integration test verifying the Brake Pressure Manager component + interfaces. Validates that the pressure demand output is correctly + consumed by the HCU valve driver and that the axle load estimator + input interface provides consistent data across task boundaries. + tags: [braking, ebd, integration] + fields: + method: automated-test + preconditions: + - Software-in-the-loop environment with HCU driver stub + - Axle load estimator component running in parallel task + steps: + - step: 1 + action: Run 10 ms cyclic task for 100 cycles with ramp pedal input + expected: Pressure demand output follows pedal ramp with correct ratio + - step: 2 + action: Inject a step change in axle load estimate mid-cycle + expected: Pressure ratio adapts within one control cycle (10 ms) + - step: 3 + action: Verify inter-component data consistency under task preemption + expected: No data tearing in shared axle load structure + links: + - type: verifies + target: SWARCH-1 + + - id: SWINTVER-2 + type: sw-integration-verification + title: ABS Slip Controller integration verification + status: approved + description: > + Integration test verifying the ABS Slip Controller component + interfaces with the wheel speed sensor abstraction layer and the + HCU command interface. Validates end-to-end data flow from sensor + read to pressure phase command output. + tags: [braking, abs, integration] + fields: + method: automated-test + preconditions: + - Software-in-the-loop environment with sensor and HCU driver stubs + - Simulated wheel speed profiles for ABS activation scenario + steps: + - step: 1 + action: Run 2 ms cyclic task with all wheels at constant speed + expected: No ABS intervention, all phases remain NORMAL + - step: 2 + action: Inject sudden wheel deceleration on one wheel (simulated lock-up) + expected: ABS activates within 3 control cycles, phase transitions to BUILD + - step: 3 + action: Verify HCU command output matches expected phase sequence + expected: Build, hold, release commands issued in correct order + links: + - type: verifies + target: SWARCH-2 + + # ── Software Verification (SWE.6) ──────────────────────────────────── + + - id: SWVER-1 + type: sw-verification + title: Brake pressure demand and axle load estimation verification + status: approved + description: > + Software-level verification of the brake pressure demand calculation + and axle load estimator against their software requirements. + Conducted on the target microcontroller using hardware-in-the-loop + simulation with calibrated brake pedal and IMU sensor inputs. + tags: [braking, ebd, hil] + fields: + method: automated-test + preconditions: + - Hardware-in-the-loop bench with calibrated pedal sensor simulator + - IMU signal generator for acceleration profiles + - CAN FD bus analyzer monitoring HCU commands + steps: + - step: 1 + action: Apply 50 % pedal input at 60 km/h on level road + expected: DAC output matches expected pressure demand within +/- 1 LSB + - step: 2 + action: Apply full braking during 0.8 g deceleration + expected: Axle load estimate shifts ratio towards front axle within 2 % + - step: 3 + action: Release brake pedal rapidly + expected: Pressure demand ramps down respecting rate limiter + links: + - type: verifies + target: SWREQ-1 + - type: verifies + target: SWREQ-2 + + - id: SWVER-2 + type: sw-verification + title: ABS slip control algorithm verification + status: approved + description: > + Software-level verification of the ABS slip control algorithm against + its software requirement. Uses a vehicle dynamics model in the + hardware-in-the-loop environment to simulate lock-up scenarios on + various road surfaces. + tags: [braking, abs, hil] + fields: + method: automated-test + preconditions: + - Hardware-in-the-loop bench with vehicle dynamics model (CarMaker) + - Wheel speed sensor emulation (4 channels, 48 teeth) + - Road surface friction profiles (ice, wet, dry, split-mu) + steps: + - step: 1 + action: Emergency braking at 100 km/h on dry asphalt (mu = 0.9) + expected: No wheel lock-up, slip stays within 10-15 % target band + - step: 2 + action: Emergency braking at 80 km/h on ice (mu = 0.15) + expected: ABS activates, slip regulated within 10-20 % band + - step: 3 + action: Emergency braking at 60 km/h on split-mu (left ice, right dry) + expected: Independent wheel regulation, vehicle maintains directional stability + links: + - type: verifies + target: SWREQ-3 + + # ── System Integration Verification (SYS.4) ────────────────────────── + + - id: SYSINTVER-1 + type: sys-integration-verification + title: HCU integration verification + status: approved + description: > + System integration verification of the Hydraulic Control Unit with + the ABS ECU. Validates the CAN FD command interface, solenoid valve + response timing, and pressure sensor feedback loop on the physical + brake system test bench. + tags: [braking, hcu, system-integration] + fields: + method: automated-test + preconditions: + - Physical brake system test bench with HCU and ABS ECU + - CAN FD bus connected and operational + - Brake fluid system primed and bled + steps: + - step: 1 + action: Send pressure build command for front axle via CAN FD + expected: Front solenoid valve opens within 5 ms, pressure rises + - step: 2 + action: Send hold command followed by release command + expected: Pressure holds stable, then decreases within 10 ms + - step: 3 + action: Verify pressure sensor feedback matches commanded pressure + expected: Feedback within 3 % of commanded value at steady state + links: + - type: verifies + target: SYSARCH-1 + + - id: SYSINTVER-2 + type: sys-integration-verification + title: ABS ECU integration verification + status: approved + description: > + System integration verification of the ABS ECU with wheel speed + sensors and the HCU. Validates the complete sensor-to-actuator + signal chain on the vehicle integration test bench. + tags: [braking, abs, system-integration] + fields: + method: automated-test + preconditions: + - Vehicle integration test bench with all four wheel speed sensors + - ABS ECU connected to HCU via CAN FD + - Wheel speed simulation via motor-driven tone wheels + steps: + - step: 1 + action: Spin all tone wheels at constant speed (60 km/h equivalent) + expected: ECU reads four valid wheel speeds, ABS inactive + - step: 2 + action: Decelerate one tone wheel rapidly (simulate lock-up) + expected: ECU detects slip, sends pressure modulation commands to HCU + - step: 3 + action: Verify end-to-end latency from sensor event to valve actuation + expected: Total latency below 6 ms (2 ms computation + 4 ms CAN + valve) + links: + - type: verifies + target: SYSARCH-2 + + # ── System Verification (SYS.5) ────────────────────────────────────── + + - id: SYSVER-1 + type: sys-verification + title: Brake pressure modulation and load-dependent ratio system test + status: approved + description: > + Full system verification of brake pressure modulation and dynamic + load-dependent ratio on the vehicle dynamometer. Validates against + system requirements for axle-independent modulation and load-based + ratio adaptation. + tags: [braking, ebd, dynamometer] + fields: + method: automated-test + preconditions: + - Vehicle on chassis dynamometer with brake pressure transducers + - Vehicle loaded to GVW (Gross Vehicle Weight) + - Data acquisition system recording at 1 kHz + steps: + - step: 1 + action: Apply 50 % brake pedal at 100 km/h, measure front and rear pressure + expected: Independent pressure modulation with front/rear ratio matching load + - step: 2 + action: Repeat with vehicle at curb weight (reduced rear load) + expected: Ratio shifts towards front axle compared to GVW test + - step: 3 + action: Apply step pedal input, measure pressure response time + expected: Pressure responds within 10 ms control cycle at each axle + links: + - type: verifies + target: SYSREQ-1 + - type: verifies + target: SYSREQ-2 + + - id: SYSVER-2 + type: sys-verification + title: ABS wheel slip regulation system test + status: approved + description: > + Full system verification of ABS wheel slip regulation on the proving + ground. Validates against the system requirement for slip regulation + within the target band on multiple surface types. + tags: [braking, abs, proving-ground] + fields: + method: manual-test + preconditions: + - Instrumented test vehicle on proving ground + - Low-mu (basalt tile), split-mu, and high-mu (dry asphalt) surfaces + - Optical wheel speed reference measurement system + - On-board data logger recording slip ratios and pressure commands + steps: + - step: 1 + action: Emergency stop from 80 km/h on dry asphalt + expected: No wheel lock-up, stopping distance within ECE R13-H limit + - step: 2 + action: Emergency stop from 60 km/h on wet basalt tiles (mu ~ 0.3) + expected: ABS activates, slip regulated within 10-20 % band + - step: 3 + action: Emergency stop from 60 km/h on split-mu surface + expected: ABS regulates each side independently, vehicle tracks straight + - step: 4 + action: Verify regulation onset timing + expected: Slip regulation achieved within 3 pressure cycles of lock-up onset + links: + - type: verifies + target: SYSREQ-3 diff --git a/examples/aspice/docs/sdd.md b/examples/aspice/docs/sdd.md new file mode 100644 index 0000000..fe6f669 --- /dev/null +++ b/examples/aspice/docs/sdd.md @@ -0,0 +1,112 @@ +--- +id: SDD-001 +type: design +title: Software Design Document — Electronic Braking System +status: approved +glossary: + EBD: Electronic Brake Force Distribution + ABS: Anti-lock Braking System + HCU: Hydraulic Control Unit + ECU: Electronic Control Unit + NVM: Non-Volatile Memory + WCET: Worst-Case Execution Time + DAC: Digital-to-Analog Converter + ADC: Analog-to-Digital Converter + IMU: Inertial Measurement Unit +--- + +# Software Design Document — Electronic Braking System + +## 1. Introduction + +This document describes the software design for the Electronic Braking +System (EBS), covering both the Electronic Brake Force Distribution (EBD) +and Anti-lock Braking System (ABS) functions. The design is structured +into two major software architecture components, each decomposed into +detailed design units. + +## 2. Software Architecture Overview + +The braking software runs on a dual-core automotive microcontroller. +The architecture is divided into two components aligned with the V-model: + +- **[[SWARCH-1]]** — Brake Pressure Manager: responsible for computing + axle-level brake pressure demands based on driver input and load + distribution. Executes in the 10 ms periodic task on Core 0. + +- **[[SWARCH-2]]** — ABS Slip Controller: responsible for detecting + incipient wheel lock-up and modulating brake pressure to maintain + wheel slip within the target band. Executes in the 2 ms high-priority + task on Core 1. + +## 3. Detailed Design + +### 3.1 Pressure Demand Calculation + +The pressure demand function (**[[SWDD-1]]**) is the core of the EBD +subsystem. It converts driver pedal input into calibrated brake pressure +commands for the front and rear axles. + +**Algorithm outline:** + +1. Read the 12-bit ADC pedal position (0–4095). +2. Retrieve the current front/rear axle load ratio from the axle load + estimator. +3. Compute `front_demand = clamp(pedal * front_ratio, 0, 4095)`. +4. Compute `rear_demand = clamp(pedal * rear_ratio, 0, 4095)`. +5. Apply a rate limiter (maximum 500 LSB per 10 ms cycle) to prevent + hydraulic pressure spikes. +6. Write the results to the HCU valve driver output buffer. + +The rate limiter is critical for driver comfort and valve protection. +Calibration constants (ratio bounds, rate limit) are stored in NVM and +can be updated via the UDS WriteDataByIdentifier service. + +### 3.2 Wheel Slip Ratio and Phase Selection + +The slip controller (**[[SWDD-2]]**) implements the ABS regulation +algorithm. It runs at 500 Hz (2 ms cycle) to achieve the required +response time. + +**Slip ratio computation:** + +``` +slip[i] = (v_ref - v_wheel[i]) / v_ref +``` + +where `v_ref` is the estimated vehicle reference speed (maximum of all +wheel speeds) and `v_wheel[i]` is the speed of wheel `i`. A +divide-by-zero guard clamps the ratio to 0.0 when the vehicle is +stationary. + +**Phase state machine (per wheel):** + +| Current State | Condition | Next State | +|---------------|------------------------------|------------| +| NORMAL | slip > threshold_high | BUILD | +| BUILD | slip > threshold_release | HOLD | +| HOLD | slip < threshold_low | RELEASE | +| RELEASE | slip < threshold_normal | NORMAL | + +Threshold values are calibratable NVM parameters with hysteresis to +prevent oscillation at state boundaries. + +## 4. Interface Summary + +The two architecture components interact through shared data structures +protected by the AUTOSAR RTE mechanism: + +| Interface | Producer | Consumer | Rate | +|------------------------|---------------|----------------|--------| +| Axle load estimate | [[SWARCH-1]] | [[SWARCH-1]] | 10 ms | +| Pressure demand output | [[SWARCH-1]] | HCU driver | 10 ms | +| Wheel speed input | Sensor HAL | [[SWARCH-2]] | 2 ms | +| Slip status output | [[SWARCH-2]] | Vehicle bus | 10 ms | +| HCU phase commands | [[SWARCH-2]] | HCU driver | 2 ms | + +## 5. Resource Budgets + +| Component | Stack | WCET | Priority | +|----------------|--------|---------|----------| +| [[SWARCH-1]] | 2 KiB | 200 us | Medium | +| [[SWARCH-2]] | 4 KiB | 400 us | High | diff --git a/examples/aspice/results/run-001.yaml b/examples/aspice/results/run-001.yaml new file mode 100644 index 0000000..8537b8b --- /dev/null +++ b/examples/aspice/results/run-001.yaml @@ -0,0 +1,26 @@ +run: + id: run-2026-03-01 + timestamp: "2026-03-01T09:15:00Z" + source: "CI pipeline #18" + environment: "HIL bench A" + commit: "a1b2c3d" +results: + - artifact: UVER-1 + status: pass + duration: "1.2s" + - artifact: UVER-2 + status: pass + duration: "2.8s" + - artifact: SWINTVER-1 + status: pass + duration: "4.5s" + - artifact: SWINTVER-2 + status: fail + message: "ABS activation latency exceeded 3 cycle threshold on ice surface" + duration: "6.1s" + - artifact: SWVER-1 + status: pass + duration: "12.3s" + - artifact: SWVER-2 + status: pass + duration: "15.7s" diff --git a/examples/aspice/results/run-002.yaml b/examples/aspice/results/run-002.yaml new file mode 100644 index 0000000..764a22b --- /dev/null +++ b/examples/aspice/results/run-002.yaml @@ -0,0 +1,38 @@ +run: + id: run-2026-03-05 + timestamp: "2026-03-05T14:30:00Z" + source: "CI pipeline #24" + environment: "HIL bench A" + commit: "e4f5g6h" +results: + - artifact: UVER-1 + status: pass + duration: "1.1s" + - artifact: UVER-2 + status: pass + duration: "2.6s" + - artifact: SWINTVER-1 + status: pass + duration: "4.2s" + - artifact: SWINTVER-2 + status: pass + duration: "5.8s" + message: "Fixed: ABS cycle threshold tuning resolved" + - artifact: SWVER-1 + status: pass + duration: "11.9s" + - artifact: SWVER-2 + status: pass + duration: "14.3s" + - artifact: SYSINTVER-1 + status: pass + duration: "22.1s" + - artifact: SYSINTVER-2 + status: skip + message: "Vehicle integration bench unavailable" + - artifact: SYSVER-1 + status: pass + duration: "45.0s" + - artifact: SYSVER-2 + status: blocked + message: "Proving ground access pending weather clearance" diff --git a/examples/aspice/rivet.yaml b/examples/aspice/rivet.yaml new file mode 100644 index 0000000..726188e --- /dev/null +++ b/examples/aspice/rivet.yaml @@ -0,0 +1,16 @@ +# Run: rivet --schemas ../../schemas validate +project: + name: aspice-braking-system + version: "1.0.0" + schemas: + - common + - aspice + +sources: + - path: artifacts + format: generic-yaml + +docs: + - docs + +results: results diff --git a/rivet-cli/src/main.rs b/rivet-cli/src/main.rs index 33dcae0..f3f1528 100644 --- a/rivet-cli/src/main.rs +++ b/rivet-cli/src/main.rs @@ -4,9 +4,12 @@ use std::process::ExitCode; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use rivet_core::coverage; use rivet_core::diff::{ArtifactDiff, DiagnosticDiff}; +use rivet_core::document::{self, DocumentStore}; use rivet_core::links::LinkGraph; use rivet_core::matrix::{self, Direction}; +use rivet_core::results::{self, ResultStore}; use rivet_core::schema::Severity; use rivet_core::store::Store; use rivet_core::validate; @@ -34,6 +37,21 @@ struct Cli { #[derive(Subcommand)] enum Command { + /// Initialize a new rivet project + Init { + /// Project name (defaults to directory name) + #[arg(long)] + name: Option, + + /// Schemas to include (e.g. common,dev or common,aspice) + #[arg(long, value_delimiter = ',', default_values_t = ["common".to_string(), "dev".to_string()])] + schema: Vec, + + /// Directory to initialize (defaults to current directory) + #[arg(long, default_value = ".")] + dir: PathBuf, + }, + /// Validate artifacts against schemas Validate, @@ -51,6 +69,17 @@ enum Command { /// Show artifact summary statistics Stats, + /// Show traceability coverage report + Coverage { + /// Output format: "table" (default) or "json" + #[arg(short, long, default_value = "table")] + format: String, + + /// Exit with failure if overall coverage is below this percentage + #[arg(long)] + fail_under: Option, + }, + /// Generate a traceability matrix Matrix { /// Source artifact type @@ -154,11 +183,18 @@ fn main() -> ExitCode { } fn run(cli: Cli) -> Result { + // Init does not need a loaded project; handle it first. + if let Command::Init { name, schema, dir } = &cli.command { + return cmd_init(name.as_deref(), schema, dir); + } + match &cli.command { + Command::Init { .. } => unreachable!(), Command::Stpa { path, schema } => cmd_stpa(path, schema.as_deref(), &cli), Command::Validate => cmd_validate(&cli), Command::List { r#type, status } => cmd_list(&cli, r#type.as_deref(), status.as_deref()), Command::Stats => cmd_stats(&cli), + Command::Coverage { format, fail_under } => cmd_coverage(&cli, format, fail_under.as_ref()), Command::Matrix { from, to, @@ -169,9 +205,19 @@ fn run(cli: Cli) -> Result { Command::Export { format, output } => cmd_export(&cli, format, output.as_deref()), Command::Serve { port } => { let port = *port; - let (store, schema, graph) = load_project(&cli)?; + let (store, schema, graph, doc_store, result_store, project_name, project_path) = + load_project_full(&cli)?; let rt = tokio::runtime::Runtime::new().context("failed to create tokio runtime")?; - rt.block_on(serve::run(store, schema, graph, port))?; + rt.block_on(serve::run( + store, + schema, + graph, + doc_store, + result_store, + project_name, + project_path, + port, + ))?; Ok(true) } #[cfg(feature = "wasm")] @@ -183,6 +229,122 @@ fn run(cli: Cli) -> Result { } } +/// Initialize a new rivet project. +fn cmd_init(name: Option<&str>, schemas: &[String], dir: &std::path::Path) -> Result { + let dir = if dir == std::path::Path::new(".") { + std::env::current_dir().context("resolving current directory")? + } else { + dir.to_path_buf() + }; + + let project_name = name.map(|s| s.to_string()).unwrap_or_else(|| { + dir.file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "my-project".to_string()) + }); + + // Check for existing rivet.yaml + let config_path = dir.join("rivet.yaml"); + if config_path.exists() { + eprintln!( + "warning: {} already exists, skipping init", + config_path.display() + ); + return Ok(false); + } + + // Ensure the target directory exists + std::fs::create_dir_all(&dir) + .with_context(|| format!("creating directory {}", dir.display()))?; + + // Build schema list for the config + let schema_entries: String = schemas + .iter() + .map(|s| format!(" - {s}")) + .collect::>() + .join("\n"); + + // Write rivet.yaml + let config_content = format!( + "\ +project: + name: {project_name} + version: \"0.1.0\" + schemas: +{schema_entries} + +sources: + - path: artifacts + format: generic-yaml +" + ); + std::fs::write(&config_path, &config_content) + .with_context(|| format!("writing {}", config_path.display()))?; + println!(" created {}", config_path.display()); + + // Create artifacts/ directory with a sample file + let artifacts_dir = dir.join("artifacts"); + std::fs::create_dir_all(&artifacts_dir) + .with_context(|| format!("creating {}", artifacts_dir.display()))?; + + let sample_artifact_path = artifacts_dir.join("requirements.yaml"); + let sample_artifact = "\ +artifacts: + - id: REQ-001 + type: requirement + title: First requirement + status: draft + description: > + Describe what the system shall do. + tags: [core] + fields: + priority: must + category: functional +"; + std::fs::write(&sample_artifact_path, sample_artifact) + .with_context(|| format!("writing {}", sample_artifact_path.display()))?; + println!(" created {}", sample_artifact_path.display()); + + // Create docs/ directory with a sample document + let docs_dir = dir.join("docs"); + std::fs::create_dir_all(&docs_dir) + .with_context(|| format!("creating {}", docs_dir.display()))?; + + let sample_doc_path = docs_dir.join("getting-started.md"); + let sample_doc = format!( + "\ +# {project_name} + +Getting started with your rivet project. + +## Overview + +This project uses [rivet](https://github.com/pulseengine/rivet) for SDLC artifact +traceability and validation. Artifacts are stored as YAML files in `artifacts/` and +validated against schemas listed in `rivet.yaml`. + +## Quick start + +```bash +rivet validate # Validate all artifacts +rivet list # List all artifacts +rivet stats # Show summary statistics +``` +" + ); + std::fs::write(&sample_doc_path, &sample_doc) + .with_context(|| format!("writing {}", sample_doc_path.display()))?; + println!(" created {}", sample_doc_path.display()); + + println!( + "\nInitialized rivet project '{}' in {}", + project_name, + dir.display() + ); + + Ok(true) +} + /// Load STPA files directly and validate them. fn cmd_stpa( stpa_dir: &std::path::Path, @@ -257,8 +419,17 @@ fn cmd_stpa( /// Validate a full project (with rivet.yaml). fn cmd_validate(cli: &Cli) -> Result { - let (store, schema, graph) = load_project(cli)?; - let diagnostics = validate::validate(&store, &schema, &graph); + let (store, schema, graph, doc_store) = load_project_with_docs(cli)?; + let mut diagnostics = validate::validate(&store, &schema, &graph); + diagnostics.extend(validate::validate_documents(&doc_store, &store)); + + if !doc_store.is_empty() { + println!( + "Loaded {} documents with {} artifact references", + doc_store.len(), + doc_store.all_references().len() + ); + } print_diagnostics(&diagnostics); @@ -326,6 +497,68 @@ fn cmd_stats(cli: &Cli) -> Result { Ok(true) } +/// Show traceability coverage report. +fn cmd_coverage(cli: &Cli, format: &str, fail_under: Option<&f64>) -> Result { + let (store, schema, graph) = load_project(cli)?; + let report = coverage::compute_coverage(&store, &schema, &graph); + + if format == "json" { + let json = report + .to_json() + .map_err(|e| anyhow::anyhow!("json serialization: {e}"))?; + println!("{json}"); + } else { + println!("Traceability Coverage Report\n"); + println!( + " {:<30} {:<20} {:>8} {:>8} {:>8}", + "Rule", "Source Type", "Covered", "Total", "%" + ); + println!(" {}", "-".repeat(80)); + + for entry in &report.entries { + println!( + " {:<30} {:<20} {:>8} {:>8} {:>7.1}%", + entry.rule_name, + entry.source_type, + entry.covered, + entry.total, + entry.percentage() + ); + } + + let overall = report.overall_coverage(); + println!(" {}", "-".repeat(80)); + println!(" {:<52} {:>7.1}%", "Overall (weighted)", overall); + + // Show uncovered artifacts + let has_uncovered = report.entries.iter().any(|e| !e.uncovered_ids.is_empty()); + if has_uncovered { + println!("\nUncovered artifacts:"); + for entry in &report.entries { + if !entry.uncovered_ids.is_empty() { + println!(" {} ({}):", entry.rule_name, entry.source_type); + for id in &entry.uncovered_ids { + println!(" {}", id); + } + } + } + } + } + + if let Some(&threshold) = fail_under { + let overall = report.overall_coverage(); + if overall < threshold { + eprintln!( + "\nerror: overall coverage {:.1}% is below threshold {:.1}%", + overall, threshold + ); + return Ok(false); + } + } + + Ok(true) +} + /// Generate a traceability matrix. fn cmd_matrix( cli: &Cli, @@ -620,6 +853,109 @@ fn load_project(cli: &Cli) -> Result<(Store, rivet_core::schema::Schema, LinkGra Ok((store, schema, graph)) } +fn load_project_with_docs( + cli: &Cli, +) -> Result<(Store, rivet_core::schema::Schema, LinkGraph, DocumentStore)> { + let config_path = cli.project.join("rivet.yaml"); + let config = rivet_core::load_project_config(&config_path) + .with_context(|| format!("loading {}", config_path.display()))?; + + let schemas_dir = resolve_schemas_dir(cli); + let schema = rivet_core::load_schemas(&config.project.schemas, &schemas_dir) + .context("loading schemas")?; + + let mut store = Store::new(); + for source in &config.sources { + let artifacts = rivet_core::load_artifacts(source, &cli.project) + .with_context(|| format!("loading source '{}'", source.path))?; + for artifact in artifacts { + store.upsert(artifact); + } + } + + let graph = LinkGraph::build(&store, &schema); + + // Load documents from configured directories. + let mut doc_store = DocumentStore::new(); + for docs_path in &config.docs { + let dir = cli.project.join(docs_path); + let docs = document::load_documents(&dir) + .with_context(|| format!("loading docs from '{docs_path}'"))?; + for doc in docs { + doc_store.insert(doc); + } + } + + Ok((store, schema, graph, doc_store)) +} + +fn load_project_full( + cli: &Cli, +) -> Result<( + Store, + rivet_core::schema::Schema, + LinkGraph, + DocumentStore, + ResultStore, + String, + PathBuf, +)> { + let config_path = cli.project.join("rivet.yaml"); + let config = rivet_core::load_project_config(&config_path) + .with_context(|| format!("loading {}", config_path.display()))?; + + let schemas_dir = resolve_schemas_dir(cli); + let schema = rivet_core::load_schemas(&config.project.schemas, &schemas_dir) + .context("loading schemas")?; + + let mut store = Store::new(); + for source in &config.sources { + let artifacts = rivet_core::load_artifacts(source, &cli.project) + .with_context(|| format!("loading source '{}'", source.path))?; + for artifact in artifacts { + store.upsert(artifact); + } + } + + let graph = LinkGraph::build(&store, &schema); + + // Load documents + let mut doc_store = DocumentStore::new(); + for docs_path in &config.docs { + let dir = cli.project.join(docs_path); + let docs = document::load_documents(&dir) + .with_context(|| format!("loading docs from '{docs_path}'"))?; + for doc in docs { + doc_store.insert(doc); + } + } + + // Load test results + let mut result_store = ResultStore::new(); + if let Some(ref results_path) = config.results { + let dir = cli.project.join(results_path); + let runs = results::load_results(&dir) + .with_context(|| format!("loading results from '{results_path}'"))?; + for run in runs { + result_store.insert(run); + } + } + + let project_name = config.project.name.clone(); + let project_path = std::fs::canonicalize(&cli.project) + .unwrap_or_else(|_| cli.project.clone()); + + Ok(( + store, + schema, + graph, + doc_store, + result_store, + project_name, + project_path, + )) +} + fn print_stats(store: &Store) { println!("Artifact summary:"); let mut types: Vec<&str> = store.types().collect(); diff --git a/rivet-cli/src/serve.rs b/rivet-cli/src/serve.rs index e96a33e..21b0c90 100644 --- a/rivet-cli/src/serve.rs +++ b/rivet-cli/src/serve.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; use std::sync::Arc; use anyhow::Result; @@ -12,25 +13,190 @@ use petgraph::visit::EdgeRef; use etch::filter::ego_subgraph; use etch::layout::{self as pgv_layout, EdgeInfo, LayoutOptions, NodeInfo}; use etch::svg::{SvgOptions, render_svg}; +use rivet_core::coverage; +use rivet_core::document::{self, DocumentStore}; use rivet_core::links::LinkGraph; use rivet_core::matrix::{self, Direction}; +use rivet_core::results::ResultStore; use rivet_core::schema::{Schema, Severity}; use rivet_core::store::Store; use rivet_core::validate; +// ── Repository context ────────────────────────────────────────────────── + +/// Git repository status captured at load time. +struct GitInfo { + branch: String, + commit_short: String, + is_dirty: bool, + dirty_count: usize, +} + +/// A discovered sibling project (example or peer). +struct SiblingProject { + name: String, + rel_path: String, +} + +/// Project context shown in the dashboard header. +struct RepoContext { + project_name: String, + project_path: String, + git: Option, + loaded_at: String, + siblings: Vec, + port: u16, +} + +fn capture_git_info(project_path: &std::path::Path) -> Option { + let branch = std::process::Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(project_path) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())?; + + let commit_short = std::process::Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .current_dir(project_path) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default(); + + let porcelain = std::process::Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(project_path) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) + .unwrap_or_default(); + + let dirty_count = porcelain.lines().filter(|l| !l.is_empty()).count(); + + Some(GitInfo { + branch, + commit_short, + is_dirty: dirty_count > 0, + dirty_count, + }) +} + +/// Discover other rivet projects (examples/ and peer directories). +fn discover_siblings(project_path: &std::path::Path) -> Vec { + let mut siblings = Vec::new(); + + // Check examples/ subdirectory + let examples_dir = project_path.join("examples"); + if examples_dir.is_dir() { + if let Ok(entries) = std::fs::read_dir(&examples_dir) { + for entry in entries.flatten() { + let p = entry.path(); + if p.join("rivet.yaml").exists() { + if let Some(name) = p.file_name().and_then(|n| n.to_str()) { + siblings.push(SiblingProject { + name: name.to_string(), + rel_path: format!("examples/{name}"), + }); + } + } + } + } + } + + // If inside examples/, offer root project and peers + if let Some(parent) = project_path.parent() { + if parent.file_name().and_then(|n| n.to_str()) == Some("examples") { + if let Some(root) = parent.parent() { + if root.join("rivet.yaml").exists() { + if let Ok(cfg) = std::fs::read_to_string(root.join("rivet.yaml")) { + let root_name = cfg + .lines() + .find(|l| l.trim().starts_with("name:")) + .map(|l| l.trim().trim_start_matches("name:").trim().to_string()) + .unwrap_or_else(|| { + root.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("root") + .to_string() + }); + siblings.push(SiblingProject { + name: root_name, + rel_path: root.display().to_string(), + }); + } + } + // Peer examples + if let Ok(entries) = std::fs::read_dir(parent) { + for entry in entries.flatten() { + let p = entry.path(); + if p != project_path && p.join("rivet.yaml").exists() { + if let Some(name) = p.file_name().and_then(|n| n.to_str()) { + siblings.push(SiblingProject { + name: name.to_string(), + rel_path: p.display().to_string(), + }); + } + } + } + } + } + } + } + + siblings.sort_by(|a, b| a.name.cmp(&b.name)); + siblings +} + /// Shared application state loaded once at startup. struct AppState { store: Store, schema: Schema, graph: LinkGraph, + doc_store: DocumentStore, + result_store: ResultStore, + context: RepoContext, } /// Start the axum HTTP server on the given port. -pub async fn run(store: Store, schema: Schema, graph: LinkGraph, port: u16) -> Result<()> { +pub async fn run( + store: Store, + schema: Schema, + graph: LinkGraph, + doc_store: DocumentStore, + result_store: ResultStore, + project_name: String, + project_path: PathBuf, + port: u16, +) -> Result<()> { + let git = capture_git_info(&project_path); + let loaded_at = std::process::Command::new("date") + .arg("+%H:%M:%S") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".into()); + let siblings = discover_siblings(&project_path); + let context = RepoContext { + project_name, + project_path: project_path.display().to_string(), + git, + loaded_at, + siblings, + port, + }; + let state = Arc::new(AppState { store, schema, graph, + doc_store, + result_store, + context, }); let app = Router::new() @@ -42,6 +208,13 @@ pub async fn run(store: Store, schema: Schema, graph: LinkGraph, port: u16) -> R .route("/matrix", get(matrix_view)) .route("/graph", get(graph_view)) .route("/stats", get(stats_view)) + .route("/coverage", get(coverage_view)) + .route("/documents", get(documents_list)) + .route("/documents/{id}", get(document_detail)) + .route("/search", get(search_view)) + .route("/verification", get(verification_view)) + .route("/results", get(results_view)) + .route("/results/{run_id}", get(result_detail)) .with_state(state); let addr = format!("0.0.0.0:{port}"); @@ -103,58 +276,374 @@ fn type_color_map() -> HashMap { .collect() } +/// Return a colored badge `` for an artifact type. +/// +/// Uses the `type_color_map` hex color as text and computes a 12%-opacity +/// tinted background from it. +fn badge_for_type(type_name: &str) -> String { + let colors = type_color_map(); + let hex = colors + .get(type_name) + .map(|s| s.as_str()) + .unwrap_or("#5b2d9e"); + // Parse hex → rgb + let hex_digits = hex.trim_start_matches('#'); + let r = u8::from_str_radix(&hex_digits[0..2], 16).unwrap_or(91); + let g = u8::from_str_radix(&hex_digits[2..4], 16).unwrap_or(45); + let b = u8::from_str_radix(&hex_digits[4..6], 16).unwrap_or(158); + format!( + "{}", + html_escape(type_name) + ) +} + // ── CSS ────────────────────────────────────────────────────────────────── const CSS: &str = r#" -*{box-sizing:border-box;margin:0;padding:0} -body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; - color:#1a1a2e;background:#f8f9fa;line-height:1.6} -a{color:#3a86ff;text-decoration:none} -a:hover{text-decoration:underline} +/* ── Reset & base ─────────────────────────────────────────────── */ +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{ + --bg: #f5f5f7; + --surface:#fff; + --sidebar:#0f0f13; + --sidebar-hover:#1c1c24; + --sidebar-text:#9898a6; + --sidebar-active:#fff; + --text: #1d1d1f; + --text-secondary:#6e6e73; + --border: #e5e5ea; + --accent: #3a86ff; + --accent-hover:#2568d6; + --radius: 10px; + --radius-sm:6px; + --shadow: 0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04); + --shadow-md:0 4px 12px rgba(0,0,0,.06),0 1px 3px rgba(0,0,0,.04); + --mono: 'JetBrains Mono','Fira Code','SF Mono',Menlo,monospace; + --font: 'Atkinson Hyperlegible',system-ui,-apple-system,sans-serif; + --transition:180ms ease; +} +html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility} +body{font-family:var(--font);color:var(--text);background:var(--bg);line-height:1.6;font-size:15px} + +/* ── Links ────────────────────────────────────────────────────── */ +a{color:var(--accent);text-decoration:none;transition:color var(--transition)} +a:hover{color:var(--accent-hover)} +a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px} + +/* ── Shell layout ─────────────────────────────────────────────── */ .shell{display:flex;min-height:100vh} -nav{width:220px;background:#1a1a2e;color:#e0e0e0;padding:1.5rem 1rem;flex-shrink:0} -nav h1{font-size:1.2rem;color:#fff;margin-bottom:1.5rem;letter-spacing:.05em} -nav ul{list-style:none} -nav li{margin-bottom:.25rem} -nav a{display:block;padding:.45rem .75rem;border-radius:6px;color:#c0c0d0;font-size:.9rem} -nav a:hover,nav a.active{background:#2a2a4e;color:#fff;text-decoration:none} -main{flex:1;padding:2rem 2.5rem;max-width:1100px} -h2{font-size:1.35rem;margin-bottom:1rem;color:#1a1a2e} -h3{font-size:1.1rem;margin:1.25rem 0 .5rem;color:#333} -table{width:100%;border-collapse:collapse;margin-bottom:1.5rem} -th,td{text-align:left;padding:.5rem .75rem;border-bottom:1px solid #dee2e6} -th{background:#e9ecef;font-weight:600;font-size:.85rem;text-transform:uppercase;letter-spacing:.03em;color:#495057} -td{font-size:.9rem} -tr:hover td{background:#f1f3f5} -.badge{display:inline-block;padding:.15rem .5rem;border-radius:4px;font-size:.78rem;font-weight:600} -.badge-error{background:#ffe0e0;color:#c62828} -.badge-warn{background:#fff3cd;color:#856404} -.badge-info{background:#d1ecf1;color:#0c5460} -.badge-ok{background:#d4edda;color:#155724} -.badge-type{background:#e8e0f0;color:#4a148c} -.card{background:#fff;border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.04)} -.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem} -.stat-box{background:#fff;border:1px solid #dee2e6;border-radius:8px;padding:1rem;text-align:center} -.stat-box .number{font-size:2rem;font-weight:700;color:#3a86ff} -.stat-box .label{font-size:.85rem;color:#6c757d} -.link-pill{display:inline-block;padding:.1rem .4rem;border-radius:3px;font-size:.8rem;background:#e9ecef;margin:.1rem} -.form-row{display:flex;gap:.75rem;align-items:end;flex-wrap:wrap;margin-bottom:1rem} -.form-row label{font-size:.85rem;font-weight:600;color:#495057} -.form-row select,.form-row input{padding:.4rem .6rem;border:1px solid #ced4da;border-radius:4px;font-size:.9rem} -.form-row button{padding:.4rem 1rem;background:#3a86ff;color:#fff;border:none;border-radius:4px; - font-size:.9rem;cursor:pointer} -.form-row button:hover{background:#2a6fdf} -dl{margin:.5rem 0} -dt{font-weight:600;font-size:.85rem;color:#495057;margin-top:.5rem} -dd{margin-left:0;margin-bottom:.25rem} -.meta{color:#6c757d;font-size:.85rem} -.nav-icon{display:inline-block;width:1.1rem;text-align:center;margin-right:.3rem;font-size:.85rem} -.graph-container{border:1px solid #dee2e6;border-radius:8px;overflow:hidden;background:#fff;cursor:grab} + +/* ── Sidebar navigation ──────────────────────────────────────── */ +nav{width:232px;background:var(--sidebar);color:var(--sidebar-text); + padding:1.75rem 1rem;flex-shrink:0;display:flex;flex-direction:column; + position:sticky;top:0;height:100vh;overflow-y:auto; + border-right:1px solid rgba(255,255,255,.06)} +nav h1{font-size:1.05rem;font-weight:700;color:var(--sidebar-active); + margin-bottom:2rem;letter-spacing:.04em;padding:0 .75rem; + display:flex;align-items:center;gap:.5rem} +nav h1::before{content:'';display:inline-block;width:8px;height:8px; + border-radius:50%;background:var(--accent);flex-shrink:0} +nav ul{list-style:none;display:flex;flex-direction:column;gap:2px} +nav li{margin:0} +nav a{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-radius:var(--radius-sm); + color:var(--sidebar-text);font-size:.875rem;font-weight:500; + transition:all var(--transition)} +nav a:hover{background:var(--sidebar-hover);color:var(--sidebar-active);text-decoration:none} +nav a.active{background:rgba(58,134,255,.08);color:var(--sidebar-active);border-left:2px solid var(--accent);padding-left:calc(.75rem - 2px)} +nav a:focus-visible{outline:2px solid var(--accent);outline-offset:-2px} + +/* ── Main content ─────────────────────────────────────────────── */ +main{flex:1;padding:2.5rem 3rem;max-width:1400px;min-width:0} +main.htmx-swapping{opacity:.4;transition:opacity 150ms ease-out} +main.htmx-settling{opacity:1;transition:opacity 200ms ease-in} + +/* ── Loading bar ──────────────────────────────────────────────── */ +#loading-bar{position:fixed;top:0;left:0;width:0;height:2px;background:var(--accent); + z-index:9999;transition:none;pointer-events:none} +#loading-bar.active{width:85%;transition:width 8s cubic-bezier(.1,.05,.1,1)} +#loading-bar.done{width:100%;transition:width 100ms ease;opacity:0;transition:width 100ms ease,opacity 300ms ease 100ms} + +/* ── Typography ───────────────────────────────────────────────── */ +h2{font-size:1.4rem;font-weight:700;margin-bottom:1.25rem;color:var(--text);letter-spacing:-.01em;padding-bottom:.75rem;border-bottom:1px solid var(--border)} +h3{font-size:1.05rem;font-weight:600;margin:1.5rem 0 .75rem;color:var(--text)} +code,pre{font-family:var(--mono);font-size:.85em} +pre{background:#f1f1f3;padding:1rem;border-radius:var(--radius-sm);overflow-x:auto} + +/* ── Tables ───────────────────────────────────────────────────── */ +table{width:100%;border-collapse:collapse;margin-bottom:1.5rem;font-size:.9rem} +th,td{text-align:left;padding:.65rem .875rem} +th{font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.06em; + color:var(--text-secondary);border-bottom:2px solid var(--border);background:transparent} +td{border-bottom:1px solid var(--border)} +tbody tr{transition:background var(--transition)} +tbody tr:nth-child(even){background:rgba(0,0,0,.015)} +tbody tr:hover{background:rgba(58,134,255,.04)} +td a{font-family:var(--mono);font-size:.85rem;font-weight:500} + +/* ── Badges ───────────────────────────────────────────────────── */ +.badge{display:inline-flex;align-items:center;padding:.2rem .55rem;border-radius:5px; + font-size:.73rem;font-weight:600;letter-spacing:.02em;line-height:1.4;white-space:nowrap} +.badge-error{background:#fee;color:#c62828} +.badge-warn{background:#fff8e1;color:#8b6914} +.badge-info{background:#e8f4fd;color:#0c5a82} +.badge-ok{background:#e6f9ed;color:#15713a} +.badge-type{background:#f0ecf9;color:#5b2d9e;font-family:var(--mono);font-size:.72rem} + +/* ── Validation bar ──────────────────────────────────────────── */ +.validation-bar{padding:1rem 1.25rem;border-radius:var(--radius);margin-bottom:1.25rem;font-weight:600;font-size:.95rem} +.validation-bar.pass{background:linear-gradient(135deg,#e6f9ed,#d4f5e0);color:#15713a;border:1px solid #b8e8c8} +.validation-bar.fail{background:linear-gradient(135deg,#fee,#fdd);color:#c62828;border:1px solid #f4c7c3} + +/* ── Status progress bars ────────────────────────────────────── */ +.status-bar-row{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem;font-size:.85rem} +.status-bar-label{width:80px;text-align:right;font-weight:500;color:var(--text-secondary)} +.status-bar-track{flex:1;height:20px;background:#e5e5ea;border-radius:4px;overflow:hidden;position:relative} +.status-bar-fill{height:100%;border-radius:4px;transition:width .3s ease} +.status-bar-count{width:40px;font-variant-numeric:tabular-nums;color:var(--text-secondary)} + +/* ── Cards ────────────────────────────────────────────────────── */ +.card{background:var(--surface);border-radius:var(--radius);padding:1.5rem; + margin-bottom:1.25rem;box-shadow:var(--shadow);border:1px solid var(--border); + transition:box-shadow var(--transition)} + +/* ── Stat grid ────────────────────────────────────────────────── */ +.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:1rem;margin-bottom:1.75rem} +.stat-box{background:var(--surface);border-radius:var(--radius);padding:1.25rem 1rem;text-align:center; + box-shadow:var(--shadow);border:1px solid var(--border);transition:box-shadow var(--transition),transform var(--transition); + border-top:3px solid var(--border)} +.stat-box:hover{box-shadow:var(--shadow-md);transform:translateY(-1px)} +.stat-box .number{font-size:2rem;font-weight:800;letter-spacing:-.02em; + font-variant-numeric:tabular-nums;line-height:1.2} +.stat-box .label{font-size:.8rem;font-weight:500;color:var(--text-secondary);margin-top:.25rem; + text-transform:uppercase;letter-spacing:.04em} +.stat-blue{border-top-color:#3a86ff}.stat-blue .number{color:#3a86ff} +.stat-green{border-top-color:#15713a}.stat-green .number{color:#15713a} +.stat-orange{border-top-color:#e67e22}.stat-orange .number{color:#e67e22} +.stat-red{border-top-color:#c62828}.stat-red .number{color:#c62828} +.stat-amber{border-top-color:#b8860b}.stat-amber .number{color:#b8860b} +.stat-purple{border-top-color:#6f42c1}.stat-purple .number{color:#6f42c1} + +/* ── Link pills ───────────────────────────────────────────────── */ +.link-pill{display:inline-block;padding:.15rem .45rem;border-radius:4px; + font-size:.76rem;font-family:var(--mono);background:#f0f0f3; + color:var(--text-secondary);margin:.1rem;font-weight:500} + +/* ── Forms ────────────────────────────────────────────────────── */ +.form-row{display:flex;gap:1rem;align-items:end;flex-wrap:wrap;margin-bottom:1rem} +.form-row label{font-size:.8rem;font-weight:600;color:var(--text-secondary); + text-transform:uppercase;letter-spacing:.04em} +.form-row select,.form-row input[type="text"],.form-row input[type="search"], +.form-row input:not([type]),.form-row input[list]{ + padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm); + font-size:.875rem;font-family:var(--font);background:var(--surface);color:var(--text); + transition:border-color var(--transition),box-shadow var(--transition);appearance:none; + -webkit-appearance:none} +.form-row select{padding-right:2rem;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236e6e73' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat:no-repeat;background-position:right .75rem center} +.form-row input:focus,.form-row select:focus{ + outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(58,134,255,.15)} +.form-row input[type="range"]{padding:0;border:none;accent-color:var(--accent);width:100%} +.form-row input[type="range"]:focus{box-shadow:none} +.form-row button{padding:.5rem 1.25rem;background:var(--accent);color:#fff;border:none; + border-radius:var(--radius-sm);font-size:.875rem;font-weight:600; + font-family:var(--font);cursor:pointer;transition:all var(--transition); + box-shadow:0 1px 2px rgba(0,0,0,.08)} +.form-row button:hover{background:var(--accent-hover);box-shadow:0 2px 6px rgba(58,134,255,.25);transform:translateY(-1px)} +.form-row button:active{transform:translateY(0)} +.form-row button:focus-visible{outline:2px solid var(--accent);outline-offset:2px} + +/* ── Definition lists ─────────────────────────────────────────── */ +dl{margin:.75rem 0} +dt{font-weight:600;font-size:.8rem;color:var(--text-secondary);margin-top:.75rem; + text-transform:uppercase;letter-spacing:.04em} +dd{margin-left:0;margin-bottom:.25rem;margin-top:.2rem} + +/* ── Meta text ────────────────────────────────────────────────── */ +.meta{color:var(--text-secondary);font-size:.85rem} + +/* ── Nav icons & badges ───────────────────────────────────────── */ +.nav-icon{display:inline-flex;width:1.25rem;height:1.25rem;align-items:center;justify-content:center;flex-shrink:0;opacity:.5} +nav a:hover .nav-icon,nav a.active .nav-icon{opacity:.9} +.nav-label{display:flex;align-items:center;gap:.5rem;flex:1;min-width:0} +.nav-badge{font-size:.65rem;font-weight:700;padding:.1rem .4rem;border-radius:4px; + background:rgba(255,255,255,.08);color:rgba(255,255,255,.4);margin-left:auto;flex-shrink:0} +.nav-badge-error{background:rgba(220,53,69,.2);color:#ff6b7a} +nav .nav-divider{height:1px;background:rgba(255,255,255,.06);margin:.75rem .75rem} + +/* ── Context bar ─────────────────────────────────────────────── */ +.context-bar{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;margin:-2.5rem -3rem 1.5rem; + background:var(--surface);border-bottom:1px solid var(--border);font-size:.78rem;color:var(--text-secondary); + flex-wrap:wrap} +.context-bar .ctx-project{font-weight:700;color:var(--text);font-size:.82rem} +.context-bar .ctx-sep{opacity:.25} +.context-bar .ctx-git{font-family:var(--mono);font-size:.72rem;padding:.15rem .4rem;border-radius:4px; + background:rgba(58,134,255,.08);color:var(--accent)} +.context-bar .ctx-dirty{font-family:var(--mono);font-size:.68rem;padding:.15rem .4rem;border-radius:4px; + background:rgba(220,53,69,.1);color:#c62828} +.context-bar .ctx-clean{font-family:var(--mono);font-size:.68rem;padding:.15rem .4rem;border-radius:4px; + background:rgba(21,113,58,.1);color:#15713a} +.context-bar .ctx-time{margin-left:auto;opacity:.6} +.ctx-switcher{position:relative;display:inline-flex;align-items:center} +.ctx-switcher-details{position:relative} +.ctx-switcher-details summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center; + padding:.15rem .35rem;border-radius:4px;opacity:.5;transition:opacity .15s} +.ctx-switcher-details summary:hover{opacity:1;background:rgba(255,255,255,.06)} +.ctx-switcher-details summary::-webkit-details-marker{display:none} +.ctx-switcher-dropdown{position:absolute;top:100%;left:0;z-index:100;margin-top:.35rem; + background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm); + padding:.5rem;min-width:280px;box-shadow:0 8px 24px rgba(0,0,0,.35)} +.ctx-switcher-item{padding:.5rem .65rem;border-radius:4px} +.ctx-switcher-item:hover{background:rgba(255,255,255,.04)} +.ctx-switcher-item .ctx-switcher-name{display:block;font-weight:600;font-size:.8rem;color:var(--text);margin-bottom:.2rem} +.ctx-switcher-item .ctx-switcher-cmd{display:block;font-size:.7rem;color:var(--text-secondary); + padding:.2rem .4rem;background:rgba(255,255,255,.04);border-radius:3px; + font-family:var(--mono);user-select:all;cursor:text} + +/* ── Footer ──────────────────────────────────────────────────── */ +.footer{padding:2rem 0 1rem;text-align:center;font-size:.75rem;color:var(--text-secondary); + border-top:1px solid var(--border);margin-top:3rem} + +/* ── Verification ────────────────────────────────────────────── */ +.ver-level{margin-bottom:1.5rem} +.ver-level-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem} +.ver-level-title{font-size:1rem;font-weight:600;color:var(--text)} +.ver-level-arrow{color:var(--text-secondary);font-size:.85rem} +details.ver-row>summary{cursor:pointer;list-style:none;padding:.6rem .875rem;border-bottom:1px solid var(--border); + display:flex;align-items:center;gap:.75rem;transition:background var(--transition)} +details.ver-row>summary::-webkit-details-marker{display:none} +details.ver-row>summary:hover{background:rgba(58,134,255,.04)} +details.ver-row[open]>summary{background:rgba(58,134,255,.04);border-bottom-color:var(--accent)} +details.ver-row>.ver-detail{padding:1rem 1.5rem;background:rgba(0,0,0,.01);border-bottom:1px solid var(--border)} +.ver-chevron{transition:transform var(--transition);display:inline-flex;opacity:.4} +details.ver-row[open] .ver-chevron{transform:rotate(90deg)} +.ver-steps{width:100%;border-collapse:collapse;font-size:.85rem;margin-top:.5rem} +.ver-steps th{text-align:left;font-weight:600;font-size:.72rem;text-transform:uppercase; + letter-spacing:.04em;color:var(--text-secondary);padding:.4rem .5rem;border-bottom:1px solid var(--border)} +.ver-steps td{padding:.4rem .5rem;border-bottom:1px solid rgba(0,0,0,.04);vertical-align:top} +.method-badge{display:inline-flex;padding:.15rem .5rem;border-radius:4px;font-size:.72rem;font-weight:600; + background:#e8f4fd;color:#0c5a82} + +/* ── Results ─────────────────────────────────────────────────── */ +.result-pass{color:#15713a}.result-fail{color:#c62828}.result-skip{color:#6e6e73} +.result-error{color:#e67e22}.result-blocked{color:#8b6914} +.result-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:.35rem} +.result-dot-pass{background:#15713a}.result-dot-fail{background:#c62828} +.result-dot-skip{background:#c5c5cd}.result-dot-error{background:#e67e22}.result-dot-blocked{background:#b8860b} + +/* ── Detail actions ──────────────────────────────────────────── */ +.detail-actions{display:flex;gap:.75rem;align-items:center;margin-top:1rem} +.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem 1rem;border-radius:var(--radius-sm); + font-size:.85rem;font-weight:600;font-family:var(--font);text-decoration:none; + transition:all var(--transition);cursor:pointer;border:none} +.btn-primary{background:var(--accent);color:#fff;box-shadow:0 1px 2px rgba(0,0,0,.08)} +.btn-primary:hover{background:var(--accent-hover);transform:translateY(-1px);color:#fff;text-decoration:none} +.btn-secondary{background:transparent;color:var(--text-secondary);border:1px solid var(--border)} +.btn-secondary:hover{background:rgba(0,0,0,.03);color:var(--text);text-decoration:none} + +/* ── Graph ────────────────────────────────────────────────────── */ +.graph-container{border-radius:var(--radius);overflow:hidden;background:#fafbfc;cursor:grab; + height:calc(100vh - 280px);min-height:400px;position:relative;border:1px solid var(--border)} .graph-container:active{cursor:grabbing} -.graph-container svg{display:block;width:100%;height:auto} -.filter-grid{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:.75rem} -.filter-grid label{font-size:.82rem;display:flex;align-items:center;gap:.25rem} -.filter-grid input[type="checkbox"]{margin:0} +.graph-container svg{display:block;width:100%;height:100%;position:absolute;top:0;left:0} +.graph-controls{position:absolute;top:.75rem;right:.75rem;display:flex;flex-direction:column;gap:.35rem;z-index:10} +.graph-controls button{width:34px;height:34px;border:1px solid var(--border);border-radius:var(--radius-sm); + background:var(--surface);font-size:1rem;cursor:pointer;display:flex;align-items:center; + justify-content:center;box-shadow:var(--shadow);color:var(--text); + transition:all var(--transition)} +.graph-controls button:hover{background:#f0f0f3;box-shadow:var(--shadow-md)} +.graph-controls button:focus-visible{outline:2px solid var(--accent);outline-offset:2px} +.graph-legend{display:flex;flex-wrap:wrap;gap:.75rem;padding:.75rem 0 .25rem;font-size:.82rem} +.graph-legend-item{display:flex;align-items:center;gap:.35rem;color:var(--text-secondary)} +.graph-legend-swatch{width:12px;height:12px;border-radius:3px;flex-shrink:0} + +/* ── Filter grid ──────────────────────────────────────────────── */ +.filter-grid{display:flex;flex-wrap:wrap;gap:.6rem;margin-bottom:.75rem} +.filter-grid label{font-size:.8rem;display:flex;align-items:center;gap:.3rem; + color:var(--text-secondary);cursor:pointer;padding:.2rem .45rem; + border-radius:4px;transition:background var(--transition); + text-transform:none;letter-spacing:0;font-weight:500} +.filter-grid label:hover{background:rgba(58,134,255,.06)} +.filter-grid input[type="checkbox"]{margin:0;accent-color:var(--accent);width:14px;height:14px; + cursor:pointer;border-radius:3px} + +/* ── Document styles ──────────────────────────────────────────── */ +.doc-body{line-height:1.8;font-size:.95rem} +.doc-body h1{font-size:1.4rem;font-weight:700;margin:2rem 0 .75rem;color:var(--text); + border-bottom:2px solid var(--border);padding-bottom:.5rem} +.doc-body h2{font-size:1.2rem;font-weight:600;margin:1.5rem 0 .5rem;color:var(--text)} +.doc-body h3{font-size:1.05rem;font-weight:600;margin:1.25rem 0 .4rem;color:var(--text-secondary)} +.doc-body p{margin:.5rem 0} +.doc-body ul{margin:.5rem 0 .5rem 1.5rem} +.doc-body li{margin:.2rem 0} +.artifact-ref{display:inline-flex;align-items:center;padding:.15rem .5rem;border-radius:5px; + font-size:.8rem;font-weight:600;font-family:var(--mono);background:#edf2ff; + color:#3a63c7;cursor:pointer;text-decoration:none; + border:1px solid #d4def5;transition:all var(--transition)} +.artifact-ref:hover{background:#d4def5;text-decoration:none;transform:translateY(-1px);box-shadow:0 2px 4px rgba(0,0,0,.06)} +.artifact-ref.broken{background:#fde8e8;color:#c62828;border-color:#f4c7c3;cursor:default} +.artifact-ref.broken:hover{transform:none;box-shadow:none} +.doc-glossary{font-size:.9rem} +.doc-glossary dt{font-weight:600;color:var(--text)} +.doc-glossary dd{margin:0 0 .5rem 1rem;color:var(--text-secondary)} +.doc-toc{font-size:.88rem;background:var(--surface);border:1px solid var(--border); + border-radius:var(--radius);padding:1rem 1.25rem;margin-bottom:1.25rem; + box-shadow:var(--shadow)} +.doc-toc strong{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary)} +.doc-toc ul{list-style:none;margin:.5rem 0 0;padding:0} +.doc-toc li{margin:.2rem 0;color:var(--text-secondary)} +.doc-toc .toc-h2{padding-left:0} +.doc-toc .toc-h3{padding-left:1.25rem} +.doc-toc .toc-h4{padding-left:2.5rem} +.doc-meta{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-bottom:1.25rem} + +/* ── Scrollbar (subtle) ───────────────────────────────────────── */ +::-webkit-scrollbar{width:6px;height:6px} +::-webkit-scrollbar-track{background:transparent} +::-webkit-scrollbar-thumb{background:#c5c5cd;border-radius:3px} +::-webkit-scrollbar-thumb:hover{background:#a0a0aa} + +/* ── Selection ────────────────────────────────────────────────── */ +::selection{background:rgba(58,134,255,.18)} + +/* ── Cmd+K search modal ──────────────────────────────────────── */ +.cmd-k-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(4px); + z-index:10000;display:none;align-items:flex-start;justify-content:center;padding-top:min(20vh,160px)} +.cmd-k-overlay.open{display:flex} +.cmd-k-modal{background:var(--sidebar);border-radius:12px;width:100%;max-width:600px; + box-shadow:0 16px 70px rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.08); + overflow:hidden;display:flex;flex-direction:column;max-height:min(70vh,520px)} +.cmd-k-input{width:100%;padding:.875rem 1rem .875rem 2.75rem;font-size:1rem;font-family:var(--font); + background:transparent;border:none;border-bottom:1px solid rgba(255,255,255,.08); + color:#fff;outline:none;caret-color:var(--accent)} +.cmd-k-input::placeholder{color:rgba(255,255,255,.35)} +.cmd-k-icon{position:absolute;left:1rem;top:.95rem;color:rgba(255,255,255,.35);pointer-events:none; + font-size:.95rem} +.cmd-k-head{position:relative} +.cmd-k-results{overflow-y:auto;padding:.5rem 0;flex:1} +.cmd-k-empty{padding:1.5rem 1rem;text-align:center;color:rgba(255,255,255,.35);font-size:.9rem} +.cmd-k-group{padding:0 .5rem} +.cmd-k-group-label{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em; + color:rgba(255,255,255,.3);padding:.5rem .625rem .25rem} +.cmd-k-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .625rem;border-radius:var(--radius-sm); + cursor:pointer;color:var(--sidebar-text);font-size:.88rem;transition:background 80ms ease} +.cmd-k-item:hover,.cmd-k-item.active{background:rgba(255,255,255,.08);color:#fff} +.cmd-k-item-icon{width:1.5rem;height:1.5rem;border-radius:4px;display:flex;align-items:center; + justify-content:center;font-size:.7rem;flex-shrink:0;background:rgba(255,255,255,.06);color:rgba(255,255,255,.5)} +.cmd-k-item-body{flex:1;min-width:0} +.cmd-k-item-title{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.cmd-k-item-title mark{background:transparent;color:var(--accent);font-weight:700} +.cmd-k-item-meta{font-size:.75rem;color:rgba(255,255,255,.35);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.cmd-k-item-meta mark{background:transparent;color:var(--accent);font-weight:600} +.cmd-k-item-field{font-size:.65rem;padding:.1rem .35rem;border-radius:3px; + background:rgba(255,255,255,.06);color:rgba(255,255,255,.4);white-space:nowrap;flex-shrink:0} +.cmd-k-kbd{display:inline-flex;align-items:center;gap:.2rem;font-size:.7rem;font-family:var(--mono); + padding:.15rem .4rem;border-radius:4px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.4); + border:1px solid rgba(255,255,255,.06)} +.nav-search-hint{display:flex;align-items:center;justify-content:space-between;padding:.5rem .75rem; + margin-top:auto;border-top:1px solid rgba(255,255,255,.06);padding-top:1rem; + color:var(--sidebar-text);font-size:.82rem;cursor:pointer;border-radius:var(--radius-sm); + transition:all var(--transition)} +.nav-search-hint:hover{background:var(--sidebar-hover);color:var(--sidebar-active)} "#; // ── Pan/zoom JS ────────────────────────────────────────────────────────── @@ -162,8 +651,45 @@ dd{margin-left:0;margin-bottom:.25rem} const GRAPH_JS: &str = r#" "#; +// ── Cmd+K search JS ────────────────────────────────────────────────────── + +const SEARCH_JS: &str = r#" + +"#; + // ── Layout ─────────────────────────────────────────────────────────────── -fn page_layout(content: &str) -> Html { +fn page_layout(content: &str, state: &AppState) -> Html { + let artifact_count = state.store.len(); + let diagnostics = validate::validate(&state.store, &state.schema, &state.graph); + let error_count = diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .count(); + let error_badge = if error_count > 0 { + format!( + "{error_count}" + ) + } else { + "OK".to_string() + }; + let doc_badge = if !state.doc_store.is_empty() { + format!( + "{}", + state.doc_store.len() + ) + } else { + String::new() + }; + let result_badge = if !state.result_store.is_empty() { + format!( + "{}", + state.result_store.len() + ) + } else { + String::new() + }; + let version = env!("CARGO_PKG_VERSION"); + + // Context bar + let ctx = &state.context; + let git_html = if let Some(ref git) = ctx.git { + let status = if git.is_dirty { + format!( + "{} uncommitted", + git.dirty_count + ) + } else { + "clean".to_string() + }; + format!( + "/\ + {branch}@{commit}\ + {status}", + branch = html_escape(&git.branch), + commit = html_escape(&git.commit_short), + ) + } else { + String::new() + }; + // Project switcher: show siblings as a dropdown if available + let switcher_html = if ctx.siblings.is_empty() { + String::new() + } else { + let mut s = String::from( + "\ + \ + \ + ", + ); + for sib in &ctx.siblings { + s.push_str(&format!( + "\ + {}\ + rivet -p {} serve -P {}\ + ", + html_escape(&sib.name), + html_escape(&sib.rel_path), + ctx.port, + )); + } + s.push_str(""); + s + }; + let context_bar = format!( + "\ + {project}{switcher_html}\ + /\ + {path}\ + {git_html}\ + Loaded {loaded_at}\ + ", + project = html_escape(&ctx.project_name), + path = html_escape(&ctx.project_path), + loaded_at = html_escape(&ctx.loaded_at), + ); Html(format!( r##" @@ -211,26 +1031,54 @@ fn page_layout(content: &str) -> Html { Rivet Dashboard + + + + Rivet - ■ Overview - ♦ Artifacts - ✓ Validation - ▦ Matrix - ● Graph + Overview + Artifacts{artifact_count} + Validation{error_badge} + + Matrix + Coverage + Graph + Documents{doc_badge} + + Verification + Results{result_badge} + + Search + ⌘K + - + +{context_bar} {content} + + + + + 🔍 + + + + Type to search artifacts and documents + + + {GRAPH_JS} +{SEARCH_JS}
rivet -p {} serve -P {}