create-living-architecture 1.0.8 → 2.0.0
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.
- package/.living-arch/SYSTEM.md +26 -0
- package/.living-arch/architecture.json +6 -0
- package/README.md +535 -0
- package/installer.js +42 -0
- package/package.json +10 -25
- package/src/app/__init__.py +16 -0
- package/src/app/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/app/__pycache__/validation-workflow.cpython-314.pyc +0 -0
- package/src/app/installer-workflow.js +56 -0
- package/src/app/validation-workflow.py +184 -0
- package/src/config/SYSTEM.md +58 -0
- package/src/config/changes.json +11 -0
- package/src/config/execution.json +9 -0
- package/src/config/f-tags.json +12 -0
- package/src/config/interfaces.json +16 -0
- package/src/config/modules.json +19 -0
- package/src/config/operations.json +272 -0
- package/src/config/r-layers.json +111 -0
- package/src/contract/cli/la-new.sh +64 -0
- package/src/contract/cli/network-scan.py +195 -0
- package/src/contract/hooks/commit-msg +17 -0
- package/src/contract/hooks/pre-commit +4 -0
- package/src/domain/__init__.py +20 -0
- package/src/domain/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/config_loader.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/la-output.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/validate-changes.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/validate-execution.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/validate-f-tags.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/validate-operations.cpython-314.pyc +0 -0
- package/src/domain/__pycache__/validate-r-layers.cpython-314.pyc +0 -0
- package/src/domain/config_loader.py +84 -0
- package/src/domain/installer-logic.js +30 -0
- package/src/domain/la-output.py +46 -0
- package/src/domain/validate-changes.py +60 -0
- package/src/domain/validate-execution.py +91 -0
- package/src/domain/validate-f-tags.py +93 -0
- package/src/domain/validate-operations.py +100 -0
- package/src/domain/validate-r-layers.py +202 -0
- package/src/exec/__init__.py +21 -0
- package/src/exec/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/exec/__pycache__/file-io.cpython-314.pyc +0 -0
- package/src/exec/__pycache__/git-io.cpython-314.pyc +0 -0
- package/src/exec/__pycache__/module-loader.cpython-314.pyc +0 -0
- package/src/exec/file-io.py +99 -0
- package/src/exec/git-io.py +59 -0
- package/src/exec/installer-io.js +36 -0
- package/src/exec/module-loader.py +29 -0
- package/index.js +0 -79
- package/templates/.living-arch/SYSTEM.md +0 -53
- package/templates/ARCHITECTURE.md +0 -56
- package/templates/README.md +0 -28
- package/templates/src/api/README.md +0 -13
- package/templates/src/database/README.md +0 -13
- package/templates/src/domain/README.md +0 -12
- package/templates/src/integrations/README.md +0 -13
- package/templates/starter-template/ARCHITECTURE.md +0 -56
- package/templates/starter-template/README.md +0 -28
- package/templates/starter-template/src/api/README.md +0 -13
- package/templates/starter-template/src/database/README.md +0 -13
- package/templates/starter-template/src/domain/README.md +0 -12
- package/templates/starter-template/src/integrations/README.md +0 -13
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate-execution.py - R1 Execution Pattern Validator
|
|
4
|
+
Validates I/O ownership, state machines, flow paths.
|
|
5
|
+
Pure functions — content passed in by R2, no file I/O here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_io_ownership(filepath, content):
|
|
14
|
+
"""Check for I/O operations and ownership."""
|
|
15
|
+
io_patterns = {
|
|
16
|
+
'stdout': [r'print\(', r'console\.log', r'echo\s'],
|
|
17
|
+
'stderr': [r'console\.error', r'sys\.stderr'],
|
|
18
|
+
'file': [r'open\(', r'fs\.', r'File\('],
|
|
19
|
+
'network': [r'fetch\(', r'axios', r'requests\.'],
|
|
20
|
+
}
|
|
21
|
+
operations = []
|
|
22
|
+
for io_type, patterns in io_patterns.items():
|
|
23
|
+
for pattern in patterns:
|
|
24
|
+
if re.findall(pattern, content):
|
|
25
|
+
operations.append({'type': io_type})
|
|
26
|
+
return {
|
|
27
|
+
'has_io': len(operations) > 0,
|
|
28
|
+
'operations': operations,
|
|
29
|
+
'valid': len(operations) <= 2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_state_machines(filepath, content):
|
|
34
|
+
"""Check for state machine completeness."""
|
|
35
|
+
has_state = bool(re.search(r'state\s*=|setState|this\.state', content))
|
|
36
|
+
has_transitions = bool(re.search(r'switch|if.*state|case\s', content))
|
|
37
|
+
if has_state:
|
|
38
|
+
return {
|
|
39
|
+
'has_state_machine': True,
|
|
40
|
+
'has_transitions': has_transitions,
|
|
41
|
+
'valid': has_transitions
|
|
42
|
+
}
|
|
43
|
+
return {'has_state_machine': False, 'valid': True}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_flow_paths(filepath, content):
|
|
47
|
+
"""Check for deep relative imports (circular risk)."""
|
|
48
|
+
imports = []
|
|
49
|
+
for line in content.split('\n'):
|
|
50
|
+
match = re.search(r'(?:from|import)\s+([^\s]+)', line)
|
|
51
|
+
if match:
|
|
52
|
+
imports.append(match.group(1))
|
|
53
|
+
bad_imports = [imp for imp in imports if imp.count('..') > 2]
|
|
54
|
+
return {
|
|
55
|
+
'imports': imports,
|
|
56
|
+
'circular_risk': len(bad_imports) > 0,
|
|
57
|
+
'valid': len(bad_imports) == 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate(filepath, content):
|
|
62
|
+
"""
|
|
63
|
+
Main validation function.
|
|
64
|
+
R2 reads the file via file-io and passes content here.
|
|
65
|
+
"""
|
|
66
|
+
io_result = check_io_ownership(filepath, content)
|
|
67
|
+
state_result = check_state_machines(filepath, content)
|
|
68
|
+
flow_result = check_flow_paths(filepath, content)
|
|
69
|
+
all_valid = io_result['valid'] and state_result['valid'] and flow_result['valid']
|
|
70
|
+
return {
|
|
71
|
+
'valid': all_valid,
|
|
72
|
+
'filepath': filepath,
|
|
73
|
+
'io_ownership': io_result,
|
|
74
|
+
'state_machines': state_result,
|
|
75
|
+
'flow_paths': flow_result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == '__main__':
|
|
80
|
+
if len(sys.argv) > 1:
|
|
81
|
+
filepath = sys.argv[1]
|
|
82
|
+
try:
|
|
83
|
+
with open(filepath) as f:
|
|
84
|
+
content = f.read()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f'Cannot read file: {e}')
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
result = validate(filepath, content)
|
|
89
|
+
print(json.dumps(result, indent=2))
|
|
90
|
+
else:
|
|
91
|
+
print('Usage: validate-execution.py <filepath>')
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate-f-tags.py - R1 F-tag Completeness Validator
|
|
4
|
+
Validates that F-tags are complete across R-layers
|
|
5
|
+
Config-driven: reads rules from src/config/f-tags.json
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from config_loader import load_f_tag_rules, load_r_layers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_ftag_from_message(message):
|
|
17
|
+
"""Extract F-tag and R-layer from commit message [F-name/R#/C#]."""
|
|
18
|
+
match = re.search(r'\[F-([a-z][a-z0-9-]*)/R([0-4])/C[1-5]\]', message)
|
|
19
|
+
if match:
|
|
20
|
+
return f"F-{match.group(1)}", f"R{match.group(2)}"
|
|
21
|
+
return None, None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_layers_from_history(f_tag):
|
|
25
|
+
"""Get R-layers already committed for this F-tag from git history."""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
['git', 'log', '--all', '--pretty=format:%s'],
|
|
29
|
+
capture_output=True, text=True
|
|
30
|
+
)
|
|
31
|
+
layers = set()
|
|
32
|
+
for line in result.stdout.split('\n'):
|
|
33
|
+
match = re.search(rf'\[{re.escape(f_tag)}/R([0-4])/C[1-5]\]', line)
|
|
34
|
+
if match:
|
|
35
|
+
layers.add(f"R{match.group(1)}")
|
|
36
|
+
return layers
|
|
37
|
+
except:
|
|
38
|
+
return set()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_ftag_completeness(f_tag):
|
|
42
|
+
"""Validate that F-tag has required R-layers (config-driven)."""
|
|
43
|
+
config = load_f_tag_rules()
|
|
44
|
+
required = set(config['completeness']['required_layers'])
|
|
45
|
+
|
|
46
|
+
layers_present = get_layers_from_history(f_tag)
|
|
47
|
+
missing = required - layers_present
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
'complete': len(missing) == 0,
|
|
51
|
+
'f_tag': f_tag,
|
|
52
|
+
'layers_present': sorted(layers_present),
|
|
53
|
+
'layers_required': sorted(required),
|
|
54
|
+
'layers_missing': sorted(missing),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate(commit_message):
|
|
59
|
+
"""Main validation function (config-driven)."""
|
|
60
|
+
config = load_f_tag_rules()
|
|
61
|
+
f_tag, current_layer = extract_ftag_from_message(commit_message)
|
|
62
|
+
|
|
63
|
+
if not f_tag:
|
|
64
|
+
return {'valid': False, 'error': 'No F-tag found in commit message'}
|
|
65
|
+
|
|
66
|
+
if not re.match(config['format']['pattern'], f_tag):
|
|
67
|
+
return {'valid': False, 'error': f'Invalid F-tag format: {f_tag}'}
|
|
68
|
+
|
|
69
|
+
# Include current commit's layer in completeness check
|
|
70
|
+
result = validate_ftag_completeness(f_tag)
|
|
71
|
+
layers_present = set(result['layers_present'])
|
|
72
|
+
if current_layer:
|
|
73
|
+
layers_present.add(current_layer)
|
|
74
|
+
|
|
75
|
+
required = set(config['completeness']['required_layers'])
|
|
76
|
+
missing = sorted(required - layers_present)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'valid': True,
|
|
80
|
+
'complete': len(missing) == 0,
|
|
81
|
+
'f_tag': f_tag,
|
|
82
|
+
'layers_present': sorted(layers_present),
|
|
83
|
+
'layers_missing': missing
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
if len(sys.argv) > 1:
|
|
89
|
+
result = validate(sys.argv[1])
|
|
90
|
+
print(json.dumps(result, indent=2))
|
|
91
|
+
else:
|
|
92
|
+
print("Usage: validate-f-tags.py '<commit message>'")
|
|
93
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate-operations.py - R1 O-rule (QC) Validator
|
|
4
|
+
Checks O1, O3, O6, O7 safety rules via pattern matching.
|
|
5
|
+
Pure functions — content passed in by R2, no file I/O here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
from config_loader import load_o_rules
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_o1_access_control(filepath, content):
|
|
15
|
+
"""O1: API endpoints must have authentication."""
|
|
16
|
+
violations = []
|
|
17
|
+
if '@app.route' in content or '@api.route' in content:
|
|
18
|
+
if '@auth.require' not in content and '@login_required' not in content:
|
|
19
|
+
violations.append('O1: API endpoint missing authentication decorator')
|
|
20
|
+
return violations
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_o3_reliability(filepath, content):
|
|
24
|
+
"""O3: Network calls must have error handling."""
|
|
25
|
+
violations = []
|
|
26
|
+
network_patterns = [r'requests\.(get|post|put|delete)', r'fetch\(', r'axios\.']
|
|
27
|
+
for pattern in network_patterns:
|
|
28
|
+
if re.search(pattern, content):
|
|
29
|
+
if 'try:' not in content and 'except' not in content:
|
|
30
|
+
violations.append('O3: Network call without error handling')
|
|
31
|
+
break
|
|
32
|
+
return violations
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_o6_configuration(filepath, content):
|
|
36
|
+
"""O6: No hardcoded secrets."""
|
|
37
|
+
violations = []
|
|
38
|
+
secret_patterns = [
|
|
39
|
+
r'API_KEY\s*=\s*["\']sk-',
|
|
40
|
+
r'PASSWORD\s*=\s*["\'].+["\']',
|
|
41
|
+
r'SECRET\s*=\s*["\'].+["\']',
|
|
42
|
+
]
|
|
43
|
+
for pattern in secret_patterns:
|
|
44
|
+
if re.search(pattern, content):
|
|
45
|
+
violations.append('O6: Hardcoded secret detected (use environment variables)')
|
|
46
|
+
break
|
|
47
|
+
return violations
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_o7_data_safety(filepath, content):
|
|
51
|
+
"""O7: No SQL injection risk."""
|
|
52
|
+
violations = []
|
|
53
|
+
sql_patterns = [
|
|
54
|
+
r'execute\(f["\'].*{.*}.*["\']',
|
|
55
|
+
r'execute\(["\'].*%s.*["\'].*%',
|
|
56
|
+
r'execute\(.*\+.*\)',
|
|
57
|
+
]
|
|
58
|
+
for pattern in sql_patterns:
|
|
59
|
+
if re.search(pattern, content):
|
|
60
|
+
violations.append('O7: Possible SQL injection (use parameterized queries)')
|
|
61
|
+
break
|
|
62
|
+
return violations
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate(filepath, content):
|
|
66
|
+
"""
|
|
67
|
+
Main validation function.
|
|
68
|
+
R2 reads the file via file-io and passes content here.
|
|
69
|
+
"""
|
|
70
|
+
violations = []
|
|
71
|
+
violations.extend(check_o1_access_control(filepath, content))
|
|
72
|
+
violations.extend(check_o3_reliability(filepath, content))
|
|
73
|
+
violations.extend(check_o6_configuration(filepath, content))
|
|
74
|
+
violations.extend(check_o7_data_safety(filepath, content))
|
|
75
|
+
return {
|
|
76
|
+
'valid': len(violations) == 0,
|
|
77
|
+
'filepath': filepath,
|
|
78
|
+
'violations': violations
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
if len(sys.argv) > 1:
|
|
84
|
+
filepath = sys.argv[1]
|
|
85
|
+
try:
|
|
86
|
+
with open(filepath) as f:
|
|
87
|
+
content = f.read()
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f'Cannot read file: {e}')
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
result = validate(filepath, content)
|
|
92
|
+
if result['valid']:
|
|
93
|
+
print('✓ No O-rule violations')
|
|
94
|
+
else:
|
|
95
|
+
print('✗ O-rule violations:')
|
|
96
|
+
for v in result['violations']:
|
|
97
|
+
print(f' - {v}')
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
else:
|
|
100
|
+
print('Usage: validate-operations.py <filepath>')
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Living Architecture - Dependency Direction Validator
|
|
4
|
+
Validates that code doesn't violate R-layer dependency rules.
|
|
5
|
+
NOW INCLUDES: Circular dependency detection within R-layers
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from collections import defaultdict, deque
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_circular_dependencies(files_with_imports):
|
|
15
|
+
"""
|
|
16
|
+
Detect circular import dependencies.
|
|
17
|
+
Returns list of cycles if found.
|
|
18
|
+
"""
|
|
19
|
+
# Build dependency graph
|
|
20
|
+
graph = defaultdict(set)
|
|
21
|
+
for filepath, imports in files_with_imports.items():
|
|
22
|
+
for imp in imports:
|
|
23
|
+
graph[filepath].add(imp)
|
|
24
|
+
|
|
25
|
+
# Detect cycles using DFS
|
|
26
|
+
def has_cycle(node, visited, rec_stack, path):
|
|
27
|
+
visited.add(node)
|
|
28
|
+
rec_stack.add(node)
|
|
29
|
+
path.append(node)
|
|
30
|
+
|
|
31
|
+
for neighbor in graph.get(node, []):
|
|
32
|
+
if neighbor not in visited:
|
|
33
|
+
if has_cycle(neighbor, visited, rec_stack, path):
|
|
34
|
+
return True
|
|
35
|
+
elif neighbor in rec_stack:
|
|
36
|
+
# Found cycle
|
|
37
|
+
cycle_start = path.index(neighbor)
|
|
38
|
+
return path[cycle_start:] + [neighbor]
|
|
39
|
+
|
|
40
|
+
path.pop()
|
|
41
|
+
rec_stack.remove(node)
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
visited = set()
|
|
45
|
+
cycles = []
|
|
46
|
+
|
|
47
|
+
for node in graph:
|
|
48
|
+
if node not in visited:
|
|
49
|
+
rec_stack = set()
|
|
50
|
+
path = []
|
|
51
|
+
cycle = has_cycle(node, visited, rec_stack, path)
|
|
52
|
+
if cycle:
|
|
53
|
+
cycles.append(cycle)
|
|
54
|
+
|
|
55
|
+
return cycles
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_r_layer_rules():
|
|
59
|
+
"""
|
|
60
|
+
Load dependency rules from R0 config (r-layers.json).
|
|
61
|
+
Returns (forbidden_deps, r_group_patterns, r_group_names) keyed by directory name.
|
|
62
|
+
"""
|
|
63
|
+
import json
|
|
64
|
+
config_path = Path(__file__).parent.parent / 'config' / 'r-layers.json'
|
|
65
|
+
with open(config_path) as f:
|
|
66
|
+
config = json.load(f)
|
|
67
|
+
|
|
68
|
+
layers = config['layers'] # e.g. {"R0": {"name": "Config", "directory": "config"}}
|
|
69
|
+
deps = config['dependencies'] # e.g. {"R0": {"forbidden": ["R1", ...]}}
|
|
70
|
+
|
|
71
|
+
# Map directory name → list of forbidden directory names
|
|
72
|
+
forbidden_deps = {}
|
|
73
|
+
r_group_patterns = {}
|
|
74
|
+
r_group_names = {}
|
|
75
|
+
|
|
76
|
+
for r_id, layer in layers.items():
|
|
77
|
+
dirname = layer['directory']
|
|
78
|
+
forbidden_r_ids = deps.get(r_id, {}).get('forbidden', [])
|
|
79
|
+
forbidden_dirs = [layers[r]['directory'] for r in forbidden_r_ids if r in layers]
|
|
80
|
+
|
|
81
|
+
forbidden_deps[dirname] = forbidden_dirs
|
|
82
|
+
r_group_patterns[dirname] = [dirname]
|
|
83
|
+
r_group_names[dirname] = f"{r_id} {layer['name']}"
|
|
84
|
+
|
|
85
|
+
return forbidden_deps, r_group_patterns, r_group_names
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
FORBIDDEN_DEPS, R_GROUP_PATTERNS, R_GROUP_NAMES = _load_r_layer_rules()
|
|
89
|
+
|
|
90
|
+
def detect_r_group(filepath):
|
|
91
|
+
"""Determine R-group from file path."""
|
|
92
|
+
path_str = str(filepath).lower()
|
|
93
|
+
|
|
94
|
+
for r_group, patterns in R_GROUP_PATTERNS.items():
|
|
95
|
+
for pattern in patterns:
|
|
96
|
+
if f'/{pattern}/' in path_str or f'\\{pattern}\\' in path_str:
|
|
97
|
+
return r_group
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def extract_import_target(line):
|
|
102
|
+
"""Extract the imported module/package path from an import statement."""
|
|
103
|
+
patterns = [
|
|
104
|
+
r'from\s+["\']?([^"\']+)["\']?\s+import', # Python: from X import
|
|
105
|
+
r'import\s+["\']?([^"\']+)["\']?', # Python/JS: import X
|
|
106
|
+
r'require\(["\']([^"\']+)["\']\)', # JS: require('X')
|
|
107
|
+
r'use\s+([^;]+);', # Rust: use X;
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for pattern in patterns:
|
|
111
|
+
match = re.search(pattern, line)
|
|
112
|
+
if match:
|
|
113
|
+
return match.group(1)
|
|
114
|
+
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def detect_import_r_group(import_path):
|
|
118
|
+
"""Determine which R-group an import is targeting."""
|
|
119
|
+
import_lower = import_path.lower()
|
|
120
|
+
|
|
121
|
+
for r_group, patterns in R_GROUP_PATTERNS.items():
|
|
122
|
+
for pattern in patterns:
|
|
123
|
+
if pattern in import_lower:
|
|
124
|
+
return r_group
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def check_file(filepath, content):
|
|
129
|
+
"""
|
|
130
|
+
Check a single file for dependency violations.
|
|
131
|
+
R2 reads the file via file-io and passes content here.
|
|
132
|
+
"""
|
|
133
|
+
violations = []
|
|
134
|
+
|
|
135
|
+
source_r_group = detect_r_group(filepath)
|
|
136
|
+
if not source_r_group:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
for line_num, line in enumerate(content.split('\n'), 1):
|
|
140
|
+
if line.strip().startswith(('#', '//', '/*', '*')):
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if any(keyword in line for keyword in ['import', 'from', 'require', 'use']):
|
|
144
|
+
import_target = extract_import_target(line)
|
|
145
|
+
if not import_target:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
target_r_group = detect_import_r_group(import_target)
|
|
149
|
+
if not target_r_group:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
forbidden = FORBIDDEN_DEPS.get(source_r_group, [])
|
|
153
|
+
if target_r_group in forbidden:
|
|
154
|
+
violations.append({
|
|
155
|
+
'file': str(filepath),
|
|
156
|
+
'line': line_num,
|
|
157
|
+
'source_r_group': source_r_group,
|
|
158
|
+
'target_r_group': target_r_group,
|
|
159
|
+
'content': line.strip()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return violations
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
"""Scan all source files for dependency violations."""
|
|
166
|
+
print("Checking dependency direction (D2)")
|
|
167
|
+
print()
|
|
168
|
+
|
|
169
|
+
violations = []
|
|
170
|
+
file_extensions = ['*.py', '*.js', '*.ts', '*.jsx', '*.tsx', '*.rs', '*.go', '*.java']
|
|
171
|
+
|
|
172
|
+
for ext in file_extensions:
|
|
173
|
+
for filepath in Path('.').rglob(ext):
|
|
174
|
+
if any(skip in str(filepath) for skip in ['node_modules', 'venv', '.venv', 'build', 'dist', '.git', '__init__.py', 'test-project']):
|
|
175
|
+
continue
|
|
176
|
+
violations.extend(check_file(filepath, filepath.read_text(encoding='utf-8', errors='ignore')))
|
|
177
|
+
|
|
178
|
+
if violations:
|
|
179
|
+
print(" ✗ Dependency Direction Violations Found (D2)")
|
|
180
|
+
print()
|
|
181
|
+
for v in violations:
|
|
182
|
+
source_name = R_GROUP_NAMES.get(v['source_r_group'], v['source_r_group'])
|
|
183
|
+
target_name = R_GROUP_NAMES.get(v['target_r_group'], v['target_r_group'])
|
|
184
|
+
print(f" {v['file']}:{v['line']}")
|
|
185
|
+
print(f" {source_name} cannot depend on {target_name}")
|
|
186
|
+
print(f" > {v['content']}")
|
|
187
|
+
print()
|
|
188
|
+
|
|
189
|
+
print(" Forbidden dependencies:")
|
|
190
|
+
for source, targets in FORBIDDEN_DEPS.items():
|
|
191
|
+
source_name = R_GROUP_NAMES.get(source, source)
|
|
192
|
+
target_names = [R_GROUP_NAMES.get(t, t) for t in targets]
|
|
193
|
+
print(f" {source_name}: cannot import from {', '.join(target_names)}")
|
|
194
|
+
print()
|
|
195
|
+
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
else:
|
|
198
|
+
print(" ✓ No dependency violations found")
|
|
199
|
+
sys.exit(0)
|
|
200
|
+
|
|
201
|
+
if __name__ == '__main__':
|
|
202
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""R4 Execution package — auto-discovers .py modules, explicit .js"""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util as _ilu
|
|
5
|
+
|
|
6
|
+
_dir = Path(__file__).parent
|
|
7
|
+
|
|
8
|
+
# Bootstrap module-loader first
|
|
9
|
+
_spec = _ilu.spec_from_file_location('exec.module_loader', _dir / 'module-loader.py')
|
|
10
|
+
_ml = _ilu.module_from_spec(_spec)
|
|
11
|
+
sys.modules['exec.module_loader'] = _ml
|
|
12
|
+
_spec.loader.exec_module(_ml)
|
|
13
|
+
module_loader = _ml
|
|
14
|
+
|
|
15
|
+
# Auto-discover all .py files (except __init__ and module-loader)
|
|
16
|
+
for _f in sorted(_dir.glob('*.py')):
|
|
17
|
+
if _f.stem in ('__init__', 'module-loader'):
|
|
18
|
+
continue
|
|
19
|
+
_name = _f.stem.replace('-', '_')
|
|
20
|
+
_mod = _ml.load_module(f'exec.{_name}', _f)
|
|
21
|
+
globals()[_name] = _mod
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
file-io.py - R4 File I/O Operations
|
|
4
|
+
Handles file reading and code analysis
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_file(filepath):
|
|
12
|
+
"""Read file content."""
|
|
13
|
+
try:
|
|
14
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
15
|
+
return f.read()
|
|
16
|
+
except Exception as e:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def analyze_imports(filepath, language='auto'):
|
|
21
|
+
"""Analyze import statements in a file."""
|
|
22
|
+
content = read_file(filepath)
|
|
23
|
+
if not content:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
# Auto-detect language
|
|
27
|
+
if language == 'auto':
|
|
28
|
+
ext = Path(filepath).suffix
|
|
29
|
+
if ext == '.py':
|
|
30
|
+
language = 'python'
|
|
31
|
+
elif ext in ['.js', '.jsx', '.ts', '.tsx']:
|
|
32
|
+
language = 'javascript'
|
|
33
|
+
else:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
imports = []
|
|
37
|
+
|
|
38
|
+
if language == 'python':
|
|
39
|
+
# Match: import X, from X import Y
|
|
40
|
+
for line in content.split('\n'):
|
|
41
|
+
match = re.match(r'^(?:from\s+(\S+)|import\s+(\S+))', line.strip())
|
|
42
|
+
if match:
|
|
43
|
+
imports.append(match.group(1) or match.group(2))
|
|
44
|
+
|
|
45
|
+
elif language == 'javascript':
|
|
46
|
+
# Match: import X from 'path', const X = require('path')
|
|
47
|
+
patterns = [
|
|
48
|
+
r"import\s+.+\s+from\s+['\"](.+)['\"]",
|
|
49
|
+
r"require\(['\"](.+)['\"]\)"
|
|
50
|
+
]
|
|
51
|
+
for line in content.split('\n'):
|
|
52
|
+
for pattern in patterns:
|
|
53
|
+
matches = re.findall(pattern, line)
|
|
54
|
+
imports.extend(matches)
|
|
55
|
+
|
|
56
|
+
return imports
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def detect_io_operations(filepath):
|
|
60
|
+
"""Detect I/O operations in code."""
|
|
61
|
+
content = read_file(filepath)
|
|
62
|
+
if not content:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
io_patterns = [
|
|
66
|
+
r'open\(',
|
|
67
|
+
r'read\(',
|
|
68
|
+
r'write\(',
|
|
69
|
+
r'fetch\(',
|
|
70
|
+
r'axios\.',
|
|
71
|
+
r'fs\.',
|
|
72
|
+
r'console\.log',
|
|
73
|
+
r'print\(',
|
|
74
|
+
r'document\.',
|
|
75
|
+
r'window\.',
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
operations = []
|
|
79
|
+
for i, line in enumerate(content.split('\n'), 1):
|
|
80
|
+
for pattern in io_patterns:
|
|
81
|
+
if re.search(pattern, line):
|
|
82
|
+
operations.append({
|
|
83
|
+
'line': i,
|
|
84
|
+
'type': pattern.strip('\\()'),
|
|
85
|
+
'code': line.strip()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return operations
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
import sys
|
|
93
|
+
if len(sys.argv) > 1:
|
|
94
|
+
filepath = sys.argv[1]
|
|
95
|
+
print(f"Analyzing: {filepath}")
|
|
96
|
+
print(f"Imports: {analyze_imports(filepath)}")
|
|
97
|
+
print(f"I/O ops: {len(detect_io_operations(filepath))}")
|
|
98
|
+
else:
|
|
99
|
+
print("Usage: file-io.py <filepath>")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
git-io.py - R4 Git I/O Operations
|
|
4
|
+
Handles git operations needed by R2 validation-workflow.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_git_command(args):
|
|
13
|
+
"""Run a git command and return output."""
|
|
14
|
+
try:
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
['git'] + args,
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
check=True
|
|
20
|
+
)
|
|
21
|
+
return result.stdout.strip()
|
|
22
|
+
except subprocess.CalledProcessError as e:
|
|
23
|
+
print(f'Git error: {e.stderr}', file=sys.stderr)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_staged_files():
|
|
28
|
+
"""Get list of staged files."""
|
|
29
|
+
output = run_git_command(['diff', '--staged', '--name-only'])
|
|
30
|
+
if output:
|
|
31
|
+
return [f for f in output.split('\n') if f]
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_commit_message():
|
|
36
|
+
"""
|
|
37
|
+
Get the commit message.
|
|
38
|
+
commit-msg hook passes file path via LA_COMMIT_MSG_FILE env var.
|
|
39
|
+
Falls back to COMMIT_EDITMSG for other contexts.
|
|
40
|
+
"""
|
|
41
|
+
msg_file = os.environ.get('LA_COMMIT_MSG_FILE')
|
|
42
|
+
if msg_file and os.path.exists(msg_file):
|
|
43
|
+
try:
|
|
44
|
+
with open(msg_file, 'r') as f:
|
|
45
|
+
return f.read().strip()
|
|
46
|
+
except IOError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open('.git/COMMIT_EDITMSG', 'r') as f:
|
|
51
|
+
return f.read().strip()
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == '__main__':
|
|
57
|
+
print('Git I/O Operations')
|
|
58
|
+
print(f'Staged files: {get_staged_files()}')
|
|
59
|
+
print(f'Commit message: {get_commit_message()}')
|