Skip to main content

termwright_protocol/
evidence.rs

1//! Session-scoped application evidence providers.
2
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::OnceLock;
7use std::sync::{Arc, Mutex};
8
9use crate::messages::EvidenceProviderRegistration;
10use crate::tree::{
11    EvidenceMethod, EvidenceProvenance, EvidenceSource, EvidenceStrength, PointerHitGrid,
12    PointerHitRegion, ProviderActionRecipes, ProviderFocusState, ProviderPaintedRegion,
13    ProviderPointerRegion, ProviderRevisionEvidence, ProviderScrollState,
14    ProviderTerminalInputModes, Rect,
15};
16
17/// Immutable coordinates identifying the provider observation being requested.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Context {
20    /// Session whose frozen contract contains this provider.
21    pub session_id: String,
22    /// Semantic revision the evidence must describe exactly.
23    pub revision: i64,
24    /// Committed terminal viewport width in cells.
25    pub columns: i64,
26    /// Committed terminal viewport height in cells.
27    pub rows: i64,
28}
29
30/// Exact production pointer-router facts returned for one revision.
31pub struct PointerObservation {
32    /// Canonical pointer-owned regions for semantic recipients.
33    pub pointer_regions: Vec<ProviderPointerRegion>,
34    /// Optional production hit-test used to verify the complete viewport.
35    pub hit_test: Option<HitTest>,
36}
37
38/// Thread-safe production pointer hit-test in `(column, row)` order.
39pub type HitTest = Arc<dyn Fn(i64, i64) -> Option<String> + Send + Sync>;
40
41/// Closed application provider family for production pointer evidence.
42pub trait PointerProvider: Debug + Send + Sync {
43    /// Stable provider identity frozen into the session contract.
44    fn id(&self) -> &str;
45    /// Provider implementation version.
46    fn version(&self) -> &str;
47    /// How the application obtains the evidence.
48    fn method(&self) -> EvidenceMethod;
49    /// Closed provider capability names advertised during negotiation.
50    /// `pointer-regions` and `hit-test` may be owned together or by two
51    /// independent providers, but each capability has one session owner.
52    fn capabilities(&self) -> Vec<String>;
53    /// Observe exact evidence for the requested session revision.
54    fn observe(&self, context: &Context) -> Result<PointerObservation, String>;
55}
56
57/// Closed application provider family for data-only physical input recipes.
58pub trait ActionStrategyProvider: Debug + Send + Sync {
59    /// Stable provider identity frozen into the session contract.
60    fn id(&self) -> &str;
61    /// Provider implementation version.
62    fn version(&self) -> &str;
63    /// How the application obtains the production recipes.
64    fn method(&self) -> EvidenceMethod;
65    /// Observe exact recipes for the requested session revision.
66    fn observe(&self, context: &Context) -> Result<Vec<ProviderActionRecipes>, String>;
67}
68
69/// Closed application provider family for the production focus manager.
70pub trait FocusProvider: Debug + Send + Sync {
71    /// Stable provider identity frozen into the session contract.
72    fn id(&self) -> &str;
73    /// Provider implementation version.
74    fn version(&self) -> &str;
75    /// How the application obtains the focus evidence.
76    fn method(&self) -> EvidenceMethod;
77    /// Focused semantic recipient, or `None` when no node owns focus.
78    fn observe(&self, context: &Context) -> Result<Option<String>, String>;
79}
80
81/// Closed provider family for the production application viewport model.
82pub trait ScrollProvider: Debug + Send + Sync {
83    /// Stable provider identity frozen into the session contract.
84    fn id(&self) -> &str;
85    /// Provider implementation version.
86    fn version(&self) -> &str;
87    /// How the application obtains viewport evidence.
88    fn method(&self) -> EvidenceMethod;
89    /// Complete scroll recipient states for this committed revision.
90    fn observe(&self, context: &Context) -> Result<Vec<ProviderScrollState>, String>;
91}
92
93/// Closed provider family for production painter attribution.
94pub trait PaintProvider: Debug + Send + Sync {
95    /// Stable provider identity frozen into the session contract.
96    fn id(&self) -> &str;
97    /// Provider implementation version.
98    fn version(&self) -> &str;
99    /// How the application obtains painter evidence.
100    fn method(&self) -> EvidenceMethod;
101    /// Complete painted regions for this committed revision.
102    fn observe(&self, context: &Context) -> Result<Vec<ProviderPaintedRegion>, String>;
103}
104
105/// Closed provider family for production terminal parser configuration.
106pub trait InputModeProvider: Debug + Send + Sync {
107    /// Stable provider identity frozen into the session contract.
108    fn id(&self) -> &str;
109    /// Provider implementation version.
110    fn version(&self) -> &str;
111    /// How the application obtains parser evidence.
112    fn method(&self) -> EvidenceMethod;
113    /// Exact production parser configuration for this committed revision.
114    fn observe(&self, context: &Context) -> Result<ProviderTerminalInputModes, String>;
115}
116
117trait Provider: Debug + Send + Sync {
118    fn id(&self) -> &str;
119    fn version(&self) -> &str;
120    fn method(&self) -> EvidenceMethod;
121    fn capabilities(&self) -> Vec<String>;
122    fn observe(&self, context: &Context) -> Result<ProviderObservation, String>;
123}
124
125struct ProviderObservation {
126    pointer_regions: Vec<ProviderPointerRegion>,
127    hit_test: Option<HitTest>,
128    action_recipes: Option<Vec<ProviderActionRecipes>>,
129    focus_state: Option<ProviderFocusState>,
130    scroll_states: Option<Vec<ProviderScrollState>>,
131    painted_regions: Option<Vec<ProviderPaintedRegion>>,
132    input_modes: Option<ProviderTerminalInputModes>,
133}
134
135#[derive(Debug)]
136struct PointerProviderAdapter(Arc<dyn PointerProvider>);
137impl Provider for PointerProviderAdapter {
138    fn id(&self) -> &str {
139        self.0.id()
140    }
141    fn version(&self) -> &str {
142        self.0.version()
143    }
144    fn method(&self) -> EvidenceMethod {
145        self.0.method()
146    }
147    fn capabilities(&self) -> Vec<String> {
148        self.0.capabilities()
149    }
150    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
151        let value = self.0.observe(context)?;
152        Ok(ProviderObservation {
153            pointer_regions: value.pointer_regions,
154            hit_test: value.hit_test,
155            action_recipes: None,
156            focus_state: None,
157            scroll_states: None,
158            painted_regions: None,
159            input_modes: None,
160        })
161    }
162}
163
164#[derive(Debug)]
165struct ActionStrategyProviderAdapter(Arc<dyn ActionStrategyProvider>);
166impl Provider for ActionStrategyProviderAdapter {
167    fn id(&self) -> &str {
168        self.0.id()
169    }
170    fn version(&self) -> &str {
171        self.0.version()
172    }
173    fn method(&self) -> EvidenceMethod {
174        self.0.method()
175    }
176    fn capabilities(&self) -> Vec<String> {
177        vec!["action-recipes".into()]
178    }
179    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
180        Ok(ProviderObservation {
181            pointer_regions: Vec::new(),
182            hit_test: None,
183            action_recipes: Some(self.0.observe(context)?),
184            focus_state: None,
185            scroll_states: None,
186            painted_regions: None,
187            input_modes: None,
188        })
189    }
190}
191
192#[derive(Debug)]
193struct FocusProviderAdapter(Arc<dyn FocusProvider>);
194impl Provider for FocusProviderAdapter {
195    fn id(&self) -> &str {
196        self.0.id()
197    }
198    fn version(&self) -> &str {
199        self.0.version()
200    }
201    fn method(&self) -> EvidenceMethod {
202        self.0.method()
203    }
204    fn capabilities(&self) -> Vec<String> {
205        vec!["focus-state".into()]
206    }
207    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
208        Ok(ProviderObservation {
209            pointer_regions: Vec::new(),
210            hit_test: None,
211            action_recipes: None,
212            focus_state: Some(match self.0.observe(context)? {
213                Some(recipient_id) => ProviderFocusState::Focused { recipient_id },
214                None => ProviderFocusState::None,
215            }),
216            scroll_states: None,
217            painted_regions: None,
218            input_modes: None,
219        })
220    }
221}
222
223#[derive(Debug)]
224struct ScrollProviderAdapter(Arc<dyn ScrollProvider>);
225impl Provider for ScrollProviderAdapter {
226    fn id(&self) -> &str {
227        self.0.id()
228    }
229    fn version(&self) -> &str {
230        self.0.version()
231    }
232    fn method(&self) -> EvidenceMethod {
233        self.0.method()
234    }
235    fn capabilities(&self) -> Vec<String> {
236        vec!["scroll-state".into()]
237    }
238    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
239        Ok(ProviderObservation {
240            pointer_regions: Vec::new(),
241            hit_test: None,
242            action_recipes: None,
243            focus_state: None,
244            scroll_states: Some(self.0.observe(context)?),
245            painted_regions: None,
246            input_modes: None,
247        })
248    }
249}
250
251#[derive(Debug)]
252struct PaintProviderAdapter(Arc<dyn PaintProvider>);
253impl Provider for PaintProviderAdapter {
254    fn id(&self) -> &str {
255        self.0.id()
256    }
257    fn version(&self) -> &str {
258        self.0.version()
259    }
260    fn method(&self) -> EvidenceMethod {
261        self.0.method()
262    }
263    fn capabilities(&self) -> Vec<String> {
264        vec!["painted-regions".into()]
265    }
266    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
267        Ok(ProviderObservation {
268            pointer_regions: Vec::new(),
269            hit_test: None,
270            action_recipes: None,
271            focus_state: None,
272            scroll_states: None,
273            painted_regions: Some(self.0.observe(context)?),
274            input_modes: None,
275        })
276    }
277}
278
279#[derive(Debug)]
280struct InputModeProviderAdapter(Arc<dyn InputModeProvider>);
281impl Provider for InputModeProviderAdapter {
282    fn id(&self) -> &str {
283        self.0.id()
284    }
285    fn version(&self) -> &str {
286        self.0.version()
287    }
288    fn method(&self) -> EvidenceMethod {
289        self.0.method()
290    }
291    fn capabilities(&self) -> Vec<String> {
292        vec!["terminal-input-modes".into()]
293    }
294    fn observe(&self, context: &Context) -> Result<ProviderObservation, String> {
295        Ok(ProviderObservation {
296            pointer_regions: Vec::new(),
297            hit_test: None,
298            action_recipes: None,
299            focus_state: None,
300            scroll_states: None,
301            painted_regions: None,
302            input_modes: Some(self.0.observe(context)?),
303        })
304    }
305}
306
307#[derive(Debug)]
308struct Entry {
309    provider: Arc<dyn Provider>,
310    capabilities: Vec<String>,
311    active: AtomicBool,
312}
313#[derive(Debug, Default)]
314struct State {
315    active_leases: usize,
316    entries: HashMap<String, Arc<Entry>>,
317}
318
319/// Session-scoped registry frozen independently by each protocol client.
320#[derive(Debug, Clone, Default)]
321pub struct Registry {
322    state: Arc<Mutex<State>>,
323}
324
325/// Handle whose disposal marks a negotiated provider as lost.
326#[derive(Debug)]
327pub struct Registration {
328    state: Arc<Mutex<State>>,
329    id: String,
330    entry: Arc<Entry>,
331}
332impl Registration {
333    /// Remove the provider and fail closed for leases that already froze it.
334    pub fn dispose(&self) {
335        self.entry.active.store(false, Ordering::SeqCst);
336        self.state
337            .lock()
338            .expect("evidence registry poisoned")
339            .entries
340            .remove(&self.id);
341    }
342}
343
344impl Registry {
345    /// Create an empty provider registry.
346    pub fn new() -> Self {
347        Self::default()
348    }
349    /// Register a provider before any session freezes the registry.
350    /// Register a production pointer provider before contract freeze.
351    pub fn register_pointer(
352        &self,
353        provider: Arc<dyn PointerProvider>,
354    ) -> Result<Registration, String> {
355        for capability in provider.capabilities() {
356            if capability != "pointer-regions" && capability != "hit-test" {
357                return Err(format!("pointer provider cannot declare {capability}"));
358            }
359        }
360        self.register(Arc::new(PointerProviderAdapter(provider)))
361    }
362
363    /// Register production physical input recipes before contract freeze.
364    pub fn register_action_strategies(
365        &self,
366        provider: Arc<dyn ActionStrategyProvider>,
367    ) -> Result<Registration, String> {
368        self.register(Arc::new(ActionStrategyProviderAdapter(provider)))
369    }
370
371    /// Register production focus-manager evidence before contract freeze.
372    pub fn register_focus(&self, provider: Arc<dyn FocusProvider>) -> Result<Registration, String> {
373        self.register(Arc::new(FocusProviderAdapter(provider)))
374    }
375
376    /// Register production viewport evidence before contract freeze.
377    pub fn register_scroll(
378        &self,
379        provider: Arc<dyn ScrollProvider>,
380    ) -> Result<Registration, String> {
381        self.register(Arc::new(ScrollProviderAdapter(provider)))
382    }
383
384    /// Register production painter attribution before contract freeze.
385    pub fn register_paint(&self, provider: Arc<dyn PaintProvider>) -> Result<Registration, String> {
386        self.register(Arc::new(PaintProviderAdapter(provider)))
387    }
388
389    /// Register production terminal parser configuration before contract freeze.
390    pub fn register_input_modes(
391        &self,
392        provider: Arc<dyn InputModeProvider>,
393    ) -> Result<Registration, String> {
394        self.register(Arc::new(InputModeProviderAdapter(provider)))
395    }
396
397    fn register(&self, provider: Arc<dyn Provider>) -> Result<Registration, String> {
398        let mut state = self
399            .state
400            .lock()
401            .map_err(|_| "evidence registry poisoned".to_owned())?;
402        if state.active_leases > 0 {
403            return Err(format!(
404                "provider {} registered after contract freeze",
405                provider.id()
406            ));
407        }
408        if provider.id().is_empty() || provider.version().is_empty() {
409            return Err("invalid provider identity".into());
410        }
411        if state.entries.contains_key(provider.id()) {
412            return Err(format!("duplicate provider {}", provider.id()));
413        }
414        let capabilities = provider.capabilities();
415        validate_capabilities(&capabilities)?;
416        let id = provider.id().to_owned();
417        let entry = Arc::new(Entry {
418            provider,
419            capabilities,
420            active: AtomicBool::new(true),
421        });
422        state.entries.insert(id.clone(), entry.clone());
423        Ok(Registration {
424            state: self.state.clone(),
425            id,
426            entry,
427        })
428    }
429    pub(crate) fn freeze(&self) -> Lease {
430        let mut state = self.state.lock().expect("evidence registry poisoned");
431        state.active_leases += 1;
432        Lease {
433            state: self.state.clone(),
434            entries: state.entries.values().cloned().collect(),
435            closed: false,
436        }
437    }
438}
439
440fn validate_capabilities(capabilities: &[String]) -> Result<(), String> {
441    if capabilities.is_empty() {
442        return Err("provider must declare at least one capability".into());
443    }
444    let mut seen = std::collections::HashSet::new();
445    for capability in capabilities {
446        if capability != "pointer-regions"
447            && capability != "hit-test"
448            && capability != "action-recipes"
449            && capability != "focus-state"
450            && capability != "scroll-state"
451            && capability != "painted-regions"
452            && capability != "terminal-input-modes"
453        {
454            return Err(format!("unknown provider capability {capability}"));
455        }
456        if !seen.insert(capability) {
457            return Err(format!("duplicate provider capability {capability}"));
458        }
459    }
460    Ok(())
461}
462
463/// Process-wide registry used by framework probes and ordinary single-app binaries.
464/// Providers must register before the first instrumented frame freezes it.
465pub fn global_registry() -> Registry {
466    static REGISTRY: OnceLock<Registry> = OnceLock::new();
467    REGISTRY.get_or_init(Registry::new).clone()
468}
469
470/// Register a production evidence provider in the process-wide registry.
471pub fn register_pointer_evidence_provider(
472    provider: Arc<dyn PointerProvider>,
473) -> Result<Registration, String> {
474    global_registry().register_pointer(provider)
475}
476
477/// Register application production input recipes in the process-wide registry.
478pub fn register_action_strategy_provider(
479    provider: Arc<dyn ActionStrategyProvider>,
480) -> Result<Registration, String> {
481    global_registry().register_action_strategies(provider)
482}
483
484/// Register application production focus-manager evidence globally.
485pub fn register_focus_evidence_provider(
486    provider: Arc<dyn FocusProvider>,
487) -> Result<Registration, String> {
488    global_registry().register_focus(provider)
489}
490
491/// Register application production viewport evidence globally.
492pub fn register_scroll_evidence_provider(
493    provider: Arc<dyn ScrollProvider>,
494) -> Result<Registration, String> {
495    global_registry().register_scroll(provider)
496}
497
498/// Register application production painter attribution globally.
499pub fn register_paint_evidence_provider(
500    provider: Arc<dyn PaintProvider>,
501) -> Result<Registration, String> {
502    global_registry().register_paint(provider)
503}
504
505/// Register production terminal parser mode evidence globally.
506pub fn register_terminal_input_mode_evidence_provider(
507    provider: Arc<dyn InputModeProvider>,
508) -> Result<Registration, String> {
509    global_registry().register_input_modes(provider)
510}
511
512#[derive(Debug)]
513pub(crate) struct Lease {
514    state: Arc<Mutex<State>>,
515    entries: Vec<Arc<Entry>>,
516    closed: bool,
517}
518impl Lease {
519    pub(crate) fn registrations(&self) -> Vec<EvidenceProviderRegistration> {
520        self.entries
521            .iter()
522            .map(|e| EvidenceProviderRegistration {
523                id: e.provider.id().into(),
524                version: e.provider.version().into(),
525                method: method_name(e.provider.method()).into(),
526                capabilities: e.capabilities.clone(),
527            })
528            .collect()
529    }
530    pub(crate) fn collect(
531        &self,
532        session_id: &str,
533        revision: i64,
534        columns: i64,
535        rows: i64,
536    ) -> Vec<ProviderRevisionEvidence> {
537        self.entries
538            .iter()
539            .map(|e| collect_entry(e, session_id, revision, columns, rows))
540            .collect()
541    }
542    pub(crate) fn close(&mut self) {
543        if self.closed {
544            return;
545        }
546        self.closed = true;
547        self.state
548            .lock()
549            .expect("evidence registry poisoned")
550            .active_leases -= 1;
551    }
552}
553impl Drop for Lease {
554    fn drop(&mut self) {
555        self.close()
556    }
557}
558fn method_name(method: EvidenceMethod) -> &'static str {
559    match method {
560        EvidenceMethod::Native => "native",
561        EvidenceMethod::Declared => "declared",
562        _ => "instrumented",
563    }
564}
565
566fn collect_entry(
567    entry: &Entry,
568    session_id: &str,
569    revision: i64,
570    columns: i64,
571    rows: i64,
572) -> ProviderRevisionEvidence {
573    let base = |status: &str, reason: Option<String>| ProviderRevisionEvidence {
574        provider_id: entry.provider.id().into(),
575        session_id: session_id.into(),
576        revision,
577        status: status.into(),
578        evidence: None,
579        pointer_regions: None,
580        focus_state: None,
581        action_recipes: None,
582        scroll_states: None,
583        painted_regions: None,
584        input_modes: None,
585        hit_grid: None,
586        reason,
587    };
588    if !entry.active.load(Ordering::SeqCst) {
589        return base("lost", Some("provider disposed after negotiation".into()));
590    }
591    let observation = match entry.provider.observe(&Context {
592        session_id: session_id.into(),
593        revision,
594        columns,
595        rows,
596    }) {
597        Ok(v) => v,
598        Err(e) => return base("violation", Some(e)),
599    };
600    let mut result = base("available", None);
601    result.evidence = Some(EvidenceProvenance {
602        source: EvidenceSource::Application,
603        method: entry.provider.method(),
604        strength: EvidenceStrength::Authoritative,
605        provider_id: entry.provider.id().into(),
606    });
607    let capabilities = &entry.capabilities;
608    if !capabilities.iter().any(|v| v == "pointer-regions")
609        && !observation.pointer_regions.is_empty()
610    {
611        return base(
612            "violation",
613            Some("published pointer regions without negotiating pointer-regions".into()),
614        );
615    }
616    if !capabilities.iter().any(|v| v == "hit-test") && observation.hit_test.is_some() {
617        return base(
618            "violation",
619            Some("published a hit-test callback without negotiating hit-test".into()),
620        );
621    }
622    if capabilities.iter().any(|v| v == "action-recipes") && observation.action_recipes.is_none() {
623        return base(
624            "violation",
625            Some("negotiated action-recipes evidence is unavailable".into()),
626        );
627    }
628    if !capabilities.iter().any(|v| v == "action-recipes") && observation.action_recipes.is_some() {
629        return base(
630            "violation",
631            Some("published action recipes without negotiating action-recipes".into()),
632        );
633    }
634    if capabilities.iter().any(|v| v == "focus-state") && observation.focus_state.is_none() {
635        return base(
636            "violation",
637            Some("negotiated focus-state evidence is unavailable".into()),
638        );
639    }
640    if !capabilities.iter().any(|v| v == "focus-state") && observation.focus_state.is_some() {
641        return base(
642            "violation",
643            Some("published focus state without negotiating focus-state".into()),
644        );
645    }
646    if capabilities.iter().any(|v| v == "scroll-state") && observation.scroll_states.is_none() {
647        return base(
648            "violation",
649            Some("negotiated scroll-state evidence is unavailable".into()),
650        );
651    }
652    if !capabilities.iter().any(|v| v == "scroll-state") && observation.scroll_states.is_some() {
653        return base(
654            "violation",
655            Some("published scroll state without negotiating scroll-state".into()),
656        );
657    }
658    if let Some(states) = &observation.scroll_states {
659        if states.iter().any(|state| {
660            state.offset < 0
661                || state.viewport < 0
662                || state.extent < 0
663                || state.offset + state.viewport > state.extent
664        }) {
665            return base(
666                "violation",
667                Some("scroll state must fit inside its extent".into()),
668            );
669        }
670    }
671    if capabilities.iter().any(|v| v == "painted-regions") && observation.painted_regions.is_none()
672    {
673        return base(
674            "violation",
675            Some("negotiated painted-regions evidence is unavailable".into()),
676        );
677    }
678    if !capabilities.iter().any(|v| v == "painted-regions") && observation.painted_regions.is_some()
679    {
680        return base(
681            "violation",
682            Some("published painted regions without negotiating painted-regions".into()),
683        );
684    }
685    if capabilities.iter().any(|v| v == "terminal-input-modes") && observation.input_modes.is_none()
686    {
687        return base(
688            "violation",
689            Some("negotiated terminal-input-modes evidence is unavailable".into()),
690        );
691    }
692    if !capabilities.iter().any(|v| v == "terminal-input-modes")
693        && observation.input_modes.is_some()
694    {
695        return base(
696            "violation",
697            Some("published input modes without negotiating terminal-input-modes".into()),
698        );
699    }
700    if let Some(modes) = &observation.input_modes {
701        if !matches!(
702            modes.mouse_tracking.as_str(),
703            "none" | "x10" | "vt200" | "drag" | "any"
704        ) || !matches!(
705            modes.mouse_encoding.as_str(),
706            "default" | "sgr" | "urxvt" | "utf8"
707        ) || !matches!(modes.focus_reporting.as_str(), "on" | "off")
708        {
709            return base(
710                "violation",
711                Some("terminal input modes contain an invalid value".into()),
712            );
713        }
714    }
715    result.pointer_regions = Some(observation.pointer_regions);
716    result.action_recipes = observation.action_recipes;
717    result.focus_state = observation.focus_state;
718    result.scroll_states = observation.scroll_states;
719    result.painted_regions = observation.painted_regions;
720    result.input_modes = observation.input_modes;
721    if capabilities.iter().any(|v| v == "hit-test") {
722        match exact_grid(
723            result.pointer_regions.as_deref().unwrap_or_default(),
724            observation.hit_test,
725            columns,
726            rows,
727            capabilities.iter().any(|v| v == "pointer-regions"),
728        ) {
729            Ok(grid) => result.hit_grid = Some(grid),
730            Err(error) => return base("violation", Some(error)),
731        }
732    }
733    result
734}
735
736fn exact_grid(
737    regions: &[ProviderPointerRegion],
738    hit_test: Option<HitTest>,
739    columns: i64,
740    rows: i64,
741    verify_declared_regions: bool,
742) -> Result<PointerHitGrid, String> {
743    let hit_test = hit_test.ok_or_else(|| "negotiated hit-test callback unavailable".to_owned())?;
744    if columns.checked_mul(rows).unwrap_or(i64::MAX) > 1_000_000 {
745        return Err("hit-test viewport exceeds provider limit".into());
746    }
747    let mut declared = HashMap::new();
748    for region in regions {
749        for span in &region.spans {
750            for column in span.from..span.to {
751                let key = (span.row, column);
752                if let Some(old) = declared.insert(key, region.recipient_id.clone()) {
753                    if old != region.recipient_id {
754                        return Err("overlapping pointer regions".into());
755                    }
756                }
757            }
758        }
759    }
760    let mut output = Vec::new();
761    for row in 0..rows {
762        let mut start: Option<i64> = None;
763        let mut owner: Option<String> = None;
764        for column in 0..=columns {
765            let actual = if column == columns {
766                None
767            } else {
768                hit_test(column, row)
769            };
770            let expected = if column == columns {
771                None
772            } else {
773                declared.get(&(row, column)).cloned()
774            };
775            if verify_declared_regions && actual != expected {
776                return Err(format!("production hit test disagrees at {column},{row}"));
777            }
778            if actual != owner {
779                if let (Some(from), Some(id)) = (start.take(), owner.take()) {
780                    output.push(PointerHitRegion {
781                        recipient_id: id,
782                        rect: Rect {
783                            row,
784                            column: from,
785                            width: column - from,
786                            height: 1,
787                        },
788                    })
789                }
790                if actual.is_some() {
791                    start = Some(column);
792                    owner = actual
793                }
794            }
795        }
796    }
797    Ok(PointerHitGrid { regions: output })
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use crate::tree::{ProviderPointerSpan, Rect};
804
805    #[derive(Debug)]
806    struct Router;
807    impl PointerProvider for Router {
808        fn id(&self) -> &str {
809            "router"
810        }
811        fn version(&self) -> &str {
812            "1"
813        }
814        fn method(&self) -> EvidenceMethod {
815            EvidenceMethod::Native
816        }
817        fn capabilities(&self) -> Vec<String> {
818            vec!["pointer-regions".into(), "hit-test".into()]
819        }
820        fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
821            Ok(PointerObservation {
822                pointer_regions: vec![ProviderPointerRegion {
823                    recipient_id: "reject".into(),
824                    region_bounds: Rect {
825                        row: 1,
826                        column: 2,
827                        width: 3,
828                        height: 1,
829                    },
830                    spans: vec![ProviderPointerSpan {
831                        row: 1,
832                        from: 2,
833                        to: 5,
834                    }],
835                }],
836                hit_test: Some(Arc::new(|column, row| {
837                    (row == 1 && (2..5).contains(&column)).then(|| "reject".into())
838                })),
839            })
840        }
841    }
842
843    #[derive(Debug)]
844    struct Regions;
845    impl PointerProvider for Regions {
846        fn id(&self) -> &str {
847            "regions"
848        }
849        fn version(&self) -> &str {
850            "1"
851        }
852        fn method(&self) -> EvidenceMethod {
853            EvidenceMethod::Declared
854        }
855        fn capabilities(&self) -> Vec<String> {
856            vec!["pointer-regions".into()]
857        }
858        fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
859            Ok(PointerObservation {
860                pointer_regions: Router.observe(_context)?.pointer_regions,
861                hit_test: None,
862            })
863        }
864    }
865
866    #[derive(Debug)]
867    struct HitTestOnly;
868    impl PointerProvider for HitTestOnly {
869        fn id(&self) -> &str {
870            "production-router"
871        }
872        fn version(&self) -> &str {
873            "2"
874        }
875        fn method(&self) -> EvidenceMethod {
876            EvidenceMethod::Native
877        }
878        fn capabilities(&self) -> Vec<String> {
879            vec!["hit-test".into()]
880        }
881        fn observe(&self, _context: &Context) -> Result<PointerObservation, String> {
882            Ok(PointerObservation {
883                pointer_regions: Vec::new(),
884                hit_test: Some(Arc::new(|column, row| {
885                    (row == 1 && (2..5).contains(&column)).then(|| "reject".into())
886                })),
887            })
888        }
889    }
890
891    #[test]
892    fn freezes_per_session_and_fails_closed_after_loss() {
893        let registry = Registry::new();
894        let registration = registry
895            .register_pointer(Arc::new(Router))
896            .expect("register");
897        let lease = registry.freeze();
898        assert!(registry.register_pointer(Arc::new(Router)).is_err());
899        let evidence = lease.collect("s1", 1, 10, 4);
900        assert_eq!(evidence[0].status, "available");
901        assert_eq!(evidence[0].hit_grid.as_ref().unwrap().regions.len(), 1);
902        registration.dispose();
903        assert_eq!(lease.collect("s1", 2, 10, 4)[0].status, "lost");
904    }
905
906    #[test]
907    fn composes_independent_region_and_hit_test_providers() {
908        let registry = Registry::new();
909        registry
910            .register_pointer(Arc::new(Regions))
911            .expect("regions");
912        registry
913            .register_pointer(Arc::new(HitTestOnly))
914            .expect("hit test");
915        let evidence = registry.freeze().collect("s1", 4, 10, 4);
916        let regions = evidence
917            .iter()
918            .find(|entry| entry.provider_id == "regions")
919            .unwrap();
920        let hits = evidence
921            .iter()
922            .find(|entry| entry.provider_id == "production-router")
923            .unwrap();
924        assert_eq!(regions.status, "available");
925        assert_eq!(regions.pointer_regions.as_ref().unwrap().len(), 1);
926        assert!(regions.hit_grid.is_none());
927        assert_eq!(hits.status, "available");
928        assert!(hits.pointer_regions.as_ref().unwrap().is_empty());
929        assert_eq!(hits.hit_grid.as_ref().unwrap().regions.len(), 1);
930    }
931
932    #[derive(Debug)]
933    struct Keys;
934    impl ActionStrategyProvider for Keys {
935        fn id(&self) -> &str {
936            "app.keys"
937        }
938        fn version(&self) -> &str {
939            "1"
940        }
941        fn method(&self) -> EvidenceMethod {
942            EvidenceMethod::Native
943        }
944        fn observe(&self, _context: &Context) -> Result<Vec<ProviderActionRecipes>, String> {
945            Ok(vec![ProviderActionRecipes {
946                recipient_id: "editor".into(),
947                recipes: vec![crate::tree::PhysicalInputRecipe {
948                    action: crate::tree::PhysicalInputRecipeAction::SetValue,
949                    requires_focus: true,
950                    steps: vec![
951                        crate::tree::PhysicalInputRecipeStep::Press {
952                            key: "Control+U".into(),
953                        },
954                        crate::tree::PhysicalInputRecipeStep::InsertActionValue,
955                    ],
956                }],
957            }])
958        }
959    }
960
961    #[derive(Debug)]
962    struct Focus;
963    impl FocusProvider for Focus {
964        fn id(&self) -> &str {
965            "app.focus"
966        }
967        fn version(&self) -> &str {
968            "1"
969        }
970        fn method(&self) -> EvidenceMethod {
971            EvidenceMethod::Native
972        }
973        fn observe(&self, context: &Context) -> Result<Option<String>, String> {
974            Ok((context.revision == 1).then(|| "editor".into()))
975        }
976    }
977
978    #[derive(Debug)]
979    struct Scroll;
980    impl ScrollProvider for Scroll {
981        fn id(&self) -> &str {
982            "app.scroll"
983        }
984        fn version(&self) -> &str {
985            "1"
986        }
987        fn method(&self) -> EvidenceMethod {
988            EvidenceMethod::Native
989        }
990        fn observe(&self, _context: &Context) -> Result<Vec<ProviderScrollState>, String> {
991            Ok(vec![ProviderScrollState {
992                recipient_id: "results".into(),
993                axis: crate::tree::Orientation::Vertical,
994                offset: 3,
995                viewport: 4,
996                extent: 20,
997            }])
998        }
999    }
1000
1001    #[test]
1002    fn focus_provider_preserves_authoritative_none() {
1003        let registry = Registry::new();
1004        registry.register_focus(Arc::new(Focus)).expect("focus");
1005        let lease = registry.freeze();
1006        assert_eq!(lease.registrations()[0].capabilities, vec!["focus-state"]);
1007        assert!(matches!(
1008            lease.collect("s", 1, 80, 24)[0].focus_state,
1009            Some(ProviderFocusState::Focused { ref recipient_id }) if recipient_id == "editor"
1010        ));
1011        assert!(matches!(
1012            lease.collect("s", 2, 80, 24)[0].focus_state,
1013            Some(ProviderFocusState::None)
1014        ));
1015    }
1016
1017    #[test]
1018    fn scroll_provider_publishes_bounded_application_viewport_state() {
1019        let registry = Registry::new();
1020        registry.register_scroll(Arc::new(Scroll)).expect("scroll");
1021        let lease = registry.freeze();
1022        assert_eq!(lease.registrations()[0].capabilities, vec!["scroll-state"]);
1023        let frame = lease.collect("s", 1, 80, 24).remove(0);
1024        assert_eq!(frame.status, "available");
1025        assert_eq!(frame.scroll_states.as_ref().unwrap()[0].offset, 3);
1026    }
1027
1028    #[test]
1029    fn action_strategy_provider_is_a_separate_closed_family() {
1030        let registry = Registry::new();
1031        registry
1032            .register_action_strategies(Arc::new(Keys))
1033            .expect("keys");
1034        let frame = registry.freeze().collect("s", 3, 80, 24).remove(0);
1035        assert_eq!(frame.status, "available");
1036        assert_eq!(frame.action_recipes.as_ref().map(Vec::len), Some(1));
1037        assert!(frame.pointer_regions.as_ref().is_some_and(Vec::is_empty));
1038        assert!(frame.hit_grid.is_none());
1039    }
1040
1041    #[test]
1042    fn rejects_invalid_and_competing_capability_declarations() {
1043        #[derive(Debug)]
1044        struct Invalid;
1045        impl PointerProvider for Invalid {
1046            fn id(&self) -> &str {
1047                "invalid"
1048            }
1049            fn version(&self) -> &str {
1050                "1"
1051            }
1052            fn method(&self) -> EvidenceMethod {
1053                EvidenceMethod::Declared
1054            }
1055            fn capabilities(&self) -> Vec<String> {
1056                vec!["unknown".into()]
1057            }
1058            fn observe(&self, _: &Context) -> Result<PointerObservation, String> {
1059                unreachable!()
1060            }
1061        }
1062        let registry = Registry::new();
1063        assert!(registry.register_pointer(Arc::new(Invalid)).is_err());
1064        registry
1065            .register_pointer(Arc::new(Regions))
1066            .expect("regions");
1067        assert!(registry.register_pointer(Arc::new(Regions)).is_err());
1068        registry
1069            .register_pointer(Arc::new(HitTestOnly))
1070            .expect("hit test");
1071    }
1072}