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.
Files changed (62) hide show
  1. package/.living-arch/SYSTEM.md +26 -0
  2. package/.living-arch/architecture.json +6 -0
  3. package/README.md +535 -0
  4. package/installer.js +42 -0
  5. package/package.json +10 -25
  6. package/src/app/__init__.py +16 -0
  7. package/src/app/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/src/app/__pycache__/validation-workflow.cpython-314.pyc +0 -0
  9. package/src/app/installer-workflow.js +56 -0
  10. package/src/app/validation-workflow.py +184 -0
  11. package/src/config/SYSTEM.md +58 -0
  12. package/src/config/changes.json +11 -0
  13. package/src/config/execution.json +9 -0
  14. package/src/config/f-tags.json +12 -0
  15. package/src/config/interfaces.json +16 -0
  16. package/src/config/modules.json +19 -0
  17. package/src/config/operations.json +272 -0
  18. package/src/config/r-layers.json +111 -0
  19. package/src/contract/cli/la-new.sh +64 -0
  20. package/src/contract/cli/network-scan.py +195 -0
  21. package/src/contract/hooks/commit-msg +17 -0
  22. package/src/contract/hooks/pre-commit +4 -0
  23. package/src/domain/__init__.py +20 -0
  24. package/src/domain/__pycache__/__init__.cpython-314.pyc +0 -0
  25. package/src/domain/__pycache__/config_loader.cpython-314.pyc +0 -0
  26. package/src/domain/__pycache__/la-output.cpython-314.pyc +0 -0
  27. package/src/domain/__pycache__/validate-changes.cpython-314.pyc +0 -0
  28. package/src/domain/__pycache__/validate-execution.cpython-314.pyc +0 -0
  29. package/src/domain/__pycache__/validate-f-tags.cpython-314.pyc +0 -0
  30. package/src/domain/__pycache__/validate-operations.cpython-314.pyc +0 -0
  31. package/src/domain/__pycache__/validate-r-layers.cpython-314.pyc +0 -0
  32. package/src/domain/config_loader.py +84 -0
  33. package/src/domain/installer-logic.js +30 -0
  34. package/src/domain/la-output.py +46 -0
  35. package/src/domain/validate-changes.py +60 -0
  36. package/src/domain/validate-execution.py +91 -0
  37. package/src/domain/validate-f-tags.py +93 -0
  38. package/src/domain/validate-operations.py +100 -0
  39. package/src/domain/validate-r-layers.py +202 -0
  40. package/src/exec/__init__.py +21 -0
  41. package/src/exec/__pycache__/__init__.cpython-314.pyc +0 -0
  42. package/src/exec/__pycache__/file-io.cpython-314.pyc +0 -0
  43. package/src/exec/__pycache__/git-io.cpython-314.pyc +0 -0
  44. package/src/exec/__pycache__/module-loader.cpython-314.pyc +0 -0
  45. package/src/exec/file-io.py +99 -0
  46. package/src/exec/git-io.py +59 -0
  47. package/src/exec/installer-io.js +36 -0
  48. package/src/exec/module-loader.py +29 -0
  49. package/index.js +0 -79
  50. package/templates/.living-arch/SYSTEM.md +0 -53
  51. package/templates/ARCHITECTURE.md +0 -56
  52. package/templates/README.md +0 -28
  53. package/templates/src/api/README.md +0 -13
  54. package/templates/src/database/README.md +0 -13
  55. package/templates/src/domain/README.md +0 -12
  56. package/templates/src/integrations/README.md +0 -13
  57. package/templates/starter-template/ARCHITECTURE.md +0 -56
  58. package/templates/starter-template/README.md +0 -28
  59. package/templates/starter-template/src/api/README.md +0 -13
  60. package/templates/starter-template/src/database/README.md +0 -13
  61. package/templates/starter-template/src/domain/README.md +0 -12
  62. 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
@@ -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()}')