-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgranger_compliance_checker.py
More file actions
487 lines (410 loc) · 18 KB
/
granger_compliance_checker.py
File metadata and controls
487 lines (410 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
#!/usr/bin/env python3
"""
Module: granger_compliance_checker.py
Description: Comprehensive compliance assessment for Granger projects against module standards
External Dependencies:
- None (uses standard library only)
Sample Input:
>>> projects = get_projects_with_github()
>>> compliance_report = check_all_projects(projects)
Expected Output:
>>> print(compliance_report['summary'])
{'total_projects': 14, 'fully_compliant': 5, 'minor_issues': 6, 'major_issues': 3}
Example Usage:
>>> python granger_compliance_checker.py
"""
import os
import json
import subprocess
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import re
from datetime import datetime
# Projects with GitHub repositories from GRANGER_PROJECTS.md
GITHUB_PROJECTS = {
"granger_hub": {
"path": "/home/graham/workspace/experiments/granger_hub/",
"github": "git+https://github.com/grahama1970/granger_hub.git"
},
"rl_commons": {
"path": "/home/graham/workspace/experiments/rl_commons/",
"github": "git+https://github.com/grahama1970/rl-commons.git"
},
"claude-test-reporter": {
"path": "/home/graham/workspace/experiments/claude-test-reporter/",
"github": "git+https://github.com/grahama1970/claude-test-reporter.git"
},
"sparta": {
"path": "/home/graham/workspace/experiments/sparta/",
"github": "git+https://github.com/grahama1970/SPARTA.git"
},
"marker": {
"path": "/home/graham/workspace/experiments/marker/",
"github": "git+https://github.com/grahama1970/marker.git"
},
"arangodb": {
"path": "/home/graham/workspace/experiments/arangodb/",
"github": "git+https://github.com/grahama1970/arangodb.git"
},
"youtube_transcripts": {
"path": "/home/graham/workspace/experiments/youtube_transcripts/",
"github": "git+https://github.com/grahama1970/youtube-transcripts-search.git"
},
"llm_call": {
"path": "/home/graham/workspace/experiments/llm_call/",
"github": "git+https://github.com/grahama1970/llm_call.git"
},
"unsloth_wip": {
"path": "/home/graham/workspace/experiments/fine_tuning/",
"github": "git+https://github.com/grahama1970/fine_tuning.git"
},
"arxiv-mcp-server": {
"path": "/home/graham/workspace/mcp-servers/arxiv-mcp-server/",
"github": "git+https://github.com/blazickjp/arxiv-mcp-server.git"
},
"mcp-screenshot": {
"path": "/home/graham/workspace/experiments/mcp-screenshot/",
"github": "git+https://github.com/grahama1970/mcp-screenshot.git"
},
"annotator": {
"path": "/home/graham/workspace/experiments/annotator/",
"github": "git+https://github.com/grahama1970/marker-ground-truth.git"
},
"aider-daemon": {
"path": "/home/graham/workspace/experiments/aider-daemon/",
"github": "git+https://github.com/grahama1970/aider-daemon.git"
}
}
# Add memvid which was mentioned but not in the main list
GITHUB_PROJECTS["memvid"] = {
"path": "/home/graham/workspace/experiments/memvid/",
"github": "git+https://github.com/grahama1970/memvid.git"
}
class ComplianceChecker:
def __init__(self):
self.results = {}
def check_project(self, name: str, info: dict) -> dict:
"""Check a single project for compliance"""
project_path = Path(info['path'])
if not project_path.exists():
return {
"exists": False,
"error": f"Project path does not exist: {project_path}"
}
result = {
"name": name,
"path": str(project_path),
"github": info['github'],
"exists": True,
"checks": {},
"issues": [],
"severity": "compliant" # compliant, minor, major
}
# 1. Check pyproject.toml
pyproject_check = self.check_pyproject_toml(project_path)
result["checks"]["pyproject"] = pyproject_check
# 2. Check .env.example
env_check = self.check_env_example(project_path)
result["checks"]["env_example"] = env_check
# 3. Check project structure
structure_check = self.check_project_structure(project_path)
result["checks"]["structure"] = structure_check
# 4. Check for UV usage
uv_check = self.check_uv_usage(project_path)
result["checks"]["uv_usage"] = uv_check
# 5. Check for NO MOCKS policy
mocks_check = self.check_no_mocks(project_path)
result["checks"]["no_mocks"] = mocks_check
# 6. Check MCP integration
mcp_check = self.check_mcp_integration(project_path)
result["checks"]["mcp_integration"] = mcp_check
# Determine overall severity
result["severity"] = self.determine_severity(result["checks"])
return result
def check_pyproject_toml(self, project_path: Path) -> dict:
"""Check pyproject.toml compliance"""
pyproject_path = project_path / "pyproject.toml"
if not pyproject_path.exists():
return {
"exists": False,
"issues": ["pyproject.toml not found"],
"severity": "major"
}
try:
content = pyproject_path.read_text()
issues = []
# Check Python version
python_match = re.search(r'requires-python\s*=\s*"([^"]+)"', content)
if python_match:
python_version = python_match.group(1)
if not python_version.startswith(">=3.10"):
issues.append(f"Python version {python_version} does not meet >=3.10.11 requirement")
else:
issues.append("requires-python not found")
# Check build system
if "[build-system]" in content:
if "setuptools" not in content:
if "hatchling" in content or "poetry" in content:
issues.append("Using hatchling/poetry instead of setuptools")
else:
issues.append("[build-system] section not found")
# Check critical dependencies
if "dependencies" in content:
# Check numpy version
if "numpy" in content:
numpy_match = re.search(r'numpy==([0-9.]+)', content)
if numpy_match:
if numpy_match.group(1) != "1.26.4":
issues.append(f"numpy version {numpy_match.group(1)} != 1.26.4")
else:
issues.append("numpy version not locked to 1.26.4")
# Check pandas constraint
if "pandas" in content and "pandas>=2.2.3,<2.3.0" not in content:
issues.append("pandas not constrained to >=2.2.3,<2.3.0")
# Check pyarrow constraint
if "pyarrow" in content:
if not re.search(r'pyarrow[>=<,0-9.]+,<20', content):
issues.append("pyarrow not constrained to <20")
# Check pillow constraint
if "pillow" in content and "pillow>=10.1.0,<11.0.0" not in content:
issues.append("pillow not constrained to >=10.1.0,<11.0.0")
# Check GitHub dependency format
git_deps = re.findall(r'(git\+https://[^"]+|https://github[^"]+)', content)
for dep in git_deps:
if not dep.startswith("git+https://"):
issues.append(f"GitHub dependency missing git+ prefix: {dep}")
return {
"exists": True,
"issues": issues,
"severity": "major" if issues else "compliant"
}
except Exception as e:
return {
"exists": True,
"issues": [f"Error reading pyproject.toml: {str(e)}"],
"severity": "major"
}
def check_env_example(self, project_path: Path) -> dict:
"""Check .env.example compliance"""
env_path = project_path / ".env.example"
if not env_path.exists():
return {
"exists": False,
"issues": [".env.example not found"],
"severity": "minor"
}
try:
content = env_path.read_text()
lines = content.strip().split('\n')
issues = []
# Check first line
if not lines or not lines[0].strip().startswith("PYTHONPATH="):
issues.append(".env.example does not start with PYTHONPATH=./src")
elif lines[0].strip() != "PYTHONPATH=./src":
issues.append(f"First line is '{lines[0].strip()}' not 'PYTHONPATH=./src'")
return {
"exists": True,
"issues": issues,
"severity": "minor" if issues else "compliant"
}
except Exception as e:
return {
"exists": True,
"issues": [f"Error reading .env.example: {str(e)}"],
"severity": "minor"
}
def check_project_structure(self, project_path: Path) -> dict:
"""Check project structure compliance"""
issues = []
# Required directories
required_dirs = ["src", "tests", "docs", "examples"]
for dir_name in required_dirs:
if not (project_path / dir_name).exists():
issues.append(f"Missing required directory: {dir_name}/")
# Check for src/project_name structure
src_path = project_path / "src"
if src_path.exists():
subdirs = [d for d in src_path.iterdir() if d.is_dir()]
if not subdirs:
issues.append("src/ directory has no subdirectories")
return {
"exists": True,
"issues": issues,
"severity": "minor" if issues else "compliant"
}
def check_uv_usage(self, project_path: Path) -> dict:
"""Check for UV usage documentation"""
issues = []
# Check for uv.lock
if not (project_path / "uv.lock").exists():
issues.append("uv.lock file not found")
# Check README for UV mentions
readme_path = project_path / "README.md"
if readme_path.exists():
content = readme_path.read_text().lower()
if "uv " not in content and "uv add" not in content:
issues.append("README.md does not mention UV usage")
return {
"exists": True,
"issues": issues,
"severity": "minor" if issues else "compliant"
}
def check_no_mocks(self, project_path: Path) -> dict:
"""Check for NO MOCKS policy compliance"""
issues = []
mock_patterns = [
"from unittest.mock import",
"from mock import",
"@patch(",
"Mock(",
"MagicMock(",
"@mock.",
]
# Check test files
test_dirs = [project_path / "tests", project_path / "test"]
for test_dir in test_dirs:
if test_dir.exists():
for py_file in test_dir.rglob("*.py"):
try:
content = py_file.read_text()
for pattern in mock_patterns:
if pattern in content:
issues.append(f"Mock usage found in {py_file.relative_to(project_path)}")
break
except:
pass
return {
"exists": True,
"issues": issues[:5], # Limit to first 5 to avoid spam
"total_mock_issues": len(issues),
"severity": "major" if issues else "compliant"
}
def check_mcp_integration(self, project_path: Path) -> dict:
"""Check for MCP integration"""
issues = []
# Check for mcp.json
if not (project_path / "mcp.json").exists():
issues.append("mcp.json not found")
# Check for MCP directory structure
mcp_dir = project_path / "src" / "*" / "mcp"
mcp_dirs = list(project_path.glob("src/*/mcp"))
if not mcp_dirs:
issues.append("No mcp/ directory found in src/")
return {
"exists": True,
"issues": issues,
"severity": "minor" if issues else "compliant"
}
def determine_severity(self, checks: dict) -> str:
"""Determine overall severity based on individual checks"""
severities = []
for check in checks.values():
if isinstance(check, dict) and "severity" in check:
severities.append(check["severity"])
if "major" in severities:
return "major"
elif "minor" in severities:
return "minor"
else:
return "compliant"
def generate_report(self, results: List[dict]) -> dict:
"""Generate comprehensive compliance report"""
report = {
"timestamp": datetime.now().isoformat(),
"total_projects": len(results),
"summary": {
"fully_compliant": 0,
"minor_issues": 0,
"major_issues": 0,
"not_found": 0
},
"projects": {},
"action_plan": []
}
# Process results
for result in results:
if not result.get("exists"):
report["summary"]["not_found"] += 1
elif result["severity"] == "compliant":
report["summary"]["fully_compliant"] += 1
elif result["severity"] == "minor":
report["summary"]["minor_issues"] += 1
elif result["severity"] == "major":
report["summary"]["major_issues"] += 1
report["projects"][result.get("name", "unknown")] = result
# Generate action plan
report["action_plan"] = self.generate_action_plan(results)
return report
def generate_action_plan(self, results: List[dict]) -> List[dict]:
"""Generate prioritized action plan"""
actions = []
# Priority 1: Major issues
for result in results:
if result.get("severity") == "major" and result.get("exists"):
project_name = result["name"]
for check_name, check_result in result["checks"].items():
if check_result.get("severity") == "major":
actions.append({
"priority": 1,
"project": project_name,
"category": check_name,
"issues": check_result.get("issues", []),
"action": self.get_action_for_issue(check_name, check_result)
})
# Priority 2: Minor issues
for result in results:
if result.get("severity") == "minor" and result.get("exists"):
project_name = result["name"]
for check_name, check_result in result["checks"].items():
if check_result.get("severity") == "minor":
actions.append({
"priority": 2,
"project": project_name,
"category": check_name,
"issues": check_result.get("issues", []),
"action": self.get_action_for_issue(check_name, check_result)
})
return sorted(actions, key=lambda x: (x["priority"], x["project"]))
def get_action_for_issue(self, category: str, check_result: dict) -> str:
"""Get recommended action for specific issue category"""
actions = {
"pyproject": "Update pyproject.toml to use setuptools and correct dependency versions",
"env_example": "Create/update .env.example with PYTHONPATH=./src as first line",
"structure": "Create missing directories (src/, tests/, docs/, examples/)",
"uv_usage": "Run 'uv init' and update README.md with UV instructions",
"no_mocks": "Replace mock usage with real service connections in tests",
"mcp_integration": "Add MCP integration following the standards guide"
}
return actions.get(category, "Review and fix according to standards")
def main():
"""Run compliance check on all projects"""
checker = ComplianceChecker()
results = []
print("🔍 Granger Ecosystem Compliance Check")
print("=" * 80)
for name, info in GITHUB_PROJECTS.items():
print(f"\nChecking {name}...")
result = checker.check_project(name, info)
results.append(result)
if result.get("exists"):
print(f" ✓ Found at {info['path']}")
print(f" Severity: {result['severity']}")
else:
print(f" ✗ Not found at {info['path']}")
# Generate report
report = checker.generate_report(results)
# Save detailed report
report_path = Path("/home/graham/workspace/shared_claude_docs/granger_compliance_report.json")
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
print("\n" + "=" * 80)
print("📊 Compliance Summary")
print(f"Total Projects: {report['total_projects']}")
print(f"✅ Fully Compliant: {report['summary']['fully_compliant']}")
print(f"⚠️ Minor Issues: {report['summary']['minor_issues']}")
print(f"❌ Major Issues: {report['summary']['major_issues']}")
print(f"🔍 Not Found: {report['summary']['not_found']}")
print(f"\n📄 Detailed report saved to: {report_path}")
return report
if __name__ == "__main__":
report = main()