specfact-cli 0.4.2__py3-none-any.whl → 0.6.8__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.
- specfact_cli/__init__.py +1 -1
- specfact_cli/agents/analyze_agent.py +2 -3
- specfact_cli/analyzers/__init__.py +2 -1
- specfact_cli/analyzers/ambiguity_scanner.py +601 -0
- specfact_cli/analyzers/code_analyzer.py +462 -30
- specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
- specfact_cli/analyzers/contract_extractor.py +419 -0
- specfact_cli/analyzers/control_flow_analyzer.py +281 -0
- specfact_cli/analyzers/requirement_extractor.py +337 -0
- specfact_cli/analyzers/test_pattern_extractor.py +330 -0
- specfact_cli/cli.py +151 -206
- specfact_cli/commands/constitution.py +281 -0
- specfact_cli/commands/enforce.py +42 -34
- specfact_cli/commands/import_cmd.py +481 -152
- specfact_cli/commands/init.py +224 -55
- specfact_cli/commands/plan.py +2133 -547
- specfact_cli/commands/repro.py +100 -78
- specfact_cli/commands/sync.py +701 -186
- specfact_cli/enrichers/constitution_enricher.py +765 -0
- specfact_cli/enrichers/plan_enricher.py +294 -0
- specfact_cli/importers/speckit_converter.py +364 -48
- specfact_cli/importers/speckit_scanner.py +65 -0
- specfact_cli/models/plan.py +42 -0
- specfact_cli/resources/mappings/node-async.yaml +49 -0
- specfact_cli/resources/mappings/python-async.yaml +47 -0
- specfact_cli/resources/mappings/speckit-default.yaml +82 -0
- specfact_cli/resources/prompts/specfact-enforce.md +185 -0
- specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
- specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
- specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
- specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
- specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
- specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
- specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
- specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
- specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
- specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
- specfact_cli/resources/prompts/specfact-repro.md +268 -0
- specfact_cli/resources/prompts/specfact-sync.md +497 -0
- specfact_cli/resources/schemas/deviation.schema.json +61 -0
- specfact_cli/resources/schemas/plan.schema.json +204 -0
- specfact_cli/resources/schemas/protocol.schema.json +53 -0
- specfact_cli/resources/templates/github-action.yml.j2 +140 -0
- specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
- specfact_cli/resources/templates/pr-template.md.j2 +58 -0
- specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
- specfact_cli/resources/templates/telemetry.yaml.example +35 -0
- specfact_cli/sync/__init__.py +10 -1
- specfact_cli/sync/watcher.py +268 -0
- specfact_cli/telemetry.py +440 -0
- specfact_cli/utils/acceptance_criteria.py +127 -0
- specfact_cli/utils/enrichment_parser.py +445 -0
- specfact_cli/utils/feature_keys.py +12 -3
- specfact_cli/utils/ide_setup.py +170 -0
- specfact_cli/utils/structure.py +179 -2
- specfact_cli/utils/yaml_utils.py +33 -0
- specfact_cli/validators/repro_checker.py +22 -1
- specfact_cli/validators/schema.py +15 -4
- specfact_cli-0.6.8.dist-info/METADATA +456 -0
- specfact_cli-0.6.8.dist-info/RECORD +99 -0
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
- specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
- specfact_cli-0.4.2.dist-info/METADATA +0 -370
- specfact_cli-0.4.2.dist-info/RECORD +0 -62
- specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plan bundle enricher for automatic enhancement of vague acceptance criteria,
|
|
3
|
+
incomplete requirements, and generic tasks.
|
|
4
|
+
|
|
5
|
+
This module provides automatic enrichment capabilities that can be triggered
|
|
6
|
+
during plan review to improve plan quality without manual intervention.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from beartype import beartype
|
|
14
|
+
from icontract import ensure, require
|
|
15
|
+
|
|
16
|
+
from specfact_cli.models.plan import PlanBundle
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlanEnricher:
|
|
20
|
+
"""
|
|
21
|
+
Enricher for automatically enhancing plan bundles.
|
|
22
|
+
|
|
23
|
+
Detects and fixes vague acceptance criteria, incomplete requirements,
|
|
24
|
+
and generic tasks using pattern-based improvements.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@beartype
|
|
28
|
+
@require(lambda plan_bundle: isinstance(plan_bundle, PlanBundle), "Plan bundle must be PlanBundle")
|
|
29
|
+
@ensure(lambda result: isinstance(result, dict), "Must return dict with enrichment summary")
|
|
30
|
+
def enrich_plan(self, plan_bundle: PlanBundle) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Enrich plan bundle by enhancing vague acceptance criteria, incomplete requirements, and generic tasks.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
plan_bundle: Plan bundle to enrich
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary with enrichment summary (features_updated, stories_updated, tasks_updated, etc.)
|
|
39
|
+
"""
|
|
40
|
+
summary: dict[str, Any] = {
|
|
41
|
+
"features_updated": 0,
|
|
42
|
+
"stories_updated": 0,
|
|
43
|
+
"acceptance_criteria_enhanced": 0,
|
|
44
|
+
"requirements_enhanced": 0,
|
|
45
|
+
"tasks_enhanced": 0,
|
|
46
|
+
"changes": [],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for feature in plan_bundle.features:
|
|
50
|
+
feature_updated = False
|
|
51
|
+
|
|
52
|
+
# Enhance incomplete requirements in outcomes
|
|
53
|
+
enhanced_outcomes = []
|
|
54
|
+
for outcome in feature.outcomes:
|
|
55
|
+
enhanced = self._enhance_incomplete_requirement(outcome, feature.title)
|
|
56
|
+
if enhanced != outcome:
|
|
57
|
+
enhanced_outcomes.append(enhanced)
|
|
58
|
+
summary["requirements_enhanced"] += 1
|
|
59
|
+
summary["changes"].append(f"Feature {feature.key}: Enhanced requirement '{outcome}' → '{enhanced}'")
|
|
60
|
+
feature_updated = True
|
|
61
|
+
else:
|
|
62
|
+
enhanced_outcomes.append(outcome)
|
|
63
|
+
|
|
64
|
+
if feature_updated:
|
|
65
|
+
feature.outcomes = enhanced_outcomes
|
|
66
|
+
summary["features_updated"] += 1
|
|
67
|
+
|
|
68
|
+
# Enhance stories
|
|
69
|
+
for story in feature.stories:
|
|
70
|
+
story_updated = False
|
|
71
|
+
|
|
72
|
+
# Enhance vague acceptance criteria
|
|
73
|
+
enhanced_acceptance = []
|
|
74
|
+
for acc in story.acceptance:
|
|
75
|
+
enhanced = self._enhance_vague_acceptance_criteria(acc, story.title, feature.title)
|
|
76
|
+
if enhanced != acc:
|
|
77
|
+
enhanced_acceptance.append(enhanced)
|
|
78
|
+
summary["acceptance_criteria_enhanced"] += 1
|
|
79
|
+
summary["changes"].append(
|
|
80
|
+
f"Story {story.key}: Enhanced acceptance criteria '{acc}' → '{enhanced}'"
|
|
81
|
+
)
|
|
82
|
+
story_updated = True
|
|
83
|
+
else:
|
|
84
|
+
enhanced_acceptance.append(acc)
|
|
85
|
+
|
|
86
|
+
if story_updated:
|
|
87
|
+
story.acceptance = enhanced_acceptance
|
|
88
|
+
summary["stories_updated"] += 1
|
|
89
|
+
|
|
90
|
+
# Enhance generic tasks
|
|
91
|
+
if story.tasks:
|
|
92
|
+
enhanced_tasks = []
|
|
93
|
+
for task in story.tasks:
|
|
94
|
+
enhanced = self._enhance_generic_task(task, story.title, feature.title)
|
|
95
|
+
if enhanced != task:
|
|
96
|
+
enhanced_tasks.append(enhanced)
|
|
97
|
+
summary["tasks_enhanced"] += 1
|
|
98
|
+
summary["changes"].append(f"Story {story.key}: Enhanced task '{task}' → '{enhanced}'")
|
|
99
|
+
story_updated = True
|
|
100
|
+
else:
|
|
101
|
+
enhanced_tasks.append(task)
|
|
102
|
+
|
|
103
|
+
if story_updated and enhanced_tasks:
|
|
104
|
+
story.tasks = enhanced_tasks
|
|
105
|
+
|
|
106
|
+
return summary
|
|
107
|
+
|
|
108
|
+
@beartype
|
|
109
|
+
@require(lambda requirement: isinstance(requirement, str), "Requirement must be string")
|
|
110
|
+
@require(lambda feature_title: isinstance(feature_title, str), "Feature title must be string")
|
|
111
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
112
|
+
@ensure(lambda result: len(result) > 0, "Result must be non-empty")
|
|
113
|
+
def _enhance_incomplete_requirement(self, requirement: str, feature_title: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Enhance incomplete requirement (e.g., "System MUST Helper class" → "System MUST provide a Helper class").
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
requirement: Requirement text to enhance
|
|
119
|
+
feature_title: Feature title for context
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Enhanced requirement text
|
|
123
|
+
"""
|
|
124
|
+
requirement_lower = requirement.lower()
|
|
125
|
+
incomplete_patterns = [
|
|
126
|
+
("system must", "System MUST provide"),
|
|
127
|
+
("system should", "System SHOULD provide"),
|
|
128
|
+
("must", "MUST provide"),
|
|
129
|
+
("should", "SHOULD provide"),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for pattern, replacement_prefix in incomplete_patterns:
|
|
133
|
+
if requirement_lower.startswith(pattern):
|
|
134
|
+
remaining = requirement[len(pattern) :].strip()
|
|
135
|
+
# Check if it's incomplete (just a noun phrase without verb)
|
|
136
|
+
if (
|
|
137
|
+
remaining
|
|
138
|
+
and len(remaining.split()) < 3
|
|
139
|
+
and any(
|
|
140
|
+
keyword in remaining.lower()
|
|
141
|
+
for keyword in ["class", "helper", "module", "component", "service", "function"]
|
|
142
|
+
)
|
|
143
|
+
):
|
|
144
|
+
# Enhance: "System MUST Helper class" → "System MUST provide a Helper class for [feature]"
|
|
145
|
+
# Extract the component name
|
|
146
|
+
component_name = remaining.strip()
|
|
147
|
+
# Capitalize first letter if needed
|
|
148
|
+
if component_name and component_name[0].islower():
|
|
149
|
+
component_name = component_name[0].upper() + component_name[1:]
|
|
150
|
+
# Generate enhanced requirement
|
|
151
|
+
if "class" in component_name.lower():
|
|
152
|
+
return f"{replacement_prefix} a {component_name} for {feature_title.lower()} operations"
|
|
153
|
+
if "helper" in component_name.lower():
|
|
154
|
+
return f"{replacement_prefix} a {component_name} class for {feature_title.lower()} operations"
|
|
155
|
+
return f"{replacement_prefix} a {component_name} for {feature_title.lower()}"
|
|
156
|
+
|
|
157
|
+
return requirement
|
|
158
|
+
|
|
159
|
+
@beartype
|
|
160
|
+
@require(lambda acceptance: isinstance(acceptance, str), "Acceptance must be string")
|
|
161
|
+
@ensure(lambda result: isinstance(result, bool), "Must return bool")
|
|
162
|
+
def _is_code_specific_criteria(self, acceptance: str) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Check if acceptance criteria are already code-specific (should not be replaced).
|
|
165
|
+
|
|
166
|
+
Delegates to shared utility function for consistency.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
acceptance: Acceptance criteria text to check
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if criteria are code-specific, False if vague/generic
|
|
173
|
+
"""
|
|
174
|
+
from specfact_cli.utils.acceptance_criteria import is_code_specific_criteria
|
|
175
|
+
|
|
176
|
+
return is_code_specific_criteria(acceptance)
|
|
177
|
+
|
|
178
|
+
@beartype
|
|
179
|
+
@require(lambda acceptance: isinstance(acceptance, str), "Acceptance must be string")
|
|
180
|
+
@require(lambda story_title: isinstance(story_title, str), "Story title must be string")
|
|
181
|
+
@require(lambda feature_title: isinstance(feature_title, str), "Feature title must be string")
|
|
182
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
183
|
+
@ensure(lambda result: len(result) > 0, "Result must be non-empty")
|
|
184
|
+
def _enhance_vague_acceptance_criteria(self, acceptance: str, story_title: str, feature_title: str) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Enhance vague acceptance criteria (e.g., "is implemented" → "Given [state], When [action], Then [outcome]").
|
|
187
|
+
|
|
188
|
+
This method only enhances vague/generic criteria. Code-specific criteria (containing method names,
|
|
189
|
+
class names, file paths, type hints) are preserved unchanged.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
acceptance: Acceptance criteria text to enhance
|
|
193
|
+
story_title: Story title for context
|
|
194
|
+
feature_title: Feature title for context
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Enhanced acceptance criteria in Given/When/Then format, or original if already code-specific
|
|
198
|
+
"""
|
|
199
|
+
# Skip enrichment if criteria are already code-specific
|
|
200
|
+
if self._is_code_specific_criteria(acceptance):
|
|
201
|
+
return acceptance
|
|
202
|
+
|
|
203
|
+
acceptance_lower = acceptance.lower()
|
|
204
|
+
vague_patterns = [
|
|
205
|
+
(
|
|
206
|
+
"is implemented",
|
|
207
|
+
"Given a developer wants to use {story}, When they interact with the system, Then {story} is functional and verified",
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
"is functional",
|
|
211
|
+
"Given a user wants to use {story}, When they perform the action, Then {story} works as expected",
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
"works",
|
|
215
|
+
"Given a user wants to use {story}, When they interact with the system, Then {story} works correctly",
|
|
216
|
+
),
|
|
217
|
+
(
|
|
218
|
+
"is done",
|
|
219
|
+
"Given a user wants to complete {story}, When they perform the action, Then {story} is completed successfully",
|
|
220
|
+
),
|
|
221
|
+
(
|
|
222
|
+
"is complete",
|
|
223
|
+
"Given a user wants to complete {story}, When they perform the action, Then {story} is completed successfully",
|
|
224
|
+
),
|
|
225
|
+
(
|
|
226
|
+
"is ready",
|
|
227
|
+
"Given a user wants to use {story}, When they access the system, Then {story} is ready and available",
|
|
228
|
+
),
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for pattern, template in vague_patterns:
|
|
232
|
+
if pattern in acceptance_lower:
|
|
233
|
+
# Replace placeholder with story title
|
|
234
|
+
return template.format(story=story_title.lower())
|
|
235
|
+
|
|
236
|
+
# If no vague pattern found, check if it's already in Given/When/Then format
|
|
237
|
+
if "given" in acceptance_lower and "when" in acceptance_lower and "then" in acceptance_lower:
|
|
238
|
+
return acceptance
|
|
239
|
+
|
|
240
|
+
# If it's a simple statement without testable keywords, enhance it
|
|
241
|
+
testable_keywords = ["must", "should", "will", "verify", "validate", "check", "ensure"]
|
|
242
|
+
if not any(keyword in acceptance_lower for keyword in testable_keywords):
|
|
243
|
+
# Convert to testable format
|
|
244
|
+
if acceptance_lower.startswith(("user can", "system can")):
|
|
245
|
+
return f"Must verify {acceptance.lower()}"
|
|
246
|
+
# Generate Given/When/Then from simple statement
|
|
247
|
+
return f"Given a user wants to use {story_title.lower()}, When they perform the action, Then {acceptance}"
|
|
248
|
+
|
|
249
|
+
return acceptance
|
|
250
|
+
|
|
251
|
+
@beartype
|
|
252
|
+
@require(lambda task: isinstance(task, str), "Task must be string")
|
|
253
|
+
@require(lambda story_title: isinstance(story_title, str), "Story title must be string")
|
|
254
|
+
@require(lambda feature_title: isinstance(feature_title, str), "Feature title must be string")
|
|
255
|
+
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
256
|
+
@ensure(lambda result: len(result) > 0, "Result must be non-empty")
|
|
257
|
+
def _enhance_generic_task(self, task: str, story_title: str, feature_title: str) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Enhance generic task (e.g., "Implement [story]" → "Implement [story] in src/... with methods for ...").
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
task: Task description to enhance
|
|
263
|
+
story_title: Story title for context
|
|
264
|
+
feature_title: Feature title for context
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Enhanced task with implementation details
|
|
268
|
+
"""
|
|
269
|
+
task_lower = task.lower()
|
|
270
|
+
generic_patterns = [
|
|
271
|
+
("implement", "Implement {story} in src/specfact_cli/... with methods for {feature} operations"),
|
|
272
|
+
("create", "Create {story} component in src/specfact_cli/... with {feature} functionality"),
|
|
273
|
+
("add", "Add {story} functionality to src/specfact_cli/... with {feature} support"),
|
|
274
|
+
("set up", "Set up {story} infrastructure in src/specfact_cli/... for {feature} operations"),
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
# Check if task is generic (has pattern but no implementation details)
|
|
278
|
+
has_details = any(
|
|
279
|
+
detail in task_lower
|
|
280
|
+
for detail in ["file", "path", "method", "class", "component", "module", "function", "src/", "tests/"]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if not has_details:
|
|
284
|
+
for pattern, template in generic_patterns:
|
|
285
|
+
if pattern in task_lower:
|
|
286
|
+
# Extract the story/feature name from task
|
|
287
|
+
remaining = task_lower.replace(pattern, "").strip()
|
|
288
|
+
if not remaining or remaining == story_title.lower():
|
|
289
|
+
# Use template with story and feature
|
|
290
|
+
return template.format(story=story_title, feature=feature_title.lower())
|
|
291
|
+
# Keep original but add implementation details
|
|
292
|
+
return f"{task} in src/specfact_cli/... with methods and tests"
|
|
293
|
+
|
|
294
|
+
return task
|