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,111 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "enforcement_mode": "blocking",
4
+ "layers": {
5
+ "R0": {
6
+ "name": "Config",
7
+ "directory": "config",
8
+ "required": false
9
+ },
10
+ "R1": {
11
+ "name": "Domain",
12
+ "directory": "domain",
13
+ "required": false
14
+ },
15
+ "R2": {
16
+ "name": "Application",
17
+ "directory": "app",
18
+ "required": false
19
+ },
20
+ "R3": {
21
+ "name": "Contract",
22
+ "directory": "contract",
23
+ "required": true
24
+ },
25
+ "R4": {
26
+ "name": "Execution",
27
+ "directory": "exec",
28
+ "required": true
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "R0": {
33
+ "allowed": [],
34
+ "forbidden": ["R1", "R2", "R3", "R4"]
35
+ },
36
+ "R1": {
37
+ "allowed": ["R0"],
38
+ "forbidden": ["R2", "R3", "R4"]
39
+ },
40
+ "R2": {
41
+ "allowed": ["R0", "R1", "R4"],
42
+ "forbidden": ["R3"]
43
+ },
44
+ "R3": {
45
+ "allowed": ["R0", "R1", "R2"],
46
+ "forbidden": ["R4"]
47
+ },
48
+ "R4": {
49
+ "allowed": ["R0"],
50
+ "forbidden": ["R1", "R2", "R3"]
51
+ }
52
+ },
53
+ "skip_matrix": {
54
+ "R0": {
55
+ "add_when": "configuration_concerns > 5",
56
+ "skip_if": "all config fits in .env"
57
+ },
58
+ "R1": {
59
+ "add_when": "business_rules > 10",
60
+ "skip_if": "simple CRUD app"
61
+ },
62
+ "R2": {
63
+ "add_when": "workflows > 3 or state_machines",
64
+ "skip_if": "direct handlers work"
65
+ },
66
+ "R3": {
67
+ "add_when": "always",
68
+ "skip_if": "never"
69
+ },
70
+ "R4": {
71
+ "add_when": "always",
72
+ "skip_if": "never"
73
+ }
74
+ },
75
+ "evolution_phases": [
76
+ {
77
+ "phase": "tiny",
78
+ "active": ["R3", "R4"]
79
+ },
80
+ {
81
+ "phase": "small",
82
+ "active": ["R1", "R2", "R3", "R4"]
83
+ },
84
+ {
85
+ "phase": "growing",
86
+ "active": ["R0", "R1", "R2", "R3", "R4"]
87
+ },
88
+ {
89
+ "phase": "enterprise",
90
+ "active": ["R0", "R1", "R2", "R3", "R4"]
91
+ }
92
+ ],
93
+ "commit_format": {
94
+ "pattern": "^\\[F-[a-z][a-z0-9-]*/R[0-4]/C[1-5]\\]",
95
+ "layer_codes": {
96
+ "R0": "Config",
97
+ "R1": "Domain",
98
+ "R2": "Application",
99
+ "R3": "Contract",
100
+ "R4": "Execution"
101
+ },
102
+ "classification_codes": {
103
+ "C1": "Internal",
104
+ "C2": "Feature",
105
+ "C3": "Contract",
106
+ "C4": "Structure",
107
+ "C5": "Migration"
108
+ },
109
+ "feature_tag": "required, F-kebab-case, groups commits into vertical slices"
110
+ }
111
+ }
@@ -0,0 +1,64 @@
1
+ #!/bin/bash
2
+ #
3
+ # la-new.sh - R3 Contract (local installer)
4
+ # Usage: ./la-new.sh <project-name>
5
+ #
6
+
7
+ LA_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
8
+
9
+ BORDER="════════════════════════════════════════════════════════════"
10
+
11
+ if [ -z "$1" ]; then
12
+ echo "Usage: ./la-new.sh <project-name>"
13
+ echo "Example: ./la-new.sh my-app"
14
+ exit 1
15
+ fi
16
+
17
+ PROJECT_NAME="$1"
18
+
19
+ if [ -d "$PROJECT_NAME" ]; then
20
+ echo "✗ Directory '$PROJECT_NAME' already exists"
21
+ exit 1
22
+ fi
23
+
24
+ echo ""
25
+ echo "$BORDER"
26
+ printf "%*s\n" $(( (${#BORDER} + 18) / 2 )) "LIVING ARCHITECTURE"
27
+ echo "$BORDER"
28
+ echo ""
29
+ echo " Creating project: $PROJECT_NAME"
30
+
31
+ # Copy LA structure
32
+ mkdir -p "$PROJECT_NAME"
33
+ cp -r "$LA_DIR/src" "$PROJECT_NAME/src"
34
+ cp -r "$LA_DIR/.living-arch" "$PROJECT_NAME/.living-arch"
35
+
36
+ # Init git
37
+ git init "$PROJECT_NAME" --quiet
38
+
39
+ # Hook up pre-commit and commit-msg
40
+ chmod +x "$PROJECT_NAME/src/contract/hooks/pre-commit"
41
+ chmod +x "$PROJECT_NAME/src/contract/hooks/commit-msg"
42
+
43
+ ln -sf "$(pwd)/$PROJECT_NAME/src/contract/hooks/pre-commit" \
44
+ "$PROJECT_NAME/.git/hooks/pre-commit"
45
+
46
+ ln -sf "$(pwd)/$PROJECT_NAME/src/contract/hooks/commit-msg" \
47
+ "$PROJECT_NAME/.git/hooks/commit-msg"
48
+
49
+ # Initial commit — skip validation, this is scaffolding not app code
50
+ cd "$PROJECT_NAME"
51
+ git add .
52
+ git commit --no-verify -m "LA v2.0 scaffold" --quiet
53
+ cd ..
54
+
55
+ echo ""
56
+ echo " ✓ Project created: $PROJECT_NAME/"
57
+ echo " ✓ Git initialized"
58
+ echo " ✓ Hooks installed"
59
+ echo " ✓ Initial commit done"
60
+ echo ""
61
+ echo " Ready: cd $PROJECT_NAME"
62
+ echo ""
63
+ echo "$BORDER"
64
+ echo ""
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ network-scan.py - R3 CLI Interface
4
+ Project health scanning with progressive zoom
5
+
6
+ Usage:
7
+ network-scan.py # All features
8
+ network-scan.py F-counter # Feature detail
9
+ network-scan.py F-counter R1 # Layer detail
10
+ """
11
+
12
+ import sys
13
+ import os
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
15
+
16
+ from domain import validate_f_tags, la_output
17
+ from app import validation_workflow
18
+ import re
19
+ import subprocess
20
+
21
+ # F-tags to exclude from feature scans (infrastructure, not features)
22
+ EXCLUDED_TAGS = {'F-la'}
23
+
24
+
25
+ def _git(args):
26
+ """Run a git command, return stripped stdout or empty string."""
27
+ try:
28
+ r = subprocess.run(['git'] + args, capture_output=True, text=True)
29
+ return r.stdout.strip()
30
+ except Exception:
31
+ return ''
32
+
33
+
34
+ def _get_commits_with_ftag_and_layer(f_tag, r_layer):
35
+ pattern = f'\\[{f_tag}/{r_layer}/'
36
+ out = _git(['log', '--all', '--pretty=format:%h %s', '--grep', pattern])
37
+ return [l for l in out.split('\n') if l] if out else []
38
+
39
+
40
+ def _get_commits_with_ftag(f_tag):
41
+ out = _git(['log', '--all', '--grep', f_tag, '--oneline'])
42
+ return [l for l in out.split('\n') if l] if out else []
43
+
44
+
45
+ def _get_files_for_ftag_and_layer(f_tag, r_layer):
46
+ pattern = f'\\[{f_tag}/{r_layer}/'
47
+ out = _git(['log', '--all', '--pretty=format:', '--name-only', '--grep', pattern])
48
+ if out:
49
+ return sorted({l.strip() for l in out.split('\n') if l.strip()})
50
+ return []
51
+
52
+
53
+ def get_all_ftags():
54
+ try:
55
+ result = subprocess.run(
56
+ ['git', 'log', '--all', '--pretty=format:%s'],
57
+ capture_output=True, text=True
58
+ )
59
+ f_tags = set()
60
+ for line in result.stdout.split('\n'):
61
+ match = re.search(r'\[F-([a-z][a-z0-9-]*)/R[0-4]/C[1-5]\]', line)
62
+ if match:
63
+ tag = f"F-{match.group(1)}"
64
+ if tag not in EXCLUDED_TAGS:
65
+ f_tags.add(tag)
66
+ return sorted(f_tags)
67
+ except:
68
+ return []
69
+
70
+
71
+ def scan_all_features():
72
+ print(la_output.header('NETWORK SCAN'))
73
+
74
+ f_tags = get_all_ftags()
75
+
76
+ if not f_tags:
77
+ print(" No features detected")
78
+ else:
79
+ for f_tag in f_tags:
80
+ result = validate_f_tags.validate_ftag_completeness(f_tag)
81
+ if result['complete']:
82
+ print(f" {f_tag:20}{la_output.ok('complete')}")
83
+ else:
84
+ missing = ', '.join(result['layers_missing'])
85
+ print(f" {f_tag:20}{la_output.warn(f'missing {missing}')}")
86
+
87
+ print()
88
+ print(f" {len(f_tags)} feature{'s' if len(f_tags) != 1 else ''} detected")
89
+ print()
90
+ print(la_output.divider())
91
+
92
+
93
+ def scan_feature(f_tag):
94
+ print(la_output.header(f'{f_tag.upper()} FEATURE'))
95
+
96
+ result = validate_f_tags.validate_ftag_completeness(f_tag)
97
+
98
+ # R-layers
99
+ if result['complete']:
100
+ print(f" {'R-layers':14}{la_output.ok('complete')}")
101
+ else:
102
+ missing = ', '.join(result['layers_missing'])
103
+ present = len(result['layers_present'])
104
+ total = len(result['layers_required'])
105
+ print(f" {'R-layers':14}{la_output.warn(f'{present}/{total} — missing {missing}')}")
106
+
107
+ # Execution + QC — get real files for this feature, run validators via R2
108
+ feature_files = []
109
+ for layer in result['layers_present']:
110
+ feature_files.extend(_get_files_for_ftag_and_layer(f_tag, layer))
111
+ feature_files = sorted(set(feature_files))
112
+
113
+ val = validation_workflow.validate_files(feature_files)
114
+ exec_val = val['execution']
115
+ qc_val = val['qc']
116
+
117
+ print(f" {'Execution':14}{la_output.ok('') if exec_val['valid'] else la_output.err('')}")
118
+ if qc_val['valid']:
119
+ print(f" {'QC':14}{la_output.ok('')}")
120
+ else:
121
+ violations = qc_val.get('violations', [])
122
+ print(f" {'QC':14}{la_output.err(violations[0] if violations else 'violation')}")
123
+
124
+ # Activity
125
+ total_commits = _get_commits_with_ftag(f_tag)
126
+ n = len(total_commits)
127
+ print(f" {'Activity':14}{n} commit{'s' if n != 1 else ''}")
128
+
129
+ print()
130
+
131
+ # Layer drill-down hints
132
+ for layer in sorted(result['layers_present']):
133
+ commits = _get_commits_with_ftag_and_layer(f_tag, layer)
134
+ print(f" {layer:14}{len(commits)} commit{'s' if len(commits) != 1 else ''}"
135
+ f" → network-scan.py {f_tag} {layer}")
136
+
137
+ print()
138
+ print(la_output.divider())
139
+ def get_layer_detail(f_tag, r_layer):
140
+ """Get files and commits for a specific F-tag + R-layer."""
141
+ commits = _get_commits_with_ftag_and_layer(f_tag, r_layer)
142
+ files = _get_files_for_ftag_and_layer(f_tag, r_layer)
143
+ return {'f_tag': f_tag, 'r_layer': r_layer, 'commits': commits, 'files': files}
144
+
145
+
146
+ def scan_layer(f_tag, r_layer):
147
+ print(la_output.header(f'{f_tag.upper()} / {r_layer}'))
148
+
149
+ detail = get_layer_detail(f_tag, r_layer)
150
+
151
+ # Files
152
+ if detail['files']:
153
+ print(f" {'Files':14}")
154
+ for f in detail['files']:
155
+ print(f" {f}")
156
+ else:
157
+ print(f" {'Files':14}none found")
158
+
159
+ print()
160
+
161
+ # Commits
162
+ if detail['commits']:
163
+ print(f" {'Commits':14}")
164
+ for c in detail['commits']:
165
+ print(f" {c}")
166
+ else:
167
+ print(f" {'Commits':14}none found")
168
+
169
+ print()
170
+ print(la_output.divider())
171
+
172
+
173
+ def main():
174
+ if len(sys.argv) == 1:
175
+ scan_all_features()
176
+ elif len(sys.argv) == 2:
177
+ f_tag = sys.argv[1]
178
+ if not f_tag.startswith('F-'):
179
+ f_tag = f'F-{f_tag}'
180
+ scan_feature(f_tag)
181
+ elif len(sys.argv) == 3:
182
+ f_tag = sys.argv[1]
183
+ r_layer = sys.argv[2].upper()
184
+ if not f_tag.startswith('F-'):
185
+ f_tag = f'F-{f_tag}'
186
+ scan_layer(f_tag, r_layer)
187
+ else:
188
+ print("Usage:")
189
+ print(" network-scan.py # All features")
190
+ print(" network-scan.py F-counter # Feature detail")
191
+ print(" network-scan.py F-counter R1 # Layer detail")
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ #
3
+ # commit-msg - Living Architecture validation hook
4
+ # Receives commit message file as $1, validates format and F-tags
5
+ #
6
+
7
+ cd "$(git rev-parse --show-toplevel)" || exit 1
8
+
9
+ if [ ! -f "src/app/validation-workflow.py" ]; then
10
+ exit 0
11
+ fi
12
+
13
+ # Pass message file path so workflow can read it
14
+ export LA_COMMIT_MSG_FILE="$1"
15
+
16
+ python3 src/app/validation-workflow.py
17
+ exit $?
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # pre-commit - Living Architecture
3
+ # Validation runs in commit-msg hook where message is available.
4
+ exit 0
@@ -0,0 +1,20 @@
1
+ """R1 Domain package — auto-discovers .py modules
2
+ Bootstrap exception: imports module_loader from R4 (exec) to load hyphenated filenames.
3
+ This is a package-init-only exception; no other R1 file may import from R4.
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+ from exec import module_loader
8
+
9
+ _dir = Path(__file__).parent
10
+
11
+ if str(_dir) not in sys.path:
12
+ sys.path.insert(0, str(_dir))
13
+
14
+ # Auto-discover all .py files (except __init__)
15
+ for _f in sorted(_dir.glob('*.py')):
16
+ if _f.stem == '__init__':
17
+ continue
18
+ _name = _f.stem.replace('-', '_')
19
+ _mod = module_loader.load_module(f'domain.{_name}', _f)
20
+ globals()[_name] = _mod
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ config_loader.py - R1 Helper
4
+ Loads configuration from R0 config files
5
+ Validators use this to stay config-driven
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+
12
+ def load_r_layers():
13
+ """Load R-layer definitions from config"""
14
+ config_path = Path(__file__).parent.parent / 'config' / 'r-layers.json'
15
+ with open(config_path) as f:
16
+ config = json.load(f)
17
+ return config['layers']
18
+
19
+
20
+ def get_valid_r_layer_ids():
21
+ """Get list of valid R-layer IDs"""
22
+ layers = load_r_layers()
23
+ return list(layers.keys())
24
+
25
+
26
+ def load_f_tag_rules():
27
+ """Load F-tag format and completeness rules"""
28
+ config_path = Path(__file__).parent.parent / 'config' / 'f-tags.json'
29
+ with open(config_path) as f:
30
+ return json.load(f)
31
+
32
+
33
+ def load_c_codes():
34
+ """Load C-code definitions"""
35
+ config_path = Path(__file__).parent.parent / 'config' / 'changes.json'
36
+ with open(config_path) as f:
37
+ return json.load(f)
38
+
39
+
40
+ def get_valid_c_codes():
41
+ """Get list of valid C-codes"""
42
+ config = load_c_codes()
43
+ return list(config['codes'].keys())
44
+
45
+
46
+ def load_o_rules():
47
+ """Load O-rule definitions"""
48
+ config_path = Path(__file__).parent.parent / 'config' / 'operations.json'
49
+ with open(config_path) as f:
50
+ return json.load(f)
51
+
52
+
53
+ def load_module_config():
54
+ """Load module configuration"""
55
+ config_path = Path(__file__).parent.parent / 'config' / 'modules.json'
56
+ with open(config_path) as f:
57
+ return json.load(f)
58
+
59
+
60
+ def get_module_from_filepath(filepath):
61
+ """
62
+ Extract module name from filepath using naming convention.
63
+ Convention: {module}-{component}.ext
64
+
65
+ Examples:
66
+ auth-login.py → "M-auth"
67
+ payment-process.py → "M-payment"
68
+ user.py → None (no module prefix)
69
+
70
+ Returns:
71
+ str: Module name (e.g., "M-auth") or None if no convention used
72
+ """
73
+ # Check if module detection is enabled
74
+ config = load_module_config()
75
+ if not config.get('enabled', False):
76
+ return None
77
+
78
+ filename = Path(filepath).stem # "auth-login.py" → "auth-login"
79
+
80
+ if '-' in filename:
81
+ module = filename.split('-')[0]
82
+ return f"M-{module}"
83
+
84
+ return None
@@ -0,0 +1,30 @@
1
+ /**
2
+ * installer-logic.js - R1 Domain
3
+ * Pure scaffold logic. No I/O. Determines what to create and where.
4
+ */
5
+
6
+ function validateProjectName(name) {
7
+ if (!name || typeof name !== 'string') {
8
+ return { valid: false, error: 'Project name required' };
9
+ }
10
+ if (!/^[a-z0-9-_]+$/i.test(name)) {
11
+ return { valid: false, error: 'Project name must be alphanumeric (hyphens/underscores allowed)' };
12
+ }
13
+ return { valid: true };
14
+ }
15
+
16
+ function buildScaffoldPlan(projectName, laSourceDir) {
17
+ return {
18
+ projectName,
19
+ targetDir: projectName,
20
+ laSourceDir,
21
+ hooks: {
22
+ preCommit: {
23
+ src: `${laSourceDir}/src/contract/hooks/pre-commit`,
24
+ dest: `${projectName}/.git/hooks/pre-commit`
25
+ }
26
+ }
27
+ };
28
+ }
29
+
30
+ module.exports = { validateProjectName, buildScaffoldPlan };
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ la-output.py - R1 Domain
4
+ Standard output formatting for all LA interfaces.
5
+ Single source of truth for borders, colors, width.
6
+ """
7
+
8
+ WIDTH = 60
9
+ BORDER = '═' * WIDTH
10
+
11
+ # ANSI colors
12
+ PURPLE = '\033[94m'
13
+ BOLD = '\033[1m'
14
+ GREEN = '\033[92m'
15
+ YELLOW = '\033[93m'
16
+ RED = '\033[91m'
17
+ RESET = '\033[0m'
18
+
19
+
20
+ def header(text):
21
+ """Centered header with double-line borders."""
22
+ return (
23
+ f"\n{PURPLE}{BOLD}{BORDER}{RESET}\n"
24
+ f"{PURPLE}{BOLD}{text:^{WIDTH}}{RESET}\n"
25
+ f"{PURPLE}{BOLD}{BORDER}{RESET}\n"
26
+ )
27
+
28
+
29
+ def divider():
30
+ return f"{PURPLE}{BORDER}{RESET}"
31
+
32
+
33
+ def ok(text):
34
+ return f"{GREEN}✓{RESET} {text}"
35
+
36
+
37
+ def warn(text):
38
+ return f"{YELLOW}⚠{RESET} {text}"
39
+
40
+
41
+ def err(text):
42
+ return f"{RED}✗{RESET} {text}"
43
+
44
+
45
+ def bold(text):
46
+ return f"{BOLD}{text}{RESET}"
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate-changes.py - R1 C-code Validator
4
+ Validates C-code format in commit messages
5
+ Config-driven: reads valid codes from src/config/changes.json
6
+ """
7
+
8
+ import re
9
+ import sys
10
+ from config_loader import get_valid_c_codes
11
+
12
+
13
+ def validate_c_code_format(commit_message):
14
+ """
15
+ Validate that commit message has valid C-code.
16
+ Only checks format (C1-C5), not semantic correctness.
17
+ """
18
+ # Extract C-code from message
19
+ match = re.search(r'/C([1-5])\]', commit_message)
20
+
21
+ if not match:
22
+ return {
23
+ 'valid': False,
24
+ 'error': 'No C-code found in format [F-*/R*/C#]'
25
+ }
26
+
27
+ c_code = f"C{match.group(1)}"
28
+
29
+ # Check against config
30
+ valid_codes = get_valid_c_codes()
31
+
32
+ if c_code not in valid_codes:
33
+ return {
34
+ 'valid': False,
35
+ 'error': f'Invalid C-code: {c_code}. Must be one of: {", ".join(valid_codes)}'
36
+ }
37
+
38
+ return {
39
+ 'valid': True,
40
+ 'c_code': c_code
41
+ }
42
+
43
+
44
+ def validate(commit_message):
45
+ """Main validation function (config-driven)"""
46
+ return validate_c_code_format(commit_message)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ if len(sys.argv) > 1:
51
+ message = sys.argv[1]
52
+ result = validate(message)
53
+
54
+ if result['valid']:
55
+ print(f"✓ C-code valid: {result['c_code']}")
56
+ else:
57
+ print(f"✗ {result['error']}")
58
+ sys.exit(1)
59
+ else:
60
+ print("Usage: validate-changes.py '<commit message>'")