"""Normalize source labels without introducing inferred target probabilities.""" import hashlib import json import math import re import unicodedata class InvalidRecord(ValueError): """A record cannot be used without changing its meaning.""" def unpack(value): if isinstance(value, str): try: return json.loads(value) except json.JSONDecodeError: pass return value def text(value) -> str: if isinstance(value, str): if not value.strip(): raise InvalidRecord("empty_text") return value return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def normalized(value: str) -> str: return re.sub(r"\s+", " ", unicodedata.normalize("NFKC", value)).strip().casefold() def digest(value) -> str: return hashlib.sha256( json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() ).hexdigest() def state_hash(state: str) -> str: parsed = unpack(state) return digest(parsed if not isinstance(parsed, str) else normalized(parsed)) def distribution(values, n: int) -> list[float]: try: p = [float(v) for v in values] except (TypeError, ValueError) as exc: raise InvalidRecord("invalid_probabilities") from exc if len(p) != n or any(not math.isfinite(v) or not 0 <= v <= 1 for v in p): raise InvalidRecord("invalid_probabilities") total = sum(p) if not math.isclose(total, 1.0, abs_tol=1e-5): raise InvalidRecord("probabilities_do_not_sum_to_one") return [v / total for v in p] def question(kind, instructions, criteria): if kind not in ("noul", "choice", "score"): raise InvalidRecord("unsupported_type") if not isinstance(instructions, str) or not instructions.strip(): raise InvalidRecord("empty_question") q = {"type": kind, "instructions": instructions} if kind == "noul": if criteria and not isinstance(criteria, dict): raise InvalidRecord("invalid_criteria") q["criteria"] = { "true": str((criteria or {}).get("true") or "yes"), "false": str((criteria or {}).get("false") or "no"), } keys = ["true", "false"] elif kind == "choice": if not isinstance(criteria, dict) or len(criteria) < 2: raise InvalidRecord("invalid_criteria") keys = [str(k) for k in criteria] q["criteria"] = {str(k): str(v) if v is not None else str(k) for k, v in criteria.items()} else: if not isinstance(criteria, list) or len(criteria) < 2: raise InvalidRecord("invalid_criteria") keys = [str(i) for i in range(len(criteria))] q["criteria"] = [str(v) for v in criteria] descriptions = ( list(q["criteria"].values()) if isinstance(q["criteria"], dict) else q["criteria"] ) if len({normalized(v) for v in descriptions}) != len(descriptions): raise InvalidRecord("duplicate_option_text") if len(keys) > 255 or any(not v.strip() for v in descriptions): raise InvalidRecord("invalid_options") return q, keys def canonical(state, q, keys, target, meta): state = text(state) original_sum = sum(float(x) for x in target) target = distribution(target, len(keys)) criteria = q["criteria"] labels = list(criteria.values()) if isinstance(criteria, dict) else criteria order = ( sorted(range(len(labels)), key=lambda i: normalized(labels[i])) if q["type"] == "choice" else list(range(len(labels))) ) fingerprint = digest( [ state_hash(state), normalized(q["instructions"]), q["type"], [normalized(labels[i]) for i in order], ] ) return { "id": digest([meta["repo"], meta["source_id"], meta["question_id"]]), "input": {"state": state, "questions": {"decision": q}}, "target": target, "option_keys": keys, "metadata": { **meta, "state_hash": state_hash(state), "fingerprint": fingerprint, "canonical_target": [target[i] for i in order], "schema_id": digest(q), "target_renormalized": original_sum != 1.0, }, } def adapt(row: dict, source: dict, split: str): """Yield independent decisions; gold and provenance never enter model inputs.""" repo = source["repo"] source_id = str(row.get("id", "")) if not source_id: raise InvalidRecord("missing_identity") base = { "repo": repo, "revision": source["revision"], "upstream_split": split, "source_id": source_id, "config": source["config"], } if source["adapter"] == "tasksource": kind = row["kind"] criteria = ( {str(i): o for i, o in enumerate(row["options"])} if kind == "choice" else row["options"] ) if kind == "noul": criteria = None q, keys = question(kind, row["question"], criteria) raw = row["target"] target = [float(raw[0]), 1 - float(raw[0])] if kind == "noul" and len(raw) == 1 else raw meta = { **base, "question_id": str(row.get("question_id", "decision")), "upstream_source": row["source"], "upstream_index": row.get("source_index"), "group_key": repo + ":" + str(row.get("group_id") or source_id), "license": row.get("license", "unspecified"), "license_use": row.get("license_use", "unspecified"), "variant": row.get("variant", "direct"), "label_origin": "source_annotation_or_transformation", } yield canonical(row["state"], q, keys, target, meta) return qs, gold = unpack(row["questions"]), unpack(row.get("answers", row.get("gold"))) if not isinstance(qs, dict) or not isinstance(gold, dict) or set(qs) != set(gold): raise InvalidRecord("question_target_mismatch") workflow = str(row.get("workflow") or row.get("task") or "unknown") procedural = "procedural" in repo group = workflow if "synthetic-typed" in repo else source_id for qid, original in qs.items(): q, keys = question(original["type"], original["instructions"], original.get("criteria")) answer = gold[qid] if q["type"] == "noul": p = float(answer["noul"]) target = [p, 1 - p] else: probs = answer["probabilities"] if set(map(str, probs)) != set(keys): raise InvalidRecord("option_target_mismatch") target = [probs[k] for k in keys] meta = { **base, "question_id": str(qid), "upstream_source": workflow, "group_key": repo + ":" + group, "license": "apache-2.0", "license_use": "commercial", "label_origin": "procedural_rule" if procedural else "teacher_distribution", "workflow": workflow, "domain": row.get("domain"), "application": row.get("application"), "item_source": row.get("source"), "skill": unpack(row.get("skills", {})).get(qid), } yield canonical(row["state"], q, keys, target, meta)