Repository navigation
feat(judge): validated named metrics and persistence (script judge, slice 1) - #347
GBX-Max1220 wants to merge 11 commits into
Conversation
) Adds the first bounded slice of the alibaba#317 contract: a per-judge metric registry, four independent state axes (execution stage, completion, termination cause, slot availability), a derived quality state, whole-payload versus single-slot validation, and group completion folding that preserves earlier member detail. Execution metadata and planned metric slots are recorded on the member outcome and the grouped path even when no judge result exists, so a gate short-circuit or a cancellation never loses them. An accepted score survives a later cancellation or timeout, and the timeout, its source and any recovery stay visible. Existing PASS/FAIL/SKIP/ERROR verdicts, the alibaba#309 assertion and member denominators, script-timeout verdict mapping and the JSONL event schema are unchanged.
The previous commit added the state machine but no producer: both grouped call sites materialized slots from a nil emission list, so no judge could ever report an AVAILABLE metric and DeriveQuality/FoldGroup had no production caller. This adds the missing slice-1 path. - script judge executes, collects its invocation-bound result file, validates it and materializes one slot per planned registry entry - whole-envelope defects (duplicate json keys, unknown top-level field, bad schema version, unbound or stale invocation, size limit) poison every slot; entry-level defects poison only their own slot and keep valid siblings - a timeout no longer discards a trustworthy result: collected scores stay AVAILABLE while the existing script-timeout verdict mapping is preserved, and slots never reached become NOT_PRODUCED rather than an invented zero - group completion and cause are derived with FoldGroup and persisted into evaluation.json beside the unchanged all_required verdict - integration tests spawn the real script judge through executeCase; the process-kill cancellation case is POSIX-only for the same process-group reason upstream already documents Removes the unconsumed definition-compatibility helper and the cancel sources that slice 1 cannot actually detect; those return with their consumers.
Found while reviewing the real data flow rather than the helper functions: - accepted only means the member reached its own final outcome: no stop, or a deadline that still left collected scores. A cancellation arriving mid-run is never acceptance; previously a killed member kept a COMPLETED-looking verdict and its untouched slots read MISSING instead of NOT_PRODUCED - the collector Lstat-s the assigned path and requires a regular, non-symlink file, and checks the size before reading instead of after - an unassigned channel (non-local runtime) reports metrics_channel_unsupported rather than pretending a stale file was found - the real child-process timeout path is now covered on Windows too: the script writes its bound result, outlives the 1s deadline and is killed; long sleeps were shortened because 30s orphans destabilised concurrent package builds - README documents the payload grammar, injected variables, persistence targets and an explicit not-implemented list for this slice
|
Thanks for the first implementation slice. I reviewed HEAD
Please add regression tests through the real evaluator paths, asserting verdicts and persisted metadata alongside envelope values. Existing tests that require PASS for every exit-zero metrics script, or INCOMPLETE for an optional omission, should be updated to the agreed contract. The bounded scope remains appropriate: Agent-judge numerical output, sandbox collection, report rendering, comparisons, and JSONL schema extensions can remain in follow-up PRs. |
JHWang-1997
left a comment
There was a problem hiding this comment.
Reviewed commit 72b449d356930945fb573da4ffafa23bd48ba8b0. Requesting changes before merge.
I independently reproduced the six issues in the existing review, and they remain present at this head. Please address those findings: applying the opt-in metric policy to verdicts, preserving single-judge execution metadata and planned slots, finalizing grouped state on early returns, respecting optional metrics when deriving completion, preserving the configured judge identity, and attributing the deadline that actually expired.
The verdict issue is the highest priority: an exit-zero script with min_score: 0.8 and score 0 still produces a case-level PASS, despite metric_quality_state=NOT_MET. Missing required metrics and an invalid score of 2 also leave the case passing. Configurations that explicitly opt into these constraints need the agreed failure/error semantics, while configurations without metrics should retain their existing behavior.
I also found an additional deterministic test-fixture defect, described in the inline comment: the fast and slow judges in TestExecuteCaseCancellationKeepsAcceptedResult reference the same script file. The second write overwrites the first, so the test never reaches the accepted-result state it is intended to protect.
Validation on macOS in an isolated snapshot:
go test -race ./internal/judge ./internal/config ./internal/evaluator ./internal/report: judge, config, and report passed; evaluator failed atTestExecuteCaseCancellationKeepsAcceptedResult.- Temporary targeted reproducers confirmed the verdict, gate-state, optional-completion, identity, and deadline-attribution findings.
These requests stay within the current slice. Agent-judge numeric output, sandbox metrics collection, report rendering, comparisons, and JSONL schema changes can remain follow-up work.
| fast := metricsScript(t, scratch, pick(posixScore, powershellScore)) | ||
| slow := metricsScript(t, scratch, pick(sleepPosix, sleepWindows)) |
There was a problem hiding this comment.
[P2] Give the fast and slow judges distinct script paths
Both metricsScript calls use the same scratch directory, and the helper always writes score.sh on POSIX, so creating slow overwrites fast before either member runs. Consequently, the first member also sleeps and is cancelled, causing TestExecuteCaseCancellationKeepsAcceptedResult to fail instead of exercising retention of an already accepted result. I reproduced this on macOS with go test -race ./internal/evaluator -run '^TestExecuteCaseCancellationKeepsAcceptedResult$' -count=1: the first member was interrupted and its metrics were NOT_PRODUCED. Give the two scripts distinct directories or filenames, then rerun this test while preserving its assertions and platform coverage.
|
Thank you both for the detailed reviews and for reproducing these issues. I appreciate the concrete examples and the extra check on the cancellation test fixture. I’ll work through the reported issues, including the verdict handling, early-return paths, group state, metric identity, and timeout attribution. I’ll also fix the fixture and add regression tests that exercise the evaluator paths. I’ll update the PR when I have changes ready for another review. |
interruptedExecution recorded termination_cause and deadline_source but never the timeout metadata, so a grouped member stopped by its own judge deadline reported the cause without the structured record that README requires. Build it once in interruptedExecution and drop the duplicate in deriveSingleExecution.
Add a grouped judge-timeout case with a live but unexpired case deadline, the shape the review asked about, and assert the stop names judge.timeout_seconds. Assert the coverage buckets behind each contract verdict, the configured judge id on grouped members, and the folded group state on every skip short circuit. Move the two tests that kill a running child onto a scratch workspace, since Windows harness cleanup of a held working directory fails after all assertions pass.
Run a real script judge through NewEvaluator/EvaluateAll, write the case artifacts, then read evaluation.json back from disk and assert the configured judge id, FINISHED execution and the explicit zero score. Split the case into helpers so the complexity gate accepts it, and provide the PowerShell fixture so the path is verified on Windows instead of skipped.
|
Thanks for the detailed review. I pushed three commits to the PR branch at
The cancellation fixture now gives the fast and slow judges distinct script paths. The accepted-result-then-cancellation end-to-end test passes on Linux; it remains skipped on Windows because process-startup timing makes that ordering nondeterministic there. Validation on Linux passed: the real pre-commit hooks for all three commits, |
|
Thanks for the fixes. I re-reviewed HEAD Additional real-subprocess tests through
Please add evaluator-level regressions for timeout plus a below-threshold valid score, nonzero exit after writing a valid metrics file, and unchanged compatibility assertion counts under metric-policy failure. The first two need both execution and scoring outcomes asserted together; testing the collector or quality derivation alone misses the finalizer interaction. The first-slice scope remains appropriate. These three corrections should stay within scoring/persistence; the deferred Agent-judge output, sandbox collection, rendering, comparisons, and JSONL schema work need not be pulled into this PR. |
A below-threshold score must not turn a timeout ERROR into FAIL, because the case then loses the retryable timeout reason, and a script that exited nonzero must not be reclassified as a missing-required-score contract error. Report both metric decisions in the additive metric metadata instead, and stop injecting a synthetic compatibility assertion: the alibaba#309 assertion and member denominators stay exactly as the execution layer produced them.
…real path Each case runs a child script through executeCase and asserts the execution outcome and the metric state together: a timeout with a below-threshold score on the single and grouped paths, a nonzero exit after writing a valid result file, and unchanged compatibility counts when the metric policy decides.
Thanks for the three reproductions — they exposed real issues. I pushed fixes and regression tests in ba9635e and 0dfde3a.
The new tests exercise these cases through the evaluator with real script processes. I also checked that they fail when the production fixes are reverted. On Linux, the repository checks and race tests passed locally. The GitHub workflows for the new head are still awaiting approval, so I don't have CI results yet. The PR remains a draft, and the previously listed follow-up work is still outside this slice. |
feat(judge): validated named metrics and persistence (script judge, slice 1)
Refs #317. First bounded implementation slice, scoped per #317 (comment) — named metrics only, with report integration and cross-run comparison left for later PRs. Marked draft because this is slice 1 of the agreed plan and awaits maintainer confirmation.
What this adds
A per-judge named-metric registry, a script-judge result channel that feeds it, and the derived state that reaches the persisted artifacts. Per the maintainer decision,
NOT_RUNis dropped (a never-started and a stopped-mid-flight member are bothINTERRUPTED, separated byexecution_stage) and nogroup_execution_stateis introduced.Configuration —
judge.metrics[]withid,required,min_score,evidence_required, validated ininternal/config(identifier shape, duplicate ids, thresholds inside[0,1]). The registry is the coverage denominator: fixed before execution, never inferred from a returned payload. Unknown ids in output are diagnostics and never create slots; a duplicated id invalidates its planned slot once.Input channel (script judge only) — the runner assigns one private, invocation-bound result path per attempt and injects
EVAL_METRICS_RESULT_PATH,EVAL_METRICS_INVOCATION_IDandEVAL_METRICS_TEMPLATE. The path isLstat-ed and must be a regular, non-symlink file; its size is checked before it is read; the payload must echo the invocation id. Rejected as a whole envelope: unparseable JSON, non-object top level, trailing content, duplicate JSON keys at any level, unknown top-level or entry fields, wrongschema_version, unbound/stale invocation, oversized file. A nonzero exit never collects scores even if a valid file exists — the file stays an on-disk diagnostic. stdout remains evidence; there is no stdout fallback.State — four fields, one question each:
execution_stageNOT_INVOKED/STARTED/COMPLETEDcompletion_stateINTERRUPTED/FAILED/INCOMPLETE/COMPLETEDtermination_causenone/cancellation/deadline_expiry/expect_gate/process_exit/execution_error/contract_error/judge_declined(+deadline_source/cancel_source)score_availabilityAVAILABLE/MISSING/ERROR/SKIP/NOT_PRODUCEDmetric_quality_stateis present whenever the envelope exists and is derived from slots only, never fromcompletion_state: thresholds configured and all thresholded values available →MET/NOT_MET; thresholds configured but a thresholded value unavailable (including cancellation, gate and error cases) →UNKNOWN; valid scores with no thresholds configured →NOT_CONFIGURED; no valid scores →NOT_ASSESSED. A judge-declaredSKIPkeeps its own reason and is never reachable for a slot the execution layer never reached — that slot isNOT_PRODUCED, so an interrupted run cannot masquerade as a judge declining a metric. Scores are never fabricated as zero.Group completion is derived, not assigned — folded over member completions by
FAILED > INCOMPLETE > INTERRUPTED > COMPLETED, sofailed + pending-cancelledkeepsFAILEDwhile the cancellation and the unfinished members stay visible on their own members, andcompleted + pending-cancelledreadsINTERRUPTED. Members with no execution metadata contribute nothing, and an empty set leaves the field unset so the artifact omits it rather than inventing a state.Timeouts do not discard a trustworthy result — a child that writes its bound result and then outlives the deadline keeps both scores
AVAILABLE(an explicit0included) and the existing script-timeout verdict mapping is preserved unchanged: groupedERROR, single-judgeFAIL. Cancellation arriving while a member is running is never treated as acceptance, so its untouched slots readNOT_PRODUCEDrather than an omission from a completed judge.Whole-envelope vs single-entry rejection — an entry-level defect (out-of-range, non-finite, wrong type, evidence requirement unmet) poisons only that slot; independently valid siblings stay
AVAILABLEand are treated identically by the collector, the stored envelope and any later consumer.Compatibility, deliberately untouched —
PASS/FAIL/SKIP/ERROR; the #309 assertion and member denominators with zero-credit skipped members (skipped: trueretained insummary.totaland counted insummary.failed); theall_requiredone-assertion-per-member aggregation;grading.json; the JUnit mapping; the existing single-vs-grouped script-timeout asymmetry. No JSONL evaluation-event field is added here — that stays behind its own schema and consumer review.cancel_sourcereportsos_signalonly when an interrupt was actually observed andunknownotherwise; it is never inferred from error text.Persistence — the envelope rides on
judge.Result.Metrics(report.json→grading.metrics) and on each groupedjudge.Outcome(evaluation.json→judge_results[].metrics,judge_results[].execution), withaggregation.completion_state/aggregation.termination_causealongside the unchangedaggregation.status. Nothing lives only insidejudge.Result: a member with no result at all still carries its execution facts and its registry-derived slots.Verification status — stated precisely
Verified by real execution on this machine (Windows, go1.26.4, base
6fec8e9):0→ both scores (including0.0) reach the envelope and the derived state, throughexecuteCasefor the single-judge path and the grouped pathAVAILABLE,quality=NOT_CONFIGURED, and the verdict mapping staysERROR(grouped) /FAIL(single). This isTestScriptJudgeRealSubprocessTimeoutCollectsResult.INTERRUPTED/NOT_PRODUCED/NOT_ASSESSED, groupINTERRUPTED+cancellation, member assertion denominator unchangedWriteEvaluationwriterNot verified on this machine:
TestExecuteCaseCancellationKeepsAcceptedResult) is POSIX-only and skipped here: on Windows whether the first member finishes before the timer fires depends on process-startup timing, so the ordering is not deterministic. The branch therefore does not claim this ordering as proven end-to-end; the decision itself is covered at unit level byrecordMemberContextErrorwith a genuinely cancelled context and deterministically byTestExecuteCaseCancellationInterruptsRunningMember. Maintainer or CI confirmation on Linux is welcome.noneenvironment behaviour beyond the explicitmetrics_channel_unsupportedresultTest status:
go build ./...,go vet,gofmtclean.internal/judge,internal/config,internal/report,internal/clipass.internal/evaluatorshows 4 failures andinternal/runtimefails to build. I ran the identical full-suite command (./internal/... ./pkg/...,-count=1) on base commit6fec8e9in this same Windows environment and on this branch: both give 18 passing packages, the same 9 failing tests and the same 4 failing packages, including theinternal/runtimebuild failure. That comparison is environment-scoped: it shows this branch adds no new failure here, and it is not a claim of zero regressions on Linux/macOS, which CI will establish.Out of scope for this PR
agent_judgenumeric metric output (its strict result contract is separate work)noneenvironment is assigned a path; anything else reportsmetrics_channel_unsupportedinstead of pretending a stale file was foundCo-authored-by / AI assistance note: this slice was drafted with AI assistance against the contract in #317 and reviewed commit by commit; no test was weakened or skipped to make it pass.