Skip to content

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)