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,281 @@
1
+ """
2
+ Constitution command - Manage project constitutions.
3
+
4
+ This module provides commands for bootstrapping, enriching, and validating
5
+ project constitutions based on repository context analysis.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import typer
14
+ from beartype import beartype
15
+ from icontract import ensure, require
16
+ from rich.console import Console
17
+
18
+ from specfact_cli.enrichers.constitution_enricher import ConstitutionEnricher
19
+ from specfact_cli.utils import print_error, print_info, print_success
20
+
21
+
22
+ app = typer.Typer(
23
+ help="Manage project constitutions (Spec-Kit compatibility layer). Generates and validates constitutions at .specify/memory/constitution.md for Spec-Kit format compatibility."
24
+ )
25
+ console = Console()
26
+
27
+
28
+ @app.command("bootstrap")
29
+ @beartype
30
+ @require(lambda repo: repo.exists(), "Repository path must exist")
31
+ @require(lambda repo: repo.is_dir(), "Repository path must be a directory")
32
+ @ensure(lambda result: result is None, "Must return None")
33
+ def bootstrap(
34
+ repo: Path = typer.Option(
35
+ Path("."),
36
+ "--repo",
37
+ help="Repository path (default: current directory)",
38
+ exists=True,
39
+ file_okay=False,
40
+ dir_okay=True,
41
+ ),
42
+ output: Path | None = typer.Option(
43
+ None,
44
+ "--output",
45
+ help="Output path for constitution (default: .specify/memory/constitution.md)",
46
+ ),
47
+ overwrite: bool = typer.Option(
48
+ False,
49
+ "--overwrite",
50
+ help="Overwrite existing constitution if it exists",
51
+ ),
52
+ ) -> None:
53
+ """
54
+ Generate bootstrap constitution from repository analysis (Spec-Kit compatibility).
55
+
56
+ This command generates a constitution in Spec-Kit format (`.specify/memory/constitution.md`)
57
+ for compatibility with Spec-Kit artifacts and sync operations.
58
+
59
+ **Note**: SpecFact itself uses plan bundles (`.specfact/plans/*.bundle.yaml`) for internal
60
+ operations. Constitutions are only needed when syncing with Spec-Kit or working in Spec-Kit format.
61
+
62
+ Analyzes the repository (README, pyproject.toml, .cursor/rules/, docs/rules/)
63
+ to extract project metadata, development principles, and quality standards,
64
+ then generates a bootstrap constitution template ready for review and adjustment.
65
+
66
+ Example:
67
+ specfact constitution bootstrap --repo .
68
+ specfact constitution bootstrap --repo . --output custom-constitution.md
69
+ """
70
+ from specfact_cli.telemetry import telemetry
71
+
72
+ with telemetry.track_command("constitution.bootstrap", {"repo": str(repo)}):
73
+ console.print(f"[bold cyan]Generating bootstrap constitution for:[/bold cyan] {repo}")
74
+
75
+ # Determine output path
76
+ if output is None:
77
+ # Use Spec-Kit convention: .specify/memory/constitution.md
78
+ specify_dir = repo / ".specify" / "memory"
79
+ specify_dir.mkdir(parents=True, exist_ok=True)
80
+ output = specify_dir / "constitution.md"
81
+ else:
82
+ output.parent.mkdir(parents=True, exist_ok=True)
83
+
84
+ # Check if constitution already exists
85
+ if output.exists() and not overwrite:
86
+ console.print(f"[yellow]⚠[/yellow] Constitution already exists: {output}")
87
+ console.print("[dim]Use --overwrite to replace it[/dim]")
88
+ raise typer.Exit(1)
89
+
90
+ # Generate bootstrap constitution
91
+ print_info("Analyzing repository...")
92
+ enricher = ConstitutionEnricher()
93
+ enriched_content = enricher.bootstrap(repo, output)
94
+
95
+ # Write constitution
96
+ output.write_text(enriched_content, encoding="utf-8")
97
+ print_success(f"✓ Bootstrap constitution generated: {output}")
98
+
99
+ console.print("\n[bold]Next Steps:[/bold]")
100
+ console.print("1. Review the generated constitution")
101
+ console.print("2. Adjust principles and sections as needed")
102
+ console.print("3. Run 'specfact constitution validate' to check completeness")
103
+ console.print("4. Run 'specfact sync spec-kit' to sync with Spec-Kit artifacts")
104
+
105
+
106
+ @app.command("enrich")
107
+ @beartype
108
+ @require(lambda repo: repo.exists(), "Repository path must exist")
109
+ @require(lambda repo: repo.is_dir(), "Repository path must be a directory")
110
+ @ensure(lambda result: result is None, "Must return None")
111
+ def enrich(
112
+ repo: Path = typer.Option(
113
+ Path("."),
114
+ "--repo",
115
+ help="Repository path (default: current directory)",
116
+ exists=True,
117
+ file_okay=False,
118
+ dir_okay=True,
119
+ ),
120
+ constitution: Path | None = typer.Option(
121
+ None,
122
+ "--constitution",
123
+ help="Path to constitution file (default: .specify/memory/constitution.md)",
124
+ ),
125
+ ) -> None:
126
+ """
127
+ Auto-enrich existing constitution with repository context (Spec-Kit compatibility).
128
+
129
+ This command enriches a constitution in Spec-Kit format (`.specify/memory/constitution.md`)
130
+ for compatibility with Spec-Kit artifacts and sync operations.
131
+
132
+ **Note**: SpecFact itself uses plan bundles (`.specfact/plans/*.bundle.yaml`) for internal
133
+ operations. Constitutions are only needed when syncing with Spec-Kit or working in Spec-Kit format.
134
+
135
+ Analyzes the repository and enriches the existing constitution with
136
+ additional principles and details extracted from repository context.
137
+
138
+ Example:
139
+ specfact constitution enrich --repo .
140
+ """
141
+ from specfact_cli.telemetry import telemetry
142
+
143
+ with telemetry.track_command("constitution.enrich", {"repo": str(repo)}):
144
+ # Determine constitution path
145
+ if constitution is None:
146
+ constitution = repo / ".specify" / "memory" / "constitution.md"
147
+
148
+ if not constitution.exists():
149
+ console.print(f"[bold red]✗[/bold red] Constitution not found: {constitution}")
150
+ console.print("[dim]Run 'specfact constitution bootstrap' first[/dim]")
151
+ raise typer.Exit(1)
152
+
153
+ console.print(f"[bold cyan]Enriching constitution:[/bold cyan] {constitution}")
154
+
155
+ # Analyze repository
156
+ print_info("Analyzing repository...")
157
+ enricher = ConstitutionEnricher()
158
+ analysis = enricher.analyze_repository(repo)
159
+
160
+ # Suggest additional principles
161
+ principles = enricher.suggest_principles(analysis)
162
+
163
+ console.print(f"[dim]Found {len(principles)} suggested principles[/dim]")
164
+
165
+ # Read existing constitution
166
+ existing_content = constitution.read_text(encoding="utf-8")
167
+
168
+ # Check if enrichment is needed (has placeholders)
169
+ import re
170
+
171
+ placeholder_pattern = r"\[[A-Z_0-9]+\]"
172
+ placeholders = re.findall(placeholder_pattern, existing_content)
173
+
174
+ if not placeholders:
175
+ console.print("[yellow]⚠[/yellow] Constitution appears complete (no placeholders found)")
176
+ console.print("[dim]No enrichment needed[/dim]")
177
+ return
178
+
179
+ console.print(f"[dim]Found {len(placeholders)} placeholders to enrich[/dim]")
180
+
181
+ # Enrich template
182
+ suggestions: dict[str, Any] = {
183
+ "project_name": analysis.get("project_name", "Project"),
184
+ "principles": principles,
185
+ "section2_name": "Development Workflow",
186
+ "section2_content": enricher._generate_workflow_section(analysis),
187
+ "section3_name": "Quality Standards",
188
+ "section3_content": enricher._generate_quality_standards_section(analysis),
189
+ "governance_rules": "Constitution supersedes all other practices. Amendments require documentation, team approval, and migration plan for breaking changes.",
190
+ }
191
+
192
+ enriched_content = enricher.enrich_template(constitution, suggestions)
193
+
194
+ # Write enriched constitution
195
+ constitution.write_text(enriched_content, encoding="utf-8")
196
+ print_success(f"✓ Constitution enriched: {constitution}")
197
+
198
+ console.print("\n[bold]Next Steps:[/bold]")
199
+ console.print("1. Review the enriched constitution")
200
+ console.print("2. Adjust as needed")
201
+ console.print("3. Run 'specfact constitution validate' to check completeness")
202
+
203
+
204
+ @app.command("validate")
205
+ @beartype
206
+ @require(lambda constitution: constitution.exists(), "Constitution path must exist")
207
+ @ensure(lambda result: result is None, "Must return None")
208
+ def validate(
209
+ constitution: Path = typer.Option(
210
+ Path(".specify/memory/constitution.md"),
211
+ "--constitution",
212
+ help="Path to constitution file",
213
+ exists=True,
214
+ ),
215
+ ) -> None:
216
+ """
217
+ Validate constitution completeness (Spec-Kit compatibility).
218
+
219
+ This command validates a constitution in Spec-Kit format (`.specify/memory/constitution.md`)
220
+ for compatibility with Spec-Kit artifacts and sync operations.
221
+
222
+ **Note**: SpecFact itself uses plan bundles (`.specfact/plans/*.bundle.yaml`) for internal
223
+ operations. Constitutions are only needed when syncing with Spec-Kit or working in Spec-Kit format.
224
+
225
+ Checks if the constitution is complete (no placeholders, has principles,
226
+ has governance section, etc.).
227
+
228
+ Example:
229
+ specfact constitution validate
230
+ specfact constitution validate --constitution custom-constitution.md
231
+ """
232
+ from specfact_cli.telemetry import telemetry
233
+
234
+ with telemetry.track_command("constitution.validate", {"constitution": str(constitution)}):
235
+ console.print(f"[bold cyan]Validating constitution:[/bold cyan] {constitution}")
236
+
237
+ enricher = ConstitutionEnricher()
238
+ is_valid, issues = enricher.validate(constitution)
239
+
240
+ if is_valid:
241
+ print_success("✓ Constitution is valid and complete")
242
+ else:
243
+ print_error("✗ Constitution validation failed")
244
+ console.print("\n[bold]Issues found:[/bold]")
245
+ for issue in issues:
246
+ console.print(f" - {issue}")
247
+
248
+ console.print("\n[bold]Next Steps:[/bold]")
249
+ console.print("1. Run 'specfact constitution bootstrap' to generate a complete constitution")
250
+ console.print("2. Or run 'specfact constitution enrich' to enrich existing constitution")
251
+ raise typer.Exit(1)
252
+
253
+
254
+ def is_constitution_minimal(constitution_path: Path) -> bool:
255
+ """
256
+ Check if constitution is minimal (essentially empty).
257
+
258
+ Args:
259
+ constitution_path: Path to constitution file
260
+
261
+ Returns:
262
+ True if constitution is minimal, False otherwise
263
+ """
264
+ if not constitution_path.exists():
265
+ return True
266
+
267
+ try:
268
+ content = constitution_path.read_text(encoding="utf-8").strip()
269
+ # Check if it's just a header or very minimal
270
+ if not content or content == "# Constitution" or len(content) < 100:
271
+ return True
272
+
273
+ # Check if it has mostly placeholders
274
+ import re
275
+
276
+ placeholder_pattern = r"\[[A-Z_0-9]+\]"
277
+ placeholders = re.findall(placeholder_pattern, content)
278
+ lines = [line.strip() for line in content.split("\n") if line.strip()]
279
+ return bool(lines and len(placeholders) > len(lines) * 0.5)
280
+ except Exception:
281
+ return True
@@ -13,6 +13,7 @@ from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
15
  from specfact_cli.models.enforcement import EnforcementConfig, EnforcementPreset
16
+ from specfact_cli.telemetry import telemetry
16
17
  from specfact_cli.utils.structure import SpecFactStructure
17
18
  from specfact_cli.utils.yaml_utils import dump_yaml
18
19
 
@@ -41,48 +42,55 @@ def stage(
41
42
  Example:
42
43
  specfact enforce stage --preset balanced
43
44
  """
44
- # Validate preset (contract-style validation)
45
- if not isinstance(preset, str) or len(preset) == 0:
46
- console.print("[bold red]✗[/bold red] Preset must be non-empty string")
47
- raise typer.Exit(1)
45
+ telemetry_metadata = {
46
+ "preset": preset.lower(),
47
+ }
48
48
 
49
- if preset.lower() not in ("minimal", "balanced", "strict"):
50
- console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
51
- console.print("Valid presets: minimal, balanced, strict")
52
- raise typer.Exit(1)
49
+ with telemetry.track_command("enforce.stage", telemetry_metadata) as record:
50
+ # Validate preset (contract-style validation)
51
+ if not isinstance(preset, str) or len(preset) == 0:
52
+ console.print("[bold red]✗[/bold red] Preset must be non-empty string")
53
+ raise typer.Exit(1)
53
54
 
54
- console.print(f"[bold cyan]Setting enforcement mode:[/bold cyan] {preset}")
55
+ if preset.lower() not in ("minimal", "balanced", "strict"):
56
+ console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
57
+ console.print("Valid presets: minimal, balanced, strict")
58
+ raise typer.Exit(1)
55
59
 
56
- # Validate preset enum
57
- try:
58
- preset_enum = EnforcementPreset(preset)
59
- except ValueError as err:
60
- console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
61
- console.print("Valid presets: minimal, balanced, strict")
62
- raise typer.Exit(1) from err
60
+ console.print(f"[bold cyan]Setting enforcement mode:[/bold cyan] {preset}")
63
61
 
64
- # Create enforcement configuration
65
- config = EnforcementConfig.from_preset(preset_enum)
62
+ # Validate preset enum
63
+ try:
64
+ preset_enum = EnforcementPreset(preset)
65
+ except ValueError as err:
66
+ console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
67
+ console.print("Valid presets: minimal, balanced, strict")
68
+ raise typer.Exit(1) from err
66
69
 
67
- # Display configuration as table
68
- table = Table(title=f"Enforcement Mode: {preset.upper()}")
69
- table.add_column("Severity", style="cyan")
70
- table.add_column("Action", style="yellow")
70
+ # Create enforcement configuration
71
+ config = EnforcementConfig.from_preset(preset_enum)
71
72
 
72
- for severity, action in config.to_summary_dict().items():
73
- table.add_row(severity, action)
73
+ # Display configuration as table
74
+ table = Table(title=f"Enforcement Mode: {preset.upper()}")
75
+ table.add_column("Severity", style="cyan")
76
+ table.add_column("Action", style="yellow")
74
77
 
75
- console.print(table)
78
+ for severity, action in config.to_summary_dict().items():
79
+ table.add_row(severity, action)
76
80
 
77
- # Ensure .specfact structure exists
78
- SpecFactStructure.ensure_structure()
81
+ console.print(table)
79
82
 
80
- # Write configuration to file
81
- config_path = SpecFactStructure.get_enforcement_config_path()
82
- config_path.parent.mkdir(parents=True, exist_ok=True)
83
+ # Ensure .specfact structure exists
84
+ SpecFactStructure.ensure_structure()
83
85
 
84
- # Use mode='json' to convert enums to their string values
85
- dump_yaml(config.model_dump(mode="json"), config_path)
86
+ # Write configuration to file
87
+ config_path = SpecFactStructure.get_enforcement_config_path()
88
+ config_path.parent.mkdir(parents=True, exist_ok=True)
86
89
 
87
- console.print(f"\n[bold green]✓[/bold green] Enforcement mode set to {preset}")
88
- console.print(f"[dim]Configuration saved to: {config_path}[/dim]")
90
+ # Use mode='json' to convert enums to their string values
91
+ dump_yaml(config.model_dump(mode="json"), config_path)
92
+
93
+ record({"config_saved": True, "enabled": config.enabled})
94
+
95
+ console.print(f"\n[bold green]✓[/bold green] Enforcement mode set to {preset}")
96
+ console.print(f"[dim]Configuration saved to: {config_path}[/dim]")