Skip to content

Commit 0ea7b5e

Browse files
committed
Code review generator
1 parent 03ee470 commit 0ea7b5e

5 files changed

Lines changed: 261 additions & 47 deletions

File tree

sync_ai_rules/__main__.py

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,15 @@ def group_rules_by_category(rules: List[RuleMetadata]) -> Dict[str, List[RuleMet
5050
return groups
5151

5252

53-
def main():
54-
"""Main entry point."""
55-
# Find project root
56-
project_root = find_project_root()
57-
rules_dir = os.path.join(project_root, ".cursor", "rules")
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."""
57+
rules = []
5858

5959
if not os.path.exists(rules_dir):
60-
print(f"Error: Rules directory not found: {rules_dir}")
61-
sys.exit(1)
62-
63-
print(f"Syncing rules from: {rules_dir}")
60+
return rules
6461

65-
# Initialize plugin manager
66-
script_dir = os.path.dirname(os.path.abspath(__file__))
67-
plugin_manager = PluginManager()
68-
plugin_manager.load_plugins(script_dir)
69-
70-
# Scan for rules using available parsers
71-
rules = []
7262
for root, dirs, files in os.walk(rules_dir):
7363
# Skip generated and personal directories
7464
if "generated" in Path(root).parts or "personal" in Path(root).parts:
@@ -84,6 +74,7 @@ def main():
8474

8575
# Create parsing context
8676
context = {
77+
"project_root": project_root,
8778
"relative_path": os.path.relpath(file_path, project_root),
8879
"category": get_category(file_path, rules_dir),
8980
}
@@ -93,40 +84,80 @@ def main():
9384
if rule:
9485
rules.append(rule)
9586

96-
if not rules:
97-
print("No rules found to sync")
98-
return
87+
return rules
9988

100-
# Group rules by category
101-
grouped_rules = group_rules_by_category(rules)
10289

103-
# Get markdown generator
104-
generator = plugin_manager.get_generator("markdown")
105-
if not generator:
106-
print("Error: Markdown generator not found")
90+
def main():
91+
"""Main entry point."""
92+
# Find project root
93+
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")
10799
sys.exit(1)
108100

109-
# Generate content
110-
content = generator.generate(grouped_rules, {})
101+
# Initialize plugin manager
102+
script_dir = os.path.dirname(os.path.abspath(__file__))
103+
plugin_manager = PluginManager()
104+
plugin_manager.load_plugins(script_dir)
105+
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)
111115

112116
# Print summary
113-
total_rules = sum(len(rules) for rules in grouped_rules.values())
114-
print(f"\nFound {total_rules} rules in {len(grouped_rules)} categories")
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"\nFound {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")
130+
sys.exit(1)
131+
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
115135

116-
# Update output files using generator's default filenames
136+
# Update output files (both generators use same target files)
117137
output_files = [
118-
os.path.join(project_root, filename) for filename in generator.default_filenames
138+
os.path.join(project_root, filename) for filename in dev_generator.default_filenames
119139
]
120140

121141
for file_path in output_files:
122-
success, message = update_documentation_file(
123-
file_path, content, generator.get_section_markers()
124-
)
125-
126-
if success:
127-
print(f"✓ {message}")
128-
else:
129-
print(f"✗ {message}")
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}")
151+
152+
# Update code review guidelines section
153+
if review_content:
154+
success, message = update_documentation_file(
155+
file_path, review_content, review_generator.get_section_markers()
156+
)
157+
if success:
158+
print(f"✓ Code review guidelines: {message}")
159+
else:
160+
print(f"✗ Code review guidelines: {message}")
130161

131162
print("\n✓ Rules synchronization completed!")
132163

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Code Review Guidelines Generator plugin - generates review guidelines for Codex.
4+
"""
5+
6+
from typing import Any, Dict, List
7+
8+
from sync_ai_rules.core.interfaces import OutputGenerator, RuleMetadata
9+
10+
11+
class CodeReviewGuidelinesGenerator(OutputGenerator):
12+
"""Generate code review guidelines documentation from .code_review/ rules."""
13+
14+
@property
15+
def name(self) -> str:
16+
return "code-review-guidelines"
17+
18+
@property
19+
def default_filenames(self) -> List[str]:
20+
return [
21+
"CLAUDE.md",
22+
"AGENTS.md",
23+
".github/copilot-instructions.md",
24+
]
25+
26+
def generate(self, rules: Dict[str, List[RuleMetadata]], config: Dict[str, Any]) -> str:
27+
"""Generate review guidelines content with XML tags."""
28+
lines = [
29+
"<code-review-guidelines>",
30+
"<!-- DO NOT EDIT THIS SECTION - Auto-generated from .code_review/ -->",
31+
"",
32+
"## Review guidelines",
33+
"",
34+
]
35+
36+
# Sort categories (root comes last)
37+
sorted_categories = sorted(rules.keys(), key=lambda x: (x == "root", x))
38+
39+
for category in sorted_categories:
40+
category_rules = rules[category]
41+
if not category_rules:
42+
continue
43+
44+
# Add category heading (skip for root or if only one category)
45+
if category != "root" and len(sorted_categories) > 1:
46+
heading = self._format_heading(category)
47+
lines.append(f"### {heading}")
48+
lines.append("")
49+
50+
# Add each rule
51+
for rule in sorted(category_rules, key=lambda r: r.title):
52+
lines.extend(self._format_rule(rule))
53+
lines.append("")
54+
55+
lines.append("</code-review-guidelines>")
56+
return "\n".join(lines)
57+
58+
def get_section_markers(self) -> tuple[str, str]:
59+
"""Return XML tags for the auto-generated section."""
60+
return ("<code-review-guidelines>", "</code-review-guidelines>")
61+
62+
def _format_heading(self, category: str) -> str:
63+
"""Format category as heading."""
64+
# Convert folder name to title case
65+
return category.replace("-", " ").title()
66+
67+
def _format_rule(self, rule: RuleMetadata) -> List[str]:
68+
"""Format individual rule as markdown."""
69+
# Use @ prefix for rule path
70+
rule_path = f"@{rule.relative_path}"
71+
72+
return [
73+
f"**{rule.title}** → `{rule_path}`",
74+
"",
75+
f"- **Description**: {rule.description or 'No description provided'}",
76+
]

sync_ai_rules/generators/markdown_generator.py renamed to sync_ai_rules/generators/development_rules_generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
#!/usr/bin/env python3
22
"""
3-
Markdown Generator plugin - generates markdown documentation with XML tags.
3+
Development Rules Generator plugin - generates development rules documentation with XML tags.
44
"""
55

66
from typing import Any, Dict, List
77

88
from sync_ai_rules.core.interfaces import OutputGenerator, RuleMetadata
99

1010

11-
class MarkdownGenerator(OutputGenerator):
12-
"""Generate markdown documentation from rules."""
11+
class DevelopmentRulesGenerator(OutputGenerator):
12+
"""Generate development rules documentation from .cursor/rules/."""
1313

1414
@property
1515
def name(self) -> str:
16-
return "markdown"
16+
return "development-rules"
1717

1818
@property
1919
def default_filenames(self) -> List[str]:
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Code Review Parser plugin - parses code review markdown files with HTML comment frontmatter.
4+
"""
5+
6+
import re
7+
from pathlib import Path
8+
from typing import Any, Dict, Optional
9+
10+
from sync_ai_rules.core.interfaces import InputParser, RuleMetadata
11+
12+
13+
class CodeReviewParser(InputParser):
14+
"""Parse code review markdown files from .code_review/ directory."""
15+
16+
@property
17+
def name(self) -> str:
18+
return "code-review"
19+
20+
@property
21+
def supported_extensions(self) -> list[str]:
22+
return [".md"]
23+
24+
def can_parse(self, file_path: str) -> bool:
25+
"""Check if this parser can handle the given file."""
26+
return ".code_review" in file_path and file_path.endswith(".md")
27+
28+
def parse(self, file_path: str, context: Dict[str, Any]) -> Optional[RuleMetadata]:
29+
"""Parse a code review markdown file and extract metadata."""
30+
try:
31+
with open(file_path, encoding="utf-8") as f:
32+
content = f.read()
33+
34+
# Extract HTML comment frontmatter
35+
metadata = self._parse_frontmatter(content)
36+
if not metadata:
37+
return None
38+
39+
# Extract category from directory structure
40+
path = Path(file_path)
41+
category = self._extract_category(path, context.get("project_root"))
42+
43+
# Get relative path from project root
44+
project_root = Path(context.get("project_root", "."))
45+
relative_path = path.relative_to(project_root)
46+
47+
return RuleMetadata(
48+
file_path=file_path,
49+
relative_path=str(relative_path),
50+
title=metadata.get("name", path.stem.replace("-", " ").title()),
51+
description=metadata.get("description", ""),
52+
scope_patterns=[], # Code review rules don't have file scope
53+
always_apply=False, # Code review rules are always contextual
54+
category=category,
55+
raw_content=content,
56+
metadata=metadata,
57+
)
58+
59+
except Exception as e:
60+
print(f"Error parsing {file_path}: {e}")
61+
return None
62+
63+
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
64+
"""Parse HTML comment frontmatter from markdown content."""
65+
# Match HTML comment block at start of file
66+
# Pattern: <!--\nname: ...\ndescription: ...\n-->
67+
pattern = r"^<!--\s*\n(.*?)\n-->"
68+
match = re.search(pattern, content, re.MULTILINE | re.DOTALL)
69+
70+
if not match:
71+
return {}
72+
73+
frontmatter_text = match.group(1)
74+
metadata = {}
75+
76+
# Parse key: value pairs
77+
for line in frontmatter_text.split("\n"):
78+
line = line.strip()
79+
if ":" in line:
80+
key, value = line.split(":", 1)
81+
metadata[key.strip()] = value.strip()
82+
83+
return metadata
84+
85+
def _extract_category(self, file_path: Path, project_root: Optional[str]) -> str:
86+
"""Extract category from directory structure."""
87+
# Find .code_review in the path
88+
parts = file_path.parts
89+
try:
90+
code_review_idx = parts.index(".code_review")
91+
# Category is the directory immediately after .code_review
92+
if code_review_idx + 1 < len(parts) - 1: # -1 because last part is filename
93+
return parts[code_review_idx + 1]
94+
except (ValueError, IndexError):
95+
pass
96+
97+
return "root"

sync_ai_rules/plugins.yaml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@ parsers:
88
class: MDCParser
99
description: Parse MDC files with YAML frontmatter
1010

11+
- name: code-review
12+
module: code_review_parser
13+
class: CodeReviewParser
14+
description: Parse code review markdown files from .code_review/
15+
1116
generators:
12-
- name: markdown
13-
module: markdown_generator
14-
class: MarkdownGenerator
15-
description: Generate markdown documentation with XML tags
17+
- name: development-rules
18+
module: development_rules_generator
19+
class: DevelopmentRulesGenerator
20+
description: Generate development rules documentation from .cursor/rules/
21+
22+
- name: code-review-guidelines
23+
module: code_review_guidelines_generator
24+
class: CodeReviewGuidelinesGenerator
25+
description: Generate code review guidelines documentation from .code_review/

0 commit comments

Comments
 (0)