test(s31): hybrid cosine tie-break load-bearing seam (500+ base)
This commit is contained in:
parent
d2aeb741cc
commit
5209775041
1 changed files with 138 additions and 2 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
"""S3.1 semretrieval — load-bearing guards (SC5 MAF-free, SC8 no-network).
|
"""S3.1 semretrieval — load-bearing seams (SC2 cosine tie-break, SC5 MAF-free, SC8 no-network).
|
||||||
|
|
||||||
``semretrieval`` is the only numpy importer in the package and it sits on the MAF-free side of
|
``semretrieval`` is the only numpy importer in the package and it sits on the MAF-free side of
|
||||||
the D7 fault line. Three independent guards hold that line, each with a named detach point:
|
the D7 fault line. Three guards hold that line, each with a named detach point:
|
||||||
|
|
||||||
1. **Meta** — the module is registered in ``_MAF_FREE_MODULES``, so the direct-import AST guard
|
1. **Meta** — the module is registered in ``_MAF_FREE_MODULES``, so the direct-import AST guard
|
||||||
(``test_okf_is_maf_free``) actually scans it. Without this the MAF-free claim is green-but-dead.
|
(``test_okf_is_maf_free``) actually scans it. Without this the MAF-free claim is green-but-dead.
|
||||||
|
|
@ -11,6 +11,10 @@ the D7 fault line. Three independent guards hold that line, each with a named de
|
||||||
past both the AST guard and an import-only probe, and trip only this one.
|
past both the AST guard and an import-only probe, and trip only this one.
|
||||||
3. **No network** — an AST sweep for network modules, since a real embeddings client is an
|
3. **No network** — an AST sweep for network modules, since a real embeddings client is an
|
||||||
extension point and must never be smuggled into the offline default.
|
extension point and must never be smuggled into the offline default.
|
||||||
|
|
||||||
|
And one load-bearing behavioural seam (SC2): on a 500+ verdict base where two candidates are
|
||||||
|
structurally INDISTINGUISHABLE, the cosine term is the only thing that can separate them.
|
||||||
|
**Detach point: zero/remove the cosine term → CORRECT no longer top-1.**
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -20,6 +24,14 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from portfolio_optimiser.semretrieval import (
|
||||||
|
FakeEmbedder,
|
||||||
|
HybridRanker,
|
||||||
|
StructuralRetriever,
|
||||||
|
cosine,
|
||||||
|
)
|
||||||
|
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict, VerdictStore, similarity
|
||||||
|
|
||||||
_SEMRETRIEVAL = (
|
_SEMRETRIEVAL = (
|
||||||
Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "semretrieval.py"
|
Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "semretrieval.py"
|
||||||
)
|
)
|
||||||
|
|
@ -91,3 +103,127 @@ def test_semretrieval_imports_no_network_modules() -> None:
|
||||||
|
|
||||||
leaked = imported_roots & _NETWORK_ROOTS
|
leaked = imported_roots & _NETWORK_ROOTS
|
||||||
assert not leaked, f"semretrieval imports network module(s): {sorted(leaked)}"
|
assert not leaked, f"semretrieval imports network module(s): {sorted(leaked)}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- SC2: the cosine term is load-bearing on a structurally tied 500+ base ---------------------
|
||||||
|
|
||||||
|
_TIED_CODES = frozenset({"05.2", "03.1"})
|
||||||
|
|
||||||
|
# CORRECT's id sorts AFTER the distractor's, so the structural ranking's `(-similarity, id)` key
|
||||||
|
# puts the DISTRACTOR first. Only the cosine term can overturn that. The id doubles as a unique
|
||||||
|
# grep marker: it appears nowhere else in the repo, so a stray match cannot fake a pass.
|
||||||
|
_CORRECT_ID = "zz-s31-cosine-tiebreak-4b7e"
|
||||||
|
_DISTRACTOR_ID = "aa-structural-tie-winner"
|
||||||
|
|
||||||
|
_QUERY = ProposalFeatures(
|
||||||
|
affected_codes=_TIED_CODES,
|
||||||
|
measure_type="scope_reduction",
|
||||||
|
claimed_saving_nok=220_000.0,
|
||||||
|
description="reduce asphalt base course thickness on the school approach",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _tied_verdict(verdict_id: str, description: str) -> Verdict:
|
||||||
|
"""Structurally IDENTICAL to the query — same codes, measure and magnitude bucket. Only the
|
||||||
|
(structurally ignored) description differs, which is exactly what makes these two candidates
|
||||||
|
inseparable without a semantic signal."""
|
||||||
|
return Verdict(
|
||||||
|
id=verdict_id,
|
||||||
|
proposal_features=ProposalFeatures(
|
||||||
|
affected_codes=_TIED_CODES,
|
||||||
|
measure_type="scope_reduction",
|
||||||
|
claimed_saving_nok=200_000.0, # same bucket [100k, 500k) as the query
|
||||||
|
description=description,
|
||||||
|
),
|
||||||
|
decision="approved",
|
||||||
|
rationale=f"prior ruling reached via {verdict_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _synthetic_base(n: int = 500) -> list[Verdict]:
|
||||||
|
"""Two structurally tied candidates plus ``n - 2`` strictly-lower-scoring fillers, so the
|
||||||
|
ranking has to hold up at a realistic base size rather than on a three-item toy store."""
|
||||||
|
correct = _tied_verdict(
|
||||||
|
_CORRECT_ID,
|
||||||
|
# Tuned, and admittedly so: a sha256 projection carries no inherent semantics, so this
|
||||||
|
# wording was selected because it lands nearer the query than the distractor does. The
|
||||||
|
# proof of the seam is the detach control below, not the plausibility of this string.
|
||||||
|
"shallower asphalt base layer along the school approach",
|
||||||
|
)
|
||||||
|
distractor = _tied_verdict(
|
||||||
|
_DISTRACTOR_ID,
|
||||||
|
"unrelated administrative rebate on office cleaning contract",
|
||||||
|
)
|
||||||
|
fillers = [
|
||||||
|
Verdict(
|
||||||
|
id=f"filler-{i:04d}",
|
||||||
|
proposal_features=ProposalFeatures(
|
||||||
|
affected_codes=frozenset({f"9{i % 90:02d}.1"}), # no overlap -> jaccard 0
|
||||||
|
measure_type="rate_renegotiation", # no measure match
|
||||||
|
claimed_saving_nok=5_000_000.0, # different magnitude bucket
|
||||||
|
description=f"unrelated filler measure {i}",
|
||||||
|
),
|
||||||
|
decision="rejected",
|
||||||
|
rationale="filler",
|
||||||
|
)
|
||||||
|
for i in range(n - 2)
|
||||||
|
]
|
||||||
|
# Interleaved so a rank that accidentally preserved input order would not pass by luck.
|
||||||
|
return [*fillers[: (n - 2) // 2], distractor, correct, *fillers[(n - 2) // 2 :]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sc2_fixture_preconditions_hold() -> None:
|
||||||
|
"""The three properties the SC2 proof rests on, asserted rather than assumed: the pair is
|
||||||
|
structurally tied, CORRECT loses the id tie-break, and cosine favours CORRECT."""
|
||||||
|
base = _synthetic_base()
|
||||||
|
correct = next(v for v in base if v.id == _CORRECT_ID)
|
||||||
|
distractor = next(v for v in base if v.id == _DISTRACTOR_ID)
|
||||||
|
|
||||||
|
assert similarity(_QUERY, correct.proposal_features) == similarity(
|
||||||
|
_QUERY, distractor.proposal_features
|
||||||
|
)
|
||||||
|
assert _DISTRACTOR_ID < _CORRECT_ID # structural id tie-break favours the distractor
|
||||||
|
|
||||||
|
embedder = FakeEmbedder()
|
||||||
|
query_vector = embedder(_QUERY)
|
||||||
|
assert cosine(query_vector, embedder(correct.proposal_features)) > cosine(
|
||||||
|
query_vector, embedder(distractor.proposal_features)
|
||||||
|
)
|
||||||
|
assert len(base) >= 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_sc2_control_structural_ranking_puts_the_distractor_first() -> None:
|
||||||
|
"""CONTROL — without a semantic term the CORRECT verdict is unreachable. If this ever goes
|
||||||
|
green with CORRECT on top, the positive below proves nothing."""
|
||||||
|
base = _synthetic_base()
|
||||||
|
top = StructuralRetriever(similarity).rank(_QUERY, base, 3)
|
||||||
|
assert top[0].id == _DISTRACTOR_ID
|
||||||
|
assert top[0].id != _CORRECT_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_sc2_control_hybrid_at_weight_zero_is_the_detach_point() -> None:
|
||||||
|
"""DETACH — zeroing the cosine weight collapses the hybrid onto the structural ranking and
|
||||||
|
the CORRECT verdict falls back out of the top slot. This is the seam being load-bearing:
|
||||||
|
remove the cosine contribution and the positive test below turns RED."""
|
||||||
|
base = _synthetic_base()
|
||||||
|
detached = HybridRanker(FakeEmbedder(), similarity, weight=0.0).rank(_QUERY, base, 3)
|
||||||
|
assert detached[0].id == _DISTRACTOR_ID
|
||||||
|
assert detached[0].id != _CORRECT_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_sc2_positive_cosine_breaks_the_structural_tie() -> None:
|
||||||
|
"""POSITIVE — with the cosine term active, the semantically closer verdict wins the tie it
|
||||||
|
loses structurally, on a 500+ base."""
|
||||||
|
base = _synthetic_base()
|
||||||
|
top = HybridRanker(FakeEmbedder(), similarity).rank(_QUERY, base, 3)
|
||||||
|
assert top[0].id == _CORRECT_ID
|
||||||
|
|
||||||
|
|
||||||
|
def test_sc2_positive_holds_through_the_store_seam() -> None:
|
||||||
|
"""The same result through the real entry point: a store with the hybrid installed must
|
||||||
|
surface CORRECT, since that is the path ``run.py --semantic-retrieval`` drives."""
|
||||||
|
store = VerdictStore(verdicts=_synthetic_base())
|
||||||
|
store.retriever = HybridRanker(FakeEmbedder(), similarity)
|
||||||
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue