specfact-cli 0.4.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.

Potentially problematic release.


This version of specfact-cli might be problematic. Click here for more details.

Files changed (60) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +23 -0
  3. specfact_cli/agents/analyze_agent.py +392 -0
  4. specfact_cli/agents/base.py +95 -0
  5. specfact_cli/agents/plan_agent.py +202 -0
  6. specfact_cli/agents/registry.py +176 -0
  7. specfact_cli/agents/sync_agent.py +133 -0
  8. specfact_cli/analyzers/__init__.py +10 -0
  9. specfact_cli/analyzers/code_analyzer.py +775 -0
  10. specfact_cli/cli.py +397 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +87 -0
  13. specfact_cli/commands/import_cmd.py +355 -0
  14. specfact_cli/commands/init.py +119 -0
  15. specfact_cli/commands/plan.py +1090 -0
  16. specfact_cli/commands/repro.py +172 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +24 -0
  19. specfact_cli/common/logger_setup.py +673 -0
  20. specfact_cli/common/logging_utils.py +41 -0
  21. specfact_cli/common/text_utils.py +52 -0
  22. specfact_cli/common/utils.py +48 -0
  23. specfact_cli/comparators/__init__.py +10 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +13 -0
  26. specfact_cli/generators/plan_generator.py +105 -0
  27. specfact_cli/generators/protocol_generator.py +115 -0
  28. specfact_cli/generators/report_generator.py +200 -0
  29. specfact_cli/generators/workflow_generator.py +111 -0
  30. specfact_cli/importers/__init__.py +6 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +704 -0
  33. specfact_cli/models/__init__.py +32 -0
  34. specfact_cli/models/deviation.py +105 -0
  35. specfact_cli/models/enforcement.py +150 -0
  36. specfact_cli/models/plan.py +97 -0
  37. specfact_cli/models/protocol.py +28 -0
  38. specfact_cli/modes/__init__.py +18 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/sync/__init__.py +11 -0
  42. specfact_cli/sync/repository_sync.py +279 -0
  43. specfact_cli/sync/speckit_sync.py +388 -0
  44. specfact_cli/utils/__init__.py +57 -0
  45. specfact_cli/utils/console.py +69 -0
  46. specfact_cli/utils/feature_keys.py +213 -0
  47. specfact_cli/utils/git.py +241 -0
  48. specfact_cli/utils/ide_setup.py +381 -0
  49. specfact_cli/utils/prompts.py +179 -0
  50. specfact_cli/utils/structure.py +496 -0
  51. specfact_cli/utils/yaml_utils.py +200 -0
  52. specfact_cli/validators/__init__.py +19 -0
  53. specfact_cli/validators/fsm.py +260 -0
  54. specfact_cli/validators/repro_checker.py +320 -0
  55. specfact_cli/validators/schema.py +200 -0
  56. specfact_cli-0.4.0.dist-info/METADATA +332 -0
  57. specfact_cli-0.4.0.dist-info/RECORD +60 -0
  58. specfact_cli-0.4.0.dist-info/WHEEL +4 -0
  59. specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
  60. specfact_cli-0.4.0.dist-info/licenses/LICENSE.md +55 -0
@@ -0,0 +1,172 @@
1
+ """
2
+ Repro command - Run full validation suite for reproducibility.
3
+
4
+ This module provides commands for running comprehensive validation
5
+ including linting, type checking, contract exploration, and tests.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import typer
14
+ from beartype import beartype
15
+ from icontract import ensure, require
16
+ from rich.console import Console
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from rich.table import Table
19
+
20
+ from specfact_cli.utils.structure import SpecFactStructure
21
+ from specfact_cli.validators.repro_checker import ReproChecker
22
+
23
+ app = typer.Typer(help="Run validation suite for reproducibility")
24
+ console = Console()
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ @beartype
29
+ @require(lambda repo: repo.exists() and repo.is_dir(), "Repo path must exist and be directory")
30
+ @require(lambda budget: budget > 0, "Budget must be positive")
31
+ @ensure(lambda out: out is None or out.exists(), "Output path must exist if provided")
32
+ def main(
33
+ repo: Path = typer.Option(
34
+ Path("."),
35
+ "--repo",
36
+ help="Path to repository",
37
+ exists=True,
38
+ file_okay=False,
39
+ dir_okay=True,
40
+ ),
41
+ verbose: bool = typer.Option(
42
+ False,
43
+ "--verbose",
44
+ "-v",
45
+ help="Verbose output",
46
+ ),
47
+ budget: int = typer.Option(
48
+ 120,
49
+ "--budget",
50
+ help="Time budget in seconds (must be > 0)",
51
+ ),
52
+ fail_fast: bool = typer.Option(
53
+ False,
54
+ "--fail-fast",
55
+ help="Stop on first failure",
56
+ ),
57
+ out: Optional[Path] = typer.Option(
58
+ None,
59
+ "--out",
60
+ help="Output report path (default: .specfact/reports/enforcement/report-<timestamp>.yaml)",
61
+ ),
62
+ ) -> None:
63
+ """
64
+ Run full validation suite.
65
+
66
+ Executes:
67
+ - Lint checks (ruff)
68
+ - Async patterns (semgrep)
69
+ - Type checking (basedpyright)
70
+ - Contract exploration (CrossHair)
71
+ - Property tests (pytest tests/contracts/)
72
+ - Smoke tests (pytest tests/smoke/)
73
+
74
+ Example:
75
+ specfact repro --verbose --budget 120
76
+ """
77
+ from specfact_cli.utils.yaml_utils import dump_yaml
78
+
79
+ console.print("[bold cyan]Running validation suite...[/bold cyan]")
80
+ console.print(f"[dim]Repository: {repo}[/dim]")
81
+ console.print(f"[dim]Time budget: {budget}s[/dim]")
82
+ if fail_fast:
83
+ console.print("[dim]Fail-fast: enabled[/dim]")
84
+ console.print()
85
+
86
+ # Ensure structure exists
87
+ SpecFactStructure.ensure_structure(repo)
88
+
89
+ # Run all checks
90
+ checker = ReproChecker(repo_path=repo, budget=budget, fail_fast=fail_fast)
91
+
92
+ with Progress(
93
+ SpinnerColumn(),
94
+ TextColumn("[progress.description]{task.description}"),
95
+ console=console,
96
+ ) as progress:
97
+ progress.add_task("Running validation checks...", total=None)
98
+
99
+ # This will show progress for each check internally
100
+ report = checker.run_all_checks()
101
+
102
+ # Display results
103
+ console.print("\n[bold]Validation Results[/bold]\n")
104
+
105
+ # Summary table
106
+ table = Table(title="Check Summary")
107
+ table.add_column("Check", style="cyan")
108
+ table.add_column("Tool", style="dim")
109
+ table.add_column("Status", style="bold")
110
+ table.add_column("Duration", style="dim")
111
+
112
+ for check in report.checks:
113
+ if check.status.value == "passed":
114
+ status_icon = "[green]✓[/green] PASSED"
115
+ elif check.status.value == "failed":
116
+ status_icon = "[red]✗[/red] FAILED"
117
+ elif check.status.value == "timeout":
118
+ status_icon = "[yellow]⏱[/yellow] TIMEOUT"
119
+ elif check.status.value == "skipped":
120
+ status_icon = "[dim]⊘[/dim] SKIPPED"
121
+ else:
122
+ status_icon = "[dim]…[/dim] PENDING"
123
+
124
+ duration_str = f"{check.duration:.2f}s" if check.duration else "N/A"
125
+
126
+ table.add_row(check.name, check.tool, status_icon, duration_str)
127
+
128
+ console.print(table)
129
+
130
+ # Summary stats
131
+ console.print("\n[bold]Summary:[/bold]")
132
+ console.print(f" Total checks: {report.total_checks}")
133
+ console.print(f" [green]Passed: {report.passed_checks}[/green]")
134
+ if report.failed_checks > 0:
135
+ console.print(f" [red]Failed: {report.failed_checks}[/red]")
136
+ if report.timeout_checks > 0:
137
+ console.print(f" [yellow]Timeout: {report.timeout_checks}[/yellow]")
138
+ if report.skipped_checks > 0:
139
+ console.print(f" [dim]Skipped: {report.skipped_checks}[/dim]")
140
+ console.print(f" Total duration: {report.total_duration:.2f}s")
141
+
142
+ # Show errors if verbose
143
+ if verbose:
144
+ for check in report.checks:
145
+ if check.error:
146
+ console.print(f"\n[bold red]{check.name} Error:[/bold red]")
147
+ console.print(f"[dim]{check.error}[/dim]")
148
+ if check.output and check.status.value == "failed":
149
+ console.print(f"\n[bold red]{check.name} Output:[/bold red]")
150
+ console.print(f"[dim]{check.output[:500]}[/dim]") # Limit output
151
+
152
+ # Write report if requested
153
+ if out is None:
154
+ # Use default path
155
+ out = SpecFactStructure.get_timestamped_report_path("enforcement", repo, "yaml")
156
+ SpecFactStructure.ensure_structure(repo)
157
+
158
+ out.parent.mkdir(parents=True, exist_ok=True)
159
+ dump_yaml(report.to_dict(), out)
160
+ console.print(f"\n[dim]Report written to: {out}[/dim]")
161
+
162
+ # Exit with appropriate code
163
+ exit_code = report.get_exit_code()
164
+ if exit_code == 0:
165
+ console.print("\n[bold green]✓[/bold green] All validations passed!")
166
+ console.print("[dim]Reproducibility verified[/dim]")
167
+ elif exit_code == 1:
168
+ console.print("\n[bold red]✗[/bold red] Some validations failed")
169
+ raise typer.Exit(1)
170
+ else:
171
+ console.print("\n[yellow]⏱[/yellow] Budget exceeded")
172
+ raise typer.Exit(2)
@@ -0,0 +1,408 @@
1
+ """
2
+ Sync command - Bidirectional synchronization for Spec-Kit and repositories.
3
+
4
+ This module provides commands for synchronizing changes between Spec-Kit artifacts,
5
+ repository changes, and SpecFact plans.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ import typer
15
+ from beartype import beartype
16
+ from rich.console import Console
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+
19
+ from specfact_cli.models.plan import PlanBundle
20
+ from specfact_cli.sync.speckit_sync import SpecKitSync
21
+
22
+ app = typer.Typer(help="Synchronize Spec-Kit artifacts and repository changes")
23
+ console = Console()
24
+
25
+
26
+ def _sync_speckit_to_specfact(repo: Path, converter: Any, scanner: Any, progress: Any) -> tuple[PlanBundle, int, int]:
27
+ """
28
+ Sync Spec-Kit artifacts to SpecFact format.
29
+
30
+ Returns:
31
+ Tuple of (merged_bundle, features_updated, features_added)
32
+ """
33
+ from specfact_cli.generators.plan_generator import PlanGenerator
34
+ from specfact_cli.utils.structure import SpecFactStructure
35
+ from specfact_cli.validators.schema import validate_plan_bundle
36
+
37
+ plan_path = repo / SpecFactStructure.DEFAULT_PLAN
38
+ existing_bundle: Optional[PlanBundle] = None
39
+
40
+ if plan_path.exists():
41
+ validation_result = validate_plan_bundle(plan_path)
42
+ if isinstance(validation_result, tuple):
43
+ is_valid, _error, bundle = validation_result
44
+ if is_valid and bundle:
45
+ existing_bundle = bundle
46
+
47
+ # Convert Spec-Kit to SpecFact
48
+ converted_bundle = converter.convert_plan(None if not existing_bundle else plan_path)
49
+
50
+ # Merge with existing plan if it exists
51
+ features_updated = 0
52
+ features_added = 0
53
+
54
+ if existing_bundle:
55
+ feature_keys_existing = {f.key for f in existing_bundle.features}
56
+
57
+ for feature in converted_bundle.features:
58
+ if feature.key in feature_keys_existing:
59
+ existing_idx = next(i for i, f in enumerate(existing_bundle.features) if f.key == feature.key)
60
+ existing_bundle.features[existing_idx] = feature
61
+ features_updated += 1
62
+ else:
63
+ existing_bundle.features.append(feature)
64
+ features_added += 1
65
+
66
+ # Update product themes
67
+ themes_existing = set(existing_bundle.product.themes)
68
+ themes_new = set(converted_bundle.product.themes)
69
+ existing_bundle.product.themes = list(themes_existing | themes_new)
70
+
71
+ # Write merged bundle
72
+ generator = PlanGenerator()
73
+ generator.generate(existing_bundle, plan_path)
74
+ return existing_bundle, features_updated, features_added
75
+ # Write new bundle
76
+ generator = PlanGenerator()
77
+ generator.generate(converted_bundle, plan_path)
78
+ return converted_bundle, 0, len(converted_bundle.features)
79
+
80
+
81
+ @app.command("spec-kit")
82
+ def sync_spec_kit(
83
+ repo: Path = typer.Option(
84
+ Path("."),
85
+ "--repo",
86
+ help="Path to repository",
87
+ exists=True,
88
+ file_okay=False,
89
+ dir_okay=True,
90
+ ),
91
+ bidirectional: bool = typer.Option(
92
+ False,
93
+ "--bidirectional",
94
+ help="Enable bidirectional sync (Spec-Kit ↔ SpecFact)",
95
+ ),
96
+ plan: Optional[Path] = typer.Option(
97
+ None,
98
+ "--plan",
99
+ help="Path to SpecFact plan bundle for SpecFact → Spec-Kit conversion (default: .specfact/plans/main.bundle.yaml)",
100
+ ),
101
+ overwrite: bool = typer.Option(
102
+ False,
103
+ "--overwrite",
104
+ help="Overwrite existing Spec-Kit artifacts (delete all existing before sync)",
105
+ ),
106
+ watch: bool = typer.Option(
107
+ False,
108
+ "--watch",
109
+ help="Watch mode for continuous sync",
110
+ ),
111
+ interval: int = typer.Option(
112
+ 5,
113
+ "--interval",
114
+ help="Watch interval in seconds (default: 5)",
115
+ min=1,
116
+ ),
117
+ ) -> None:
118
+ """
119
+ Sync changes between Spec-Kit artifacts and SpecFact.
120
+
121
+ Synchronizes markdown artifacts generated by Spec-Kit slash commands
122
+ with SpecFact plan bundles and protocols.
123
+
124
+ Example:
125
+ specfact sync spec-kit --repo . --bidirectional
126
+ """
127
+ from specfact_cli.importers.speckit_converter import SpecKitConverter
128
+ from specfact_cli.importers.speckit_scanner import SpecKitScanner
129
+ from specfact_cli.utils.structure import SpecFactStructure
130
+ from specfact_cli.validators.schema import validate_plan_bundle
131
+
132
+ console.print(f"[bold cyan]Syncing Spec-Kit artifacts from:[/bold cyan] {repo}")
133
+
134
+ # Watch mode (not implemented yet)
135
+ if watch:
136
+ console.print("[yellow]→ Watch mode enabled (not implemented yet)[/yellow]")
137
+ console.print(f"[dim]Would watch for changes every {interval} seconds[/dim]")
138
+ raise typer.Exit(0)
139
+
140
+ # Step 1: Detect Spec-Kit repository
141
+ scanner = SpecKitScanner(repo)
142
+ if not scanner.is_speckit_repo():
143
+ console.print("[bold red]✗[/bold red] Not a Spec-Kit repository")
144
+ console.print("[dim]Expected Spec-Kit structure (.specify/ directory)[/dim]")
145
+ raise typer.Exit(1)
146
+
147
+ console.print("[bold green]✓[/bold green] Detected Spec-Kit repository")
148
+
149
+ # Step 2: Detect SpecFact structure
150
+ specfact_exists = (repo / SpecFactStructure.ROOT).exists()
151
+
152
+ if not specfact_exists:
153
+ console.print("[yellow]⚠[/yellow] SpecFact structure not found")
154
+ console.print(f"[dim]Initialize with: specfact plan init --scaffold --repo {repo}[/dim]")
155
+ # Create structure automatically
156
+ SpecFactStructure.ensure_structure(repo)
157
+ console.print("[bold green]✓[/bold green] Created SpecFact structure")
158
+
159
+ if specfact_exists:
160
+ console.print("[bold green]✓[/bold green] Detected SpecFact structure")
161
+
162
+ sync = SpecKitSync(repo)
163
+ converter = SpecKitConverter(repo)
164
+
165
+ with Progress(
166
+ SpinnerColumn(),
167
+ TextColumn("[progress.description]{task.description}"),
168
+ console=console,
169
+ ) as progress:
170
+ # Step 3: Scan Spec-Kit artifacts
171
+ task = progress.add_task("[cyan]📦[/cyan] Scanning Spec-Kit artifacts...", total=None)
172
+ features = scanner.discover_features()
173
+ progress.update(task, description=f"[green]✓[/green] Found {len(features)} features in specs/")
174
+
175
+ # Step 4: Sync based on mode
176
+ specfact_changes: dict[str, Any] = {}
177
+ conflicts: list[dict[str, Any]] = []
178
+ features_converted_speckit = 0
179
+
180
+ if bidirectional:
181
+ # Bidirectional sync: Spec-Kit → SpecFact and SpecFact → Spec-Kit
182
+ # Step 5.1: Spec-Kit → SpecFact (unidirectional sync)
183
+ task = progress.add_task("[cyan]📝[/cyan] Converting Spec-Kit → SpecFact...", total=None)
184
+ merged_bundle, features_updated, features_added = _sync_speckit_to_specfact(
185
+ repo, converter, scanner, progress
186
+ )
187
+
188
+ if features_updated > 0 or features_added > 0:
189
+ progress.update(
190
+ task,
191
+ description=f"[green]✓[/green] Updated {features_updated}, Added {features_added} features",
192
+ )
193
+ console.print(f"[dim] - Updated {features_updated} features[/dim]")
194
+ console.print(f"[dim] - Added {features_added} new features[/dim]")
195
+ else:
196
+ progress.update(
197
+ task,
198
+ description=f"[green]✓[/green] Created plan with {len(merged_bundle.features)} features",
199
+ )
200
+
201
+ # Step 5.2: SpecFact → Spec-Kit (reverse conversion)
202
+ task = progress.add_task("[cyan]🔄[/cyan] Converting SpecFact → Spec-Kit...", total=None)
203
+
204
+ # Detect SpecFact changes
205
+ specfact_changes = sync.detect_specfact_changes(repo)
206
+
207
+ if specfact_changes:
208
+ # Load plan bundle and convert to Spec-Kit
209
+ # Use provided plan path, or default to main plan
210
+ if plan:
211
+ plan_path = plan if plan.is_absolute() else repo / plan
212
+ else:
213
+ plan_path = repo / SpecFactStructure.DEFAULT_PLAN
214
+
215
+ if plan_path.exists():
216
+ validation_result = validate_plan_bundle(plan_path)
217
+ if isinstance(validation_result, tuple):
218
+ is_valid, _error, plan_bundle = validation_result
219
+ if is_valid and plan_bundle:
220
+ # Handle overwrite mode
221
+ if overwrite:
222
+ # Delete existing Spec-Kit artifacts before conversion
223
+ specs_dir = repo / "specs"
224
+ if specs_dir.exists():
225
+ console.print(
226
+ "[yellow]⚠[/yellow] Overwrite mode: Removing existing Spec-Kit artifacts..."
227
+ )
228
+ shutil.rmtree(specs_dir)
229
+ specs_dir.mkdir(parents=True, exist_ok=True)
230
+ console.print("[green]✓[/green] Existing artifacts removed")
231
+
232
+ # Convert SpecFact plan bundle to Spec-Kit markdown
233
+ features_converted_speckit = converter.convert_to_speckit(plan_bundle)
234
+ progress.update(
235
+ task,
236
+ description=f"[green]✓[/green] Converted {features_converted_speckit} features to Spec-Kit",
237
+ )
238
+ mode_text = "overwritten" if overwrite else "generated"
239
+ console.print(
240
+ f"[dim] - {mode_text.capitalize()} spec.md, plan.md, tasks.md for {features_converted_speckit} features[/dim]"
241
+ )
242
+ else:
243
+ progress.update(task, description="[yellow]⚠[/yellow] Plan bundle validation failed")
244
+ console.print("[yellow]⚠[/yellow] Could not load plan bundle for conversion")
245
+ else:
246
+ progress.update(task, description="[yellow]⚠[/yellow] Plan bundle not found")
247
+ else:
248
+ progress.update(task, description="[green]✓[/green] No SpecFact plan to sync")
249
+ else:
250
+ progress.update(task, description="[green]✓[/green] No SpecFact changes to sync")
251
+
252
+ # Detect conflicts between both directions
253
+ speckit_changes = sync.detect_speckit_changes(repo)
254
+ conflicts = sync.detect_conflicts(speckit_changes, specfact_changes)
255
+
256
+ if conflicts:
257
+ console.print(f"[yellow]⚠[/yellow] Found {len(conflicts)} conflicts")
258
+ console.print("[dim]Conflicts resolved using priority rules (SpecFact > Spec-Kit for artifacts)[/dim]")
259
+ else:
260
+ console.print("[bold green]✓[/bold green] No conflicts detected")
261
+ else:
262
+ # Unidirectional sync: Spec-Kit → SpecFact
263
+ task = progress.add_task("[cyan]📝[/cyan] Converting to SpecFact format...", total=None)
264
+
265
+ merged_bundle, features_updated, features_added = _sync_speckit_to_specfact(
266
+ repo, converter, scanner, progress
267
+ )
268
+
269
+ if features_updated > 0 or features_added > 0:
270
+ task = progress.add_task("[cyan]🔀[/cyan] Merging with existing plan...", total=None)
271
+ progress.update(
272
+ task,
273
+ description=f"[green]✓[/green] Updated {features_updated} features, Added {features_added} features",
274
+ )
275
+ console.print(f"[dim] - Updated {features_updated} features[/dim]")
276
+ console.print(f"[dim] - Added {features_added} new features[/dim]")
277
+ else:
278
+ progress.update(
279
+ task, description=f"[green]✓[/green] Created plan with {len(merged_bundle.features)} features"
280
+ )
281
+ console.print(f"[dim]Created plan with {len(merged_bundle.features)} features[/dim]")
282
+
283
+ # Report features synced
284
+ console.print()
285
+ if features:
286
+ console.print("[bold cyan]Features synced:[/bold cyan]")
287
+ for feature in features:
288
+ feature_key = feature.get("feature_key", "UNKNOWN")
289
+ feature_title = feature.get("title", "Unknown Feature")
290
+ console.print(f" - [cyan]{feature_key}[/cyan]: {feature_title}")
291
+
292
+ # Step 8: Output Results
293
+ console.print()
294
+ if bidirectional:
295
+ console.print("[bold cyan]Sync Summary (Bidirectional):[/bold cyan]")
296
+ console.print(f" - Spec-Kit → SpecFact: Updated {features_updated}, Added {features_added} features")
297
+ if specfact_changes:
298
+ console.print(
299
+ f" - SpecFact → Spec-Kit: {features_converted_speckit} features converted to Spec-Kit markdown"
300
+ )
301
+ else:
302
+ console.print(" - SpecFact → Spec-Kit: No changes detected")
303
+ if conflicts:
304
+ console.print(f" - Conflicts: {len(conflicts)} detected and resolved")
305
+ else:
306
+ console.print(" - Conflicts: None detected")
307
+ else:
308
+ console.print("[bold cyan]Sync Summary (Unidirectional):[/bold cyan]")
309
+ if features:
310
+ console.print(f" - Features synced: {len(features)}")
311
+ if features_updated > 0 or features_added > 0:
312
+ console.print(f" - Updated: {features_updated} features")
313
+ console.print(f" - Added: {features_added} new features")
314
+ console.print(" - Direction: Spec-Kit → SpecFact")
315
+
316
+ console.print()
317
+ console.print("[bold green]✓[/bold green] Sync complete!")
318
+
319
+
320
+ @app.command("repository")
321
+ def sync_repository(
322
+ repo: Path = typer.Option(
323
+ Path("."),
324
+ "--repo",
325
+ help="Path to repository",
326
+ exists=True,
327
+ file_okay=False,
328
+ dir_okay=True,
329
+ ),
330
+ target: Optional[Path] = typer.Option(
331
+ None,
332
+ "--target",
333
+ help="Target directory for artifacts (default: .specfact)",
334
+ ),
335
+ watch: bool = typer.Option(
336
+ False,
337
+ "--watch",
338
+ help="Watch mode for continuous sync",
339
+ ),
340
+ interval: int = typer.Option(
341
+ 5,
342
+ "--interval",
343
+ help="Watch interval in seconds (default: 5)",
344
+ min=1,
345
+ ),
346
+ confidence: float = typer.Option(
347
+ 0.5,
348
+ "--confidence",
349
+ help="Minimum confidence threshold for feature detection (default: 0.5)",
350
+ min=0.0,
351
+ max=1.0,
352
+ ),
353
+ ) -> None:
354
+ """
355
+ Sync code changes to SpecFact artifacts.
356
+
357
+ Monitors repository code changes, updates plan artifacts based on detected
358
+ features/stories, and tracks deviations from manual plans.
359
+
360
+ Example:
361
+ specfact sync repository --repo . --confidence 0.5
362
+ """
363
+ from specfact_cli.sync.repository_sync import RepositorySync
364
+
365
+ console.print(f"[bold cyan]Syncing repository changes from:[/bold cyan] {repo}")
366
+
367
+ if target is None:
368
+ target = repo / ".specfact"
369
+
370
+ sync = RepositorySync(repo, target, confidence_threshold=confidence)
371
+
372
+ if watch:
373
+ console.print("[yellow]→ Watch mode enabled (not implemented yet)[/yellow]")
374
+ console.print(f"[dim]Would watch for changes every {interval} seconds[/dim]")
375
+ raise typer.Exit(0)
376
+
377
+ with Progress(
378
+ SpinnerColumn(),
379
+ TextColumn("[progress.description]{task.description}"),
380
+ console=console,
381
+ ) as progress:
382
+ # Step 1: Detect code changes
383
+ task = progress.add_task("Detecting code changes...", total=None)
384
+ result = sync.sync_repository_changes(repo)
385
+ progress.update(task, description=f"✓ Detected {len(result.code_changes)} code changes")
386
+
387
+ # Step 2: Show plan updates
388
+ if result.plan_updates:
389
+ task = progress.add_task("Updating plan artifacts...", total=None)
390
+ total_features = sum(update.get("features", 0) for update in result.plan_updates)
391
+ progress.update(task, description=f"✓ Updated plan artifacts ({total_features} features)")
392
+
393
+ # Step 3: Show deviations
394
+ if result.deviations:
395
+ task = progress.add_task("Tracking deviations...", total=None)
396
+ progress.update(task, description=f"✓ Found {len(result.deviations)} deviations")
397
+
398
+ # Report results
399
+ console.print(f"[bold cyan]Code Changes:[/bold cyan] {len(result.code_changes)}")
400
+ if result.plan_updates:
401
+ console.print(f"[bold cyan]Plan Updates:[/bold cyan] {len(result.plan_updates)}")
402
+ if result.deviations:
403
+ console.print(f"[yellow]⚠[/yellow] Found {len(result.deviations)} deviations from manual plan")
404
+ console.print("[dim]Run 'specfact plan compare' for detailed deviation report[/dim]")
405
+ else:
406
+ console.print("[bold green]✓[/bold green] No deviations detected")
407
+
408
+ console.print("[bold green]✓[/bold green] Repository sync complete!")
@@ -0,0 +1,24 @@
1
+ """
2
+ Common module for shared functionality across SpecFact CLI.
3
+
4
+ This module contains shared infrastructure components and utilities used throughout
5
+ the SpecFact CLI application:
6
+ - Logging infrastructure (logger_setup, logging_utils)
7
+ - Text and file utilities (text_utils, utils)
8
+ """
9
+
10
+ from specfact_cli.common.logger_setup import LoggerSetup
11
+ from specfact_cli.common.logging_utils import get_bridge_logger
12
+ from specfact_cli.common.text_utils import TextUtils
13
+ from specfact_cli.common.utils import compute_sha256, dump_json, ensure_directory, load_json
14
+
15
+ # Define what gets imported with "from specfact_cli.common import *"
16
+ __all__ = [
17
+ "LoggerSetup",
18
+ "TextUtils",
19
+ "compute_sha256",
20
+ "dump_json",
21
+ "ensure_directory",
22
+ "get_bridge_logger",
23
+ "load_json",
24
+ ]