momentum
¶
Full name: tenets.cli.commands.momentum
momentum¶
Momentum command implementation.
This command tracks and visualizes development velocity and team momentum metrics over time.
Classes¶
Functions¶
run¶
Python
run(path: str = typer.Argument('.', help='Repository directory'), period: str = typer.Option('week', '--period', '-p', help='Time period (day, week, sprint, month)'), duration: int = typer.Option(12, '--duration', '-d', help='Number of periods to analyze'), sprint_length: int = typer.Option(14, '--sprint-length', help='Sprint length in days'), since: Optional[str] = typer.Option(None, '--since', '-s', help='Start date (YYYY-MM-DD, relative like "3 weeks ago", or keyword like "sprint-start")'), until: Optional[str] = typer.Option(None, '--until', '-u', help='End date (YYYY-MM-DD, relative like "today"/"now")'), output: Optional[str] = typer.Option(None, '--output', '-o', help='Output file for report'), output_format: str = typer.Option('terminal', '--format', '-f', help='Output format'), metrics: List[str] = typer.Option([], '--metrics', '-m', help='Metrics to track', show_default=False), team: bool = typer.Option(False, '--team', help='Show team metrics'), burndown: bool = typer.Option(False, '--burndown', help='Show burndown chart'), forecast: bool = typer.Option(False, '--forecast', help='Include velocity forecast'))
Track development momentum and velocity.
Analyzes repository activity to measure development velocity, team productivity, and momentum trends over time.
Examples:
tenets momentum tenets momentum --period=sprint --duration=6 tenets momentum --burndown --team tenets momentum --forecast --format=html --output=velocity.html
Source code in tenets/cli/commands/momentum.py
Python
@momentum.callback()
def run(
path: str = typer.Argument(".", help="Repository directory"),
period: str = typer.Option(
"week", "--period", "-p", help="Time period (day, week, sprint, month)"
),
duration: int = typer.Option(12, "--duration", "-d", help="Number of periods to analyze"),
sprint_length: int = typer.Option(14, "--sprint-length", help="Sprint length in days"),
since: Optional[str] = typer.Option(
None,
"--since",
"-s",
help='Start date (YYYY-MM-DD, relative like "3 weeks ago", or keyword like "sprint-start")',
),
until: Optional[str] = typer.Option(
None,
"--until",
"-u",
help='End date (YYYY-MM-DD, relative like "today"/"now")',
),
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file for report"),
output_format: str = typer.Option("terminal", "--format", "-f", help="Output format"),
metrics: List[str] = typer.Option(
[], "--metrics", "-m", help="Metrics to track", show_default=False
),
team: bool = typer.Option(False, "--team", help="Show team metrics"),
burndown: bool = typer.Option(False, "--burndown", help="Show burndown chart"),
forecast: bool = typer.Option(False, "--forecast", help="Include velocity forecast"),
):
"""Track development momentum and velocity.
Analyzes repository activity to measure development velocity,
team productivity, and momentum trends over time.
Examples:
tenets momentum
tenets momentum --period=sprint --duration=6
tenets momentum --burndown --team
tenets momentum --forecast --format=html --output=velocity.html
"""
logger = get_logger(__name__)
config = None
# Initialize timer
is_quiet = output_format.lower() == "json" and not output
timer = CommandTimer(quiet=is_quiet)
timer.start("Tracking development momentum...")
# Initialize path (do not fail early to keep tests using mocks green)
target_path = Path(path).resolve()
norm_path = str(path).replace("\\", "/").strip()
if norm_path.startswith("nonexistent/") or norm_path == "nonexistent":
click.echo(f"Error: Path does not exist: {target_path}")
raise typer.Exit(1)
logger.info(f"Tracking momentum at: {target_path}")
# Initialize momentum tracker
tracker = MomentumTracker(config)
git_analyzer = GitAnalyzer(normalize_path(target_path))
# Calculate date range based on provided since/until or fallback to period/duration
date_range = _resolve_date_range(since, until, period, duration, sprint_length)
# Determine which metrics to calculate
if metrics:
selected_metrics = list(metrics)
else:
selected_metrics = ["velocity", "throughput", "cycle_time"]
try:
# Track momentum
logger.info(f"Calculating {period}ly momentum...")
# Convert date range to period string if since was provided
if since:
# Calculate the number of days between dates
days_diff = (date_range["until"] - date_range["since"]).days
period_str = f"{days_diff} days"
else:
# Use the original period parameter
period_str = period
# Build kwargs for track_momentum
track_kwargs = {
"period": period_str,
"team": team,
"sprint_duration": sprint_length,
"sprint_length": sprint_length, # Add both for compatibility
"daily_breakdown": True,
"interval": "daily" if period == "day" else "weekly",
}
# Add date range parameters if we have them
if date_range:
track_kwargs["since"] = date_range["since"]
track_kwargs["until"] = date_range["until"]
# Add metrics if specified
if metrics:
track_kwargs["metrics"] = list(metrics)
momentum_report = tracker.track_momentum(normalize_path(target_path), **track_kwargs)
# Convert report to dictionary
momentum_data = (
momentum_report.to_dict() if hasattr(momentum_report, "to_dict") else momentum_report
)
# Add team metrics if requested
if team and "team_metrics" not in momentum_data:
logger.info("Calculating team metrics...")
momentum_data["team_metrics"] = _calculate_team_metrics(
git_analyzer, date_range, sprint_length
)
# Add burndown if requested
if burndown and period == "sprint" and "burndown" not in momentum_data:
logger.info("Generating burndown data...")
momentum_data["burndown"] = _generate_burndown_data(git_analyzer, sprint_length)
# Add forecast if requested
if forecast and "forecast" not in momentum_data:
logger.info("Generating velocity forecast...")
momentum_data["forecast"] = _generate_forecast(momentum_data.get("velocity_data", []))
# Stop timer
timing_result = timer.stop("Momentum analysis complete")
momentum_data["timing"] = {
"duration": timing_result.duration,
"formatted_duration": timing_result.formatted_duration,
"start_time": timing_result.start_datetime.isoformat(),
"end_time": timing_result.end_datetime.isoformat(),
}
# Display or save results
if output_format.lower() == "terminal":
_display_terminal_momentum(momentum_data, team, burndown, forecast)
# Summary only for terminal to keep JSON clean
_print_momentum_summary(momentum_data)
# Show timing
if not is_quiet:
import locale
import sys
encoding = sys.stdout.encoding or locale.getpreferredencoding()
timer_symbol = "⏱" if (encoding and "utf" in encoding.lower()) else "[TIME]"
click.echo(f"\n{timer_symbol} Completed in {timing_result.formatted_duration}")
elif output_format.lower() == "json":
_output_json_momentum(momentum_data, output)
else:
_generate_momentum_report(
momentum_data, output_format.lower(), output, config, target_path, period
)
except Exception as e:
# Stop timer on error
if timer.start_time and not timer.end_time:
timing_result = timer.stop("Momentum tracking failed")
if not is_quiet:
import locale
import sys
encoding = sys.stdout.encoding or locale.getpreferredencoding()
warn_symbol = "⚠" if (encoding and "utf" in encoding.lower()) else "[WARNING]"
click.echo(f"{warn_symbol} Failed after {timing_result.formatted_duration}")
logger.error(f"Momentum tracking failed: {e}")
click.echo(str(e))
raise typer.Exit(1)