← Files CA Property Tax Appeal GuideARCHIVED FILE
skills/ca-property-tax-appeal/scripts/validate_case_state.py
3.84 KB · Oct 5, 2026 · 18:34 UTC
#!/usr/bin/env python3
"""Validate critical invariants in a California appeal case-state JSON file."""
import argparse
import json
from datetime import date
from pathlib import Path
REQUIRED = {"schema_version", "language", "stage", "property", "sources", "unresolved_facts", "deadlines", "documents", "filing"}
FILING_STATUSES = {"not_started", "draft", "submitted_user_reported", "submitted_verified", "rejected", "withdrawn"}
PROOF_STATUSES = {"none", "draft_only", "confirmation_unverified", "confirmation_verified"}
def is_iso_date(value):
if not isinstance(value, str):
return False
try:
date.fromisoformat(value)
return True
except ValueError:
return False
def validate(data):
errors, warnings = [], []
missing = sorted(REQUIRED - set(data))
if missing:
errors.append(f"Missing required top-level fields: {', '.join(missing)}")
if data.get("schema_version") != 1:
errors.append("schema_version must be 1.")
if not isinstance(data.get("stage"), int) or not 0 <= data.get("stage", -1) <= 8:
errors.append("stage must be an integer from 0 through 8.")
prop = data.get("property", {})
parcel_status = prop.get("parcel_status")
if parcel_status not in {"unknown", "single_verified", "ambiguous"}:
errors.append("property.parcel_status is invalid.")
if data.get("stage", 0) >= 3 and parcel_status != "single_verified":
errors.append("A single parcel must be verified before Stage 3 or later.")
for index, item in enumerate(data.get("deadlines", [])):
status = item.get("status")
if status == "verified":
if not is_iso_date(item.get("date")):
errors.append(f"deadlines[{index}] verified date must be YYYY-MM-DD.")
if not str(item.get("source_url", "")).startswith("https://"):
errors.append(f"deadlines[{index}] verified deadline needs an HTTPS source.")
if not is_iso_date(item.get("verified_on")):
errors.append(f"deadlines[{index}] verified_on must be YYYY-MM-DD.")
filing = data.get("filing", {})
submission = filing.get("submission_status")
proof = filing.get("proof_status")
if submission not in FILING_STATUSES:
errors.append("filing.submission_status is invalid.")
if proof not in PROOF_STATUSES:
errors.append("filing.proof_status is invalid.")
if submission == "submitted_verified" and proof != "confirmation_verified":
errors.append("submitted_verified requires confirmation_verified proof.")
if proof == "confirmation_verified":
if not filing.get("confirmation_number"):
errors.append("Verified filing proof requires a confirmation number or official case identifier.")
if not filing.get("submitted_at"):
errors.append("Verified filing proof requires submitted_at or received_at.")
if submission == "draft" and proof == "confirmation_verified":
errors.append("A draft cannot have verified filing proof.")
if data.get("unresolved_facts"):
warnings.append("Unresolved facts remain; disclose them before a conclusion or filing review.")
return {"valid": not errors, "errors": errors, "warnings": warnings}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("case_json", type=Path)
args = parser.parse_args()
try:
data = json.loads(args.case_json.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(json.dumps({"valid": False, "errors": [str(exc)], "warnings": []}, indent=2))
return 2
result = validate(data) if isinstance(data, dict) else {"valid": False, "errors": ["Case state must be an object."], "warnings": []}
print(json.dumps(result, indent=2))
return 0 if result["valid"] else 1
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 39f86faa7cce87de59d5b6b301b5eae8bd194691cd9a9bb1dccb1abf68133bca