ref-agents 1.0.0__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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""REF license client — validates API keys against ref-license backend.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- Key validation via POST /validate (with in-memory + file cache)
|
|
5
|
+
- Paid asset fetching: GET /prompt/:name, /rule/:path, /config/:path
|
|
6
|
+
- Graceful degrade: network error → cached status → free tier (never hard-fails)
|
|
7
|
+
|
|
8
|
+
Environment:
|
|
9
|
+
REF_API_KEY Single API key (sk_ref_...)
|
|
10
|
+
REF_LICENSE_URL Override default server URL (optional)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
import structlog
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
# ── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
REF_LICENSE_URL: str = os.environ.get(
|
|
31
|
+
"REF_LICENSE_URL", "https://ref-license.workers.dev"
|
|
32
|
+
).rstrip("/")
|
|
33
|
+
|
|
34
|
+
CACHE_TTL_SECONDS: int = 14_400 # 4 hrs — revalidate on next boot after this
|
|
35
|
+
OFFLINE_GRACE_SECONDS: int = (
|
|
36
|
+
86_400 # 24 hrs — serve cached status if server unreachable
|
|
37
|
+
)
|
|
38
|
+
REQUEST_TIMEOUT_SECONDS: int = 5
|
|
39
|
+
|
|
40
|
+
_CACHE_FILE = Path.home() / ".ref_cache" / "license.json"
|
|
41
|
+
|
|
42
|
+
Plan = Literal["free", "paid"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── Data classes ─────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class LicenseStatus:
|
|
50
|
+
"""Result of a license validation check."""
|
|
51
|
+
|
|
52
|
+
valid: bool
|
|
53
|
+
plan: Plan
|
|
54
|
+
reason: str = ""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_paid(self) -> bool:
|
|
58
|
+
"""Return True if the active plan is paid."""
|
|
59
|
+
return self.valid and self.plan == "paid"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_FREE = LicenseStatus(valid=False, plan="free", reason="no_key")
|
|
63
|
+
_FREE_DEGRADED = LicenseStatus(valid=False, plan="free", reason="server_unreachable")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── In-process cache ─────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
_mem_status: LicenseStatus | None = None
|
|
69
|
+
_mem_fetched_at: float = 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_file_cache() -> tuple[LicenseStatus, float] | None:
|
|
73
|
+
"""Read license status from disk cache.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
(status, fetched_at) tuple, or None if cache missing/corrupt.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
if not _CACHE_FILE.exists():
|
|
80
|
+
return None
|
|
81
|
+
data = json.loads(_CACHE_FILE.read_text())
|
|
82
|
+
status = LicenseStatus(
|
|
83
|
+
valid=bool(data["valid"]),
|
|
84
|
+
plan=data["plan"],
|
|
85
|
+
reason=data.get("reason", ""),
|
|
86
|
+
)
|
|
87
|
+
return status, float(data["fetched_at"])
|
|
88
|
+
except (KeyError, json.JSONDecodeError, OSError):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _write_file_cache(status: LicenseStatus) -> None:
|
|
93
|
+
"""Persist license status to disk for offline-grace support."""
|
|
94
|
+
try:
|
|
95
|
+
_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
_CACHE_FILE.write_text(
|
|
97
|
+
json.dumps(
|
|
98
|
+
{
|
|
99
|
+
"valid": status.valid,
|
|
100
|
+
"plan": status.plan,
|
|
101
|
+
"reason": status.reason,
|
|
102
|
+
"fetched_at": time.time(),
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
except OSError as e:
|
|
107
|
+
logger.warning("license_cache_write_failed", error=str(e))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ── Network validation ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _fetch_from_server(api_key: str) -> LicenseStatus | None:
|
|
114
|
+
"""Call POST /validate. Returns None on any network/parse error.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
api_key: The REF_API_KEY value.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
LicenseStatus on success, None on failure.
|
|
121
|
+
"""
|
|
122
|
+
url = f"{REF_LICENSE_URL}/validate"
|
|
123
|
+
payload = json.dumps({"key": api_key}).encode()
|
|
124
|
+
req = urllib.request.Request(
|
|
125
|
+
url,
|
|
126
|
+
data=payload,
|
|
127
|
+
headers={
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
"Authorization": f"Bearer {api_key}",
|
|
130
|
+
},
|
|
131
|
+
method="POST",
|
|
132
|
+
)
|
|
133
|
+
try:
|
|
134
|
+
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
|
|
135
|
+
body = json.loads(resp.read())
|
|
136
|
+
valid = bool(body.get("valid", False))
|
|
137
|
+
plan: Plan = "paid" if body.get("plan") == "paid" else "free"
|
|
138
|
+
reason: str = body.get("reason", "")
|
|
139
|
+
return LicenseStatus(valid=valid, plan=plan, reason=reason)
|
|
140
|
+
except urllib.error.HTTPError as e:
|
|
141
|
+
logger.warning("license_validate_http_error", status=e.code)
|
|
142
|
+
return None
|
|
143
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e:
|
|
144
|
+
logger.warning("license_validate_network_error", error=str(e))
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_status() -> LicenseStatus:
|
|
152
|
+
"""Return current license status.
|
|
153
|
+
|
|
154
|
+
Resolution order:
|
|
155
|
+
1. In-memory cache (TTL=4hrs)
|
|
156
|
+
2. Network validate → update both caches
|
|
157
|
+
3. File cache within offline grace period (24hrs)
|
|
158
|
+
4. Free tier (never hard-fails)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
LicenseStatus with valid/plan fields.
|
|
162
|
+
"""
|
|
163
|
+
global _mem_status, _mem_fetched_at
|
|
164
|
+
|
|
165
|
+
api_key = os.environ.get("REF_API_KEY", "").strip()
|
|
166
|
+
if not api_key:
|
|
167
|
+
return _FREE
|
|
168
|
+
|
|
169
|
+
now = time.time()
|
|
170
|
+
|
|
171
|
+
# 1. In-memory cache still fresh
|
|
172
|
+
if _mem_status is not None and (now - _mem_fetched_at) < CACHE_TTL_SECONDS:
|
|
173
|
+
return _mem_status
|
|
174
|
+
|
|
175
|
+
# 2. Fetch from server
|
|
176
|
+
fetched = _fetch_from_server(api_key)
|
|
177
|
+
if fetched is not None:
|
|
178
|
+
_mem_status = fetched
|
|
179
|
+
_mem_fetched_at = now
|
|
180
|
+
_write_file_cache(fetched)
|
|
181
|
+
logger.info("license_validated", plan=fetched.plan, valid=fetched.valid)
|
|
182
|
+
return fetched
|
|
183
|
+
|
|
184
|
+
# 3. File cache within offline grace
|
|
185
|
+
cached = _load_file_cache()
|
|
186
|
+
if cached is not None:
|
|
187
|
+
status, fetched_at = cached
|
|
188
|
+
if (now - fetched_at) < OFFLINE_GRACE_SECONDS:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"license_using_cached_offline", age_hours=(now - fetched_at) / 3600
|
|
191
|
+
)
|
|
192
|
+
_mem_status = status
|
|
193
|
+
_mem_fetched_at = now # don't hammer server on every call
|
|
194
|
+
return status
|
|
195
|
+
|
|
196
|
+
# 4. Degrade to free — never hard-fail
|
|
197
|
+
logger.warning("license_degraded_to_free", reason="no_valid_cache")
|
|
198
|
+
return _FREE_DEGRADED
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def fetch_asset(
|
|
202
|
+
asset_type: Literal["prompt", "rule", "config"], path: str
|
|
203
|
+
) -> str | None:
|
|
204
|
+
"""Fetch a paid asset from the license server.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
asset_type: One of "prompt", "rule", "config".
|
|
208
|
+
path: Asset path (e.g. "developer", "architecture/system_design", "languages/python").
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Asset text content, or None on error/not found.
|
|
212
|
+
"""
|
|
213
|
+
api_key = os.environ.get("REF_API_KEY", "").strip()
|
|
214
|
+
if not api_key:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
url = f"{REF_LICENSE_URL}/{asset_type}/{path}"
|
|
218
|
+
req = urllib.request.Request(
|
|
219
|
+
url,
|
|
220
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
221
|
+
method="GET",
|
|
222
|
+
)
|
|
223
|
+
try:
|
|
224
|
+
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
|
|
225
|
+
return resp.read().decode("utf-8")
|
|
226
|
+
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as e:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"asset_fetch_failed", asset_type=asset_type, path=path, error=str(e)
|
|
229
|
+
)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def upgrade_message(feature: str = "agent prompts") -> str:
|
|
234
|
+
"""Return a clear upgrade prompt for free-tier users.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
feature: Name of the gated feature.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Human-readable upgrade message.
|
|
241
|
+
"""
|
|
242
|
+
return (
|
|
243
|
+
f"⚠️ **REF Pro required** — {feature} require an active REF subscription.\n\n"
|
|
244
|
+
"Subscribe at **https://ref.dev** then set:\n"
|
|
245
|
+
"```\nexport REF_API_KEY=sk_ref_...\n```\n"
|
|
246
|
+
"Free tier: all scanners (`scan`, `log`, `workflow`, `validate`) remain available."
|
|
247
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""REF-Agents data models.
|
|
2
|
+
|
|
3
|
+
Provides structured data models for requirements, INVEST scoring, Gherkin,
|
|
4
|
+
hierarchy, and version management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ref_agents.models.gherkin import GherkinScenario, GherkinStep
|
|
8
|
+
from ref_agents.models.hierarchy import Epic, Feature, Story
|
|
9
|
+
from ref_agents.models.invest import INVESTScores, INVESTPrinciple
|
|
10
|
+
from ref_agents.models.version import RequirementVersion, VersionStatus
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"GherkinStep",
|
|
14
|
+
"GherkinScenario",
|
|
15
|
+
"INVESTPrinciple",
|
|
16
|
+
"INVESTScores",
|
|
17
|
+
"Epic",
|
|
18
|
+
"Feature",
|
|
19
|
+
"Story",
|
|
20
|
+
"RequirementVersion",
|
|
21
|
+
"VersionStatus",
|
|
22
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Gherkin/BDD format models for structured acceptance criteria."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GherkinStep(BaseModel):
|
|
9
|
+
"""Single Gherkin step (Given/When/Then/And/But)."""
|
|
10
|
+
|
|
11
|
+
keyword: Literal["Given", "When", "Then", "And", "But"] = Field(
|
|
12
|
+
description="Gherkin keyword"
|
|
13
|
+
)
|
|
14
|
+
text: str = Field(description="Step text content")
|
|
15
|
+
|
|
16
|
+
def to_markdown(self) -> str:
|
|
17
|
+
"""Convert step to markdown format."""
|
|
18
|
+
return f"**{self.keyword}** {self.text}"
|
|
19
|
+
|
|
20
|
+
def to_gherkin(self) -> str:
|
|
21
|
+
"""Convert step to Gherkin text format."""
|
|
22
|
+
return f"{self.keyword} {self.text}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GherkinScenario(BaseModel):
|
|
26
|
+
"""Gherkin scenario with multiple steps."""
|
|
27
|
+
|
|
28
|
+
scenario: str = Field(description="Scenario title/description")
|
|
29
|
+
steps: list[GherkinStep] = Field(
|
|
30
|
+
default_factory=list, description="Ordered list of Gherkin steps"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def to_markdown(self) -> str:
|
|
34
|
+
"""Convert scenario to markdown format."""
|
|
35
|
+
lines = [f"### {self.scenario}", ""]
|
|
36
|
+
for step in self.steps:
|
|
37
|
+
lines.append(step.to_markdown())
|
|
38
|
+
return "\n".join(lines)
|
|
39
|
+
|
|
40
|
+
def to_gherkin(self) -> str:
|
|
41
|
+
"""Convert scenario to Gherkin text format."""
|
|
42
|
+
lines = [f"Scenario: {self.scenario}", ""]
|
|
43
|
+
for step in self.steps:
|
|
44
|
+
lines.append(step.to_gherkin())
|
|
45
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Hierarchy models for Epic → Feature → Story structure."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Epic(BaseModel):
|
|
9
|
+
"""Epic model (top-level requirement grouping)."""
|
|
10
|
+
|
|
11
|
+
epic_id: str = Field(description="Unique epic identifier (e.g., EPIC-001)")
|
|
12
|
+
title: str = Field(description="Epic title")
|
|
13
|
+
description: str = Field(default="", description="Epic description")
|
|
14
|
+
status: str = Field(default="Draft", description="Epic status")
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> dict:
|
|
17
|
+
"""Convert epic to dictionary."""
|
|
18
|
+
return {
|
|
19
|
+
"epic_id": self.epic_id,
|
|
20
|
+
"title": self.title,
|
|
21
|
+
"description": self.description,
|
|
22
|
+
"status": self.status,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Feature(BaseModel):
|
|
27
|
+
"""Feature model (mid-level requirement grouping under Epic)."""
|
|
28
|
+
|
|
29
|
+
feature_id: str = Field(description="Unique feature identifier (e.g., FEAT-001)")
|
|
30
|
+
epic_id: str = Field(description="Parent epic identifier")
|
|
31
|
+
title: str = Field(description="Feature title")
|
|
32
|
+
description: str = Field(default="", description="Feature description")
|
|
33
|
+
status: str = Field(default="Draft", description="Feature status")
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""Convert feature to dictionary."""
|
|
37
|
+
return {
|
|
38
|
+
"feature_id": self.feature_id,
|
|
39
|
+
"epic_id": self.epic_id,
|
|
40
|
+
"title": self.title,
|
|
41
|
+
"description": self.description,
|
|
42
|
+
"status": self.status,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Story(BaseModel):
|
|
47
|
+
"""Story model (low-level requirement, can be flat or hierarchical)."""
|
|
48
|
+
|
|
49
|
+
story_id: str = Field(description="Unique story identifier (e.g., STORY-001)")
|
|
50
|
+
title: str = Field(description="Story title")
|
|
51
|
+
description: str = Field(default="", description="Story description")
|
|
52
|
+
user_story: str = Field(
|
|
53
|
+
default="", description="User story format: As a... I want... So that..."
|
|
54
|
+
)
|
|
55
|
+
epic_id: Optional[str] = Field(
|
|
56
|
+
default=None, description="Parent epic identifier (optional for flat stories)"
|
|
57
|
+
)
|
|
58
|
+
feature_id: Optional[str] = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="Parent feature identifier (optional for flat stories)",
|
|
61
|
+
)
|
|
62
|
+
status: str = Field(default="Draft", description="Story status")
|
|
63
|
+
priority: str = Field(default="P2", description="Story priority (P1/P2/P3)")
|
|
64
|
+
|
|
65
|
+
def is_flat(self) -> bool:
|
|
66
|
+
"""Check if story is flat (no epic/feature parent)."""
|
|
67
|
+
return self.epic_id is None and self.feature_id is None
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
"""Convert story to dictionary."""
|
|
71
|
+
return {
|
|
72
|
+
"story_id": self.story_id,
|
|
73
|
+
"title": self.title,
|
|
74
|
+
"description": self.description,
|
|
75
|
+
"user_story": self.user_story,
|
|
76
|
+
"epic_id": self.epic_id,
|
|
77
|
+
"feature_id": self.feature_id,
|
|
78
|
+
"status": self.status,
|
|
79
|
+
"priority": self.priority,
|
|
80
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""INVEST scoring models for user story quality assessment.
|
|
2
|
+
|
|
3
|
+
INVEST principles: Independent, Negotiable, Valuable, Estimable, Small, Testable.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class INVESTPrinciple(str, Enum):
|
|
12
|
+
"""INVEST principle enumeration."""
|
|
13
|
+
|
|
14
|
+
INDEPENDENT = "Independent"
|
|
15
|
+
NEGOTIABLE = "Negotiable"
|
|
16
|
+
VALUABLE = "Valuable"
|
|
17
|
+
ESTIMABLE = "Estimable"
|
|
18
|
+
SMALL = "Small"
|
|
19
|
+
TESTABLE = "Testable"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class INVESTScores(BaseModel):
|
|
23
|
+
"""INVEST scores for a user story.
|
|
24
|
+
|
|
25
|
+
Each principle scored 0-5:
|
|
26
|
+
- 0: Not met
|
|
27
|
+
- 1-2: Poor
|
|
28
|
+
- 3: Acceptable
|
|
29
|
+
- 4-5: Excellent
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
independent: int = Field(ge=0, le=5, description="Independent score (0-5)")
|
|
33
|
+
negotiable: int = Field(ge=0, le=5, description="Negotiable score (0-5)")
|
|
34
|
+
valuable: int = Field(ge=0, le=5, description="Valuable score (0-5)")
|
|
35
|
+
estimable: int = Field(ge=0, le=5, description="Estimable score (0-5)")
|
|
36
|
+
small: int = Field(ge=0, le=5, description="Small score (0-5)")
|
|
37
|
+
testable: int = Field(ge=0, le=5, description="Testable score (0-5)")
|
|
38
|
+
|
|
39
|
+
def overall_score(self) -> float:
|
|
40
|
+
"""Calculate overall INVEST score (average of all principles)."""
|
|
41
|
+
return (
|
|
42
|
+
self.independent
|
|
43
|
+
+ self.negotiable
|
|
44
|
+
+ self.valuable
|
|
45
|
+
+ self.estimable
|
|
46
|
+
+ self.small
|
|
47
|
+
+ self.testable
|
|
48
|
+
) / 6.0
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, int]:
|
|
51
|
+
"""Convert scores to dictionary."""
|
|
52
|
+
return {
|
|
53
|
+
"independent": self.independent,
|
|
54
|
+
"negotiable": self.negotiable,
|
|
55
|
+
"valuable": self.valuable,
|
|
56
|
+
"estimable": self.estimable,
|
|
57
|
+
"small": self.small,
|
|
58
|
+
"testable": self.testable,
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Version management models for requirement tracking."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VersionStatus(str, Enum):
|
|
11
|
+
"""Requirement version status."""
|
|
12
|
+
|
|
13
|
+
CURRENT = "Current"
|
|
14
|
+
SUPERSEDED = "Superseded"
|
|
15
|
+
DRAFT = "Draft"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequirementVersion(BaseModel):
|
|
19
|
+
"""Version metadata for a requirement artifact."""
|
|
20
|
+
|
|
21
|
+
artifact_id: str = Field(
|
|
22
|
+
description="Artifact identifier (STORY-XXX, EPIC-XXX, etc.)"
|
|
23
|
+
)
|
|
24
|
+
version: str = Field(description="Version identifier (semantic or git commit hash)")
|
|
25
|
+
status: VersionStatus = Field(
|
|
26
|
+
default=VersionStatus.CURRENT, description="Version status"
|
|
27
|
+
)
|
|
28
|
+
created_at: datetime = Field(
|
|
29
|
+
default_factory=datetime.now, description="Version creation timestamp"
|
|
30
|
+
)
|
|
31
|
+
modified_at: Optional[datetime] = Field(
|
|
32
|
+
default=None, description="Last modification timestamp"
|
|
33
|
+
)
|
|
34
|
+
git_commit: Optional[str] = Field(default=None, description="Git commit hash")
|
|
35
|
+
modified_by: Optional[str] = Field(
|
|
36
|
+
default=None, description="Last modifier (git user email)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
"""Convert version to dictionary."""
|
|
41
|
+
return {
|
|
42
|
+
"artifact_id": self.artifact_id,
|
|
43
|
+
"version": self.version,
|
|
44
|
+
"status": self.status.value,
|
|
45
|
+
"created_at": self.created_at.isoformat(),
|
|
46
|
+
"modified_at": self.modified_at.isoformat() if self.modified_at else None,
|
|
47
|
+
"git_commit": self.git_commit,
|
|
48
|
+
"modified_by": self.modified_by,
|
|
49
|
+
}
|