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)
|
- 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
|
- 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
|
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
|
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.
|
# 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_IDS = frozenset({"prototype_contract_clear_combat_pocket"})
|
||||||
PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
||||||
"prototype_contract_clear_combat_pocket": {
|
"prototype_contract_clear_combat_pocket": {
|
||||||
|
|
@ -291,6 +293,7 @@ PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
||||||
"completionRewardBundle": {
|
"completionRewardBundle": {
|
||||||
"itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}],
|
"itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}],
|
||||||
"skillXpGrants": [{"skillId": "salvage", "amount": 15}],
|
"skillXpGrants": [{"skillId": "salvage", "amount": 15}],
|
||||||
|
"reputationGrants": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1064,6 +1067,22 @@ def _contract_template_validator() -> Draft202012Validator:
|
||||||
return Draft202012Validator(template_schema, registry=registry)
|
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(
|
def _validate_faction_catalogs(
|
||||||
*,
|
*,
|
||||||
faction_files: list[Path],
|
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")
|
bundle = row.get("completionRewardBundle")
|
||||||
if not isinstance(bundle, dict):
|
if not isinstance(bundle, dict):
|
||||||
return f"error: contract template {tid!r} must include completionRewardBundle object"
|
return f"error: contract template {tid!r} must include completionRewardBundle object"
|
||||||
actual_bundle = _normalize_completion_bundle(bundle)
|
actual_bundle = _normalize_e7m3_completion_bundle(bundle)
|
||||||
expected_bundle = _normalize_completion_bundle(expected["completionRewardBundle"])
|
expected_bundle = _normalize_e7m3_completion_bundle(expected["completionRewardBundle"])
|
||||||
if actual_bundle != expected_bundle:
|
if actual_bundle != expected_bundle:
|
||||||
return (
|
return (
|
||||||
f"error: contract template {tid!r} completionRewardBundle must match E7M4 freeze table "
|
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]],
|
skill_id_to_allowed_kinds: dict[str, list[str]],
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Return a human-readable error if contract template refs fail cross-checks, else 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)
|
row = id_to_row.get(tid)
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
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:
|
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."""
|
"""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)
|
row = id_to_row.get(tid)
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
continue
|
continue
|
||||||
|
|
@ -2590,6 +2609,11 @@ def main() -> int:
|
||||||
print(f"content validation failed with {contract_errors} error(s)", file=sys.stderr)
|
print(f"content validation failed with {contract_errors} error(s)", file=sys.stderr)
|
||||||
return 1
|
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)
|
e7m4_template_roster_err = _prototype_e7m4_template_roster_gate(contract_seen_ids)
|
||||||
if e7m4_template_roster_err:
|
if e7m4_template_roster_err:
|
||||||
print(e7m4_template_roster_err, file=sys.stderr)
|
print(e7m4_template_roster_err, file=sys.stderr)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue