11#!/usr/bin/env python3
22"""
3- This script uses a plugin architecture to parse rules and generate documentation.
3+ Sync AI Rules - Plugin-based rule parser and documentation generator.
4+ Scans source directories, parses rules, and generates documentation sections.
45"""
56
67import os
1415
1516
1617def find_project_root () -> str :
17- """Find the project root by looking for key indicators ."""
18+ """Find project root by looking for .cursor/rules or .code_review directories ."""
1819 current = Path .cwd ()
1920
20- # Look for .cursor/rules directory
2121 for path in [current ] + list (current .parents ):
22- if (path / ".cursor" / "rules" ).exists ():
22+ if (path / ".cursor" / "rules" ).exists () or ( path / ".code_review" ). exists () :
2323 return str (path )
2424
25- # Fallback to current directory
2625 return str (current )
2726
2827
29- def get_category (file_path : str , rules_dir : str ) -> str :
30- """Get category name from file path."""
31- rel_path = os .path .relpath (file_path , rules_dir )
28+ def get_category (file_path : str , source_dir : str ) -> str :
29+ """Extract category from file path relative to source directory ."""
30+ rel_path = os .path .relpath (file_path , source_dir )
3231 folder = os .path .dirname (rel_path )
32+ return folder if folder and folder != "." else "root"
3333
34- if not folder or folder == "." :
35- return "root"
36-
37- return folder
38-
39-
40- def group_rules_by_category (rules : List [RuleMetadata ]) -> Dict [str , List [RuleMetadata ]]:
41- """Group rules by their category."""
42- groups = {}
4334
35+ def group_by_category (rules : List [RuleMetadata ]) -> Dict [str , List [RuleMetadata ]]:
36+ """Group rules by category."""
37+ groups : Dict [str , List [RuleMetadata ]] = {}
4438 for rule in rules :
45- category = rule .category
46- if category not in groups :
47- groups [category ] = []
48- groups [category ].append (rule )
49-
39+ groups .setdefault (rule .category , []).append (rule )
5040 return groups
5141
5242
53- def scan_rules_directory (
54- rules_dir : str , project_root : str , plugin_manager : PluginManager
55- ) -> List [RuleMetadata ]:
56- """Scan a directory for rules and parse them."""
43+ def scan_and_parse (parser , source_dir : str , project_root : str ) -> List [RuleMetadata ]:
44+ """Scan directory and parse files with given parser."""
5745 rules = []
5846
59- if not os .path .exists (rules_dir ):
47+ if not os .path .exists (source_dir ):
6048 return rules
6149
62- for root , dirs , files in os .walk (rules_dir ):
63- # Skip generated and personal directories
50+ for root , _ , files in os .walk (source_dir ):
51+ # Skip generated/ personal directories
6452 if "generated" in Path (root ).parts or "personal" in Path (root ).parts :
6553 continue
6654
6755 for file in files :
6856 file_path = os .path .join (root , file )
6957
70- # Find appropriate parser
71- parser = plugin_manager .get_parser_for_file (file_path )
72- if not parser :
58+ if not parser .can_parse (file_path ):
7359 continue
7460
75- # Create parsing context
7661 context = {
7762 "project_root" : project_root ,
7863 "relative_path" : os .path .relpath (file_path , project_root ),
79- "category" : get_category (file_path , rules_dir ),
64+ "category" : get_category (file_path , source_dir ),
8065 }
8166
82- # Parse the rule
8367 rule = parser .parse (file_path , context )
8468 if rule :
8569 rules .append (rule )
@@ -88,76 +72,74 @@ def scan_rules_directory(
8872
8973
9074def main ():
91- """Main entry point ."""
92- # Find project root
75+ """Main orchestration: load plugins → parse → generate → update files ."""
76+ # Setup
9377 project_root = find_project_root ()
94- cursor_rules_dir = os .path .join (project_root , ".cursor" , "rules" )
95- code_review_dir = os .path .join (project_root , ".code_review" )
96-
97- if not os .path .exists (cursor_rules_dir ) and not os .path .exists (code_review_dir ):
98- print ("Error: Neither .cursor/rules nor .code_review directory found" )
99- sys .exit (1 )
100-
101- # Initialize plugin manager
10278 script_dir = os .path .dirname (os .path .abspath (__file__ ))
79+
10380 plugin_manager = PluginManager ()
10481 plugin_manager .load_plugins (script_dir )
10582
106- # Scan development rules (.cursor/rules/)
107- print (f"Scanning development rules from: { cursor_rules_dir } " )
108- dev_rules = scan_rules_directory (cursor_rules_dir , project_root , plugin_manager )
109- grouped_dev_rules = group_rules_by_category (dev_rules )
110-
111- # Scan code review guidelines (.code_review/)
112- print (f"Scanning code review guidelines from: { code_review_dir } " )
113- review_rules = scan_rules_directory (code_review_dir , project_root , plugin_manager )
114- grouped_review_rules = group_rules_by_category (review_rules )
115-
116- # Print summary
117- total_dev_rules = sum (len (rules ) for rules in grouped_dev_rules .values ())
118- total_review_rules = sum (len (rules ) for rules in grouped_review_rules .values ())
119- print (f"\n Found { total_dev_rules } development rules in { len (grouped_dev_rules )} categories" )
120- print (
121- f"Found { total_review_rules } code review guidelines in { len (grouped_review_rules )} categories"
122- )
123-
124- # Get generators
125- dev_generator = plugin_manager .get_generator ("development-rules" )
126- review_generator = plugin_manager .get_generator ("code-review-guidelines" )
127-
128- if not dev_generator or not review_generator :
129- print ("Error: Required generators not found" )
83+ # Process each parser → generator pair
84+ results = {}
85+
86+ for parser in plugin_manager .parsers .values ():
87+ # Get source directories from parser
88+ source_dirs = parser .source_directories
89+ if not source_dirs :
90+ continue
91+
92+ all_rules = []
93+ for rel_dir in source_dirs :
94+ source_dir = os .path .join (project_root , rel_dir )
95+ print (f"Scanning { rel_dir } ..." )
96+ rules = scan_and_parse (parser , source_dir , project_root )
97+ all_rules .extend (rules )
98+
99+ if not all_rules :
100+ continue
101+
102+ # Group rules by category
103+ grouped_rules = group_by_category (all_rules )
104+
105+ print (f" Found { len (all_rules )} rules in { len (grouped_rules )} categories" )
106+
107+ # Store for generator
108+ results [parser .name ] = grouped_rules
109+
110+ if not results :
111+ print ("Error: No rules found in any source directory" )
130112 sys .exit (1 )
131113
132- # Generate content for both sections
133- dev_content = dev_generator .generate (grouped_dev_rules , {}) if dev_rules else None
134- review_content = review_generator .generate (grouped_review_rules , {}) if review_rules else None
114+ # Generate and update documentation
115+ print ("\n Generating documentation..." )
135116
136- # Update output files (both generators use same target files)
117+ # Get target files (all generators use same files)
118+ first_generator = next (iter (plugin_manager .generators .values ()))
137119 output_files = [
138- os .path .join (project_root , filename ) for filename in dev_generator .default_filenames
120+ os .path .join (project_root , filename ) for filename in first_generator .default_filenames
139121 ]
140122
141- for file_path in output_files :
142- # Update development rules section
143- if dev_content :
144- success , message = update_documentation_file (
145- file_path , dev_content , dev_generator .get_section_markers ()
146- )
147- if success :
148- print (f"✓ Development rules: { message } " )
149- else :
150- print (f"✗ Development rules: { message } " )
123+ # Generate content from each generator
124+ for parser_name , grouped_rules in results .items ():
125+ # Get the generator for this parser
126+ generator_name = plugin_manager .parser_to_generator .get (parser_name )
127+ if not generator_name :
128+ continue
129+
130+ generator = plugin_manager .generators .get (generator_name )
131+ if not generator :
132+ continue
133+
134+ content = generator .generate (grouped_rules , {})
151135
152- # Update code review guidelines section
153- if review_content :
136+ # Update all target files
137+ for file_path in output_files :
154138 success , message = update_documentation_file (
155- file_path , review_content , review_generator .get_section_markers ()
139+ file_path , content , generator .get_section_markers ()
156140 )
157- if success :
158- print (f"✓ Code review guidelines: { message } " )
159- else :
160- print (f"✗ Code review guidelines: { message } " )
141+ status = "✓" if success else "✗"
142+ print (f"{ status } { generator .name } : { message } " )
161143
162144 print ("\n ✓ Rules synchronization completed!" )
163145
0 commit comments