Skip to content

Commit df89e10

Browse files
Pigbibicodex
andcommitted
fix: keep legacy identities out of v3 allocation
Co-Authored-By: Codex <noreply@openai.com>
1 parent bc1f376 commit df89e10

2 files changed

Lines changed: 101 additions & 9 deletions

File tree

src/quant_advisor_research/allocation_context.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .identity_lifecycle import FINGERPRINT_VERSION, IdentityMetadataError
1919
from .identity_v3 import (
2020
PENDING_ARTIFACT_VALIDATION,
21+
LEGACY_V2,
2122
V3_CANONICAL,
2223
V3_VARIANT,
2324
V3IdentityBinding,
@@ -329,9 +330,16 @@ def allocate_v3_identity(
329330
raise _error("allocation_context_mismatch")
330331
verified_inventory = _revalidate_inventory(inventory)
331332
bindings = verified_inventory.index.bindings
332-
exact_matches = [binding for binding in bindings if _exact_key(binding) == _exact_key_from_metadata(metadata)]
333-
same_artifact = [
333+
v3_bindings = [
334334
binding for binding in bindings
335+
if binding.identity_class in {V3_CANONICAL, V3_VARIANT}
336+
]
337+
exact_matches = [
338+
binding for binding in v3_bindings
339+
if _exact_key(binding) == _exact_key_from_metadata(metadata)
340+
]
341+
same_artifact = [
342+
binding for binding in v3_bindings
335343
if binding.period_key == period_key
336344
and binding.artifact_integrity_version == ARTIFACT_INTEGRITY_VERSION
337345
and binding.artifact_integrity_digest == artifact_digest
@@ -343,10 +351,10 @@ def allocate_v3_identity(
343351
_validate_reuse_policy(binding, requested, display_placement)
344352
return V3AllocationPlan(binding, context.mode, True)
345353
if context.mode is AllocationMode.EXACT_ARTIFACT_REUSE:
346-
raise _error("identity_reuse_mismatch" if same_artifact else "identity_reuse_not_found")
354+
raise _error("exact_artifact_not_found")
347355
if same_artifact:
348356
raise _error("identity_reuse_mismatch")
349-
period_bindings = [binding for binding in bindings if binding.period_key == period_key]
357+
period_bindings = [binding for binding in v3_bindings if binding.period_key == period_key]
350358
canonical_exists = any(binding.canonical_identity for binding in period_bindings)
351359
if context.mode is AllocationMode.HISTORICAL_RECOVERY and not canonical_exists:
352360
raise _error("canonical_bootstrap_required")
@@ -371,9 +379,13 @@ def _exact_key_from_metadata(metadata: tuple[dict[str, object], str, str, str, s
371379

372380
def _simulate(index: V3IdentityIndex, candidate: V3IdentityBinding) -> None:
373381
try:
382+
# LEGACY_V2 entries remain dual-read migration evidence. The candidate
383+
# must satisfy the complete v3 ledger invariants without rewriting or
384+
# treating legacy canonical metadata as a v3 canonical owner.
385+
v3_bindings = tuple(binding for binding in index.bindings if binding.identity_class != LEGACY_V2)
374386
parse_v3_index({
375387
"schema_version": 3,
376-
"reports": [_binding_payload(binding) for binding in (*index.bindings, candidate)],
388+
"reports": [_binding_payload(binding) for binding in (*v3_bindings, candidate)],
377389
})
378390
except IdentityMetadataError as exc:
379391
raise _error(exc.code) from None

tests/test_allocation_context.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import hashlib
44
from collections import OrderedDict
5+
from dataclasses import replace
56
from pathlib import Path
67
from types import MappingProxyType
78

@@ -61,13 +62,15 @@ def binding_for(
6162
display_order: int = 0,
6263
include_md: bool = False,
6364
include_manifest: bool = False,
65+
identity_class: str | None = None,
6466
):
6567
period_key, semantic, artifact, contract = metadata(report)
6668
artifact_digest = artifact_digest or artifact
6769
semantic_digest = semantic_digest or semantic
6870
as_of = report["as_of"]
6971
cadence = report["cadence"]
70-
suffix = "" if canonical else f".variant-{artifact_digest}"
72+
identity_class = identity_class or ("V3_CANONICAL" if canonical else "V3_VARIANT")
73+
suffix = "" if canonical else f".variant-{semantic_digest if identity_class == 'LEGACY_V2' else artifact_digest}"
7174
entry = {
7275
"period_key": period_key,
7376
"as_of": as_of,
@@ -80,7 +83,7 @@ def binding_for(
8083
"artifact_integrity_digest": artifact_digest,
8184
"json": f"advisory_report_{as_of}{suffix}.json",
8285
"html": f"{as_of}-{cadence}-model-recommendations{suffix}.html",
83-
"identity_class": "V3_CANONICAL" if canonical else "V3_VARIANT",
86+
"identity_class": identity_class,
8487
"canonical_identity": canonical,
8588
"display_primary": display_primary,
8689
"display_order": display_order,
@@ -199,7 +202,7 @@ def test_exact_artifact_reuse_is_immutable_and_requires_full_match() -> None:
199202

200203
changed = dict(report)
201204
changed["generated_at"] = "2026-07-15T00:00:00Z"
202-
with pytest.raises(IdentityMetadataError, match="identity_reuse_not_found|identity_reuse_mismatch"):
205+
with pytest.raises(IdentityMetadataError, match="exact_artifact_not_found"):
203206
allocate_v3_identity(
204207
changed,
205208
inventory=inventory,
@@ -276,6 +279,83 @@ def test_inventory_accepts_declared_v3_schema_version() -> None:
276279
assert inventory.index.schema_version == 3
277280

278281

282+
def test_legacy_exact_match_is_not_reused_or_migrated() -> None:
283+
report = build_report()
284+
legacy = binding_for(report, identity_class="LEGACY_V2")
285+
286+
with pytest.raises(IdentityMetadataError, match="exact_artifact_not_found"):
287+
allocate_v3_identity(
288+
report,
289+
inventory=inventory_for((legacy, report)),
290+
context=AllocationContext.exact_artifact_reuse(),
291+
requested_artifacts=REQUESTED,
292+
display_placement=DISPLAY,
293+
)
294+
295+
296+
def test_legacy_only_current_bootstraps_v3_canonical() -> None:
297+
report = build_report()
298+
legacy = binding_for(report, identity_class="LEGACY_V2")
299+
period_key = metadata(report)[0]
300+
301+
result = allocate_v3_identity(
302+
report,
303+
inventory=inventory_for((legacy, report)),
304+
context=AllocationContext.current_mandatory(period_key),
305+
requested_artifacts=REQUESTED,
306+
display_placement=DISPLAY,
307+
)
308+
309+
assert result.reused_existing is False
310+
assert result.binding.identity_class == "V3_CANONICAL"
311+
assert result.binding.canonical_identity is True
312+
313+
314+
def test_legacy_only_historical_recovery_requires_v3_canonical() -> None:
315+
report = build_report()
316+
legacy = binding_for(report, identity_class="LEGACY_V2")
317+
318+
with pytest.raises(IdentityMetadataError, match="canonical_bootstrap_required"):
319+
allocate_v3_identity(
320+
report,
321+
inventory=inventory_for((legacy, report)),
322+
context=AllocationContext.historical_recovery(),
323+
requested_artifacts=REQUESTED,
324+
display_placement=DISPLAY,
325+
)
326+
327+
328+
def test_mixed_legacy_and_v3_uses_v3_canonical_for_variant_allocation() -> None:
329+
legacy_report = build_report()
330+
current_report = dict(legacy_report)
331+
current_report["generated_at"] = "2026-07-15T00:00:00Z"
332+
candidate_report = dict(current_report)
333+
candidate_report["generated_at"] = "2026-07-16T00:00:00Z"
334+
legacy_canonical = binding_for(legacy_report, identity_class="LEGACY_V2")
335+
legacy = replace(
336+
legacy_canonical,
337+
canonical_identity=False,
338+
json_name=f"advisory_report_{legacy_report['as_of']}.variant-{metadata(legacy_report)[1]}.json",
339+
html_name=(
340+
f"{legacy_report['as_of']}-{legacy_report['cadence']}-model-recommendations"
341+
f".variant-{metadata(legacy_report)[1]}.html"
342+
),
343+
)
344+
canonical = binding_for(current_report)
345+
period_key = metadata(candidate_report)[0]
346+
347+
result = allocate_v3_identity(
348+
candidate_report,
349+
inventory=inventory_for((legacy, legacy_report), (canonical, current_report)),
350+
context=AllocationContext.current_mandatory(period_key),
351+
requested_artifacts=REQUESTED,
352+
display_placement=DISPLAY,
353+
)
354+
355+
assert result.binding.identity_class == "V3_VARIANT"
356+
assert result.reused_existing is False
357+
358+
279359
def test_semantic_same_artifact_different_is_not_reused() -> None:
280360
report = build_report()
281361
changed = dict(report)
@@ -284,7 +364,7 @@ def test_semantic_same_artifact_different_is_not_reused() -> None:
284364
period_key, semantic, artifact, _contract = metadata(changed)
285365

286366
assert semantic == metadata(report)[1]
287-
with pytest.raises(IdentityMetadataError, match="identity_reuse_not_found|identity_reuse_mismatch"):
367+
with pytest.raises(IdentityMetadataError, match="exact_artifact_not_found"):
288368
allocate_v3_identity(
289369
changed,
290370
inventory=inventory,

0 commit comments

Comments
 (0)