"""Minimal output-schema validation for AI generators. A spec is a dict: field name -> rule dict with keys: type: "str" | "list" min_len / max_len: for str, character bounds; for list, item-count bounds count: exact list length (e.g. exactly 3 DM hooks) item_min / item_max: character bounds for each list item (str items) validate() returns a list of human-readable problems (empty = valid). Deliberately dependency-free so every Space stays light. """ def validate(data, spec) -> list[str]: problems = [] if not isinstance(data, dict): return [f"expected a JSON object, got {type(data).__name__}"] for field, rule in spec.items(): if field not in data: problems.append(f"missing field '{field}'") continue value = data[field] ftype = rule.get("type", "str") if ftype == "str": if not isinstance(value, str): problems.append(f"'{field}' must be a string") continue v = value.strip() if rule.get("min_len") and len(v) < rule["min_len"]: problems.append(f"'{field}' too short (min {rule['min_len']} chars)") if rule.get("max_len") and len(v) > rule["max_len"]: problems.append(f"'{field}' too long (max {rule['max_len']} chars)") elif ftype == "list": if not isinstance(value, list): problems.append(f"'{field}' must be a list") continue if "count" in rule and len(value) != rule["count"]: problems.append(f"'{field}' must have exactly {rule['count']} items") if rule.get("min_len") and len(value) < rule["min_len"]: problems.append(f"'{field}' needs at least {rule['min_len']} items") if rule.get("max_len") and len(value) > rule["max_len"]: problems.append(f"'{field}' allows at most {rule['max_len']} items") for i, item in enumerate(value): if not isinstance(item, str): problems.append(f"'{field}[{i}]' must be a string") continue if rule.get("item_min") and len(item.strip()) < rule["item_min"]: problems.append(f"'{field}[{i}]' too short") if rule.get("item_max") and len(item.strip()) > rule["item_max"]: problems.append(f"'{field}[{i}]' too long") return problems