Skip to main content

telltale_language/ast/
choreography.rs

1// Choreography struct definition and validation
2
3use super::{ChoiceGuard, Protocol, Role, ValidationError};
4use proc_macro2::Ident;
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeSet, HashMap};
7
8const ATTR_THEOREM_PACKS: &str = "dsl.proof_bundles";
9const ATTR_REQUIRED_THEOREM_PACKS: &str = "dsl.required_proof_bundles";
10const ATTR_INFERRED_REQUIRED_THEOREM_PACKS: &str = "dsl.inferred_required_proof_bundles";
11const ATTR_ROLE_SETS: &str = "dsl.role_sets";
12const ATTR_TOPOLOGIES: &str = "dsl.topologies";
13const ATTR_TYPE_DECLARATIONS: &str = "dsl.type_decls";
14const ATTR_EFFECT_INTERFACE_DECLARATIONS: &str = "dsl.effect_decls";
15const ATTR_PROTOCOL_USES: &str = "dsl.protocol_uses";
16const ATTR_REGION_DECLARATIONS: &str = "dsl.fragment_decls";
17const ATTR_OPERATION_DECLARATIONS: &str = "dsl.operation_decls";
18const ATTR_GUEST_RUNTIME_DECLARATIONS: &str = "dsl.guest_runtime_decls";
19const ATTR_EXECUTION_PROFILE_DECLARATIONS: &str = "dsl.execution_profile_decls";
20const ATTR_PROTOCOL_EXECUTION_PROFILES: &str = "dsl.protocol_execution_profiles";
21const ATTR_AGREEMENT_PROFILE_DECLARATIONS: &str = "dsl.agreement_profile_decls";
22
23/// Typed proof-bundle declaration metadata from DSL.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct TheoremPackDeclaration {
26    /// Stable bundle name.
27    pub name: String,
28    /// Capabilities provided by this bundle.
29    #[serde(default)]
30    pub capabilities: Vec<String>,
31    /// Optional bundle version.
32    #[serde(default)]
33    pub version: Option<String>,
34    /// Optional bundle issuer.
35    #[serde(default)]
36    pub issuer: Option<String>,
37    /// Optional constraints attached to the bundle.
38    #[serde(default)]
39    pub constraints: Vec<String>,
40}
41
42/// Typed role-set declaration metadata from DSL.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct RoleSetDeclaration {
45    /// Stable role-set name.
46    pub name: String,
47    /// Explicit members for this role-set.
48    #[serde(default)]
49    pub members: Vec<String>,
50    /// Optional subset selector source role-set or family.
51    #[serde(default)]
52    pub subset_of: Option<String>,
53    /// Optional subset selector start index (inclusive).
54    #[serde(default)]
55    pub subset_start: Option<u32>,
56    /// Optional subset selector end index (exclusive).
57    #[serde(default)]
58    pub subset_end: Option<u32>,
59}
60
61/// Typed topology declaration metadata from DSL.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct TopologyDeclaration {
64    /// Topology kind (`cluster`, `ring`, `mesh`).
65    pub kind: String,
66    /// Topology name.
67    pub name: String,
68    /// Referenced members.
69    #[serde(default)]
70    pub members: Vec<String>,
71}
72
73/// DSL type declaration metadata.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct TypeDeclaration {
76    /// Declared type name.
77    pub name: String,
78    /// Whether this is a `type alias`.
79    pub is_alias: bool,
80    /// Right-hand side for aliases.
81    #[serde(default)]
82    pub alias_of: Option<String>,
83    /// Union constructors for nominal sum types.
84    #[serde(default)]
85    pub constructors: Vec<TypeConstructorDeclaration>,
86}
87
88/// Constructor declaration for one nominal union type.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TypeConstructorDeclaration {
91    /// Constructor name.
92    pub name: String,
93    /// Optional payload type rendered from source syntax.
94    #[serde(default)]
95    pub payload_type: Option<String>,
96}
97
98/// Nominal effect interface declaration metadata.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct EffectInterfaceDeclaration {
101    /// Effect interface name.
102    pub name: String,
103    /// Declared operations for this interface.
104    #[serde(default)]
105    pub operations: Vec<EffectOperationDeclaration>,
106}
107
108/// Authority class for one nominal effect operation.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
110#[serde(rename_all = "snake_case")]
111pub enum EffectAuthorityClass {
112    /// Operation may produce authoritative semantic evidence.
113    Authoritative,
114    /// Operation performs command work without itself proving semantic truth.
115    #[default]
116    Command,
117    /// Operation is observational only and must not be consumed via `check`.
118    Observe,
119}
120
121/// One operation in a nominal effect interface.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct EffectOperationDeclaration {
124    /// Operation name.
125    pub name: String,
126    /// Authority class attached to this operation.
127    #[serde(default)]
128    pub authority_class: EffectAuthorityClass,
129    /// Semantic class declared for this operation.
130    pub semantic_class: String,
131    /// Progress class declared for this operation.
132    pub progress: String,
133    /// Region scope declared for this operation.
134    pub region: String,
135    /// Agreement-use discipline declared for this operation.
136    pub agreement_use: String,
137    /// Reentrancy policy declared for this operation.
138    pub reentrancy: String,
139    /// Input type as declared in DSL surface syntax.
140    pub input_type: String,
141    /// Output type as declared in DSL surface syntax.
142    pub output_type: String,
143}
144
145/// Runtime-facing effect metadata derived from one nominal effect declaration.
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct EffectContractDeclaration {
148    /// Nominal effect interface name.
149    pub interface_name: String,
150    /// Nominal effect operation name.
151    pub operation_name: String,
152    /// Authority class attached to this operation.
153    pub authority_class: EffectAuthorityClass,
154    /// Semantic class attached to this operation.
155    pub semantic_class: String,
156    /// Progress class attached to this operation.
157    pub progress: String,
158    /// Region scope attached to this operation.
159    pub region: String,
160    /// Agreement-use discipline attached to this operation.
161    pub agreement_use: String,
162    /// Runtime admissibility policy name.
163    pub admissibility: String,
164    /// Runtime totality policy name.
165    pub totality: String,
166    /// Runtime timeout policy name.
167    pub timeout_policy: String,
168    /// Runtime reentrancy policy name.
169    pub reentrancy_policy: String,
170    /// Runtime handler-domain name.
171    pub handler_domain: String,
172}
173
174/// Fragment declaration metadata from DSL.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub struct RegionDeclaration {
177    /// Fragment name.
178    pub name: String,
179    /// Named fragment parameters.
180    #[serde(default)]
181    pub params: Vec<String>,
182}
183
184/// Operation parameter declaration metadata.
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct OperationParameterDeclaration {
187    /// Parameter name.
188    pub name: String,
189    /// Parameter type rendered from source syntax.
190    pub type_name: String,
191}
192
193/// Structured progress-contract attachment metadata from DSL.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ProgressAttachment {
196    /// Stable progress-contract name.
197    pub contract_name: String,
198    /// Required execution/admission profile.
199    #[serde(default)]
200    pub requires_profile: Option<String>,
201    /// Required escalation window class.
202    #[serde(default)]
203    pub within_window: Option<String>,
204    /// Named timeout branch or escalation action.
205    #[serde(default)]
206    pub on_timeout: Option<String>,
207    /// Named stall branch or escalation action.
208    #[serde(default)]
209    pub on_stall: Option<String>,
210}
211
212/// Named reusable agreement-profile declaration metadata from DSL.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct AgreementProfileDeclaration {
215    /// Stable profile name.
216    pub name: String,
217    /// Visibility timing fixed by this profile.
218    pub visibility: String,
219    /// Agreement/decision rule name.
220    pub rule: String,
221    /// Minimum agreement level required for provisional usability.
222    pub usable_at: String,
223    /// Minimum agreement level required for finalization.
224    pub finalized_at: String,
225    /// Required evidence kind for this profile.
226    pub evidence: String,
227}
228
229/// Operation-level attachment of one named agreement profile.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct OperationAgreementAttachment {
232    /// Referenced agreement-profile name.
233    pub profile_name: String,
234    /// Optional named prestate binding required by the operation.
235    #[serde(default)]
236    pub prestate: Option<String>,
237}
238
239/// Operation declaration metadata from DSL.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct OperationDeclaration {
242    /// Operation name.
243    pub name: String,
244    /// Operation parameters.
245    #[serde(default)]
246    pub params: Vec<OperationParameterDeclaration>,
247    /// Semantic owner role.
248    pub owner_role: String,
249    /// Optional fragment scope rendered from source syntax.
250    #[serde(default)]
251    pub within: Option<String>,
252    /// Required progress contract for parity-critical execution.
253    #[serde(default)]
254    pub progress_contract: Option<ProgressAttachment>,
255    /// Named agreement/finality attachment for the operation.
256    #[serde(default)]
257    pub agreement: Option<OperationAgreementAttachment>,
258    /// Declared child-effect aggregation for the operation.
259    #[serde(default)]
260    pub child_effect_aggregation: Option<ChildEffectAggregation>,
261    /// Raw operation body source.
262    pub body_source: String,
263}
264
265/// Canonical child-effect aggregation policy attached below one agreement boundary.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub enum ChildEffectAggregationPolicy {
268    /// All child effects must succeed.
269    All,
270    /// The first successful child effect resolves the aggregation.
271    First,
272    /// A fixed number of successful child effects is required.
273    Threshold {
274        /// Required success count.
275        required_successes: u64,
276    },
277}
278
279/// One declared child-effect aggregation attached to an operation.
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct ChildEffectAggregation {
282    /// Canonical aggregation policy.
283    pub policy: ChildEffectAggregationPolicy,
284}
285
286impl ChildEffectAggregationPolicy {
287    /// Canonical DSL spelling for this child-effect aggregation policy.
288    #[must_use]
289    pub fn dsl_name(&self) -> String {
290        match self {
291            Self::All => "all_success".to_string(),
292            Self::First => "first_success".to_string(),
293            Self::Threshold { required_successes } => {
294                format!("threshold_success({required_successes})")
295            }
296        }
297    }
298}
299
300impl ChildEffectAggregation {
301    /// Canonical DSL spelling for this child-effect aggregation declaration.
302    #[must_use]
303    pub fn dsl_name(&self) -> String {
304        self.policy.dsl_name()
305    }
306}
307
308/// Guest-runtime declaration metadata from DSL.
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310pub struct GuestRuntimeDeclaration {
311    /// Guest-runtime name.
312    pub name: String,
313    /// Declared effect interface uses.
314    #[serde(default)]
315    pub uses: Vec<String>,
316    /// Entry protocol name.
317    pub entry: String,
318}
319
320/// Execution-profile declaration metadata from DSL.
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct ExecutionProfileDeclaration {
323    /// Stable profile name.
324    pub name: String,
325    /// Fairness class fixed by the profile.
326    #[serde(default)]
327    pub fairness: Option<String>,
328    /// Admissibility class fixed by the profile.
329    #[serde(default)]
330    pub admissibility: Option<String>,
331    /// Escalation-window class fixed by the profile.
332    #[serde(default)]
333    pub escalation_window: Option<String>,
334}
335
336/// Strongest artifact tier justified by the parsed specification.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum LanguageTier {
340    FullSpec,
341    SessionProjectable,
342    ProtocolMachineExecutable,
343    ProofOnly,
344}
345
346/// Explicit language-tier status for one parsed specification.
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct LanguageTierStatus {
349    pub strongest_tier: LanguageTier,
350    pub session_projectable: bool,
351    pub protocol_machine_executable: bool,
352    pub theory_convertible: bool,
353    pub proof_only: bool,
354    pub diagnostic: String,
355}
356
357/// A complete choreographic protocol specification
358#[derive(Debug)]
359pub struct Choreography {
360    /// Protocol name
361    pub name: Ident,
362    /// Optional namespace for the protocol
363    pub namespace: Option<String>,
364    /// Participating roles
365    pub roles: Vec<Role>,
366    /// The protocol specification
367    pub protocol: Protocol,
368    /// Metadata and attributes
369    pub attrs: HashMap<String, String>,
370}
371
372impl Choreography {
373    /// Get the qualified name of the choreography (namespace::name or just name)
374    #[must_use]
375    pub fn qualified_name(&self) -> String {
376        match &self.namespace {
377            Some(ns) => format!("{}::{}", ns, self.name),
378            None => self.name.to_string(),
379        }
380    }
381
382    /// Validate the choreography for correctness
383    ///
384    /// # Errors
385    ///
386    /// Returns [`ValidationError`] if the choreography is invalid (unused roles,
387    /// malformed protocol, duplicate/missing proof bundles, or missing capabilities).
388    pub fn validate(&self) -> Result<(), ValidationError> {
389        // Check all roles are used
390        for role in &self.roles {
391            if !self.protocol.mentions_role(role) {
392                return Err(ValidationError::UnusedRole(role.name().to_string()));
393            }
394        }
395
396        // Check protocol is well-formed
397        self.protocol.validate(&self.roles)?;
398        self.validate_proof_bundles()?;
399        self.validate_effect_surface()?;
400        self.validate_execution_profile_surface()?;
401        self.validate_operation_surface()?;
402
403        Ok(())
404    }
405
406    fn validate_proof_bundles(&self) -> Result<(), ValidationError> {
407        let bundles = self.theorem_packs();
408        let mut declared: BTreeSet<String> = BTreeSet::new();
409        for bundle in &bundles {
410            if !declared.insert(bundle.name.clone()) {
411                return Err(ValidationError::DuplicateProofBundle(bundle.name.clone()));
412            }
413        }
414
415        for required in self.required_theorem_packs() {
416            if !declared.contains(&required) {
417                return Err(ValidationError::MissingProofBundle(required));
418            }
419        }
420
421        let required_caps = self.required_theorem_pack_capabilities();
422        for capability in self.required_protocol_machine_core_capabilities() {
423            if !required_caps.contains(&capability) {
424                return Err(ValidationError::MissingCapability(capability));
425            }
426        }
427
428        Ok(())
429    }
430
431    fn validate_effect_surface(&self) -> Result<(), ValidationError> {
432        let mut effect_names = BTreeSet::new();
433        let mut effect_ops: HashMap<String, HashMap<String, EffectOperationDeclaration>> =
434            HashMap::new();
435        for effect in self.effect_interface_declarations() {
436            if !effect_names.insert(effect.name.clone()) {
437                return Err(ValidationError::ExtensionError(format!(
438                    "duplicate effect interface declaration `{}`",
439                    effect.name
440                )));
441            }
442            let mut ops = HashMap::new();
443            for op in effect.operations {
444                let semantic_class_ok = matches!(
445                    op.semantic_class.as_str(),
446                    "authoritative" | "observational" | "best_effort"
447                );
448                if !semantic_class_ok {
449                    return Err(ValidationError::ExtensionError(format!(
450                        "effect operation `{}.{}` has unsupported `class {}`",
451                        effect.name, op.name, op.semantic_class
452                    )));
453                }
454                let progress_ok = matches!(op.progress.as_str(), "immediate" | "may_block");
455                if !progress_ok {
456                    return Err(ValidationError::ExtensionError(format!(
457                        "effect operation `{}.{}` has unsupported `progress {}`",
458                        effect.name, op.name, op.progress
459                    )));
460                }
461                if !matches!(op.region.as_str(), "session" | "fragment" | "global") {
462                    return Err(ValidationError::ExtensionError(format!(
463                        "effect operation `{}.{}` has unsupported `region {}`",
464                        effect.name, op.name, op.region
465                    )));
466                }
467                if !matches!(op.agreement_use.as_str(), "required" | "none" | "forbidden") {
468                    return Err(ValidationError::ExtensionError(format!(
469                        "effect operation `{}.{}` has unsupported `agreement_use {}`",
470                        effect.name, op.name, op.agreement_use
471                    )));
472                }
473                if !matches!(
474                    op.reentrancy.as_str(),
475                    "allow" | "reject_same_operation" | "reject_same_fragment"
476                ) {
477                    return Err(ValidationError::ExtensionError(format!(
478                        "effect operation `{}.{}` has unsupported `reentrancy {}`",
479                        effect.name, op.name, op.reentrancy
480                    )));
481                }
482                if matches!(op.authority_class, EffectAuthorityClass::Observe)
483                    && op.semantic_class != "observational"
484                {
485                    return Err(ValidationError::ExtensionError(format!(
486                        "effect operation `{}.{}` is observational and must declare `class : observational`",
487                        effect.name, op.name
488                    )));
489                }
490                if matches!(op.authority_class, EffectAuthorityClass::Authoritative)
491                    && op.semantic_class != "authoritative"
492                {
493                    return Err(ValidationError::ExtensionError(format!(
494                        "effect operation `{}.{}` is authoritative and must declare `class : authoritative`",
495                        effect.name, op.name
496                    )));
497                }
498                if op.progress == "immediate"
499                    && matches!(op.authority_class, EffectAuthorityClass::Authoritative)
500                {
501                    return Err(ValidationError::ExtensionError(format!(
502                        "effect operation `{}.{}` may not be both `authoritative` and `progress immediate`",
503                        effect.name, op.name
504                    )));
505                }
506                if op.agreement_use == "required"
507                    && matches!(op.authority_class, EffectAuthorityClass::Observe)
508                {
509                    return Err(ValidationError::ExtensionError(format!(
510                        "effect operation `{}.{}` may not require agreement use on an observational surface",
511                        effect.name, op.name
512                    )));
513                }
514                if ops.insert(op.name.clone(), op.clone()).is_some() {
515                    return Err(ValidationError::ExtensionError(format!(
516                        "duplicate effect operation `{}.{}`",
517                        effect.name, op.name
518                    )));
519                }
520            }
521            effect_ops.insert(effect.name, ops);
522        }
523
524        let declared = effect_names;
525        let used: BTreeSet<String> = self.protocol_uses().into_iter().collect();
526        for effect in &used {
527            if !declared.contains(effect) {
528                return Err(ValidationError::ExtensionError(format!(
529                    "protocol uses undeclared effect interface `{effect}`"
530                )));
531            }
532        }
533
534        fn validate_expr(
535            expr: &super::AuthorityExpr,
536            effect_ops: &HashMap<String, HashMap<String, EffectOperationDeclaration>>,
537            used: &BTreeSet<String>,
538        ) -> Result<(), ValidationError> {
539            match expr {
540                super::AuthorityExpr::Check {
541                    effect, operation, ..
542                } => {
543                    if !used.contains(effect) {
544                        return Err(ValidationError::ExtensionError(format!(
545                            "effect invocation `{effect}.{operation}` is not allowed without `uses {effect}`"
546                        )));
547                    }
548                    let Some(ops) = effect_ops.get(effect) else {
549                        return Err(ValidationError::ExtensionError(format!(
550                            "effect invocation references undeclared interface `{effect}`"
551                        )));
552                    };
553                    let Some(op_decl) = ops.get(operation) else {
554                        return Err(ValidationError::ExtensionError(format!(
555                            "effect invocation references undeclared operation `{effect}.{operation}`"
556                        )));
557                    };
558                    if matches!(op_decl.authority_class, EffectAuthorityClass::Observe) {
559                        return Err(ValidationError::ExtensionError(format!(
560                            "effect invocation `{effect}.{operation}` is observational and may not be invoked with `check`"
561                        )));
562                    }
563                    Ok(())
564                }
565                super::AuthorityExpr::Observe {
566                    effect, operation, ..
567                } => {
568                    if !used.contains(effect) {
569                        return Err(ValidationError::ExtensionError(format!(
570                            "effect invocation `{effect}.{operation}` is not allowed without `uses {effect}`"
571                        )));
572                    }
573                    let Some(ops) = effect_ops.get(effect) else {
574                        return Err(ValidationError::ExtensionError(format!(
575                            "effect invocation references undeclared interface `{effect}`"
576                        )));
577                    };
578                    let Some(op_decl) = ops.get(operation) else {
579                        return Err(ValidationError::ExtensionError(format!(
580                            "effect invocation references undeclared operation `{effect}.{operation}`"
581                        )));
582                    };
583                    if !matches!(op_decl.authority_class, EffectAuthorityClass::Observe) {
584                        return Err(ValidationError::ExtensionError(format!(
585                            "effect invocation `{effect}.{operation}` is not observational and may not be invoked with `observe`"
586                        )));
587                    }
588                    Ok(())
589                }
590                super::AuthorityExpr::Var(_)
591                | super::AuthorityExpr::Transfer { .. }
592                | super::AuthorityExpr::Constructor { .. }
593                | super::AuthorityExpr::Call { .. } => Ok(()),
594            }
595        }
596
597        fn validate_protocol_effects(
598            protocol: &Protocol,
599            effect_ops: &HashMap<String, HashMap<String, EffectOperationDeclaration>>,
600            used: &BTreeSet<String>,
601        ) -> Result<(), ValidationError> {
602            match protocol {
603                Protocol::Begin { continuation, .. }
604                | Protocol::Await { continuation, .. }
605                | Protocol::Resolve { continuation, .. }
606                | Protocol::Invalidate { continuation, .. }
607                | Protocol::Send { continuation, .. }
608                | Protocol::Broadcast { continuation, .. }
609                | Protocol::Extension { continuation, .. }
610                | Protocol::Let { continuation, .. }
611                | Protocol::Publish { continuation, .. }
612                | Protocol::PublishAuthority { continuation, .. }
613                | Protocol::Materialize { continuation, .. }
614                | Protocol::Handoff { continuation, .. }
615                | Protocol::DependentWork { continuation, .. } => {
616                    if let Protocol::Let { mode, expr, .. } = protocol {
617                        validate_expr(expr, effect_ops, used)?;
618                        match (mode, expr) {
619                            (
620                                super::AuthorityBindingMode::Plain,
621                                super::AuthorityExpr::Check {
622                                    effect, operation, ..
623                                },
624                            ) => {
625                                let op_decl = effect_ops
626                                    .get(effect)
627                                    .and_then(|ops| ops.get(operation))
628                                    .expect("validated effect operation should exist");
629                                if matches!(
630                                    op_decl.authority_class,
631                                    EffectAuthorityClass::Authoritative
632                                ) {
633                                    return Err(ValidationError::ExtensionError(format!(
634                                        "authoritative effect invocation `{effect}.{operation}` must bind through `authoritative let`"
635                                    )));
636                                }
637                            }
638                            (
639                                super::AuthorityBindingMode::Plain,
640                                super::AuthorityExpr::Observe { .. },
641                            ) => {
642                                return Err(ValidationError::ExtensionError(
643                                    "`observe` expressions must bind through `observe let`"
644                                        .to_string(),
645                                ));
646                            }
647                            (
648                                super::AuthorityBindingMode::Authoritative,
649                                super::AuthorityExpr::Check {
650                                    effect, operation, ..
651                                },
652                            ) => {
653                                let op_decl = effect_ops
654                                    .get(effect)
655                                    .and_then(|ops| ops.get(operation))
656                                    .expect("validated effect operation should exist");
657                                if !matches!(
658                                    op_decl.authority_class,
659                                    EffectAuthorityClass::Authoritative
660                                ) {
661                                    return Err(ValidationError::ExtensionError(format!(
662                                        "`authoritative let` may only bind authoritative effect invocations; `{effect}.{operation}` is {:?}",
663                                        op_decl.authority_class
664                                    )));
665                                }
666                            }
667                            (
668                                super::AuthorityBindingMode::Observe,
669                                super::AuthorityExpr::Observe { .. },
670                            ) => {}
671                            (super::AuthorityBindingMode::Plain, _) => {}
672                            (super::AuthorityBindingMode::Authoritative, _) => {
673                                return Err(ValidationError::ExtensionError(
674                                    "`authoritative let` must bind a `check` expression"
675                                        .to_string(),
676                                ));
677                            }
678                            (super::AuthorityBindingMode::Observe, _) => {
679                                return Err(ValidationError::ExtensionError(
680                                    "`observe let` must bind an `observe` expression".to_string(),
681                                ));
682                            }
683                        }
684                    }
685                    validate_protocol_effects(continuation, effect_ops, used)
686                }
687                Protocol::Choice { branches, .. } => {
688                    for branch in branches {
689                        if let Some(ChoiceGuard::Evidence {
690                            effect, operation, ..
691                        }) = &branch.guard
692                        {
693                            if !used.contains(effect) {
694                                return Err(ValidationError::ExtensionError(format!(
695                                    "effect guard `{effect}.{operation}` is not allowed without `uses {effect}`"
696                                )));
697                            }
698                            let Some(ops) = effect_ops.get(effect) else {
699                                return Err(ValidationError::ExtensionError(format!(
700                                    "effect guard references undeclared interface `{effect}`"
701                                )));
702                            };
703                            let Some(op_decl) = ops.get(operation) else {
704                                return Err(ValidationError::ExtensionError(format!(
705                                    "effect guard references undeclared operation `{effect}.{operation}`"
706                                )));
707                            };
708                            if matches!(op_decl.authority_class, EffectAuthorityClass::Observe) {
709                                return Err(ValidationError::ExtensionError(format!(
710                                    "effect guard `{effect}.{operation}` is observational and may not be invoked with `check`"
711                                )));
712                            }
713                        }
714                        validate_protocol_effects(&branch.protocol, effect_ops, used)?;
715                    }
716                    Ok(())
717                }
718                Protocol::Case { expr, branches } => {
719                    validate_expr(expr, effect_ops, used)?;
720                    for branch in branches {
721                        validate_protocol_effects(&branch.protocol, effect_ops, used)?;
722                    }
723                    Ok(())
724                }
725                Protocol::Timeout {
726                    body,
727                    on_timeout,
728                    on_cancel,
729                    ..
730                } => {
731                    validate_protocol_effects(body, effect_ops, used)?;
732                    validate_protocol_effects(on_timeout, effect_ops, used)?;
733                    if let Some(on_cancel) = on_cancel.as_deref() {
734                        validate_protocol_effects(on_cancel, effect_ops, used)?;
735                    }
736                    Ok(())
737                }
738                Protocol::Loop { body, .. } | Protocol::Rec { body, .. } => {
739                    validate_protocol_effects(body, effect_ops, used)
740                }
741                Protocol::Parallel { protocols } => {
742                    for protocol in protocols {
743                        validate_protocol_effects(protocol, effect_ops, used)?;
744                    }
745                    Ok(())
746                }
747                Protocol::Var(_) | Protocol::End => Ok(()),
748            }
749        }
750
751        validate_protocol_effects(&self.protocol, &effect_ops, &used)
752    }
753
754    fn validate_execution_profile_surface(&self) -> Result<(), ValidationError> {
755        let mut declared = BTreeSet::new();
756        for profile in self.execution_profile_declarations() {
757            if !declared.insert(profile.name.clone()) {
758                return Err(ValidationError::ExtensionError(format!(
759                    "duplicate execution profile declaration `{}`",
760                    profile.name
761                )));
762            }
763        }
764
765        for profile in self.protocol_execution_profiles() {
766            if !declared.contains(&profile) {
767                return Err(ValidationError::ExtensionError(format!(
768                    "protocol references undeclared execution profile `{profile}`"
769                )));
770            }
771        }
772
773        Ok(())
774    }
775
776    fn validate_operation_surface(&self) -> Result<(), ValidationError> {
777        let declared_agreement_profiles = self
778            .agreement_profile_declarations()
779            .into_iter()
780            .map(|profile| profile.name)
781            .collect::<BTreeSet<_>>();
782
783        for operation in self.operation_declarations() {
784            if operation.progress_contract.is_none() {
785                return Err(ValidationError::ExtensionError(format!(
786                    "operation `{}` is parity-critical and must declare a progress contract",
787                    operation.name
788                )));
789            }
790            if !operation
791                .progress_contract
792                .as_ref()
793                .is_some_and(|progress| progress.is_explicit())
794            {
795                return Err(ValidationError::ExtensionError(format!(
796                    "operation `{}` must declare explicit progress metadata using `requires`, `within`, `on timeout`, or `on stall`",
797                    operation.name
798                )));
799            }
800            let Some(agreement) = operation.agreement.as_ref() else {
801                return Err(ValidationError::ExtensionError(format!(
802                    "operation `{}` must attach a named agreement profile",
803                    operation.name
804                )));
805            };
806            if !declared_agreement_profiles.contains(&agreement.profile_name) {
807                return Err(ValidationError::ExtensionError(format!(
808                    "operation `{}` references undeclared agreement profile `{}`",
809                    operation.name, agreement.profile_name
810                )));
811            }
812        }
813        Ok(())
814    }
815
816    #[must_use]
817    pub fn language_tier_status(&self) -> LanguageTierStatus {
818        let theory_convertible = super::convert::protocol_to_global(&self.protocol).is_ok();
819        let session_blocker = find_session_projection_blocker(&self.protocol);
820        let missing_progress = self
821            .operation_declarations()
822            .iter()
823            .find(|operation| {
824                operation.progress_contract.is_none()
825                    || !operation
826                        .progress_contract
827                        .as_ref()
828                        .is_some_and(|progress| progress.is_explicit())
829            })
830            .map(|operation| {
831                format!(
832                    "operation `{}` is missing the required progress contract",
833                    operation.name
834                )
835            });
836
837        let protocol_machine_executable = missing_progress.is_none();
838        let session_projectable = session_blocker.is_none();
839        let strongest_tier = match (session_projectable, protocol_machine_executable) {
840            (true, true) => LanguageTier::SessionProjectable,
841            (false, true) => LanguageTier::ProtocolMachineExecutable,
842            (_, false) => LanguageTier::ProofOnly,
843        };
844        let diagnostic = if let Some(blocker) = session_blocker {
845            format!(
846                "full spec is valid, protocol-machine execution is available, but session projection is blocked: {blocker}"
847            )
848        } else if let Some(missing_progress) = missing_progress {
849            format!(
850                "full spec is valid for proof analysis only; protocol-machine execution is blocked: {missing_progress}"
851            )
852        } else if !theory_convertible {
853            "full spec is valid, protocol-machine execution is available, and the protocol is session-projectable, but theory conversion remains explicitly unavailable for this surface".to_string()
854        } else {
855            "full spec is valid, protocol-machine execution is available, the protocol is session-projectable, and theory conversion is available".to_string()
856        };
857
858        LanguageTierStatus {
859            strongest_tier,
860            session_projectable,
861            protocol_machine_executable,
862            theory_convertible,
863            proof_only: matches!(strongest_tier, LanguageTier::ProofOnly),
864            diagnostic,
865        }
866    }
867
868    /// Get choreography-level attributes/annotations
869    #[must_use]
870    pub fn get_attributes(&self) -> &HashMap<String, String> {
871        &self.attrs
872    }
873
874    /// Get mutable reference to choreography-level attributes
875    pub fn get_attributes_mut(&mut self) -> &mut HashMap<String, String> {
876        &mut self.attrs
877    }
878
879    /// Get a specific choreography attribute
880    #[must_use]
881    pub fn get_attribute(&self, key: &str) -> Option<&String> {
882        self.attrs.get(key)
883    }
884
885    /// Set a choreography attribute
886    pub fn set_attribute(&mut self, key: String, value: String) {
887        self.attrs.insert(key, value);
888    }
889
890    /// Remove a choreography attribute
891    pub fn remove_attribute(&mut self, key: &str) -> Option<String> {
892        self.attrs.remove(key)
893    }
894
895    /// Check if choreography has a specific attribute
896    #[must_use]
897    pub fn has_attribute(&self, key: &str) -> bool {
898        self.attrs.contains_key(key)
899    }
900
901    /// Get attribute as a specific type
902    pub fn get_attribute_as<T>(&self, key: &str) -> Option<T>
903    where
904        T: std::str::FromStr,
905    {
906        self.get_attribute(key)?.parse().ok()
907    }
908
909    /// Get attribute as boolean
910    pub fn get_attribute_as_bool(&self, key: &str) -> Option<bool> {
911        let value = self.get_attribute(key)?;
912        match value.to_lowercase().as_str() {
913            "true" | "1" | "yes" | "on" => Some(true),
914            "false" | "0" | "no" | "off" => Some(false),
915            _ => None,
916        }
917    }
918
919    /// Clear all choreography attributes
920    pub fn clear_attributes(&mut self) {
921        self.attrs.clear();
922    }
923
924    /// Count of choreography attributes
925    pub fn attribute_count(&self) -> usize {
926        self.attrs.len()
927    }
928
929    /// Get all attribute keys
930    pub fn attribute_keys(&self) -> Vec<&String> {
931        self.attrs.keys().collect()
932    }
933
934    /// Validate that required attributes are present
935    pub fn validate_required_attributes(&self, required_keys: &[&str]) -> Result<(), Vec<String>> {
936        let missing: Vec<String> = required_keys
937            .iter()
938            .filter(|&key| !self.has_attribute(key))
939            .map(|&key| key.to_string())
940            .collect();
941
942        if missing.is_empty() {
943            Ok(())
944        } else {
945            Err(missing)
946        }
947    }
948
949    /// Find all protocol nodes with a specific annotation
950    pub fn find_nodes_with_annotation(&self, key: &str) -> Vec<&Protocol> {
951        let mut nodes = Vec::new();
952        self.protocol.collect_nodes_with_annotation(key, &mut nodes);
953        nodes
954    }
955
956    /// Find all protocol nodes with a specific annotation value
957    pub fn find_nodes_with_annotation_value(&self, key: &str, value: &str) -> Vec<&Protocol> {
958        let mut nodes = Vec::new();
959        self.protocol
960            .collect_nodes_with_annotation_value(key, value, &mut nodes);
961        nodes
962    }
963
964    /// Count total annotations across the entire choreography
965    pub fn total_annotation_count(&self) -> usize {
966        self.attribute_count() + self.protocol.deep_annotation_count()
967    }
968
969    /// Set theorem-pack declarations for this choreography.
970    pub fn set_theorem_packs(
971        &mut self,
972        theorem_packs: &[TheoremPackDeclaration],
973    ) -> Result<(), String> {
974        let encoded = serde_json::to_string(theorem_packs)
975            .map_err(|e| format!("encode theorem packs: {e}"))?;
976        self.attrs.insert(ATTR_THEOREM_PACKS.to_string(), encoded);
977        Ok(())
978    }
979
980    /// Get typed theorem-pack declarations.
981    #[must_use]
982    pub fn theorem_packs(&self) -> Vec<TheoremPackDeclaration> {
983        self.attrs
984            .get(ATTR_THEOREM_PACKS)
985            .and_then(|s| serde_json::from_str::<Vec<TheoremPackDeclaration>>(s).ok())
986            .unwrap_or_default()
987    }
988
989    /// Set protocol-required theorem packs.
990    pub fn set_required_theorem_packs(&mut self, required: &[String]) -> Result<(), String> {
991        let encoded = serde_json::to_string(required)
992            .map_err(|e| format!("encode required theorem packs: {e}"))?;
993        self.attrs
994            .insert(ATTR_REQUIRED_THEOREM_PACKS.to_string(), encoded);
995        Ok(())
996    }
997
998    /// Get protocol-required theorem packs.
999    #[must_use]
1000    pub fn required_theorem_packs(&self) -> Vec<String> {
1001        self.attrs
1002            .get(ATTR_REQUIRED_THEOREM_PACKS)
1003            .and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
1004            .unwrap_or_default()
1005    }
1006
1007    /// Set inferred protocol-required theorem packs.
1008    pub fn set_inferred_required_theorem_packs(
1009        &mut self,
1010        required: &[String],
1011    ) -> Result<(), String> {
1012        let encoded = serde_json::to_string(required)
1013            .map_err(|e| format!("encode inferred theorem packs: {e}"))?;
1014        self.attrs
1015            .insert(ATTR_INFERRED_REQUIRED_THEOREM_PACKS.to_string(), encoded);
1016        Ok(())
1017    }
1018
1019    /// Get inferred protocol-required theorem packs.
1020    #[must_use]
1021    pub fn inferred_required_theorem_packs(&self) -> Vec<String> {
1022        self.attrs
1023            .get(ATTR_INFERRED_REQUIRED_THEOREM_PACKS)
1024            .and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
1025            .unwrap_or_default()
1026    }
1027
1028    /// Set role-set declarations for this choreography.
1029    pub fn set_role_sets(&mut self, role_sets: &[RoleSetDeclaration]) -> Result<(), String> {
1030        let encoded =
1031            serde_json::to_string(role_sets).map_err(|e| format!("encode role sets: {e}"))?;
1032        self.attrs.insert(ATTR_ROLE_SETS.to_string(), encoded);
1033        Ok(())
1034    }
1035
1036    /// Get typed role-set declarations.
1037    #[must_use]
1038    pub fn role_sets(&self) -> Vec<RoleSetDeclaration> {
1039        self.attrs
1040            .get(ATTR_ROLE_SETS)
1041            .and_then(|s| serde_json::from_str::<Vec<RoleSetDeclaration>>(s).ok())
1042            .unwrap_or_default()
1043    }
1044
1045    /// Set topology declarations for this choreography.
1046    pub fn set_topologies(&mut self, topologies: &[TopologyDeclaration]) -> Result<(), String> {
1047        let encoded =
1048            serde_json::to_string(topologies).map_err(|e| format!("encode topologies: {e}"))?;
1049        self.attrs.insert(ATTR_TOPOLOGIES.to_string(), encoded);
1050        Ok(())
1051    }
1052
1053    /// Get typed topology declarations.
1054    #[must_use]
1055    pub fn topologies(&self) -> Vec<TopologyDeclaration> {
1056        self.attrs
1057            .get(ATTR_TOPOLOGIES)
1058            .and_then(|s| serde_json::from_str::<Vec<TopologyDeclaration>>(s).ok())
1059            .unwrap_or_default()
1060    }
1061
1062    /// Set nominal type declarations for this choreography.
1063    pub fn set_type_declarations(&mut self, decls: &[TypeDeclaration]) -> Result<(), String> {
1064        let encoded =
1065            serde_json::to_string(decls).map_err(|e| format!("encode type declarations: {e}"))?;
1066        self.attrs
1067            .insert(ATTR_TYPE_DECLARATIONS.to_string(), encoded);
1068        Ok(())
1069    }
1070
1071    /// Get nominal type declarations.
1072    #[must_use]
1073    pub fn type_declarations(&self) -> Vec<TypeDeclaration> {
1074        self.attrs
1075            .get(ATTR_TYPE_DECLARATIONS)
1076            .and_then(|s| serde_json::from_str::<Vec<TypeDeclaration>>(s).ok())
1077            .unwrap_or_default()
1078    }
1079
1080    /// Set nominal effect interface declarations for this choreography.
1081    pub fn set_effect_interface_declarations(
1082        &mut self,
1083        decls: &[EffectInterfaceDeclaration],
1084    ) -> Result<(), String> {
1085        let encoded =
1086            serde_json::to_string(decls).map_err(|e| format!("encode effect declarations: {e}"))?;
1087        self.attrs
1088            .insert(ATTR_EFFECT_INTERFACE_DECLARATIONS.to_string(), encoded);
1089        Ok(())
1090    }
1091
1092    /// Get nominal effect interface declarations.
1093    #[must_use]
1094    pub fn effect_interface_declarations(&self) -> Vec<EffectInterfaceDeclaration> {
1095        self.attrs
1096            .get(ATTR_EFFECT_INTERFACE_DECLARATIONS)
1097            .and_then(|s| serde_json::from_str::<Vec<EffectInterfaceDeclaration>>(s).ok())
1098            .unwrap_or_default()
1099    }
1100
1101    /// Derive runtime-facing effect metadata from nominal effect declarations
1102    /// and protocol `uses` dependencies.
1103    #[must_use]
1104    pub fn effect_contract_declarations(&self) -> Vec<EffectContractDeclaration> {
1105        let used: BTreeSet<String> = self.protocol_uses().into_iter().collect();
1106        self.effect_interface_declarations()
1107            .into_iter()
1108            .flat_map(|effect| {
1109                let is_used = used.contains(effect.name.as_str());
1110                effect.operations.into_iter().map(move |op| {
1111                    let admissibility = if is_used {
1112                        "declared_use_only"
1113                    } else {
1114                        "internal_only"
1115                    };
1116                    let (totality, timeout_policy) = match op.progress.as_str() {
1117                        "immediate" => ("immediate", "none"),
1118                        "may_block" => ("may_block", "required"),
1119                        _ => ("may_block", "required"),
1120                    };
1121                    let handler_domain = if is_used { "external" } else { "internal" };
1122                    EffectContractDeclaration {
1123                        interface_name: effect.name.clone(),
1124                        operation_name: op.name,
1125                        authority_class: op.authority_class,
1126                        semantic_class: op.semantic_class.clone(),
1127                        progress: op.progress.clone(),
1128                        region: op.region.clone(),
1129                        agreement_use: op.agreement_use.clone(),
1130                        admissibility: admissibility.to_string(),
1131                        totality: totality.to_string(),
1132                        timeout_policy: timeout_policy.to_string(),
1133                        reentrancy_policy: op.reentrancy,
1134                        handler_domain: handler_domain.to_string(),
1135                    }
1136                })
1137            })
1138            .collect()
1139    }
1140
1141    /// Set explicit protocol effect dependencies.
1142    pub fn set_protocol_uses(&mut self, uses: &[String]) -> Result<(), String> {
1143        let encoded =
1144            serde_json::to_string(uses).map_err(|e| format!("encode protocol uses: {e}"))?;
1145        self.attrs.insert(ATTR_PROTOCOL_USES.to_string(), encoded);
1146        Ok(())
1147    }
1148
1149    /// Get explicit protocol effect dependencies.
1150    #[must_use]
1151    pub fn protocol_uses(&self) -> Vec<String> {
1152        self.attrs
1153            .get(ATTR_PROTOCOL_USES)
1154            .and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
1155            .unwrap_or_default()
1156    }
1157
1158    /// Set region declarations for this choreography.
1159    pub fn set_region_declarations(&mut self, decls: &[RegionDeclaration]) -> Result<(), String> {
1160        let encoded =
1161            serde_json::to_string(decls).map_err(|e| format!("encode region declarations: {e}"))?;
1162        self.attrs
1163            .insert(ATTR_REGION_DECLARATIONS.to_string(), encoded);
1164        Ok(())
1165    }
1166
1167    /// Get region declarations.
1168    #[must_use]
1169    pub fn region_declarations(&self) -> Vec<RegionDeclaration> {
1170        self.attrs
1171            .get(ATTR_REGION_DECLARATIONS)
1172            .and_then(|s| serde_json::from_str::<Vec<RegionDeclaration>>(s).ok())
1173            .unwrap_or_default()
1174    }
1175
1176    /// Set operation declarations for this choreography.
1177    pub fn set_operation_declarations(
1178        &mut self,
1179        decls: &[OperationDeclaration],
1180    ) -> Result<(), String> {
1181        let encoded = serde_json::to_string(decls)
1182            .map_err(|e| format!("encode operation declarations: {e}"))?;
1183        self.attrs
1184            .insert(ATTR_OPERATION_DECLARATIONS.to_string(), encoded);
1185        Ok(())
1186    }
1187
1188    /// Get operation declarations.
1189    #[must_use]
1190    pub fn operation_declarations(&self) -> Vec<OperationDeclaration> {
1191        self.attrs
1192            .get(ATTR_OPERATION_DECLARATIONS)
1193            .and_then(|s| serde_json::from_str::<Vec<OperationDeclaration>>(s).ok())
1194            .unwrap_or_default()
1195    }
1196
1197    /// Set guest-runtime declarations for this choreography.
1198    pub fn set_guest_runtime_declarations(
1199        &mut self,
1200        decls: &[GuestRuntimeDeclaration],
1201    ) -> Result<(), String> {
1202        let encoded = serde_json::to_string(decls)
1203            .map_err(|e| format!("encode guest runtime declarations: {e}"))?;
1204        self.attrs
1205            .insert(ATTR_GUEST_RUNTIME_DECLARATIONS.to_string(), encoded);
1206        Ok(())
1207    }
1208
1209    /// Get guest-runtime declarations.
1210    #[must_use]
1211    pub fn guest_runtime_declarations(&self) -> Vec<GuestRuntimeDeclaration> {
1212        self.attrs
1213            .get(ATTR_GUEST_RUNTIME_DECLARATIONS)
1214            .and_then(|s| serde_json::from_str::<Vec<GuestRuntimeDeclaration>>(s).ok())
1215            .unwrap_or_default()
1216    }
1217
1218    /// Set execution-profile declarations for this choreography.
1219    pub fn set_execution_profile_declarations(
1220        &mut self,
1221        decls: &[ExecutionProfileDeclaration],
1222    ) -> Result<(), String> {
1223        let encoded = serde_json::to_string(decls)
1224            .map_err(|e| format!("encode execution profile declarations: {e}"))?;
1225        self.attrs
1226            .insert(ATTR_EXECUTION_PROFILE_DECLARATIONS.to_string(), encoded);
1227        Ok(())
1228    }
1229
1230    /// Get execution-profile declarations.
1231    #[must_use]
1232    pub fn execution_profile_declarations(&self) -> Vec<ExecutionProfileDeclaration> {
1233        self.attrs
1234            .get(ATTR_EXECUTION_PROFILE_DECLARATIONS)
1235            .and_then(|s| serde_json::from_str::<Vec<ExecutionProfileDeclaration>>(s).ok())
1236            .unwrap_or_default()
1237    }
1238
1239    /// Set protocol-selected execution profiles.
1240    pub fn set_protocol_execution_profiles(&mut self, profiles: &[String]) -> Result<(), String> {
1241        let encoded = serde_json::to_string(profiles)
1242            .map_err(|e| format!("encode protocol execution profiles: {e}"))?;
1243        self.attrs
1244            .insert(ATTR_PROTOCOL_EXECUTION_PROFILES.to_string(), encoded);
1245        Ok(())
1246    }
1247
1248    /// Get protocol-selected execution profiles.
1249    #[must_use]
1250    pub fn protocol_execution_profiles(&self) -> Vec<String> {
1251        self.attrs
1252            .get(ATTR_PROTOCOL_EXECUTION_PROFILES)
1253            .and_then(|s| serde_json::from_str::<Vec<String>>(s).ok())
1254            .unwrap_or_default()
1255    }
1256
1257    /// Set reusable agreement-profile declarations for this choreography.
1258    pub fn set_agreement_profile_declarations(
1259        &mut self,
1260        decls: &[AgreementProfileDeclaration],
1261    ) -> Result<(), String> {
1262        let encoded = serde_json::to_string(decls)
1263            .map_err(|e| format!("encode agreement profile declarations: {e}"))?;
1264        self.attrs
1265            .insert(ATTR_AGREEMENT_PROFILE_DECLARATIONS.to_string(), encoded);
1266        Ok(())
1267    }
1268
1269    /// Get reusable agreement-profile declarations.
1270    #[must_use]
1271    pub fn agreement_profile_declarations(&self) -> Vec<AgreementProfileDeclaration> {
1272        self.attrs
1273            .get(ATTR_AGREEMENT_PROFILE_DECLARATIONS)
1274            .and_then(|s| serde_json::from_str::<Vec<AgreementProfileDeclaration>>(s).ok())
1275            .unwrap_or_default()
1276    }
1277
1278    fn required_theorem_pack_capabilities(&self) -> BTreeSet<String> {
1279        let required = self.required_theorem_packs();
1280        let required_set: BTreeSet<&str> = required.iter().map(String::as_str).collect();
1281        self.theorem_packs()
1282            .into_iter()
1283            .filter(|bundle| required_set.contains(bundle.name.as_str()))
1284            .flat_map(|bundle| bundle.capabilities.into_iter())
1285            .collect()
1286    }
1287
1288    fn required_protocol_machine_core_capabilities(&self) -> BTreeSet<String> {
1289        fn collect(protocol: &Protocol, out: &mut BTreeSet<String>) {
1290            if let Some(cap) = protocol.get_annotations().custom("required_capability") {
1291                out.insert(cap.to_string());
1292            }
1293            match protocol {
1294                Protocol::Begin { continuation, .. }
1295                | Protocol::Await { continuation, .. }
1296                | Protocol::Resolve { continuation, .. }
1297                | Protocol::Invalidate { continuation, .. }
1298                | Protocol::Send { continuation, .. }
1299                | Protocol::Broadcast { continuation, .. }
1300                | Protocol::Extension { continuation, .. }
1301                | Protocol::Publish { continuation, .. }
1302                | Protocol::PublishAuthority { continuation, .. }
1303                | Protocol::Materialize { continuation, .. }
1304                | Protocol::Handoff { continuation, .. }
1305                | Protocol::DependentWork { continuation, .. } => collect(continuation, out),
1306                Protocol::Choice { branches, .. } => {
1307                    for branch in branches {
1308                        collect(&branch.protocol, out);
1309                    }
1310                }
1311                Protocol::Loop { body, .. } | Protocol::Rec { body, .. } => collect(body, out),
1312                Protocol::Let { continuation, .. } => collect(continuation, out),
1313                Protocol::Case { branches, .. } => {
1314                    for branch in branches {
1315                        collect(&branch.protocol, out);
1316                    }
1317                }
1318                Protocol::Timeout {
1319                    body,
1320                    on_timeout,
1321                    on_cancel,
1322                    ..
1323                } => {
1324                    collect(body, out);
1325                    collect(on_timeout, out);
1326                    if let Some(on_cancel) = on_cancel.as_deref() {
1327                        collect(on_cancel, out);
1328                    }
1329                }
1330                Protocol::Parallel { protocols } => {
1331                    for p in protocols {
1332                        collect(p, out);
1333                    }
1334                }
1335                Protocol::Var(_) | Protocol::End => {}
1336            }
1337        }
1338
1339        let mut out = BTreeSet::new();
1340        collect(&self.protocol, &mut out);
1341        out
1342    }
1343}
1344
1345impl ProgressAttachment {
1346    /// Whether this attachment carries explicit progress policy rather than a bare name.
1347    #[must_use]
1348    pub fn is_explicit(&self) -> bool {
1349        self.requires_profile.is_some()
1350            || self.within_window.is_some()
1351            || self.on_timeout.is_some()
1352            || self.on_stall.is_some()
1353    }
1354}
1355
1356fn find_session_projection_blocker(protocol: &Protocol) -> Option<&'static str> {
1357    match protocol {
1358        Protocol::Begin { .. } => Some("begin requires protocol-machine commitment semantics"),
1359        Protocol::Await { .. } => Some("await requires protocol-machine commitment semantics"),
1360        Protocol::Resolve { .. } => Some("resolve requires protocol-machine commitment semantics"),
1361        Protocol::Invalidate { .. } => {
1362            Some("invalidate requires protocol-machine commitment semantics")
1363        }
1364        Protocol::Case { .. } => Some("authority-local case/of requires protocol-machine lowering"),
1365        Protocol::Timeout { .. } => Some("timeout requires protocol-machine progress semantics"),
1366        Protocol::Publish { .. } => Some("publish requires publication/materialization semantics"),
1367        Protocol::PublishAuthority { .. } => {
1368            Some("publish requires publication/materialization semantics")
1369        }
1370        Protocol::Materialize { .. } => {
1371            Some("materialize requires publication/materialization semantics")
1372        }
1373        Protocol::Handoff { .. } => Some("handoff requires semantic handoff semantics"),
1374        Protocol::DependentWork { .. } => {
1375            Some("dependent work requires protocol-machine commitment semantics")
1376        }
1377        Protocol::Send { continuation, .. }
1378        | Protocol::Broadcast { continuation, .. }
1379        | Protocol::Let { continuation, .. }
1380        | Protocol::Extension { continuation, .. } => find_session_projection_blocker(continuation),
1381        Protocol::Choice { branches, .. } => branches
1382            .iter()
1383            .find_map(|branch| find_session_projection_blocker(&branch.protocol)),
1384        Protocol::Loop { body, .. } | Protocol::Rec { body, .. } => {
1385            find_session_projection_blocker(body)
1386        }
1387        Protocol::Parallel { protocols } => {
1388            protocols.iter().find_map(find_session_projection_blocker)
1389        }
1390        Protocol::Var(_) | Protocol::End => None,
1391    }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396    use super::*;
1397    use quote::format_ident;
1398
1399    #[test]
1400    fn proof_bundle_metadata_roundtrip() {
1401        let mut choreo = Choreography {
1402            name: format_ident!("RoundTrip"),
1403            namespace: None,
1404            roles: Vec::new(),
1405            protocol: Protocol::End,
1406            attrs: HashMap::new(),
1407        };
1408        let bundles = vec![
1409            TheoremPackDeclaration {
1410                name: "Base".to_string(),
1411                capabilities: vec!["delegation".to_string()],
1412                version: None,
1413                issuer: None,
1414                constraints: Vec::new(),
1415            },
1416            TheoremPackDeclaration {
1417                name: "Guard".to_string(),
1418                capabilities: vec!["guard_tokens".to_string()],
1419                version: None,
1420                issuer: None,
1421                constraints: Vec::new(),
1422            },
1423        ];
1424        let required = vec!["Base".to_string()];
1425
1426        choreo
1427            .set_theorem_packs(&bundles)
1428            .expect("set proof bundles");
1429        choreo
1430            .set_required_theorem_packs(&required)
1431            .expect("set required bundles");
1432
1433        assert_eq!(choreo.theorem_packs(), bundles);
1434        assert_eq!(choreo.required_theorem_packs(), required);
1435    }
1436}