Skip to main content

bock_air/
validate_context.rs

1//! Context validation pass — validates context annotations across the AIR tree.
2//!
3//! This pass runs after [`crate::context::interpret_context`] and checks:
4//! 1. **Capability consistency**: child `@requires` propagate to parents (additive).
5//! 2. **Security consistency**: security levels don't contradict in parent-child.
6//! 3. **Performance budget validity**: values are positive and well-formed.
7//! 4. **Completeness**: in strict mode, public items must have context annotations.
8
9use std::collections::HashSet;
10
11use bock_ast::Visibility;
12use bock_errors::{DiagnosticBag, DiagnosticCode};
13
14use crate::node::{AIRNode, NodeKind};
15use crate::stubs::{security_level_rank, Capability, ContextBlock, SecurityInfo, SECURITY_LEVELS};
16
17/// Strictness level for context validation.
18///
19/// Three profiles with increasing strictness:
20/// - **Lax** (sketch mode): only error-level validations — contradictions, invalid
21///   values. No completeness warnings. Auto-inference is assumed at this level.
22/// - **Standard** (development mode): lax checks + **warnings** on public items
23///   and modules missing context annotations.
24/// - **Strict** (production mode): standard checks but missing context annotations
25///   become **errors** instead of warnings.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StrictnessLevel {
28    /// Lax / Sketch: only error-level validations (contradictions, invalid values).
29    /// No completeness warnings; auto-infer capabilities.
30    Lax,
31    /// Standard / Development: lax + warnings for public items without context
32    /// and undeclared effects.
33    Standard,
34    /// Strict / Production: standard but missing context / undeclared effects
35    /// are errors, not warnings.
36    Strict,
37}
38
39impl StrictnessLevel {
40    /// Map a profile name string to a strictness level.
41    ///
42    /// Recognised names (case-insensitive):
43    /// - `"sketch"` / `"lax"` → [`Lax`](StrictnessLevel::Lax)
44    /// - `"development"` / `"standard"` → [`Standard`](StrictnessLevel::Standard)
45    /// - `"production"` / `"strict"` → [`Strict`](StrictnessLevel::Strict)
46    ///
47    /// Returns `None` for unrecognised names.
48    #[must_use]
49    pub fn from_name(name: &str) -> Option<Self> {
50        match name.to_ascii_lowercase().as_str() {
51            "sketch" | "lax" => Some(Self::Lax),
52            "development" | "standard" => Some(Self::Standard),
53            "production" | "strict" => Some(Self::Strict),
54            _ => None,
55        }
56    }
57}
58
59/// Validates context annotations across the AIR tree.
60///
61/// Walks the tree and checks:
62/// - Security levels are consistent (children don't have lower sensitivity than parents).
63/// - Capabilities propagate upward correctly.
64/// - Performance budget values are positive.
65/// - In strict mode: all public items and modules have context annotations.
66///
67/// Returns a [`DiagnosticBag`] with any errors/warnings.
68#[must_use]
69pub fn validate_context(root: &AIRNode, strictness: StrictnessLevel) -> DiagnosticBag {
70    let mut diags = DiagnosticBag::new();
71    validate_node(root, None, &HashSet::new(), strictness, &mut diags);
72    diags
73}
74
75/// Validate a single node and recurse into children.
76///
77/// `parent_security` is the security info inherited from the nearest ancestor with one.
78/// `parent_capabilities` is the union of all ancestor-declared capabilities.
79fn validate_node(
80    node: &AIRNode,
81    parent_security: Option<&SecurityInfo>,
82    parent_capabilities: &HashSet<Capability>,
83    strictness: StrictnessLevel,
84    diags: &mut DiagnosticBag,
85) {
86    // Compute the effective security and capabilities for this node.
87    let node_security = node
88        .context
89        .as_ref()
90        .and_then(|c| c.security.as_ref())
91        .or(parent_security);
92
93    // @requires is additive: child capabilities union with parent capabilities.
94    let mut effective_caps = parent_capabilities.clone();
95    if let Some(ctx) = &node.context {
96        for cap in &ctx.capabilities {
97            effective_caps.insert(cap.clone());
98        }
99    }
100
101    // Validate this node's context block.
102    if let Some(ctx) = &node.context {
103        validate_security_consistency(ctx, parent_security, node.span, diags);
104        validate_performance_budget(ctx, node.span, diags);
105        validate_security_level_known(ctx, node.span, diags);
106    }
107
108    // Completeness checking in standard and strict modes.
109    if strictness == StrictnessLevel::Standard || strictness == StrictnessLevel::Strict {
110        validate_completeness(node, strictness, diags);
111    }
112
113    // Recurse into children.
114    validate_children(node, node_security, &effective_caps, strictness, diags);
115}
116
117/// Check that a node's security level doesn't contradict its parent's.
118///
119/// A child with *lower* sensitivity than its parent is a contradiction:
120/// the child would leak the parent's classification.
121fn validate_security_consistency(
122    ctx: &ContextBlock,
123    parent_security: Option<&SecurityInfo>,
124    span: bock_errors::Span,
125    diags: &mut DiagnosticBag,
126) {
127    let Some(child_sec) = &ctx.security else {
128        return;
129    };
130    let Some(parent_sec) = parent_security else {
131        return;
132    };
133
134    let parent_rank = security_level_rank(&parent_sec.level);
135    let child_rank = security_level_rank(&child_sec.level);
136
137    if let (Some(p), Some(c)) = (parent_rank, child_rank) {
138        if c < p {
139            diags.error(
140                DiagnosticCode {
141                    prefix: 'E',
142                    number: 8011,
143                },
144                format!(
145                    "security level `{}` is less restrictive than parent level `{}`",
146                    child_sec.level, parent_sec.level
147                ),
148                span,
149            );
150        }
151    }
152
153    // PII contradiction: parent says pii=true but child says pii=false.
154    if parent_sec.pii && !child_sec.pii {
155        diags.warning(
156            DiagnosticCode {
157                prefix: 'W',
158                number: 8011,
159            },
160            "child declares pii=false but parent declares pii=true; PII status is inherited"
161                .to_string(),
162            span,
163        );
164    }
165}
166
167/// Check that security level strings are recognized.
168fn validate_security_level_known(
169    ctx: &ContextBlock,
170    span: bock_errors::Span,
171    diags: &mut DiagnosticBag,
172) {
173    if let Some(sec) = &ctx.security {
174        if !sec.level.is_empty() && security_level_rank(&sec.level).is_none() {
175            diags.warning(
176                DiagnosticCode {
177                    prefix: 'W',
178                    number: 8015,
179                },
180                format!(
181                    "unknown security level `{}`; known levels are: {}",
182                    sec.level,
183                    SECURITY_LEVELS.join(", ")
184                ),
185                span,
186            );
187        }
188    }
189}
190
191/// Check that performance budget values are valid (positive and non-zero).
192fn validate_performance_budget(
193    ctx: &ContextBlock,
194    span: bock_errors::Span,
195    diags: &mut DiagnosticBag,
196) {
197    if let Some(perf) = &ctx.performance {
198        if let Some(lat) = &perf.max_latency {
199            if lat.value <= 0.0 {
200                diags.error(
201                    DiagnosticCode {
202                        prefix: 'E',
203                        number: 8016,
204                    },
205                    "performance max_latency must be a positive value".to_string(),
206                    span,
207                );
208            }
209        }
210        if let Some(mem) = &perf.max_memory {
211            if mem.value <= 0.0 {
212                diags.error(
213                    DiagnosticCode {
214                        prefix: 'E',
215                        number: 8016,
216                    },
217                    "performance max_memory must be a positive value".to_string(),
218                    span,
219                );
220            }
221        }
222    }
223}
224
225/// Check that public items and modules have context annotations.
226///
227/// In **Standard** mode, missing annotations produce **warnings**.
228/// In **Strict** mode, missing annotations produce **errors**.
229fn validate_completeness(node: &AIRNode, strictness: StrictnessLevel, diags: &mut DiagnosticBag) {
230    let is_strict = strictness == StrictnessLevel::Strict;
231    let mode_label = if is_strict { "production" } else { "standard" };
232
233    match &node.kind {
234        NodeKind::Module { .. } => {
235            if node.context.is_none() {
236                if is_strict {
237                    diags.error(
238                        DiagnosticCode {
239                            prefix: 'E',
240                            number: 8014,
241                        },
242                        format!(
243                            "module is missing @context annotation (required in {mode_label} mode)"
244                        ),
245                        node.span,
246                    );
247                } else {
248                    diags.warning(
249                        DiagnosticCode {
250                            prefix: 'W',
251                            number: 8014,
252                        },
253                        format!("module is missing @context annotation (recommended in {mode_label} mode)"),
254                        node.span,
255                    );
256                }
257            }
258        }
259        NodeKind::FnDecl {
260            visibility: Visibility::Public,
261            name,
262            ..
263        }
264        | NodeKind::ClassDecl {
265            visibility: Visibility::Public,
266            name,
267            ..
268        }
269        | NodeKind::TraitDecl {
270            visibility: Visibility::Public,
271            name,
272            ..
273        }
274        | NodeKind::RecordDecl {
275            visibility: Visibility::Public,
276            name,
277            ..
278        }
279        | NodeKind::EnumDecl {
280            visibility: Visibility::Public,
281            name,
282            ..
283        } => {
284            if node.context.is_none() {
285                if is_strict {
286                    diags.error(
287                        DiagnosticCode {
288                            prefix: 'E',
289                            number: 8013,
290                        },
291                        format!(
292                            "public item `{}` is missing context annotations (required in {mode_label} mode)",
293                            name.name
294                        ),
295                        node.span,
296                    );
297                } else {
298                    diags.warning(
299                        DiagnosticCode {
300                            prefix: 'W',
301                            number: 8013,
302                        },
303                        format!(
304                            "public item `{}` is missing context annotations (recommended in {mode_label} mode)",
305                            name.name
306                        ),
307                        node.span,
308                    );
309                }
310            }
311        }
312        _ => {}
313    }
314}
315
316/// Recurse into child nodes, threading security and capability context.
317fn validate_children(
318    node: &AIRNode,
319    parent_security: Option<&SecurityInfo>,
320    parent_capabilities: &HashSet<Capability>,
321    strictness: StrictnessLevel,
322    diags: &mut DiagnosticBag,
323) {
324    match &node.kind {
325        NodeKind::Module { imports, items, .. } => {
326            for child in imports.iter().chain(items.iter()) {
327                validate_node(
328                    child,
329                    parent_security,
330                    parent_capabilities,
331                    strictness,
332                    diags,
333                );
334            }
335        }
336        NodeKind::FnDecl {
337            params,
338            return_type,
339            body,
340            ..
341        } => {
342            for p in params {
343                validate_node(p, parent_security, parent_capabilities, strictness, diags);
344            }
345            if let Some(rt) = return_type.as_ref() {
346                validate_node(rt, parent_security, parent_capabilities, strictness, diags);
347            }
348            validate_node(
349                body,
350                parent_security,
351                parent_capabilities,
352                strictness,
353                diags,
354            );
355        }
356        NodeKind::ClassDecl { methods, .. } | NodeKind::TraitDecl { methods, .. } => {
357            for m in methods {
358                validate_node(m, parent_security, parent_capabilities, strictness, diags);
359            }
360        }
361        NodeKind::ImplBlock {
362            target, methods, ..
363        } => {
364            validate_node(
365                target,
366                parent_security,
367                parent_capabilities,
368                strictness,
369                diags,
370            );
371            for m in methods {
372                validate_node(m, parent_security, parent_capabilities, strictness, diags);
373            }
374        }
375        NodeKind::EffectDecl { operations, .. } => {
376            for op in operations {
377                validate_node(op, parent_security, parent_capabilities, strictness, diags);
378            }
379        }
380        NodeKind::EnumDecl { variants, .. } => {
381            for v in variants {
382                validate_node(v, parent_security, parent_capabilities, strictness, diags);
383            }
384        }
385        NodeKind::Block { stmts, tail, .. } => {
386            for stmt in stmts {
387                validate_node(
388                    stmt,
389                    parent_security,
390                    parent_capabilities,
391                    strictness,
392                    diags,
393                );
394            }
395            if let Some(t) = tail.as_ref() {
396                validate_node(t, parent_security, parent_capabilities, strictness, diags);
397            }
398        }
399        NodeKind::If {
400            condition,
401            then_block,
402            else_block,
403            ..
404        } => {
405            validate_node(
406                condition,
407                parent_security,
408                parent_capabilities,
409                strictness,
410                diags,
411            );
412            validate_node(
413                then_block,
414                parent_security,
415                parent_capabilities,
416                strictness,
417                diags,
418            );
419            if let Some(e) = else_block.as_ref() {
420                validate_node(e, parent_security, parent_capabilities, strictness, diags);
421            }
422        }
423        NodeKind::Match {
424            scrutinee, arms, ..
425        } => {
426            validate_node(
427                scrutinee,
428                parent_security,
429                parent_capabilities,
430                strictness,
431                diags,
432            );
433            for arm in arms {
434                validate_node(arm, parent_security, parent_capabilities, strictness, diags);
435            }
436        }
437        NodeKind::MatchArm {
438            pattern,
439            guard,
440            body,
441            ..
442        } => {
443            validate_node(
444                pattern,
445                parent_security,
446                parent_capabilities,
447                strictness,
448                diags,
449            );
450            if let Some(g) = guard.as_ref() {
451                validate_node(g, parent_security, parent_capabilities, strictness, diags);
452            }
453            validate_node(
454                body,
455                parent_security,
456                parent_capabilities,
457                strictness,
458                diags,
459            );
460        }
461        NodeKind::For {
462            pattern,
463            iterable,
464            body,
465            ..
466        } => {
467            validate_node(
468                pattern,
469                parent_security,
470                parent_capabilities,
471                strictness,
472                diags,
473            );
474            validate_node(
475                iterable,
476                parent_security,
477                parent_capabilities,
478                strictness,
479                diags,
480            );
481            validate_node(
482                body,
483                parent_security,
484                parent_capabilities,
485                strictness,
486                diags,
487            );
488        }
489        NodeKind::While {
490            condition, body, ..
491        } => {
492            validate_node(
493                condition,
494                parent_security,
495                parent_capabilities,
496                strictness,
497                diags,
498            );
499            validate_node(
500                body,
501                parent_security,
502                parent_capabilities,
503                strictness,
504                diags,
505            );
506        }
507        NodeKind::Loop { body, .. } => {
508            validate_node(
509                body,
510                parent_security,
511                parent_capabilities,
512                strictness,
513                diags,
514            );
515        }
516        NodeKind::LetBinding { value, .. } => {
517            validate_node(
518                value,
519                parent_security,
520                parent_capabilities,
521                strictness,
522                diags,
523            );
524        }
525        NodeKind::HandlingBlock { body, handlers, .. } => {
526            validate_node(
527                body,
528                parent_security,
529                parent_capabilities,
530                strictness,
531                diags,
532            );
533            for h in handlers {
534                validate_node(
535                    &h.handler,
536                    parent_security,
537                    parent_capabilities,
538                    strictness,
539                    diags,
540                );
541            }
542        }
543        _ => {}
544    }
545}
546
547// ─── Tests ───────────────────────────────────────────────────────────────────
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::context::interpret_context;
553    use crate::node::{NodeIdGen, NodeKind};
554    use crate::stubs::{
555        ByteSize, Capability, ContextBlock, Duration, PerformanceBudget, SecurityInfo, SizeUnit,
556        TimeUnit,
557    };
558    use bock_ast::{Annotation, Ident, Visibility};
559    use bock_errors::Span;
560
561    fn test_span() -> Span {
562        Span::dummy()
563    }
564
565    fn str_expr(s: &str) -> bock_ast::Expr {
566        bock_ast::Expr::Literal {
567            id: 0,
568            span: test_span(),
569            lit: bock_ast::Literal::String(s.to_string()),
570        }
571    }
572
573    fn bool_expr(b: bool) -> bock_ast::Expr {
574        bock_ast::Expr::Literal {
575            id: 0,
576            span: test_span(),
577            lit: bock_ast::Literal::Bool(b),
578        }
579    }
580
581    fn capability_expr(name: &str) -> bock_ast::Expr {
582        bock_ast::Expr::FieldAccess {
583            id: 0,
584            span: test_span(),
585            object: Box::new(bock_ast::Expr::Identifier {
586                id: 0,
587                span: test_span(),
588                name: Ident {
589                    name: "Capability".to_string(),
590                    span: test_span(),
591                },
592            }),
593            field: Ident {
594                name: name.to_string(),
595                span: test_span(),
596            },
597        }
598    }
599
600    fn method_call_expr(value: &str, method: &str) -> bock_ast::Expr {
601        bock_ast::Expr::MethodCall {
602            id: 0,
603            span: test_span(),
604            receiver: Box::new(bock_ast::Expr::Literal {
605                id: 0,
606                span: test_span(),
607                lit: bock_ast::Literal::Int(value.to_string()),
608            }),
609            method: Ident {
610                name: method.to_string(),
611                span: test_span(),
612            },
613            type_args: vec![],
614            args: vec![],
615        }
616    }
617
618    fn ann(name: &str, args: Vec<bock_ast::Expr>) -> Annotation {
619        Annotation {
620            id: 0,
621            span: test_span(),
622            name: Ident {
623                name: name.to_string(),
624                span: test_span(),
625            },
626            args: args
627                .into_iter()
628                .map(|e| bock_ast::AnnotationArg {
629                    label: None,
630                    value: e,
631                })
632                .collect(),
633        }
634    }
635
636    fn fn_node(
637        id_gen: &NodeIdGen,
638        annotations: Vec<Annotation>,
639        visibility: Visibility,
640    ) -> AIRNode {
641        let body = AIRNode::new(
642            id_gen.next(),
643            test_span(),
644            NodeKind::Block {
645                stmts: vec![],
646                tail: None,
647            },
648        );
649        AIRNode::new(
650            id_gen.next(),
651            test_span(),
652            NodeKind::FnDecl {
653                annotations,
654                visibility,
655                is_async: false,
656                name: Ident {
657                    name: "test_fn".to_string(),
658                    span: test_span(),
659                },
660                generic_params: vec![],
661                params: vec![],
662                return_type: None,
663                effect_clause: vec![],
664                where_clause: vec![],
665                body: Box::new(body),
666            },
667        )
668    }
669
670    fn fn_node_named(
671        id_gen: &NodeIdGen,
672        name: &str,
673        annotations: Vec<Annotation>,
674        visibility: Visibility,
675    ) -> AIRNode {
676        let body = AIRNode::new(
677            id_gen.next(),
678            test_span(),
679            NodeKind::Block {
680                stmts: vec![],
681                tail: None,
682            },
683        );
684        AIRNode::new(
685            id_gen.next(),
686            test_span(),
687            NodeKind::FnDecl {
688                annotations,
689                visibility,
690                is_async: false,
691                name: Ident {
692                    name: name.to_string(),
693                    span: test_span(),
694                },
695                generic_params: vec![],
696                params: vec![],
697                return_type: None,
698                effect_clause: vec![],
699                where_clause: vec![],
700                body: Box::new(body),
701            },
702        )
703    }
704
705    fn module_with_items(id_gen: &NodeIdGen, items: Vec<AIRNode>) -> AIRNode {
706        AIRNode::new(
707            id_gen.next(),
708            test_span(),
709            NodeKind::Module {
710                path: None,
711                annotations: vec![],
712                imports: vec![],
713                items,
714            },
715        )
716    }
717
718    // ── Security consistency tests ──────────────────────────────────────────
719
720    #[test]
721    fn security_consistent_levels_no_error() {
722        let id_gen = NodeIdGen::new();
723        let child = fn_node(
724            &id_gen,
725            vec![ann("security", vec![str_expr("confidential")])],
726            Visibility::Public,
727        );
728        let mut module = module_with_items(&id_gen, vec![child]);
729
730        // Set parent security manually.
731        module.context = Some(ContextBlock {
732            security: Some(SecurityInfo {
733                level: "internal".to_string(),
734                pii: false,
735            }),
736            ..Default::default()
737        });
738
739        // Interpret child context.
740        let _ = interpret_context(&mut module);
741
742        let diags = validate_context(&module, StrictnessLevel::Lax);
743        // Child confidential >= parent internal: OK.
744        assert_eq!(diags.error_count(), 0);
745    }
746
747    #[test]
748    fn security_child_less_restrictive_than_parent_error() {
749        let id_gen = NodeIdGen::new();
750        let child = fn_node(
751            &id_gen,
752            vec![ann("security", vec![str_expr("public")])],
753            Visibility::Public,
754        );
755
756        let mut module = module_with_items(&id_gen, vec![child]);
757        module.context = Some(ContextBlock {
758            security: Some(SecurityInfo {
759                level: "confidential".to_string(),
760                pii: false,
761            }),
762            ..Default::default()
763        });
764
765        // Interpret children context.
766        let _ = interpret_context(&mut module);
767
768        let diags = validate_context(&module, StrictnessLevel::Lax);
769        assert!(
770            diags.error_count() > 0,
771            "should error on security level contradiction"
772        );
773    }
774
775    #[test]
776    fn security_pii_inheritance_warning() {
777        let id_gen = NodeIdGen::new();
778        let mut child = fn_node(
779            &id_gen,
780            vec![ann(
781                "security",
782                vec![str_expr("confidential"), bool_expr(false)],
783            )],
784            Visibility::Public,
785        );
786        let _ = interpret_context(&mut child);
787
788        let mut module = module_with_items(&id_gen, vec![child]);
789        module.context = Some(ContextBlock {
790            security: Some(SecurityInfo {
791                level: "confidential".to_string(),
792                pii: true,
793            }),
794            ..Default::default()
795        });
796
797        let diags = validate_context(&module, StrictnessLevel::Lax);
798        assert!(
799            diags.warning_count() > 0,
800            "should warn on PII contradiction"
801        );
802    }
803
804    #[test]
805    fn security_unknown_level_warning() {
806        let id_gen = NodeIdGen::new();
807        let mut node = fn_node(
808            &id_gen,
809            vec![ann("security", vec![str_expr("top-secret")])],
810            Visibility::Public,
811        );
812        let _ = interpret_context(&mut node);
813
814        let diags = validate_context(&node, StrictnessLevel::Lax);
815        assert!(
816            diags.warning_count() > 0,
817            "should warn on unknown security level"
818        );
819    }
820
821    // ── Performance budget tests ────────────────────────────────────────────
822
823    #[test]
824    fn performance_valid_budget_no_error() {
825        let id_gen = NodeIdGen::new();
826        let mut node = fn_node(
827            &id_gen,
828            vec![ann(
829                "performance",
830                vec![method_call_expr("100", "ms"), method_call_expr("50", "mb")],
831            )],
832            Visibility::Public,
833        );
834        let _ = interpret_context(&mut node);
835
836        let diags = validate_context(&node, StrictnessLevel::Lax);
837        assert_eq!(diags.error_count(), 0);
838    }
839
840    #[test]
841    fn performance_negative_latency_error() {
842        let id_gen = NodeIdGen::new();
843        let mut node = fn_node(&id_gen, vec![], Visibility::Public);
844        node.context = Some(ContextBlock {
845            performance: Some(PerformanceBudget {
846                max_latency: Some(Duration {
847                    value: -10.0,
848                    unit: TimeUnit::Ms,
849                }),
850                max_memory: None,
851            }),
852            ..Default::default()
853        });
854
855        let diags = validate_context(&node, StrictnessLevel::Lax);
856        assert!(diags.error_count() > 0, "should error on negative latency");
857    }
858
859    #[test]
860    fn performance_zero_memory_error() {
861        let id_gen = NodeIdGen::new();
862        let mut node = fn_node(&id_gen, vec![], Visibility::Public);
863        node.context = Some(ContextBlock {
864            performance: Some(PerformanceBudget {
865                max_latency: None,
866                max_memory: Some(ByteSize {
867                    value: 0.0,
868                    unit: SizeUnit::Mb,
869                }),
870            }),
871            ..Default::default()
872        });
873
874        let diags = validate_context(&node, StrictnessLevel::Lax);
875        assert!(
876            diags.error_count() > 0,
877            "should error on zero memory budget"
878        );
879    }
880
881    // ── Completeness tests — lax (sketch) ────────────────────────────────────
882
883    #[test]
884    fn completeness_lax_no_warnings() {
885        let id_gen = NodeIdGen::new();
886        let node = fn_node(&id_gen, vec![], Visibility::Public);
887        let module = module_with_items(&id_gen, vec![node]);
888
889        let diags = validate_context(&module, StrictnessLevel::Lax);
890        assert_eq!(
891            diags.warning_count(),
892            0,
893            "lax: no warnings on missing context"
894        );
895        assert_eq!(diags.error_count(), 0, "lax: no errors on missing context");
896    }
897
898    #[test]
899    fn completeness_lax_private_fn_ok() {
900        let id_gen = NodeIdGen::new();
901        let node = fn_node(&id_gen, vec![], Visibility::Private);
902        let diags = validate_context(&node, StrictnessLevel::Lax);
903        assert_eq!(diags.warning_count(), 0);
904        assert_eq!(diags.error_count(), 0);
905    }
906
907    // ── Completeness tests — standard (development) ─────────────────────────
908
909    #[test]
910    fn completeness_standard_public_fn_without_context_warns() {
911        let id_gen = NodeIdGen::new();
912        let node = fn_node(&id_gen, vec![], Visibility::Public);
913
914        let diags = validate_context(&node, StrictnessLevel::Standard);
915        assert!(
916            diags.warning_count() > 0,
917            "standard mode should warn on public fn without context"
918        );
919        assert_eq!(
920            diags.error_count(),
921            0,
922            "standard mode should not error on public fn without context"
923        );
924    }
925
926    #[test]
927    fn completeness_standard_private_fn_without_context_ok() {
928        let id_gen = NodeIdGen::new();
929        let node = fn_node(&id_gen, vec![], Visibility::Private);
930
931        let diags = validate_context(&node, StrictnessLevel::Standard);
932        assert_eq!(diags.warning_count(), 0);
933        assert_eq!(diags.error_count(), 0);
934    }
935
936    #[test]
937    fn completeness_standard_module_without_context_warns() {
938        let id_gen = NodeIdGen::new();
939        let module = module_with_items(&id_gen, vec![]);
940
941        let diags = validate_context(&module, StrictnessLevel::Standard);
942        assert!(
943            diags.warning_count() > 0,
944            "standard mode should warn on module without context"
945        );
946        assert_eq!(diags.error_count(), 0);
947    }
948
949    #[test]
950    fn completeness_standard_module_with_context_ok() {
951        let id_gen = NodeIdGen::new();
952        let mut module = module_with_items(&id_gen, vec![]);
953        module.context = Some(ContextBlock {
954            context_text: Some("Payment module.".to_string()),
955            ..Default::default()
956        });
957
958        let diags = validate_context(&module, StrictnessLevel::Standard);
959        assert_eq!(diags.warning_count(), 0);
960        assert_eq!(diags.error_count(), 0);
961    }
962
963    // ── Completeness tests — strict (production) ────────────────────────────
964
965    #[test]
966    fn completeness_strict_public_fn_without_context_errors() {
967        let id_gen = NodeIdGen::new();
968        let node = fn_node(&id_gen, vec![], Visibility::Public);
969
970        let diags = validate_context(&node, StrictnessLevel::Strict);
971        assert!(
972            diags.error_count() > 0,
973            "strict mode should error on public fn without context"
974        );
975    }
976
977    #[test]
978    fn completeness_strict_private_fn_without_context_ok() {
979        let id_gen = NodeIdGen::new();
980        let node = fn_node(&id_gen, vec![], Visibility::Private);
981
982        let diags = validate_context(&node, StrictnessLevel::Strict);
983        assert_eq!(
984            diags.error_count(),
985            0,
986            "strict mode should not error on private fn without context"
987        );
988    }
989
990    #[test]
991    fn completeness_strict_module_without_context_errors() {
992        let id_gen = NodeIdGen::new();
993        let module = module_with_items(&id_gen, vec![]);
994
995        let diags = validate_context(&module, StrictnessLevel::Strict);
996        assert!(
997            diags.error_count() > 0,
998            "strict mode should error on module without context"
999        );
1000    }
1001
1002    #[test]
1003    fn completeness_strict_module_with_context_ok() {
1004        let id_gen = NodeIdGen::new();
1005        let mut module = module_with_items(&id_gen, vec![]);
1006        module.context = Some(ContextBlock {
1007            context_text: Some("Payment module.".to_string()),
1008            ..Default::default()
1009        });
1010
1011        let diags = validate_context(&module, StrictnessLevel::Strict);
1012        assert_eq!(diags.error_count(), 0);
1013    }
1014
1015    // ── Strictness level mapping tests ──────────────────────────────────────
1016
1017    #[test]
1018    fn strictness_from_name_sketch() {
1019        assert_eq!(
1020            StrictnessLevel::from_name("sketch"),
1021            Some(StrictnessLevel::Lax)
1022        );
1023        assert_eq!(
1024            StrictnessLevel::from_name("lax"),
1025            Some(StrictnessLevel::Lax)
1026        );
1027        assert_eq!(
1028            StrictnessLevel::from_name("Sketch"),
1029            Some(StrictnessLevel::Lax)
1030        );
1031    }
1032
1033    #[test]
1034    fn strictness_from_name_development() {
1035        assert_eq!(
1036            StrictnessLevel::from_name("development"),
1037            Some(StrictnessLevel::Standard)
1038        );
1039        assert_eq!(
1040            StrictnessLevel::from_name("standard"),
1041            Some(StrictnessLevel::Standard)
1042        );
1043        assert_eq!(
1044            StrictnessLevel::from_name("Development"),
1045            Some(StrictnessLevel::Standard)
1046        );
1047    }
1048
1049    #[test]
1050    fn strictness_from_name_production() {
1051        assert_eq!(
1052            StrictnessLevel::from_name("production"),
1053            Some(StrictnessLevel::Strict)
1054        );
1055        assert_eq!(
1056            StrictnessLevel::from_name("strict"),
1057            Some(StrictnessLevel::Strict)
1058        );
1059        assert_eq!(
1060            StrictnessLevel::from_name("Production"),
1061            Some(StrictnessLevel::Strict)
1062        );
1063    }
1064
1065    #[test]
1066    fn strictness_from_name_unknown() {
1067        assert_eq!(StrictnessLevel::from_name(""), None);
1068        assert_eq!(StrictnessLevel::from_name("debug"), None);
1069    }
1070
1071    // ── Three-level differentiation test ────────────────────────────────────
1072
1073    #[test]
1074    fn three_levels_differ_on_public_fn_without_context() {
1075        let id_gen = NodeIdGen::new();
1076
1077        // Public fn with no context annotations.
1078        let node_lax = fn_node(&id_gen, vec![], Visibility::Public);
1079        let node_std = fn_node(&id_gen, vec![], Visibility::Public);
1080        let node_strict = fn_node(&id_gen, vec![], Visibility::Public);
1081
1082        let d_lax = validate_context(&node_lax, StrictnessLevel::Lax);
1083        let d_std = validate_context(&node_std, StrictnessLevel::Standard);
1084        let d_strict = validate_context(&node_strict, StrictnessLevel::Strict);
1085
1086        // Lax: no diagnostics at all.
1087        assert_eq!(
1088            d_lax.warning_count() + d_lax.error_count(),
1089            0,
1090            "lax: silent"
1091        );
1092        // Standard: warning but no error.
1093        assert!(d_std.warning_count() > 0, "standard: warns");
1094        assert_eq!(d_std.error_count(), 0, "standard: no errors");
1095        // Strict: error.
1096        assert!(d_strict.error_count() > 0, "strict: errors");
1097    }
1098
1099    #[test]
1100    fn three_levels_differ_on_module_without_context() {
1101        let id_gen = NodeIdGen::new();
1102
1103        let mod_lax = module_with_items(&id_gen, vec![]);
1104        let mod_std = module_with_items(&id_gen, vec![]);
1105        let mod_strict = module_with_items(&id_gen, vec![]);
1106
1107        let d_lax = validate_context(&mod_lax, StrictnessLevel::Lax);
1108        let d_std = validate_context(&mod_std, StrictnessLevel::Standard);
1109        let d_strict = validate_context(&mod_strict, StrictnessLevel::Strict);
1110
1111        assert_eq!(
1112            d_lax.warning_count() + d_lax.error_count(),
1113            0,
1114            "lax: silent"
1115        );
1116        assert!(d_std.warning_count() > 0, "standard: warns");
1117        assert_eq!(d_std.error_count(), 0, "standard: no errors");
1118        assert!(d_strict.error_count() > 0, "strict: errors");
1119    }
1120
1121    // ── Invariant type-check tests ──────────────────────────────────────────
1122
1123    #[test]
1124    fn invariant_comparison_expr_ok() {
1125        let id_gen = NodeIdGen::new();
1126        let invariant_expr = bock_ast::Expr::Binary {
1127            id: 0,
1128            span: test_span(),
1129            op: bock_ast::BinOp::Le,
1130            left: Box::new(bock_ast::Expr::Identifier {
1131                id: 0,
1132                span: test_span(),
1133                name: Ident {
1134                    name: "a".to_string(),
1135                    span: test_span(),
1136                },
1137            }),
1138            right: Box::new(bock_ast::Expr::Identifier {
1139                id: 0,
1140                span: test_span(),
1141                name: Ident {
1142                    name: "b".to_string(),
1143                    span: test_span(),
1144                },
1145            }),
1146        };
1147        let mut node = fn_node(
1148            &id_gen,
1149            vec![ann("invariant", vec![invariant_expr])],
1150            Visibility::Public,
1151        );
1152        let diags = interpret_context(&mut node);
1153        assert_eq!(diags.error_count(), 0, "comparison invariant should pass");
1154    }
1155
1156    #[test]
1157    fn invariant_arithmetic_expr_error() {
1158        let id_gen = NodeIdGen::new();
1159        // `a + b` is not a boolean expression.
1160        let invariant_expr = bock_ast::Expr::Binary {
1161            id: 0,
1162            span: test_span(),
1163            op: bock_ast::BinOp::Add,
1164            left: Box::new(bock_ast::Expr::Identifier {
1165                id: 0,
1166                span: test_span(),
1167                name: Ident {
1168                    name: "a".to_string(),
1169                    span: test_span(),
1170                },
1171            }),
1172            right: Box::new(bock_ast::Expr::Identifier {
1173                id: 0,
1174                span: test_span(),
1175                name: Ident {
1176                    name: "b".to_string(),
1177                    span: test_span(),
1178                },
1179            }),
1180        };
1181        let mut node = fn_node(
1182            &id_gen,
1183            vec![ann("invariant", vec![invariant_expr])],
1184            Visibility::Public,
1185        );
1186        let diags = interpret_context(&mut node);
1187        assert!(
1188            diags.error_count() > 0,
1189            "arithmetic invariant should produce E8010 error"
1190        );
1191    }
1192
1193    #[test]
1194    fn invariant_logical_expr_ok() {
1195        let id_gen = NodeIdGen::new();
1196        let invariant_expr = bock_ast::Expr::Binary {
1197            id: 0,
1198            span: test_span(),
1199            op: bock_ast::BinOp::And,
1200            left: Box::new(bock_ast::Expr::Identifier {
1201                id: 0,
1202                span: test_span(),
1203                name: Ident {
1204                    name: "x".to_string(),
1205                    span: test_span(),
1206                },
1207            }),
1208            right: Box::new(bock_ast::Expr::Identifier {
1209                id: 0,
1210                span: test_span(),
1211                name: Ident {
1212                    name: "y".to_string(),
1213                    span: test_span(),
1214                },
1215            }),
1216        };
1217        let mut node = fn_node(
1218            &id_gen,
1219            vec![ann("invariant", vec![invariant_expr])],
1220            Visibility::Public,
1221        );
1222        let diags = interpret_context(&mut node);
1223        assert_eq!(diags.error_count(), 0, "logical invariant should pass");
1224    }
1225
1226    #[test]
1227    fn invariant_not_expr_ok() {
1228        let id_gen = NodeIdGen::new();
1229        let invariant_expr = bock_ast::Expr::Unary {
1230            id: 0,
1231            span: test_span(),
1232            op: bock_ast::UnaryOp::Not,
1233            operand: Box::new(bock_ast::Expr::Identifier {
1234                id: 0,
1235                span: test_span(),
1236                name: Ident {
1237                    name: "flag".to_string(),
1238                    span: test_span(),
1239                },
1240            }),
1241        };
1242        let mut node = fn_node(
1243            &id_gen,
1244            vec![ann("invariant", vec![invariant_expr])],
1245            Visibility::Public,
1246        );
1247        let diags = interpret_context(&mut node);
1248        assert_eq!(diags.error_count(), 0, "negation invariant should pass");
1249    }
1250
1251    #[test]
1252    fn invariant_negate_numeric_error() {
1253        let id_gen = NodeIdGen::new();
1254        // `-x` is a numeric negation, not boolean.
1255        let invariant_expr = bock_ast::Expr::Unary {
1256            id: 0,
1257            span: test_span(),
1258            op: bock_ast::UnaryOp::Neg,
1259            operand: Box::new(bock_ast::Expr::Identifier {
1260                id: 0,
1261                span: test_span(),
1262                name: Ident {
1263                    name: "x".to_string(),
1264                    span: test_span(),
1265                },
1266            }),
1267        };
1268        let mut node = fn_node(
1269            &id_gen,
1270            vec![ann("invariant", vec![invariant_expr])],
1271            Visibility::Public,
1272        );
1273        let diags = interpret_context(&mut node);
1274        assert!(
1275            diags.error_count() > 0,
1276            "numeric negation invariant should produce E8010 error"
1277        );
1278    }
1279
1280    #[test]
1281    fn invariant_call_expr_ok() {
1282        let id_gen = NodeIdGen::new();
1283        // `is_valid()` — can't verify return type without full types, accepted.
1284        let invariant_expr = bock_ast::Expr::Call {
1285            id: 0,
1286            span: test_span(),
1287            callee: Box::new(bock_ast::Expr::Identifier {
1288                id: 0,
1289                span: test_span(),
1290                name: Ident {
1291                    name: "is_valid".to_string(),
1292                    span: test_span(),
1293                },
1294            }),
1295            args: vec![],
1296            type_args: vec![],
1297        };
1298        let mut node = fn_node(
1299            &id_gen,
1300            vec![ann("invariant", vec![invariant_expr])],
1301            Visibility::Public,
1302        );
1303        let diags = interpret_context(&mut node);
1304        assert_eq!(diags.error_count(), 0, "call invariant should be accepted");
1305    }
1306
1307    // ── Capability additive propagation test ────────────────────────────────
1308
1309    #[test]
1310    fn capabilities_additive_no_error() {
1311        let id_gen = NodeIdGen::new();
1312        let mut child = fn_node(
1313            &id_gen,
1314            vec![ann("requires", vec![capability_expr("Crypto")])],
1315            Visibility::Public,
1316        );
1317        let _ = interpret_context(&mut child);
1318
1319        let mut module = module_with_items(&id_gen, vec![child]);
1320        module.context = Some(ContextBlock {
1321            capabilities: {
1322                let mut s = HashSet::new();
1323                s.insert(Capability::new("Network"));
1324                s
1325            },
1326            ..Default::default()
1327        });
1328
1329        let diags = validate_context(&module, StrictnessLevel::Standard);
1330        // Additive: module has Network, child has Crypto. No contradiction.
1331        assert_eq!(diags.error_count(), 0);
1332    }
1333
1334    // ── Integration: combined annotations ───────────────────────────────────
1335
1336    #[test]
1337    fn full_tree_validation() {
1338        let id_gen = NodeIdGen::new();
1339
1340        // Child fn with proper security level >= parent.
1341        let mut child1 = fn_node_named(
1342            &id_gen,
1343            "process_payment",
1344            vec![
1345                ann("context", vec![str_expr("Process a payment.")]),
1346                ann("security", vec![str_expr("secret"), bool_expr(true)]),
1347                ann("requires", vec![capability_expr("Network")]),
1348            ],
1349            Visibility::Public,
1350        );
1351        let _ = interpret_context(&mut child1);
1352
1353        // Private child — no context needed even in strict mode.
1354        let child2 = fn_node_named(&id_gen, "helper", vec![], Visibility::Private);
1355
1356        let mut module = module_with_items(&id_gen, vec![child1, child2]);
1357        module.context = Some(ContextBlock {
1358            context_text: Some("Payment module.".to_string()),
1359            security: Some(SecurityInfo {
1360                level: "confidential".to_string(),
1361                pii: true,
1362            }),
1363            ..Default::default()
1364        });
1365
1366        let diags = validate_context(&module, StrictnessLevel::Strict);
1367        // child1 has secret >= confidential: OK.
1368        // child2 is private: no completeness warning.
1369        // module has context: OK.
1370        assert_eq!(diags.error_count(), 0);
1371        assert_eq!(diags.warning_count(), 0);
1372    }
1373}