T08: the classifier, measured and attacked

False Adaptation Rate = 0/7 across the labelled catalogue and the three E-003
attacks. 11 of 12 mechanical mutations absorbed without a human, so the safety
result is not bought by escalating everything.

- classification.py: total function over three signals, rule order chosen so
  every rule that could excuse a regression sits after the rule that reports
  one. SAFE_TO_ACCEPT is a two-element closed set, asserted.
- CompositeDriver plus scenarios/full_journey.py: one asset crossing both
  surfaces, so UI mutations are visible as surface differences while the
  claims they do not touch stay green.
- E-003: surface substitution (new M23), concurrent mechanical+defect,
  evidence starvation, provenance laundering. All held.

F-0006 (CONCEPT_DRIFT, resolved): the T02 design listed SEMANTIC_CHANGE as an
outcome the table could produce. It cannot - M12 and M19 are behaviourally
identical, as the lab has asserted since T05. PRODUCT_DEFECT and
SEMANTIC_CHANGE collapse into one escalating outcome, BEHAVIOUR_CHANGED, and
the distinction becomes a human adjudication. INTENT_CHANGED survives but is
detected by the claim fingerprint moving, not inferred from behaviour.

Two classifier defects found and fixed rather than reported: claims downstream
of a failed realization now yield INCONCLUSIVE rather than FAIL (a false
accusation is the mirror image of a false adaptation), and the browser driver
records a page signature so surface change is detectable when the interaction
path is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1629012@bnt-lap001
Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
This commit is contained in:
tegwick 2026-08-23 00:02:58 +02:00
parent 4b78ce4597
commit 84848e9a0e
26 changed files with 824 additions and 26 deletions

View file

@ -28,7 +28,7 @@ def run_against(*mutations: str):
def test_catalogue_is_large_enough_to_support_a_rate():
"""Six mutations cannot support precision or recall. Twenty can begin to."""
assert len(CATALOGUE) >= 20
assert len(CATALOGUE) >= 20 # 23 as of T08
def test_every_mutation_is_labelled_and_reasoned():
@ -82,10 +82,19 @@ EXPECTED_VERDICT = {
"M18": Verdict.FAIL, "M19": Verdict.FAIL, "M20": Verdict.FAIL,
}
# The two SEMANTIC mutations the reference scenario cannot see, and why.
KNOWN_INERT = {
# Mutations the reference scenario cannot see, and why. Coverage is scoped to
# what a scenario asserts (F-0002) *and* to the surfaces it touches: this
# scenario is API-only, so a defect that lives in the UI is outside its reach.
# Declaring them is mandatory — `test_out_of_scope_mutations_are_declared`
# fails on any invisible mutation that is not named here.
OUT_OF_SCOPE = {
"M13": "only affects grants that omit a permission; the scenario passes READ explicitly",
"M14": "a change of intent with no change of code; nothing observable moved",
"M21": "a UI mutation; this scenario never loads the UI",
"M22": "a UI mutation; this scenario never loads the UI",
"M23": "a UI-surface defect; this scenario is API-only. Covered by the "
"cross-surface journey in tests/test_classification.py, where it is "
"the E-003 surface-substitution attack.",
}
@ -105,26 +114,43 @@ def test_no_mechanical_mutation_changes_the_verdict():
assert run_against(mutation.id).verdict is Verdict.PASS, mutation.id
def test_every_defect_is_detected():
def test_every_in_scope_defect_is_detected():
"""The floor of the whole project. A defect the lab cannot surface is a
defect no later classifier can be measured against."""
missed = [
m.id for m in CATALOGUE
if m.label == "DEFECT" and run_against(m.id).verdict is Verdict.PASS
if m.label == "DEFECT"
and m.id not in OUT_OF_SCOPE
and run_against(m.id).verdict is Verdict.PASS
]
assert missed == [], f"undetected seeded defects: {missed}"
def test_inert_semantic_mutations_are_declared():
"""A mutation the scenario cannot see must be named, not silently ignored."""
def test_out_of_scope_mutations_are_declared():
"""A mutation the scenario cannot see must be named, not silently ignored.
Applies to defects as much as to semantic changes an undeclared invisible
defect is precisely how a suite comes to look greener than it is.
"""
for mutation in CATALOGUE:
if mutation.label != "SEMANTIC":
if run_against(mutation.id).verdict is not Verdict.PASS:
continue
if run_against(mutation.id).verdict is Verdict.PASS:
assert mutation.id in KNOWN_INERT, (
f"{mutation.id} is invisible to the reference scenario and "
"undeclared — either cover it or record why not"
)
if mutation.label == "MECHANICAL":
continue # passing is the correct outcome for these
assert mutation.id in OUT_OF_SCOPE, (
f"{mutation.id} ({mutation.label}) is invisible to the reference "
"scenario and undeclared — either cover it or record why not"
)
def test_declared_out_of_scope_mutations_really_are_invisible():
"""Stale declarations rot silently. If a mutation becomes visible, the
declaration must be removed rather than left as a standing excuse."""
for mutation_id in OUT_OF_SCOPE:
assert run_against(mutation_id).verdict is Verdict.PASS, (
f"{mutation_id} is declared out of scope but the reference scenario "
"now detects it — remove the declaration"
)
def test_deferred_revoke_and_revoke_race_are_behaviourally_identical():