This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-whiteboard.git
The following commit(s) were added to refs/heads/master by this push:
new d2ebd640 feat(skill-evals): add experimental tool to compare multiple
execution runs
d2ebd640 is described below
commit d2ebd6406306bcd658812c9c545d3af8bb9faaff
Author: Robert Munteanu <[email protected]>
AuthorDate: Thu Apr 23 15:32:56 2026 +0200
feat(skill-evals): add experimental tool to compare multiple execution runs
---
skill-evals/README.md | 49 ++++++++++++++
skill-evals/pyproject.toml | 3 +
skill-evals/src/skill_evals/compare_eval_runs.py | 85 ++++++++++++++++++++++++
3 files changed, 137 insertions(+)
diff --git a/skill-evals/README.md b/skill-evals/README.md
index 925a7e02..94a0bd2d 100644
--- a/skill-evals/README.md
+++ b/skill-evals/README.md
@@ -76,6 +76,55 @@ Logs can be inspected afterwards with
uv run inspect view
```
+## Comparing Eval Runs
+
+Use the reusable comparison utility to compare two Inspect log files by
configuration, score,
+execution time, and token usage.
+
+```bash
+uv run skill-evals-compare LOG_A LOG_B
+```
+
+Supported output formats:
+
+```bash
+uv run skill-evals-compare LOG_A LOG_B --format text
+uv run skill-evals-compare LOG_A LOG_B --format json
+uv run skill-evals-compare LOG_A LOG_B --format markdown
+```
+
+Compare runs that are expected to differ only by `skill_enabled`:
+
+```bash
+uv run skill-evals-compare \
+ "logs/2026-04-23T09-16-03-00-00_jcr-js-nodetypes
[skills]_LLH4FxC9xVenX3nFv5fubf.eval" \
+ "logs/2026-04-23T09-35-29-00-00_jcr-js-nodetypes [no
skills]_L3rf4MQBPZ5LKYd574wucQ.eval" \
+ --expect-diff skill_enabled
+```
+
+Compare runs that use the same parameters but different models:
+
+```bash
+uv run skill-evals-compare \
+ "logs/2026-04-22T15-52-45-00-00_jcr-js-nodetypes
[skills]_SS3oqPNNabLYozQvEQs6U2.eval" \
+ "logs/2026-04-23T09-16-03-00-00_jcr-js-nodetypes
[skills]_LLH4FxC9xVenX3nFv5fubf.eval" \
+ --expect-diff model
+```
+
+Useful options:
+
+```bash
+uv run skill-evals-compare LOG_A LOG_B --samples
+uv run skill-evals-compare LOG_A LOG_B --expect-diff model,skill_enabled
+uv run skill-evals-compare LOG_A LOG_B --headline-score parent_pom_update
+uv run skill-evals-compare LOG_A LOG_B --fail-on-unexpected-diff
+```
+
+`--samples` adds dataset-entry comparison aggregated across all epochs for
each sample id.
+
+If a run contains multiple scores, use `--headline-score` to choose which
scorer or score record
+is used for the headline score and stderr rows.
+
## Task Notes
Current status:
diff --git a/skill-evals/pyproject.toml b/skill-evals/pyproject.toml
index b1852382..c94b4df8 100644
--- a/skill-evals/pyproject.toml
+++ b/skill-evals/pyproject.toml
@@ -13,6 +13,9 @@ dependencies = [
[project.entry-points.inspect_ai]
skill_evals = "skill_evals._registry"
+[project.scripts]
+skill-evals-compare = "skill_evals.compare_eval_runs:main"
+
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
diff --git a/skill-evals/src/skill_evals/compare_eval_runs.py
b/skill-evals/src/skill_evals/compare_eval_runs.py
new file mode 100644
index 00000000..c056d94a
--- /dev/null
+++ b/skill-evals/src/skill_evals/compare_eval_runs.py
@@ -0,0 +1,85 @@
+import argparse
+import sys
+from pathlib import Path
+
+from skill_evals.compare_eval_render import render_json, render_markdown,
render_text
+from skill_evals.compare_eval_report import build_report
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Compare two Inspect eval runs by score, time, usage, and
configuration."
+ )
+ parser.add_argument("left_log", help="Path or file:// URI for the left
eval log")
+ parser.add_argument("right_log", help="Path or file:// URI for the right
eval log")
+ parser.add_argument(
+ "--expect-diff",
+ default="",
+ help=(
+ "Comma-separated logical coordinates allowed to differ, e.g. "
+ "skill_enabled,model"
+ ),
+ )
+ parser.add_argument(
+ "--format",
+ choices=("text", "json", "markdown"),
+ default="text",
+ help="Output format",
+ )
+ parser.add_argument(
+ "--samples",
+ action="store_true",
+ help="Include per-sample and per-epoch comparison details",
+ )
+ parser.add_argument(
+ "--headline-score",
+ default="",
+ help=(
+ "Score key to use for headline score reporting when runs contain
multiple "
+ "scores"
+ ),
+ )
+ parser.add_argument(
+ "--fail-on-unexpected-diff",
+ action="store_true",
+ help="Exit with status 1 when differences outside --expect-diff are
found",
+ )
+ return parser.parse_args()
+
+
+def _normalize_log_path(value: str) -> str:
+ if value.startswith("file://"):
+ return value
+ return str(Path(value).expanduser().resolve())
+
+
+def main() -> int:
+ args = _parse_args()
+ left_path = _normalize_log_path(args.left_log)
+ right_path = _normalize_log_path(args.right_log)
+ expected_diff = {item.strip() for item in args.expect_diff.split(",") if
item.strip()}
+ headline_score = args.headline_score.strip() or None
+ report = build_report(
+ left_path,
+ right_path,
+ expected_diff,
+ args.samples,
+ headline_score=headline_score,
+ )
+
+ if args.format == "json":
+ output = render_json(report)
+ elif args.format == "markdown":
+ output = render_markdown(report)
+ else:
+ output = render_text(report)
+
+ print(output)
+
+ if args.fail_on_unexpected_diff and report["unexpected_differences"]:
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())