opsward 0.0.2__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.
- opsward/__init__.py +18 -0
- opsward/__main__.py +13 -0
- opsward/base.py +204 -0
- opsward/cli.py +211 -0
- opsward/data/__init__.py +0 -0
- opsward/data/templates/__init__.py +0 -0
- opsward/data/templates/jsts/conventions.md +30 -0
- opsward/data/templates/jsts/testing.md +28 -0
- opsward/data/templates/python/conventions.md +33 -0
- opsward/data/templates/python/testing.md +29 -0
- opsward/data/templates/shared/architecture.md +21 -0
- opsward/data/templates/shared/claude_md.md +28 -0
- opsward/data/templates/shared/decisions/0000-template.md +17 -0
- opsward/data/templates/shared/dependencies.md +17 -0
- opsward/data/templates/shared/deployment.md +17 -0
- opsward/data/templates/shared/diagnose-setup/SKILL.md +22 -0
- opsward/data/templates/shared/docs_guide.md +24 -0
- opsward/data/templates/shared/glossary.md +7 -0
- opsward/data/templates/shared/known_issues.md +16 -0
- opsward/data/templates/shared/maintain-docs/SKILL.md +21 -0
- opsward/data/templates/shared/roadmap.md +17 -0
- opsward/data/templates/shared/session_log.md +10 -0
- opsward/data/templates/shared/setup-auditor.md +21 -0
- opsward/generate.py +422 -0
- opsward/maintain.py +237 -0
- opsward/scan.py +155 -0
- opsward/score.py +451 -0
- opsward/util.py +48 -0
- opsward-0.0.2.dist-info/METADATA +135 -0
- opsward-0.0.2.dist-info/RECORD +33 -0
- opsward-0.0.2.dist-info/WHEEL +4 -0
- opsward-0.0.2.dist-info/entry_points.txt +2 -0
- opsward-0.0.2.dist-info/licenses/LICENSE +21 -0
opsward/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Diagnose, generate, and maintain the AI agent setup of your projects."""
|
|
2
|
+
|
|
3
|
+
from opsward.base import (
|
|
4
|
+
AgentInfo,
|
|
5
|
+
ComponentScore,
|
|
6
|
+
DiagnosisReport,
|
|
7
|
+
DocSpec,
|
|
8
|
+
GeneratedFile,
|
|
9
|
+
MaintenanceSuggestion,
|
|
10
|
+
ProjectType,
|
|
11
|
+
RuleInfo,
|
|
12
|
+
ScanResult,
|
|
13
|
+
SkillInfo,
|
|
14
|
+
)
|
|
15
|
+
from opsward.scan import scan
|
|
16
|
+
from opsward.score import diagnose
|
|
17
|
+
from opsward.generate import generate
|
|
18
|
+
from opsward.maintain import maintain
|
opsward/__main__.py
ADDED
opsward/base.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""All dataclasses and type definitions for opsward."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectType(Enum):
|
|
10
|
+
"""Detected project type."""
|
|
11
|
+
|
|
12
|
+
python = "python"
|
|
13
|
+
jsts = "jsts"
|
|
14
|
+
mixed = "mixed"
|
|
15
|
+
unknown = "unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Inventory items
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SkillInfo:
|
|
25
|
+
"""A skill found in .claude/skills/."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
path: Path
|
|
29
|
+
has_skill_md: bool = False
|
|
30
|
+
description: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class AgentInfo:
|
|
35
|
+
"""An agent found in .claude/agents/."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
path: Path
|
|
39
|
+
description: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class RuleInfo:
|
|
44
|
+
"""A rule found in .claude/rules/."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
path: Path
|
|
48
|
+
content: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class DocSpec:
|
|
53
|
+
"""A document found in the docs directory."""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
path: Path
|
|
57
|
+
size_bytes: int = 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Scan output
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ScanResult:
|
|
67
|
+
"""Everything we learned by reading (never writing) a target project."""
|
|
68
|
+
|
|
69
|
+
project_root: Path
|
|
70
|
+
project_type: ProjectType = ProjectType.unknown
|
|
71
|
+
|
|
72
|
+
# CLAUDE.md
|
|
73
|
+
claude_md_path: Optional[Path] = None
|
|
74
|
+
claude_md_content: str = ""
|
|
75
|
+
|
|
76
|
+
# .claude/ inventories
|
|
77
|
+
skills: list[SkillInfo] = field(default_factory=list)
|
|
78
|
+
agents: list[AgentInfo] = field(default_factory=list)
|
|
79
|
+
rules: list[RuleInfo] = field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
# Hooks
|
|
82
|
+
hooks_path: Optional[Path] = None
|
|
83
|
+
hooks_config: Optional[dict] = None
|
|
84
|
+
|
|
85
|
+
# Docs
|
|
86
|
+
docs: list[DocSpec] = field(default_factory=list)
|
|
87
|
+
has_docs_guide: bool = False
|
|
88
|
+
docs_guide_path: Optional[Path] = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Scoring / diagnosis output
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ComponentScore:
|
|
98
|
+
"""Score for a single component (0–100) with optional notes."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
score: int
|
|
102
|
+
max_score: int = 100
|
|
103
|
+
notes: list[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class DiagnosisReport:
|
|
108
|
+
"""Report card produced by scoring a ScanResult."""
|
|
109
|
+
|
|
110
|
+
project_root: Path
|
|
111
|
+
project_type: ProjectType
|
|
112
|
+
scores: list[ComponentScore] = field(default_factory=list)
|
|
113
|
+
missing_items: list[str] = field(default_factory=list)
|
|
114
|
+
suggestions: list[str] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
# Weighted overall score (0–100), set by score.py
|
|
117
|
+
weighted_score: float = 0.0
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def overall_score(self) -> float:
|
|
121
|
+
"""Weighted score if set, else simple average."""
|
|
122
|
+
if self.weighted_score:
|
|
123
|
+
return self.weighted_score
|
|
124
|
+
if not self.scores:
|
|
125
|
+
return 0.0
|
|
126
|
+
return sum(s.score for s in self.scores) / len(self.scores)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def grade(self) -> str:
|
|
130
|
+
"""Letter grade: A (90-100), B (80-89), C (70-79), D (60-69), F (<60)."""
|
|
131
|
+
s = self.overall_score
|
|
132
|
+
if s >= 90:
|
|
133
|
+
return "A"
|
|
134
|
+
if s >= 80:
|
|
135
|
+
return "B"
|
|
136
|
+
if s >= 70:
|
|
137
|
+
return "C"
|
|
138
|
+
if s >= 60:
|
|
139
|
+
return "D"
|
|
140
|
+
return "F"
|
|
141
|
+
|
|
142
|
+
def __str__(self) -> str:
|
|
143
|
+
lines = [
|
|
144
|
+
f"Diagnosis Report: {self.project_root.name}",
|
|
145
|
+
f"Project type: {self.project_type.value}",
|
|
146
|
+
f"Overall score: {self.overall_score:.0f}/100 (Grade: {self.grade})",
|
|
147
|
+
"",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
if self.scores:
|
|
151
|
+
lines.append("Components:")
|
|
152
|
+
for cs in self.scores:
|
|
153
|
+
bar = _score_bar(cs.score, cs.max_score)
|
|
154
|
+
lines.append(f" {cs.name:<25s} {bar} {cs.score}/{cs.max_score}")
|
|
155
|
+
for note in cs.notes:
|
|
156
|
+
lines.append(f" - {note}")
|
|
157
|
+
lines.append("")
|
|
158
|
+
|
|
159
|
+
if self.missing_items:
|
|
160
|
+
lines.append("Missing:")
|
|
161
|
+
for item in self.missing_items:
|
|
162
|
+
lines.append(f" [ ] {item}")
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
if self.suggestions:
|
|
166
|
+
lines.append("Suggestions:")
|
|
167
|
+
for i, sug in enumerate(self.suggestions, 1):
|
|
168
|
+
lines.append(f" {i}. {sug}")
|
|
169
|
+
|
|
170
|
+
return "\n".join(lines)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _score_bar(score: int, max_score: int, *, width: int = 20) -> str:
|
|
174
|
+
"""Return a simple ASCII progress bar."""
|
|
175
|
+
filled = round(width * score / max_score) if max_score else 0
|
|
176
|
+
return "[" + "#" * filled + "." * (width - filled) + "]"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Generation output
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True)
|
|
185
|
+
class GeneratedFile:
|
|
186
|
+
"""A file to be written by the generate step."""
|
|
187
|
+
|
|
188
|
+
target_path: Path
|
|
189
|
+
content: str
|
|
190
|
+
overwrite_policy: str = "skip" # 'skip' | 'overwrite' | 'merge'
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# Maintenance output
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass(frozen=True)
|
|
199
|
+
class MaintenanceSuggestion:
|
|
200
|
+
"""A single maintenance action proposed by maintain.py."""
|
|
201
|
+
|
|
202
|
+
category: str # e.g. 'stale_path', 'outdated_doc', 'sync_issue'
|
|
203
|
+
description: str
|
|
204
|
+
diff: str = ""
|
opsward/cli.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""CLI dispatch for opsward."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from opsward.base import DiagnosisReport
|
|
9
|
+
from opsward.generate import generate
|
|
10
|
+
from opsward.maintain import maintain
|
|
11
|
+
from opsward.scan import scan
|
|
12
|
+
from opsward.score import diagnose
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _serialize_report(report: DiagnosisReport) -> dict:
|
|
16
|
+
"""Convert a DiagnosisReport to a JSON-serialisable dict."""
|
|
17
|
+
d = asdict(report)
|
|
18
|
+
d["project_root"] = str(report.project_root)
|
|
19
|
+
d["project_type"] = report.project_type.value
|
|
20
|
+
d["overall_score"] = report.overall_score
|
|
21
|
+
d["grade"] = report.grade
|
|
22
|
+
return d
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def diagnose_cmd(
|
|
26
|
+
*project_roots: str,
|
|
27
|
+
format: str = "text",
|
|
28
|
+
verbose: bool = False,
|
|
29
|
+
):
|
|
30
|
+
"""Diagnose the AI agent setup of one or more projects.
|
|
31
|
+
|
|
32
|
+
:param project_roots: one or more paths to project directories
|
|
33
|
+
:param format: output format — 'text' or 'json'
|
|
34
|
+
:param verbose: show additional detail in text output
|
|
35
|
+
"""
|
|
36
|
+
if not project_roots:
|
|
37
|
+
project_roots = (".",)
|
|
38
|
+
|
|
39
|
+
reports = []
|
|
40
|
+
for root_str in project_roots:
|
|
41
|
+
root = Path(root_str).resolve()
|
|
42
|
+
if not root.is_dir():
|
|
43
|
+
print(f"Error: {root_str} is not a directory", file=sys.stderr)
|
|
44
|
+
sys.exit(2)
|
|
45
|
+
|
|
46
|
+
sr = scan(root)
|
|
47
|
+
report = diagnose(sr)
|
|
48
|
+
reports.append(report)
|
|
49
|
+
|
|
50
|
+
if format == "json":
|
|
51
|
+
data = [_serialize_report(r) for r in reports]
|
|
52
|
+
output = data[0] if len(data) == 1 else data
|
|
53
|
+
print(json.dumps(output, indent=2, default=str))
|
|
54
|
+
else:
|
|
55
|
+
for i, report in enumerate(reports):
|
|
56
|
+
if i > 0:
|
|
57
|
+
print("\n" + "=" * 60 + "\n")
|
|
58
|
+
print(report)
|
|
59
|
+
if verbose:
|
|
60
|
+
sr = scan(report.project_root)
|
|
61
|
+
_print_verbose(sr)
|
|
62
|
+
|
|
63
|
+
worst = min(r.overall_score for r in reports)
|
|
64
|
+
sys.exit(0 if worst >= 80 else 1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_cmd(
|
|
68
|
+
*project_roots: str,
|
|
69
|
+
write: bool = False,
|
|
70
|
+
format: str = "text",
|
|
71
|
+
):
|
|
72
|
+
"""Generate missing AI setup artifacts for one or more projects.
|
|
73
|
+
|
|
74
|
+
By default, shows what would be created (dry run). Use --write to
|
|
75
|
+
actually write files. Existing files are never overwritten.
|
|
76
|
+
|
|
77
|
+
:param project_roots: one or more paths to project directories
|
|
78
|
+
:param write: actually write files (default: dry run)
|
|
79
|
+
:param format: output format — 'text' or 'json'
|
|
80
|
+
"""
|
|
81
|
+
if not project_roots:
|
|
82
|
+
project_roots = (".",)
|
|
83
|
+
|
|
84
|
+
all_files = []
|
|
85
|
+
for root_str in project_roots:
|
|
86
|
+
root = Path(root_str).resolve()
|
|
87
|
+
if not root.is_dir():
|
|
88
|
+
print(f"Error: {root_str} is not a directory", file=sys.stderr)
|
|
89
|
+
sys.exit(2)
|
|
90
|
+
|
|
91
|
+
sr = scan(root)
|
|
92
|
+
files = generate(sr)
|
|
93
|
+
all_files.extend(files)
|
|
94
|
+
|
|
95
|
+
if format == "json":
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Text output
|
|
99
|
+
if not files:
|
|
100
|
+
print(f"{root.name}: nothing to generate — all artifacts present")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
action = "Creating" if write else "Would create"
|
|
104
|
+
print(f"{root.name}: {len(files)} artifact(s)\n")
|
|
105
|
+
for gf in files:
|
|
106
|
+
rel = _relative_path(gf.target_path, root)
|
|
107
|
+
exists = gf.target_path.exists()
|
|
108
|
+
if exists:
|
|
109
|
+
print(f" SKIP {rel} (already exists)")
|
|
110
|
+
else:
|
|
111
|
+
print(f" {action} {rel}")
|
|
112
|
+
|
|
113
|
+
if write and not exists:
|
|
114
|
+
gf.target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
gf.target_path.write_text(gf.content, encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
if not write:
|
|
118
|
+
print(f"\nDry run — pass --write to create files.")
|
|
119
|
+
|
|
120
|
+
if format == "json":
|
|
121
|
+
data = [
|
|
122
|
+
{
|
|
123
|
+
"target_path": str(gf.target_path),
|
|
124
|
+
"exists": gf.target_path.exists(),
|
|
125
|
+
"overwrite_policy": gf.overwrite_policy,
|
|
126
|
+
"content_length": len(gf.content),
|
|
127
|
+
}
|
|
128
|
+
for gf in all_files
|
|
129
|
+
]
|
|
130
|
+
print(json.dumps(data, indent=2))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def maintain_cmd(
|
|
134
|
+
*project_roots: str,
|
|
135
|
+
format: str = "text",
|
|
136
|
+
):
|
|
137
|
+
"""Check for stale references, out-of-sync docs, and other drift.
|
|
138
|
+
|
|
139
|
+
:param project_roots: one or more paths to project directories
|
|
140
|
+
:param format: output format — 'text' or 'json'
|
|
141
|
+
"""
|
|
142
|
+
if not project_roots:
|
|
143
|
+
project_roots = (".",)
|
|
144
|
+
|
|
145
|
+
all_suggestions = []
|
|
146
|
+
for root_str in project_roots:
|
|
147
|
+
root = Path(root_str).resolve()
|
|
148
|
+
if not root.is_dir():
|
|
149
|
+
print(f"Error: {root_str} is not a directory", file=sys.stderr)
|
|
150
|
+
sys.exit(2)
|
|
151
|
+
|
|
152
|
+
sr = scan(root)
|
|
153
|
+
suggestions = maintain(sr)
|
|
154
|
+
all_suggestions.extend(suggestions)
|
|
155
|
+
|
|
156
|
+
if format == "json":
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if not suggestions:
|
|
160
|
+
print(f"{root.name}: no maintenance issues found")
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
print(f"{root.name}: {len(suggestions)} issue(s)\n")
|
|
164
|
+
for ms in suggestions:
|
|
165
|
+
print(f" [{ms.category}] {ms.description}")
|
|
166
|
+
if ms.diff:
|
|
167
|
+
for line in ms.diff.splitlines():
|
|
168
|
+
print(f" {line}")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
if format == "json":
|
|
172
|
+
data = [
|
|
173
|
+
{
|
|
174
|
+
"category": ms.category,
|
|
175
|
+
"description": ms.description,
|
|
176
|
+
"diff": ms.diff,
|
|
177
|
+
}
|
|
178
|
+
for ms in all_suggestions
|
|
179
|
+
]
|
|
180
|
+
print(json.dumps(data, indent=2))
|
|
181
|
+
|
|
182
|
+
sys.exit(0 if not all_suggestions else 1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _relative_path(path: Path, base: Path) -> str:
|
|
186
|
+
"""Return path relative to base, or absolute if not under base."""
|
|
187
|
+
try:
|
|
188
|
+
return str(path.relative_to(base))
|
|
189
|
+
except ValueError:
|
|
190
|
+
return str(path)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _print_verbose(sr):
|
|
194
|
+
"""Print extra scan details."""
|
|
195
|
+
print("\nDetailed inventory:")
|
|
196
|
+
print(f" Skills: {len(sr.skills)}")
|
|
197
|
+
for s in sr.skills:
|
|
198
|
+
print(f" - {s.name} (SKILL.md: {'yes' if s.has_skill_md else 'no'})")
|
|
199
|
+
print(f" Agents: {len(sr.agents)}")
|
|
200
|
+
for a in sr.agents:
|
|
201
|
+
print(f" - {a.name}")
|
|
202
|
+
print(f" Rules: {len(sr.rules)}")
|
|
203
|
+
for r in sr.rules:
|
|
204
|
+
print(f" - {r.name}")
|
|
205
|
+
print(f" Docs: {len(sr.docs)}")
|
|
206
|
+
for d in sr.docs:
|
|
207
|
+
print(f" - {d.name} ({d.size_bytes} bytes)")
|
|
208
|
+
print(f" Hooks: {'yes' if sr.hooks_config else 'no'}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_dispatch_funcs = [diagnose_cmd, generate_cmd, maintain_cmd]
|
opsward/data/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Conventions — ${project_name}
|
|
2
|
+
|
|
3
|
+
## Code Style
|
|
4
|
+
|
|
5
|
+
- **Formatter:** ${formatter}
|
|
6
|
+
- **Linter:** ${linter}
|
|
7
|
+
- **Package manager:** ${package_manager}
|
|
8
|
+
|
|
9
|
+
## Naming
|
|
10
|
+
|
|
11
|
+
- Files: `camelCase.ts` or `PascalCase.tsx` (components)
|
|
12
|
+
- Functions/variables: `camelCase`
|
|
13
|
+
- Classes/components: `PascalCase`
|
|
14
|
+
- Constants: `UPPER_SNAKE_CASE`
|
|
15
|
+
- Types/interfaces: `PascalCase`
|
|
16
|
+
|
|
17
|
+
## Imports
|
|
18
|
+
|
|
19
|
+
- Use named exports over default exports
|
|
20
|
+
- Group imports: external, then internal, then relative
|
|
21
|
+
|
|
22
|
+
## TypeScript
|
|
23
|
+
|
|
24
|
+
- Enable strict mode
|
|
25
|
+
- Prefer `interface` over `type` for object shapes
|
|
26
|
+
- Use `const` assertions where applicable
|
|
27
|
+
|
|
28
|
+
## Project-Specific Conventions
|
|
29
|
+
|
|
30
|
+
<!-- Add conventions specific to this project -->
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Testing — ${project_name}
|
|
2
|
+
|
|
3
|
+
## Test Framework
|
|
4
|
+
|
|
5
|
+
${test_framework}
|
|
6
|
+
|
|
7
|
+
## Running Tests
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
${test_command}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Test Structure
|
|
14
|
+
|
|
15
|
+
- Tests live alongside source files or in `${test_dir}/`
|
|
16
|
+
- Test files follow `*.test.ts` or `*.spec.ts` naming
|
|
17
|
+
|
|
18
|
+
## Coverage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
${coverage_command}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Writing Tests
|
|
25
|
+
|
|
26
|
+
- Each test block tests one behavior
|
|
27
|
+
- Use descriptive test names
|
|
28
|
+
- Prefer `describe` / `it` structure for grouping
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Conventions — ${project_name}
|
|
2
|
+
|
|
3
|
+
## Code Style
|
|
4
|
+
|
|
5
|
+
- **Formatter:** ${formatter}
|
|
6
|
+
- **Linter:** ${linter}
|
|
7
|
+
- **Line length:** ${line_length}
|
|
8
|
+
|
|
9
|
+
## Naming
|
|
10
|
+
|
|
11
|
+
- Modules: `snake_case`
|
|
12
|
+
- Classes: `PascalCase`
|
|
13
|
+
- Functions/variables: `snake_case`
|
|
14
|
+
- Constants: `UPPER_SNAKE_CASE`
|
|
15
|
+
|
|
16
|
+
## Imports
|
|
17
|
+
|
|
18
|
+
- Standard library first, then third-party, then local
|
|
19
|
+
- Prefer absolute imports
|
|
20
|
+
|
|
21
|
+
## Type Hints
|
|
22
|
+
|
|
23
|
+
- Use type hints for all public function signatures
|
|
24
|
+
- Prefer `collections.abc` types (`Mapping`, `Iterable`, `Sequence`) over concrete types
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
- Tests in `tests/` with `test_{module_name}.py` naming
|
|
29
|
+
- Use pytest fixtures for shared setup
|
|
30
|
+
|
|
31
|
+
## Project-Specific Conventions
|
|
32
|
+
|
|
33
|
+
<!-- Add conventions specific to this project -->
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Testing — ${project_name}
|
|
2
|
+
|
|
3
|
+
## Test Framework
|
|
4
|
+
|
|
5
|
+
${test_framework}
|
|
6
|
+
|
|
7
|
+
## Running Tests
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
${test_command}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Test Structure
|
|
14
|
+
|
|
15
|
+
- Tests live in `${test_dir}/`
|
|
16
|
+
- Test files follow `test_{module_name}.py` naming
|
|
17
|
+
- Fixtures and shared helpers in `${test_dir}/conftest.py`
|
|
18
|
+
|
|
19
|
+
## Coverage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
${coverage_command}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Writing Tests
|
|
26
|
+
|
|
27
|
+
- Each test function tests one behavior
|
|
28
|
+
- Use descriptive test names: `test_{what}_{condition}_{expected}`
|
|
29
|
+
- Prefer pytest fixtures over setUp/tearDown
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
${project_name} — ${project_description}
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
${tech_stack}
|
|
10
|
+
|
|
11
|
+
## Module Map
|
|
12
|
+
|
|
13
|
+
${module_map}
|
|
14
|
+
|
|
15
|
+
## Data Flow
|
|
16
|
+
|
|
17
|
+
<!-- Describe the primary data flow through the system -->
|
|
18
|
+
|
|
19
|
+
## Key Invariants
|
|
20
|
+
|
|
21
|
+
<!-- List any important invariants that must be maintained -->
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ${project_name}
|
|
2
|
+
|
|
3
|
+
${project_description}
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
${project_name} is a ${project_type} project.
|
|
8
|
+
|
|
9
|
+
## Tech Stack
|
|
10
|
+
|
|
11
|
+
${tech_stack}
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
For detailed project knowledge, see `${docs_path}/docs_guide.md`.
|
|
16
|
+
Read it to discover which docs to consult for your current task.
|
|
17
|
+
|
|
18
|
+
## Module Map
|
|
19
|
+
|
|
20
|
+
${module_map}
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
${commands}
|
|
25
|
+
|
|
26
|
+
## Conventions
|
|
27
|
+
|
|
28
|
+
${conventions}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# NNNN — Title of Decision
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Proposed | Accepted | Deprecated | Superseded by [NNNN](NNNN-title.md)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
<!-- What is the issue motivating this decision? -->
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
<!-- What is the change being proposed? -->
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
|
|
17
|
+
<!-- What are the positive and negative consequences of this decision? -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# External Dependencies — ${project_name}
|
|
2
|
+
|
|
3
|
+
Services, APIs, and system-level dependencies beyond the package manager.
|
|
4
|
+
|
|
5
|
+
## Environment Variables
|
|
6
|
+
|
|
7
|
+
| Variable | Purpose | Required |
|
|
8
|
+
|----------|---------|----------|
|
|
9
|
+
${env_vars_table}
|
|
10
|
+
|
|
11
|
+
## External Services
|
|
12
|
+
|
|
13
|
+
<!-- List any APIs, databases, or third-party services the project depends on -->
|
|
14
|
+
|
|
15
|
+
## System Dependencies
|
|
16
|
+
|
|
17
|
+
<!-- List any system packages, binaries, or tools required beyond the language runtime -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Deployment — ${project_name}
|
|
2
|
+
|
|
3
|
+
## Environments
|
|
4
|
+
|
|
5
|
+
<!-- List deployment environments (dev, staging, production) -->
|
|
6
|
+
|
|
7
|
+
## How to Deploy
|
|
8
|
+
|
|
9
|
+
<!-- Step-by-step deployment instructions -->
|
|
10
|
+
|
|
11
|
+
## CI/CD Pipeline
|
|
12
|
+
|
|
13
|
+
<!-- Describe the continuous integration and deployment pipeline -->
|
|
14
|
+
|
|
15
|
+
## Rollback Procedure
|
|
16
|
+
|
|
17
|
+
<!-- How to roll back a bad deployment -->
|