NEO-144: address review findings in contract CI gates
Use E7M3 bundle normalization for rep grants, smoke-test contract-seed schema, and apply cross-ref/band-cap checks to all loaded template rows.pull/184/head
parent
fe9b706092
commit
f7a014fe69
|
|
@ -20,6 +20,7 @@ Validates:
|
|||
- faction catalogs: content/factions/*_factions.json rows vs content/schemas/faction-def.schema.json (NEO-133)
|
||||
- contract template catalogs: content/contracts/*_contract_templates.json rows vs
|
||||
content/schemas/contract-template.schema.json (NEO-144); completionRewardBundle via quest-reward-bundle.schema.json
|
||||
- contract-seed schema smoke: content/schemas/contract-seed.schema.json minimal fixture parse (NEO-144)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -276,7 +277,8 @@ PROTOTYPE_E7M3_COMPLETION_BUNDLES: dict[str, dict] = {
|
|||
}
|
||||
|
||||
# Epic 7 Slice 4 prototype lock (NEO-144): one contract template + band-1 economy caps.
|
||||
# Keep in sync with E7.M4 freeze table and future NEO-145 server loader.
|
||||
# Keep in sync with E7.M4 freeze table, future NEO-145 server loader, and NEO-147
|
||||
# ContractEconomyValidation (port PROTOTYPE_E7M4_BAND_CAPS keys to C# in E7M4-07).
|
||||
PROTOTYPE_E7M4_TEMPLATE_IDS = frozenset({"prototype_contract_clear_combat_pocket"})
|
||||
PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
||||
"prototype_contract_clear_combat_pocket": {
|
||||
|
|
@ -291,6 +293,7 @@ PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
|||
"completionRewardBundle": {
|
||||
"itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}],
|
||||
"skillXpGrants": [{"skillId": "salvage", "amount": 15}],
|
||||
"reputationGrants": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -1064,6 +1067,22 @@ def _contract_template_validator() -> Draft202012Validator:
|
|||
return Draft202012Validator(template_schema, registry=registry)
|
||||
|
||||
|
||||
def _contract_seed_schema_smoke_gate() -> str | None:
|
||||
"""Return a human-readable error if contract-seed.schema.json fails a minimal fixture parse, else None."""
|
||||
seed_schema = json.loads(CONTRACT_SEED_SCHEMA.read_text(encoding="utf-8"))
|
||||
validator = Draft202012Validator(seed_schema)
|
||||
fixture = {
|
||||
"playerId": "prototype_player",
|
||||
"templateId": "prototype_contract_clear_combat_pocket",
|
||||
"seedBucket": "2026-06-20",
|
||||
}
|
||||
errors = sorted(validator.iter_errors(fixture), key=lambda err: err.path)
|
||||
if errors:
|
||||
loc = ".".join(str(part) for part in errors[0].path) or "(root)"
|
||||
return f"error: contract-seed schema smoke fixture {loc}: {errors[0].message}"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_faction_catalogs(
|
||||
*,
|
||||
faction_files: list[Path],
|
||||
|
|
@ -1649,8 +1668,8 @@ def _prototype_e7m4_template_freeze_gate(id_to_row: dict[str, dict]) -> str | No
|
|||
bundle = row.get("completionRewardBundle")
|
||||
if not isinstance(bundle, dict):
|
||||
return f"error: contract template {tid!r} must include completionRewardBundle object"
|
||||
actual_bundle = _normalize_completion_bundle(bundle)
|
||||
expected_bundle = _normalize_completion_bundle(expected["completionRewardBundle"])
|
||||
actual_bundle = _normalize_e7m3_completion_bundle(bundle)
|
||||
expected_bundle = _normalize_e7m3_completion_bundle(expected["completionRewardBundle"])
|
||||
if actual_bundle != expected_bundle:
|
||||
return (
|
||||
f"error: contract template {tid!r} completionRewardBundle must match E7M4 freeze table "
|
||||
|
|
@ -1666,7 +1685,7 @@ def _prototype_e7m4_cross_ref_gate(
|
|||
skill_id_to_allowed_kinds: dict[str, list[str]],
|
||||
) -> str | None:
|
||||
"""Return a human-readable error if contract template refs fail cross-checks, else None."""
|
||||
for tid in sorted(PROTOTYPE_E7M4_TEMPLATE_IDS):
|
||||
for tid in sorted(id_to_row.keys()):
|
||||
row = id_to_row.get(tid)
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
|
|
@ -1733,7 +1752,7 @@ def _prototype_e7m4_cross_ref_gate(
|
|||
|
||||
def _prototype_e7m4_band_cap_gate(id_to_row: dict[str, dict]) -> str | None:
|
||||
"""Return a human-readable error if a template bundle exceeds band caps, else None."""
|
||||
for tid in sorted(PROTOTYPE_E7M4_TEMPLATE_IDS):
|
||||
for tid in sorted(id_to_row.keys()):
|
||||
row = id_to_row.get(tid)
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
|
|
@ -2590,6 +2609,11 @@ def main() -> int:
|
|||
print(f"content validation failed with {contract_errors} error(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
contract_seed_smoke_err = _contract_seed_schema_smoke_gate()
|
||||
if contract_seed_smoke_err:
|
||||
print(contract_seed_smoke_err, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
e7m4_template_roster_err = _prototype_e7m4_template_roster_gate(contract_seen_ids)
|
||||
if e7m4_template_roster_err:
|
||||
print(e7m4_template_roster_err, file=sys.stderr)
|
||||
|
|
|
|||
Loading…
Reference in New Issue