jugar_probar/brick/
mod.rs

1//! Brick Architecture: Tests ARE the Interface (PROBAR-SPEC-009)
2//!
3//! This module implements the core Brick abstraction where UI components
4//! are defined by their test assertions, not by their implementation.
5//!
6//! # Design Philosophy
7//!
8//! The Brick Architecture inverts the traditional UI/Test relationship:
9//!
10//! ```text
11//! Traditional: Widget → Tests
12//! Brick:       Brick(Assertions) → Widget + Tests
13//! ```
14//!
15//! A `Brick` defines:
16//! 1. **Assertions**: What must be true (contrast ratio, visibility, latency)
17//! 2. **Budget**: Performance envelope (max render time in ms)
18//! 3. **Events**: State transitions that trigger assertions
19//!
20//! # Zero-Artifact Architecture (PROBAR-SPEC-009-P7)
21//!
22//! The following brick types generate all web artifacts from Rust:
23//!
24//! - [`WorkerBrick`] - Web Worker JavaScript and Rust web_sys bindings
25//! - [`EventBrick`] - DOM event handlers
26//! - [`AudioBrick`] - AudioWorklet processor code
27//!
28//! # Popperian Falsification
29//!
30//! Each assertion is a falsifiable hypothesis. If ANY assertion fails,
31//! the brick is falsified and cannot render.
32//!
33//! # Example
34//!
35//! ```rust,ignore
36//! use probar::brick::{Brick, BrickAssertion, BrickBudget};
37//!
38//! #[derive(Brick)]
39//! #[brick(
40//!     html = "div.transcription",
41//!     budget_ms = 100,
42//!     assertions = [text_visible, contrast_ratio(4.5)]
43//! )]
44//! struct TranscriptionBrick {
45//!     text: String,
46//!     is_final: bool,
47//! }
48//! ```
49//!
50//! # References
51//!
52//! - Popper, K. (1959). The Logic of Scientific Discovery
53//! - Beizer, B. (1990). Software Testing Techniques
54//! - PROBAR-SPEC-009: Bug Hunting Probador
55
56// Zero-Artifact submodules (PROBAR-SPEC-009-P7)
57pub mod audio;
58pub mod compute;
59pub mod deterministic;
60pub mod distributed;
61pub mod event;
62pub mod pipeline;
63pub mod tui;
64pub mod web_sys_gen;
65pub mod widget;
66pub mod worker;
67
68// Re-export submodule types
69pub use audio::{AudioBrick, AudioParam, RingBufferConfig};
70pub use compute::{
71    ComputeBrick, ElementwiseOp, ReduceKind, TensorBinding, TensorType, TileOp, TileStrategy,
72};
73pub use deterministic::{
74    BrickHistory, BrickState, DeterministicBrick, DeterministicClock, DeterministicRng,
75    ExecutionTrace, GuardSeverity, GuardViolation, GuardedBrick, InvariantGuard, StateValue,
76};
77pub use distributed::{
78    Backend, BackendSelector, BrickCoordinator, BrickDataTracker, BrickInput, BrickMessage,
79    BrickOutput, DataLocation, DistributedBrick, ExecutionMetrics, MultiBrickExecutor,
80    SchedulerStats, Subscription, TaskSpec, WorkStealingScheduler, WorkStealingTask, WorkerId,
81    WorkerQueue, WorkerStats,
82};
83pub use event::{EventBinding, EventBrick, EventHandler, EventType};
84pub use pipeline::{
85    AuditEntry, BrickPipeline, BrickStage, Checkpoint, PipelineAuditCollector, PipelineContext,
86    PipelineData, PipelineError, PipelineMetadata, PipelineResult, PrivacyTier, StageTrace,
87    ValidationLevel, ValidationMessage, ValidationResult,
88};
89pub use tui::{
90    AnalyzerBrick, CielabColor, CollectorBrick, CollectorError, PanelBrick, PanelId, PanelState,
91    RingBuffer,
92};
93pub use web_sys_gen::{
94    get_base_url, BlobUrl, CustomEventDispatcher, EventDetail, FetchClient, GeneratedWebSys,
95    GenerationMetadata, PerformanceTiming, WebSysError, GENERATION_METADATA,
96};
97pub use widget::{
98    commands_to_gpu_instances, Canvas, Constraints, CornerRadius, DrawCommand, Event, GpuInstance,
99    LayoutResult, LineCap, LineJoin, Modifiers, RecordingCanvas, Rect, RenderMetrics, Size,
100    StrokeStyle, TextStyle, Transform2D, Widget, WidgetColor, WidgetExt, WidgetMouseButton,
101    WidgetPoint,
102};
103pub use worker::{
104    BrickWorkerMessage, BrickWorkerMessageDirection, FieldType, MessageField, WorkerBrick,
105    WorkerTransition,
106};
107
108use std::time::Duration;
109
110/// Brick assertion that must be verified at runtime.
111///
112/// Assertions are falsifiable hypotheses about the UI state.
113/// If any assertion fails, the brick is falsified.
114#[derive(Debug, Clone, PartialEq)]
115pub enum BrickAssertion {
116    /// Text content must be visible (not hidden, not zero-opacity)
117    TextVisible,
118
119    /// WCAG 2.1 AA contrast ratio requirement (4.5:1 for normal text)
120    ContrastRatio(f32),
121
122    /// Maximum render latency in milliseconds
123    MaxLatencyMs(u32),
124
125    /// Element must be present in DOM
126    ElementPresent(String),
127
128    /// Element must be focusable for accessibility
129    Focusable,
130
131    /// Custom assertion with name and validation function ID
132    Custom {
133        /// Assertion name for error reporting
134        name: String,
135        /// Validation function identifier
136        validator_id: u64,
137    },
138}
139
140impl BrickAssertion {
141    /// Create a text visibility assertion
142    #[must_use]
143    pub const fn text_visible() -> Self {
144        Self::TextVisible
145    }
146
147    /// Create a contrast ratio assertion (WCAG 2.1 AA)
148    #[must_use]
149    pub const fn contrast_ratio(ratio: f32) -> Self {
150        Self::ContrastRatio(ratio)
151    }
152
153    /// Create a max latency assertion
154    #[must_use]
155    pub const fn max_latency_ms(ms: u32) -> Self {
156        Self::MaxLatencyMs(ms)
157    }
158
159    /// Create an element presence assertion
160    #[must_use]
161    pub fn element_present(selector: impl Into<String>) -> Self {
162        Self::ElementPresent(selector.into())
163    }
164}
165
166/// Performance budget for a brick.
167///
168/// Budgets are enforced at runtime. Exceeding the budget triggers
169/// a Jidoka (stop-the-line) alert.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct BrickBudget {
172    /// Maximum time for measure phase
173    pub measure_ms: u32,
174    /// Maximum time for layout phase
175    pub layout_ms: u32,
176    /// Maximum time for paint phase
177    pub paint_ms: u32,
178    /// Total budget (may be less than sum of phases)
179    pub total_ms: u32,
180}
181
182impl BrickBudget {
183    /// Create a budget with equal distribution across phases
184    #[must_use]
185    pub const fn uniform(total_ms: u32) -> Self {
186        let phase_ms = total_ms / 3;
187        Self {
188            measure_ms: phase_ms,
189            layout_ms: phase_ms,
190            paint_ms: phase_ms,
191            total_ms,
192        }
193    }
194
195    /// Create a custom budget with specified phase limits
196    #[must_use]
197    pub const fn new(measure_ms: u32, layout_ms: u32, paint_ms: u32) -> Self {
198        Self {
199            measure_ms,
200            layout_ms,
201            paint_ms,
202            total_ms: measure_ms + layout_ms + paint_ms,
203        }
204    }
205
206    /// Convert to Duration
207    #[must_use]
208    pub const fn as_duration(&self) -> Duration {
209        Duration::from_millis(self.total_ms as u64)
210    }
211}
212
213impl Default for BrickBudget {
214    fn default() -> Self {
215        // Default: 16ms total for 60fps
216        Self::uniform(16)
217    }
218}
219
220/// Result of verifying brick assertions
221#[derive(Debug, Clone)]
222pub struct BrickVerification {
223    /// All assertions that passed
224    pub passed: Vec<BrickAssertion>,
225    /// All assertions that failed with reasons
226    pub failed: Vec<(BrickAssertion, String)>,
227    /// Time taken to verify
228    pub verification_time: Duration,
229}
230
231impl BrickVerification {
232    /// Check if all assertions passed
233    #[must_use]
234    pub fn is_valid(&self) -> bool {
235        self.failed.is_empty()
236    }
237
238    /// Get the falsification score (passed / total)
239    #[must_use]
240    pub fn score(&self) -> f32 {
241        let total = self.passed.len() + self.failed.len();
242        if total == 0 {
243            1.0
244        } else {
245            self.passed.len() as f32 / total as f32
246        }
247    }
248}
249
250/// Budget violation report
251#[derive(Debug, Clone)]
252pub struct BudgetViolation {
253    /// Name of the brick that violated
254    pub brick_name: String,
255    /// Budget that was exceeded
256    pub budget: BrickBudget,
257    /// Actual time taken
258    pub actual: Duration,
259    /// Phase that exceeded (if known)
260    pub phase: Option<BrickPhase>,
261}
262
263/// Rendering phase for budget tracking
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum BrickPhase {
266    /// Measure phase (compute intrinsic size)
267    Measure,
268    /// Layout phase (position children)
269    Layout,
270    /// Paint phase (generate draw commands)
271    Paint,
272}
273
274/// Core Brick trait - the foundation of the Brick Architecture.
275///
276/// All UI components implement this trait. The trait defines:
277/// 1. Assertions that must pass for the brick to be valid
278/// 2. Performance budget that must not be exceeded
279/// 3. HTML/CSS generation for rendering targets
280///
281/// # Trait Bound
282///
283/// Presentar's `Widget` trait requires `Brick`:
284/// ```rust,ignore
285/// pub trait Widget: Brick + Send + Sync { ... }
286/// ```
287///
288/// This ensures every widget has verifiable assertions and budgets.
289pub trait Brick: Send + Sync {
290    /// Get the brick's unique type name
291    fn brick_name(&self) -> &'static str;
292
293    /// Get all assertions for this brick
294    fn assertions(&self) -> &[BrickAssertion];
295
296    /// Get the performance budget
297    fn budget(&self) -> BrickBudget;
298
299    /// Verify all assertions against current state
300    ///
301    /// Returns a verification result with passed/failed assertions.
302    fn verify(&self) -> BrickVerification;
303
304    /// Generate HTML for this brick (WASM target)
305    ///
306    /// Returns the HTML string that represents this brick.
307    /// Must be deterministic (same state → same output).
308    fn to_html(&self) -> String;
309
310    /// Generate CSS for this brick (WASM target)
311    ///
312    /// Returns the CSS rules for styling this brick.
313    /// Must be deterministic and scoped to avoid conflicts.
314    fn to_css(&self) -> String;
315
316    /// Get the test ID for DOM queries
317    fn test_id(&self) -> Option<&str> {
318        None
319    }
320
321    /// Check if this brick can be rendered (all assertions pass)
322    fn can_render(&self) -> bool {
323        self.verify().is_valid()
324    }
325}
326
327/// Yuan Gate: Zero-swallow error handling for bricks
328///
329/// Named after the Yuan dynasty's strict quality standards.
330/// Every error must be explicitly handled - no silent drops.
331#[derive(Debug, Clone)]
332pub enum BrickError {
333    /// Assertion failed during verification
334    AssertionFailed {
335        /// The assertion that failed
336        assertion: BrickAssertion,
337        /// Reason for failure
338        reason: String,
339    },
340
341    /// Budget exceeded during rendering
342    BudgetExceeded(BudgetViolation),
343
344    /// Invalid state transition
345    InvalidTransition {
346        /// Current state
347        from: String,
348        /// Attempted target state
349        to: String,
350        /// Reason transition is invalid
351        reason: String,
352    },
353
354    /// Missing required child brick
355    MissingChild {
356        /// Expected child brick name
357        expected: String,
358    },
359
360    /// HTML generation failed
361    HtmlGenerationFailed {
362        /// Reason for failure
363        reason: String,
364    },
365}
366
367impl std::fmt::Display for BrickError {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        match self {
370            Self::AssertionFailed { assertion, reason } => {
371                write!(f, "Assertion {assertion:?} failed: {reason}")
372            }
373            Self::BudgetExceeded(violation) => {
374                write!(
375                    f,
376                    "Budget exceeded for {}: {:?} > {:?}",
377                    violation.brick_name, violation.actual, violation.budget.total_ms
378                )
379            }
380            Self::InvalidTransition { from, to, reason } => {
381                write!(f, "Invalid transition {from} -> {to}: {reason}")
382            }
383            Self::MissingChild { expected } => {
384                write!(f, "Missing required child brick: {expected}")
385            }
386            Self::HtmlGenerationFailed { reason } => {
387                write!(f, "HTML generation failed: {reason}")
388            }
389        }
390    }
391}
392
393impl std::error::Error for BrickError {}
394
395/// Result type for brick operations
396pub type BrickResult<T> = Result<T, BrickError>;
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    struct TestBrick {
403        text: String,
404        visible: bool,
405    }
406
407    impl Brick for TestBrick {
408        fn brick_name(&self) -> &'static str {
409            "TestBrick"
410        }
411
412        fn assertions(&self) -> &[BrickAssertion] {
413            &[
414                BrickAssertion::TextVisible,
415                BrickAssertion::ContrastRatio(4.5),
416            ]
417        }
418
419        fn budget(&self) -> BrickBudget {
420            BrickBudget::uniform(16)
421        }
422
423        fn verify(&self) -> BrickVerification {
424            let mut passed = Vec::new();
425            let mut failed = Vec::new();
426
427            for assertion in self.assertions() {
428                match assertion {
429                    BrickAssertion::TextVisible => {
430                        if self.visible && !self.text.is_empty() {
431                            passed.push(assertion.clone());
432                        } else {
433                            failed.push((assertion.clone(), "Text not visible".into()));
434                        }
435                    }
436                    BrickAssertion::ContrastRatio(_) => {
437                        // Assume pass for test
438                        passed.push(assertion.clone());
439                    }
440                    _ => passed.push(assertion.clone()),
441                }
442            }
443
444            BrickVerification {
445                passed,
446                failed,
447                verification_time: Duration::from_micros(100),
448            }
449        }
450
451        fn to_html(&self) -> String {
452            format!(r#"<div class="test-brick">{}</div>"#, self.text)
453        }
454
455        fn to_css(&self) -> String {
456            ".test-brick { color: #fff; background: #000; }".into()
457        }
458    }
459
460    #[test]
461    fn test_brick_verification_passes() {
462        let brick = TestBrick {
463            text: "Hello".into(),
464            visible: true,
465        };
466
467        let result = brick.verify();
468        assert!(result.is_valid());
469        assert_eq!(result.score(), 1.0);
470    }
471
472    #[test]
473    fn test_brick_verification_fails() {
474        let brick = TestBrick {
475            text: String::new(),
476            visible: false,
477        };
478
479        let result = brick.verify();
480        assert!(!result.is_valid());
481        assert!(result.score() < 1.0);
482    }
483
484    #[test]
485    fn test_budget_uniform() {
486        let budget = BrickBudget::uniform(30);
487        assert_eq!(budget.total_ms, 30);
488        assert_eq!(budget.measure_ms, 10);
489    }
490
491    #[test]
492    fn test_can_render_valid() {
493        let brick = TestBrick {
494            text: "Hello".into(),
495            visible: true,
496        };
497        assert!(brick.can_render());
498    }
499
500    #[test]
501    fn test_can_render_invalid() {
502        let brick = TestBrick {
503            text: String::new(),
504            visible: false,
505        };
506        assert!(!brick.can_render());
507    }
508
509    #[test]
510    fn test_brick_assertion_constructors() {
511        let text_vis = BrickAssertion::text_visible();
512        assert!(matches!(text_vis, BrickAssertion::TextVisible));
513
514        let contrast = BrickAssertion::contrast_ratio(4.5);
515        assert!(
516            matches!(contrast, BrickAssertion::ContrastRatio(r) if (r - 4.5).abs() < f32::EPSILON)
517        );
518
519        let latency = BrickAssertion::max_latency_ms(100);
520        assert!(matches!(latency, BrickAssertion::MaxLatencyMs(100)));
521
522        let elem = BrickAssertion::element_present("div.test");
523        assert!(matches!(elem, BrickAssertion::ElementPresent(s) if s == "div.test"));
524    }
525
526    #[test]
527    fn test_brick_assertion_focusable() {
528        let focusable = BrickAssertion::Focusable;
529        assert!(matches!(focusable, BrickAssertion::Focusable));
530    }
531
532    #[test]
533    fn test_brick_assertion_custom() {
534        let custom = BrickAssertion::Custom {
535            name: "test_assertion".into(),
536            validator_id: 42,
537        };
538        match custom {
539            BrickAssertion::Custom { name, validator_id } => {
540                assert_eq!(name, "test_assertion");
541                assert_eq!(validator_id, 42);
542            }
543            _ => panic!("Expected Custom variant"),
544        }
545    }
546
547    #[test]
548    fn test_budget_new() {
549        let budget = BrickBudget::new(5, 10, 15);
550        assert_eq!(budget.measure_ms, 5);
551        assert_eq!(budget.layout_ms, 10);
552        assert_eq!(budget.paint_ms, 15);
553        assert_eq!(budget.total_ms, 30);
554    }
555
556    #[test]
557    fn test_budget_default() {
558        let budget = BrickBudget::default();
559        assert_eq!(budget.total_ms, 16); // 60fps
560    }
561
562    #[test]
563    fn test_budget_as_duration() {
564        let budget = BrickBudget::uniform(100);
565        let duration = budget.as_duration();
566        assert_eq!(duration, Duration::from_millis(100));
567    }
568
569    #[test]
570    fn test_verification_score_empty() {
571        let verification = BrickVerification {
572            passed: vec![],
573            failed: vec![],
574            verification_time: Duration::from_micros(10),
575        };
576        assert_eq!(verification.score(), 1.0); // Empty = perfect score
577        assert!(verification.is_valid());
578    }
579
580    #[test]
581    fn test_verification_score_partial() {
582        let verification = BrickVerification {
583            passed: vec![BrickAssertion::TextVisible],
584            failed: vec![(BrickAssertion::Focusable, "Not focusable".into())],
585            verification_time: Duration::from_micros(10),
586        };
587        assert_eq!(verification.score(), 0.5);
588        assert!(!verification.is_valid());
589    }
590
591    #[test]
592    fn test_brick_phase_variants() {
593        let measure = BrickPhase::Measure;
594        let layout = BrickPhase::Layout;
595        let paint = BrickPhase::Paint;
596
597        assert!(matches!(measure, BrickPhase::Measure));
598        assert!(matches!(layout, BrickPhase::Layout));
599        assert!(matches!(paint, BrickPhase::Paint));
600        assert_ne!(measure, layout);
601    }
602
603    #[test]
604    fn test_budget_violation() {
605        let violation = BudgetViolation {
606            brick_name: "TestBrick".into(),
607            budget: BrickBudget::uniform(16),
608            actual: Duration::from_millis(50),
609            phase: Some(BrickPhase::Paint),
610        };
611        assert_eq!(violation.brick_name, "TestBrick");
612        assert_eq!(violation.phase, Some(BrickPhase::Paint));
613    }
614
615    #[test]
616    fn test_brick_to_html_css() {
617        let brick = TestBrick {
618            text: "Test".into(),
619            visible: true,
620        };
621        let html = brick.to_html();
622        let css = brick.to_css();
623
624        assert!(html.contains("test-brick"));
625        assert!(html.contains("Test"));
626        assert!(css.contains(".test-brick"));
627    }
628
629    #[test]
630    fn test_brick_name() {
631        let brick = TestBrick {
632            text: "Test".into(),
633            visible: true,
634        };
635        assert_eq!(brick.brick_name(), "TestBrick");
636    }
637
638    #[test]
639    fn test_brick_assertions_list() {
640        let brick = TestBrick {
641            text: "Test".into(),
642            visible: true,
643        };
644        let assertions = brick.assertions();
645        assert_eq!(assertions.len(), 2);
646        assert!(matches!(assertions[0], BrickAssertion::TextVisible));
647    }
648
649    #[test]
650    fn test_brick_budget_method() {
651        let brick = TestBrick {
652            text: "Test".into(),
653            visible: true,
654        };
655        let budget = brick.budget();
656        assert_eq!(budget.total_ms, 16);
657    }
658}