reporails-cli 0.0.1__py3-none-any.whl
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.
- reporails_cli/.env.example +1 -0
- reporails_cli/__init__.py +24 -0
- reporails_cli/bundled/.semgrepignore +51 -0
- reporails_cli/bundled/__init__.py +31 -0
- reporails_cli/bundled/capability-patterns.yml +54 -0
- reporails_cli/bundled/levels.yml +99 -0
- reporails_cli/core/__init__.py +35 -0
- reporails_cli/core/agents.py +147 -0
- reporails_cli/core/applicability.py +150 -0
- reporails_cli/core/bootstrap.py +147 -0
- reporails_cli/core/cache.py +352 -0
- reporails_cli/core/capability.py +245 -0
- reporails_cli/core/discover.py +362 -0
- reporails_cli/core/engine.py +177 -0
- reporails_cli/core/init.py +309 -0
- reporails_cli/core/levels.py +177 -0
- reporails_cli/core/models.py +329 -0
- reporails_cli/core/opengrep/__init__.py +34 -0
- reporails_cli/core/opengrep/runner.py +203 -0
- reporails_cli/core/opengrep/semgrepignore.py +39 -0
- reporails_cli/core/opengrep/templates.py +138 -0
- reporails_cli/core/registry.py +155 -0
- reporails_cli/core/sarif.py +181 -0
- reporails_cli/core/scorer.py +178 -0
- reporails_cli/core/semantic.py +193 -0
- reporails_cli/core/utils.py +139 -0
- reporails_cli/formatters/__init__.py +19 -0
- reporails_cli/formatters/json.py +137 -0
- reporails_cli/formatters/mcp.py +68 -0
- reporails_cli/formatters/text/__init__.py +32 -0
- reporails_cli/formatters/text/box.py +89 -0
- reporails_cli/formatters/text/chars.py +42 -0
- reporails_cli/formatters/text/compact.py +119 -0
- reporails_cli/formatters/text/components.py +117 -0
- reporails_cli/formatters/text/full.py +135 -0
- reporails_cli/formatters/text/rules.py +50 -0
- reporails_cli/formatters/text/violations.py +92 -0
- reporails_cli/interfaces/__init__.py +1 -0
- reporails_cli/interfaces/cli/__init__.py +7 -0
- reporails_cli/interfaces/cli/main.py +352 -0
- reporails_cli/interfaces/mcp/__init__.py +5 -0
- reporails_cli/interfaces/mcp/server.py +194 -0
- reporails_cli/interfaces/mcp/tools.py +136 -0
- reporails_cli/py.typed +0 -0
- reporails_cli/templates/__init__.py +65 -0
- reporails_cli/templates/cli_box.txt +10 -0
- reporails_cli/templates/cli_cta.txt +4 -0
- reporails_cli/templates/cli_delta.txt +1 -0
- reporails_cli/templates/cli_file_header.txt +1 -0
- reporails_cli/templates/cli_legend.txt +1 -0
- reporails_cli/templates/cli_pending.txt +3 -0
- reporails_cli/templates/cli_violation.txt +1 -0
- reporails_cli/templates/cli_working.txt +2 -0
- reporails_cli-0.0.1.dist-info/METADATA +108 -0
- reporails_cli-0.0.1.dist-info/RECORD +58 -0
- reporails_cli-0.0.1.dist-info/WHEEL +4 -0
- reporails_cli-0.0.1.dist-info/entry_points.txt +3 -0
- reporails_cli-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OPENGREP_VERSION=1.51.1
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Reporails - Lint and score CLAUDE.md files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.0.1"
|
|
6
|
+
|
|
7
|
+
from reporails_cli.core.models import (
|
|
8
|
+
Category,
|
|
9
|
+
Level,
|
|
10
|
+
RuleType,
|
|
11
|
+
Severity,
|
|
12
|
+
ValidationResult,
|
|
13
|
+
Violation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Category",
|
|
18
|
+
"Level",
|
|
19
|
+
"RuleType",
|
|
20
|
+
"Severity",
|
|
21
|
+
"ValidationResult",
|
|
22
|
+
"Violation",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Default semgrepignore for reporails
|
|
2
|
+
# Reduces scan time by excluding common non-instruction directories
|
|
3
|
+
|
|
4
|
+
# Package managers
|
|
5
|
+
node_modules/
|
|
6
|
+
vendor/
|
|
7
|
+
bower_components/
|
|
8
|
+
|
|
9
|
+
# Build outputs
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
out/
|
|
13
|
+
target/
|
|
14
|
+
*.min.js
|
|
15
|
+
*.min.css
|
|
16
|
+
*.bundle.js
|
|
17
|
+
|
|
18
|
+
# Python
|
|
19
|
+
__pycache__/
|
|
20
|
+
*.pyc
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
.tox/
|
|
24
|
+
.eggs/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
|
|
27
|
+
# Version control
|
|
28
|
+
.git/
|
|
29
|
+
|
|
30
|
+
# Coverage and testing
|
|
31
|
+
coverage/
|
|
32
|
+
.coverage
|
|
33
|
+
htmlcov/
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.nyc_output/
|
|
36
|
+
|
|
37
|
+
# IDE and editors
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/
|
|
40
|
+
*.swp
|
|
41
|
+
*.swo
|
|
42
|
+
|
|
43
|
+
# OS files
|
|
44
|
+
.DS_Store
|
|
45
|
+
Thumbs.db
|
|
46
|
+
|
|
47
|
+
# Large binary files
|
|
48
|
+
*.wasm
|
|
49
|
+
*.so
|
|
50
|
+
*.dylib
|
|
51
|
+
*.dll
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Bundled configuration files for reporails CLI.
|
|
2
|
+
|
|
3
|
+
This package contains CLI-owned configuration:
|
|
4
|
+
- levels.yml: Level definitions and rule-to-level mapping
|
|
5
|
+
- capability-patterns.yml: OpenGrep patterns for capability detection
|
|
6
|
+
- .semgrepignore: Default ignore patterns for OpenGrep/Semgrep
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_bundled_path() -> Path:
|
|
15
|
+
"""Get path to bundled configuration directory."""
|
|
16
|
+
return Path(__file__).parent
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_levels_path() -> Path:
|
|
20
|
+
"""Get path to bundled levels.yml."""
|
|
21
|
+
return get_bundled_path() / "levels.yml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_capability_patterns_path() -> Path:
|
|
25
|
+
"""Get path to bundled capability-patterns.yml."""
|
|
26
|
+
return get_bundled_path() / "capability-patterns.yml"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_semgrepignore_path() -> Path:
|
|
30
|
+
"""Get path to bundled .semgrepignore."""
|
|
31
|
+
return get_bundled_path() / ".semgrepignore"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# OpenGrep patterns for capability detection
|
|
2
|
+
# CLI-owned configuration for content analysis (Phase 2)
|
|
3
|
+
|
|
4
|
+
rules:
|
|
5
|
+
- id: capability.has-sections
|
|
6
|
+
message: "Has markdown sections (H2+)"
|
|
7
|
+
languages: [generic]
|
|
8
|
+
severity: INFO
|
|
9
|
+
pattern-regex: "^##+ "
|
|
10
|
+
paths:
|
|
11
|
+
include:
|
|
12
|
+
- "*.md"
|
|
13
|
+
- "**/CLAUDE.md"
|
|
14
|
+
- "**/*.md"
|
|
15
|
+
|
|
16
|
+
- id: capability.has-imports
|
|
17
|
+
message: "Has @imports or file references"
|
|
18
|
+
languages: [generic]
|
|
19
|
+
severity: INFO
|
|
20
|
+
pattern-either:
|
|
21
|
+
- pattern-regex: "@import"
|
|
22
|
+
- pattern-regex: "@docs/"
|
|
23
|
+
- pattern-regex: "@\\.shared/"
|
|
24
|
+
- pattern-regex: "Read `[^`]+`"
|
|
25
|
+
paths:
|
|
26
|
+
include:
|
|
27
|
+
- "*.md"
|
|
28
|
+
- "**/CLAUDE.md"
|
|
29
|
+
- "**/*.md"
|
|
30
|
+
|
|
31
|
+
- id: capability.has-explicit-constraints
|
|
32
|
+
message: "Has MUST/MUST NOT/NEVER constraints"
|
|
33
|
+
languages: [generic]
|
|
34
|
+
severity: INFO
|
|
35
|
+
pattern-either:
|
|
36
|
+
- pattern-regex: "\\bMUST\\b"
|
|
37
|
+
- pattern-regex: "\\bMUST NOT\\b"
|
|
38
|
+
- pattern-regex: "\\bNEVER\\b"
|
|
39
|
+
paths:
|
|
40
|
+
include:
|
|
41
|
+
- "*.md"
|
|
42
|
+
- "**/CLAUDE.md"
|
|
43
|
+
- "**/*.md"
|
|
44
|
+
|
|
45
|
+
- id: capability.has-path-scoped-rules
|
|
46
|
+
message: "Has path-scoped rules (paths: in frontmatter)"
|
|
47
|
+
languages: [generic]
|
|
48
|
+
severity: INFO
|
|
49
|
+
pattern-regex: "^paths:\\s*$"
|
|
50
|
+
paths:
|
|
51
|
+
include:
|
|
52
|
+
- ".claude/rules/*.md"
|
|
53
|
+
- ".cursor/rules/*.md"
|
|
54
|
+
- ".ai/rules/*.md"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Level definitions and rule-to-level mapping
|
|
2
|
+
# CLI-owned configuration for orchestration logic
|
|
3
|
+
|
|
4
|
+
levels:
|
|
5
|
+
L1:
|
|
6
|
+
name: Absent
|
|
7
|
+
required_rules: []
|
|
8
|
+
|
|
9
|
+
L2:
|
|
10
|
+
name: Basic
|
|
11
|
+
includes: [L1]
|
|
12
|
+
required_rules:
|
|
13
|
+
- S1 # Size limits
|
|
14
|
+
- C1 # Core sections
|
|
15
|
+
- C2 # Explicit over implicit
|
|
16
|
+
- C4 # Anti-pattern documentation
|
|
17
|
+
- C7 # Emphasis discipline
|
|
18
|
+
- C8 # Instructions over philosophy
|
|
19
|
+
- C9 # Has project description
|
|
20
|
+
- C10 # Has NEVER statements
|
|
21
|
+
- C12 # Has version/date
|
|
22
|
+
- M5 # Auto-generated content review
|
|
23
|
+
|
|
24
|
+
L3:
|
|
25
|
+
name: Structured
|
|
26
|
+
includes: [L2]
|
|
27
|
+
required_rules:
|
|
28
|
+
- S2 # Progressive disclosure
|
|
29
|
+
- S3 # No embedded code snippets
|
|
30
|
+
- S7 # Clear markdown structure
|
|
31
|
+
- C3 # Context-specific content
|
|
32
|
+
- C6 # Single source of truth
|
|
33
|
+
- C11 # Links are valid
|
|
34
|
+
- E6 # Code block line limit
|
|
35
|
+
- E7 # Import count
|
|
36
|
+
- M1 # Version control
|
|
37
|
+
- M2 # Review process
|
|
38
|
+
|
|
39
|
+
L4:
|
|
40
|
+
name: Abstracted
|
|
41
|
+
includes: [L3]
|
|
42
|
+
required_rules:
|
|
43
|
+
- S4 # Hierarchical memory
|
|
44
|
+
- S5 # Path-scoped rules
|
|
45
|
+
- E1 # Deterministic tools for style
|
|
46
|
+
- E3 # Purpose-based file reading
|
|
47
|
+
- E4 # Memory reference
|
|
48
|
+
- E5 # Grep efficiency
|
|
49
|
+
- E8 # Context window awareness
|
|
50
|
+
- M7 # Rule snippet length enforcement
|
|
51
|
+
|
|
52
|
+
L5:
|
|
53
|
+
name: Governed
|
|
54
|
+
includes: [L4]
|
|
55
|
+
required_rules:
|
|
56
|
+
- G1 # Organization-level policies
|
|
57
|
+
- G2 # Team governance structure
|
|
58
|
+
- G3 # Security rules ownership
|
|
59
|
+
- G4 # Ownership assignment
|
|
60
|
+
- G8 # Metrics and CI/CD checks
|
|
61
|
+
- M3 # Change management
|
|
62
|
+
- M4 # No conflicting rules
|
|
63
|
+
|
|
64
|
+
L6:
|
|
65
|
+
name: Adaptive
|
|
66
|
+
includes: [L5]
|
|
67
|
+
required_rules:
|
|
68
|
+
- S6 # YAML backbone
|
|
69
|
+
- C5 # MUST/MUST NOT with context
|
|
70
|
+
- E2 # Session start ritual
|
|
71
|
+
- G5 # Contract registry
|
|
72
|
+
- G6 # Component-contract binding
|
|
73
|
+
- G7 # Architecture tests
|
|
74
|
+
- M6 # Map staleness prevention
|
|
75
|
+
|
|
76
|
+
# Capability score thresholds
|
|
77
|
+
score_thresholds:
|
|
78
|
+
L1: 0
|
|
79
|
+
L2: 1
|
|
80
|
+
L3: 3
|
|
81
|
+
L4: 5
|
|
82
|
+
L5: 7
|
|
83
|
+
L6: 10
|
|
84
|
+
|
|
85
|
+
# Feature detection for capability scoring
|
|
86
|
+
detection:
|
|
87
|
+
L6:
|
|
88
|
+
- has_backbone
|
|
89
|
+
L5:
|
|
90
|
+
- component_count_3plus
|
|
91
|
+
- has_shared_files
|
|
92
|
+
L4:
|
|
93
|
+
- has_rules_dir
|
|
94
|
+
L3:
|
|
95
|
+
- has_imports
|
|
96
|
+
- has_multiple_instruction_files
|
|
97
|
+
L2:
|
|
98
|
+
- has_instruction_file
|
|
99
|
+
L1: []
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Core domain logic for reporails."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from reporails_cli.core.models import (
|
|
6
|
+
Category,
|
|
7
|
+
Check,
|
|
8
|
+
JudgmentRequest,
|
|
9
|
+
JudgmentResponse,
|
|
10
|
+
Level,
|
|
11
|
+
Rule,
|
|
12
|
+
RuleType,
|
|
13
|
+
Severity,
|
|
14
|
+
ValidationResult,
|
|
15
|
+
Violation,
|
|
16
|
+
)
|
|
17
|
+
from reporails_cli.core.scorer import (
|
|
18
|
+
calculate_score,
|
|
19
|
+
estimate_friction,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Category",
|
|
24
|
+
"Check",
|
|
25
|
+
"JudgmentRequest",
|
|
26
|
+
"JudgmentResponse",
|
|
27
|
+
"Level",
|
|
28
|
+
"Rule",
|
|
29
|
+
"RuleType",
|
|
30
|
+
"Severity",
|
|
31
|
+
"ValidationResult",
|
|
32
|
+
"Violation",
|
|
33
|
+
"calculate_score",
|
|
34
|
+
"estimate_friction",
|
|
35
|
+
]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Agent definitions - coding agent agnostic discovery.
|
|
2
|
+
|
|
3
|
+
Supports multiple AI coding assistants:
|
|
4
|
+
- Claude (Anthropic)
|
|
5
|
+
- Cursor
|
|
6
|
+
- Windsurf
|
|
7
|
+
- GitHub Copilot
|
|
8
|
+
- Aider
|
|
9
|
+
- Generic/custom
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AgentType:
|
|
20
|
+
"""Definition of a coding agent's file conventions."""
|
|
21
|
+
|
|
22
|
+
id: str
|
|
23
|
+
name: str
|
|
24
|
+
instruction_patterns: tuple[str, ...] # Glob patterns for instruction files
|
|
25
|
+
config_patterns: tuple[str, ...] # Glob patterns for config files
|
|
26
|
+
rule_patterns: tuple[str, ...] # Glob patterns for rule/snippet files
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Known coding agents and their conventions
|
|
30
|
+
KNOWN_AGENTS: dict[str, AgentType] = {
|
|
31
|
+
"claude": AgentType(
|
|
32
|
+
id="claude",
|
|
33
|
+
name="Claude (Anthropic)",
|
|
34
|
+
instruction_patterns=("CLAUDE.md", "**/CLAUDE.md"),
|
|
35
|
+
config_patterns=(".claude/settings.json", ".claude/mcp.json"),
|
|
36
|
+
rule_patterns=(".claude/rules/*.md", ".claude/**/*.md"),
|
|
37
|
+
),
|
|
38
|
+
"cursor": AgentType(
|
|
39
|
+
id="cursor",
|
|
40
|
+
name="Cursor",
|
|
41
|
+
instruction_patterns=(".cursorrules", ".cursor/rules/*.md"),
|
|
42
|
+
config_patterns=(".cursor/settings.json",),
|
|
43
|
+
rule_patterns=(".cursor/rules/*.md",),
|
|
44
|
+
),
|
|
45
|
+
"windsurf": AgentType(
|
|
46
|
+
id="windsurf",
|
|
47
|
+
name="Windsurf",
|
|
48
|
+
instruction_patterns=(".windsurfrules",),
|
|
49
|
+
config_patterns=(),
|
|
50
|
+
rule_patterns=(),
|
|
51
|
+
),
|
|
52
|
+
"copilot": AgentType(
|
|
53
|
+
id="copilot",
|
|
54
|
+
name="GitHub Copilot",
|
|
55
|
+
instruction_patterns=(".github/copilot-instructions.md",),
|
|
56
|
+
config_patterns=(),
|
|
57
|
+
rule_patterns=(),
|
|
58
|
+
),
|
|
59
|
+
"aider": AgentType(
|
|
60
|
+
id="aider",
|
|
61
|
+
name="Aider",
|
|
62
|
+
instruction_patterns=(".aider.conf.yml", "CONVENTIONS.md"),
|
|
63
|
+
config_patterns=(".aider.conf.yml",),
|
|
64
|
+
rule_patterns=(),
|
|
65
|
+
),
|
|
66
|
+
"generic": AgentType(
|
|
67
|
+
id="generic",
|
|
68
|
+
name="Generic AI Instructions",
|
|
69
|
+
instruction_patterns=("AGENTS.md", ".ai/instructions.md", ".ai/**/*.md"),
|
|
70
|
+
config_patterns=(),
|
|
71
|
+
rule_patterns=(".ai/rules/*.md",),
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DetectedAgent:
|
|
78
|
+
"""An agent detected in a project."""
|
|
79
|
+
|
|
80
|
+
agent_type: AgentType
|
|
81
|
+
instruction_files: list[Path] = field(default_factory=list)
|
|
82
|
+
config_files: list[Path] = field(default_factory=list)
|
|
83
|
+
rule_files: list[Path] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def detect_agents(target: Path) -> list[DetectedAgent]:
|
|
87
|
+
"""
|
|
88
|
+
Detect which coding agents are configured in the target directory.
|
|
89
|
+
|
|
90
|
+
Scans for known file patterns and returns detected agents with their files.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
target: Project root to scan
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of detected agents with their associated files
|
|
97
|
+
"""
|
|
98
|
+
detected: list[DetectedAgent] = []
|
|
99
|
+
|
|
100
|
+
for _agent_id, agent_type in KNOWN_AGENTS.items():
|
|
101
|
+
instruction_files: list[Path] = []
|
|
102
|
+
config_files: list[Path] = []
|
|
103
|
+
rule_files: list[Path] = []
|
|
104
|
+
|
|
105
|
+
# Find instruction files
|
|
106
|
+
for pattern in agent_type.instruction_patterns:
|
|
107
|
+
instruction_files.extend(target.glob(pattern))
|
|
108
|
+
|
|
109
|
+
# Find config files
|
|
110
|
+
for pattern in agent_type.config_patterns:
|
|
111
|
+
config_files.extend(target.glob(pattern))
|
|
112
|
+
|
|
113
|
+
# Find rule files
|
|
114
|
+
for pattern in agent_type.rule_patterns:
|
|
115
|
+
rule_files.extend(target.glob(pattern))
|
|
116
|
+
|
|
117
|
+
# Only include if we found at least one instruction file
|
|
118
|
+
if instruction_files:
|
|
119
|
+
detected.append(
|
|
120
|
+
DetectedAgent(
|
|
121
|
+
agent_type=agent_type,
|
|
122
|
+
instruction_files=sorted(set(instruction_files)),
|
|
123
|
+
config_files=sorted(set(config_files)),
|
|
124
|
+
rule_files=sorted(set(rule_files)),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return detected
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_all_instruction_files(target: Path) -> list[Path]:
|
|
132
|
+
"""
|
|
133
|
+
Get all instruction files for all detected agents.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
target: Project root to scan
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Deduplicated list of all instruction file paths
|
|
140
|
+
"""
|
|
141
|
+
all_files: set[Path] = set()
|
|
142
|
+
|
|
143
|
+
for detected in detect_agents(target):
|
|
144
|
+
all_files.update(detected.instruction_files)
|
|
145
|
+
all_files.update(detected.rule_files)
|
|
146
|
+
|
|
147
|
+
return sorted(all_files)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Feature detection (filesystem) and rule applicability.
|
|
2
|
+
|
|
3
|
+
Phase 1 of capability detection - scans filesystem for features.
|
|
4
|
+
Phase 2 (content detection) is in capability.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from reporails_cli.core.levels import get_rules_for_level
|
|
14
|
+
from reporails_cli.core.models import DetectedFeatures, Level, Rule
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def detect_features_filesystem(target: Path) -> DetectedFeatures:
|
|
18
|
+
"""Detect project features from file structure.
|
|
19
|
+
|
|
20
|
+
Phase 1 of capability detection - filesystem only, no content analysis.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
target: Project root path
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
DetectedFeatures with filesystem-based indicators
|
|
27
|
+
"""
|
|
28
|
+
features = DetectedFeatures()
|
|
29
|
+
|
|
30
|
+
# Check for CLAUDE.md at root
|
|
31
|
+
root_claude = target / "CLAUDE.md"
|
|
32
|
+
features.has_claude_md = root_claude.exists()
|
|
33
|
+
features.has_instruction_file = features.has_claude_md
|
|
34
|
+
|
|
35
|
+
# Check for .claude/rules/
|
|
36
|
+
rules_dir = target / ".claude" / "rules"
|
|
37
|
+
features.has_rules_dir = rules_dir.exists() and any(rules_dir.glob("*.md"))
|
|
38
|
+
|
|
39
|
+
# Check for other agent rules directories
|
|
40
|
+
other_rules = [".cursor/rules", ".ai/rules"]
|
|
41
|
+
for pattern in other_rules:
|
|
42
|
+
other_dir = target / pattern
|
|
43
|
+
if other_dir.exists() and any(other_dir.glob("*.md")):
|
|
44
|
+
features.has_rules_dir = True
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
# Check for backbone.yml
|
|
48
|
+
backbone_path = target / ".reporails" / "backbone.yml"
|
|
49
|
+
features.has_backbone = backbone_path.exists()
|
|
50
|
+
|
|
51
|
+
# Count instruction files
|
|
52
|
+
claude_files = list(target.rglob("CLAUDE.md"))
|
|
53
|
+
features.instruction_file_count = len(claude_files)
|
|
54
|
+
features.has_multiple_instruction_files = len(claude_files) > 1
|
|
55
|
+
|
|
56
|
+
if features.instruction_file_count > 0:
|
|
57
|
+
features.has_instruction_file = True
|
|
58
|
+
|
|
59
|
+
# Check for hierarchical structure (nested CLAUDE.md)
|
|
60
|
+
for cf in claude_files:
|
|
61
|
+
if cf.parent != target:
|
|
62
|
+
features.has_hierarchical_structure = True
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
# Check for @imports in content (simple check, full check in Phase 2)
|
|
66
|
+
if features.has_claude_md:
|
|
67
|
+
try:
|
|
68
|
+
content = root_claude.read_text(encoding="utf-8")
|
|
69
|
+
features.has_imports = "@" in content
|
|
70
|
+
except (OSError, UnicodeDecodeError):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Check for shared files
|
|
74
|
+
shared_patterns = [".shared", "shared", ".ai/shared"]
|
|
75
|
+
for pattern in shared_patterns:
|
|
76
|
+
if (target / pattern).exists():
|
|
77
|
+
features.has_shared_files = True
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
# Count components from backbone if present
|
|
81
|
+
if features.has_backbone:
|
|
82
|
+
try:
|
|
83
|
+
backbone_content = backbone_path.read_text(encoding="utf-8")
|
|
84
|
+
backbone_data = yaml.safe_load(backbone_content)
|
|
85
|
+
features.component_count = len(backbone_data.get("components", {}))
|
|
86
|
+
except (yaml.YAMLError, OSError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return features
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_applicable_rules(rules: dict[str, Rule], level: Level) -> dict[str, Rule]:
|
|
93
|
+
"""Filter rules to those applicable at the given level.
|
|
94
|
+
|
|
95
|
+
Rules apply at their minimum level and above.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
rules: Dict of all rules
|
|
99
|
+
level: Detected capability level
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict of applicable rules
|
|
103
|
+
"""
|
|
104
|
+
# Get rule IDs for this level from levels.yml
|
|
105
|
+
applicable_ids = get_rules_for_level(level)
|
|
106
|
+
|
|
107
|
+
# Filter rules
|
|
108
|
+
return {k: v for k, v in rules.items() if k in applicable_ids}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_feature_summary(features: DetectedFeatures) -> str:
|
|
112
|
+
"""Generate human-readable summary of detected features.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
features: Detected project features
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Summary string for display
|
|
119
|
+
"""
|
|
120
|
+
parts = []
|
|
121
|
+
|
|
122
|
+
# File count
|
|
123
|
+
if features.instruction_file_count == 0:
|
|
124
|
+
parts.append("No instruction files")
|
|
125
|
+
elif features.instruction_file_count == 1:
|
|
126
|
+
parts.append("1 instruction file")
|
|
127
|
+
else:
|
|
128
|
+
parts.append(f"{features.instruction_file_count} instruction files")
|
|
129
|
+
|
|
130
|
+
# Features present
|
|
131
|
+
feature_list = []
|
|
132
|
+
if features.has_rules_dir:
|
|
133
|
+
feature_list.append(".claude/rules/")
|
|
134
|
+
if features.has_backbone:
|
|
135
|
+
feature_list.append("backbone.yml")
|
|
136
|
+
if features.has_shared_files:
|
|
137
|
+
feature_list.append("shared files")
|
|
138
|
+
if features.has_hierarchical_structure:
|
|
139
|
+
feature_list.append("hierarchical")
|
|
140
|
+
|
|
141
|
+
if feature_list:
|
|
142
|
+
parts.append(" + ".join(feature_list))
|
|
143
|
+
|
|
144
|
+
return ", ".join(parts) if parts else "No features detected"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Legacy alias for backward compatibility
|
|
148
|
+
def detect_features(target: Path) -> DetectedFeatures:
|
|
149
|
+
"""Legacy alias for detect_features_filesystem()."""
|
|
150
|
+
return detect_features_filesystem(target)
|