aiox-core 5.0.7 → 5.0.8
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/.aiox-core/cli/commands/pro/buyer.js +379 -0
- package/.aiox-core/cli/commands/pro/index.js +191 -52
- package/.aiox-core/cli/commands/validate/index.js +2 -0
- package/.aiox-core/core/code-intel/helpers/dev-helper.js +1 -1
- package/.aiox-core/core/code-intel/helpers/devops-helper.js +0 -1
- package/.aiox-core/core/code-intel/helpers/planning-helper.js +1 -1
- package/.aiox-core/core/code-intel/helpers/qa-helper.js +2 -2
- package/.aiox-core/core/config/schemas/framework-config.schema.json +1 -0
- package/.aiox-core/core/config/template-overrides.js +1 -1
- package/.aiox-core/core/doctor/checks/ide-sync.js +81 -25
- package/.aiox-core/core/doctor/checks/rules-files.js +0 -1
- package/.aiox-core/core/doctor/checks/skills-count.js +83 -15
- package/.aiox-core/core/graph-dashboard/cli.js +1 -2
- package/.aiox-core/core/graph-dashboard/data-sources/code-intel-source.js +1 -1
- package/.aiox-core/core/ids/layer-classifier.js +1 -1
- package/.aiox-core/core/pro/pro-updater.js +578 -0
- package/.aiox-core/core/synapse/context/context-tracker.js +107 -9
- package/.aiox-core/core/synapse/layers/layer-processor.js +1 -1
- package/.aiox-core/core-config.yaml +15 -1
- package/.aiox-core/data/capability-detection.js +15 -15
- package/.aiox-core/data/entity-registry.yaml +18 -2
- package/.aiox-core/data/registry-update-log.jsonl +5 -0
- package/.aiox-core/data/tok3-token-comparison.js +0 -4
- package/.aiox-core/data/tool-search-validation.js +1 -1
- package/.aiox-core/development/agents/aiox-master.md +44 -6
- package/.aiox-core/development/agents/data-engineer.md +4 -4
- package/.aiox-core/development/agents/devops.md +52 -2
- package/.aiox-core/development/agents/po.md +1 -1
- package/.aiox-core/development/agents/qa.md +5 -11
- package/.aiox-core/development/agents/sm.md +3 -3
- package/.aiox-core/development/agents/ux-design-expert.md +1 -1
- package/.aiox-core/development/scripts/unified-activation-pipeline.js +29 -3
- package/.aiox-core/development/tasks/dev-develop-story.md +46 -7
- package/.aiox-core/development/tasks/devops-pro-access-grant.md +93 -0
- package/.aiox-core/development/tasks/devops-pro-activate.md +42 -0
- package/.aiox-core/development/tasks/devops-pro-check-access.md +34 -0
- package/.aiox-core/development/tasks/devops-pro-request-reset.md +34 -0
- package/.aiox-core/development/tasks/devops-pro-resend-verification.md +32 -0
- package/.aiox-core/development/tasks/devops-pro-reset-password.md +36 -0
- package/.aiox-core/development/tasks/devops-pro-validate-login.md +36 -0
- package/.aiox-core/development/tasks/devops-pro-verify-status.md +33 -0
- package/.aiox-core/development/tasks/qa-gate.md +54 -4
- package/.aiox-core/development/tasks/validate-next-story.md +39 -2
- package/.aiox-core/framework-config.yaml +1 -0
- package/.aiox-core/infrastructure/scripts/codex-skills-sync/README.md +69 -0
- package/.aiox-core/infrastructure/scripts/codex-skills-sync/bootstrap.js +727 -0
- package/.aiox-core/infrastructure/scripts/codex-skills-sync/index.js +10 -0
- package/.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js +65 -4
- package/.aiox-core/infrastructure/scripts/generate-settings-json.js +29 -4
- package/.aiox-core/infrastructure/scripts/ide-sync/agent-parser.js +4 -0
- package/.aiox-core/infrastructure/scripts/ide-sync/index.js +67 -7
- package/.aiox-core/infrastructure/scripts/ide-sync/transformers/claude-code.js +145 -3
- package/.aiox-core/infrastructure/scripts/repair-agent-references.js +263 -0
- package/.aiox-core/infrastructure/scripts/validate-claude-integration.js +60 -8
- package/.aiox-core/infrastructure/scripts/validate-paths.js +13 -0
- package/.aiox-core/install-manifest.yaml +134 -82
- package/.aiox-core/utils/filters/index.js +2 -1
- package/.claude/commands/AIOX/agents/aiox-master.md +21 -0
- package/.claude/commands/AIOX/agents/analyst.md +21 -0
- package/.claude/commands/AIOX/agents/architect.md +21 -0
- package/.claude/commands/AIOX/agents/data-engineer.md +21 -0
- package/.claude/commands/AIOX/agents/dev.md +21 -0
- package/.claude/commands/AIOX/agents/devops.md +21 -0
- package/.claude/commands/AIOX/agents/pm.md +21 -0
- package/.claude/commands/AIOX/agents/po.md +21 -0
- package/.claude/commands/AIOX/agents/qa.md +21 -0
- package/.claude/commands/AIOX/agents/sm.md +21 -0
- package/.claude/commands/AIOX/agents/squad-creator.md +21 -0
- package/.claude/commands/AIOX/agents/ux-design-expert.md +21 -0
- package/.claude/commands/AIOX/scripts/agent-config-loader.js +624 -0
- package/.claude/commands/AIOX/scripts/generate-greeting.js +160 -0
- package/.claude/commands/AIOX/scripts/greeting-builder.js +866 -0
- package/.claude/commands/AIOX/scripts/session-context-loader.js +286 -0
- package/.claude/commands/AIOX/stories/story-6.1.4.md +1404 -0
- package/.claude/commands/cohort-squad/agents/cohort-manager.md +156 -0
- package/.claude/commands/design-system/agents/brad-frost.md +1097 -0
- package/.claude/commands/design-system/agents/dan-mall.md +857 -0
- package/.claude/commands/design-system/agents/dave-malouf.md +2272 -0
- package/.claude/commands/design-system/agents/design-chief.md +102 -0
- package/.claude/commands/design-system/agents/nano-banana-generator.md +162 -0
- package/.claude/commands/greet.md +101 -0
- package/.claude/commands/synapse/manager.md +75 -0
- package/.claude/commands/synapse/tasks/add-rule.md +94 -0
- package/.claude/commands/synapse/tasks/create-command.md +109 -0
- package/.claude/commands/synapse/tasks/create-domain.md +127 -0
- package/.claude/commands/synapse/tasks/diagnose-synapse.md +245 -0
- package/.claude/commands/synapse/tasks/edit-rule.md +109 -0
- package/.claude/commands/synapse/tasks/suggest-domain.md +116 -0
- package/.claude/commands/synapse/tasks/toggle-domain.md +83 -0
- package/.claude/commands/synapse/templates/domain-template +8 -0
- package/.claude/commands/synapse/templates/manifest-entry-template +4 -0
- package/.claude/commands/synapse/utils/manifest-parser-reference.md +134 -0
- package/.claude/hooks/precompact-session-digest.cjs +2 -2
- package/.claude/skills/AIOX/agents/aiox-master/SKILL.md +511 -0
- package/.claude/skills/AIOX/agents/analyst/SKILL.md +281 -0
- package/.claude/skills/AIOX/agents/architect/SKILL.md +482 -0
- package/.claude/skills/AIOX/agents/data-engineer/SKILL.md +503 -0
- package/.claude/skills/AIOX/agents/dev/SKILL.md +568 -0
- package/.claude/skills/AIOX/agents/devops/SKILL.md +597 -0
- package/.claude/skills/AIOX/agents/pm/SKILL.md +385 -0
- package/.claude/skills/AIOX/agents/po/SKILL.md +343 -0
- package/.claude/skills/AIOX/agents/qa/SKILL.md +451 -0
- package/.claude/skills/AIOX/agents/sm/SKILL.md +295 -0
- package/.claude/skills/AIOX/agents/squad-creator/SKILL.md +352 -0
- package/.claude/skills/AIOX/agents/ux-design-expert/SKILL.md +503 -0
- package/.claude/skills/architect-first/SKILL.md +275 -0
- package/.claude/skills/architect-first/assets/architecture-template.md +505 -0
- package/.claude/skills/architect-first/assets/config-template.yaml +351 -0
- package/.claude/skills/architect-first/references/architecture-checklist.md +216 -0
- package/.claude/skills/architect-first/references/pre-implementation-checklist.md +119 -0
- package/.claude/skills/architect-first/references/stop-rules-guide.md +291 -0
- package/.claude/skills/architect-first/references/testing-strategy-guide.md +477 -0
- package/.claude/skills/architect-first/scripts/architecture_validator.py +490 -0
- package/.claude/skills/architect-first/scripts/check_coupling.py +306 -0
- package/.claude/skills/architect-first/scripts/validate_risk_mitigation.py +382 -0
- package/.claude/skills/checklist-runner/SKILL.md +113 -0
- package/.claude/skills/clone-mind.md +329 -0
- package/.claude/skills/coderabbit-review/SKILL.md +106 -0
- package/.claude/skills/course-generation-workflow.md +76 -0
- package/.claude/skills/enhance-workflow.md +466 -0
- package/.claude/skills/mcp-builder/LICENSE.txt +202 -0
- package/.claude/skills/mcp-builder/SKILL.md +328 -0
- package/.claude/skills/mcp-builder/reference/evaluation.md +602 -0
- package/.claude/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/.claude/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/.claude/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/.claude/skills/mcp-builder/scripts/connections.py +151 -0
- package/.claude/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/.claude/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/.claude/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/.claude/skills/ralph.md +181 -0
- package/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/.claude/skills/skill-creator/SKILL.md +209 -0
- package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/.claude/skills/squad.md +301 -0
- package/.claude/skills/synapse/SKILL.md +132 -0
- package/.claude/skills/synapse/assets/README.md +50 -0
- package/.claude/skills/synapse/references/brackets.md +100 -0
- package/.claude/skills/synapse/references/commands.md +118 -0
- package/.claude/skills/synapse/references/domains.md +126 -0
- package/.claude/skills/synapse/references/layers.md +186 -0
- package/.claude/skills/synapse/references/manifest.md +142 -0
- package/.claude/skills/tech-search/SKILL.md +431 -0
- package/.claude/skills/tech-search/prompts/page-extract.md +133 -0
- package/README.en.md +2 -2
- package/README.md +8 -2
- package/bin/aiox.js +55 -4
- package/bin/utils/framework-guard.js +4 -2
- package/bin/utils/pro-detector.js +119 -28
- package/bin/utils/validate-publish.js +6 -6
- package/docs/aiox-agent-flows/devops-system.md +18 -0
- package/docs/aiox-workflows/README.md +1 -0
- package/docs/aiox-workflows/pro-access-grant-workflow.md +218 -0
- package/docs/guides/pro/access-grant-ops-playbook.md +370 -0
- package/docs/guides/pro/install-gate-setup.md +12 -6
- package/docs/guides/pro/squad-creator-handoff-pro-access-ops.md +134 -0
- package/docs/guides/supabase-ops-handoff.md +768 -0
- package/package.json +12 -1
- package/packages/aiox-pro-cli/bin/aiox-pro.js +33 -12
- package/packages/installer/src/config/configure-environment.js +118 -50
- package/packages/installer/src/installer/aiox-core-installer.js +124 -27
- package/packages/installer/src/installer/brownfield-upgrader.js +66 -9
- package/packages/installer/src/installer/dependency-installer.js +4 -0
- package/packages/installer/src/pro/pro-scaffolder.js +5 -5
- package/packages/installer/src/updater/index.js +151 -10
- package/packages/installer/src/wizard/ide-config-generator.js +73 -7
- package/packages/installer/src/wizard/index.js +119 -31
- package/packages/installer/src/wizard/pro-setup.js +118 -47
- package/packages/installer/src/wizard/validation/validators/dependency-validator.js +32 -25
- package/packages/installer/src/wizard/validation/validators/file-structure-validator.js +26 -0
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +84 -1
- package/packages/installer/tests/unit/claude-md-template-v5/claude-md-template-v5.test.js +1 -1
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +85 -19
- package/packages/installer/tests/unit/entity-registry-bootstrap.test.js +4 -4
- package/packages/installer/tests/unit/generate-settings-json/generate-settings-json.test.js +5 -5
- package/packages/installer/tests/unit/ide-sync-integration/ide-sync-integration.test.js +4 -4
- package/packages/installer/tests/unit/merger/yaml-merger.test.js +11 -11
- package/pro/README.md +12 -1
- package/pro/license/index.js +3 -11
- package/pro/license/license-api.js +25 -0
- package/pro/license/license-cache.js +135 -31
- package/pro/license/license-crypto.js +59 -3
- package/pro/package.json +5 -4
- package/pro/squads/README.md +16 -16
- package/pro/squads/index.js +1 -1
- package/scripts/e2e/installed-skills-smoke.js +264 -0
- package/scripts/package-synapse.js +3 -3
- package/scripts/validate-package-completeness.js +8 -11
- package/.aiox-core/lib/build.json +0 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Zero-Coupling Validation Script
|
|
4
|
+
|
|
5
|
+
Validates that modules/expansion-packs maintain zero-coupling principle:
|
|
6
|
+
- No hardcoded cross-module imports
|
|
7
|
+
- No hardcoded file paths to other modules
|
|
8
|
+
- Configuration-based integration only
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python check_coupling.py [--path PROJECT_PATH] [--config CONFIG_FILE]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Dict, Set, Tuple
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CouplingViolation:
|
|
24
|
+
"""Represents a coupling violation found in code"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
file_path: str,
|
|
29
|
+
line_number: int,
|
|
30
|
+
line_content: str,
|
|
31
|
+
violation_type: str,
|
|
32
|
+
description: str,
|
|
33
|
+
):
|
|
34
|
+
self.file_path = file_path
|
|
35
|
+
self.line_number = line_number
|
|
36
|
+
self.line_content = line_content.strip()
|
|
37
|
+
self.violation_type = violation_type
|
|
38
|
+
self.description = description
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return (
|
|
42
|
+
f"\n {self.violation_type}: {self.file_path}:{self.line_number}\n"
|
|
43
|
+
f" {self.description}\n"
|
|
44
|
+
f" > {self.line_content}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CouplingChecker:
|
|
49
|
+
"""Checks for coupling violations in codebase"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, project_path: Path, config_path: Path = None):
|
|
52
|
+
self.project_path = project_path
|
|
53
|
+
self.violations: List[CouplingViolation] = []
|
|
54
|
+
self.config = self._load_config(config_path)
|
|
55
|
+
|
|
56
|
+
# Modules to check for coupling (from config or defaults)
|
|
57
|
+
self.modules = self.config.get("modules", [])
|
|
58
|
+
if not self.modules:
|
|
59
|
+
# Auto-detect expansion packs
|
|
60
|
+
expansion_pack_dir = project_path / "expansion-packs"
|
|
61
|
+
if expansion_pack_dir.exists():
|
|
62
|
+
self.modules = [
|
|
63
|
+
d.name for d in expansion_pack_dir.iterdir() if d.is_dir()
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# File patterns to scan
|
|
67
|
+
self.file_patterns = self.config.get(
|
|
68
|
+
"file_patterns", ["*.py", "*.js", "*.ts", "*.yaml", "*.yml"]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Exclude patterns
|
|
72
|
+
self.exclude_patterns = self.config.get(
|
|
73
|
+
"exclude_patterns",
|
|
74
|
+
[
|
|
75
|
+
"**/node_modules/**",
|
|
76
|
+
"**/__pycache__/**",
|
|
77
|
+
"**/venv/**",
|
|
78
|
+
"**/.venv/**",
|
|
79
|
+
"**/dist/**",
|
|
80
|
+
"**/build/**",
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _load_config(self, config_path: Path = None) -> Dict:
|
|
85
|
+
"""Load configuration file if exists"""
|
|
86
|
+
if config_path and config_path.exists():
|
|
87
|
+
with open(config_path, "r") as f:
|
|
88
|
+
return yaml.safe_load(f) or {}
|
|
89
|
+
|
|
90
|
+
# Try default config location
|
|
91
|
+
default_config = self.project_path / ".coupling-check.yaml"
|
|
92
|
+
if default_config.exists():
|
|
93
|
+
with open(default_config, "r") as f:
|
|
94
|
+
return yaml.safe_load(f) or {}
|
|
95
|
+
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
def _should_exclude(self, file_path: Path) -> bool:
|
|
99
|
+
"""Check if file should be excluded from scanning"""
|
|
100
|
+
path_str = str(file_path)
|
|
101
|
+
for pattern in self.exclude_patterns:
|
|
102
|
+
if Path(path_str).match(pattern):
|
|
103
|
+
return True
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def _find_files_to_scan(self) -> List[Path]:
|
|
107
|
+
"""Find all files to scan for coupling violations"""
|
|
108
|
+
files = []
|
|
109
|
+
for pattern in self.file_patterns:
|
|
110
|
+
for file_path in self.project_path.rglob(pattern):
|
|
111
|
+
if file_path.is_file() and not self._should_exclude(file_path):
|
|
112
|
+
files.append(file_path)
|
|
113
|
+
return files
|
|
114
|
+
|
|
115
|
+
def _check_hardcoded_imports(self, file_path: Path, content: str):
|
|
116
|
+
"""Check for hardcoded imports to other modules"""
|
|
117
|
+
lines = content.split("\n")
|
|
118
|
+
|
|
119
|
+
for line_num, line in enumerate(lines, 1):
|
|
120
|
+
# Python imports
|
|
121
|
+
if re.match(r"^\s*(from|import)\s+", line):
|
|
122
|
+
for module in self.modules:
|
|
123
|
+
# Check if importing from another module directly
|
|
124
|
+
if re.search(rf"\bfrom\s+{module}\b", line) or re.search(
|
|
125
|
+
rf"\bimport\s+{module}\b", line
|
|
126
|
+
):
|
|
127
|
+
self.violations.append(
|
|
128
|
+
CouplingViolation(
|
|
129
|
+
str(file_path),
|
|
130
|
+
line_num,
|
|
131
|
+
line,
|
|
132
|
+
"HARDCODED_IMPORT",
|
|
133
|
+
f"Direct import of module '{module}'. "
|
|
134
|
+
f"Use plugin/config-based loading instead.",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# JavaScript/TypeScript imports
|
|
139
|
+
if re.search(r"(import|require)\s*\(?\s*['\"]", line):
|
|
140
|
+
for module in self.modules:
|
|
141
|
+
if module in line:
|
|
142
|
+
self.violations.append(
|
|
143
|
+
CouplingViolation(
|
|
144
|
+
str(file_path),
|
|
145
|
+
line_num,
|
|
146
|
+
line,
|
|
147
|
+
"HARDCODED_IMPORT",
|
|
148
|
+
f"Direct import of module '{module}'. "
|
|
149
|
+
f"Use plugin/config-based loading instead.",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _check_hardcoded_paths(self, file_path: Path, content: str):
|
|
154
|
+
"""Check for hardcoded file paths to other modules"""
|
|
155
|
+
lines = content.split("\n")
|
|
156
|
+
|
|
157
|
+
for line_num, line in enumerate(lines, 1):
|
|
158
|
+
# Look for file path patterns
|
|
159
|
+
path_patterns = [
|
|
160
|
+
r'["\']([^"\']*(?:expansion-packs|modules)/([^"\']+))["\']',
|
|
161
|
+
r"Path\(['\"]([^'\"]*(?:expansion-packs|modules)/[^'\"]+)['\"]\)",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for pattern in path_patterns:
|
|
165
|
+
matches = re.finditer(pattern, line)
|
|
166
|
+
for match in matches:
|
|
167
|
+
path_str = match.group(1)
|
|
168
|
+
# Check if path references another module
|
|
169
|
+
for module in self.modules:
|
|
170
|
+
if module in path_str:
|
|
171
|
+
self.violations.append(
|
|
172
|
+
CouplingViolation(
|
|
173
|
+
str(file_path),
|
|
174
|
+
line_num,
|
|
175
|
+
line,
|
|
176
|
+
"HARDCODED_PATH",
|
|
177
|
+
f"Hardcoded path to module '{module}': {path_str}. "
|
|
178
|
+
f"Use configuration-based path resolution.",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _check_shared_state(self, file_path: Path, content: str):
|
|
183
|
+
"""Check for shared global state between modules"""
|
|
184
|
+
lines = content.split("\n")
|
|
185
|
+
|
|
186
|
+
# Patterns that suggest shared state
|
|
187
|
+
shared_state_patterns = [
|
|
188
|
+
(r"\bglobal\s+\w+", "Global variable usage"),
|
|
189
|
+
(r"^\s*[A-Z_]+\s*=\s*", "Module-level constant that might be shared"),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
for line_num, line in enumerate(lines, 1):
|
|
193
|
+
for pattern, description in shared_state_patterns:
|
|
194
|
+
if re.search(pattern, line):
|
|
195
|
+
# Check if it references other modules
|
|
196
|
+
for module in self.modules:
|
|
197
|
+
if module.lower() in line.lower():
|
|
198
|
+
self.violations.append(
|
|
199
|
+
CouplingViolation(
|
|
200
|
+
str(file_path),
|
|
201
|
+
line_num,
|
|
202
|
+
line,
|
|
203
|
+
"SHARED_STATE",
|
|
204
|
+
f"{description} referencing '{module}'.",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def check_file(self, file_path: Path):
|
|
209
|
+
"""Check a single file for coupling violations"""
|
|
210
|
+
try:
|
|
211
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
212
|
+
content = f.read()
|
|
213
|
+
|
|
214
|
+
self._check_hardcoded_imports(file_path, content)
|
|
215
|
+
self._check_hardcoded_paths(file_path, content)
|
|
216
|
+
self._check_shared_state(file_path, content)
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Warning: Could not scan {file_path}: {e}", file=sys.stderr)
|
|
220
|
+
|
|
221
|
+
def run(self) -> int:
|
|
222
|
+
"""Run coupling check on entire project"""
|
|
223
|
+
print("🔍 Checking for coupling violations...")
|
|
224
|
+
print(f" Project: {self.project_path}")
|
|
225
|
+
print(f" Modules: {', '.join(self.modules)}")
|
|
226
|
+
print()
|
|
227
|
+
|
|
228
|
+
files = self._find_files_to_scan()
|
|
229
|
+
print(f"📁 Scanning {len(files)} files...")
|
|
230
|
+
print()
|
|
231
|
+
|
|
232
|
+
for file_path in files:
|
|
233
|
+
self.check_file(file_path)
|
|
234
|
+
|
|
235
|
+
return self._report_results()
|
|
236
|
+
|
|
237
|
+
def _report_results(self) -> int:
|
|
238
|
+
"""Report results and return exit code"""
|
|
239
|
+
if not self.violations:
|
|
240
|
+
print("✅ No coupling violations found!")
|
|
241
|
+
print(" Zero-coupling principle maintained.")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
print(f"❌ Found {len(self.violations)} coupling violation(s):")
|
|
245
|
+
|
|
246
|
+
# Group violations by type
|
|
247
|
+
by_type: Dict[str, List[CouplingViolation]] = {}
|
|
248
|
+
for violation in self.violations:
|
|
249
|
+
if violation.violation_type not in by_type:
|
|
250
|
+
by_type[violation.violation_type] = []
|
|
251
|
+
by_type[violation.violation_type].append(violation)
|
|
252
|
+
|
|
253
|
+
# Report by type
|
|
254
|
+
for violation_type, violations in by_type.items():
|
|
255
|
+
print(f"\n{violation_type} ({len(violations)}):")
|
|
256
|
+
for violation in violations:
|
|
257
|
+
print(violation)
|
|
258
|
+
|
|
259
|
+
print("\n" + "=" * 80)
|
|
260
|
+
print("REMEDIATION STEPS:")
|
|
261
|
+
print("=" * 80)
|
|
262
|
+
print()
|
|
263
|
+
print("1. Remove hardcoded imports:")
|
|
264
|
+
print(" - Use plugin/adapter pattern")
|
|
265
|
+
print(" - Load modules via configuration")
|
|
266
|
+
print(" - Implement dependency injection")
|
|
267
|
+
print()
|
|
268
|
+
print("2. Externalize paths to YAML configuration:")
|
|
269
|
+
print(" - Define module paths in config file")
|
|
270
|
+
print(" - Use configuration loader to resolve paths")
|
|
271
|
+
print(" - Never hardcode cross-module paths")
|
|
272
|
+
print()
|
|
273
|
+
print("3. Eliminate shared state:")
|
|
274
|
+
print(" - Use message passing between modules")
|
|
275
|
+
print(" - Implement clean interfaces")
|
|
276
|
+
print(" - Each module maintains its own state")
|
|
277
|
+
print()
|
|
278
|
+
print("See: references/stop-rules-guide.md (Stop Rule 3)")
|
|
279
|
+
print()
|
|
280
|
+
|
|
281
|
+
return 1 # Exit code 1 indicates violations found
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main():
|
|
285
|
+
parser = argparse.ArgumentParser(
|
|
286
|
+
description="Check for coupling violations in codebase"
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument(
|
|
289
|
+
"--path",
|
|
290
|
+
type=Path,
|
|
291
|
+
default=Path.cwd(),
|
|
292
|
+
help="Project path to scan (default: current directory)",
|
|
293
|
+
)
|
|
294
|
+
parser.add_argument(
|
|
295
|
+
"--config", type=Path, help="Configuration file path (YAML)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
|
|
300
|
+
checker = CouplingChecker(args.path, args.config)
|
|
301
|
+
exit_code = checker.run()
|
|
302
|
+
sys.exit(exit_code)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
main()
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Risk Mitigation Validation Script
|
|
4
|
+
|
|
5
|
+
Validates that identified risks have appropriate mitigation strategies.
|
|
6
|
+
Checks project documentation for risk/mitigation coverage.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python validate_risk_mitigation.py [--path PROJECT_PATH] [--risks RISKS_FILE]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import argparse
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Dict, Tuple
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Risk:
|
|
22
|
+
"""Represents an identified risk"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: str, description: str, severity: str, source: str):
|
|
25
|
+
self.name = name
|
|
26
|
+
self.description = description
|
|
27
|
+
self.severity = severity # high, medium, low
|
|
28
|
+
self.source = source # file where risk identified
|
|
29
|
+
self.mitigation = None
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
status = "✅ MITIGATED" if self.mitigation else "❌ NO MITIGATION"
|
|
33
|
+
return (
|
|
34
|
+
f"\n [{self.severity.upper()}] {self.name}\n"
|
|
35
|
+
f" {self.description}\n"
|
|
36
|
+
f" Source: {self.source}\n"
|
|
37
|
+
f" Status: {status}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RiskMitigationValidator:
|
|
42
|
+
"""Validates risk mitigation coverage"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, project_path: Path, risks_file: Path = None):
|
|
45
|
+
self.project_path = project_path
|
|
46
|
+
self.risks_file = risks_file
|
|
47
|
+
self.risks: List[Risk] = []
|
|
48
|
+
self.mitigation_docs: Dict[str, str] = {}
|
|
49
|
+
|
|
50
|
+
def _find_risk_documents(self) -> List[Path]:
|
|
51
|
+
"""Find all documents that might contain risks"""
|
|
52
|
+
risk_doc_patterns = [
|
|
53
|
+
"**/architecture/*.md",
|
|
54
|
+
"**/design/*.md",
|
|
55
|
+
"**/docs/**/*risk*.md",
|
|
56
|
+
"**/docs/**/*adr*.md", # Architecture Decision Records
|
|
57
|
+
"**/*RISK*.md",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
docs = []
|
|
61
|
+
for pattern in risk_doc_patterns:
|
|
62
|
+
docs.extend(self.project_path.glob(pattern))
|
|
63
|
+
|
|
64
|
+
return list(set(docs)) # Remove duplicates
|
|
65
|
+
|
|
66
|
+
def _extract_risks_from_doc(self, doc_path: Path):
|
|
67
|
+
"""Extract risks from a document"""
|
|
68
|
+
try:
|
|
69
|
+
with open(doc_path, "r", encoding="utf-8") as f:
|
|
70
|
+
content = f.read()
|
|
71
|
+
|
|
72
|
+
# Look for risk sections
|
|
73
|
+
risk_patterns = [
|
|
74
|
+
# Markdown headers with "risk"
|
|
75
|
+
r"#{1,6}\s+.*[Rr]isks?\s*.*\n(.*?)(?=\n#{1,6}|\Z)",
|
|
76
|
+
# Table format
|
|
77
|
+
r"\|\s*[Rr]isk\s*\|.*\n\|[-\s|]+\n((?:\|.*\n)*)",
|
|
78
|
+
# Bullet points
|
|
79
|
+
r"[-*]\s+\*\*[Rr]isk:?\*\*\s+(.*?)(?=\n[-*]|\n\n|\Z)",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for pattern in risk_patterns:
|
|
83
|
+
matches = re.finditer(pattern, content, re.MULTILINE | re.DOTALL)
|
|
84
|
+
for match in matches:
|
|
85
|
+
risk_text = match.group(1)
|
|
86
|
+
self._parse_risks_from_text(risk_text, str(doc_path))
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Warning: Could not parse {doc_path}: {e}", file=sys.stderr)
|
|
90
|
+
|
|
91
|
+
def _parse_risks_from_text(self, text: str, source: str):
|
|
92
|
+
"""Parse individual risks from text block"""
|
|
93
|
+
# Simple heuristic: each line or paragraph is a risk
|
|
94
|
+
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
|
95
|
+
|
|
96
|
+
for line in lines:
|
|
97
|
+
# Skip table headers
|
|
98
|
+
if re.match(r"^\|?[-\s|]+\|?$", line):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Extract from table row: | risk | description | severity |
|
|
102
|
+
table_match = re.match(r"\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|", line)
|
|
103
|
+
if table_match:
|
|
104
|
+
name = table_match.group(1).strip()
|
|
105
|
+
description = table_match.group(2).strip()
|
|
106
|
+
severity = self._infer_severity(name, description)
|
|
107
|
+
self.risks.append(Risk(name, description, severity, source))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Extract from bullet: - **Risk:** description
|
|
111
|
+
bullet_match = re.match(
|
|
112
|
+
r"[-*]\s+\*\*([^:*]+):?\*\*\s+(.*)", line, re.IGNORECASE
|
|
113
|
+
)
|
|
114
|
+
if bullet_match:
|
|
115
|
+
name = bullet_match.group(1).strip()
|
|
116
|
+
description = bullet_match.group(2).strip()
|
|
117
|
+
severity = self._infer_severity(name, description)
|
|
118
|
+
self.risks.append(Risk(name, description, severity, source))
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Generic line as risk
|
|
122
|
+
if len(line) > 10: # Minimum length to be meaningful
|
|
123
|
+
severity = self._infer_severity(line, line)
|
|
124
|
+
self.risks.append(Risk(line[:50], line, severity, source))
|
|
125
|
+
|
|
126
|
+
def _infer_severity(self, name: str, description: str) -> str:
|
|
127
|
+
"""Infer severity from risk name/description"""
|
|
128
|
+
text = (name + " " + description).lower()
|
|
129
|
+
|
|
130
|
+
high_keywords = ["critical", "severe", "major", "high", "blocker"]
|
|
131
|
+
low_keywords = ["minor", "low", "trivial", "cosmetic"]
|
|
132
|
+
|
|
133
|
+
if any(kw in text for kw in high_keywords):
|
|
134
|
+
return "high"
|
|
135
|
+
elif any(kw in text for kw in low_keywords):
|
|
136
|
+
return "low"
|
|
137
|
+
else:
|
|
138
|
+
return "medium"
|
|
139
|
+
|
|
140
|
+
def _find_mitigation_documents(self) -> List[Path]:
|
|
141
|
+
"""Find documents that might contain mitigations"""
|
|
142
|
+
mitigation_patterns = [
|
|
143
|
+
"**/architecture/*.md",
|
|
144
|
+
"**/design/*.md",
|
|
145
|
+
"**/docs/**/*mitigation*.md",
|
|
146
|
+
"**/docs/**/*strategy*.md",
|
|
147
|
+
"**/*MITIGATION*.md",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
docs = []
|
|
151
|
+
for pattern in mitigation_patterns:
|
|
152
|
+
docs.extend(self.project_path.glob(pattern))
|
|
153
|
+
|
|
154
|
+
return list(set(docs))
|
|
155
|
+
|
|
156
|
+
def _extract_mitigations_from_doc(self, doc_path: Path):
|
|
157
|
+
"""Extract mitigations from document"""
|
|
158
|
+
try:
|
|
159
|
+
with open(doc_path, "r", encoding="utf-8") as f:
|
|
160
|
+
content = f.read()
|
|
161
|
+
|
|
162
|
+
# Store entire content as potential mitigation source
|
|
163
|
+
self.mitigation_docs[str(doc_path)] = content
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(
|
|
167
|
+
f"Warning: Could not parse {doc_path}: {e}", file=sys.stderr
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _match_risks_to_mitigations(self):
|
|
171
|
+
"""Match identified risks to mitigations"""
|
|
172
|
+
for risk in self.risks:
|
|
173
|
+
# Search for risk name/keywords in mitigation docs
|
|
174
|
+
risk_keywords = set(
|
|
175
|
+
re.findall(r"\b\w{4,}\b", risk.name.lower())
|
|
176
|
+
) # Words 4+ chars
|
|
177
|
+
|
|
178
|
+
for doc_path, content in self.mitigation_docs.items():
|
|
179
|
+
content_lower = content.lower()
|
|
180
|
+
|
|
181
|
+
# Check if risk keywords appear in mitigation section
|
|
182
|
+
if any(keyword in content_lower for keyword in risk_keywords):
|
|
183
|
+
# Look for mitigation keywords nearby
|
|
184
|
+
mitigation_keywords = [
|
|
185
|
+
"mitigation",
|
|
186
|
+
"mitigate",
|
|
187
|
+
"solution",
|
|
188
|
+
"strategy",
|
|
189
|
+
"address",
|
|
190
|
+
"resolve",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if any(kw in content_lower for kw in mitigation_keywords):
|
|
194
|
+
risk.mitigation = doc_path
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
def _load_risks_from_yaml(self):
|
|
198
|
+
"""Load risks from YAML file if provided"""
|
|
199
|
+
if not self.risks_file or not self.risks_file.exists():
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
with open(self.risks_file, "r") as f:
|
|
204
|
+
data = yaml.safe_load(f)
|
|
205
|
+
|
|
206
|
+
if "risks" in data:
|
|
207
|
+
for risk_data in data["risks"]:
|
|
208
|
+
risk = Risk(
|
|
209
|
+
name=risk_data.get("name", "Unnamed"),
|
|
210
|
+
description=risk_data.get("description", ""),
|
|
211
|
+
severity=risk_data.get("severity", "medium"),
|
|
212
|
+
source=str(self.risks_file),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Check if mitigation provided in YAML
|
|
216
|
+
if "mitigation" in risk_data:
|
|
217
|
+
risk.mitigation = "YAML: " + risk_data["mitigation"]
|
|
218
|
+
|
|
219
|
+
self.risks.append(risk)
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
print(
|
|
223
|
+
f"Warning: Could not load risks from {self.risks_file}: {e}",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def run(self) -> int:
|
|
228
|
+
"""Run risk mitigation validation"""
|
|
229
|
+
print("🔍 Validating risk mitigation coverage...")
|
|
230
|
+
print(f" Project: {self.project_path}")
|
|
231
|
+
print()
|
|
232
|
+
|
|
233
|
+
# Load risks from YAML if provided
|
|
234
|
+
if self.risks_file:
|
|
235
|
+
print(f"📄 Loading risks from: {self.risks_file}")
|
|
236
|
+
self._load_risks_from_yaml()
|
|
237
|
+
|
|
238
|
+
# Find and parse risk documents
|
|
239
|
+
print("📁 Scanning project documentation for risks...")
|
|
240
|
+
risk_docs = self._find_risk_documents()
|
|
241
|
+
print(f" Found {len(risk_docs)} potential risk documents")
|
|
242
|
+
|
|
243
|
+
for doc in risk_docs:
|
|
244
|
+
self._extract_risks_from_doc(doc)
|
|
245
|
+
|
|
246
|
+
print(f" Identified {len(self.risks)} risks")
|
|
247
|
+
print()
|
|
248
|
+
|
|
249
|
+
# Find mitigation documents
|
|
250
|
+
print("📁 Scanning for mitigation documentation...")
|
|
251
|
+
mitigation_docs = self._find_mitigation_documents()
|
|
252
|
+
print(f" Found {len(mitigation_docs)} potential mitigation documents")
|
|
253
|
+
|
|
254
|
+
for doc in mitigation_docs:
|
|
255
|
+
self._extract_mitigations_from_doc(doc)
|
|
256
|
+
|
|
257
|
+
print()
|
|
258
|
+
|
|
259
|
+
# Match risks to mitigations
|
|
260
|
+
print("🔗 Matching risks to mitigations...")
|
|
261
|
+
self._match_risks_to_mitigations()
|
|
262
|
+
print()
|
|
263
|
+
|
|
264
|
+
return self._report_results()
|
|
265
|
+
|
|
266
|
+
def _report_results(self) -> int:
|
|
267
|
+
"""Report results and return exit code"""
|
|
268
|
+
if not self.risks:
|
|
269
|
+
print("⚠️ No risks identified in project documentation.")
|
|
270
|
+
print(
|
|
271
|
+
" This might mean:"
|
|
272
|
+
)
|
|
273
|
+
print(" - Project has no documented risks")
|
|
274
|
+
print(" - Risk documentation not in expected locations")
|
|
275
|
+
print(" - Risk documentation format not recognized")
|
|
276
|
+
print()
|
|
277
|
+
print(" Recommended: Create risk documentation in:")
|
|
278
|
+
print(" - docs/architecture/risks.md")
|
|
279
|
+
print(" - Design documents with ## Risks section")
|
|
280
|
+
print(" - Architecture Decision Records (ADRs)")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
# Categorize risks
|
|
284
|
+
mitigated = [r for r in self.risks if r.mitigation]
|
|
285
|
+
unmitigated = [r for r in self.risks if not r.mitigation]
|
|
286
|
+
|
|
287
|
+
high_unmitigated = [
|
|
288
|
+
r for r in unmitigated if r.severity == "high"
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
print("=" * 80)
|
|
292
|
+
print("RISK MITIGATION REPORT")
|
|
293
|
+
print("=" * 80)
|
|
294
|
+
print()
|
|
295
|
+
print(f"Total Risks: {len(self.risks)}")
|
|
296
|
+
print(f"Mitigated: {len(mitigated)}")
|
|
297
|
+
print(f"Unmitigated: {len(unmitigated)}")
|
|
298
|
+
print()
|
|
299
|
+
|
|
300
|
+
if high_unmitigated:
|
|
301
|
+
print(f"⚠️ HIGH PRIORITY: {len(high_unmitigated)} high-severity risks without mitigation")
|
|
302
|
+
print()
|
|
303
|
+
|
|
304
|
+
# Report unmitigated risks first
|
|
305
|
+
if unmitigated:
|
|
306
|
+
print("❌ UNMITIGATED RISKS:")
|
|
307
|
+
for risk in sorted(
|
|
308
|
+
unmitigated, key=lambda r: {"high": 0, "medium": 1, "low": 2}[r.severity]
|
|
309
|
+
):
|
|
310
|
+
print(risk)
|
|
311
|
+
print()
|
|
312
|
+
|
|
313
|
+
# Report mitigated risks
|
|
314
|
+
if mitigated:
|
|
315
|
+
print("✅ MITIGATED RISKS:")
|
|
316
|
+
for risk in mitigated:
|
|
317
|
+
print(risk)
|
|
318
|
+
print(f" Mitigation: {risk.mitigation}")
|
|
319
|
+
print()
|
|
320
|
+
|
|
321
|
+
# Overall assessment
|
|
322
|
+
coverage = len(mitigated) / len(self.risks) * 100 if self.risks else 0
|
|
323
|
+
|
|
324
|
+
print("=" * 80)
|
|
325
|
+
print(f"COVERAGE: {coverage:.1f}%")
|
|
326
|
+
print("=" * 80)
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
if coverage == 100:
|
|
330
|
+
print("✅ All identified risks have mitigation strategies!")
|
|
331
|
+
return 0
|
|
332
|
+
elif coverage >= 80:
|
|
333
|
+
print("⚠️ Good coverage, but some risks lack mitigation.")
|
|
334
|
+
if not high_unmitigated:
|
|
335
|
+
print(" No high-severity risks unmitigated. Consider acceptable.")
|
|
336
|
+
return 0
|
|
337
|
+
else:
|
|
338
|
+
print("❌ Low mitigation coverage. Address unmitigated risks.")
|
|
339
|
+
|
|
340
|
+
if unmitigated:
|
|
341
|
+
print()
|
|
342
|
+
print("REMEDIATION STEPS:")
|
|
343
|
+
print("-" * 80)
|
|
344
|
+
print("1. Document mitigation strategy for each unmitigated risk")
|
|
345
|
+
print("2. Create mitigation documentation in:")
|
|
346
|
+
print(" - Architecture Decision Records (ADRs)")
|
|
347
|
+
print(" - Design documents (## Risk Mitigation section)")
|
|
348
|
+
print(" - Dedicated mitigation strategy documents")
|
|
349
|
+
print("3. For each risk, specify:")
|
|
350
|
+
print(" - Mitigation approach (avoid, reduce, transfer, accept)")
|
|
351
|
+
print(" - Concrete steps to implement mitigation")
|
|
352
|
+
print(" - Contingency plan if mitigation fails")
|
|
353
|
+
print(" - Responsible party/team")
|
|
354
|
+
print()
|
|
355
|
+
|
|
356
|
+
# Exit code based on high-severity unmitigated risks
|
|
357
|
+
return 1 if high_unmitigated else 0
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def main():
|
|
361
|
+
parser = argparse.ArgumentParser(
|
|
362
|
+
description="Validate risk mitigation coverage"
|
|
363
|
+
)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"--path",
|
|
366
|
+
type=Path,
|
|
367
|
+
default=Path.cwd(),
|
|
368
|
+
help="Project path to scan (default: current directory)",
|
|
369
|
+
)
|
|
370
|
+
parser.add_argument(
|
|
371
|
+
"--risks", type=Path, help="YAML file with explicit risk definitions"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
args = parser.parse_args()
|
|
375
|
+
|
|
376
|
+
validator = RiskMitigationValidator(args.path, args.risks)
|
|
377
|
+
exit_code = validator.run()
|
|
378
|
+
sys.exit(exit_code)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
main()
|