perfgate-cli 0.18.1

CLI for perfgate performance budgets and baseline diffs
//! Probe starter template generation.

use anyhow::Context;
use std::fs;
use std::path::Path;

use crate::{ProbeInitArgs, ProbeTemplate};

pub(crate) fn execute_probe_init(args: ProbeInitArgs) -> anyhow::Result<()> {
    let template = probe_template(args.template);
    fs::create_dir_all(&args.out_dir)
        .with_context(|| format!("create probe template directory {}", args.out_dir.display()))?;

    write_probe_template_file(
        &args.out_dir.join("README.md"),
        &render_probe_template_readme(&template, &args.out_dir),
        args.force,
    )?;
    write_probe_template_file(
        &args.out_dir.join("probes-baseline.jsonl"),
        &format!("{}\n", template.baseline_jsonl.join("\n")),
        args.force,
    )?;
    write_probe_template_file(
        &args.out_dir.join("probes-current.jsonl"),
        &format!("{}\n", template.current_jsonl.join("\n")),
        args.force,
    )?;
    write_probe_template_file(
        &args.out_dir.join("scenario.toml"),
        &render_probe_template_scenario(&template),
        args.force,
    )?;
    write_probe_template_file(
        &args.out_dir.join("tradeoff.toml"),
        &render_probe_template_tradeoff(&template),
        args.force,
    )?;

    eprintln!(
        "Probe starter template '{}' written to {}",
        template.slug,
        args.out_dir.display()
    );
    eprintln!("Review the generated snippets before adding them to perfgate.toml.");
    eprintln!(
        "Next: perfgate ingest probes --file {}/probes-current.jsonl --out artifacts/perfgate/probes-current.json",
        args.out_dir.display()
    );
    Ok(())
}

fn write_probe_template_file(path: &Path, content: &str, force: bool) -> anyhow::Result<()> {
    if path.exists() && !force {
        anyhow::bail!(
            "{} already exists; pass --force to overwrite generated probe starter files",
            path.display()
        );
    }

    fs::write(path, content).with_context(|| format!("write probe template {}", path.display()))
}

#[derive(Debug, Clone, Copy)]
struct ProbeStarterTemplate {
    slug: &'static str,
    title: &'static str,
    bench: &'static str,
    scenario: &'static str,
    workload: &'static str,
    dominant_probe: &'static str,
    local_probe: &'static str,
    support_probe: &'static str,
    baseline_jsonl: &'static [&'static str],
    current_jsonl: &'static [&'static str],
}

fn probe_template(template: ProbeTemplate) -> ProbeStarterTemplate {
    match template {
        ProbeTemplate::Parser => ProbeStarterTemplate {
            slug: "parser",
            title: "Parser pipeline probes",
            bench: "parser",
            scenario: "large_file_parse",
            workload: "large-file parser throughput",
            dominant_probe: "parser.batch_loop",
            local_probe: "parser.tokenize",
            support_probe: "parser.ast_build",
            baseline_jsonl: &[
                r#"{"probe":"parser.tokenize","scope":"local","wall_ms":12.4,"items":10000}"#,
                r#"{"probe":"parser.ast_build","scope":"local","wall_ms":28.2,"items":10000}"#,
                r#"{"probe":"parser.batch_loop","scope":"dominant","wall_ms":44.8,"items":10000}"#,
            ],
            current_jsonl: &[
                r#"{"probe":"parser.tokenize","scope":"local","wall_ms":12.8,"items":10000}"#,
                r#"{"probe":"parser.ast_build","scope":"local","wall_ms":27.5,"items":10000}"#,
                r#"{"probe":"parser.batch_loop","scope":"dominant","wall_ms":39.6,"items":10000}"#,
            ],
        },
        ProbeTemplate::Batch => ProbeStarterTemplate {
            slug: "batch",
            title: "Batch processing probes",
            bench: "batch",
            scenario: "batch_transform",
            workload: "batch transform throughput",
            dominant_probe: "batch.transform",
            local_probe: "batch.read_inputs",
            support_probe: "batch.write_outputs",
            baseline_jsonl: &[
                r#"{"probe":"batch.read_inputs","scope":"local","wall_ms":18.0,"items":5000}"#,
                r#"{"probe":"batch.transform","scope":"dominant","wall_ms":92.0,"items":5000}"#,
                r#"{"probe":"batch.write_outputs","scope":"local","wall_ms":26.0,"items":5000}"#,
            ],
            current_jsonl: &[
                r#"{"probe":"batch.read_inputs","scope":"local","wall_ms":18.4,"items":5000}"#,
                r#"{"probe":"batch.transform","scope":"dominant","wall_ms":82.0,"items":5000}"#,
                r#"{"probe":"batch.write_outputs","scope":"local","wall_ms":25.2,"items":5000}"#,
            ],
        },
        ProbeTemplate::Cli => ProbeStarterTemplate {
            slug: "cli",
            title: "CLI workflow probes",
            bench: "cli-command",
            scenario: "cli_user_path",
            workload: "CLI command response path",
            dominant_probe: "cli.execute",
            local_probe: "cli.parse_args",
            support_probe: "cli.render_output",
            baseline_jsonl: &[
                r#"{"probe":"cli.parse_args","scope":"local","wall_ms":3.2,"items":1}"#,
                r#"{"probe":"cli.execute","scope":"dominant","wall_ms":42.0,"items":1}"#,
                r#"{"probe":"cli.render_output","scope":"local","wall_ms":5.8,"items":1}"#,
            ],
            current_jsonl: &[
                r#"{"probe":"cli.parse_args","scope":"local","wall_ms":3.3,"items":1}"#,
                r#"{"probe":"cli.execute","scope":"dominant","wall_ms":37.8,"items":1}"#,
                r#"{"probe":"cli.render_output","scope":"local","wall_ms":5.9,"items":1}"#,
            ],
        },
        ProbeTemplate::Server => ProbeStarterTemplate {
            slug: "server",
            title: "Server request probes",
            bench: "server-request",
            scenario: "server_local_request",
            workload: "controlled local server request path",
            dominant_probe: "server.handle_request",
            local_probe: "server.decode_request",
            support_probe: "server.encode_response",
            baseline_jsonl: &[
                r#"{"probe":"server.decode_request","scope":"local","wall_ms":4.4,"items":100}"#,
                r#"{"probe":"server.handle_request","scope":"dominant","wall_ms":31.0,"items":100}"#,
                r#"{"probe":"server.encode_response","scope":"local","wall_ms":6.5,"items":100}"#,
            ],
            current_jsonl: &[
                r#"{"probe":"server.decode_request","scope":"local","wall_ms":4.5,"items":100}"#,
                r#"{"probe":"server.handle_request","scope":"dominant","wall_ms":27.0,"items":100}"#,
                r#"{"probe":"server.encode_response","scope":"local","wall_ms":6.7,"items":100}"#,
            ],
        },
    }
}

fn render_probe_template_readme(template: &ProbeStarterTemplate, out_dir: &Path) -> String {
    let out_dir = out_dir.display();
    format!(
        r#"# {title}

This starter shows a small probe set for {workload}. Review and edit the probe
names before using them in durable decision policy.

Generated files:

- `probes-baseline.jsonl`: reviewed baseline probe events
- `probes-current.jsonl`: current-run probe events for local experimentation
- `scenario.toml`: scenario snippet to copy into `perfgate.toml`
- `tradeoff.toml`: tradeoff snippet to copy into `perfgate.toml`

Next:

```bash
perfgate ingest probes --file {out_dir}/probes-baseline.jsonl --out baselines/probes.json
perfgate ingest probes --file {out_dir}/probes-current.jsonl --out artifacts/perfgate/probes-current.json
perfgate probe compare --baseline baselines/probes.json --current artifacts/perfgate/probes-current.json --out artifacts/perfgate/probe-compare.json
perfgate decision suggest --config perfgate.toml
```

Do not:

- keep probes that no reviewer can act on;
- use generated sample values as release proof;
- turn a temporary debugging span into a durable probe id.
"#,
        title = template.title,
        workload = template.workload,
        out_dir = out_dir
    )
}

fn render_probe_template_scenario(template: &ProbeStarterTemplate) -> String {
    format!(
        r#"# Copy into perfgate.toml after reviewing paths and names.
[[scenario]]
name = "{scenario}"
bench = "{bench}"
weight = 1.0
probe_baseline = "baselines/probes.json"
probe_current = "artifacts/perfgate/probes-current.json"
probe_compare = "artifacts/perfgate/probe-compare.json"
"#,
        scenario = template.scenario,
        bench = template.bench
    )
}

fn render_probe_template_tradeoff(template: &ProbeStarterTemplate) -> String {
    format!(
        r#"# Copy into perfgate.toml only when reviewers need a bounded tradeoff.
[[tradeoff]]
name = "{slug}-dominant-probe-improvement"
if_failed = "wall_ms"
downgrade_to = "warn"

[[tradeoff.require]]
metric = "wall_ms"
probe = "{dominant_probe}"
min_improvement_ratio = 1.10

[[tradeoff.allow]]
metric = "wall_ms"
probe = "{local_probe}"
max_regression = 0.03

# Supporting probe to keep visible in reviews: {support_probe}
"#,
        slug = template.slug,
        dominant_probe = template.dominant_probe,
        local_probe = template.local_probe,
        support_probe = template.support_probe
    )
}