# mixpanel_headless development commands
# Run `just` to see available commands

# Default: list available commands
default:
    @just --list

# Run all checks — must be a strict superset of CI (.github/workflows/ci.yml).
# CI runs the same commands plus HYPOTHESIS_PROFILE=ci for tests; that
# profile is the only documented difference (deterministic seed, 200 examples
# vs default 100). Locally we use the default profile for faster iteration.
# The conformance corpus has its own workflow and recipe (`just conformance`).
check: lint fmt-check typecheck docstring-cov test-cov build

# Install git hooks so commits are blocked on lint/format failures BEFORE
# they reach CI. Run once after cloning the repo.
install-hooks:
    uv run --group dev pre-commit install

# Run tests
test *args:
    uv run pytest {{ args }}

# Run tests with dev Hypothesis profile (fast, 10 examples)
test-dev *args:
    HYPOTHESIS_PROFILE=dev uv run pytest {{ args }}

# Run tests with CI Hypothesis profile (thorough, 200 examples, deterministic)
test-ci *args:
    HYPOTHESIS_PROFILE=ci uv run pytest {{ args }}

# Run property-based tests only
test-pbt *args:
    uv run pytest -k "_pbt" {{ args }}

# Run property-based tests with dev profile (fast iteration)
test-pbt-dev *args:
    HYPOTHESIS_PROFILE=dev uv run pytest -k "_pbt" {{ args }}

# Run property-based tests with CI profile (thorough)
test-pbt-ci *args:
    HYPOTHESIS_PROFILE=ci uv run pytest -k "_pbt" {{ args }}

# Run the auth-subsystem fast iteration suite (042 redesign)
test-auth *args:
    uv run pytest tests/unit/test_account.py tests/unit/test_session.py \
        tests/unit/test_resolver.py tests/unit/test_workspace_use.py \
        tests/unit/test_config_v3.py tests/pbt/test_account_pbt.py \
        tests/pbt/test_resolver_pbt.py tests/pbt/test_session_pbt.py \
        -v {{ args }}

# Run the 042 live auth QA against real Mixpanel API (requires creds)
# Set env vars before running:
#   MP_LIVE_SA_USERNAME / _SA_SECRET / _SA_PROJECT_ID / _SA_REGION  (Cat A, D, G)
#   MP_LIVE_OAUTH_TOKEN / _PROJECT_ID / _REGION                     (Cat C, D, G)
#   (Cat B uses ~/.mp/oauth/tokens_us.json automatically)
test-live-auth *args:
    MP_LIVE_TESTS=1 uv run pytest tests/live/test_042_auth_redesign_live.py -v -m live {{ args }}

# === Mutation Testing ===

# Run mutation testing on entire codebase
mutate *args:
    uv run mutmut run {{ args }}

# Show mutation testing results
mutate-results:
    uv run mutmut results

# Show details for a specific mutant (e.g., just mutate-show 1)
mutate-show id:
    uv run mutmut show {{ id }}

# Apply a mutant to see the change (use 0 to reset)
mutate-apply id:
    uv run mutmut apply {{ id }}

# Check mutation score meets threshold (default 80%)
mutate-check threshold="80":
    #!/usr/bin/env bash
    set -euo pipefail
    RESULTS=$(uv run mutmut results 2>/dev/null | tail -1)
    KILLED=$(echo "$RESULTS" | grep -oP 'Killed \K\d+' || echo 0)
    SURVIVED=$(echo "$RESULTS" | grep -oP 'Survived \K\d+' || echo 0)
    TOTAL=$((KILLED + SURVIVED))
    if [ "$TOTAL" -eq 0 ]; then
        echo "No mutants found"
        exit 1
    fi
    SCORE=$((KILLED * 100 / TOTAL))
    echo "Mutation score: $SCORE% (killed $KILLED/$TOTAL, threshold {{ threshold }}%)"
    if [ "$SCORE" -lt {{ threshold }} ]; then
        echo "FAIL: Mutation score below threshold"
        exit 1
    fi
    echo "PASS: Mutation score meets threshold"

# Run tests with coverage (fails if below 90%)
test-cov:
    uv run pytest --cov=src/mixpanel_headless --cov-report=term-missing --cov-fail-under=90

# === Conformance (recorded corpus that the TypeScript port replays) ===
# Maintainer tooling. Not part of `check`: library PRs never touch the corpus,
# and the corpus is re-pinned once per release (conformance/record/README.md,
# "When to re-pin"). The Conformance workflow runs these checks on PRs that
# change conformance/, after each merge to main, and on each release.

# Conformance typecheck, tooling tests, corpus runner, and stamp guard: the
# local steps of the Conformance workflow. Leaves out the record-mode drift
# re-extraction (slow; the workflow runs it). Between releases, drift fails
# some of these steps; that is expected until the next re-pin.
conformance:
    #!/usr/bin/env bash
    set -euo pipefail
    uv run mypy conformance/
    uv run pytest conformance/tests -o addopts="" -q
    uv run pytest conformance/runner -o addopts="" -q
    just conformance-stamps

# Stamp-provenance guard (mirrors the CI step of the same name): every
# corpus/contract stamp must be a 40-hex SHA reachable from origin/main, and
# in-scope corpus content must not change against the merge-base with
# origin/main unless manifest.source_commit changes too. Needs a fetched
# origin/main. Protocol: conformance/record/README.md, "Which SHA to stamp".
conformance-stamps *args:
    uv run python -m conformance.record.check_stamps --main-ref origin/main --base-ref origin/main {{ args }}

# Record-mode vector extraction over the full non-live suite (design D1.3/D3).
# `-o addopts=""` is required: the repo default addopts pollute collection.
# `uv run python -m pytest` (NOT bare `uv run pytest`) is required: only the
# `-m` form puts the repo root on sys.path so `-p conformance.record.plugin`
# can import (found at PR-5 first invocation).
# Manual recipe — NOT part of `check`; the extraction commit is a deliberate
# act (D3 regeneration story). Pass --mp-record-date/--mp-record-commit via
# args to reproduce committed manifest stamps byte-for-byte (D8 drift check).
conformance-record *args:
    #!/usr/bin/env bash
    set -euo pipefail
    EXCLUSIONS=""
    if [ -f conformance/record/exclusions.args ]; then
        EXCLUSIONS=$(cat conformance/record/exclusions.args)
    fi
    uv run python -m pytest tests -p conformance.record.plugin \
        --mp-record-vectors=conformance/vectors \
        -o addopts="" -m "not live" $EXCLUSIONS {{ args }}

# Deliberate-break smoke test: control worktree + 13 sabotage patches (D9).
# Manual/release-gate recipe — tens of minutes of worktree+sync cycles;
# never part of `check` or per-PR CI. Lands with PR-8.
conformance-smoke *args:
    uv run python -m conformance.smoke.run_smoke {{ args }}

# === Hypothesis CLI ===

# Refactor deprecated Hypothesis code (e.g., just hypo-codemod src/)
hypo-codemod *args:
    uv run hypothesis codemod {{ args }}

# Generate property-based tests for a module (e.g., just hypo-write mixpanel_headless.types)
hypo-write *args:
    uv run hypothesis write {{ args }}

# Lint code with ruff
lint *args:
    uv run ruff check src/ tests/ conformance/ {{ args }}

# Fix lint errors automatically
lint-fix:
    uv run ruff check --fix src/ tests/ conformance/

# Format code with ruff
fmt:
    uv run ruff format src/ tests/ conformance/

# Check formatting without applying changes
fmt-check:
    uv run ruff format --check src/ tests/ conformance/

# Type check with mypy (`just conformance` type-checks conformance/)
typecheck:
    uv run mypy src/ tests/

# Docstring coverage — src is gated at 99% (see [tool.interrogate] in
# pyproject.toml); tests is gated at 95% via the second invocation. Bump
# either threshold up if coverage rises and you want to lock the gain in.
docstring-cov:
    uv run interrogate src/
    uv run interrogate tests/ --fail-under=95
    uv run interrogate conformance/ --fail-under=100

# Sync dependencies
sync:
    uv sync --all-extras

# Clean build artifacts and caches
clean:
    rm -rf .pytest_cache .mypy_cache .ruff_cache
    rm -rf dist build *.egg-info
    rm -rf .coverage htmlcov
    find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

# Build package
build: clean
    uv build

# === Documentation ===

# Generate man pages for the CLI
man:
    uv run python -c "from typer.main import get_command; from mixpanel_headless.cli.main import app; from click_man.core import write_man_pages; write_man_pages('mp', get_command(app), target_dir='./man')"

# Build documentation
docs:
    uv run mkdocs build

# Serve documentation locally with live reload
docs-serve:
    uv run mkdocs serve

# Deploy docs to GitHub Pages (manual, uses gh-pages branch)
# --force is used because gh-pages contains only build artifacts, not source history
docs-deploy:
    uv run mkdocs gh-deploy --force

# Clean documentation build artifacts
docs-clean:
    rm -rf site/

# === Plugin Development ===

# Validate the plugin: claude plugin validate --strict (skipped without the claude CLI), then the content guards
plugin-validate:
    #!/usr/bin/env bash
    set -euo pipefail
    if command -v claude >/dev/null 2>&1; then
        claude plugin validate mixpanel-plugin --strict
    else
        echo "⚠ claude CLI not found; skipping 'claude plugin validate' (the content guards still run)"
    fi
    uv run pytest tests/unit/plugin -q

# plugin.json wins silently when both files set a version, so the root
# marketplace.json entry must not carry one.
# Check that only plugin.json sets the plugin version
plugin-check-version:
    #!/usr/bin/env bash
    set -euo pipefail
    PLUGIN_VERSION=$(jq -r '.version // empty' mixpanel-plugin/.claude-plugin/plugin.json)
    MARKETPLACE_VERSION=$(jq -r '.plugins[] | select(.name == "mixpanel-headless") | .version // empty' .claude-plugin/marketplace.json)
    if [ -z "$MARKETPLACE_VERSION" ]; then
        echo "✓ Version set in plugin.json only: ${PLUGIN_VERSION:-<unset>}"
    else
        echo "✗ .claude-plugin/marketplace.json sets version $MARKETPLACE_VERSION for mixpanel-headless"
        echo "  plugin.json sets:  ${PLUGIN_VERSION:-<unset>}"
        echo "  Remove the marketplace version; plugin.json wins silently when both are set."
        exit 1
    fi

# The eval grants differ from the skills' allowed-tools on purpose: the eval
# child ignores skill allowed-tools (only --allow-tools grants), and it has no
# plugin venv, because ${CLAUDE_PLUGIN_DATA} is a new empty folder per run and
# cannot be named here. So the list grants the venv by wildcard path and the
# skills' read-only look-up fallback to an `mp` on PATH (the same three exact
# grants the skills carry), plus the read-only Grep and Glob
# that the harness gates. --allow-tools takes a list, so
# --no-publish ends it before the caller's args. --no-publish keeps the report
# on this machine; --trust-plugin answers the first-run prompt for this repo's plugin.
# Run the offline plugin eval suite (costs model calls; not part of `check`)
plugin-eval *args:
    claude plugin eval mixpanel-plugin --tag offline --allow-tools "Bash(mp --version)" "Bash(mp help)" "Bash(mp help *)" "Bash(uv run *)" "Bash(*/venv/bin/mp *)" "Bash(*/venv/bin/python *)" Read Grep Glob Write Edit "WebFetch(domain:mixpanel.github.io)" --no-publish --trust-plugin {{ args }}

# Plugin statistics: skills, entry-file lines, reference files
plugin-stats:
    #!/usr/bin/env bash
    set -euo pipefail
    echo "📊 Plugin statistics:"
    echo ""
    TOTAL_LINES=0
    TOTAL_REFS=0
    SKILL_COUNT=0
    for skill_md in mixpanel-plugin/skills/*/SKILL.md; do
        dir=$(dirname "$skill_md")
        name=$(basename "$dir")
        lines=$(wc -l < "$skill_md" | tr -d ' ')
        ref_count=0
        ref_lines=0
        if [ -d "$dir/references" ]; then
            ref_count=$(find "$dir/references" -name "*.md" -type f | wc -l | tr -d ' ')
            if [ "$ref_count" -gt 0 ]; then
                ref_lines=$(find "$dir/references" -name "*.md" -type f -exec cat {} + | wc -l | tr -d ' ')
            fi
        fi
        printf "%-18s SKILL.md %4s lines   references %2s files, %5s lines\n" "$name" "$lines" "$ref_count" "$ref_lines"
        SKILL_COUNT=$((SKILL_COUNT + 1))
        TOTAL_REFS=$((TOTAL_REFS + ref_count))
        TOTAL_LINES=$((TOTAL_LINES + lines + ref_lines))
    done
    echo ""
    echo "Skills:      $SKILL_COUNT"
    echo "References:  $TOTAL_REFS files"
    echo "Total:       $TOTAL_LINES lines"
