RFC: grouped/sibling ResultSets with rollup semantics (mutation testing, seed sweeps, model comparisons) — follow-up to #36 #45
Replies: 9 comments
Update: both open questions answered from muteval's actual current source, plus a fully tested reference implementationRather than let this sit waiting on a reply, I went back and read Q2 — does muteval know the total mutant count upfront? Yes, verified.
(This also generalizes the field correctly: a producer whose grouping strategy doesn't know its member count upfront — an incrementally-discovering search, say — can simply omit Q1 — a role vocabulary for mutation testing? A narrower one than I first assumed.I'd originally floated The one dimension that does map cleanly onto a single A fully tested reference implementation, ready for reviewRather than leave the shape above as prose, I built and locally verified the complete implementation on a separate branch — explicitly not merged to → PR #46 (draft, "DO NOT MERGE") It covers every layer this kind of change needs, matching the precedent #22 → #35 set:
Verification, run locally in this session — nothing fabricated: To be clear about status: this doesn't shortcut the comment period — the PR is a draft specifically so the design has something concrete and fully working to review against, not a claim that consensus has been reached. Still very much open to pushback on the |
|
Thanks for chasing this all the way into a proper RFC — and for reading the source rather than making me answer from memory. Confirming both: Total mutant count upfront: yes. select_mutants() is synchronous and returns the full list before any evaluation runs, so the count (and a stable index) exist at per-mutant emission time. sequence is well-defined for this domain. role = killed / survived / errored: right as the primary verdict — that's exactly the MutantOutcome state. One caveat worth baking into your thinking, because it's where a naive rollup would go wrong: muteval's "survived" isn't one thing. A survivor is either a real coverage gap or an observationally-unchanged (inert) mutant whose output matched the baseline — and muteval drops the inert ones from the effective score, which is the number that actually means something. On top of that, the verdict is a majority vote over runs_per_mutant, and each mutant carries a severity. So killed/survived/errored is a faithful coarse verdict, but a consumer reconstructing a mutation score from only that gets muteval's raw score, not the effective one, and loses the inert exclusion. If role is meant as a coarse label, three values is right; if it's meant to reconstruct the rollup, "survived" needs to distinguish real-gap vs inert. On the bigger call: agreed the rollup stays consumer-computed (Stryker not baking in a score field is the right precedent). I'm not committing muteval to emit EvalPort — result_to_dict() stays the stable surface on our side — but happy to be the data point for the shape. If grouped/sweep ResultSets land with real usage behind them, glad to revisit. |
Update: role/metadata refinement implemented, tested, and CI is fully green on PR #46Closing the loop on the "survived isn't one thing" gap raised above: it's now fully implemented on PR #46, with zero schema changes needed. Concretely:
Also fixed along the way: PR #46's CI had been red on the Still a draft, still not proposing to merge ahead of the comment period — just wanted the reference implementation to actually reflect where this design landed. |
|
Update: the reference-implementation branch now grounds all three of issue #36's named use cases for Mutation testing (muteval) and hyperparameter sweeps (Optuna) were already covered above. The remaining one — issue #36's "a seed-sweep or a multi-model comparison" — was, until now, only asserted; it's now checked against
Also fixed along the way: Current state: 138 Python tests (2 pre-existing, unrelated skips) / 155 TypeScript tests / 141 CLI tests, all passing; 16/16 conformance fixtures passing, 6 of them covering |
Update: a sixth confirming system, plus one honest counter-example — checked deliberately, not just accumulatedFive independent systems (W&B, MLflow, Stryker, Optuna, promptfoo) converging on the same shape is strong evidence, but five wins in a row is also exactly the point where a design should go looking for something that might disagree rather than stop at a comfortable streak. The remaining gap in the precedent set was that all five are open-source libraries — none of them a managed, hyperscaler-run service. So the two largest cloud ML platforms were checked next, specifically for that reason: Google Cloud's Vertex AI Vizier and AWS SageMaker's hyperparameter tuning API. Vertex AI Vizier confirms the pattern a sixth time. AWS SageMaker's hyperparameter tuning API does not confirm it — and that's worth stating plainly, not omitting. It doesn't change this RFC's design, for a reason that's actually informative rather than a rationalization: a SageMaker tuning job has exactly one pre-declared This landed as grounding/rationale prose only — no schema, fixture, or SDK changes, since the design itself doesn't move. Verified before pushing: 138 Python tests (2 pre-existing, unrelated skips), 155 TypeScript tests, 141 CLI tests, 16/16 conformance fixtures, all passing locally; all CI checks green as of commit Still a draft, still not proposing to merge ahead of the comment period (through 2026-09-18) — this is additional grounding on the existing proposal, not a design change. |
Update: fixed a self-inflicted corruption of
|
Update:
|
Status: comment period closed (2026-09-18) — here's where this actually standsThe two-week comment period this project's governance process calls for closed two days ago. Being honest about what "consensus" means here for a project with one maintainer and one directly-relevant external voice: @AshwinUgale (muteval's maintainer, and the only external maintainer with a real stake in this specific shape) engaged substantively twice — confirming both the Beyond that one direct conversation, the design is grounded against six independently-read systems (W&B Sweeps, MLflow, Stryker, Optuna, promptfoo, Vertex AI Vizier) plus one honest counter-example (AWS SageMaker) that was discussed rather than omitted, and the nesting extension ( Given all of that, this RFC looks ready to move from draft reference implementation to an actual merge. I want to make that call deliberately rather than just because the calendar ran out, so I'm holding off finalizing the merge for a moment to make sure nothing else needs to weigh in — will follow up here once it lands, the same way #22 got a "this has landed" comment for #35. |
|
Landed: #54 merged into Summary of what shipped, for anyone following this thread without reading the PR: Thanks again to @AshwinUgale for the two rounds of substantive engagement here — the raw-vs-effective mutation score distinction you caught is directly reflected in how Closing the loop on the two-week comment period → rough consensus → merge process for this one. If anyone hits a case this doesn't cover cleanly (a grouping shape muteval or another sweep tool needs that |
Uh oh!
There was an error while loading. Please reload this page.
Summary
This is the promised follow-up to #36 ("No representation for grouped/sweep ResultSets with rollup semantics"), which itself grew out of a cross-project conversation in AshwinUgale/muteval#44. #36 flagged the gap precisely but deliberately didn't propose a shape yet. Since filing it I've read the current
spec/schemas/resultset.json(post-#35) end to end and looked at how three real systems that already solve some version of this problem represent it. This discussion proposes a concrete direction, grounded in that reading, and is the start of the same comment period #8→#22 went through before #22 became #35.Restating the gap against the current schema
resultset.jsontoday (verified by reading the live schema, not from memory) has no field anywhere — top-level or per-Result— that relates oneResultSetto a set of siblingResultSets. The closest thing issuite_id, which is informal: twoResultSets can share asuite_idby convention, but nothing in the schema says "these are members of one group" the way#35's(test_case_id, run_id, attempt)says "these are repeated trials of one case."#35fixed within-run repetition. It did nothing for across-run grouping, because that wasn't its job.muteval's actual shape makes this concrete: one
ResultSetper mutant (already gets a legitimate home for its ownruns_per_mutantmajority vote viaattempt+isolation), but nothing joins the N per-mutantResultSets into "this sweep," and nothing carries the sweep-level rollup (mutation score, survivor list) anywhere a generic EvalPort consumer would recognize as such — it would have to live in unstructuredmetadata.*, which is exactly the "no generic consumer can read this" failure mode the spec exists to avoid.Prior art: how three real systems already solve (part of) this
I looked at how three tools outside the EvalPort ecosystem represent grouped-run relationships, since precedent from working systems is worth more than a shape I'd invent from scratch:
W&B Sweeps. A
Sweepobject is a distinct container with its ownid/config/state, and eachRunjoins it via asweep_idfield — the join is on the member, pointing at the group, not the other way around. TheSweepobject exposesruns(the member collection) and abest_run()method, butbest_run()is a query the client computes over the member runs' logged metrics at call time — W&B does not persist a pre-computed rollup document as part of the sweep's stored schema. (W&B Sweep API docs)MLflow nested runs. A child run created via
mlflow.start_run(nested=True)carries a link back to its parent, retrievable viamlflow.get_parent_run(run_id)— again a pointer on the member, and again no group-level rollup stored anywhere; the UI computes hierarchy display and any aggregate view from the member runs' own metrics on the fly. (MLflow nested runs tutorial)Stryker's
mutation-testing-report-schemais the closest analogue there is to muteval's exact problem — it's a real, versioned, cross-language JSON Schema for mutation-testing reports, with astatusenum ofKilled/Survived/NoCoverage/CompileError/RuntimeError/Timeout/Ignored/Pendingon each mutant. I checked specifically for a rollup/mutation-score field at the schema level: there isn't one. Consumers compute the mutation score themselves from the per-mutantstatuscounts. (mutation-testing-report-schema.json)The pattern across all three, independently arrived at: the join key lives on the member and points at the group; the group-level rollup is computed by the consumer, not stored as a schema-mandated document. That's a real signal, not just my preference, and it directly bears on which of #36's three directions to pick.
Proposal
Extend
ResultSetwith an optionalgroupobject — the member-points-at-group shape every precedent above uses — and explicitly do not standardize a rollup/manifest document in this schema. That resolves the "not proposing exact shape yet" line in #36 as a lightweight version of direction (1), with direction (1)'s "separate rollup manifest" piece deliberately dropped in favor of precedent.Added as a new optional top-level property alongside
isolation/runner/provider, soadditionalProperties: falseonResultSetstill holds and a validator flags typos the same way it does today.Worked example: muteval's actual case
Three (of N)
ResultSets from one mutation-testing sweep, each already usingattempt+isolationinternally for its own majority-vote-per-case behavior, now also carryinggroup:{ "version": "1.1.0", "suite_id": "billing-suite", "run_id": "mutant-017-run", "started_at": "2026-09-01T10:00:00Z", "isolation": "fresh", "group": { "group_id": "mutation-sweep-2026-09-01", "role": "mutant", "label": "mutant_017 (relational-operator-swap in billing.py:42)", "sequence": 17 }, "results": [ { "test_case_id": "case_1", "attempt": 1, "grader_results": [...], "passed": true }, { "test_case_id": "case_1", "attempt": 2, "grader_results": [...], "passed": true }, { "test_case_id": "case_2", "attempt": 1, "grader_results": [...], "passed": false } ] }A muteval-side tool (or any consumer) can now
SELECT * WHERE group.group_id = "mutation-sweep-2026-09-01"across a directory ofResultSetfiles, compute killed/survived from each member's ownresults[].passed, and produce the mutation score itself — exactly the division of labor Stryker's own schema settled on.What this deliberately does not do
It does not standardize how a rollup is computed (mutation score, best-run selection by some metric, win-rate for a model comparison) — those differ enough by domain that baking one in would either be mutation-testing-shaped and useless for seed sweeps, or so generic it says nothing. That computation is exactly what direction (3) from #36 (a
profile:convention) is for, the same way stability-over-repeated-attempts computation was deliberately deferred out of #22/#35. I'd rather land the join key now — it's genuinely domain-agnostic and every precedent above independently converged on the same shape — and let amutation-score-v1-style profile be its own future RFC once there's a second and third real consumer to generalize from, rather than guess at a rollup shape today from one data point.Why not direction (2) (purely informal
suite_idconvention)?Same objection raised against the equivalent informal option in #22: it formalizes the status quo instead of fixing it.
suite_idalready exists and nothing stops two unrelatedResultSets from colliding on it by coincidence, or a producer from reusing it across genuinely different groups. A dedicatedgroup_idwith an explicit schema slot is unambiguous in a way an overloaded field can't be, at the cost of one small, fully-optional addition.Question for @AshwinUgale
Two things I'd like muteval-side input on before this is anywhere near final:
roleneed any standardized vocabulary for mutation testing specifically (e.g. reserving"killed"/"survived"/"pending"/"timeout"as conventional-but-not-enforced values, mirroring howisolationdocuments"fresh"/"shared"/"unknown"as conventional without closing the enum), or is a fully free-form string enough for how muteval would actually emit these?ResultSetis written (sosequencewould be populated and meaningful), or is that only known once the whole sweep finishes? If the latter,sequencemay be dead weight for muteval's actual emission order and I'd want to hear that before keeping it in the final shape.Process
Leaving this open for comment before any schema/SDK PR, same as #22. If it holds up, the implementation path is the same one #22→#35 used: schema addition here, both reference SDK validators (
sdk/python/openeval/validate.pyand its counterpart),SPEC.md, and a conformance fixture pair (spec/conformance/fixtures/) demonstrating a valid groupedResultSetand an invalid one (e.g.grouppresent withoutgroup_id). I'll post a "this has landed" follow-up here once that PR merges, the same way #22 got one for #35.All reactions