[M] 2026-08-13 at 0f5ca91c, from the #350 ruling investigation.
#342 and #340 N6b closed the headline path: Solution.converged() (orpheus/sn/solution.py:500-526) answers fully_converged, so a caller who asks the obvious question gets the tree-wide answer. Its own docstring names the pre-N6b behaviour as the campaign's founding defect (:508-517).
Two public per-level surfaces still answer the narrow question, and neither requires the caller to know there is a wider one.
1. IterationHistory.converged — orpheus/sn/solution.py:201-217
return self.record.converged
Public, documented, reachable as sol.history.converged — one extra attribute hop from the correct answer. Its own docstring already warns "A gate asserting physics wants fully_converged" (:214-215), and IterationHistory's class docstring says not to grow this surface (:186-192). The sibling fully_converged sits at :219-226.
[M] read by tests/sn/solve/test_d3_admission.py:186, 229 and tests/sn/solve/test_convergence_contract.py:380-407, 850.
2. PowerIterationOutcome.converged — orpheus/numerics/eigenvalue.py:348-371
return self.record.converged
Public on the numerics primitive, per-level scoped. [M] zero production readers — SN/CP/MoC/diffusion all read outcome.record instead (orpheus/sn/solver.py:2569; orpheus/cp/solver.py:999; orpheus/moc/solver.py:195; orpheus/diffusion/solver.py:473). Latent only — a hole nothing currently falls into.
Why this is worth a decision rather than a shrug
The per-level fact is load-bearing and must not be deleted: distinguishing "the outer met its own criteria but an inner was starved" from "the outer itself did not converge" is exactly what #340 N2b-ii established, and collapsing them is the plan's already-refuted candidate (.claude/plans/nested_iteration_diagnostics.md:1571 — "Loses the per-level fact… needed to tell 'inner starved' from 'genuinely non-critical'").
So this is not "make them return fully_converged". It is: should a per-level verdict be public and identically named to the tree verdict, one attribute apart, when the whole of #342 was about a caller reading the narrow answer and believing it was the wide one?
What "done" looks like
A caller cannot reach a per-level convergence verdict by accident, while a caller who genuinely wants the per-level fact still has it.
⚠ Not decided. Options, unmeasured: rename the per-level surfaces so the two cannot be confused (converged_at_this_level, per feedback_high_signal_names); keep the names and rely on the existing in-place docstring warnings; or narrow IterationHistory per its own class docstring's instruction not to grow.
Blast radius, measured
[M] static assertion sites that would move under a rename: 43 on .converged in the IterationRecord/IterationHistory family — tests/numerics/test_power_iteration_record.py (9), tests/numerics/test_iteration_record.py (9), tests/sn/solve/test_convergence_contract.py (8), tests/sn/primitives/test_solution.py (8), plus 9 singletons.
⚠ A further 87 .converged assertion lines in tests/derivations/** are a DIFFERENT class — scipy's res.converged and the derivations-local PowerIterationResult (orpheus/derivations/continuous/trajectory_resolvent/power_iteration.py:78), which has a stored converged field. They cannot move and must not be counted. (The plan records an earlier version of this same miscount at .claude/plans/nested_iteration_diagnostics.md:108-112.)
⚠ Also unresolved: tests/cross_method/adapters.py:196, 270, 340, 404 each write "converged": bool(res.converged) with a per-method res type — UNKNOWN which class each resolves to.
⚠ Two gates would need re-scoping, not re-baselining, if the two verdicts ever stop being distinguishable: tests/sn/primitives/test_solution.py:214 (test_converged_answers_for_the_LEVEL_and_fully_converged_for_the_TREE, whose :249 asserts the exact statement at issue) and tests/numerics/test_iteration_record.py:481 (test_fully_converged_IMPLIES_converged).
Spun out of #350. Siblings: #364 (twin default literals), #365 (the verdict's precision).
[M]2026-08-13 at0f5ca91c, from the #350 ruling investigation.#342 and #340 N6b closed the headline path:
Solution.converged()(orpheus/sn/solution.py:500-526) answersfully_converged, so a caller who asks the obvious question gets the tree-wide answer. Its own docstring names the pre-N6b behaviour as the campaign's founding defect (:508-517).Two public per-level surfaces still answer the narrow question, and neither requires the caller to know there is a wider one.
1.
IterationHistory.converged—orpheus/sn/solution.py:201-217Public, documented, reachable as
sol.history.converged— one extra attribute hop from the correct answer. Its own docstring already warns "A gate asserting physics wantsfully_converged" (:214-215), andIterationHistory's class docstring says not to grow this surface (:186-192). The siblingfully_convergedsits at:219-226.[M]read bytests/sn/solve/test_d3_admission.py:186, 229andtests/sn/solve/test_convergence_contract.py:380-407, 850.2.
PowerIterationOutcome.converged—orpheus/numerics/eigenvalue.py:348-371Public on the numerics primitive, per-level scoped.
[M]zero production readers — SN/CP/MoC/diffusion all readoutcome.recordinstead (orpheus/sn/solver.py:2569;orpheus/cp/solver.py:999;orpheus/moc/solver.py:195;orpheus/diffusion/solver.py:473). Latent only — a hole nothing currently falls into.Why this is worth a decision rather than a shrug
The per-level fact is load-bearing and must not be deleted: distinguishing "the outer met its own criteria but an inner was starved" from "the outer itself did not converge" is exactly what #340 N2b-ii established, and collapsing them is the plan's already-refuted candidate (
.claude/plans/nested_iteration_diagnostics.md:1571— "Loses the per-level fact… needed to tell 'inner starved' from 'genuinely non-critical'").So this is not "make them return
fully_converged". It is: should a per-level verdict be public and identically named to the tree verdict, one attribute apart, when the whole of #342 was about a caller reading the narrow answer and believing it was the wide one?What "done" looks like
A caller cannot reach a per-level convergence verdict by accident, while a caller who genuinely wants the per-level fact still has it.
⚠ Not decided. Options, unmeasured: rename the per-level surfaces so the two cannot be confused (
converged_at_this_level, perfeedback_high_signal_names); keep the names and rely on the existing in-place docstring warnings; or narrowIterationHistoryper its own class docstring's instruction not to grow.Blast radius, measured
[M]static assertion sites that would move under a rename: 43 on.convergedin theIterationRecord/IterationHistoryfamily —tests/numerics/test_power_iteration_record.py(9),tests/numerics/test_iteration_record.py(9),tests/sn/solve/test_convergence_contract.py(8),tests/sn/primitives/test_solution.py(8), plus 9 singletons.⚠ A further 87
.convergedassertion lines intests/derivations/**are a DIFFERENT class — scipy'sres.convergedand the derivations-localPowerIterationResult(orpheus/derivations/continuous/trajectory_resolvent/power_iteration.py:78), which has a storedconvergedfield. They cannot move and must not be counted. (The plan records an earlier version of this same miscount at.claude/plans/nested_iteration_diagnostics.md:108-112.)⚠ Also unresolved:
tests/cross_method/adapters.py:196, 270, 340, 404each write"converged": bool(res.converged)with a per-methodrestype — UNKNOWN which class each resolves to.⚠ Two gates would need re-scoping, not re-baselining, if the two verdicts ever stop being distinguishable:
tests/sn/primitives/test_solution.py:214(test_converged_answers_for_the_LEVEL_and_fully_converged_for_the_TREE, whose:249asserts the exact statement at issue) andtests/numerics/test_iteration_record.py:481(test_fully_converged_IMPLIES_converged).Spun out of #350. Siblings: #364 (twin default literals), #365 (the verdict's precision).