braxis 1.0.0__tar.gz

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.
braxis-1.0.0/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ MIT License
2
+ Copyright (c) 2026 Jayakrishna Ichapurapu
3
+ Permission is hereby granted to use this software.
braxis-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: braxis
3
+ Version: 1.0.0
4
+ Summary: Auto-generate context files for AI agents
5
+ Author: Jayakrishna Ichapurapu
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ License-File: LICENSE
9
+ Dynamic: author
10
+ Dynamic: license
11
+ Dynamic: license-file
12
+ Dynamic: requires-python
13
+ Dynamic: summary
braxis-1.0.0/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # Braxis
2
+
3
+ **Auto-generate AI agent context files. Keep them in sync with your code.**
4
+
5
+ Your AI agents (Claude Code, Cursor, Copilot) read from `AGENTS.md` to understand your project. When your code changes, that file gets stale. Agents miss patterns, violate conventions, hallucinate.
6
+
7
+ Braxis solves this: **one command generates four context files that stay in sync with your codebase.**
8
+
9
+ ---
10
+
11
+ ## See It In Action
12
+
13
+ Before Braxis:
14
+
15
+ Day 1: Agent reads stale AGENTS.md from 2 weeks ago
16
+ Sees old directory structure
17
+ Doesn't know about new error handling pattern
18
+ Makes bad suggestions based on outdated info
19
+
20
+
21
+ After Braxis:
22
+
23
+ Every push: GitHub Actions runs Braxis
24
+ Analyzes current codebase
25
+ Regenerates AGENTS.md, CLAUDE.md, .cursorrules, .agentic-config.json
26
+ Creates PR with updates
27
+ Your agents always see current reality
28
+
29
+
30
+ ---
31
+
32
+ ## How To Use Braxis On Your Project (5 minutes)
33
+
34
+ ### Step 1: Install
35
+
36
+ ```bash
37
+ pip install braxis
38
+ ```
39
+
40
+ ### Step 2: Go to Your Project
41
+
42
+ ```bash
43
+ cd /path/to/your/project
44
+ ```
45
+
46
+ ### Step 3: Generate Context Files
47
+
48
+ ```bash
49
+ braxis generate
50
+ ```
51
+
52
+ That's it. Braxis creates:
53
+
54
+ your-project/
55
+ ├── AGENTS.md (Universal agent instructions)
56
+ ├── CLAUDE.md (Claude Code optimized)
57
+ ├── .cursorrules (Cursor IDE rules)
58
+ ├── .agentic-config.json (Machine-readable metadata)
59
+ └── (your existing files)
60
+
61
+
62
+ ### Step 4: See What It Generated
63
+
64
+ ```bash
65
+ cat AGENTS.md
66
+ ```
67
+
68
+ ### Step 5: Commit to Your Repo
69
+
70
+ ```bash
71
+ git add AGENTS.md CLAUDE.md .cursorrules .agentic-config.json
72
+ git commit -m "chore: add AI agent context files"
73
+ git push
74
+ ```
75
+
76
+ ### Step 6: Your Agents Now Use These Files
77
+
78
+ **In Claude Code:** Automatically reads `CLAUDE.md`
79
+ **In Cursor:** Copy `.cursorrules` into Cursor Settings → Rules
80
+ **In any agent:** Reads `AGENTS.md` (universal format)
81
+
82
+ ---
83
+
84
+ ## Make It Auto-Update (Optional)
85
+
86
+ Create `.github/workflows/braxis.yml`:
87
+
88
+ ```yaml
89
+ name: Auto-Regenerate Context Files
90
+
91
+ on:
92
+ push:
93
+ paths:
94
+ - 'src/**'
95
+ - 'lib/**'
96
+ - 'package.json'
97
+ - 'pyproject.toml'
98
+ - 'setup.py'
99
+
100
+ jobs:
101
+ regenerate:
102
+ runs-on: ubuntu-latest
103
+ steps:
104
+ - uses: actions/checkout@v4
105
+ - uses: actions/setup-python@v4
106
+ with:
107
+ python-version: '3.11'
108
+ - run: pip install braxis
109
+ - run: braxis generate
110
+ - uses: peter-evans/create-pull-request@v5
111
+ with:
112
+ commit-message: "chore: regenerate context files"
113
+ title: "chore: update AGENTS.md and context files"
114
+ branch: braxis/auto-update
115
+ ```
116
+
117
+ Now every time you push code, GitHub Actions automatically regenerates all context files.
118
+
119
+ ---
120
+
121
+ ## Customize For Your Project (Optional)
122
+
123
+ Create `.agentic-config.json`:
124
+
125
+ ```json
126
+ {
127
+ "name": "MyApp",
128
+ "description": "A production API service",
129
+ "exclude_patterns": [
130
+ "node_modules/**",
131
+ ".venv/**",
132
+ "build/**"
133
+ ],
134
+ "custom_conventions": {
135
+ "error_handling": "Always use try/except and log",
136
+ "async_patterns": "All I/O must be async",
137
+ "validation": "Use Pydantic models for inputs"
138
+ },
139
+ "critical_files": [
140
+ "src/main.py",
141
+ "src/api/routes.py",
142
+ "README.md"
143
+ ]
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ## What Each File Does
150
+
151
+ **AGENTS.md** - Universal format read by any AI agent
152
+
153
+ **CLAUDE.md** - Optimized for Claude Code
154
+
155
+ **.cursorrules** - Rules for Cursor IDE
156
+
157
+ **.agentic-config.json** - Machine-readable metadata
158
+
159
+ ---
160
+
161
+ ## Commands
162
+
163
+ ```bash
164
+ braxis generate # Generate context files
165
+ braxis score # Score agent readiness (0-100)
166
+ braxis inspect # See what Braxis found
167
+ braxis validate # Verify AGENTS.md exists
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Requirements
173
+
174
+ - Python 3.8+
175
+ - Zero external dependencies
176
+
177
+ ---
178
+
179
+ ## License
180
+
181
+ MIT
182
+
183
+ ---
184
+
185
+ **Braxis** — Keep your agents aligned. Keep your code context current.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: braxis
3
+ Version: 1.0.0
4
+ Summary: Auto-generate context files for AI agents
5
+ Author: Jayakrishna Ichapurapu
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ License-File: LICENSE
9
+ Dynamic: author
10
+ Dynamic: license
11
+ Dynamic: license-file
12
+ Dynamic: requires-python
13
+ Dynamic: summary
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ braxis.py
4
+ setup.py
5
+ braxis.egg-info/PKG-INFO
6
+ braxis.egg-info/SOURCES.txt
7
+ braxis.egg-info/dependency_links.txt
8
+ braxis.egg-info/top_level.txt
9
+ tests/test_braxis.py
@@ -0,0 +1 @@
1
+ braxis
braxis-1.0.0/braxis.py ADDED
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Braxis - Auto-generate AI agent context files.
4
+ Keep AGENTS.md, CLAUDE.md, .cursorrules, and .agentic-config.json in sync with your codebase.
5
+ """
6
+ import os
7
+ import sys
8
+ import json
9
+ import argparse
10
+ import tempfile
11
+ from pathlib import Path
12
+ from collections import defaultdict
13
+ from datetime import datetime
14
+
15
+
16
+ class BraxisAnalyzer:
17
+ """Analyzes a codebase and generates agent context files."""
18
+
19
+ LANGUAGE_EXTENSIONS = {
20
+ 'python': ['.py'],
21
+ 'javascript': ['.js', '.jsx'],
22
+ 'typescript': ['.ts', '.tsx'],
23
+ 'java': ['.java'],
24
+ 'go': ['.go'],
25
+ 'rust': ['.rs'],
26
+ 'c': ['.c', '.h'],
27
+ 'cpp': ['.cpp', '.cc', '.cxx', '.h', '.hpp'],
28
+ 'csharp': ['.cs'],
29
+ 'php': ['.php'],
30
+ 'ruby': ['.rb'],
31
+ 'swift': ['.swift'],
32
+ 'kotlin': ['.kt'],
33
+ 'scala': ['.scala'],
34
+ 'r': ['.R', '.r'],
35
+ 'sql': ['.sql'],
36
+ }
37
+ TEST_PATTERNS = ['test_', '_test.', 'spec_', '.spec.', 'tests/', 'test/']
38
+ BUILD_FILES = ['package.json', 'pyproject.toml', 'setup.py', 'Makefile', 'build.gradle', 'pom.xml', 'Cargo.toml']
39
+ CONFIG_FILES = ['.env', '.env.example', 'config.json', 'settings.py', 'config.yaml']
40
+
41
+ def __init__(self, project_path='.'):
42
+ self.project_path = self._validate_project_path(project_path)
43
+ self.files = []
44
+ self.languages = defaultdict(int)
45
+ self.test_files = []
46
+ self.config_files = []
47
+ self.build_files = []
48
+ self.conventions = defaultdict(int)
49
+ self.critical_files = []
50
+ self.score_breakdown = {}
51
+ self.tier = "Not Ready"
52
+
53
+ def _validate_project_path(self, project_path):
54
+ """Validate and normalize project path."""
55
+ if not project_path:
56
+ raise ValueError("Project path cannot be empty")
57
+ path = Path(project_path).resolve()
58
+ if not path.exists():
59
+ raise FileNotFoundError(f"Project path does not exist: {project_path}")
60
+ if not path.is_dir():
61
+ raise NotADirectoryError(f"Project path is not a directory: {project_path}")
62
+ return path
63
+
64
+ def analyze(self):
65
+ """Analyze the project."""
66
+ self._scan_files()
67
+ self._detect_languages()
68
+ self._detect_build_system()
69
+ self._detect_test_framework()
70
+ self._detect_conventions()
71
+ self._identify_critical_files()
72
+ self._calculate_score()
73
+
74
+ def _scan_files(self):
75
+ """Scan all files in project."""
76
+ ignore_dirs = {'.git', '.venv', 'node_modules', '__pycache__', 'dist', 'build', '.idea', '.vscode'}
77
+ for root, dirs, files in os.walk(self.project_path):
78
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
79
+ for file in files:
80
+ file_path = Path(root) / file
81
+ self.files.append(file_path)
82
+ if any(pattern in str(file_path) for pattern in self.TEST_PATTERNS):
83
+ self.test_files.append(file_path)
84
+ if file in self.CONFIG_FILES:
85
+ self.config_files.append(file_path)
86
+ if file in self.BUILD_FILES:
87
+ self.build_files.append(file_path)
88
+
89
+ def _detect_languages(self):
90
+ """Detect languages used in project."""
91
+ for file_path in self.files:
92
+ ext = file_path.suffix.lower()
93
+ for lang, exts in self.LANGUAGE_EXTENSIONS.items():
94
+ if ext in exts:
95
+ self.languages[lang] += 1
96
+ break
97
+
98
+ def _detect_build_system(self):
99
+ """Detect build system."""
100
+ build_system = "Unknown"
101
+ if any('package.json' in str(f) for f in self.build_files):
102
+ build_system = "npm/Node.js"
103
+ elif any('pyproject.toml' in str(f) or 'setup.py' in str(f) for f in self.build_files):
104
+ build_system = "Python (pip/setuptools)"
105
+ elif any('Cargo.toml' in str(f) for f in self.build_files):
106
+ build_system = "Rust (cargo)"
107
+ elif any('go.mod' in str(f) for f in self.build_files):
108
+ build_system = "Go (go modules)"
109
+ elif any('pom.xml' in str(f) for f in self.build_files):
110
+ build_system = "Java (Maven)"
111
+ self.build_system = build_system
112
+
113
+ def _detect_test_framework(self):
114
+ """Detect test framework."""
115
+ test_frameworks = set()
116
+ content_samples = self._sample_file_contents(limit=20)
117
+ for content in content_samples:
118
+ if 'pytest' in content or 'from pytest' in content:
119
+ test_frameworks.add('pytest')
120
+ if 'unittest' in content or 'import unittest' in content:
121
+ test_frameworks.add('unittest')
122
+ if 'jest' in content or 'describe(' in content:
123
+ test_frameworks.add('Jest')
124
+ if 'mocha' in content or 'describe(' in content:
125
+ test_frameworks.add('Mocha')
126
+ if 'rspec' in content or 'describe' in content:
127
+ test_frameworks.add('RSpec')
128
+ if 'junit' in content.lower():
129
+ test_frameworks.add('JUnit')
130
+ self.test_frameworks = test_frameworks if test_frameworks else {"None detected"}
131
+
132
+ def _detect_conventions(self):
133
+ """Detect code conventions."""
134
+ content_samples = self._sample_file_contents(limit=20)
135
+ for content in content_samples:
136
+ if 'async def' in content or 'await ' in content:
137
+ self.conventions['async'] += 1
138
+ if 'async function' in content or 'async (' in content:
139
+ self.conventions['async'] += 1
140
+ if 'try:' in content or 'except' in content:
141
+ self.conventions['error_handling'] += 1
142
+ if 'try {' in content or 'catch' in content:
143
+ self.conventions['error_handling'] += 1
144
+ if '->' in content or ': ' in content:
145
+ self.conventions['type_hints'] += 1
146
+ if 'interface ' in content or 'type ' in content:
147
+ self.conventions['type_hints'] += 1
148
+ if 'logger' in content or 'logging' in content:
149
+ self.conventions['logging'] += 1
150
+ if 'log.' in content or 'console.log' in content:
151
+ self.conventions['logging'] += 1
152
+ if 'validate' in content.lower() or 'schema' in content.lower():
153
+ self.conventions['validation'] += 1
154
+
155
+ def _identify_critical_files(self):
156
+ """Identify critical files (main, entry points, etc)."""
157
+ critical_names = ['main.py', 'app.py', 'server.py', 'index.js', 'main.js', 'app.js', 'main.rs', 'main.go', 'main.ts']
158
+ for file_path in self.files:
159
+ if file_path.name in critical_names or 'src/main' in str(file_path):
160
+ self.critical_files.append(file_path)
161
+
162
+ def _sample_file_contents(self, limit=20):
163
+ """Sample file contents for convention detection."""
164
+ samples = []
165
+ code_files = [f for f in self.files if f.suffix in ['.py', '.js', '.ts', '.go', '.rs', '.java']]
166
+ for file_path in code_files[:limit]:
167
+ try:
168
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
169
+ samples.append(f.read())
170
+ except Exception:
171
+ pass
172
+ return samples
173
+
174
+ def _calculate_score(self):
175
+ """Calculate agent readiness score."""
176
+ scores = {}
177
+ arch_score = min(20, len(self.critical_files) * 5 + 10)
178
+ scores['Architecture'] = arch_score
179
+ test_score = min(15, len(self.test_files) * 2 + 5)
180
+ scores['Testing'] = test_score
181
+ dep_score = 12 if self.build_system != "Unknown" else 6
182
+ scores['Dependencies'] = dep_score
183
+ convention_count = sum(1 for v in self.conventions.values() if v > 0)
184
+ conv_score = min(10, convention_count * 2)
185
+ scores['Conventions'] = conv_score
186
+ entry_score = min(10, len(self.critical_files) * 3 + 4)
187
+ scores['Entry Points'] = entry_score
188
+ sec_score = 10 if 'validation' in self.conventions else 5
189
+ sec_score += 5 if len(self.config_files) > 0 else 0
190
+ scores['Security'] = min(15, sec_score)
191
+ build_score = 10 if len(self.build_files) > 0 else 5
192
+ scores['Build'] = build_score
193
+ readme_exists = any(f.name.lower() == 'readme.md' for f in self.files)
194
+ doc_score = 8 if readme_exists else 3
195
+ scores['Documentation'] = doc_score
196
+ self.score_breakdown = scores
197
+ total_score = sum(scores.values())
198
+ self.total_score = total_score
199
+ if total_score >= 90:
200
+ self.tier = "Agent-Optimized"
201
+ elif total_score >= 80:
202
+ self.tier = "AI-Native-Plus"
203
+ elif total_score >= 60:
204
+ self.tier = "AI-Native"
205
+ elif total_score >= 30:
206
+ self.tier = "Agent-Aware"
207
+ else:
208
+ self.tier = "Not Ready"
209
+
210
+ def _write_file_safely(self, filepath, content):
211
+ """Write file safely using atomic operation with temp file."""
212
+ if not filepath:
213
+ raise ValueError("Filepath cannot be empty")
214
+ if not isinstance(content, str):
215
+ raise TypeError(f"Content must be str, got {type(content).__name__}")
216
+ filepath = Path(filepath)
217
+ tmp_path = None
218
+ try:
219
+ filepath.parent.mkdir(parents=True, exist_ok=True)
220
+ with tempfile.NamedTemporaryFile(
221
+ mode='w',
222
+ dir=filepath.parent,
223
+ suffix='.tmp',
224
+ delete=False,
225
+ encoding='utf-8'
226
+ ) as tmp_file:
227
+ tmp_file.write(content)
228
+ tmp_path = Path(tmp_file.name)
229
+ tmp_path.replace(filepath)
230
+ except Exception as e:
231
+ if tmp_path and tmp_path.exists():
232
+ tmp_path.unlink()
233
+ raise IOError(f"Failed to write file {filepath}: {e}")
234
+
235
+ def print_score(self):
236
+ """Print the score report."""
237
+ print(f"\n{'='*60}")
238
+ print(f"Agent Readiness Score: {self.total_score}/100")
239
+ print(f"{'='*60}\n")
240
+ print("Breakdown:\n")
241
+ for category, score in self.score_breakdown.items():
242
+ bar = chr(9608) * (score // 5) + chr(9617) * ((100 - score) // 5)
243
+ print(f" {category:20} {score:3}/100 [{bar}]")
244
+ print(f"\nTier: {self.tier}")
245
+ print(f"\nDetected:")
246
+ print(f" Languages: {', '.join(self.languages.keys()) if self.languages else 'None'}")
247
+ print(f" Build System: {self.build_system}")
248
+ print(f" Test Frameworks: {', '.join(self.test_frameworks)}")
249
+ print(f" Test Files: {len(self.test_files)}")
250
+ print(f" Critical Files: {len(self.critical_files)}")
251
+ print(f"\nRecommendations:")
252
+ if self.score_breakdown['Documentation'] < 8:
253
+ print(f" * Add or improve README.md")
254
+ if self.score_breakdown['Testing'] < 15:
255
+ print(f" * Increase test coverage")
256
+ if self.score_breakdown['Conventions'] < 10:
257
+ print(f" * Standardize code conventions")
258
+ if self.score_breakdown['Security'] < 15:
259
+ print(f" * Add input validation and security checks")
260
+ print(f"\nNext Step:")
261
+ print(f" braxis generate")
262
+ print(f"\n{'='*60}\n")
263
+
264
+ def generate_agents_md(self):
265
+ """Generate AGENTS.md file."""
266
+ primary_lang = max(self.languages.items(), key=lambda x: x[1])[0] if self.languages else "Unknown"
267
+ content = f"""# AGENTS.md
268
+ AI agents read this file to understand your project.
269
+ ## Project Identity
270
+ This is a {primary_lang.capitalize()} project. It uses {self.build_system}.
271
+ ## Tech Stack
272
+ - Language: {primary_lang.capitalize()}
273
+ - Build: {self.build_system}
274
+ - Testing: {', '.join(self.test_frameworks)}
275
+ - Total Files: {len(self.files)}
276
+ ## Quick Start
277
+ """
278
+ return content
279
+
280
+ def generate_claude_md(self):
281
+ """Generate CLAUDE.md file."""
282
+ primary_lang = max(self.languages.items(), key=lambda x: x[1])[0] if self.languages else "Unknown"
283
+ content = f"""# CLAUDE.md
284
+ Quick context for Claude Code working in this project.
285
+ ## Project
286
+ {primary_lang.capitalize()} project using {self.build_system}.
287
+ """
288
+ return content
289
+
290
+ def generate_cursorrules(self):
291
+ """Generate .cursorrules file."""
292
+ primary_lang = max(self.languages.items(), key=lambda x: x[1])[0] if self.languages else "Unknown"
293
+ rules = f"""# Cursor Rules
294
+ This is a {primary_lang.capitalize()} project using {self.build_system}.
295
+ """
296
+ return rules
297
+
298
+ def generate_agentic_config(self):
299
+ """Generate .agentic-config.json file."""
300
+ primary_lang = max(self.languages.items(), key=lambda x: x[1])[0] if self.languages else "Unknown"
301
+ config = {
302
+ "name": self.project_path.name,
303
+ "description": f"A {primary_lang.capitalize()} project",
304
+ "language": primary_lang,
305
+ "build_system": self.build_system,
306
+ "agent_readiness_score": self.total_score,
307
+ "tier": self.tier,
308
+ }
309
+ return json.dumps(config, indent=2)
310
+
311
+
312
+ def main():
313
+ parser = argparse.ArgumentParser(description='Braxis - AI agent context generator')
314
+ parser.add_argument('command', choices=['generate', 'score', 'inspect', 'validate'],
315
+ help='Command to run')
316
+ parser.add_argument('--path', default='.', help='Project path')
317
+ args = parser.parse_args()
318
+
319
+ if not args.command or args.command not in ['generate', 'score', 'inspect', 'validate']:
320
+ parser.print_help()
321
+ sys.exit(1)
322
+
323
+ try:
324
+ analyzer = BraxisAnalyzer(args.path)
325
+ analyzer.analyze()
326
+ except (ValueError, FileNotFoundError, NotADirectoryError) as e:
327
+ print(f"Error: {e}", file=sys.stderr)
328
+ sys.exit(1)
329
+
330
+ if args.command == 'score':
331
+ analyzer.print_score()
332
+ elif args.command == 'generate':
333
+ print("Generating context files...")
334
+ try:
335
+ analyzer._write_file_safely('AGENTS.md', analyzer.generate_agents_md())
336
+ print("* AGENTS.md")
337
+ analyzer._write_file_safely('CLAUDE.md', analyzer.generate_claude_md())
338
+ print("* CLAUDE.md")
339
+ analyzer._write_file_safely('.cursorrules', analyzer.generate_cursorrules())
340
+ print("* .cursorrules")
341
+ analyzer._write_file_safely('.agentic-config.json', analyzer.generate_agentic_config())
342
+ print("* .agentic-config.json")
343
+ print(f"\nAgent Readiness: {analyzer.total_score}/100 ({analyzer.tier})")
344
+ print("\nFiles created successfully!")
345
+ except IOError as e:
346
+ print(f"Error writing files: {e}", file=sys.stderr)
347
+ sys.exit(1)
348
+ elif args.command == 'inspect':
349
+ print(f"\nProject Analysis:")
350
+ print(f" Languages: {dict(analyzer.languages)}")
351
+ print(f" Build System: {analyzer.build_system}")
352
+ print(f" Test Frameworks: {list(analyzer.test_frameworks)}")
353
+ print(f" Files: {len(analyzer.files)}")
354
+ print(f" Test Files: {len(analyzer.test_files)}")
355
+ print(f" Critical Files: {len(analyzer.critical_files)}")
356
+ elif args.command == 'validate':
357
+ required_files = ['AGENTS.md', 'CLAUDE.md', '.cursorrules', '.agentic-config.json']
358
+ missing = [f for f in required_files if not Path(f).exists()]
359
+ if missing:
360
+ print(f"* Missing: {', '.join(missing)}")
361
+ print(f"Run: braxis generate")
362
+ sys.exit(1)
363
+ else:
364
+ print(f"* All context files present")
365
+ print(f"Agent Readiness: {analyzer.total_score}/100 ({analyzer.tier})")
366
+
367
+
368
+ if __name__ == '__main__':
369
+ main()
braxis-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
braxis-1.0.0/setup.py ADDED
@@ -0,0 +1,10 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="braxis",
5
+ version="1.0.0",
6
+ author="Jayakrishna Ichapurapu",
7
+ description="Auto-generate context files for AI agents",
8
+ license="MIT",
9
+ python_requires=">=3.8",
10
+ )
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unit tests for Braxis - AI agent context file generator.
4
+ Uses unittest for compatibility without external dependencies.
5
+ """
6
+ import os
7
+ import sys
8
+ import json
9
+ import unittest
10
+ import tempfile
11
+ from pathlib import Path
12
+ from unittest.mock import patch, MagicMock
13
+
14
+ from braxis import BraxisAnalyzer
15
+
16
+
17
+ class TestValidateProjectPath(unittest.TestCase):
18
+ """Tests for project path validation."""
19
+
20
+ def test_validate_valid_path(self):
21
+ """Test validation of valid project path."""
22
+ with tempfile.TemporaryDirectory() as tmpdir:
23
+ analyzer = BraxisAnalyzer(tmpdir)
24
+ self.assertEqual(analyzer.project_path, Path(tmpdir).resolve())
25
+
26
+ def test_validate_empty_path(self):
27
+ """Test validation rejects empty path."""
28
+ with self.assertRaises(ValueError):
29
+ BraxisAnalyzer("")
30
+
31
+ def test_validate_nonexistent_path(self):
32
+ """Test validation rejects nonexistent path."""
33
+ with self.assertRaises(FileNotFoundError):
34
+ BraxisAnalyzer("/nonexistent/path/12345")
35
+
36
+ def test_validate_file_not_directory(self):
37
+ """Test validation rejects file path."""
38
+ with tempfile.NamedTemporaryFile() as tmpfile:
39
+ with self.assertRaises(NotADirectoryError):
40
+ BraxisAnalyzer(tmpfile.name)
41
+
42
+ def test_validate_relative_path_conversion(self):
43
+ """Test that relative paths are converted to absolute."""
44
+ with tempfile.TemporaryDirectory() as tmpdir:
45
+ original_cwd = os.getcwd()
46
+ try:
47
+ os.chdir(tmpdir)
48
+ analyzer = BraxisAnalyzer(".")
49
+ self.assertTrue(analyzer.project_path.is_absolute())
50
+ finally:
51
+ os.chdir(original_cwd)
52
+
53
+
54
+ class TestWriteFileSafely(unittest.TestCase):
55
+ """Tests for safe file writing functionality."""
56
+
57
+ def test_write_file_successfully(self):
58
+ """Test successful file write."""
59
+ with tempfile.TemporaryDirectory() as tmpdir:
60
+ analyzer = BraxisAnalyzer(tmpdir)
61
+ filepath = Path(tmpdir) / "test.txt"
62
+ content = "Test content"
63
+ analyzer._write_file_safely(str(filepath), content)
64
+ self.assertTrue(filepath.exists())
65
+ self.assertEqual(filepath.read_text(), content)
66
+
67
+ def test_write_file_creates_parent_directories(self):
68
+ """Test that parent directories are created."""
69
+ with tempfile.TemporaryDirectory() as tmpdir:
70
+ analyzer = BraxisAnalyzer(tmpdir)
71
+ filepath = Path(tmpdir) / "subdir" / "nested" / "test.txt"
72
+ content = "Test content"
73
+ analyzer._write_file_safely(str(filepath), content)
74
+ self.assertTrue(filepath.exists())
75
+ self.assertEqual(filepath.read_text(), content)
76
+
77
+ def test_write_file_empty_filepath(self):
78
+ """Test that empty filepath raises error."""
79
+ with tempfile.TemporaryDirectory() as tmpdir:
80
+ analyzer = BraxisAnalyzer(tmpdir)
81
+ with self.assertRaises(ValueError):
82
+ analyzer._write_file_safely("", "content")
83
+
84
+ def test_write_file_non_string_content(self):
85
+ """Test that non-string content raises error."""
86
+ with tempfile.TemporaryDirectory() as tmpdir:
87
+ analyzer = BraxisAnalyzer(tmpdir)
88
+ filepath = Path(tmpdir) / "test.txt"
89
+ with self.assertRaises(TypeError):
90
+ analyzer._write_file_safely(str(filepath), 123)
91
+
92
+ def test_write_file_overwrites_existing(self):
93
+ """Test that file write overwrites existing file."""
94
+ with tempfile.TemporaryDirectory() as tmpdir:
95
+ analyzer = BraxisAnalyzer(tmpdir)
96
+ filepath = Path(tmpdir) / "test.txt"
97
+ filepath.write_text("Old content")
98
+ analyzer._write_file_safely(str(filepath), "New content")
99
+ self.assertEqual(filepath.read_text(), "New content")
100
+
101
+ def test_write_file_cleans_temp_file(self):
102
+ """Test that temp files are cleaned up."""
103
+ with tempfile.TemporaryDirectory() as tmpdir:
104
+ analyzer = BraxisAnalyzer(tmpdir)
105
+ filepath = Path(tmpdir) / "test.txt"
106
+ analyzer._write_file_safely(str(filepath), "content")
107
+ tmp_files = list(Path(tmpdir).glob("*.tmp"))
108
+ self.assertEqual(len(tmp_files), 0)
109
+
110
+
111
+ class TestBraxisAnalyzer(unittest.TestCase):
112
+ """Tests for BraxisAnalyzer functionality."""
113
+
114
+ def test_analyzer_initialization(self):
115
+ """Test analyzer initialization."""
116
+ with tempfile.TemporaryDirectory() as tmpdir:
117
+ analyzer = BraxisAnalyzer(tmpdir)
118
+ self.assertEqual(analyzer.files, [])
119
+ self.assertEqual(analyzer.test_files, [])
120
+ self.assertEqual(analyzer.config_files, [])
121
+ self.assertEqual(analyzer.build_files, [])
122
+ self.assertEqual(analyzer.tier, "Not Ready")
123
+
124
+ def test_scan_files(self):
125
+ """Test file scanning."""
126
+ with tempfile.TemporaryDirectory() as tmpdir:
127
+ Path(tmpdir, "test.py").write_text("# test")
128
+ Path(tmpdir, "main.py").write_text("# main")
129
+ Path(tmpdir, "config.json").write_text("{}")
130
+
131
+ analyzer = BraxisAnalyzer(tmpdir)
132
+ analyzer._scan_files()
133
+
134
+ self.assertGreaterEqual(len(analyzer.files), 3)
135
+ self.assertTrue(any("test.py" in str(f) for f in analyzer.files))
136
+
137
+ def test_detect_languages(self):
138
+ """Test language detection."""
139
+ with tempfile.TemporaryDirectory() as tmpdir:
140
+ Path(tmpdir, "test.py").write_text("# python")
141
+ Path(tmpdir, "script.js").write_text("// javascript")
142
+
143
+ analyzer = BraxisAnalyzer(tmpdir)
144
+ analyzer._scan_files()
145
+ analyzer._detect_languages()
146
+
147
+ self.assertIn("python", analyzer.languages)
148
+ self.assertIn("javascript", analyzer.languages)
149
+
150
+ def test_detect_test_files(self):
151
+ """Test test file detection."""
152
+ with tempfile.TemporaryDirectory() as tmpdir:
153
+ Path(tmpdir, "test_main.py").write_text("# test")
154
+ Path(tmpdir, "main_test.py").write_text("# test")
155
+ Path(tmpdir, "main.py").write_text("# main")
156
+
157
+ analyzer = BraxisAnalyzer(tmpdir)
158
+ analyzer._scan_files()
159
+
160
+ self.assertGreaterEqual(len(analyzer.test_files), 2)
161
+
162
+ def test_detect_build_system_python(self):
163
+ """Test Python build system detection."""
164
+ with tempfile.TemporaryDirectory() as tmpdir:
165
+ Path(tmpdir, "setup.py").write_text("# setup")
166
+
167
+ analyzer = BraxisAnalyzer(tmpdir)
168
+ analyzer._scan_files()
169
+ analyzer._detect_build_system()
170
+
171
+ self.assertIn("Python", analyzer.build_system)
172
+
173
+ def test_detect_build_system_npm(self):
174
+ """Test npm build system detection."""
175
+ with tempfile.TemporaryDirectory() as tmpdir:
176
+ Path(tmpdir, "package.json").write_text("{}")
177
+
178
+ analyzer = BraxisAnalyzer(tmpdir)
179
+ analyzer._scan_files()
180
+ analyzer._detect_build_system()
181
+
182
+ self.assertTrue("npm" in analyzer.build_system or "Node" in analyzer.build_system)
183
+
184
+ def test_detect_build_system_unknown(self):
185
+ """Test unknown build system detection."""
186
+ with tempfile.TemporaryDirectory() as tmpdir:
187
+ analyzer = BraxisAnalyzer(tmpdir)
188
+ analyzer._scan_files()
189
+ analyzer._detect_build_system()
190
+
191
+ self.assertEqual(analyzer.build_system, "Unknown")
192
+
193
+ def test_calculate_score(self):
194
+ """Test score calculation."""
195
+ with tempfile.TemporaryDirectory() as tmpdir:
196
+ Path(tmpdir, "main.py").write_text("# main")
197
+ Path(tmpdir, "test.py").write_text("# test")
198
+ Path(tmpdir, "README.md").write_text("# Readme")
199
+ Path(tmpdir, "setup.py").write_text("# setup")
200
+
201
+ analyzer = BraxisAnalyzer(tmpdir)
202
+ analyzer.analyze()
203
+
204
+ self.assertTrue(hasattr(analyzer, "total_score"))
205
+ self.assertGreaterEqual(analyzer.total_score, 0)
206
+ self.assertLessEqual(analyzer.total_score, 100)
207
+ self.assertIn(analyzer.tier, ["Not Ready", "Agent-Aware", "AI-Native", "AI-Native-Plus", "Agent-Optimized"])
208
+
209
+ def test_generate_agents_md(self):
210
+ """Test AGENTS.md generation."""
211
+ with tempfile.TemporaryDirectory() as tmpdir:
212
+ Path(tmpdir, "test.py").write_text("# test")
213
+
214
+ analyzer = BraxisAnalyzer(tmpdir)
215
+ analyzer.analyze()
216
+ content = analyzer.generate_agents_md()
217
+
218
+ self.assertIn("AGENTS.md", content)
219
+ self.assertIn("Tech Stack", content)
220
+
221
+ def test_generate_claude_md(self):
222
+ """Test CLAUDE.md generation."""
223
+ with tempfile.TemporaryDirectory() as tmpdir:
224
+ Path(tmpdir, "test.py").write_text("# test")
225
+
226
+ analyzer = BraxisAnalyzer(tmpdir)
227
+ analyzer.analyze()
228
+ content = analyzer.generate_claude_md()
229
+
230
+ self.assertIn("CLAUDE.md", content)
231
+ self.assertIn("Project", content)
232
+
233
+ def test_generate_cursorrules(self):
234
+ """Test .cursorrules generation."""
235
+ with tempfile.TemporaryDirectory() as tmpdir:
236
+ Path(tmpdir, "test.py").write_text("# test")
237
+
238
+ analyzer = BraxisAnalyzer(tmpdir)
239
+ analyzer.analyze()
240
+ content = analyzer.generate_cursorrules()
241
+
242
+ self.assertIn("Cursor Rules", content)
243
+
244
+ def test_generate_agentic_config(self):
245
+ """Test .agentic-config.json generation."""
246
+ with tempfile.TemporaryDirectory() as tmpdir:
247
+ Path(tmpdir, "test.py").write_text("# test")
248
+
249
+ analyzer = BraxisAnalyzer(tmpdir)
250
+ analyzer.analyze()
251
+ content = analyzer.generate_agentic_config()
252
+
253
+ config = json.loads(content)
254
+ self.assertIn("name", config)
255
+ self.assertIn("language", config)
256
+ self.assertIn("agent_readiness_score", config)
257
+ self.assertIn("tier", config)
258
+
259
+ def test_analyze_full_workflow(self):
260
+ """Test complete analysis workflow."""
261
+ with tempfile.TemporaryDirectory() as tmpdir:
262
+ Path(tmpdir, "main.py").write_text("# main")
263
+ Path(tmpdir, "test.py").write_text("import unittest")
264
+ Path(tmpdir, "README.md").write_text("# Project")
265
+ Path(tmpdir, "setup.py").write_text("# setup")
266
+ Path(tmpdir, "config.json").write_text("{}")
267
+
268
+ analyzer = BraxisAnalyzer(tmpdir)
269
+ analyzer.analyze()
270
+
271
+ self.assertGreater(len(analyzer.files), 0)
272
+ self.assertGreater(len(analyzer.languages), 0)
273
+ self.assertGreater(analyzer.total_score, 0)
274
+ self.assertNotEqual(analyzer.tier, "Not Ready")
275
+
276
+
277
+ class TestEdgeCases(unittest.TestCase):
278
+ """Tests for edge cases and error handling."""
279
+
280
+ def test_empty_directory(self):
281
+ """Test analysis of empty directory."""
282
+ with tempfile.TemporaryDirectory() as tmpdir:
283
+ analyzer = BraxisAnalyzer(tmpdir)
284
+ analyzer.analyze()
285
+ self.assertGreater(len(analyzer.files), -1)
286
+ self.assertGreater(analyzer.total_score, 0)
287
+
288
+ def test_special_characters_in_path(self):
289
+ """Test handling of special characters in file paths."""
290
+ with tempfile.TemporaryDirectory() as tmpdir:
291
+ special_dir = Path(tmpdir) / "test-dir_123"
292
+ special_dir.mkdir()
293
+
294
+ analyzer = BraxisAnalyzer(str(special_dir))
295
+ self.assertEqual(analyzer.project_path, special_dir.resolve())
296
+
297
+ def test_large_file_scanning(self):
298
+ """Test handling of large files."""
299
+ with tempfile.TemporaryDirectory() as tmpdir:
300
+ large_file = Path(tmpdir) / "large.py"
301
+ large_file.write_text("# " + "x" * 10000)
302
+
303
+ analyzer = BraxisAnalyzer(tmpdir)
304
+ analyzer.analyze()
305
+ self.assertIn(large_file, analyzer.files)
306
+
307
+ def test_ignored_directories(self):
308
+ """Test that common build directories are ignored."""
309
+ with tempfile.TemporaryDirectory() as tmpdir:
310
+ (Path(tmpdir) / ".git").mkdir()
311
+ (Path(tmpdir) / ".git" / "config").write_text("git")
312
+ (Path(tmpdir) / "node_modules").mkdir()
313
+ (Path(tmpdir) / "node_modules" / "pkg.js").write_text("js")
314
+
315
+ analyzer = BraxisAnalyzer(tmpdir)
316
+ analyzer._scan_files()
317
+
318
+ git_files = [f for f in analyzer.files if ".git" in str(f)]
319
+ node_files = [f for f in analyzer.files if "node_modules" in str(f)]
320
+
321
+ self.assertEqual(len(git_files), 0)
322
+ self.assertEqual(len(node_files), 0)
323
+
324
+ def test_file_write_with_unicode(self):
325
+ """Test writing files with unicode content."""
326
+ with tempfile.TemporaryDirectory() as tmpdir:
327
+ analyzer = BraxisAnalyzer(tmpdir)
328
+ filepath = Path(tmpdir) / "unicode.txt"
329
+ content = "Unicode test: 你好世界 🚀"
330
+ analyzer._write_file_safely(str(filepath), content)
331
+ self.assertEqual(filepath.read_text(encoding='utf-8'), content)
332
+
333
+ def test_score_breakdown_keys(self):
334
+ """Test that score breakdown has expected keys."""
335
+ with tempfile.TemporaryDirectory() as tmpdir:
336
+ Path(tmpdir, "test.py").write_text("# test")
337
+ analyzer = BraxisAnalyzer(tmpdir)
338
+ analyzer.analyze()
339
+
340
+ expected_keys = ['Architecture', 'Testing', 'Dependencies', 'Conventions',
341
+ 'Entry Points', 'Security', 'Build', 'Documentation']
342
+ for key in expected_keys:
343
+ self.assertIn(key, analyzer.score_breakdown)
344
+
345
+
346
+ if __name__ == '__main__':
347
+ unittest.main()