Skip to content

Commit b79c33e

Browse files
d-burgclaude
andcommitted
REGRESSION - BUG FIX - Harden the golden mechanism per adversarial review
Fifteen findings from a line-level adversarial review, the load-bearing ones: - A --check that finds no golden file for any requested case now exits 1: a green gate that checked nothing was indistinguishable from a passing one, so a deleted or typo-named golden file silently disabled CI. Mixed coverage warns per case. - --update-golden now prints a REMOVED line for every pin it drops: an h5 rename made extraction return missing, the entry silently vanished from the rewritten file, and the git diff read like intentional cleanup. Deleting a gate is now loud. - Golden provenance records "-dirty" when generated from an uncommitted tree; the commit field is what a reviewer uses to reproduce a disputed number, and it was recording HEAD for numbers HEAD cannot reproduce. - save_golden refuses valueless entries (a NaN scalar became a gating entry with a class and tolerance but no number — failing forever, with regeneration reproducing it identically). SLAYER gamma is legitimately NaN when no root is found, so this path is reachable. - The check path now normalizes SQLite missing before compare_to_golden (the update path already did); a NULL reaching the === nothing guards crashed the report with a non-boolean Missing TypeError instead of failing the quantity. - Tolerance validation runs on every load, not only at write: a hand edit or a merge taking the wrong side could otherwise ship a gating entry with no finite rtol (unsatisfiable) or an rtol below its recorded platform_spread (the exact quiet loosening this mechanism exists to prevent). - A value_type change resets carry-forward: a topological rtol=0 landing on a float gates at bit-exactness forever; a float tolerance landing on a count lets it drift. - A crashed run under --check reports as a crash, not as N tolerance failures. - Non-finite tolerances short-circuit within (Inf + NaN arithmetic made zero-valued diagnostic entries compare false); checksums no longer inflate the untracked count; sing_psi/sing_q reclassified physics_converged (root-found, not pure quadrature); nstep matched exactly instead of by prefix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 522b29b commit b79c33e

2 files changed

Lines changed: 67 additions & 7 deletions

File tree

regression-harness/regress.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ function main(args=ARGS)
265265
n_changed = 0
266266
n_golden_fail = 0
267267
n_untracked = 0
268+
n_checked = 0
269+
n_no_golden = 0
268270

269271
# Run every case against each ref, grouped by ref so cases sharing a commit share
270272
# one worktree (and one Pkg.instantiate/precompile) instead of paying for it per case.
@@ -288,6 +290,7 @@ function main(args=ARGS)
288290
summary = report_golden_check(db, case_spec, resolved_refs[1].commit_hash)
289291
n_golden_fail += summary.n_fail
290292
n_untracked += summary.n_untracked
293+
has_golden(case_spec.name) ? (n_checked += 1) : (n_no_golden += 1)
291294
else
292295
summary = if length(resolved_refs) == 2
293296
report_two_ref_comparison(db, case_spec,
@@ -308,6 +311,15 @@ function main(args=ARGS)
308311
@error "$n_golden_fail quantity/quantities are outside their golden tolerance"
309312
exit(1)
310313
end
314+
if opts.check && n_checked == 0
315+
# A green gate that checked nothing is worse than a red one: a deleted or
316+
# typo-named golden file must not be indistinguishable from a passing check.
317+
@error "--check ran $n_no_golden case(s) but found no golden file for any of them — nothing was actually gated"
318+
exit(1)
319+
end
320+
if opts.check && n_no_golden > 0
321+
@warn "$n_no_golden of $(n_no_golden + n_checked) requested case(s) have no golden file and were not gated"
322+
end
311323
if n_untracked > 0
312324
@info "$n_untracked tracked quantity/quantities have no golden value yet (not gating)"
313325
end

regression-harness/src/golden.jl

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ function load_golden(case_name::AbstractString)
125125
for (name, v) in get(data, "values", Dict{String,Any}())
126126
class = get(v, "class", "physics_converged")
127127
class in TOLERANCE_CLASSES || error("Golden file $path: quantity '$name' has unknown class '$class'")
128+
# Enforced on every load, not only at write time: a hand edit or a merge taking the
129+
# wrong side must not produce a gate quieter than the one save_golden refused to write.
130+
if class in GATING_CLASSES
131+
rt = Float64(get(v, "rtol", NaN))
132+
(isfinite(rt) && rt >= 0) || error("Golden file $path: gating quantity '$name' has no finite rtol — malformed or hand-edited entry")
133+
sp = Float64(get(v, "platform_spread", NaN))
134+
isfinite(sp) && rt < sp && error("Golden file $path: quantity '$name' has rtol $rt below its recorded platform_spread $sp")
135+
end
128136
values[name] = GoldenValue(
129137
name,
130138
get(v, "value_type", "real"),
@@ -153,6 +161,11 @@ deliberate act the caller performs, not something this function does behind the
153161
"""
154162
function save_golden(meta::GoldenMeta, values::Dict{String,GoldenValue})
155163
for g in Base.values(values)
164+
if g.value_real === nothing && g.value_int === nothing && g.value_text === nothing
165+
error("Quantity '$(g.name)': refusing to write a golden entry with no value (the run " *
166+
"produced NaN or nothing) — a valueless gating entry fails forever and regenerating " *
167+
"reproduces it. Exclude the quantity or fix the extraction.")
168+
end
156169
if isfinite(g.platform_spread) && isfinite(g.rtol) && g.rtol < g.platform_spread
157170
error("Quantity '$(g.name)': rtol $(g.rtol) is tighter than the measured platform " *
158171
"spread $(g.platform_spread). Widen it deliberately, with the measurement recorded, or " *
@@ -208,12 +221,13 @@ converged, which is the conservative assumption — it gates.
208221
"""
209222
function infer_class(spec::QuantitySpec)::String
210223
spec.type == "runtime" && return "diagnostic"
211-
startswith(spec.name, "nstep") && return "diagnostic"
224+
spec.name in ("nstep", "nstep_total") && return "diagnostic"
212225
spec.type == "int_scalar" && return "topological"
213226
name = spec.name
227+
# sing_psi / sing_q are deliberately absent: singular-surface locations come from a root
228+
# search, not pure spline/quadrature, so they take the measured physics_converged path.
214229
equilibrium_names = ("q0", "q95", "betat", "betan", "betap1", "betap2", "betap3", "betaj",
215-
"li1", "li2", "li3", "volume", "crnt", "bt0", "bwall", "aratio", "kappa",
216-
"sing_psi", "sing_q")
230+
"li1", "li2", "li3", "volume", "crnt", "bt0", "bwall", "aratio", "kappa")
217231
name in equilibrium_names && return "equilibrium_scalar"
218232
return "physics_converged"
219233
end
@@ -226,7 +240,10 @@ the golden value is zero) and `detail` is a human-readable reason on failure. Ar
226240
pass only when every element is within tolerance; the reported deviation is the worst element.
227241
"""
228242
function compare_to_golden(q::NamedTuple, g::GoldenValue)
229-
within = (x, gold) -> abs(x - gold) <= g.atol + g.rtol * abs(gold)
243+
# Non-finite tolerances mean "recorded, never judged" (diagnostic/unconverged); without this
244+
# guard, gold == 0 turns atol + rtol*|gold| into Inf + NaN and the comparison is false.
245+
within = (x, gold) -> !isfinite(g.atol) || !isfinite(g.rtol) ||
246+
abs(x - gold) <= g.atol + g.rtol * abs(gold)
230247

231248
if q.value_type != g.value_type
232249
return (false, NaN, "type changed: golden $(g.value_type), got $(q.value_type)")
@@ -296,23 +313,35 @@ function report_golden_check(db::SQLite.DB, case_spec::CaseSpec, commit_hash::St
296313

297314
quantities = get_quantities(db, commit_hash, case_spec.name)
298315
info = get_run_info(db, commit_hash, case_spec.name)
316+
if info !== nothing && !info.success
317+
println(" RUN FAILED — nothing to compare (this is a crash, not a tolerance failure):")
318+
println(" $(_short_err(info.error_msg))")
319+
return (n_pass=0, n_fail=1, n_untracked=0, n_informational=0)
320+
end
299321

300322
rows = Vector{Vector{String}}()
301323
n_pass = n_fail = n_untracked = n_informational = 0
302324

303325
for spec in case_spec.quantities
304326
g = get(golden.values, spec.name, nothing)
305327
if g === nothing
306-
spec.type == "runtime" && continue
328+
# Runtime and checksums are structurally un-goldenable (no tolerance semantics), so
329+
# they must not inflate the untracked count that flags genuinely unpinned physics.
330+
(spec.type == "runtime" || spec.extract == "checksum") && continue
307331
n_untracked += 1
308332
continue
309333
end
310-
q = get(quantities, spec.name, nothing)
311-
if q === nothing
334+
q_raw = get(quantities, spec.name, nothing)
335+
if q_raw === nothing
312336
push!(rows, [spec.label, g.class, "MISSING", "", "FAIL"])
313337
n_fail += 1
314338
continue
315339
end
340+
# SQLite NULLs surface as , which the === nothing guards in compare_to_golden
341+
# never match; normalize here as the update path already does.
342+
q = (label=q_raw.label, value_real=_column(q_raw.value_real, nothing),
343+
value_int=_column(q_raw.value_int, nothing), value_text=_column(q_raw.value_text, nothing),
344+
value_type=q_raw.value_type, noise_threshold=q_raw.noise_threshold)
316345
passed, deviation, detail = compare_to_golden(q, g)
317346
gating = is_gating(g)
318347
status = if !gating
@@ -409,10 +438,23 @@ function update_golden_from_run(db::SQLite.DB, case_spec::CaseSpec, commit_hash:
409438
Dates.format(Dates.now(), "yyyy-mm-dd"),
410439
strip(read(`git -C $repo_root rev-parse --short HEAD`, String)),
411440
reason, fp.julia_version, fp.os_arch, fp.manifest_sha, fp.nthreads, fp.blas_threads)
441+
if commit_hash == LOCAL_REF && !isempty(strip(read(`git -C $repo_root status --porcelain`, String)))
442+
# The numbers came from uncommitted source; a clean HEAD checkout will not reproduce
443+
# them, and the commit field is the one a reviewer uses to reproduce a disputed number.
444+
meta = GoldenMeta(meta.case, meta.golden_version, meta.generated_at,
445+
meta.commit * "-dirty", meta.reason, meta.julia_version, meta.os_arch,
446+
meta.manifest_sha, meta.nthreads, meta.blas_threads)
447+
@warn "Working tree is dirty: golden provenance recorded as $(meta.commit). Commit first if these values are meant to be reproducible."
448+
end
412449

413450
println()
414451
println("Golden update: $(case_spec.name) (v$(previous === nothing ? 0 : previous.meta.golden_version) → v$(meta.golden_version))")
415452
if existing !== nothing
453+
for name in sort(collect(keys(existing)))
454+
if !haskey(values, name)
455+
println(@sprintf(" %-34s REMOVED — extraction returned missing (renamed h5 path?) or the quantity left the case. This deletes its gate; confirm it is intentional.", name))
456+
end
457+
end
416458
for (name, g) in sort(collect(values); by=first)
417459
old = get(existing, name, nothing)
418460
old === nothing && (println(@sprintf(" %-34s NEW", name)); continue)
@@ -449,6 +491,12 @@ function build_golden_values(extracted::Vector{ExtractedQuantity}, specs::Vector
449491
spec = get(spec_by_name, eq.name, nothing)
450492
spec === nothing && continue
451493
prior = existing === nothing ? nothing : get(existing, eq.name, nothing)
494+
if prior !== nothing && prior.value_type != eq.value_type
495+
# A type change invalidates the class and every measurement made under the old type;
496+
# carrying a topological rtol=0 onto a float (or a float rtol onto a count) mis-gates.
497+
@warn "Golden '$(eq.name)': value_type changed $(prior.value_type)$(eq.value_type); resetting class and tolerances to provisional"
498+
prior = nothing
499+
end
452500
class = prior === nothing ? infer_class(spec) : prior.class
453501
# Checksums have no notion of "close", so they cannot carry a tolerance; they stay a
454502
# same-machine differential tool rather than a golden gate.

0 commit comments

Comments
 (0)