Files
tracer-cloud--opensre/surfaces/cli/commands/misses.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

225 lines
7.1 KiB
Python

"""``opensre misses`` — triage and export investigation misses as regressions.
Misses are written to ``~/.opensre/misses.jsonl`` by the post-investigation
feedback prompt whenever a user rates an outcome ``partial`` or ``inaccurate``.
These commands let humans and the weekly automation read that store, surface
recurrence trends, and convert the top offenders into benchmark scenarios that
the existing eval runner consumes.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import click
from rich.console import Console
from rich.table import Table
from core.domain.feedback import (
MissTaxonomy,
compute_stats,
load_misses,
misses_path,
to_benchmark_scenario,
)
from core.domain.feedback.misses import (
export_scenarios,
filter_top_misses,
parse_since,
)
_console = Console(highlight=False)
_TAXONOMY_VALUES = [t.value for t in MissTaxonomy]
def _parse_since_option(value: str | None) -> Any:
if not value:
return None
try:
return parse_since(value)
except ValueError as exc:
raise click.BadParameter(str(exc), param_hint="--since") from None
@click.group(name="misses")
def misses_command() -> None:
"""Investigation miss triage and eval scenario export."""
@misses_command.command(name="list")
@click.option(
"--since",
"since_spec",
default=None,
help="Only misses captured after this point. e.g. 7d, 24h, 2w, or an ISO timestamp.",
)
@click.option(
"--taxonomy",
type=click.Choice(_TAXONOMY_VALUES, case_sensitive=False),
default=None,
help="Filter by taxonomy bucket.",
)
@click.option("--json", "json_out", is_flag=True, help="Emit JSON instead of a table.")
@click.option("--limit", type=int, default=50, show_default=True, help="Max rows to render.")
def list_command(since_spec: str | None, taxonomy: str | None, json_out: bool, limit: int) -> None:
"""List recent misses."""
since = _parse_since_option(since_spec)
rows = load_misses(since=since, taxonomy=taxonomy)
rows.sort(key=lambda r: r.get("timestamp", ""), reverse=True)
rows = rows[:limit]
if json_out:
click.echo(json.dumps(rows, indent=2, ensure_ascii=False))
return
if not rows:
_console.print(f"[dim]No misses recorded in scope. Store: {misses_path()}[/]")
return
table = Table(show_lines=False)
table.add_column("when", style="dim")
table.add_column("alert_name")
table.add_column("taxonomy")
table.add_column("rating")
table.add_column("root cause", overflow="fold")
for row in rows:
table.add_row(
(row.get("timestamp") or "")[:19],
row.get("alert_name") or "<unknown>",
row.get("taxonomy") or MissTaxonomy.UNKNOWN.value,
row.get("rating") or "",
(row.get("root_cause") or "")[:80],
)
_console.print(table)
_console.print(f"[dim]{len(rows)} miss(es) shown · store: {misses_path()}[/]")
@misses_command.command(name="stats")
@click.option(
"--since",
"since_spec",
default=None,
help="Only misses captured after this point. e.g. 7d, 24h, 2w.",
)
@click.option("--json", "json_out", is_flag=True, help="Emit JSON instead of a table.")
def stats_command(since_spec: str | None, json_out: bool) -> None:
"""Show taxonomy breakdown and recurrence — the CLI dashboard."""
since = _parse_since_option(since_spec)
rows = load_misses(since=since)
stats = compute_stats(rows)
if json_out:
click.echo(json.dumps(stats, indent=2, ensure_ascii=False, default=str))
return
_console.print(
f"[bold]Closed-loop learning · {stats['total']} miss(es) across "
f"{stats['unique_alerts']} alert(s)[/]"
)
by_taxonomy = Table(title="By taxonomy", title_justify="left")
by_taxonomy.add_column("taxonomy")
by_taxonomy.add_column("count", justify="right")
for tax in _TAXONOMY_VALUES:
by_taxonomy.add_row(tax, str(stats["by_taxonomy"].get(tax, 0)))
_console.print(by_taxonomy)
if stats["recurring"]:
recurring = Table(title="Recurring misses (2+ occurrences)", title_justify="left")
recurring.add_column("alert_name")
recurring.add_column("taxonomy")
recurring.add_column("count", justify="right")
for alert, tax, count in stats["recurring"]:
recurring.add_row(alert or "<unknown>", tax or "", str(count))
_console.print(recurring)
else:
_console.print("[dim]No recurring (alert, taxonomy) pairs in scope.[/]")
@misses_command.command(name="export")
@click.option(
"--out",
"out_dir",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
required=True,
help="Directory to write per-case alert.json files into.",
)
@click.option(
"--since",
"since_spec",
default="7d",
show_default=True,
help="Only convert misses captured after this point.",
)
@click.option(
"--top",
type=int,
default=10,
show_default=True,
help="Maximum scenarios to write (deduped by alert+taxonomy).",
)
@click.option(
"--taxonomy",
type=click.Choice(_TAXONOMY_VALUES, case_sensitive=False),
default=None,
help="Only export misses in this bucket.",
)
def export_command(out_dir: Path, since_spec: str, top: int, taxonomy: str | None) -> None:
"""Convert top misses into benchmark scenarios.
Writes one ``alert.json`` per (alert, taxonomy) into ``out_dir`` using the
same schema as benchmark scenario ``alert.json`` files so the
existing benchmark runner can pick them up as regressions.
"""
since = _parse_since_option(since_spec)
rows = load_misses(since=since, taxonomy=taxonomy)
if not rows:
_console.print(
f"[yellow]No misses in scope (since={since_spec}, taxonomy={taxonomy or 'any'}).[/]"
)
return
selected = filter_top_misses(rows, top=top)
written = export_scenarios(selected, out_dir=out_dir)
_console.print(
f"[green]Wrote {len(written)} scenario(s) to {out_dir}.[/] "
f"[dim]({len(rows)} miss(es) in scope, deduped to {len(selected)})[/]"
)
for path in written:
_console.print(f" [dim]· {path}[/]")
@misses_command.command(name="convert")
@click.argument("miss_id")
@click.option(
"--out",
"out_file",
type=click.Path(dir_okay=False, path_type=Path),
default=None,
help="Write the scenario JSON to this path instead of stdout.",
)
def convert_command(miss_id: str, out_file: Path | None) -> None:
"""Convert a single miss (by id) into a benchmark scenario payload."""
rows = load_misses()
match = next((r for r in rows if r.get("miss_id") == miss_id), None)
if match is None:
raise click.ClickException(f"miss_id {miss_id!r} not found in {misses_path()}")
scenario = to_benchmark_scenario(match)
payload = json.dumps(scenario, indent=2, ensure_ascii=False) + "\n"
if out_file is None:
click.echo(payload)
return
out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_text(payload, encoding="utf-8")
_console.print(f"[green]Wrote scenario to {out_file}.[/]")