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 +3 -0
- braxis-1.0.0/PKG-INFO +13 -0
- braxis-1.0.0/README.md +185 -0
- braxis-1.0.0/braxis.egg-info/PKG-INFO +13 -0
- braxis-1.0.0/braxis.egg-info/SOURCES.txt +9 -0
- braxis-1.0.0/braxis.egg-info/dependency_links.txt +1 -0
- braxis-1.0.0/braxis.egg-info/top_level.txt +1 -0
- braxis-1.0.0/braxis.py +369 -0
- braxis-1.0.0/setup.cfg +4 -0
- braxis-1.0.0/setup.py +10 -0
- braxis-1.0.0/tests/test_braxis.py +347 -0
braxis-1.0.0/LICENSE
ADDED
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 @@
|
|
|
1
|
+
|
|
@@ -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
braxis-1.0.0/setup.py
ADDED
|
@@ -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()
|