docuchango 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
docuchango/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """Docuchango - Docusaurus Validation and Repair Framework.
2
+
3
+ A comprehensive toolkit for Docusaurus documentation validation, testing, and repair.
4
+ Designed for opinionated micro-CMS documentation systems with human-agent collaboration.
5
+
6
+ This package provides:
7
+ - Documentation validation (frontmatter, links, formatting)
8
+ - Automated fixing of common documentation issues
9
+ - Testing utilities for documentation workflows
10
+ - CLI tools for all operations
11
+
12
+ Example:
13
+ >>> from docuchango.validator import DocValidator
14
+ >>> validator = DocValidator(repo_root=".")
15
+ >>> validator.scan_documents()
16
+ >>> validator.check_code_blocks()
17
+ """
18
+
19
+ __version__ = "0.1.0"
20
+ __author__ = "Jacob Repp"
21
+ __email__ = "jacobrepp@gmail.com"
22
+
23
+ # Core validation exports
24
+ from docuchango.schemas import (
25
+ ADRFrontmatter,
26
+ GenericDocFrontmatter,
27
+ MemoFrontmatter,
28
+ RFCFrontmatter,
29
+ )
30
+
31
+ # Testing framework exports
32
+ from docuchango.testing import AGFAssertions, AGFCLIRunner, CLIResult, HealthChecker
33
+
34
+ __all__ = [
35
+ # Version info
36
+ "__version__",
37
+ "__author__",
38
+ "__email__",
39
+ # Schemas
40
+ "ADRFrontmatter",
41
+ "RFCFrontmatter",
42
+ "MemoFrontmatter",
43
+ "GenericDocFrontmatter",
44
+ # Testing
45
+ "AGFCLIRunner",
46
+ "CLIResult",
47
+ "AGFAssertions",
48
+ "HealthChecker",
49
+ ]
docuchango/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Main entry point for docuchango package.
2
+
3
+ Allows running the package as a module:
4
+ python -m docuchango
5
+ """
6
+
7
+ from docuchango.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
docuchango/cli.py ADDED
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env python3
2
+ """Docuchango CLI.
3
+
4
+ Docusaurus validation and repair framework for opinionated micro-CMS documentation.
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.group()
17
+ @click.version_option(version="0.1.0")
18
+ def main():
19
+ """Docuchango - Docusaurus validation and repair framework."""
20
+ pass
21
+
22
+
23
+ @main.command()
24
+ @click.option(
25
+ "--repo-root",
26
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
27
+ default=Path.cwd(),
28
+ help="Repository root directory (default: current directory)",
29
+ )
30
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
31
+ @click.option("--skip-build", is_flag=True, help="Skip Docusaurus build validation")
32
+ @click.option("--fix", is_flag=True, help="Auto-fix issues where possible")
33
+ def validate(
34
+ repo_root: Path,
35
+ verbose: bool,
36
+ skip_build: bool,
37
+ fix: bool,
38
+ ):
39
+ """Validate documentation files for correctness.
40
+
41
+ Validates markdown documents for:
42
+ - YAML frontmatter format and required fields
43
+ - Internal link reachability
44
+ - Markdown formatting issues
45
+ - Consistent ADR/RFC numbering
46
+ - MDX compilation compatibility
47
+ - Docusaurus build validation (unless --skip-build)
48
+ """
49
+ try:
50
+ from docuchango.validator import DocValidator
51
+ except ImportError as e:
52
+ console.print(f"[red]Error importing validator: {e}[/red]")
53
+ sys.exit(2)
54
+
55
+ console.print("[bold blue]🔍 Validating Documentation[/bold blue]\n")
56
+ console.print(f"Repository root: {repo_root}")
57
+ console.print(f"Verbose: {verbose}")
58
+ console.print(f"Skip build: {skip_build}")
59
+ console.print(f"Auto-fix: {fix}\n")
60
+
61
+ try:
62
+ validator = DocValidator(repo_root=repo_root, verbose=verbose)
63
+ validator.scan_documents()
64
+ validator.check_code_blocks()
65
+ validator.check_formatting()
66
+
67
+ # Check if we should run build validation
68
+ if not skip_build:
69
+ # This would need to be implemented based on the validate_docs.py logic
70
+ pass
71
+
72
+ # Check for errors
73
+ has_errors = False
74
+ for doc in validator.documents:
75
+ if doc.errors:
76
+ has_errors = True
77
+ console.print(f"[red]✗[/red] {doc.file_path}")
78
+ for error in doc.errors:
79
+ console.print(f" [red]{error}[/red]")
80
+
81
+ if validator.errors:
82
+ has_errors = True
83
+ for error in validator.errors:
84
+ console.print(f"[red]{error}[/red]")
85
+
86
+ if has_errors:
87
+ console.print("\n[bold red]❌ Validation failed[/bold red]")
88
+ sys.exit(1)
89
+ else:
90
+ console.print("\n[bold green]✅ All documents valid[/bold green]")
91
+ sys.exit(0)
92
+
93
+ except Exception as e:
94
+ console.print(f"[bold red]Error during validation: {e}[/bold red]")
95
+ if verbose:
96
+ import traceback
97
+
98
+ traceback.print_exc()
99
+ sys.exit(2)
100
+
101
+
102
+ @main.group()
103
+ def fix():
104
+ """Fix documentation issues automatically."""
105
+ pass
106
+
107
+
108
+ @fix.command("all")
109
+ @click.option(
110
+ "--repo-root",
111
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
112
+ default=Path.cwd(),
113
+ help="Repository root directory (default: current directory)",
114
+ )
115
+ @click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
116
+ def fix_all(repo_root: Path, dry_run: bool):
117
+ """Run all automatic fixes on documentation."""
118
+
119
+ console.print("[bold blue]🔧 Fixing Documentation Issues[/bold blue]\n")
120
+ if dry_run:
121
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
122
+
123
+ # This would call the fix functions
124
+ # For now, just show what would happen
125
+ console.print("Would fix:")
126
+ console.print(" • Trailing whitespace")
127
+ console.print(" • Code fence languages")
128
+ console.print(" • Blank lines before fences")
129
+ console.print(" • Missing frontmatter fields")
130
+
131
+
132
+ @fix.command("links")
133
+ @click.option(
134
+ "--repo-root",
135
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
136
+ default=Path.cwd(),
137
+ help="Repository root directory (default: current directory)",
138
+ )
139
+ @click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
140
+ def fix_links(repo_root: Path, dry_run: bool):
141
+ """Fix broken links in documentation."""
142
+ console.print("[bold blue]🔗 Fixing Broken Links[/bold blue]\n")
143
+ if dry_run:
144
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
145
+
146
+ # Import and run the fix_broken_links module
147
+ console.print("Fixing broken links...")
148
+
149
+
150
+ @fix.command("code-blocks")
151
+ @click.option(
152
+ "--repo-root",
153
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
154
+ default=Path.cwd(),
155
+ help="Repository root directory (default: current directory)",
156
+ )
157
+ @click.option("--dry-run", is_flag=True, help="Show what would be fixed without making changes")
158
+ def fix_code_blocks(repo_root: Path, dry_run: bool):
159
+ """Fix code block formatting issues."""
160
+ console.print("[bold blue]📝 Fixing Code Blocks[/bold blue]\n")
161
+ if dry_run:
162
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]\n")
163
+
164
+ console.print("Fixing code blocks...")
165
+
166
+
167
+ @main.group()
168
+ def test():
169
+ """Testing utilities and helpers."""
170
+ pass
171
+
172
+
173
+ @test.command("health")
174
+ @click.option("--url", default="http://localhost:8080", help="Service URL to check")
175
+ @click.option("--timeout", default=30, help="Timeout in seconds")
176
+ def test_health(url: str, timeout: int):
177
+ """Check service health."""
178
+ console.print(f"[bold blue]🏥 Checking Health: {url}[/bold blue]\n")
179
+
180
+ # Placeholder for health check implementation
181
+ # HealthChecker would be initialized and used here
182
+ console.print(f"[yellow]ℹ[/yellow] Health check not yet implemented for {url}")
183
+ console.print(f"[dim]Timeout: {timeout}s[/dim]")
184
+ console.print("[green]✓[/green] Placeholder completed")
185
+
186
+
187
+ @main.command()
188
+ @click.option(
189
+ "--guide",
190
+ type=click.Choice(["bootstrap", "agent", "best-practices"], case_sensitive=False),
191
+ default="bootstrap",
192
+ help="Which guide to display (default: bootstrap)",
193
+ )
194
+ @click.option(
195
+ "--output",
196
+ type=click.Path(path_type=Path),
197
+ help="Save guide to file instead of displaying",
198
+ )
199
+ def bootstrap(guide: str, output: Path | None):
200
+ """Display or save docs-cms bootstrap and agent guides.
201
+
202
+ Provides quick access to documentation for setting up and using docs-cms:
203
+
204
+ - bootstrap: Step-by-step setup guide for docs-cms
205
+ - agent: Instructions for AI agents using docs-cms
206
+ - best-practices: Best practices for agent-CMS interaction
207
+
208
+ Examples:
209
+
210
+ \b
211
+ # Display bootstrap guide
212
+ docuchango bootstrap
213
+
214
+ \b
215
+ # Display agent guide
216
+ docuchango bootstrap --guide agent
217
+
218
+ \b
219
+ # Save bootstrap guide to file
220
+ docuchango bootstrap --output /path/to/BOOTSTRAP_GUIDE.md
221
+ """
222
+ # Map guide names to file names
223
+ guide_files = {
224
+ "bootstrap": "BOOTSTRAP_GUIDE.md",
225
+ "agent": "AGENT_GUIDE.md",
226
+ "best-practices": "BEST_PRACTICES.md",
227
+ }
228
+
229
+ guide_file = guide_files[guide]
230
+
231
+ # Try to find the guide in the package
232
+ try:
233
+ # First, try to find it in the installed package
234
+ import importlib.resources as resources
235
+
236
+ try:
237
+ # Python 3.9+
238
+ guide_path = resources.files("docuchango") / ".." / "docs" / guide_file
239
+ guide_content = guide_path.read_text()
240
+ except AttributeError:
241
+ # Python 3.8 fallback
242
+ with resources.path("docuchango", "__init__.py") as pkg_path:
243
+ docs_dir = pkg_path.parent.parent / "docs"
244
+ guide_path = docs_dir / guide_file
245
+ guide_content = guide_path.read_text()
246
+
247
+ except (FileNotFoundError, ModuleNotFoundError):
248
+ # Fallback: try relative to the script location
249
+ script_dir = Path(__file__).parent.parent
250
+ guide_path = script_dir / "docs" / guide_file
251
+
252
+ if not guide_path.exists():
253
+ from rich.console import Console as RichConsole
254
+
255
+ stderr_console = RichConsole(stderr=True)
256
+ stderr_console.print(f"[red]✗[/red] Guide not found: {guide_file}")
257
+ stderr_console.print(f"[dim]Searched in: {guide_path}[/dim]")
258
+ sys.exit(1)
259
+
260
+ guide_content = guide_path.read_text()
261
+
262
+ # Output or display
263
+ if output:
264
+ output.write_text(guide_content)
265
+ console.print(f"[green]✓[/green] Saved {guide} guide to: {output}")
266
+ else:
267
+ # Display with rich markdown rendering
268
+ from rich.markdown import Markdown
269
+
270
+ console.print(Markdown(guide_content))
271
+
272
+
273
+ # Export the validate command as a separate entry point
274
+ def validate_main():
275
+ """Entry point for agf-validate command."""
276
+ validate()
277
+
278
+
279
+ # Export the fix command as a separate entry point
280
+ def fix_main():
281
+ """Entry point for agf-fix command."""
282
+ fix()
283
+
284
+
285
+ if __name__ == "__main__":
286
+ main()
@@ -0,0 +1,17 @@
1
+ """Documentation fix modules.
2
+
3
+ This package contains modules for automatically fixing common documentation issues:
4
+ - broken_links: Fix broken internal and cross-reference links
5
+ - code_blocks: Fix code block formatting and language tags
6
+ - docs: General documentation fixes (whitespace, frontmatter, etc.)
7
+ - internal_links: Fix internal link references
8
+ - mdx_syntax: Fix MDX syntax issues
9
+ """
10
+
11
+ __all__ = [
12
+ "broken_links",
13
+ "code_blocks",
14
+ "docs",
15
+ "internal_links",
16
+ "mdx_syntax",
17
+ ]
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """Fix broken documentation links by converting full filenames to short-form IDs.
3
+
4
+ Usage:
5
+ uv run tooling/fix_broken_links.py [--dry-run]
6
+ """
7
+
8
+ import argparse
9
+ import re
10
+ from pathlib import Path
11
+
12
+ # Mapping of common broken link patterns to correct formats
13
+ LINK_FIXES = {
14
+ # RFCs - full filename to short ID
15
+ r"/rfc/rfc-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
16
+ r"/prism-data-layer/rfc/rfc-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
17
+ r"/prism-data-layer/rfc/RFC-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
18
+ r"\.\/RFC-(\d+)-[a-z-]+": r"/rfc/rfc-\1",
19
+ # ADRs - full filename to short ID
20
+ r"/adr/adr-(\d+)-[a-z-]+": r"/adr/adr-\1",
21
+ r"/prism-data-layer/adr/adr-(\d+)-[a-z-]+": r"/adr/adr-\1",
22
+ # MEMOs - full filename to short ID
23
+ r"/memos/memo-(\d+)-[a-z-]+": r"/memos/memo-\1",
24
+ r"/prism-data-layer/memos/memo-(\d+)-[a-z-]+": r"/memos/memo-\1",
25
+ # Remove /prism-data-layer prefix from already short paths
26
+ r"/prism-data-layer/(adr/adr-\d+)": r"/\1",
27
+ r"/prism-data-layer/(rfc/rfc-\d+)": r"/\1",
28
+ r"/prism-data-layer/(memos/memo-\d+)": r"/\1",
29
+ r"/prism-data-layer/(prd)": r"/\1",
30
+ r"/prism-data-layer/(key-documents)": r"/\1",
31
+ r"/prism-data-layer/(netflix/[a-z\-]+)": r"/\1",
32
+ # Fix incorrectly converted RFC numbers (rfc-211 should be rfc-021)
33
+ r"/rfc/rfc-211([^0-9])": r"/rfc/rfc-021\1",
34
+ r"/rfc/rfc-211$": r"/rfc/rfc-021",
35
+ # Fix netflix links - add netflix- prefix to all document names
36
+ r"/netflix/abstractions\b": r"/netflix/netflix-abstractions",
37
+ r"/netflix/write-ahead-log\b": r"/netflix/netflix-write-ahead-log",
38
+ r"/netflix/scale\b": r"/netflix/netflix-scale",
39
+ r"/netflix/dual-write-migration\b": r"/netflix/netflix-dual-write-migration",
40
+ r"/netflix/data-evolve-migration\b": r"/netflix/netflix-data-evolve-migration",
41
+ r"/netflix/summary\b": r"/netflix/netflix-summary",
42
+ r"/netflix/key-use-cases\b": r"/netflix/netflix-key-use-cases",
43
+ r"/netflix/netflix-index\b": r"/netflix/netflix-index", # This one is already correct but keeping for completeness
44
+ r"/netflix/video1\b": r"/netflix/netflix-video1",
45
+ r"/netflix/video2\b": r"/netflix/netflix-video2",
46
+ }
47
+
48
+
49
+ def fix_links_in_file(file_path: Path, dry_run: bool = False) -> int:
50
+ """Fix broken links in a single file."""
51
+ try:
52
+ content = file_path.read_text()
53
+ original_content = content
54
+ changes = 0
55
+
56
+ for pattern, replacement in LINK_FIXES.items():
57
+ new_content, count = re.subn(pattern, replacement, content)
58
+ if count > 0:
59
+ changes += count
60
+ content = new_content
61
+
62
+ if content != original_content:
63
+ if dry_run:
64
+ print(f"Would fix {changes} links in: {file_path}")
65
+ else:
66
+ file_path.write_text(content)
67
+ print(f"Fixed {changes} links in: {file_path}")
68
+ return changes
69
+
70
+ return 0
71
+ except Exception as e:
72
+ print(f"Error processing {file_path}: {e}")
73
+ return 0
74
+
75
+
76
+ def main():
77
+ parser = argparse.ArgumentParser(description="Fix broken documentation links")
78
+ parser.add_argument("--dry-run", action="store_true", help="Show what would be changed without making changes")
79
+ args = parser.parse_args()
80
+
81
+ repo_root = Path(__file__).parent.parent
82
+ docs_cms = repo_root / "docs-cms"
83
+ docusaurus_docs = repo_root / "docusaurus" / "docs"
84
+
85
+ total_changes = 0
86
+ total_files = 0
87
+
88
+ # Process all markdown files in docs-cms
89
+ for md_file in docs_cms.rglob("*.md"):
90
+ changes = fix_links_in_file(md_file, args.dry_run)
91
+ if changes > 0:
92
+ total_changes += changes
93
+ total_files += 1
94
+
95
+ # Process all markdown files in docusaurus/docs
96
+ for md_file in docusaurus_docs.rglob("*.md"):
97
+ changes = fix_links_in_file(md_file, args.dry_run)
98
+ if changes > 0:
99
+ total_changes += changes
100
+ total_files += 1
101
+
102
+ print(f"\n{'[DRY RUN] ' if args.dry_run else ''}Fixed {total_changes} links in {total_files} files")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ """Auto-fix code block formatting issues
3
+
4
+ Fixes:
5
+ 1. Missing blank lines before opening code fences
6
+ 2. Missing blank lines after closing code fences
7
+ 3. Closing fences with extra text (```bash -> ```)
8
+ 4. Opening fences without language (``` -> ```text)
9
+ 5. Unclosed code blocks
10
+
11
+ Usage:
12
+ uv run python -m tooling.fix_code_blocks
13
+ """
14
+
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ def fix_code_blocks(file_path: Path) -> tuple[bool, list[str]]:
21
+ """Fix code block issues in a file"""
22
+ changes = []
23
+
24
+ try:
25
+ content = file_path.read_text(encoding="utf-8")
26
+ lines = content.split("\n")
27
+ fixed_lines = []
28
+
29
+ in_code_block = False
30
+ in_frontmatter = False
31
+ frontmatter_count = 0
32
+ frontmatter_end_line = None
33
+ i = 0
34
+
35
+ while i < len(lines):
36
+ line = lines[i]
37
+ stripped = line.strip()
38
+
39
+ # Track frontmatter
40
+ if stripped == "---":
41
+ frontmatter_count += 1
42
+ if frontmatter_count == 1:
43
+ in_frontmatter = True
44
+ elif frontmatter_count == 2:
45
+ in_frontmatter = False
46
+ frontmatter_end_line = i
47
+ fixed_lines.append(line)
48
+ i += 1
49
+ continue
50
+
51
+ # Skip frontmatter content
52
+ if in_frontmatter:
53
+ fixed_lines.append(line)
54
+ i += 1
55
+ continue
56
+
57
+ # Check for code fence (match beginning of stripped line)
58
+ fence_match = re.match(r"^(`{3,})(.*)$", stripped)
59
+ if fence_match:
60
+ fence_backticks = fence_match.group(1)
61
+ remainder = fence_match.group(2).strip()
62
+
63
+ if not in_code_block:
64
+ # Opening fence
65
+ content_start = (frontmatter_end_line + 1) if frontmatter_end_line is not None else 0
66
+ is_after_frontmatter = frontmatter_end_line is not None and i == frontmatter_end_line + 1
67
+ is_document_start = i == content_start
68
+
69
+ # Check if previous line was blank
70
+ previous_line_blank = len(fixed_lines) == 0 or not fixed_lines[-1].strip()
71
+
72
+ # Add blank line before if needed
73
+ if not previous_line_blank and not is_after_frontmatter and not is_document_start:
74
+ fixed_lines.append("")
75
+ changes.append(f"Line {i + 1}: Added blank line before opening fence")
76
+
77
+ # Check if language is missing
78
+ if not remainder:
79
+ fixed_lines.append(f"{fence_backticks}text")
80
+ changes.append(f"Line {i + 1}: Added 'text' language to bare opening fence")
81
+ else:
82
+ fixed_lines.append(line)
83
+
84
+ in_code_block = True
85
+ else:
86
+ # Closing fence
87
+ if remainder:
88
+ # Remove extra text from closing fence
89
+ fixed_lines.append(fence_backticks)
90
+ changes.append(f"Line {i + 1}: Removed extra text from closing fence (```{remainder} -> ```)")
91
+ else:
92
+ fixed_lines.append(line)
93
+
94
+ in_code_block = False
95
+
96
+ # Check next line for blank line requirement
97
+ if i + 1 < len(lines):
98
+ next_line = lines[i + 1].strip()
99
+ if next_line: # Next line has content
100
+ # Insert blank line after closing fence
101
+ i += 1 # Move to next line
102
+ fixed_lines.append("") # Add blank line
103
+ changes.append(f"Line {i}: Added blank line after closing fence")
104
+ continue # Will process next line in next iteration
105
+ else:
106
+ # Regular line (not a code fence)
107
+ if not in_code_block:
108
+ fixed_lines.append(line)
109
+ else:
110
+ # Inside code block - preserve exactly
111
+ fixed_lines.append(line)
112
+
113
+ i += 1
114
+
115
+ # Check for unclosed code block
116
+ if in_code_block:
117
+ fixed_lines.append("```")
118
+ changes.append("End of file: Added closing fence for unclosed code block")
119
+
120
+ # Write back if changes were made
121
+ if changes:
122
+ new_content = "\n".join(fixed_lines)
123
+ file_path.write_text(new_content, encoding="utf-8")
124
+ return True, changes
125
+
126
+ return False, []
127
+
128
+ except Exception as e:
129
+ print(f"Error processing {file_path}: {e}", file=sys.stderr)
130
+ return False, []
131
+
132
+
133
+ def main():
134
+ repo_root = Path(__file__).parent.parent
135
+ docs_cms = repo_root / "docs-cms"
136
+
137
+ print("🔧 Auto-fixing code block issues...\n")
138
+
139
+ total_files = 0
140
+ fixed_files = 0
141
+ total_changes = 0
142
+
143
+ # Process all markdown files
144
+ for md_file in docs_cms.rglob("*.md"):
145
+ # Skip README and index files
146
+ if md_file.name in ["README.md", "index.md"]:
147
+ continue
148
+
149
+ total_files += 1
150
+ modified, changes = fix_code_blocks(md_file)
151
+
152
+ if modified:
153
+ fixed_files += 1
154
+ total_changes += len(changes)
155
+ print(f"✓ {md_file.relative_to(repo_root)}")
156
+ for change in changes:
157
+ print(f" • {change}")
158
+ print()
159
+
160
+ print(f"\n{'=' * 80}")
161
+ print("📊 Summary:")
162
+ print(f" Files scanned: {total_files}")
163
+ print(f" Files fixed: {fixed_files}")
164
+ print(f" Total changes: {total_changes}")
165
+ print(f"{'=' * 80}\n")
166
+
167
+ if fixed_files > 0:
168
+ print("✅ Fixes applied! Run validation again to verify.")
169
+ else:
170
+ print("✅ No fixes needed.")
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()