test(s31): pin the structural-order-preservation contract — RED at the shipped weight
This commit is contained in:
parent
5a6ee03955
commit
b05747a3cd
1 changed files with 155 additions and 0 deletions
|
|
@ -26,6 +26,7 @@ system's own echo of the query can no longer outrank genuine expert prose.
|
|||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import itertools
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
|
@ -34,12 +35,14 @@ from pathlib import Path
|
|||
import numpy as np
|
||||
|
||||
from portfolio_optimiser.semretrieval import (
|
||||
SEMANTIC_WEIGHT_DEFAULT,
|
||||
FakeEmbedder,
|
||||
HybridRanker,
|
||||
StructuralRetriever,
|
||||
cosine,
|
||||
)
|
||||
from portfolio_optimiser.verdicts import (
|
||||
_W_MAGNITUDE,
|
||||
ProposalFeatures,
|
||||
Verdict,
|
||||
VerdictStore,
|
||||
|
|
@ -439,3 +442,155 @@ def test_sc2_positive_holds_through_the_store_seam() -> None:
|
|||
assert store.retrieve(_QUERY, k=3)[0].id == _CORRECT_ID
|
||||
# ...and the untouched default store still cannot see it.
|
||||
assert VerdictStore(verdicts=_synthetic_base()).retrieve(_QUERY, k=3)[0].id == _DISTRACTOR_ID
|
||||
|
||||
|
||||
# --- The other half of the contract: the hybrid must PRESERVE a real structural gap ------------
|
||||
#
|
||||
# SC2 above proves the cosine term can break a structural TIE. Nothing above proves it stops
|
||||
# there. The two claims are opposites at the margin, and only one of them was ever pinned: an
|
||||
# arbitrary sha256 term with enough weight does not merely settle ties, it overturns genuine
|
||||
# orderings. The blend weight is the only thing that bounds how large a gap it can overturn, and
|
||||
# until this test that bound rested on a comment rather than on a guard.
|
||||
#
|
||||
# The fixture family is fully enumerated and RNG-free — the repo's byte-determinism posture rules
|
||||
# out a flaky property test, so the sweep is exhaustive over a small vocabulary instead.
|
||||
|
||||
_ORDER_CODES = ("A10", "A20", "B10", "B20")
|
||||
_ORDER_MEASURES = ("energy", "material")
|
||||
# One amount per magnitude bucket boundary that matters: [0, 1e5), [1e5, 5e5), [5e5, 1e6).
|
||||
_ORDER_AMOUNTS = (5e4, 2e5, 7e5)
|
||||
|
||||
# The structural score is a sum of three float terms, so a gap that is mathematically EXACTLY one
|
||||
# ``_W_MAGNITUDE`` step can subtract to 0.1499999999999999. Comparing strictly would silently drop
|
||||
# those pairs — and they are real contract violations: admitting them raises the qualifying count
|
||||
# from 71 820 to 73 980 and the adverse count at ``w=0.5`` from 7 to 9. A guard errs toward
|
||||
# sensitivity, so the threshold carries a float tolerance rather than pretending the arithmetic is
|
||||
# exact.
|
||||
_GAP_TOLERANCE = 1e-9
|
||||
|
||||
|
||||
def _order_features(codes: frozenset[str], measure: str, amount: float) -> ProposalFeatures:
|
||||
"""``description == measure_type`` — the shape both live minting paths emit."""
|
||||
return ProposalFeatures(
|
||||
affected_codes=codes,
|
||||
measure_type=measure,
|
||||
claimed_saving_nok=amount,
|
||||
description=measure,
|
||||
)
|
||||
|
||||
|
||||
def _order_family() -> list[ProposalFeatures]:
|
||||
"""4 codes (subsets of size 1-2) x 2 measures x 3 magnitude buckets = 60 feature sets."""
|
||||
code_sets = [
|
||||
frozenset(combo) for size in (1, 2) for combo in itertools.combinations(_ORDER_CODES, size)
|
||||
]
|
||||
return [
|
||||
_order_features(codes, measure, amount)
|
||||
for codes in code_sets
|
||||
for measure in _ORDER_MEASURES
|
||||
for amount in _ORDER_AMOUNTS
|
||||
]
|
||||
|
||||
|
||||
def test_hybrid_preserves_structural_order_across_one_magnitude_step() -> None:
|
||||
"""CONTRACT — over a structural gap of one ``_W_MAGNITUDE`` step the hybrid must not overturn
|
||||
the ordering. Asserted at the SHIPPED default weight, so the constant itself is what is pinned.
|
||||
|
||||
Two deliberately split assertions:
|
||||
|
||||
1. **Named anchor, through the real API.** query ``{A10}/energy/200k``; better
|
||||
``{A10,B20}/energy/50k`` (structural 0.5500); worse ``{A20}/energy/200k`` (structural
|
||||
0.4000). The gap is exactly one ``_W_MAGNITUDE`` step, and ``worse`` shares NO cost code
|
||||
with the query while ``better`` does. Human-readable, instant, and it exercises
|
||||
``HybridRanker.rank`` rather than a re-implementation of the score.
|
||||
2. **Exhaustive breadth sweep** over the n=60 family, scoring
|
||||
``w * cosine + (1 - w) * structural`` directly against one cached embedding per feature
|
||||
set. ``rank()`` re-embeds every candidate on every call, which measured 6,1 s over this
|
||||
family versus 1,1 s cached — too expensive for a 37 s suite, and the anchor above already
|
||||
covers the real ranking path.
|
||||
|
||||
MEASURED BASIS (this family, this embedder, ``EMBED_DIM=64``): 73 980 qualifying triples;
|
||||
**9 adverse at ``w=0.5``**, and **0 at every weight from 0.45 down to 0.05**. So this test is
|
||||
genuinely RED at 0.5 and green at the shipped 0.25 — it is not vacuous, and it is not
|
||||
fixture-tuned to the exact value either, since the whole corridor below 0.45 passes.
|
||||
|
||||
THE TRAP, quantified rather than warned about: a 3x2x2 family (n=24) yields **0** adverse
|
||||
triples and passes SILENTLY at ``w=0.5``. The family size is therefore load-bearing and must
|
||||
not be shrunk — a test that cannot fail proves nothing, which is the exact defect class this
|
||||
test was written to close.
|
||||
|
||||
Detach point: restore ``SEMANTIC_WEIGHT_DEFAULT = 0.5`` -> RED."""
|
||||
embedder = FakeEmbedder()
|
||||
|
||||
# 1. Named anchor, through the shipped ranking path.
|
||||
query = _order_features(frozenset({"A10"}), "energy", 200_000.0)
|
||||
better = capture_verdict(
|
||||
_order_features(frozenset({"A10", "B20"}), "energy", 50_000.0),
|
||||
"approved",
|
||||
"shares a cost code with the query",
|
||||
)
|
||||
worse = capture_verdict(
|
||||
_order_features(frozenset({"A20"}), "energy", 200_000.0),
|
||||
"rejected",
|
||||
"shares no cost code with the query",
|
||||
)
|
||||
# The anchor's structural values are asserted, not assumed — if ``similarity``'s weights ever
|
||||
# move, this test must fail loudly rather than quietly stop testing a one-step gap.
|
||||
structural_better = similarity(query, better.proposal_features)
|
||||
structural_worse = similarity(query, worse.proposal_features)
|
||||
assert round(structural_better, 4) == 0.5500
|
||||
assert round(structural_worse, 4) == 0.4000
|
||||
assert round(structural_better - structural_worse, 9) == round(_W_MAGNITUDE, 9)
|
||||
|
||||
top = HybridRanker(embedder, similarity).rank(query, [better, worse], 2)
|
||||
assert top[0].id == better.id, (
|
||||
"the hybrid ranked a candidate sharing NO cost code with the query above one that does, "
|
||||
f"across a full {_W_MAGNITUDE} structural step, on nothing but sha256 cosine noise "
|
||||
f"(weight={SEMANTIC_WEIGHT_DEFAULT})"
|
||||
)
|
||||
|
||||
# 2. Exhaustive breadth sweep, one cached embedding per feature set.
|
||||
family = _order_family()
|
||||
assert len(family) == 60, "the family size is load-bearing — n=24 passes this test vacuously"
|
||||
vectors = [embedder(features) for features in family]
|
||||
|
||||
qualifying = 0
|
||||
adverse: list[str] = []
|
||||
for query_index, query_features in enumerate(family):
|
||||
structural = [similarity(query_features, candidate) for candidate in family]
|
||||
for i, j in itertools.combinations(range(len(family)), 2):
|
||||
if query_index in (i, j):
|
||||
continue
|
||||
high, low = (i, j) if structural[i] >= structural[j] else (j, i)
|
||||
if structural[high] - structural[low] < _W_MAGNITUDE - _GAP_TOLERANCE:
|
||||
continue
|
||||
qualifying += 1
|
||||
score_high = (
|
||||
SEMANTIC_WEIGHT_DEFAULT * cosine(vectors[query_index], vectors[high])
|
||||
+ (1.0 - SEMANTIC_WEIGHT_DEFAULT) * structural[high]
|
||||
)
|
||||
score_low = (
|
||||
SEMANTIC_WEIGHT_DEFAULT * cosine(vectors[query_index], vectors[low])
|
||||
+ (1.0 - SEMANTIC_WEIGHT_DEFAULT) * structural[low]
|
||||
)
|
||||
# Compared at the ranking key's own tolerance — ``HybridRanker`` orders on
|
||||
# ``(-round(score, 9), id)``, so anything finer than that is not an inversion the
|
||||
# ranker can express.
|
||||
if round(score_low, 9) > round(score_high, 9):
|
||||
adverse.append(
|
||||
f"query={sorted(query_features.affected_codes)}/{query_features.measure_type}"
|
||||
f"/{query_features.claimed_saving_nok:.0f} "
|
||||
f"better={sorted(family[high].affected_codes)} ({structural[high]:.4f}) "
|
||||
f"lost to worse={sorted(family[low].affected_codes)} ({structural[low]:.4f})"
|
||||
)
|
||||
|
||||
# Anti-vacuity floor: if a future edit narrows the vocabulary, this fails before the contract
|
||||
# assertion below can pass for the wrong reason.
|
||||
assert qualifying > 70_000, (
|
||||
f"only {qualifying} qualifying triples — the family no longer expresses the contract "
|
||||
"(measured basis: 73 980)"
|
||||
)
|
||||
assert adverse == [], (
|
||||
f"{len(adverse)} structural orderings overturned by the semantic term at "
|
||||
f"weight={SEMANTIC_WEIGHT_DEFAULT}; first three: {adverse[:3]}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue