Skip to content

Commit 1a507ff

Browse files
committed
fix(operator-trend): floor rererestore persistence magnitude (T3-2 phase 5)
The rererestore persistence builder decremented per-event `magnitude` without the `max(0.0, ..)` floor its rebuild sibling applies. `magnitude` is per-event evidence strength (non-negative by concept; `sign` carries the direction), so an over-penalized confirmation event went negative and -- after `sign` -- counted against its own side, producing a spurious clearance-leaning persistence_score. Floor the 4 decrements to match rebuild. On the same structural input the two tiers now agree (persistence_score 0.31 == 0.31, was 0.31 vs 0.24), and the rerererestore wrapper that delegates to it no longer yields a spurious negative (-0.08 -> 0.09). The phase-4 composer golden caught this exactly: the regenerated golden diff is 3 score lines, and the anchor test flips from pinning the divergence to pinning the convergence (test_magnitude_floor_unifies_rebuild_and_rererestore_tiers). No existing test pinned the buggy value -- the negative-magnitude path was uncharacterized, which is why the bug survived.
1 parent 2371338 commit 1a507ff

4 files changed

Lines changed: 42 additions & 39 deletions

File tree

src/operator_trend_closure_forecast_reset_controls.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5773,19 +5773,19 @@ def closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persisten
57735773
if freshness_status == "fresh":
57745774
magnitude += 0.10
57755775
elif freshness_status == "mixed-age":
5776-
magnitude -= 0.10
5776+
magnitude = max(0.0, magnitude - 0.10)
57775777
if momentum_status in {"reversing", "unstable"}:
5778-
magnitude -= 0.15
5778+
magnitude = max(0.0, magnitude - 0.15)
57795779
if stability_status == "oscillating":
5780-
magnitude -= 0.15
5780+
magnitude = max(0.0, magnitude - 0.15)
57815781
if (
57825782
event.get(
57835783
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerestore_reset_status",
57845784
"none",
57855785
)
57865786
!= "none"
57875787
):
5788-
magnitude -= 0.15
5788+
magnitude = max(0.0, magnitude - 0.15)
57895789
weighted_total += sign * magnitude * weight
57905790
weight_sum += weight
57915791

tests/golden/composer_contract.golden.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@
637637
"reversing": {
638638
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerererestore_age_runs": 2,
639639
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerererestore_persistence_reason": "The re-re-re-restored rebuilt re-entry posture is already weakening, so it is being softened again.",
640-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerererestore_persistence_score": -0.08,
640+
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerererestore_persistence_score": 0.09,
641641
"closure_forecast_reset_reentry_rebuild_reentry_restore_rerererestore_persistence_status": "reversing",
642642
"recent_reset_reentry_rebuild_reentry_restore_rerererestore_persistence_path": "rebuilt-confirmation-reentry -> strong-conf"
643643
},
@@ -894,7 +894,7 @@
894894
"clamp-divergence": {
895895
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_age_runs": 2,
896896
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_reason": "Confirmation-side re-re-restored posture has stayed aligned long enough to keep the stronger rerestored forecast in place.",
897-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_score": 0.24,
897+
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_score": 0.31,
898898
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_status": "holding-confirmation-rebuild-reentry-rererestore",
899899
"recent_reset_reentry_rebuild_reentry_restore_rererestore_persistence_path": "strong-conf -> decrement-conf"
900900
},
@@ -929,7 +929,7 @@
929929
"sparse": {
930930
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_age_runs": 0,
931931
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_reason": "",
932-
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_score": 0.24,
932+
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_score": 0.31,
933933
"closure_forecast_reset_reentry_rebuild_reentry_restore_rererestore_persistence_status": "none",
934934
"recent_reset_reentry_rebuild_reentry_restore_rererestore_persistence_path": "sparse-none -> strong-conf -> decrement-conf"
935935
},

tests/golden/enumerate_composer_contract.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,34 @@
88
outputs feed the operator decision-quality string contract. This enumerator
99
captures those composers across a status-spanning corpus into a frozen golden.
1010
11-
Why it must exist BEFORE any ``recovery_state(target, *, depth)`` collapse: the
12-
per-tier composer families (rebuild / rererestore / rerererestore) are near-clones
13-
with a real, OBSERVABLE logic delta. Concretely, the rebuild persistence builder
14-
floors per-event magnitude at zero::
11+
Why it exists BEFORE any ``recovery_state(target, *, depth)`` collapse: the per-tier
12+
composer families (rebuild / rererestore / rerererestore) are near-clones, and the
13+
net both pins all 44 composers for the collapse and guards a real correctness fix.
14+
``magnitude`` is per-event evidence strength (non-negative by concept; ``sign``
15+
carries the direction). The rebuild persistence builder floors it at zero on every
16+
decrement::
1517
1618
magnitude = max(0.0, magnitude - 0.10) # rebuild
1719
18-
while the rererestore builder does not::
19-
20-
magnitude -= 0.10 # rererestore
21-
22-
That unclamped negative survives the symmetric final ``clamp_round(.., -0.95,
23-
0.95)`` and reaches ``persistence_score`` -- so the SAME structural input yields a
24-
different score per tier (the pinned "clamp-divergence" scenario records 0.31 for
25-
rebuild vs 0.24 for rererestore). The str->str golden cannot
26-
see this. Any collapse of these families must reproduce THIS golden byte-for-byte;
27-
an intentional unification shows up here as a reviewed diff, never a silent one.
20+
The rererestore builder originally omitted that floor (``magnitude -= 0.10``), so an
21+
over-penalized confirmation event could go NEGATIVE and -- after ``sign`` -- count
22+
against its own side, producing a spurious clearance-leaning ``persistence_score``.
23+
That divergence was invisible to the str->str golden. T3-2 phase 5 fixed it: the
24+
rererestore builder now floors exactly like rebuild, so the SAME structural input
25+
yields the SAME score across the tiers. The "clamp-divergence" scenario (the input
26+
that drives a decrement event's magnitude below zero) now records 0.31 for BOTH
27+
rebuild and rererestore, and ``test_magnitude_floor_unifies_rebuild_and_rererestore_tiers``
28+
pins that convergence -- a regression that re-removed the floor would re-diverge and
29+
fail. Any future change to these families must reproduce THIS golden byte-for-byte;
30+
an intentional behavior change shows up here as a reviewed diff, never a silent one.
2831
2932
Fixed-harness validity: the injected callables below are deterministic stand-ins,
3033
not the production wiring (which threads ~15 helpers per composer). The golden's
3134
validity does NOT depend on production-identical callables -- it pins composer
3235
behavior under a FIXED harness, and the collapse must reproduce it under the SAME
3336
harness. The harness only has to be (a) held constant between generation and check
3437
(it is -- one enumerator) and (b) rich enough to exercise the real branches (the
35-
companion test guards a non-degenerate branch count and the clamp divergence).
38+
companion test guards a non-degenerate branch count and the magnitude-floor fix).
3639
3740
Regenerate only with an intentional, reviewed behavior change::
3841
@@ -373,8 +376,9 @@ def _for_target_scenarios() -> list[
373376
}
374377
reversing_target = {**conf_target, "closure_forecast_momentum_status": "reversing"}
375378
return [
376-
# The divergence fixture: confirmation side, one strong event + one decrement
377-
# event -> rebuild floors the decrement to 0, rererestore keeps it negative.
379+
# The floor-path fixture: confirmation side, one strong event + one decrement
380+
# event whose magnitude crosses below zero. Both rebuild and the (now-fixed)
381+
# rererestore floor it at 0, so they agree; the scenario name is historical.
378382
("clamp-divergence", conf_target, [strong, decrement], {}),
379383
("sustained-confirmation", conf_target, [strong, strong, strong], {}),
380384
(

tests/test_composer_golden_contract.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,23 @@ def test_composer_reproduces_golden(name: str) -> None:
8383
assert LIVE[name] == GOLDEN[name]
8484

8585

86-
def test_clamp_divergence_between_tiers_is_pinned() -> None:
87-
# The whole reason this net exists: the rebuild and rererestore persistence
88-
# builders are near-clones, but the rebuild per-event magnitude floor makes the
89-
# SAME structural input ("clamp-divergence" scenario) yield a different score.
90-
# A naive collapse that translated one tier into the other would erase this and
91-
# this assertion would fail -- which is the point.
86+
def test_magnitude_floor_unifies_rebuild_and_rererestore_tiers() -> None:
87+
# T3-2 phase 5 fix: the rererestore persistence builder now floors per-event
88+
# magnitude at 0 (max(0.0, ..)) exactly like rebuild -- previously it omitted the
89+
# floor (magnitude -= X), letting an over-penalized confirmation event go negative
90+
# and count against its own side. With the floor, the SAME structural input (the
91+
# "clamp-divergence" floor-path scenario) yields the SAME score across the tiers.
92+
# This pins the fix: a regression that re-removed the floor would re-diverge here.
9293
rebuild = GOLDEN[_REBUILD_PERSISTENCE]["clamp-divergence"]
9394
rererestore = GOLDEN[_RERERESTORE_PERSISTENCE]["clamp-divergence"]
9495
rebuild_score = rebuild[_REBUILD_SCORE_KEY]
9596
rererestore_score = rererestore[_RERERESTORE_SCORE_KEY]
96-
assert rebuild_score != rererestore_score, (rebuild_score, rererestore_score)
97-
# Rebuild floors the negative decrement term at 0, so it stays the higher score.
98-
assert rebuild_score > rererestore_score, (rebuild_score, rererestore_score)
99-
100-
# The rerererestore tier is a thin wrapper that translates keys and delegates to
101-
# the rererestore builder, so it also does NOT floor the magnitude. Pin it as a
102-
# named three-way invariant: it diverges from the flooring rebuild tier (F7). A
103-
# collapse that accidentally gave it rebuild's floor would break this.
97+
assert rebuild_score == rererestore_score, (rebuild_score, rererestore_score)
98+
99+
# The rerererestore tier wraps + delegates to the now-fixed rererestore builder,
100+
# so this confirmation-side scenario no longer yields a spurious NEGATIVE score
101+
# (the bug symptom). It need not equal rebuild -- the wrapper feeds translated
102+
# inputs -- but it must reflect the floor (be non-negative here).
104103
rerererestore = GOLDEN[_RERERESTORE_WRAP_PERSISTENCE]["clamp-divergence"]
105104
rerererestore_score = rerererestore[_RERERESTORE_WRAP_SCORE_KEY]
106-
assert rerererestore_score != rebuild_score, (rerererestore_score, rebuild_score)
105+
assert rerererestore_score >= 0.0, rerererestore_score

0 commit comments

Comments
 (0)