specfact-cli 0.4.2__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 (62) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +24 -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 +11 -0
  9. specfact_cli/analyzers/code_analyzer.py +796 -0
  10. specfact_cli/cli.py +396 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +88 -0
  13. specfact_cli/commands/import_cmd.py +365 -0
  14. specfact_cli/commands/init.py +125 -0
  15. specfact_cli/commands/plan.py +1089 -0
  16. specfact_cli/commands/repro.py +192 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +25 -0
  19. specfact_cli/common/logger_setup.py +654 -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 +11 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +14 -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 +120 -0
  30. specfact_cli/importers/__init__.py +7 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +711 -0
  33. specfact_cli/models/__init__.py +33 -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 +19 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/resources/semgrep/async.yml +285 -0
  42. specfact_cli/sync/__init__.py +12 -0
  43. specfact_cli/sync/repository_sync.py +279 -0
  44. specfact_cli/sync/speckit_sync.py +388 -0
  45. specfact_cli/utils/__init__.py +58 -0
  46. specfact_cli/utils/console.py +70 -0
  47. specfact_cli/utils/feature_keys.py +212 -0
  48. specfact_cli/utils/git.py +241 -0
  49. specfact_cli/utils/github_annotations.py +399 -0
  50. specfact_cli/utils/ide_setup.py +382 -0
  51. specfact_cli/utils/prompts.py +180 -0
  52. specfact_cli/utils/structure.py +497 -0
  53. specfact_cli/utils/yaml_utils.py +200 -0
  54. specfact_cli/validators/__init__.py +20 -0
  55. specfact_cli/validators/fsm.py +262 -0
  56. specfact_cli/validators/repro_checker.py +759 -0
  57. specfact_cli/validators/schema.py +196 -0
  58. specfact_cli-0.4.2.dist-info/METADATA +370 -0
  59. specfact_cli-0.4.2.dist-info/RECORD +62 -0
  60. specfact_cli-0.4.2.dist-info/WHEEL +4 -0
  61. specfact_cli-0.4.2.dist-info/entry_points.txt +2 -0
  62. specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +61 -0
@@ -0,0 +1,192 @@
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
+
12
+ import typer
13
+ from beartype import beartype
14
+ from icontract import ensure, require
15
+ from rich.console import Console
16
+ from rich.progress import Progress, SpinnerColumn, TextColumn
17
+ from rich.table import Table
18
+
19
+ from specfact_cli.utils.structure import SpecFactStructure
20
+ from specfact_cli.validators.repro_checker import ReproChecker
21
+
22
+
23
+ app = typer.Typer(help="Run validation suite for reproducibility")
24
+ console = Console()
25
+
26
+
27
+ def _is_valid_repo_path(path: Path) -> bool:
28
+ """Check if path exists and is a directory."""
29
+ return path.exists() and path.is_dir()
30
+
31
+
32
+ def _is_valid_output_path(path: Path | None) -> bool:
33
+ """Check if output path exists if provided."""
34
+ return path is None or path.exists()
35
+
36
+
37
+ @app.callback(invoke_without_command=True)
38
+ @beartype
39
+ @require(lambda repo: _is_valid_repo_path(repo), "Repo path must exist and be directory")
40
+ @require(lambda budget: budget > 0, "Budget must be positive")
41
+ @ensure(lambda out: _is_valid_output_path(out), "Output path must exist if provided")
42
+ # CrossHair: Skip analysis for Typer-decorated functions (signature analysis limitation)
43
+ # type: ignore[crosshair]
44
+ def main(
45
+ repo: Path = typer.Option(
46
+ Path("."),
47
+ "--repo",
48
+ help="Path to repository",
49
+ exists=True,
50
+ file_okay=False,
51
+ dir_okay=True,
52
+ ),
53
+ verbose: bool = typer.Option(
54
+ False,
55
+ "--verbose",
56
+ "-v",
57
+ help="Verbose output",
58
+ ),
59
+ budget: int = typer.Option(
60
+ 120,
61
+ "--budget",
62
+ help="Time budget in seconds (must be > 0)",
63
+ ),
64
+ fail_fast: bool = typer.Option(
65
+ False,
66
+ "--fail-fast",
67
+ help="Stop on first failure",
68
+ ),
69
+ fix: bool = typer.Option(
70
+ False,
71
+ "--fix",
72
+ help="Apply auto-fixes where available (Semgrep auto-fixes)",
73
+ ),
74
+ out: Path | None = typer.Option(
75
+ None,
76
+ "--out",
77
+ help="Output report path (default: .specfact/reports/enforcement/report-<timestamp>.yaml)",
78
+ ),
79
+ ) -> None:
80
+ """
81
+ Run full validation suite.
82
+
83
+ Executes:
84
+ - Lint checks (ruff)
85
+ - Async patterns (semgrep)
86
+ - Type checking (basedpyright)
87
+ - Contract exploration (CrossHair)
88
+ - Property tests (pytest tests/contracts/)
89
+ - Smoke tests (pytest tests/smoke/)
90
+
91
+ Example:
92
+ specfact repro --verbose --budget 120
93
+ specfact repro --fix --budget 120
94
+ """
95
+ from specfact_cli.utils.yaml_utils import dump_yaml
96
+
97
+ console.print("[bold cyan]Running validation suite...[/bold cyan]")
98
+ console.print(f"[dim]Repository: {repo}[/dim]")
99
+ console.print(f"[dim]Time budget: {budget}s[/dim]")
100
+ if fail_fast:
101
+ console.print("[dim]Fail-fast: enabled[/dim]")
102
+ if fix:
103
+ console.print("[dim]Auto-fix: enabled[/dim]")
104
+ console.print()
105
+
106
+ # Ensure structure exists
107
+ SpecFactStructure.ensure_structure(repo)
108
+
109
+ # Run all checks
110
+ checker = ReproChecker(repo_path=repo, budget=budget, fail_fast=fail_fast, fix=fix)
111
+
112
+ with Progress(
113
+ SpinnerColumn(),
114
+ TextColumn("[progress.description]{task.description}"),
115
+ console=console,
116
+ ) as progress:
117
+ progress.add_task("Running validation checks...", total=None)
118
+
119
+ # This will show progress for each check internally
120
+ report = checker.run_all_checks()
121
+
122
+ # Display results
123
+ console.print("\n[bold]Validation Results[/bold]\n")
124
+
125
+ # Summary table
126
+ table = Table(title="Check Summary")
127
+ table.add_column("Check", style="cyan")
128
+ table.add_column("Tool", style="dim")
129
+ table.add_column("Status", style="bold")
130
+ table.add_column("Duration", style="dim")
131
+
132
+ for check in report.checks:
133
+ if check.status.value == "passed":
134
+ status_icon = "[green]✓[/green] PASSED"
135
+ elif check.status.value == "failed":
136
+ status_icon = "[red]✗[/red] FAILED"
137
+ elif check.status.value == "timeout":
138
+ status_icon = "[yellow]⏱[/yellow] TIMEOUT"
139
+ elif check.status.value == "skipped":
140
+ status_icon = "[dim]⊘[/dim] SKIPPED"
141
+ else:
142
+ status_icon = "[dim]…[/dim] PENDING"
143
+
144
+ duration_str = f"{check.duration:.2f}s" if check.duration else "N/A"
145
+
146
+ table.add_row(check.name, check.tool, status_icon, duration_str)
147
+
148
+ console.print(table)
149
+
150
+ # Summary stats
151
+ console.print("\n[bold]Summary:[/bold]")
152
+ console.print(f" Total checks: {report.total_checks}")
153
+ console.print(f" [green]Passed: {report.passed_checks}[/green]")
154
+ if report.failed_checks > 0:
155
+ console.print(f" [red]Failed: {report.failed_checks}[/red]")
156
+ if report.timeout_checks > 0:
157
+ console.print(f" [yellow]Timeout: {report.timeout_checks}[/yellow]")
158
+ if report.skipped_checks > 0:
159
+ console.print(f" [dim]Skipped: {report.skipped_checks}[/dim]")
160
+ console.print(f" Total duration: {report.total_duration:.2f}s")
161
+
162
+ # Show errors if verbose
163
+ if verbose:
164
+ for check in report.checks:
165
+ if check.error:
166
+ console.print(f"\n[bold red]{check.name} Error:[/bold red]")
167
+ console.print(f"[dim]{check.error}[/dim]")
168
+ if check.output and check.status.value == "failed":
169
+ console.print(f"\n[bold red]{check.name} Output:[/bold red]")
170
+ console.print(f"[dim]{check.output[:500]}[/dim]") # Limit output
171
+
172
+ # Write report if requested
173
+ if out is None:
174
+ # Use default path
175
+ out = SpecFactStructure.get_timestamped_report_path("enforcement", repo, "yaml")
176
+ SpecFactStructure.ensure_structure(repo)
177
+
178
+ out.parent.mkdir(parents=True, exist_ok=True)
179
+ dump_yaml(report.to_dict(), out)
180
+ console.print(f"\n[dim]Report written to: {out}[/dim]")
181
+
182
+ # Exit with appropriate code
183
+ exit_code = report.get_exit_code()
184
+ if exit_code == 0:
185
+ console.print("\n[bold green]✓[/bold green] All validations passed!")
186
+ console.print("[dim]Reproducibility verified[/dim]")
187
+ elif exit_code == 1:
188
+ console.print("\n[bold red]✗[/bold red] Some validations failed")
189
+ raise typer.Exit(1)
190
+ else:
191
+ console.print("\n[yellow]⏱[/yellow] Budget exceeded")
192
+ 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
13
+
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.progress import Progress, SpinnerColumn, TextColumn
17
+
18
+ from specfact_cli.models.plan import PlanBundle
19
+ from specfact_cli.sync.speckit_sync import SpecKitSync
20
+
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: PlanBundle | None = 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: Path | None = 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: Path | None = 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,25 @@
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
+
16
+ # Define what gets imported with "from specfact_cli.common import *"
17
+ __all__ = [
18
+ "LoggerSetup",
19
+ "TextUtils",
20
+ "compute_sha256",
21
+ "dump_json",
22
+ "ensure_directory",
23
+ "get_bridge_logger",
24
+ "load_json",
25
+ ]