diff --git a/scripts/validate_content.py b/scripts/validate_content.py index f69ea29..1c2fba2 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -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)