@@ -680,6 +680,12 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
680680 errors. push ( format ! ( "Manifest validation failed: {e}" ) ) ;
681681 }
682682
683+ // Check for system/evaluate.rego at rulebook root (shared entrypoint)
684+ let system_eval = path. join ( "system" ) . join ( "evaluate.rego" ) ;
685+ if !system_eval. exists ( ) {
686+ errors. push ( "Missing system/evaluate.rego at rulebook root" . to_string ( ) ) ;
687+ }
688+
683689 // Check policies exist for declared harnesses
684690 let policies_dir = path. join ( "policies" ) ;
685691 if !policies_dir. exists ( ) {
@@ -692,27 +698,17 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
692698 continue ;
693699 }
694700
695- // Check for system/evaluate.rego
696- let system_eval = harness_dir. join ( "system" ) . join ( "evaluate.rego" ) ;
697- if !system_eval. exists ( ) {
698- errors. push ( format ! (
699- "Missing system/evaluate.rego for harness: {harness}"
700- ) ) ;
701- }
702-
703- // Check that at least some .rego files exist
704- let rego_files = count_rego_files ( & harness_dir) ;
701+ // Check that harness directory has at least one .rego file directly
702+ let rego_files = count_rego_files_direct ( & harness_dir) ;
705703 if rego_files == 0 {
706- errors. push ( format ! ( "No .rego files found for harness: {harness}" ) ) ;
704+ errors. push ( format ! ( "No .rego policy files in policies/{}/" , harness ) ) ;
707705 }
708706 }
709707 }
710708
711- // Validate Rego namespaces
712- if policies_dir. exists ( ) {
713- if let Err ( e) = validate_rego_namespaces ( path, & manifest. metadata . name ) {
714- errors. push ( format ! ( "Namespace validation failed: {e}" ) ) ;
715- }
709+ // Validate Rego namespaces (policies, helpers, and system)
710+ if let Err ( e) = validate_rego_namespaces ( path, & manifest. metadata . name ) {
711+ errors. push ( format ! ( "Namespace validation failed: {}" , e) ) ;
716712 }
717713
718714 // Check for README
@@ -745,25 +741,89 @@ async fn execute_lint(path: &std::path::Path) -> Result<()> {
745741 Ok ( ( ) )
746742}
747743
748- fn count_rego_files ( dir : & std:: path:: Path ) -> usize {
749- walkdir:: WalkDir :: new ( dir)
750- . into_iter ( )
751- . filter_map ( |e| e. ok ( ) )
752- . filter ( |e| {
753- e. path ( ) . is_file ( )
754- && e. path ( )
755- . extension ( )
756- . map ( |ext| ext == "rego" )
757- . unwrap_or ( false )
744+ /// Count .rego files directly in a directory (non-recursive)
745+ fn count_rego_files_direct ( dir : & std:: path:: Path ) -> usize {
746+ std:: fs:: read_dir ( dir)
747+ . map ( |entries| {
748+ entries
749+ . filter_map ( |e| e. ok ( ) )
750+ . filter ( |e| {
751+ e. path ( ) . is_file ( )
752+ && e. path ( )
753+ . extension ( )
754+ . map ( |ext| ext == "rego" )
755+ . unwrap_or ( false )
756+ } )
757+ . count ( )
758758 } )
759- . count ( )
759+ . unwrap_or ( 0 )
760760}
761761
762762fn validate_rego_namespaces ( rulebook_path : & std:: path:: Path , rulebook_name : & str ) -> Result < ( ) > {
763+ let normalized_name = rulebook_name. replace ( '-' , "_" ) ;
764+ let base_prefix = format ! ( "cupcake.catalog.{}" , normalized_name) ;
765+
766+ // Validate policies/ directory (policies namespace)
763767 let policies_dir = rulebook_path. join ( "policies" ) ;
764- let expected_prefix = format ! ( "cupcake.catalog.{}" , rulebook_name. replace( '-' , "_" ) ) ;
768+ if policies_dir. exists ( ) {
769+ let expected_prefix = format ! ( "{}.policies" , base_prefix) ;
770+ validate_rego_files_in_dir ( & policies_dir, & expected_prefix, rulebook_path) ?;
771+ }
772+
773+ // Validate helpers/ directory (helpers namespace)
774+ let helpers_dir = rulebook_path. join ( "helpers" ) ;
775+ if helpers_dir. exists ( ) {
776+ let expected_prefix = format ! ( "{}.helpers" , base_prefix) ;
777+ validate_rego_files_in_dir ( & helpers_dir, & expected_prefix, rulebook_path) ?;
778+ }
779+
780+ // Validate system/ directory (exact system namespace)
781+ let system_dir = rulebook_path. join ( "system" ) ;
782+ if system_dir. exists ( ) {
783+ let expected_package = format ! ( "{}.system" , base_prefix) ;
784+ for entry in walkdir:: WalkDir :: new ( & system_dir) {
785+ let entry = entry?;
786+ if !entry. path ( ) . is_file ( ) {
787+ continue ;
788+ }
789+ if entry
790+ . path ( )
791+ . extension ( )
792+ . map ( |ext| ext != "rego" )
793+ . unwrap_or ( true )
794+ {
795+ continue ;
796+ }
797+
798+ let content = std:: fs:: read_to_string ( entry. path ( ) ) ?;
799+ let package_name = extract_package_name ( & content) ;
800+
801+ if let Some ( pkg) = package_name {
802+ if pkg != expected_package {
803+ anyhow:: bail!(
804+ "System file at {:?} has invalid namespace '{}'. Expected exactly '{}'" ,
805+ entry
806+ . path( )
807+ . strip_prefix( rulebook_path)
808+ . unwrap_or( entry. path( ) ) ,
809+ pkg,
810+ expected_package
811+ ) ;
812+ }
813+ }
814+ }
815+ }
816+
817+ Ok ( ( ) )
818+ }
765819
766- for entry in walkdir:: WalkDir :: new ( & policies_dir) {
820+ /// Validate .rego files in a directory have the expected namespace prefix
821+ fn validate_rego_files_in_dir (
822+ dir : & std:: path:: Path ,
823+ expected_prefix : & str ,
824+ rulebook_path : & std:: path:: Path ,
825+ ) -> Result < ( ) > {
826+ for entry in walkdir:: WalkDir :: new ( dir) {
767827 let entry = entry?;
768828 if !entry. path ( ) . is_file ( ) {
769829 continue ;
@@ -779,30 +839,43 @@ fn validate_rego_namespaces(rulebook_path: &std::path::Path, rulebook_name: &str
779839 }
780840
781841 let content = std:: fs:: read_to_string ( entry. path ( ) ) ?;
782-
783- // Find package declaration
784- for line in content. lines ( ) {
785- let trimmed = line. trim ( ) ;
786- if trimmed. starts_with ( "package " ) {
787- let package_name = trimmed. strip_prefix ( "package " ) . unwrap_or ( "" ) . trim ( ) ;
788-
789- // Check namespace prefix
790- if !package_name. starts_with ( & expected_prefix) {
791- anyhow:: bail!(
792- "Policy at {:?} has invalid namespace '{}'. Expected prefix '{}'" ,
793- entry. path( ) ,
794- package_name,
795- expected_prefix
796- ) ;
797- }
798- break ;
842+ let package_name = extract_package_name ( & content) ;
843+
844+ if let Some ( pkg) = package_name {
845+ if !pkg. starts_with ( expected_prefix) {
846+ anyhow:: bail!(
847+ "File at {:?} has invalid namespace '{}'. Expected prefix '{}'" ,
848+ entry
849+ . path( )
850+ . strip_prefix( rulebook_path)
851+ . unwrap_or( entry. path( ) ) ,
852+ pkg,
853+ expected_prefix
854+ ) ;
799855 }
800856 }
801857 }
802858
803859 Ok ( ( ) )
804860}
805861
862+ /// Extract package name from Rego content
863+ fn extract_package_name ( content : & str ) -> Option < String > {
864+ for line in content. lines ( ) {
865+ let trimmed = line. trim ( ) ;
866+ if trimmed. starts_with ( "package " ) {
867+ return Some (
868+ trimmed
869+ . strip_prefix ( "package " )
870+ . unwrap_or ( "" )
871+ . trim ( )
872+ . to_string ( ) ,
873+ ) ;
874+ }
875+ }
876+ None
877+ }
878+
806879fn print_validation_results ( errors : & [ String ] , warnings : & [ String ] ) {
807880 if errors. is_empty ( ) && warnings. is_empty ( ) {
808881 println ! ( "Rulebook is valid." ) ;
0 commit comments