refactor(app): hold one outcome per check and enrichment report - #323
Conversation
kstonekuan
left a comment
There was a problem hiding this comment.
Merging. PublishFailed is the part I want to call out: I wrote the issue asserting three variants and you found a fourth by reading the publish pass, which is the one place the old shape genuinely needed two fields set. Collapsing it to Errored would have dropped the labels and no test would have caught it, because nothing asserts labels survive a publish failure. Now one does.
The derived-property move is what made this land in three files instead of thirty. Reads keep working, writes don't compile, which is exactly the right half to break.
Validated on your branch: ruff check, ruff format --check, ty check clean, 1266 passed / 6 skipped, no test file touched except the new one. That last part is the evidence for DoD 3: every status assertion in test_default_checks.py, test_gates.py and test_run_profiles.py held without edits.
I checked the invariant statically, since a runtime TypeError test passes even if the type checker has nothing to say. ty on the bad constructions:
error[missing-argument]: No argument provided for required parameter `outcome`
error[unknown-argument]: Argument `result` does not match any known parameter
error[unknown-argument]: Argument `error` does not match any known parameter
So the old three-field spelling is a type error, not just a convention.
Two notes, neither blocking.
The else Errored(combined_error) branch at the publish failure is unreachable. The loop above it does if enrichment_result is None: continue, so enrichment_run.result is non-None every time you get there, including on a second failing artifact (PublishFailed.result keeps it, which is why the accumulation still works). Defensive rather than wrong, so I'm leaving it.
Measured is frozen but shallow: _apply_gate still mutates result.verdict in place after construction. Correct, and the same as before, but "set once" is about which variant the report holds, not about the result's contents. Worth knowing before someone reads frozen=True as more than it is.
Also grepped docs/, examples/, packages/ and README.md for the old construction spelling: nothing outside app.py builds either report, so there's nothing else to update.
Closes #205.
CheckRunReportandEnrichmentRunReportnow carry oneoutcomefield instead of three optional ones. The variants areMeasured(result),Errored(error)andNotRun(step_not_run), withPublishFailed(result, error)on the enrichment side.outcomehas no default, so a report with no outcome is aTypeErrorat construction and themeasuredfallback is gone.statuson both is a match over the outcome. No precedence, no fallback.result,errorandnot_runstay as read-only properties derived from the outcome, which is why 53 reads inapp.pyand about 100 in the tests did not have to move.Nothing reads a partially built report. Every reader of
TestReport.checksand.enrichmentsruns after its loop finishes:_check_run_rows,has_errors,summary,_yield_defaults_superseded_by_the_pipelineand the artifact publishing pass. Every other.checks/.enrichmentsin the tree isApp.checksorManifest.checks, a different attribute. So moving the append to after the outcome exists changes when an entry becomes visible, and nothing observes the difference.One state I had to keep. At the artifact publishing pass, a publish failure sets
erroron an enrichment that already holds a result, and_check_run_rowsstill records that result's labels. Collapsing it toErroredwould drop those labels. So it gets a fourth named variant,PublishFailed, which keeps them while the run still reportserror.No test asserted that an empty report is
measured. Nothing in the suite constructs either report directly, so there was no such assertion to change.Validation
uv run pytest -q: 1246 passed, 11 skipped, no test file editeduv run ruff check/ruff format/uv run ty check: cleancheck_runs.statusunchanged: 114 rows over a 3 episode corpus run twice, once tripping a critical check and once not, coveringmeasured,passed,failed,error,skippedandsuperseded. Identical onfbfd553and this branch, sha2567ac8466f.tests/test_run_report_outcome.pypins that neither report can be built without an outcome, that each variant still produces the status it produced before, and that a publish failure keeps its labels.