Skip to content

feat(bench): aelf bench all reproducibility harness (#437) - #465

Merged
robotrocketscience merged 18 commits into
mainfrom
feat/issue-437-bench-all-harness
May 8, 2026
Merged

feat(bench): aelf bench all reproducibility harness (#437)#465
robotrocketscience merged 18 commits into
mainfrom
feat/issue-437-bench-all-harness

Conversation

@yoshi280

@yoshi280 yoshi280 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lands the v2.0 reproducibility harness for #437. aelf bench all --canonical --out PATH now subprocess-dispatches every academic-suite adapter (MAB, LoCoMo, LongMemEval, StructMemEval, AMA-Bench) at the canonical headline cut and merges the per-adapter outputs into one schema-v2 JSON. Tolerance-band classifier ships alongside; nightly cron is wired to a dedicated bench-canonical-results branch (same pattern #461 adopted for replay-soak — sidesteps the main ruleset).

The 8 design asks in docs/v2_reproducibility_harness.md were ratified inline before implementation:

# Asked Resolved
1 CLI shape aelf bench subcommand
2 What "all" means Full benchmarks (override; spec recommended sized headline cut)
3 Schema version schema_version: 2
4 Tolerance bands Relative-with-floor (±X%, ±2pp absolute floor)
5 CI shape Two-tier nightly canonical + PR smoke (smoke deferred — see Blockers)
6 Missing data dirs Skip with status: skipped_data_missing (exit 2)
7 LLM-judge cadence Manual local run
8 README badge Static, committed by cron

The override on #2 is the only deviation from the spec memo's recommendation. Implications captured in docs/v2_reproducibility_harness.md § Ratification — full LongMemEval (not oracle), StructMemEval --bench big (not small), multi-hour cron runtime instead of tens-of-minutes.

What landed

  • benchmarks/run.py (340 LOC) — registry-driven dispatcher (11 canonical invocations, 2 smoke). Subprocess-per-adapter; injectable runner for tests. --canonical cut-mismatch refusal.
  • benchmarks/tolerance.py (217 LOC) — relative-with-floor band classifier. Reads metric_overrides from canonical JSON; explicit caller-passed overrides take precedence. Pass/warn/fail classification with FAIL-dominates summary.
  • src/aelfrice/cli.pyaelf bench all wired with --out, --canonical, --adapters, --smoke flags. all removed from _BENCH_INERT_TARGETS.
  • .github/workflows/bench-canonical.yml — daily 05:00 UTC cron, hardened-runner, pinned actions, dedicated-branch push, in-pipeline band-check.
  • tests/test_bench_dispatcher.py + tests/test_bench_tolerance.py — 29 stub-driven unit tests, all <1s wall-clock.
  • tests/test_benchmarks_dir.py — adjusted [all] parametrization (no longer inert) + new test_aelf_bench_all_requires_out.
  • benchmarks/results/v2.0.0.json — schema-v2 skeleton with the canonical headline_cut, _calibration_pass_required: true. Real numbers land via the operator's calibration pass.
  • README.md — reproducibility badge between OSSInsight and the lede (markered for cron rewrite). New ## Reproducibility section before Roadmap.
  • docs/COMMANDS.md — new row under Diagnostics. bench [--top-k N] row (synthetic) preserved.

Test plan

  • uv run pytest tests/ — 2624 passed, 41 skipped (no regressions vs main).
  • New tests: 29 unit + 2 integration adjustments.
  • python -c "from benchmarks import run, tolerance" imports clean.
  • Workflow YAML parses.
  • Discretion grep on full branch diff vs github/main: clean.
  • Calibration pass — aelf bench all --canonical --out benchmarks/results/v2.0.0.json against full data dirs (operator action; see Blockers).
  • First successful nightly cron entry on bench-canonical-results branch (lands organically once cron fires).

Blockers / follow-ups

  1. Calibration pass — operator runs aelf bench all --canonical --out benchmarks/results/v2.0.0.json ≥3 times against full HuggingFace caches + populated /tmp/LoCoMo + /tmp/StructMemEval + (optional) ANTHROPIC_API_KEY for judge metrics. Take observed range × 1.5 as per-metric override band. Until this lands, the cron's band-check is a no-op (no leaves in canonical → summarize → PASS).
  2. PR smoke fixtures (offline) — spec recommended tests/fixtures/bench_smoke/ pinned LoCoMo turns + MAB QA so a 2-min PR smoke can exercise real adapter shapes. Not in this PR; license-attribution review needed. Pytest unit tests cover dispatcher regressions in the meantime via stubs.
  3. README badge cron-rewrite — badge ships placeholder ("pending first canonical run"). The cron commits to bench-canonical-results; the in-place README rewrite (between bench-canonical-badge:start/end markers) is not wired. Trivial sed in the workflow once you decide if this is wanted.
  4. Adapter exit-code 2 contract (skipped_data_missing) — dispatcher honors it; no adapter currently emits it. Land per-adapter as small follow-ups once /tmp data-dir plumbing is decided.

Provenance

Summary by Sourcery

Introduce a v2.0 reproducibility harness for aelf bench all, wiring a dispatcher, tolerance-band classifier, CLI flags, nightly cron workflow, and documentation to produce and validate canonical benchmark results.

New Features:

  • Add aelf bench all dispatcher that runs all academic-suite benchmarks and merges results into a schema-v2 JSON report.
  • Introduce tolerance-band classification module for comparing benchmark runs against canonical results.
  • Expose new --out, --canonical, --adapters, and --smoke flags on the aelf bench CLI for reproducibility runs.
  • Add a reproducibility badge and README section describing the canonical bench harness.
  • Add a schema-v2 skeleton canonical results file benchmarks/results/v2.0.0.json.

Enhancements:

  • Ratify and document the v2.0 reproducibility harness spec, including the full-benchmark headline cut override.
  • Document the new bench all harness in the commands reference.

CI:

  • Add a Bench Canonical nightly GitHub Actions workflow that runs the canonical harness, band-checks against the canonical JSON, and pushes results to a dedicated branch.

Tests:

  • Add unit tests for the benchmark dispatcher and tolerance-band classifier, plus CLI tests for aelf bench all behaviour.

Summary by CodeRabbit

  • New Features

    • Added aelf bench all command to run reproducible benchmark suites with --canonical, --smoke, and --adapters filtering options
    • Implemented nightly canonical benchmark verification with tolerance-band regression detection on the main branch
  • Documentation

    • Added reproducibility section to README describing canonical benchmark workflow
    • Documented new bench all command and tolerance-based validation policy
    • Ratified v2.0 reproducibility harness specification with concrete implementation details

@sourcery-ai

sourcery-ai Bot commented May 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the v2.0 reproducibility harness for aelf bench all by adding a registry-driven benchmark dispatcher, tolerance-band classifier, CLI wiring, nightly GitHub Actions cron, and corresponding tests/docs to produce and validate a unified schema-v2 benchmark JSON.

Sequence diagram for aelf bench all reproducibility harness

sequenceDiagram
    actor User
    participant CLI as aelf_cli__cmd_bench
    participant Dispatcher as benchmarks_run_main_all
    participant Adapter as benchmark_adapters
    participant FS as Filesystem

    User->>CLI: run "aelf bench all --canonical --out PATH"
    CLI->>Dispatcher: main_all(out_path, canonical=true, adapters=None, smoke=false)

    Dispatcher->>Dispatcher: select invocations = CANONICAL_INVOCATIONS
    Dispatcher->>Dispatcher: _validate_canonical_cut(invocations)

    loop for each AdapterInvocation
        Dispatcher->>Adapter: subprocess python -m benchmarks.<name>_adapter --output tmp.json
        Adapter-->>Dispatcher: exit code, stdout/stderr
        Dispatcher->>FS: read tmp.json
        FS-->>Dispatcher: adapter JSON
        Dispatcher->>Dispatcher: run_invocation() → InvocationResult
    end

    Dispatcher->>Dispatcher: _merge(results)
    Dispatcher->>Dispatcher: _headline_cut_for(invocations)
    Dispatcher->>Dispatcher: build_report(results, label="v2.0.0 canonical")
    Dispatcher->>FS: write merged JSON to PATH
    Dispatcher-->>CLI: return exit code (0/1/2)
    CLI-->>User: print summary, exit
Loading

Class diagram for benchmarks.run dispatcher (v2.0 reproducibility harness)

classDiagram
    class AdapterInvocation {
        +str adapter
        +str sub_key
        +str module
        +tuple~str~ args
        +str label
    }

    class InvocationResult {
        +AdapterInvocation invocation
        +str status
        +float elapsed_sec
        +dict~str, Any~ output
        +str error_message
    }

    class DispatcherConfig {
        <<interface>>
        +CANONICAL_INVOCATIONS : tuple~AdapterInvocation~
        +SMOKE_INVOCATIONS : tuple~AdapterInvocation~
    }

    class BenchRunner {
        +int main_all(out_path, canonical, adapters, smoke, runner, tmp_root)
        +InvocationResult run_invocation(inv, runner, tmp_root)
        +dict~str, dict~str, Any~~ build_report(results, label, invocations_used)
        +dict~str, dict~str, Any~~ _merge(results)
        +dict~str, Any~ _headline_cut_for(invocations)
        +void _validate_canonical_cut(invocations_used)
    }

    class EnvironmentIntrospection {
        +str _utc_now_iso()
        +str _git_commit()
        +str _aelfrice_version()
    }

    class SubprocessRunner {
        +CompletedProcess _default_runner(cmd, out_path)
    }

    AdapterInvocation <.. DispatcherConfig : used in
    AdapterInvocation <.. BenchRunner : used in
    InvocationResult <.. BenchRunner : used in
    SubprocessRunner <.. BenchRunner : used in
    EnvironmentIntrospection <.. BenchRunner : used in

    DispatcherConfig <|.. BenchRunner
    SubprocessRunner <|.. BenchRunner
    EnvironmentIntrospection <|.. BenchRunner
Loading

Class diagram for benchmarks.tolerance band classifier

classDiagram
    class Verdict {
        <<enum>>
        PASS
        WARN
        FAIL
    }

    class BandCheck {
        +tuple~str~ path
        +float canonical
        +float observed
        +float lower
        +float upper
        +str band_kind
        +Verdict verdict
        +str note
    }

    class BandConfig {
        <<interface>>
        +dict~str, float~ DEFAULT_RELATIVE_BANDS
        +float FALLBACK_RELATIVE_BAND
        +float ABSOLUTE_FLOOR
    }

    class BandComputer {
        +tuple~float, float, str~ compute_band(metric_name, canonical, overrides, floor)
        +Verdict classify(canonical, observed, lower, upper)
        +list~tuple~tuple~str~, float~~ _walk_leaves(obj, path)
        +list~BandCheck~ check_report(canonical, observed, metric_overrides, floor)
        +tuple~Verdict, dict~str, int~~ summarize(checks)
        +dict~str, Any~ load_report(path)
    }

    BandCheck --> Verdict : uses
    BandComputer --> BandCheck : produces
    BandConfig <|.. BandComputer
Loading

File-Level Changes

Change Details Files
Introduce registry-driven dispatcher for aelf bench all that subprocesses each academic adapter and merges outputs into a single schema-v2 report with canonical vs smoke cuts.
  • Define CANONICAL_INVOCATIONS and SMOKE_INVOCATIONS registries describing full and smoke headline cuts for MAB, LoCoMo, LongMemEval, StructMemEval, and AMA-Bench.
  • Implement AdapterInvocation and InvocationResult data models plus a pluggable subprocess runner that writes per-adapter JSON to temp files and parses results.
  • Merge per-invocation outputs into an adapter-keyed results tree, capture metadata (schema_version=2, harness_version, git commit, aelfrice version, headline_cut), and write labeled reports for canonical/cron/smoke runs.
  • Implement main_all() and a small CLI wrapper (python -m benchmarks.run) that support --out, --canonical, --adapters filter, --smoke, adapters-filter validation, canonical cut enforcement, and exit-code semantics (0 ok, 1 error, 2 skipped_data_missing).
benchmarks/run.py
Add tolerance-band classification module to compare cron runs against canonical results using relative-with-floor bands and per-metric overrides.
  • Define default relative bands for common metrics, a fallback band, and an absolute floor; implement compute_band() and classify() to produce pass/warn/fail per metric.
  • Walk nested result trees, skipping metadata keys, to band-check numeric leaves with optional metric_overrides from canonical JSON or explicit caller overrides.
  • Summarize per-metric verdicts into an overall verdict where FAIL dominates WARN, and implement load_report() that enforces schema_version=2.
  • Add unit tests covering band computation, classification behavior, overrides precedence, tree walking, summarize semantics, and schema validation.
benchmarks/tolerance.py
tests/test_bench_tolerance.py
Wire aelf bench all subcommand to the new dispatcher and adjust CLI/tests/docs to reflect that all is now an active reproducibility harness.
  • Update _cmd_bench in src/aelfrice/cli.py to special-case target "all": require --out PATH, parse --adapters, --canonical, --smoke flags, and call benchmarks.run.main_all().
  • Extend the bench subcommand parser to add --out, --canonical, --adapters, and --smoke options, and remove all from the inert benchmarks target list/documentation.
  • Adjust tests so only single-name academic targets remain inert and add a new test asserting aelf bench all without --out exits 2 with a clear error.
  • Document the new command in docs/COMMANDS.md and expand docs/v2_reproducibility_harness.md with a Ratification section describing the finalized decisions and implications of the full-headline-cut override.
src/aelfrice/cli.py
tests/test_benchmarks_dir.py
docs/COMMANDS.md
docs/v2_reproducibility_harness.md
Add nightly GitHub Actions workflow to run canonical benchmarks and push results to a dedicated branch, with in-pipeline tolerance-band checking.
  • Create Bench Canonical workflow that installs the dev environment via uv, bootstraps a bench-canonical-results worktree (creating the branch on first run), and runs aelf bench all --canonical --out to produce date-stamped cron JSON.
  • Wire band-check step that loads the canonical benchmarks/results/v2.0.0.json and the latest cron output, invokes the tolerance module, logs non-pass metrics, and maps FAIL verdicts to job failure while still committing snapshots.
  • Configure hardened runner, pinned actions versions, long timeout (6h), daily 05:00 UTC cron plus manual dispatch, and safe push of changes to the dedicated branch with a bot identity.
.github/workflows/bench-canonical.yml
Seed canonical results skeleton, README badge/section, and tests for the dispatcher harness.
  • Add an empty schema-v2 benchmarks/results/v2.0.0.json skeleton (canonical headline cut, calibration-pass-required) for future calibrated metrics.
  • Introduce tests/test_bench_dispatcher.py with stubbed runner helpers to validate registry sizes, smoke/canonical end-to-end behavior, cut enforcement, exit-code aggregation, headline_cut recording, and error/skip handling.
  • Update README with a reproducibility badge (cron rewrite markers) and a new section explaining the harness, nightly cron, tolerance bands, and badge semantics.
benchmarks/results/v2.0.0.json
.github/workflows/bench-canonical.yml
tests/test_bench_dispatcher.py
README.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@robotrocketscience has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 29 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a9bc1ea-e4fb-41cd-bc0f-92d69530cddb

📥 Commits

Reviewing files that changed from the base of the PR and between f75926b and a279f7a.

📒 Files selected for processing (11)
  • .github/workflows/bench-canonical.yml
  • README.md
  • benchmarks/results/v2.0.0.json
  • benchmarks/run.py
  • benchmarks/tolerance.py
  • docs/COMMANDS.md
  • docs/v2_reproducibility_harness.md
  • src/aelfrice/cli.py
  • tests/test_bench_dispatcher.py
  • tests/test_bench_tolerance.py
  • tests/test_benchmarks_dir.py
📝 Walkthrough

Walkthrough

This PR implements the v2.0 reproducibility harness for benchmarks, comprising a nightly canonical runner workflow, dispatcher (aelf bench all), tolerance-band classifier, results schema, and comprehensive tests. It enables daily canonical benchmark execution on main, automated tolerance-based regression detection, and reproducible result capture via structured JSON.

Changes

v2.0 Reproducibility Harness

Layer / File(s) Summary
Schema & Data Shape
benchmarks/results/v2.0.0.json
Canonical benchmark results JSON skeleton (schema v2) with metadata, headline-cut selection, metric-override placeholders, and calibration protocol.
Tolerance-Band Classification
benchmarks/tolerance.py
Computes per-metric relative/absolute band bounds and classifies observed results as pass, warn, or fail based on drift thresholds; includes rollup summarize() and schema-validation load_report().
Benchmark Dispatcher
benchmarks/run.py
Runs multiple benchmark adapters as subprocesses, merges their JSON outputs into schema-v2 report, enforces canonical invocation list validation, and exports status/timing per invocation.
CLI Integration
src/aelfrice/cli.py
Routes bench all subcommand to benchmarks.run.main_all() with --out, --canonical, --adapters, and --smoke flags; validates --out requirement.
Workflow & Results Storage
.github/workflows/bench-canonical.yml
Daily scheduled + manual-dispatch GitHub Actions job that runs canonical benchmarks, writes dated JSON to bench-canonical-results worktree, performs tolerance band-check, and exits non-zero on regression.
Tests
tests/test_bench_dispatcher.py, tests/test_bench_tolerance.py, tests/test_benchmarks_dir.py
Unit and end-to-end tests for dispatcher registry, subprocess integration, tolerance classification, schema loading, and CLI --out requirement enforcement.
Documentation
README.md, docs/COMMANDS.md, docs/v2_reproducibility_harness.md
Add reproducibility section to README with badge, document bench all CLI subcommand, and ratify v2.0 harness spec with decision overrides and implementation order.

Sequence Diagram

sequenceDiagram
    participant GHA as GitHub Actions<br/>(Nightly Cron)
    participant Disp as Benchmark<br/>Dispatcher
    participant Adap as Benchmark<br/>Adapters
    participant Tol as Tolerance<br/>Classifier
    participant Res as Results<br/>Branch

    GHA->>Disp: aelf bench all --canonical<br/>--out daily.json
    Disp->>Disp: Select CANONICAL_INVOCATIONS
    
    loop For each adapter invocation
        Disp->>Adap: python -m adapter<br/>--output temp.json
        Adap->>Adap: Run benchmark
        Adap->>Disp: Write JSON results
        Disp->>Disp: Capture status &<br/>parse output
    end
    
    Disp->>Disp: Merge outputs into<br/>schema-v2 report
    Disp->>GHA: Write daily.json
    
    GHA->>Tol: Load canonical +<br/>cron report
    Tol->>Tol: Walk metric trees<br/>& compute bands
    Tol->>Tol: Classify each leaf<br/>(pass/warn/fail)
    Tol->>Tol: Summarize verdicts
    Tol->>GHA: Report regression?
    
    alt Regression detected
        GHA->>GHA: Fail workflow
    else No regression
        GHA->>Res: Commit & push<br/>daily JSON
        Res->>Res: Update branch
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Suggested labels

attn:review

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(bench): aelf bench all reproducibility harness (#437)' clearly and specifically identifies the main feature addition—the v2.0 reproducibility harness for benchmark orchestration.
Description check ✅ Passed The PR description comprehensively covers all required template sections: a detailed summary of the reproducibility harness implementation, linked issue (#437), type of change (feat), verification steps completed, thorough test plan, and notes for reviewers including blockers and follow-ups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-437-bench-all-harness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

from pathlib import Path
from benchmarks import tolerance
cano = tolerance.load_report(Path('benchmarks/results/v2.0.0.json'))
obs = tolerance.load_report(Path('${{ steps.bench.outputs.out }}'))
Comment thread benchmarks/run.py
import subprocess
import sys
import time
from dataclasses import dataclass, field

import json
import subprocess
from dataclasses import dataclass
from __future__ import annotations

import json
from pathlib import Path

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 4 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • In benchmarks/run.py::main_all, the no adapters matched error message is built after invocations has already been filtered, so the available: list will always be empty; capture the full set of adapters before filtering so the message can enumerate valid options.
  • In .github/workflows/bench-canonical.yml, the condition if: hashFiles(steps.bench.outputs.out) != '' is invalid because hashFiles only accepts string literals; use a direct check on the output (e.g. if: steps.bench.outputs.out != '') or a static path instead.
  • The type annotation for _headline_cut_for (dict[str, list[str] | dict[str, Any]]) does not match the actual structure being returned (lists of dicts), which may confuse static type checkers; tighten the annotation to reflect list[dict[str, Any]].
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `benchmarks/run.py::main_all`, the `no adapters matched` error message is built after `invocations` has already been filtered, so the `available:` list will always be empty; capture the full set of adapters before filtering so the message can enumerate valid options.
- In `.github/workflows/bench-canonical.yml`, the condition `if: hashFiles(steps.bench.outputs.out) != ''` is invalid because `hashFiles` only accepts string literals; use a direct check on the output (e.g. `if: steps.bench.outputs.out != ''`) or a static path instead.
- The type annotation for `_headline_cut_for` (`dict[str, list[str] | dict[str, Any]]`) does not match the actual structure being returned (lists of dicts), which may confuse static type checkers; tighten the annotation to reflect `list[dict[str, Any]]`.

## Individual Comments

### Comment 1
<location path="benchmarks/run.py" line_range="280-284" />
<code_context>
+    "error", 2 if any was "skipped_data_missing" and none was "error").
+    """
+    invocations = SMOKE_INVOCATIONS if smoke else CANONICAL_INVOCATIONS
+    if adapters is not None:
+        invocations = tuple(i for i in invocations if i.adapter in adapters)
+        if not invocations:
+            raise SystemExit(
+                f"no adapters matched filter {adapters!r}; "
+                "available: " + ", ".join(sorted({i.adapter for i in invocations}))
+            )
</code_context>
<issue_to_address>
**issue (bug_risk):** Filtering adapters mutates the `invocations` set, breaking the error message and making adapter lists misleading.

In `main_all`, `invocations` is overwritten with the filtered tuple and then reused for the "available" list in the error path. When no adapters match, `invocations` is empty, so the message shows no available adapters and loses the original registry context. Keep the original tuple (e.g. `all_invocations = SMOKE_INVOCATIONS if smoke else CANONICAL_INVOCATIONS`), derive the filtered tuple from that, and use `all_invocations` when building the "available" list. You could also normalize the `adapters` filter (e.g. `.lower()`) to avoid case-sensitivity issues.
</issue_to_address>

### Comment 2
<location path="benchmarks/tolerance.py" line_range="122-126" />
<code_context>
+    metadata like `_status`, `_elapsed_sec`).
+    """
+    leaves: list[tuple[tuple[str, ...], float]] = []
+    if isinstance(obj, dict):
+        for k, v in obj.items():
+            if isinstance(k, str) and k.startswith("_"):
+                continue
+            leaves.extend(_walk_leaves(v, (*path, str(k))))
+    elif isinstance(obj, (int, float)) and not isinstance(obj, bool):
+        leaves.append((path, float(obj)))
</code_context>
<issue_to_address>
**issue (bug_risk):** Skipping all keys starting with '_' means single-invocation adapter results (stored under "_") will never be band-checked.

`_walk_leaves` skips all dict keys starting with `_`, but `build_report`/`_merge` stores single‑invocation adapter results under the `_` key (e.g. `results['locomo']['_'] = {...}`). As a result, you never traverse into that subtree, so band checks are never applied to single‑invocation adapters (only multi‑invocation ones with non‑`_` keys are covered).

To fix this, consider narrowing the skip condition to specific metadata keys (e.g. `_status`, `_elapsed_sec`) or only skipping non‑dict/non‑metric values, so that the `_` bucket for actual metrics is still traversed.
</issue_to_address>

### Comment 3
<location path="tests/test_bench_dispatcher.py" line_range="181-190" />
<code_context>
+        assert "big" in entry["args"]
+
+
+def test_runner_crash_recorded_as_error(tmp_path):
+    """If the runner raises, status=error and message captured."""
+    def crashing_runner(cmd, out_path):
+        raise RuntimeError("boom")
+    out = tmp_path / "crash.json"
+    rc = bench_run.main_all(
+        out_path=out, canonical=False, smoke=True,
+        runner=crashing_runner, tmp_root=tmp_path / "tmp",
+    )
+    assert rc == 1
+    data = json.loads(out.read_text())
+    err = data["results"]["amabench"]["_"]
+    assert err["status"] == "error"
+    assert "boom" in err["error_message"]
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the case where an adapter exits 0 but fails to write its output file

There’s a code path in `run_invocation` that sets status=`error` when the adapter exits with code 0 but the output file is missing. Current tests don’t cover this. Please add a test using a stub runner that returns `returncode=0` without writing `out_path`, and assert that this path is taken and that `main_all` returns 1.
</issue_to_address>

### Comment 4
<location path="tests/test_bench_dispatcher.py" line_range="122-131" />
<code_context>
+    ) == 11
+
+
+def test_skipped_data_missing_propagates(tmp_path):
+    """Adapter exit-code 2 → status=skipped_data_missing, overall rc=2."""
+    out = tmp_path / "skip.json"
+    rc = bench_run.main_all(
+        out_path=out, canonical=False, smoke=True,
+        runner=_make_runner(per_adapter_returncode={"amabench": 2}),
+        tmp_root=tmp_path / "tmp",
+    )
+    assert rc == 2
+    data = json.loads(out.read_text())
+    assert data["results"]["amabench"]["_"]["status"] == "skipped_data_missing"
+    assert "data missing" in data["results"]["amabench"]["_"]["error_message"]
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a test for malformed JSON output from the adapter

There’s a specific branch handling `json.JSONDecodeError` when reading the adapter’s `--output` file that isn’t exercised by tests. Please add a case where the stub runner writes invalid JSON (e.g., a plain string or truncated JSON) with `returncode=0`, and assert that the status is `"error"`, the error_message indicates a parse failure, and `main_all` returns 1 to cover this path.

Suggested implementation:

```python
    assert sum(
        len([k for k in v if k != "_"]) or (1 if "_" in v else 0)
        for v in data["results"].values()
    ) == 11


def test_malformed_adapter_output_sets_error(tmp_path):
    """Malformed adapter --output → status=error, overall rc=1."""
    out = tmp_path / "malformed.json"
    rc = bench_run.main_all(
        out_path=out,
        canonical=False,
        smoke=True,
        runner=_make_runner(
            per_adapter_returncode={"amabench": 0},
            per_adapter_output={"amabench": "{not-json"},
        ),
        tmp_root=tmp_path / "tmp",
    )
    assert rc == 1
    data = json.loads(out.read_text())
    result = data["results"]["amabench"]["_"]
    assert result["status"] == "error"
    assert (
        "parse" in result["error_message"].lower()
        or "json" in result["error_message"].lower()
        or "decode" in result["error_message"].lower()
    )


The dispatcher subprocesses each adapter, so all tests stub the runner

```

To make this test pass, `tests/test_bench_dispatcher.py` (or the helper module that defines `_make_runner`) will also need:
1. Updating `_make_runner` to accept an optional `per_adapter_output` mapping, e.g. `def _make_runner(*, per_adapter_returncode=None, per_adapter_output=None, ...)`.
2. In the returned stub runner, when invoked for adapter `name` (e.g. `"amabench"`), detect the `--output` argument in the `cmd` list, and:
   - If `per_adapter_output` contains an entry for that adapter, write its string value directly to the resolved `--output` path (without JSON validation), so `" {not-json"` triggers a `json.JSONDecodeError` in the dispatcher.
   - Preserve existing behavior for any other adapters or when `per_adapter_output` is `None`.
3. Ensure the stub runner still returns a `subprocess.CompletedProcess` (or equivalent) with `returncode` taken from `per_adapter_returncode[name]` (defaulting to 0) so that this test exercises the branch where the adapter exits successfully but its output cannot be parsed.
</issue_to_address>

### Comment 5
<location path="benchmarks/run.py" line_range="187" />
<code_context>
    return subprocess.run(cmd, capture_output=True, text=True, check=False)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread benchmarks/run.py
Comment thread benchmarks/tolerance.py
Comment on lines +122 to +126
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(k, str) and k.startswith("_"):
continue
leaves.extend(_walk_leaves(v, (*path, str(k))))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Skipping all keys starting with '' means single-invocation adapter results (stored under "") will never be band-checked.

_walk_leaves skips all dict keys starting with _, but build_report/_merge stores single‑invocation adapter results under the _ key (e.g. results['locomo']['_'] = {...}). As a result, you never traverse into that subtree, so band checks are never applied to single‑invocation adapters (only multi‑invocation ones with non‑_ keys are covered).

To fix this, consider narrowing the skip condition to specific metadata keys (e.g. _status, _elapsed_sec) or only skipping non‑dict/non‑metric values, so that the _ bucket for actual metrics is still traversed.

Comment on lines +181 to +190
def test_runner_crash_recorded_as_error(tmp_path):
"""If the runner raises, status=error and message captured."""
def crashing_runner(cmd, out_path):
raise RuntimeError("boom")
out = tmp_path / "crash.json"
rc = bench_run.main_all(
out_path=out, canonical=False, smoke=True,
runner=crashing_runner, tmp_root=tmp_path / "tmp",
)
assert rc == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for the case where an adapter exits 0 but fails to write its output file

There’s a code path in run_invocation that sets status=error when the adapter exits with code 0 but the output file is missing. Current tests don’t cover this. Please add a test using a stub runner that returns returncode=0 without writing out_path, and assert that this path is taken and that main_all returns 1.

Comment on lines +122 to +131
def test_skipped_data_missing_propagates(tmp_path):
"""Adapter exit-code 2 → status=skipped_data_missing, overall rc=2."""
out = tmp_path / "skip.json"
rc = bench_run.main_all(
out_path=out, canonical=False, smoke=True,
runner=_make_runner(per_adapter_returncode={"amabench": 2}),
tmp_root=tmp_path / "tmp",
)
assert rc == 2
data = json.loads(out.read_text())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider a test for malformed JSON output from the adapter

There’s a specific branch handling json.JSONDecodeError when reading the adapter’s --output file that isn’t exercised by tests. Please add a case where the stub runner writes invalid JSON (e.g., a plain string or truncated JSON) with returncode=0, and assert that the status is "error", the error_message indicates a parse failure, and main_all returns 1 to cover this path.

Suggested implementation:

    assert sum(
        len([k for k in v if k != "_"]) or (1 if "_" in v else 0)
        for v in data["results"].values()
    ) == 11


def test_malformed_adapter_output_sets_error(tmp_path):
    """Malformed adapter --output → status=error, overall rc=1."""
    out = tmp_path / "malformed.json"
    rc = bench_run.main_all(
        out_path=out,
        canonical=False,
        smoke=True,
        runner=_make_runner(
            per_adapter_returncode={"amabench": 0},
            per_adapter_output={"amabench": "{not-json"},
        ),
        tmp_root=tmp_path / "tmp",
    )
    assert rc == 1
    data = json.loads(out.read_text())
    result = data["results"]["amabench"]["_"]
    assert result["status"] == "error"
    assert (
        "parse" in result["error_message"].lower()
        or "json" in result["error_message"].lower()
        or "decode" in result["error_message"].lower()
    )


The dispatcher subprocesses each adapter, so all tests stub the runner

To make this test pass, tests/test_bench_dispatcher.py (or the helper module that defines _make_runner) will also need:

  1. Updating _make_runner to accept an optional per_adapter_output mapping, e.g. def _make_runner(*, per_adapter_returncode=None, per_adapter_output=None, ...).
  2. In the returned stub runner, when invoked for adapter name (e.g. "amabench"), detect the --output argument in the cmd list, and:
    • If per_adapter_output contains an entry for that adapter, write its string value directly to the resolved --output path (without JSON validation), so " {not-json" triggers a json.JSONDecodeError in the dispatcher.
    • Preserve existing behavior for any other adapters or when per_adapter_output is None.
  3. Ensure the stub runner still returns a subprocess.CompletedProcess (or equivalent) with returncode taken from per_adapter_returncode[name] (defaulting to 0) so that this test exercises the branch where the adapter exits successfully but its output cannot be parsed.

Comment thread benchmarks/run.py


def _default_runner(cmd: list[str], out_path: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, capture_output=True, text=True, check=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/bench-canonical.yml:
- Around line 91-109: The canonical report is an uncalibrated skeleton so the
current script can falsely pass; after loading the canonical report (cano =
tolerance.load_report(...)) add a check for the calibration marker (e.g.,
inspect cano['_calibration_pass_required'] or the emptied 'results' tree) and if
calibration is required or results are empty exit non-zero (fail fast) with a
clear message before calling tolerance.check_report/summarize; update the inline
CI python block that calls tolerance.load_report, check_report, and summarize to
perform this calibration check and abort early when the canonical is not yet
calibrated.
- Around line 75-89: The "Run aelf bench all --canonical" step (id: bench) must
not allow hard failures to mark the workflow green — remove or set
continue-on-error: false for the bench step so the `uv run aelf bench all
--canonical --out "${out}"` command fails the job on error; ensure downstream
band-check still runs as a separate step that can inspect the bench outputs but
does not mask the bench step failure.

In `@benchmarks/run.py`:
- Around line 186-187: The _default_runner currently calls subprocess.run
without a timeout, allowing a hung adapter to block the harness; modify the
_default_runner function to include a timeout argument when calling
subprocess.run (e.g., subprocess.run(cmd, capture_output=True, text=True,
check=False, timeout=300)) or alternatively add an optional timeout parameter to
_default_runner (def _default_runner(cmd: list[str], out_path: Path, timeout:
int = 300)) and pass it through to subprocess.run so stuck processes are
terminated after the configured timeout.
- Around line 282-286: The error message builds "available:" from the
already-empty filtered variable invocations; change the raise SystemExit in the
adapter-filter branch so it computes available adapters from the original
unfiltered source (the list/iterator used before filtering) instead of
invocations, and include that computed set (e.g., the unique adapter names) in
the processLogger/raise message—update the block around the raise SystemExit to
reference that original collection when building ", ".join(sorted({...})) so the
message shows actual available adapters.
- Around line 143-144: The current out_path uses tmp_root /
f"{inv.adapter}_{inv.sub_key or 'all'}.json" which can collide across concurrent
runs; change out_path generation in benchmarks/run.py to produce a unique
per-invocation filename (for example by appending a uuid4, timestamp+pid, or
using tempfile to create a unique temp filename) so that inv.adapter,
inv.sub_key and inv.module still appear in the name but each invocation gets its
own file; update the cmd to use that new unique out_path variable.

In `@docs/v2_reproducibility_harness.md`:
- Around line 225-231: The runbook's Step 6 references pushing to the wrong
branch name; update the documented branch name to match the workflow used in the
PR by replacing "benchmark-results" with "bench-canonical-results" in the
Reproducibility section and any related entries (Step 6, README badge,
docs/COMMANDS.md mention), and ensure the
`.github/workflows/bench-canonical.yml` workflow branch target and the runbook
text are consistent so operators are pointed to the actual
`bench-canonical-results` branch.

In `@README.md`:
- Around line 137-139: The README overstates automation for the "Bench
Canonical" badge (the README badge block referencing the nightly cron and the
bench-canonical-results branch); update the README.md badge text to indicate the
badge is pending/manual (not live/auto-updating) and clarify that the workflow
in this PR does not rewrite the README badge block; mention "Bench Canonical",
"bench-canonical-results" and the README badge block so the change is easy to
find and ensure the docs/v2_reproducibility_harness.md link remains correct.

In `@src/aelfrice/cli.py`:
- Around line 4301-4320: The new optional flags (bench_out, bench_canonical,
bench_adapters, bench_smoke) are never parsed because p_bench already defines a
positional argument named rest with nargs=argparse.REMAINDER before them;
argparse will consume everything after the positional into rest. Fix by moving
the p_bench.add_argument(..., dest="rest", nargs=argparse.REMAINDER) declaration
so it comes after the optional flags, or change that positional to nargs="*" (or
otherwise optional) and adjust downstream logic to accept an empty rest; ensure
you update references to rest parsing code accordingly so flags like bench_out,
bench_canonical, bench_adapters, and bench_smoke are recognized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a4e56680-2a49-4093-9872-9d76a141ef13

📥 Commits

Reviewing files that changed from the base of the PR and between 7f880ca and f75926b.

📒 Files selected for processing (11)
  • .github/workflows/bench-canonical.yml
  • README.md
  • benchmarks/results/v2.0.0.json
  • benchmarks/run.py
  • benchmarks/tolerance.py
  • docs/COMMANDS.md
  • docs/v2_reproducibility_harness.md
  • src/aelfrice/cli.py
  • tests/test_bench_dispatcher.py
  • tests/test_bench_tolerance.py
  • tests/test_benchmarks_dir.py

Comment on lines +75 to +89
- name: Run aelf bench all --canonical
id: bench
run: |
set -euo pipefail
today=$(date -u +%Y-%m-%d)
out=".bench-results-branch/v2.0.0-cron-${today}.json"
# `--canonical` so the dispatcher refuses if the cut doesn't
# match CANONICAL_INVOCATIONS. The merged JSON's label still
# reads `v2.0.0 cron <ts>` (canonical-vs-cron is by filename,
# not by --canonical flag inside the run).
uv run aelf bench all --canonical --out "${out}"
echo "out=${out}" >> "$GITHUB_OUTPUT"
# Continue on band-check failure so we still commit the cron
# entry; `Band-check` step below sets the actual job status.
continue-on-error: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let benchmark execution failures go green.

Because this step has continue-on-error: true and the final gate only checks steps.bandcheck.outcome, a hard failure in uv run aelf bench all ... will skip the band-check and still leave the workflow successful. That turns adapter crashes, parser failures, and missing-data failures into false-green nightlies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bench-canonical.yml around lines 75 - 89, The "Run aelf
bench all --canonical" step (id: bench) must not allow hard failures to mark the
workflow green — remove or set continue-on-error: false for the bench step so
the `uv run aelf bench all --canonical --out "${out}"` command fails the job on
error; ensure downstream band-check still runs as a separate step that can
inspect the bench outputs but does not mask the bench step failure.

Comment thread .github/workflows/bench-canonical.yml
Comment thread benchmarks/run.py
Comment on lines +143 to +144
out_path = tmp_root / f"{inv.adapter}_{inv.sub_key or 'all'}.json"
cmd = [sys.executable, "-m", inv.module, *inv.args, "--output", str(out_path)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use unique per-invocation temp output files to avoid cross-run collisions.

{adapter}_{sub_key}.json under a shared tmp dir can be overwritten by concurrent bench all runs, producing mixed or flaky results.

Suggested fix
+import tempfile
...
-    out_path = tmp_root / f"{inv.adapter}_{inv.sub_key or 'all'}.json"
+    safe_sub = inv.sub_key or "all"
+    fd, tmp_name = tempfile.mkstemp(
+        prefix=f"{inv.adapter}_{safe_sub}_",
+        suffix=".json",
+        dir=str(tmp_root),
+    )
+    os.close(fd)
+    out_path = Path(tmp_name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/run.py` around lines 143 - 144, The current out_path uses tmp_root
/ f"{inv.adapter}_{inv.sub_key or 'all'}.json" which can collide across
concurrent runs; change out_path generation in benchmarks/run.py to produce a
unique per-invocation filename (for example by appending a uuid4, timestamp+pid,
or using tempfile to create a unique temp filename) so that inv.adapter,
inv.sub_key and inv.module still appear in the name but each invocation gets its
own file; update the cmd to use that new unique out_path variable.

Comment thread benchmarks/run.py
Comment on lines +186 to +187
def _default_runner(cmd: list[str], out_path: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(cmd, capture_output=True, text=True, check=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a subprocess timeout to prevent a hung adapter from blocking the harness.

A single stuck adapter process currently blocks the whole run indefinitely.

Suggested fix
-def _default_runner(cmd: list[str], out_path: Path) -> subprocess.CompletedProcess[str]:
-    return subprocess.run(cmd, capture_output=True, text=True, check=False)
+def _default_runner(cmd: list[str], out_path: Path) -> subprocess.CompletedProcess[str]:
+    return subprocess.run(
+        cmd,
+        capture_output=True,
+        text=True,
+        check=False,
+        timeout=60 * 30,  # 30 min per adapter, adjust as needed
+    )
🧰 Tools
🪛 Ruff (0.15.12)

[error] 187-187: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/run.py` around lines 186 - 187, The _default_runner currently
calls subprocess.run without a timeout, allowing a hung adapter to block the
harness; modify the _default_runner function to include a timeout argument when
calling subprocess.run (e.g., subprocess.run(cmd, capture_output=True,
text=True, check=False, timeout=300)) or alternatively add an optional timeout
parameter to _default_runner (def _default_runner(cmd: list[str], out_path:
Path, timeout: int = 300)) and pass it through to subprocess.run so stuck
processes are terminated after the configured timeout.

Comment thread benchmarks/run.py
Comment thread docs/v2_reproducibility_harness.md
Comment thread README.md
Comment thread src/aelfrice/cli.py
robotrocketscience added a commit that referenced this pull request May 7, 2026
Two bugs caught by the first local smoke run after PR #465 went up:

1. `from benchmarks import run` failed at runtime — `benchmarks/` is
   the top-level academic-suite directory and per pyproject.toml is
   dev-only / not packaged. `aelf bench all` is therefore reachable
   only from a source checkout. Added a cwd-presence check
   (`benchmarks/run.py` must exist), pushes cwd onto sys.path before
   importing, and emits a clear pointer ("git clone … && uv sync &&
   aelf bench all") when run from an installed-only context.
2. `rest = nargs=argparse.REMAINDER` on the bench subparser was dead
   code (`args.rest` is read nowhere) but it consumed every
   downstream optional flag, including the new --out / --canonical /
   --adapters / --smoke flags. Dropped it; new flags now parse
   correctly.

Smoke verified end-to-end after fix:
  uv run aelf bench all --smoke --out /tmp/bench-smoke.json
  → mab/Conflict_Resolution: ok (49.5s), amabench: ok (6.4s)
  → schema-v2 JSON written, real metrics inside.

All 46 dispatcher/tolerance/inert-target tests still green.
@github-actions github-actions Bot added the attn:merge-conflict PR branch needs rebase label May 7, 2026
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

This PR is now behind main. Rebase locally so your commit signatures stay intact:

git fetch origin && git checkout 'feat/issue-437-bench-all-harness' && git rebase origin/main
# resolve conflicts if any, then
git push --force-with-lease

Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the required_signatures rule on main then blocks the merge. See #341.

@robotrocketscience
robotrocketscience force-pushed the feat/issue-437-bench-all-harness branch from 92e094a to 8ccb9f3 Compare May 8, 2026 04:13
robotrocketscience added a commit that referenced this pull request May 8, 2026
Two bugs caught by the first local smoke run after PR #465 went up:

1. `from benchmarks import run` failed at runtime — `benchmarks/` is
   the top-level academic-suite directory and per pyproject.toml is
   dev-only / not packaged. `aelf bench all` is therefore reachable
   only from a source checkout. Added a cwd-presence check
   (`benchmarks/run.py` must exist), pushes cwd onto sys.path before
   importing, and emits a clear pointer ("git clone … && uv sync &&
   aelf bench all") when run from an installed-only context.
2. `rest = nargs=argparse.REMAINDER` on the bench subparser was dead
   code (`args.rest` is read nowhere) but it consumed every
   downstream optional flag, including the new --out / --canonical /
   --adapters / --smoke flags. Dropped it; new flags now parse
   correctly.

Smoke verified end-to-end after fix:
  uv run aelf bench all --smoke --out /tmp/bench-smoke.json
  → mab/Conflict_Resolution: ok (49.5s), amabench: ok (6.4s)
  → schema-v2 JSON written, real metrics inside.

All 46 dispatcher/tolerance/inert-target tests still green.
robotrocketscience added a commit that referenced this pull request May 8, 2026
The error message at `benchmarks.run.main_all` built the
`available:` list from `invocations` after the filter applied,
producing an empty list whenever the filter matched nothing — exactly
the case the message exists to help with.

Capture the available-adapters set BEFORE filtering so the user sees
the real list of valid adapter names when their --adapters flag fails
to match.

Refs Sourcery review on #465.
robotrocketscience added a commit that referenced this pull request May 8, 2026
…es (#437)

`hashFiles()` only accepts string literals as arguments — passing
`steps.bench.outputs.out` (an expression) is silently invalid and
returns an empty hash, so the band-check step's
`if: hashFiles(steps.bench.outputs.out) != ''`
condition was always-false and the band-check was skipped on every
cron run.

Gate on `steps.bench.outputs.out != ''` instead — when the bench
step succeeded (under continue-on-error), it sets the `out` output;
when it errored, the output is unset.

Refs Sourcery review on #465.
Capture the 2026-05-06 ratification of all eight design asks in the
spec memo. Seven resolved against the spec's recommendations; #2 (what
"all" means) overridden — operator picked full benchmarks instead of
the recommended sized headline cut.

Adds a Ratification section at the bottom with the resolved table, the
superseded headline-cut numbers (LongMemEval full vs oracle subset,
StructMemEval --bench big vs small), the multi-hour cron-runtime
implication, and the 8-step implementation order this PR follows.
Subprocess-per-adapter dispatcher that fans out the canonical headline
cut (full per the 2026-05-06 ratification), parses each adapter's
--output JSON, and merges into one schema-v2 file.

Registry-driven: CANONICAL_INVOCATIONS lists every (adapter, sub_key,
module, args) tuple. MAB and StructMemEval are multi-invocation (4
splits / 4 tasks); the rest are single-invocation. SMOKE_INVOCATIONS
is a separate ≤2-minute registry the PR-CI tier consumes.

--canonical flag asserts the active invocation set matches
CANONICAL_INVOCATIONS verbatim; mismatched cut → SystemExit so a
partial run cannot overwrite v2.0.0.json. Adapter exit-code contract:
0 ok / 2 skipped_data_missing / non-zero error. The runner is
injectable so unit tests can exercise the merge / schema / cut-mismatch
paths without spawning real benchmarks.

Reachable as `python -m benchmarks.run` immediately; cli.py wiring for
`aelf bench all` lands in the next commit.
`target == "all"` short-circuits to benchmarks.run.main_all() with the
new --out / --canonical / --adapters / --smoke flags. Removes "all"
from _BENCH_INERT_TARGETS so the inert path no longer shadows.

Single-name targets (mab, locomo, longmemeval, structmemeval,
amabench) stay inert in cli.py — the dispatcher is the only
v1 wrapper. Direct adapter invocation (`python -m
benchmarks.<name>_adapter`) remains the path until a sub-subcommand
wrapper lands.

Docstring updated to point at the 2026-05-06 ratification.
Relative-with-floor band policy ratified 2026-05-06. Each canonical
metric value gets a [lower, upper] band:

- Relative: ±X% of canonical, X picked from per-metric defaults (F1
  ±7%, exact-match ±10%, latency ±25%) or per-metric overrides in
  the canonical JSON.
- Absolute floor: ±0.02 (2pp on a 0..1 metric) when relative would be
  tighter — prevents tiny-value flapping.
- Soft warn: drift inside band but >50% of band half-width emits
  warn (workflow notice) instead of pass.

Walks the canonical results tree leaf-by-leaf so per-adapter shape
(MAB by-split, StructMemEval by-task, etc.) needs no special-casing.
Missing leaves in the observed report are FAIL not silent. Extra
leaves are ignored — canonical is source of truth for what's checked.

`load_report` enforces schema_version=2 on read.
27 tests total, all green.

Dispatcher (10 tests):
- registry counts (4 MAB + 1 LoCoMo + 1 LongMemEval + 4 StructMemEval
  + 1 AMA-Bench = 11 canonical, 2 smoke)
- end-to-end smoke run produces schema-v2 JSON
- --canonical with partial --adapters refuses
- --canonical with full set accepts
- adapter exit-code 2 → status=skipped_data_missing, overall rc=2
- error overrides skip in overall rc
- unknown adapter filter raises
- headline_cut declaration recorded (LongMemEval full / StructMemEval
  --bench big — captures the override from spec recommendation)
- runner-crash exception → status=error

Tolerance (17 tests):
- relative band for f1 / exact_match / latency
- absolute floor activates for tiny canonical values
- per-metric override takes precedence
- pass/warn/fail classification at boundaries
- nested-leaf walk over adapter shapes
- missing leaves → FAIL, extras → ignored
- underscore-prefixed metadata keys skipped
- summarize: FAIL dominates WARN dominates PASS
- schema_version=2 enforcement on read

Both modules use injectable runners / pure-data inputs so tests run
in <1s without spawning real benchmark subprocesses.
Daily cron at 05:00 UTC (staggered after replay-soak's 04:00) runs
`aelf bench all --canonical` and writes the merged JSON to a
dedicated `bench-canonical-results` branch — same dedicated-branch
pattern replay-soak adopted in #461 to sidestep the main-branch
ruleset (`required_signatures`, no direct push). No long-lived
signing key required.

Band-check step compares the cron entry against
`benchmarks/results/v2.0.0.json` via benchmarks.tolerance and fails
the job on any band-busting regression. WARN-tier drift (inside band,
>50% of half-width) commits and passes — the workflow notice surfaces
without paging.

timeout-minutes: 360 (6h) for the full-cut runtime; tunable down once
real cron entries land. continue-on-error on the bench step so a
crashing adapter still commits the partial JSON for diffability.

PR smoke tier deferred — pytest matrix already exercises the
dispatcher and tolerance modules with stubs, which catches harness
regressions in the same way the spec's PR smoke job would. A real PR
smoke against tiny adapter inputs needs offline fixtures
(`tests/fixtures/bench_smoke/`) which is substantial scope; tracked
as a follow-up.
…l` (#437)

README:
- Reproducibility badge between OSSInsight and the lede. Wrapped in
  `bench-canonical-badge:start/end` markers so the cron can rewrite
  just the badge line without touching surrounding text. Initial
  state is `pending first canonical run` (lightgrey) until the
  operator runs the calibration pass.
- New `## Reproducibility` section before `## Roadmap` documenting
  `aelf bench all --canonical` as the ship-gate command and pointing
  at docs/v2_reproducibility_harness.md.

COMMANDS:
- New row under Diagnostics for `bench all --out PATH ...`. Documents
  --canonical / --adapters / --smoke and the 0/1/2 exit-code contract.
- Existing `bench [--top-k N]` row (synthetic harness) stays as-is.
…anonical (#437)

Bug spotted while writing the skeleton v2.0.0.json: the canonical
schema declares a `metric_overrides` field, but check_report ignored
it unless the caller explicitly passed one. Cron-time band-checking
would have used DEFAULT_RELATIVE_BANDS for everything, silently
ignoring per-metric overrides operators wrote into the canonical.

check_report now falls back to canonical["metric_overrides"] when the
caller doesn't pass one. Explicit caller-passed overrides still take
precedence (used by tests). +2 tests pin the fallback behavior.
Schema-v2 skeleton with the canonical headline_cut declaration matching
benchmarks.run.CANONICAL_INVOCATIONS exactly: 4 MAB splits, 1 LoCoMo,
1 LongMemEval (full per the override), 4 StructMemEval tasks
(--bench big), 1 AMA-Bench. results: {} until the operator runs the
calibration pass.

`_calibration_pass_required: true` flags the file as not-yet-canonical;
`_calibration_protocol` describes the procedure (3+ runs, observed
range × 1.5 as per-metric override band, full data dirs and judge
keys present). The cron's band-check step against this skeleton is
a no-op (no leaves to check, summarize → PASS) until results is
populated, which is the intended behavior — harness lands first,
canonical numbers land in a follow-up commit by the operator.
…ation (#437)

Removes "all" from the parametrized inert-target list (it now
dispatches via benchmarks.run.main_all) and adds a new
test_aelf_bench_all_requires_out test pinning the --out-required
exit-2 contract.

The other inert targets (mab, locomo, longmemeval, structmemeval,
amabench) still emit the v1.0 pointer-to-README message — only `all`
moved.
10 atomic commits total (this is #11). Lands the harness, dispatcher,
band-checker, nightly cron, docs, and skeleton canonical JSON. The
calibration pass that fills v2.0.0.json with real numbers is the
explicit operator action this gate hands off.

Verification:
- pytest: 2624 passed, 41 skipped (no regressions vs main).
- New tests: 29 unit (10 dispatcher / 19 tolerance, all stub-driven,
  <1s wall) + 2 inert-list/integration adjustments.
- Discretion grep against entire branch diff vs github/main: clean.
- benchmarks.run loads + canonical_invocations matches the spec
  (4 MAB + 1 LoCoMo + 1 LongMemEval + 4 StructMemEval + 1 AMA = 11).
- benchmarks.tolerance compute_band/classify/check_report exercised
  end-to-end via tests including override-from-canonical fallback.
- Workflow YAML parses; uses the same dedicated-branch pattern
  replay-soak adopted in #461 (sidesteps main ruleset).

Blockers (require user decision before next phase):
- [user] Calibration pass: run `aelf bench all --canonical --out
  benchmarks/results/v2.0.0.json` ≥3 times against full data dirs
  (HF caches populated, /tmp/LoCoMo + /tmp/StructMemEval present,
  ANTHROPIC_API_KEY for judge metrics if in scope), take observed
  range × 1.5 as the per-metric override band, replace the skeleton
  with the result. Until this lands, the cron's band-check is a no-op
  (no leaves to check → PASS by default).
- [user] PR-smoke fixtures (offline): not landed in this PR. Spec
  recommended `tests/fixtures/bench_smoke/` pinned fixtures so a 2-min
  PR smoke job can exercise real adapter shapes without HF download.
  This needs licensed-content review (LoCoMo turns, MAB QA) and is
  scope for a follow-up. Pytest unit tests cover dispatcher
  regressions in the meantime via stubs.
- [user] README badge cron-rewrite path: badge ships with placeholder
  state ("pending first canonical run"). The cron's
  `bench-canonical-results` branch commit is wired, but the README
  in-place rewrite (between `bench-canonical-badge:start/end` markers)
  is not. Wire it in a follow-up after first canonical run, or wire
  it now if you want — small sed in the workflow.

Open questions:
- adapter exit-code 2 contract (skipped_data_missing): dispatcher
  honors it but no adapter currently emits it. Spec called for it.
  Can land per-adapter as separate small PRs once data-dir plumbing
  is decided.

Rollback:
- `git revert 2a11f22..HEAD` (or revert just the `feat(cli)` commit
  b5564d1 to disable `aelf bench all` while keeping the modules
  available via `python -m benchmarks.run`).
Two bugs caught by the first local smoke run after PR #465 went up:

1. `from benchmarks import run` failed at runtime — `benchmarks/` is
   the top-level academic-suite directory and per pyproject.toml is
   dev-only / not packaged. `aelf bench all` is therefore reachable
   only from a source checkout. Added a cwd-presence check
   (`benchmarks/run.py` must exist), pushes cwd onto sys.path before
   importing, and emits a clear pointer ("git clone … && uv sync &&
   aelf bench all") when run from an installed-only context.
2. `rest = nargs=argparse.REMAINDER` on the bench subparser was dead
   code (`args.rest` is read nowhere) but it consumed every
   downstream optional flag, including the new --out / --canonical /
   --adapters / --smoke flags. Dropped it; new flags now parse
   correctly.

Smoke verified end-to-end after fix:
  uv run aelf bench all --smoke --out /tmp/bench-smoke.json
  → mab/Conflict_Resolution: ok (49.5s), amabench: ok (6.4s)
  → schema-v2 JSON written, real metrics inside.

All 46 dispatcher/tolerance/inert-target tests still green.
First-canonical-run finding: raw merged JSON was 37MB, dominated by
per_question lists (6,667 rows across 6 adapters at full cut). The
band-check doesn't read per-row scores; canonical contract is
summary metrics that change rarely. Stripping per_question shrinks
v2.0.0.json from 37MB → 6.5KB without information loss for the gate.

`_strip_detail()` walks the output recursively; field set is
configurable via `_DETAIL_FIELDS_TO_STRIP` (currently just
`per_question`; add to the set if a future adapter emits another
per-row list). +1 test pinning the behavior with a 2000-row payload.
Per-row detail still exists in the in-memory output and the
adapter's --output write path; only the merged dispatcher JSON is
stripped.
… it (#437)

Same calibration finding as the per_question strip: dispatcher-level
metadata fields (status, elapsed_sec, error_message) were exposed as
peers of the adapter's `output` block. tolerance.check_report walks
all numeric leaves under `results`; elapsed_sec varies every run and
would FAIL every band-check by design.

Renamed to `_status` / `_elapsed_sec` / `_error_message` so the
existing underscore-prefix skip in `_walk_leaves` keeps them out of
the metric walk. Tests updated; same 47 pass.
…bers (#437)

Replaces the skeleton with the 2026-05-07 canonical pass. 6 of 11
invocations green (MAB ×4, LongMemEval, AMA-Bench); 5 errored due
to missing /tmp/ data dirs (LoCoMo + StructMemEval ×4). Total wall
~33 min on the local checkout.

Headline numbers in this canonical:
- MAB f1: 0.0001 (Test_Time_Learning) → 0.1811 (Long_Range_Understanding)
- MAB substring_em: 0.0234 → 0.7025 (Conflict_Resolution wins big)
- LongMemEval: avg_beliefs/q = 49.15, avg_latency_ms = 7.8, n=500
- AMA-Bench: 208 episodes, 2496 QA (no aggregate F1 — adapter-side
  aggregation gap; counts only)

Per-metric override bands not calibrated yet (3+ runs × 1.5 spread
is a follow-up). Defaults from `benchmarks.tolerance` apply.

Spec memo updated with § First calibration pass: per-invocation
status table, three dispatcher-level fixes that landed alongside,
and the open follow-ups (adapter exit-code-2 contract, AMA aggregation,
LongMemEval LLM-judge pass, dispatcher-side path scrubbing).

Local-run absolute paths in error messages sanitized by hand:
$HOME / $TMPDIR substitution before commit. CI cron writes from
/home/runner/... so this isn't a recurring concern; flagged as a
follow-up if an operator re-runs locally.
…#437)

First canonical pass landed real numbers for 6 of 11 invocations
(MAB ×4, LongMemEval, AMA-Bench). LoCoMo and StructMemEval ×4
errored on missing /tmp/ data dirs and stay at status="error" until
operator populates the data and re-runs those adapters.

Badge color: yellow (partial), not green (full). Cron will flip it
on the next pass that brings all 11 to ok.
The error message at `benchmarks.run.main_all` built the
`available:` list from `invocations` after the filter applied,
producing an empty list whenever the filter matched nothing — exactly
the case the message exists to help with.

Capture the available-adapters set BEFORE filtering so the user sees
the real list of valid adapter names when their --adapters flag fails
to match.

Refs Sourcery review on #465.
…es (#437)

`hashFiles()` only accepts string literals as arguments — passing
`steps.bench.outputs.out` (an expression) is silently invalid and
returns an empty hash, so the band-check step's
`if: hashFiles(steps.bench.outputs.out) != ''`
condition was always-false and the band-check was skipped on every
cron run.

Gate on `steps.bench.outputs.out != ''` instead — when the bench
step succeeded (under continue-on-error), it sets the `out` output;
when it errored, the output is unset.

Refs Sourcery review on #465.
@robotrocketscience
robotrocketscience force-pushed the feat/issue-437-bench-all-harness branch from 8ccb9f3 to a279f7a Compare May 8, 2026 04:16
@robotrocketscience
robotrocketscience merged commit a279f7a into main May 8, 2026
21 of 22 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-437-bench-all-harness branch May 8, 2026 04:18
robotrocketscience added a commit that referenced this pull request May 8, 2026
Per the 3-state exit-code contract ratified at PR #465 gate commit
60e1b68 and resurfaced by the StructMemEval calibration session in
issue #473: adapters that find no data (data dir missing, dataset
file absent, filters matching zero rows) now sys.exit(2) instead of
print + plain `return`.

The dispatcher (`benchmarks/run.py::run_invocation`) already classifies
exit-code 2 as `_status: skipped_data_missing` per the 2026-05-06
contract; this commit closes the producer half so the canonical JSON
distinguishes "data not installed" from "adapter crashed."

Adapters touched (uniform pattern: try/except FileNotFoundError around
the load, plus the existing post-load empty-list guard, both routing
to sys.stderr + sys.exit(2)):

- structmemeval_adapter.py — surfaced the bug via issue #473's
  /tmp/StructMemEval-empty trace.
- locomo_adapter.py — load_locomo() reads a JSON file directly.
- longmemeval_adapter.py — load_from_file() OR HuggingFace fallback.
- mab_adapter.py — load_mab_split() can FileNotFoundError on absent
  HuggingFace cache.
- mab_entity_index_adapter.py — same load_mab_split() path.
- amabench_adapter.py — HuggingFace load_dataset() typically raises
  on absent data; if it returns zero rows after filtering, also exit 2.

The dispatcher's existing fallback at run.py:167-172 (`adapter exited
0 but did not write $TMPDIR/T/<adapter>.json`) becomes a strict
"adapter bug" signal rather than masking missing-data situations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:merge-conflict PR branch needs rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants