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.
Files changed (66) hide show
  1. specfact_cli/__init__.py +1 -1
  2. specfact_cli/agents/analyze_agent.py +2 -3
  3. specfact_cli/analyzers/__init__.py +2 -1
  4. specfact_cli/analyzers/ambiguity_scanner.py +601 -0
  5. specfact_cli/analyzers/code_analyzer.py +462 -30
  6. specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
  7. specfact_cli/analyzers/contract_extractor.py +419 -0
  8. specfact_cli/analyzers/control_flow_analyzer.py +281 -0
  9. specfact_cli/analyzers/requirement_extractor.py +337 -0
  10. specfact_cli/analyzers/test_pattern_extractor.py +330 -0
  11. specfact_cli/cli.py +151 -206
  12. specfact_cli/commands/constitution.py +281 -0
  13. specfact_cli/commands/enforce.py +42 -34
  14. specfact_cli/commands/import_cmd.py +481 -152
  15. specfact_cli/commands/init.py +224 -55
  16. specfact_cli/commands/plan.py +2133 -547
  17. specfact_cli/commands/repro.py +100 -78
  18. specfact_cli/commands/sync.py +701 -186
  19. specfact_cli/enrichers/constitution_enricher.py +765 -0
  20. specfact_cli/enrichers/plan_enricher.py +294 -0
  21. specfact_cli/importers/speckit_converter.py +364 -48
  22. specfact_cli/importers/speckit_scanner.py +65 -0
  23. specfact_cli/models/plan.py +42 -0
  24. specfact_cli/resources/mappings/node-async.yaml +49 -0
  25. specfact_cli/resources/mappings/python-async.yaml +47 -0
  26. specfact_cli/resources/mappings/speckit-default.yaml +82 -0
  27. specfact_cli/resources/prompts/specfact-enforce.md +185 -0
  28. specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
  29. specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
  30. specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
  31. specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
  32. specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
  33. specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
  34. specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
  35. specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
  36. specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
  37. specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
  38. specfact_cli/resources/prompts/specfact-repro.md +268 -0
  39. specfact_cli/resources/prompts/specfact-sync.md +497 -0
  40. specfact_cli/resources/schemas/deviation.schema.json +61 -0
  41. specfact_cli/resources/schemas/plan.schema.json +204 -0
  42. specfact_cli/resources/schemas/protocol.schema.json +53 -0
  43. specfact_cli/resources/templates/github-action.yml.j2 +140 -0
  44. specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
  45. specfact_cli/resources/templates/pr-template.md.j2 +58 -0
  46. specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
  47. specfact_cli/resources/templates/telemetry.yaml.example +35 -0
  48. specfact_cli/sync/__init__.py +10 -1
  49. specfact_cli/sync/watcher.py +268 -0
  50. specfact_cli/telemetry.py +440 -0
  51. specfact_cli/utils/acceptance_criteria.py +127 -0
  52. specfact_cli/utils/enrichment_parser.py +445 -0
  53. specfact_cli/utils/feature_keys.py +12 -3
  54. specfact_cli/utils/ide_setup.py +170 -0
  55. specfact_cli/utils/structure.py +179 -2
  56. specfact_cli/utils/yaml_utils.py +33 -0
  57. specfact_cli/validators/repro_checker.py +22 -1
  58. specfact_cli/validators/schema.py +15 -4
  59. specfact_cli-0.6.8.dist-info/METADATA +456 -0
  60. specfact_cli-0.6.8.dist-info/RECORD +99 -0
  61. {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
  62. specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
  63. specfact_cli-0.4.2.dist-info/METADATA +0 -370
  64. specfact_cli-0.4.2.dist-info/RECORD +0 -62
  65. specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
  66. {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