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.
- specfact_cli/__init__.py +14 -0
- specfact_cli/agents/__init__.py +23 -0
- specfact_cli/agents/analyze_agent.py +392 -0
- specfact_cli/agents/base.py +95 -0
- specfact_cli/agents/plan_agent.py +202 -0
- specfact_cli/agents/registry.py +176 -0
- specfact_cli/agents/sync_agent.py +133 -0
- specfact_cli/analyzers/__init__.py +10 -0
- specfact_cli/analyzers/code_analyzer.py +775 -0
- specfact_cli/cli.py +397 -0
- specfact_cli/commands/__init__.py +7 -0
- specfact_cli/commands/enforce.py +87 -0
- specfact_cli/commands/import_cmd.py +355 -0
- specfact_cli/commands/init.py +119 -0
- specfact_cli/commands/plan.py +1090 -0
- specfact_cli/commands/repro.py +172 -0
- specfact_cli/commands/sync.py +408 -0
- specfact_cli/common/__init__.py +24 -0
- specfact_cli/common/logger_setup.py +673 -0
- specfact_cli/common/logging_utils.py +41 -0
- specfact_cli/common/text_utils.py +52 -0
- specfact_cli/common/utils.py +48 -0
- specfact_cli/comparators/__init__.py +10 -0
- specfact_cli/comparators/plan_comparator.py +391 -0
- specfact_cli/generators/__init__.py +13 -0
- specfact_cli/generators/plan_generator.py +105 -0
- specfact_cli/generators/protocol_generator.py +115 -0
- specfact_cli/generators/report_generator.py +200 -0
- specfact_cli/generators/workflow_generator.py +111 -0
- specfact_cli/importers/__init__.py +6 -0
- specfact_cli/importers/speckit_converter.py +773 -0
- specfact_cli/importers/speckit_scanner.py +704 -0
- specfact_cli/models/__init__.py +32 -0
- specfact_cli/models/deviation.py +105 -0
- specfact_cli/models/enforcement.py +150 -0
- specfact_cli/models/plan.py +97 -0
- specfact_cli/models/protocol.py +28 -0
- specfact_cli/modes/__init__.py +18 -0
- specfact_cli/modes/detector.py +126 -0
- specfact_cli/modes/router.py +153 -0
- specfact_cli/sync/__init__.py +11 -0
- specfact_cli/sync/repository_sync.py +279 -0
- specfact_cli/sync/speckit_sync.py +388 -0
- specfact_cli/utils/__init__.py +57 -0
- specfact_cli/utils/console.py +69 -0
- specfact_cli/utils/feature_keys.py +213 -0
- specfact_cli/utils/git.py +241 -0
- specfact_cli/utils/ide_setup.py +381 -0
- specfact_cli/utils/prompts.py +179 -0
- specfact_cli/utils/structure.py +496 -0
- specfact_cli/utils/yaml_utils.py +200 -0
- specfact_cli/validators/__init__.py +19 -0
- specfact_cli/validators/fsm.py +260 -0
- specfact_cli/validators/repro_checker.py +320 -0
- specfact_cli/validators/schema.py +200 -0
- specfact_cli-0.4.0.dist-info/METADATA +332 -0
- specfact_cli-0.4.0.dist-info/RECORD +60 -0
- specfact_cli-0.4.0.dist-info/WHEEL +4 -0
- specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|