feat: classify gear and weapon mods
Validate and deploy production / validate (push) Successful in 24s
Validate and deploy staging / validate (push) Successful in 44s
Validate and deploy production / deploy (push) Failing after 29s
Validate and deploy staging / deploy (push) Failing after 6s

This commit is contained in:
jb
2026-07-31 21:54:47 -04:00
parent 1bd9facd3f
commit f48cc878aa
21 changed files with 595 additions and 39 deletions
@@ -17,7 +17,12 @@ from game_data_capture.domain.state_machines import (
InvalidStateTransition,
require_transition,
)
from game_data_capture.domain.taxonomy import RECORD_TYPES_BY_VIEW, SCREEN_VIEWS, Family
from game_data_capture.domain.taxonomy import (
RECORD_TYPES_BY_VIEW,
SCREEN_VIEWS,
Family,
RecordType,
)
from game_data_capture.domain.validation import (
ContractValidationError,
validate_archive_document,
@@ -28,6 +33,10 @@ from game_data_capture.domain.validation import (
ROOT = Path(__file__).resolve().parents[2]
CONTRACTS = ROOT / "contracts"
CLASSIFICATION_EXAMPLES = CONTRACTS / "examples" / "classification"
MOD_FAMILY_CONTRACT = {
Family.WEAPON_MOD: RecordType.WEAPON_MOD,
Family.GEAR_MOD: RecordType.GEAR_MOD,
}
def load_json(path: Path) -> dict[str, Any]:
@@ -82,6 +91,31 @@ def test_domain_contract_and_python_taxonomy_cover_every_family_view() -> None:
assert families[family.value]["views"][view] == expected
def test_mod_families_have_exact_frozen_detail_semantics() -> None:
contract = load_json(CONTRACTS / "domain-contract-v1.json")
assert contract["contract_version"] == 1
assert set(MOD_FAMILY_CONTRACT) == {Family.WEAPON_MOD, Family.GEAR_MOD}
for family, record_type in MOD_FAMILY_CONTRACT.items():
assert SCREEN_VIEWS[family] == ("detail",)
assert RECORD_TYPES_BY_VIEW[(family, "detail")] == (record_type,)
assert contract["families"][family.value] == {
"gear_slot": False,
"views": {"detail": [record_type.value]},
}
def test_approved_taxonomy_change_remains_contract_version_one() -> None:
archive_schema = load_json(CONTRACTS / "evidence-archive-manifest-v1.schema.json")
handoff_schema = load_json(CONTRACTS / "isac-handoff-v1.schema.json")
classification_schema = load_json(CONTRACTS / "screenshot-classification.schema.json")
assert archive_schema["properties"]["schema_version"] == {"const": 1}
assert handoff_schema["properties"]["schema_version"] == {"const": 1}
assert handoff_schema["properties"]["source_archive"]["properties"]["schema_version"] == {
"const": 1
}
assert classification_schema["$id"].endswith("screenshot-classification-v1.json")
def test_domain_contract_and_python_state_machines_match() -> None:
contract_states = load_json(CONTRACTS / "domain-contract-v1.json")["states"]
assert set(contract_states) == set(STATE_MACHINES)
@@ -106,6 +140,79 @@ def test_each_family_has_a_valid_and_invalid_example(family: str) -> None:
validate_classification(invalid)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
def test_mod_family_examples_validate_through_schema_and_semantics(
family: Family, record_type: RecordType
) -> None:
validator = schema_validator("screenshot-classification.schema.json")
valid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json"
invalid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.invalid.json"
assert valid_path.is_file()
assert invalid_path.is_file()
valid = load_json(valid_path)
invalid = load_json(invalid_path)
assert valid == {
"family": family.value,
"view": "detail",
"gear_slot": None,
"record_types": [record_type.value],
"confidence": valid["confidence"],
"abstention_reason": None,
}
validator.validate(valid)
validate_classification(valid)
with pytest.raises(ValidationError):
validator.validate(invalid)
with pytest.raises(ContractValidationError):
validate_classification(invalid)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
def test_archive_and_handoff_schemas_accept_mod_record_types(
family: Family, record_type: RecordType
) -> None:
archive = load_json(CONTRACTS / "examples" / "archive" / "manifest.valid.json")
archive["screens"][0]["classification"] = {
"family": family.value,
"view": "detail",
"gear_slot": None,
"record_types": [record_type.value],
"confidence": 0.96,
"abstention_reason": None,
}
archive["observed_instances"][0]["family"] = family.value
archive["canonical_candidates"][0]["record_type"] = record_type.value
schema_validator("evidence-archive-manifest-v1.schema.json").validate(archive)
validate_archive_document(archive)
handoff = load_json(CONTRACTS / "examples" / "handoff" / "isac-handoff.valid.json")
handoff["records"][0]["record_type"] = record_type.value
schema_validator("isac-handoff-v1.schema.json").validate(handoff)
validate_handoff_document(handoff)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
@pytest.mark.parametrize(
("field", "invalid_value"),
[
("view", "quality_profile"),
("record_types", ["weapon"]),
("record_types", ["weapon_mod_effect"]),
("gear_slot", "Mask"),
],
)
def test_mod_classifications_fail_closed_on_unsupported_values(
family: Family, record_type: RecordType, field: str, invalid_value: object
) -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json")
assert classification["record_types"] == [record_type.value]
classification[field] = invalid_value
with pytest.raises(ValidationError):
schema_validator("screenshot-classification.schema.json").validate(classification)
with pytest.raises(ContractValidationError):
validate_classification(classification)
def test_composite_classification_preserves_multiple_record_types() -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
validate_classification(classification)
@@ -121,6 +228,15 @@ def test_unknown_enum_value_is_rejected() -> None:
validate_classification(classification)
def test_semantic_validator_enforces_the_bounded_record_type_collection() -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
classification["record_types"] = [f"future_record_{index}" for index in range(9)]
with pytest.raises(ValidationError):
schema_validator("screenshot-classification.schema.json").validate(classification)
with pytest.raises(ContractValidationError, match="more than 8"):
validate_classification(classification)
@pytest.mark.parametrize(
("schema_name", "valid_fixture", "invalid_fixture"),
[